WebGoat代码审计-04-身份认证缺陷(上)
Drunkbaby Lv6

WebGoat代码审计-04-身份认证缺陷(上)

WebGoat代码审计-04-身份认证缺陷(上)

0x01 写在前面

  • 换了一种思维,先看代码,从代码里面找漏洞,然后去做题,代码审计也应该是这样一个思路。

0x02 Authentication Bypasses

1. Authentication Bypasses PageLesson2 2FA 验证方式

讲的是 2FA 的密码重置方式

源码部分

打开源代码,优先去找 return success 的代码模块

说实话这一块看不懂,关键点应该是这一块代码,很明显是继承

1
if (verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap) submittedAnswers))

而在第 60 行这里

1
AccountVerificationHelper verificationHelper = new AccountVerificationHelper();

进入 AccoutVerificationHelper 看看,并移步到 AccoutVerificationHelper.verifyAccout 下。

return true 的条件:

  • ①:UserId 相同
  • ②:第一个密保问题,即 secQuestion0 和上文中的 “作弊” 答案不一致
  • ③:第二个密保问题,即 secQuestion1 和上文中的 “作弊” 答案不一致

上框为 “作弊” 答案,下框的意思是,密码问题的参数是 “secQuestion0” 与 “secQuestion1”。只需包含这个参数即可进行判断。

靶场部分

于是想到绕过手段,构造 payload 成功绕过 ~

0x03 JWT Tokens

1. JWT Tokens PageLesson3 简单理解 JWT

  • 打开源码,去找 JWT 那一块,代码很简单,简单判断了 “$user” 是否等于 user

对 JWT 进行 base64 编码解密,轻松过

2. JWT Tokens PageLesson5 替换 payload 绕过

  • 题意,通过 JWT Token 的问题,获得 admin 权限,从而修改这些 Vote 的内容/样子。
  • 如果只是普通的 Guest 或者其他人的用户,是无法删除或者对投票界面进行更改操作的。

源码部分

  • 打开文件 “JWTVotesEndPoint.java”,首先第一个函数,构造 Vote 界面

这里对应的就是四个投票界面,在前端界面如图所示

这里有一段语句要单独拿出来讲一下,因为和 JWT 的代码原理密不可分。

1
2
3
4
String token = Jwts.builder()   // 创建 JWT 对象
.setClaims(claims) // 设置主题(声明信息)
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD) // 设置安全密钥(生成签名所需的密钥和算法)
.compact(); // 生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)

寻找一下何处可以绕过 admin 验证的,第 163 - 184 行

逐行分析,代码审计

前面的 166 - 168 行这里,判断 “accessToken” 是否为空,若为空,则返回 failed;若不为空,则进入 else 语句

第 169 - 178 行,核心的判断语句。

170 行的语句,验证 token

1
2
3
4
Jwt jwt = Jwt.paraser()     // 创建解析对象
.setSigningKey(JWT_PASSWORD)// 设置安全密钥(生成签名所需的密钥和算法)
.parse(accessToken) // 解析token
Claims claims = (Claims) jwt.getBody(); // 获取 payload 部分内容

第 172 行解读一下,中间是空格就可以了; claims.get("admin") 这句语句得到的是 JWT 的 Payload 信息,数据类型是 String,通过 Boolean.valueOf(String),将其转变为 Boolean 的数据类型;判断依据则是 Payload 的值是否等于 admin;若等于 admin,则为 True,反之为 False

1
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
  • 代码解读到这里,师傅们应该也能看懂原理了吧,我们加快分析进度~

第 173 - 178 行,判断 isAdmin 是否为 admin,若为 admin 时,将 vote 的值还原,并且返回 success 的消息。

1
2
votes.values().forEach(vote -> vote.reset());  // 四个 vote 的值还原到最开始
return success(this).build();

靶场部分

  • 根据上面的代码审计分析,其实我们只需要抓一个 JWT 的包,并且将 JWT 当中的 Payload 修改为 “Admin”,再发包即可。

点击删除时抓包,如图所示

将这一串 token 拿出来,base64 解码一下。

那这里,我们发包的时候将 "admin":"false" 修改为 "admin":"true" 即可,再发包。

这里推荐一个 JWT 在线生成的工具,不要想着用 base64 去弄,我这里踩坑花了大概半个小时才解决,不信邪的小伙伴们可以试一试。
工具网站:JSON Web Tokens - jwt.io

JWT token 是

eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOiIxNjQ5NzUwMzg1IiwiYWRtaW4iOiJ0cnVlIiwidXNlciI6IlRvbSJ9.

3. JWT Tokens PageLesson7 parseClaimsJws 与 parse

这里虽然是两道选择题,但是还是有必要提一嘴

放进 Burpsuite 当中比较一下两段代码

  • 对于 parseClaimsJws 来说,alg 为 none 则直接抛出异常
  • 对于 parse 来说,alg 为 none 则是判断是否为某个身份的依据。这里的 alg 如果被设置成 none,可以很好的绕过

综上,parseCliamsJws 的防御能力能强
答案是 1,3

