0%

JSON Web Token(JWT)攻击探究(二)

JWT 攻击面:

敏感信息泄露:

当服务端的秘钥泄密的时候,JWT的伪造就变得非常简单容易。对此,服务端应该妥善保管好私钥,以免被他人窃取。

header和payload部分实际上只是进行了base64编码

20200417143359


alg=None签名绕过:

签名算法确保恶意用户在传输过程中不会修改JWT。

但是标题中的alg字段可以更改为none。

一些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证。将alg更改为none后,从JWT中删除签名数据(仅标题+’.‘+ payload +’.’)并将其提交给服务器

例子:

http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

1
2
3
4
5
6
7
8
9
10
import base64

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


result = b64urlencode("{\"typ\":\"JWT\",\"alg\":\"none\"}") + \
'.' + b64urlencode("{\"data\":\"test\"}") + '.'

print(result)
1
2
# 输出
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.

20200417143748

server源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# https://github.com/Sjord/jwtdemo/blob/master/MishalHS256.php
<?php
require __DIR__ . '/vendor/autoload.php';

use Jwt\Jwt;
use Jwt\Algorithm\NoneAlgorithm;
use Jwt\Algorithm\HS256Algorithm;

class MishalHS256 {
function __construct() {
$this->algorithms = [
'none' => new NoneAlgorithm(),
'HS256' => new HS256Algorithm('secret'),
];
}

function encodeJwt($tokenObj) {
return Jwt::encode($tokenObj, $this->algorithms['HS256']);
}

function decodeJwt($token) {
return JWT::decode($token, ['algorithm' => array_values($this->algorithms)]); # header部分中指定加密方法为none方法,服务端也用none方法来解密。
}
}

加密方法是读取JWT的header部分吗?


将签名算法从非对称类型改为对称类型:

使用非对称加密算法(主要基于RSA、ECDSA,如S256)分发JWT的过程是使用私钥(private)加密生成JWT,使用公钥(public)解密验证。

使用对称加密算法(主要基于HMAC,如HS256)分发JWT的过程是使用同一个密钥(secret)生成和验证JWT。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FirebaseRS256 {
function __construct() {
$this->private_key = file_get_contents('private.pem');
$this->public_key = file_get_contents('public.pem');
}

function encodeJwt($tokenObj) {
return JWT::encode($tokenObj, $this->private_key, 'RS256'); // 私钥加密作为签名
}

function decodeJwt($token) {
// Explicitly configured to be vulnerable:
// we expect a RS256 signature, but also accept a HS256 signature.
return JWT::decode($token, $this->public_key, ['RS256', 'HS256']); // HS256对称加密方式 用公钥作为秘钥
}
}

一开始的流程是,服务端用私钥加密作为签名,然后用公钥进行验证。

由于我们很难知道私钥,所以我们想自己构造数据的话,没法用相同的私钥签名回去。

但解密时支持HS256这种对称加密方式的话,它只有一个秘钥,如图是公钥。

也就是说我们只要找到公钥,就能篡改数据后,再把数据签名回去,这样能通过延签的部分。


HS256(对称加密)密钥破解:

如果HS256密钥强度较弱,则可以直接强制使用,通过爆破 HS256的秘钥可以完成该操作。

不过对 JWT 的密钥爆破需要在一定的前提下进行:

  • 知悉JWT使用的加密算法

  • 一段有效的、已签名的token

  • 签名用的密钥不复杂(弱密钥)

所以其实JWT密钥爆破的局限性很大。

20200418113805

可以看到简单的字母数字组合都是可以爆破的,但是密钥位数稍微长一点或者更复杂一点的话,爆破时间就会需要很久。


修改KID参数:

kidjwt header中的一个可选参数,全称是key ID,它用于指定加密算法的密钥

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/home/jwt/.ssh/pem"
}

因为该参数可以由用户输入,所以也可能造成一些安全问题。

任意文件读取:

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

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

SQL注入:

kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "key11111111' || union select 'secretkey' -- "
}

命令注入

kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。

1
"/path/to/key_file|whoami"

对于其他的语言,例如php,如果代码中使用的是exec或者是system来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。


总结:

与许多其他方面一样,JWT基本上是安全的,但某些实现则不是。

该应用程序可能在JWT中存储敏感信息,允许更改签名算法,或者签名中使用的密钥强度不足、kid参数可控。


其他:

RSA的公钥和私钥区别

公钥和私钥是成对的,它们互相解密。

公钥加密,私钥解密。

私钥数字签名,公钥验证。

  • A 要给 B 发信息要用 B 的公钥加密,B 用自己的私钥解密。

  • A 发布了一篇文章,说这是我发的,我用自己的私钥签名了。由于之前 A 已经发布了他的公钥,所以其它人可以用这个公钥去验证。因为知道 A 私钥的只可能是 A 自己。

这样的话:加密是保护数据,签名是核对身份,解决的是不同需求。那么我的理解:不应该是需要解决什么问题就用什么吗?不希望第三方收到内容就加密,不希望第三方冒充发布就签名,或者同时使用。


refs: