因23暑期实训针对crAPI靶场实验中JWT部分未成功,所以又重新使用PortSwigger上的Labs重新练习。
英文部分从PortSwigger上复制。
详细内容请访问:https://portswigger.net/web-security/jwt
JSON web tokens (JWTs) are a standardized format for sending cryptographically signed JSON data between systems. They can theoretically contain any kind of data, but are most commonly used to send information (“claims”) about users as part of authentication, session handling, and access control mechanisms.
Unlike with classic session tokens, all of the data that a server needs is stored client-side within the JWT itself. This makes JWTs a popular choice for highly distributed websites where users need to interact seamlessly with multiple back-end servers.
A JWT consists of 3 parts: a header, a payload, and a signature. These are each separated by a dot, as shown in the following example:
1 | eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA |
The header and payload parts of a JWT are just base64url-encoded JSON objects. The header contains metadata about the token itself, while the payload contains the actual “claims” about the user. For example, you can decode the payload from the token above to reveal the following claims:
1 | { |
In most cases, this data can be easily read or modified by anyone with access to the token. Therefore, the security of any JWT-based mechanism is heavily reliant on the cryptographic signature.
The server that issues the token typically generates the signature by hashing the header and payload. In some cases, they also encrypt the resulting hash. Either way, this process involves a secret signing key. This mechanism provides a way for servers to verify that none of the data within the token has been tampered with since it was issued:
JWT libraries typically provide one method for verifying tokens and another that just decodes them. For example, the Node.js library jsonwebtoken
has verify()
and decode()
.
Occasionally, developers confuse these two methods and only pass incoming tokens to the decode()
method. This effectively means that the application doesn’t verify the signature at all.
即程序员将verify()
和decode()
弄混了,导致网站后台并没有检验签名有效性。
Lab1: JWT authentication bypass via unverified signature
首先,用已有的普通用户的账号密码登录,然后使用Burp Suite抓包可以知道Cookie中的Session存放着JWT。
接着使用base64解码之后可以得到header和payload部分的内容:
由于Lab 1并没有验证signature部分,所以我们直接将sub
的值改为admin,得到新的JWT:
通过Burp Suite将Session的值替换,即可以达到以admin身份登录的效果。但是无法访问/admin目录,再将sub改为administrator即可,然后执行删除carlos的操作即可完成实验。
可以直接登录操作,也可以通过访问接口删除:
1 | /admin/delete?username=carlos |
Among other things, the JWT header contains an alg
parameter. This tells the server which algorithm was used to sign the token and, therefore, which algorithm it needs to use when verifying the signature.
1 | { |
This is inherently flawed because the server has no option but to implicitly trust user-controllable input from the token which, at this point, hasn’t been verified at all. In other words, an attacker can directly influence how the server checks whether the token is trustworthy.
JWTs can be signed using a range of different algorithms, but can also be left unsigned. In this case, the alg
parameter is set to none
, which indicates a so-called “unsecured JWT”. Due to the obvious dangers of this, servers usually reject tokens with no signature. However, as this kind of filtering relies on string parsing, you can sometimes bypass these filters using classic obfuscation techniques, such as mixed capitalization and unexpected encodings.
即服务器会根据alg
的值来确定验证的算法,如果alg
为none,则服务器就不会对signature部分进行检测,由于这样的错误比较明显,服务器后台程序员一般不会允许alg
为none(通过字符串过滤等技术),攻击者可以选择方法(类似文件上传漏洞的绕过)进行绕过。
注:即使不验证signature,但是最后还是要有.
表示分割。
Lab2: JWT authentication bypass via flawed signature verification
同Lab 1一样,利用已知的账号密码登录获得Session中存的JWT,再对JWT进行修改即可。
测试后发现,signature部分要为空才能成功登录/admin页面。登陆后删除carlos用户完成实验。
Some signing algorithms, such as HS256 (HMAC + SHA-256), use an arbitrary, standalone string as the secret key. Just like a password, it’s crucial that this secret can’t be easily guessed or brute-forced by an attacker. Otherwise, they may be able to create JWTs with any header and payload values they like, then use the key to re-sign the token with a valid signature.
When implementing JWT applications, developers sometimes make mistakes like forgetting to change default or placeholder secrets. They may even copy and paste code snippets they find online, then forget to change a hardcoded secret that’s provided as an example. In this case, it can be trivial for an attacker to brute-force a server’s secret using a wordlist of well-known secrets.
Brute-forcing secret keys using hashcat
You just need a valid, signed JWT from the target server and a wordlist of well-known secrets. You can then run the following command, passing in the JWT and wordlist as arguments:
1 | hashcat -a 0 -m 16500 <jwt> <wordlist> |
Hashcat signs the header and payload from the JWT using each secret in the wordlist, then compares the resulting signature with the original one from the server. If any of the signatures match, hashcat outputs the identified secret in the following format, along with various other details:
1 | <jwt>:<identified-secret> |
即有的网站的后台的JWT验证程序使用了弱密钥,或者有些程序员会直接粘贴网上的线程的程序并且没有改密码,这样就可以被暴力破解,然后这里暴力破解用到的工具是hashcat。
Lab3: JWT authentication bypass via weak signing key
首先先获得已知用户wiener的JWT,利用Wordlist中的所有密钥遍历,直到找出符合的那一个。
这里我们使用了hashcat工具,在hashact的目录下执行以下命令:
1 | hashcat -a 0 -m 16500 <jwt> <wordlist> |
-a 指 –attack-mode,0表示攻击模式中的Straight;-m 指 –hash-type,16500表示JWT。
执行结束后可以看到执行加密的密钥为secret1
然后再更改JWT的header和payload,在用密钥对其签名即可。
最后在Burp Suite中修改Session中的JWT,以管理员身份访问到/admin目录,并删除用户carlos。
According to the JWS specification, only the alg
header parameter is mandatory. In practice, however, JWT headers (also known as JOSE headers) often contain several other parameters. The following ones are of particular interest to attackers.
jwk
(JSON Web Key) - Provides an embedded JSON object representing the key.jku
(JSON Web Key Set URL) - Provides a URL from which servers can fetch a set of keys containing the correct key.kid
(Key ID) - Provides an ID that servers can use to identify the correct key in cases where there are multiple keys to choose from. Depending on the format of the key, this may have a matching kid
parameter.As you can see, these user-controllable parameters each tell the recipient server which key to use when verifying the signature. In this section, you’ll learn how to exploit these to inject modified JWTs signed using your own arbitrary key rather than the server’s secret.
The JSON Web Signature (JWS) specification describes an optional jwk
header parameter, which servers can use to embed their public key directly within the token itself in JWK format.
A JWK (JSON Web Key) is a standardized format for representing keys as a JSON object.
You can see an example of this in the following JWT header:
1 | { |
Ideally, servers should only use a limited whitelist of public keys to verify JWT signatures. However, misconfigured servers sometimes use any key that’s embedded in the jwk
parameter.
You can exploit this behavior by signing a modified JWT using your own RSA private key, then embedding the matching public key in the jwk
header.
即我们可以在JWT的头部插入jwk
,让服务器利用我们给密钥来进行验证。这里我们可以自己创建一对RSA公私钥,先将header的jwk
部分填充我们自己的公钥,然后用自己的私钥对header和payload进行签名,生成signature,这样完成了JWT的创建。
我们将此JWT发送至服务器后,服务器会根据头部的jwk对signature部分进行验证,由于这是一对公私钥,所以可以被验证成功。
Lab4: JWT authentication bypass via jwk header injection
首先还是登录已知的账号,获得wiener(一直账号的名称)的JWT,然后在此基础上进行修改。
我们利用Burp Suite的JWT插件自动生成RSA公私钥对:
接着将payload的sub
改为administrator,再点击Attack,使用Embedded JWT功能,Burp Suite会自动将刚才创建的公钥作为jwk添加到header中,将header中的kid和jwk中的kid改为一致,并且用私钥对header和payload进行加密生成signature。
至此就完成了对JWT的修改,然后直接利用该JWT登录管理员界面,删除carlos用户即可完成实验。
Instead of embedding public keys directly using the jwk
header parameter, some servers let you use the jku
(JWK Set URL) header parameter to reference a JWK Set containing the key. When verifying the signature, the server fetches the relevant key from this URL.
A JWK Set is a JSON object containing an array of JWKs representing different keys. You can see an example of this below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 {
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}
JWK Sets like this are sometimes exposed publicly via a standard endpoint, such as /.well-known/jwks.json
.
More secure websites will only fetch keys from trusted domains, but you can sometimes take advantage of URL parsing discrepancies to bypass this kind of filtering.
Lab5: JWT authentication bypass via jku header injection
意思是,我们可以在header中插入jku
,jku是一个URL,定位一个JWK Set,该Set中存有密钥,服务器会根据此URL访问Set,并从Set中选取密钥进行验证(应该是根据kid
选取)。
注:JWK指的是公钥,所以可以被公开,用户也可以获取,可以在网站的如下路劲获取到:/.well-known/jwks.json。服务器先用私钥对header+payload签名,然后签发给用户,当用户将JWT发送至服务器时,服务器用公钥进行验证。
我们可以通过插入jku
来达到Lab 4中插入JWT的效果。
具体步骤与Lab 4差不多,先将自己生成的公钥以JWT的形式复制下来,并保存至json文件,再使用某个url定位此json文件,然后将url插入header中的jku
,修改sub
为administrator,最后再使用自己的私钥对header和payload签名即可。
关键步骤如下:
将公钥以JWK的形式上传至服务器。
修改header(加上jku
)、payload(修改sub
),最后用对应的私钥签名即可完成JWT的伪造。最后发送数据包访问/admin即可。
成功访问到/admin页面,将carlos删除即可完成实验。
附:但是在做的时候出现了一个问题,刚开始我把JWK部署在Github的静态个人博客上,但是这样一直没有成功,返回的数据都是401 Unauthorized。
但是使用Port Swigger自带的服务器就可以….
Github返回的返回内容:
Port Swigger自带服务器返回的内容:
他们的Response内容都一样…..但是用Github的url就会失败,可能是因为Port Swigger下载下来的是html文件,而且内容为:
而我的github部署的是json文件。
Servers may use several cryptographic keys for signing different kinds of data, not just JWTs. For this reason, the header of a JWT may contain a kid
(Key ID) parameter, which helps the server identify which key to use when verifying the signature.
Verification keys are often stored as a JWK Set. In this case, the server may simply look for the JWK with the same kid
as the token. However, the JWS specification doesn’t define a concrete structure for this ID - it’s just an arbitrary string of the developer’s choosing. For example, they might use the kid
parameter to point to a particular entry in a database, or even the name of a file.
If this parameter is also vulnerable to directory traversal, an attacker could potentially force the server to use an arbitrary file from its filesystem as the verification key.
1 | { |
This is especially dangerous if the server also supports JWTs signed using a symmetric algorithm. In this case, an attacker could potentially point the kid
parameter to a predictable, static file, then sign the JWT using a secret that matches the contents of this file.
You could theoretically do this with any file, but one of the simplest methods is to use /dev/null
, which is present on most Linux systems. As this is an empty file, reading it returns an empty string. Therefore, signing the token with a empty string will result in a valid signature.
这段文字的大致的意思是:
服务器上可能有很多的key用于加密、解密或者验证。当服务器收到一个JWT时,它会根据kid
的值去找key。
在JWT的命名规范中,并没有对kid
的特殊规定,它仅仅只是字符串string。程序员可以随意设定kid
,有时他们可以将kid
设为数据在数据库中或者文件系统中的位置。
如果存在文件遍历漏洞,则攻击者可以指定服务器上的文件作为验证的密钥,这样就可控制服务器使用的密钥了,进而可以伪造JWT。
这里的文件遍历漏洞,在我的理解下就是:服务器默认kid是存的数据(key)的路径,将kid作为路径去寻找密钥,而不是在JWT Set中比对。
若存在文件遍历漏洞,同时该服务器使用的验证算法为对称加密,那么就非常危险,理论上来说,可以使服务器上将任何一个文件(前提是知道它的路径)的内容作为验证的密钥。但最简单的方法是使用dev/null
,它存在于大多数 Linux 系统上。由于这是一个空文件,读取它会返回一个空字符串。因此,使用空字符串对令牌进行签名将得到有效的签名。
Lab6: JWT authentication bypass via kid header path traversal
通过抓包发现服务器的验证程序使用的是对称密码算法HS256。于是我们尝试对kid
进行修改,让服务器用空字符串进行验证,然后我们也用空字符串签名,这样就可以达到伪造JWT的效果了。
在linux系统中,/
(根目录):整个文件系统的起始点,所有其他目录都是在根目录下的子目录,dev文件夹就在根目录下。
显然,kid是相对路径,如果直接将kid
设为/dev/null
,服务器会从当前目录下寻找,当然会失败。这时就必须先一步一步回退上一级目录,使用../
,回退到根目录(即使回退到了根目录,再退也还是根目录),从才能正确进入dev文件夹读取null。经过我的测试,回退三次即可成功读取到null。
服务器实际上从 /dev/null
读取的字符串就是 00
的十六进制表示。这是因为 /dev/null
是一个特殊设备,读取时返回的是一个空字符,其 ASCII 码值为 0(一个ASCII码字符对应一个字节),对应的十六进制表示就是 00
。那么服务器就会将十六进制的00
作为对称密钥对签名进行验证。
然后在Burp Suite中新生成一个对称密钥,生成后将”k”改为”AA==”:
为什么K要改为AA==
?
因为JWT的key需要进过base64编码,而Base64 编码是将二进制数据按照每 3 个字节为一组进行处理,并转换为四个可打印字符组成的字符串。具体过程如下:
如果数据的长度不是 3 的倍数,则需要进行填充操作。填充通常使用 =
字符,填充的规则如下:
=
字符,以凑齐两个 6 位组。=
字符,以凑齐三个 6 位组。对于十六进制的00
来说,它被看作是一个具有 8 位的二进制数 00000000
,先将 00000000
拆分为两个 6 位的值:000000
和 00
;然后将这两个 6 位的值映射到 Base64 字符表中:000000
对应字符 A
,00
对应字符 A
;最后,添加 Base64 的 padding 字符 =
,得到的编码结果是:AA==
。
然后再使用修改过后的对称密钥对伪造的header和payload进行加密:
至此就完成了JWT的伪造,现在可以成功以administrator的身份登录/admin界面了。
最后删除carlos用户完成实验。
The following header parameters may also be interesting for attackers:
cty
(Content Type) - Sometimes used to declare a media type for the content in the JWT payload. This is usually omitted from the header, but the underlying parsing library may support it anyway. If you have found a way to bypass signature verification, you can try injecting a cty
header to change the content type to text/xml
or application/x-java-serialized-object
, which can potentially enable new vectors for XXE and deserialization attacks.x5c
(X.509 Certificate Chain) - Sometimes used to pass the X.509 public key certificate or certificate chain of the key used to digitally sign the JWT. This header parameter can be used to inject self-signed certificates, similar to the jwk
header injection attacks discussed above. Due to the complexity of the X.509 format and its extensions, parsing these certificates can also introduce vulnerabilities. Details of these attacks are beyond the scope of these materials, but for more details, check out CVE-2017-2800 and CVE-2018-2633.Even if a server uses robust secrets that you are unable to brute-force, you may still be able to forge valid JWTs by signing the token using an algorithm that the developers haven’t anticipated. This is known as an algorithm confusion attack.
Algorithm confusion attacks (also known as key confusion attacks) occur when an attacker is able to force the server to verify the signature of a JSON web token (JWT) using a different algorithm than is intended by the website’s developers. If this case isn’t handled properly, this may enable attackers to forge valid JWTs containing arbitrary values without needing to know the server’s secret signing key.
How do algorithm confusion vulnerabilities arise?
Algorithm confusion vulnerabilities typically arise due to flawed implementation of JWT libraries. Although the actual verification process differs depending on the algorithm used, many libraries provide a single, algorithm-agnostic method for verifying signatures. These methods rely on the alg
parameter in the token’s header to determine the type of verification they should perform.
The following pseudo-code shows a simplified example of what the declaration for this generic verify()
method might look like in a JWT library:
1 | function verify(token, secretOrPublicKey){ |
Problems arise when website developers who subsequently use this method assume that it will exclusively handle JWTs signed using an asymmetric algorithm like RS256. Due to this flawed assumption, they may always pass a fixed public key to the method as follows:
1 | publicKey = <public-key-of-server>; |
In this case, if the server receives a token signed using a symmetric algorithm like HS256, the library’s generic verify()
method will treat the public key as an HMAC secret. This means that an attacker could sign the token using HS256 and the public key, and the server will use the same public key to verify the signature.
简单地说,算法混淆攻击的前提是:
如果攻击者将header部分的alg
改为对称加密验证算法,然后获取服务器验证签名的公钥,将此公钥作为对称加密算法的密钥对header和payload签名。
服务器收到攻击者篡改后的JWT,进入验证函数verify,根据alg
的值选择验证算法。因为alg
的值被攻击者篡改为对称加密算法,那么服务器就会使用对称加密算法来验证,但是仍然还是取自己的公钥进行验证(因为服务器的建设者起初用的就是非对称加密算法进行验证,使用固定的公钥验证签名,逻辑如上文的伪代码:先取自己的公钥,然后再取得token,最后将这两个值传入verify函数进行验证)。
这时,攻击者是使用服务器的公钥进行签名的,服务器还是使用公钥来验证,那么被篡改的JWT就会通过检测。
注:用于签名的对称加密算法的密钥必须与服务器上存储的公钥完全相同。这包括使用相同的格式(例如 X.509 PEM)并保留任何非打印字符(例如换行符)。
Lab 7: JWT authentication bypass via algorithm confusion
第一步:先获得服务器的公钥
登陆已知的账号之后,抓取数据包分析JWT发现该服务器使用的是RS256验证算法,即使用私钥签名,公钥验证。要成功伪造JWT,就要先获得服务器的公钥。
服务器有时会通过映射到 /jwks.json 或 /.well-known/jwks.json 的标准端点将其公钥公开为 JWK对象。即使公钥没有被公开,但是我们也可以从一对JWT中进行提取(Lab 8的内容)。
在本次实验中公钥映射到了/jwks.json
我们将JWK Set中的公钥复制到Burp Suite中,生成一个RSA公钥对象。
第二步:将公钥作为对称加密算法的密钥
然后我们将其转换为PEM格式,然后将公钥PEM格式的内容全部进行Base64编码。
再随便生成一个对称加密算法的密钥,然后将上面Base64编码的结果复制到刚生成的密钥的K参数上。
第三步:修改header,和payload
注意,kid不要改,kid本身就与服务器上的公钥的kid一致。
第四步:用对称加密算法密钥签名,并发送请求
点击Sign按钮,使用刚刚创建的对称加密算法的密钥,使用HS256算法进行签名,然后点击Send发送请求,即可进入/admin管理员页面,删除carlos用户完成实验。
但如果,服务器并没有把public key公开呢?
Lab 8: JWT authentication bypass via algorithm confusion with no exposed key
可以使用工具jwt_forger,他会使用我们提供的 JWT 来计算 n 的一个或多个潜在值。其中只有一个与服务器密钥使用的 n 值匹配,该程序就会将其保存为x509格式或者pkcs1格式的pem文件。这样就可以获得服务器的公钥,实施算法混淆攻击。
1 | jwt_forgery <token1> <token2> |
jwt_forger命令行实际运行效果:
1 | root@ecdfab3005f4:/app# python3 jwt_forgery.py eyJraWQiOiIzZjRhZjRiMS1mY2M1LTQwZjUtODM4My02NGRmZjI3NGE0NDUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY5MTE0MTQwMn0.Bl2KswJWpEeSRhAb03UiXq9-4a_Powo-bLTj1HD2nlEeu8uDGp2NhPeLRnMwq4hFtj8lW811nymxU3Q--Fig5hbuJhUh1wolTfir7CattvAmTRC-i88URUs-oIH6BvMYbbD0dNMhNt-9uaamJ413vh9GH6glojM1ph_Bg6awqLntoioyv_PV6VSA12x6NHKNMq-0bs2GAxOCBoHvFFuq5hBU5HYfCg8ExdXbewwqJjLRqnkwyjJ6lKf24iVbe7uiYrSH1wUQZl2_Q7Fu0ly7houPCBIkuJbAs0-YrAIvjKfNPKPZszXFceZysFYdi4u0oA0o-lbTcwnpm_juoYWvhw eyJraWQiOiIzZjRhZjRiMS1mY2M1LTQwZjUtODM4My02NGRmZjI3NGE0NDUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY5MTE0MTY2Mn0.CPrJ7dQEg-rVuHNrtHdHJfiUkxwnT7UmY8B_NMqlS2RutXG9BGtkHy-pMc9QS7twcUnw9zv0Hfvf49bAcUb7wFgQ53SPB9r5OsuRw0BwPuxJVheoGCSbvdeW6tD8PivV5yqDEJOnkaYuxFYeaeIs85YxusgRj_OnKlWvhROICAE7kYe7RnLzrYX3_WiU-bIWT-WMEWcr1FwK2RF7DWb48DF0W-gETdl-n2AYiEsa2VbSHnt2vH4gvBVjNPxLr7IuTEYHr7H-S2YDN4-0p6TBcKnkoaJzTYSt1XWnsYbnyjq0kmzEDaToaA7cPGkNvWAmZzD_NBNm2mQry_S4VknKzA |
可以看出jwt_forger已经把公钥分为两个格式存入文件了,分别使用两个形式的公钥进行尝试就可以了。最后使用x509格式的公钥成功完成了实验。