4. JWT Tokens PageLesson8 Secret Key

  • 题意:让我们通过爆破的手段找出 JWT 当中的 Secret Key

一旦拥有了一个JWT token,我们可以尝试离线暴力破解或字典攻击。

源码部分

代码审计

查看源码,第 85 行,返回成功的条件

1
if (WEBGOAT_USER.equalsIgnoreCase(user))

再返回去看上面的判断条件

最后爆得 “Secret Key” 的值为 shipping(做题时每个人是不一样的),也可以使用 hashcat

5. JWT Tokens PageLesson10 Refresh a token

题目是 Refreshing a token,那总归是要先探究为什么要 Refreshing a token。

token类型分为两种:access token 和 refreshing token

为什么需要refreshing token?

  • 为了避免多次验证access token(超时或是浪费资源)

  • refreshing token 由server生成并存储在server的数据库里,验证时对比即可。

个人感觉就是 session cookie 和 set-cookie 差不多

题目要求我们,让 Tom 付钱

源码部分

源码部分如图所示,直接看如何才能成功的部分

第 106 行,这里比对了 Tom 是否为 user。但是一番抓包之后一无所获,甚至连 Tom 是谁,token 是啥全然不知。

回到题目界面,查看一下 logs.txt,发现了 Tom 的 token,丢到 jwt.io 中去验证一下。

应该是没这么简单的,回到源代码中看一看其他的接口

这里是关键点了,之前尝试了使用 logs.txt 中的 JWT 登录,最终失败了,个人的一点猜测是这样的:之前 Tom 购买时是很早之前的记录了,所以对于我们来说,这时候的 JWT 其实已经是失效了的,但如果我们需要让 Tom 来付款,必须要临时给 Tom 创建一个新的 JWT,或者把 Tom 的 access_token 找出来。

逐行代码审计又来了

第 137 - 146 行,当 user 与 refreshToken 都存在,不为空的时候,JWT 成功被 Refresh。

1
2
3
4
5
6
7
8
if (user == null || refreshToken == null) {  
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
} else if (validRefreshTokens.contains(refreshToken)) {
validRefreshTokens.remove(refreshToken);
return ok(createNewTokens(user));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

第 128 - 131 行,新构造出一个 JWT

1
2
3
4
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");

这里值得一提得是,最后解析 token 的参数,如果参数中存在 “Bearer” 就被替换为空格,这里感觉是有个双写绕过(?),不清楚,一会儿试了就知道了。

再明确一下攻击的方式,通过构造 JWT 中的 Header 部分,也就是 Header = Authorization,把这一块放进 HTTP Request 当中。代码如下

解题(巨坑,醉了)

首先是在 WebGoat 的界面下抓包,然后会抓到 /WebGoat/JWT/refresh/login 的这么一个包,接着添加 Authorization 的头,值为 Bearer null,目的是获取一个 access_token,再发包。

  • 发包完之后,注意!!!!!!!!这里巨坑!!!!!

这个 access_token 里面的最后一个字段是签名,不要拿进来!不要拿进来!不要拿进来!不要拿进来!

在 JWTio 中编辑完毕后,去到 /checkout 接口,发送前两个字段。

6. JWT Tokens PageLesson11 final

  • 题意: Jerry 想从 Twitter 上删除 Tom 的账号,算是越权吧。

源码部分

核心部分在 89- 103 行这里。

  • 这里我把源代码贴出来再分析分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {  
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
final String kid = (String) header.get("kid");
try (var connection = dataSource.getConnection()) {
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;
}
}).parseClaimsJws(token);

前面的代码都是之前差不多,新生成一个 JWT,引人注目的是第 94 行这里的 SQL 语句,感觉是存在 SQL 注入的。再好好审一审代码

这次的 secret 是直接从数据库进行读取,而这个 SQL 语句查询的就是 JWT 的 secret。而因为之前使用了 parseClaimsJws 这个方法,于是无法构造 {"alg":"none"} 来绕过。因此确定思路通过 SQL 注入来绕过。

题目部分

先点击 Delete 抓包,抓包的接口是 /JWT/final/delete?token=

我们看到 JWT header 当中多了一个 “kid”

修改包,将 “username” 修改为 Tom,再对 kid 进行 SQL 注入。这里使用 Union 联合查询注入。

  • 构造 payload,这里的 bmV3X2tleQ== 需要经过 base64 编码,因为 rs.next() 会执行一次 base64 的编码,所以我们要去 SELECT 的 secret key 需要先经过 base64 编码。
1
"kid": "something_else' UNION SELECT 'bmV3X2tleQ==' FROM INFORMATION_SCHEMA.SYSTEM_USERS; --",

修改 kid 进行 SQL 注入,并修改 iat 以及其他需要修改的数据,如上图所示。并修改下面的 secret key

  • 不要点 secret base64 encoded !不要点 secret base64 encoded !不要点 secret base64 encoded !不要点 secret base64 encoded !不要点 secret base64 encoded !不要点 secret base64 encoded !
  • 这里踩坑卡了好久 …………

接着,发包即可。

 评论