L'apologie de ZFS selon OVH(Hack)

Tout commence par un simple site web : http://ovhack.com qui indique que le site a été compromis, et que du contenu offusqué se trouve caché quelque part, suis le lapin blanc.

En parcourant les autres pages, notament la page "win rewards", j'ai trouvé le fameux lapin dans les sources.

<!--
                /|      __ 
               / |   ,-~ / 
              Y :|  //  /   
              | jj /( .^ 
              >-"~"-v" 
             /       Y   
            jo  o    | 
           ( ~T~     j  
            >._-' _./  
           /   "~"  |   
   -->
<a href="http://www.ovh.com"><img src="img/ovhacked.png" alt="ovh"/></a>
<!--
          Y     _,  |     
         /| ;-"~ _  l   
        / l/ ,-"~    \ 
        \//\/      .- \ 
         Y        /    Y* 
         l       I     !
         ]\      _\    /"\
        (" ~----( ~   Y.  )  
~~~~~~~~~~~~~~~~~~~~~~~~~~   
-->

Ayant mal vu, c'est bête, mais j'ai commencé par faire un tour site le site d'OVH.
Il y avait une news sur l'intérêt que porte OVH à l'impression 3D (http://www.ovh.com/fr/a1084.impression-3d-test-ovh). Tient, il y a justement une imprimante 3D à gagner...

Affichons la source.
(Nota: il me *semble* qu'il n'y avait pas de lapin blanc dans les sources au moment où j'ai regardé, maintenant si)

On remarque la présence (invisible) d'éléments "ndhId":

<span ndhId="2k13#13" style="display:none">slash</span>
<span ndhId="2k13#10" style="display:none">dot</span>

Pas de doute, ce doit-être une URL. Et le numéro qui suit le dieze doit-être l'ordre pour reconstituer la phrase.

Un coup de python (et du sympatique module BeautifulSoup) plus loin, on obtient bien les urls.
(la première étant http://ovh.to/gfexdsai)

#!/usr/bin/env python

import BeautifulSoup, re, sys

def valof(e):
    if e.has_key('ndhid'):
        key = re.search('#([0-9]+)', e.get('ndhid')).group(1)
        return (key, e.text)
    else:
        return None

def replace_value(it):
    if it == "slash":
        return "/"
    elif it == "double_dot":
        return ":"
    elif it == "dot":
        return "."
    else:
        return it

file = ''

f1 = 'a1084.impression-3d-test-ovh'
f1 = 'a1036.ovh-derniere-innovation-ethylcooling'
f1 = 'a1037.ovh-nouveautes-telephonie-voip-ovh'
f1 = 'a1040.bilan-datacentre-ovh-usa-canada'
f1 = 'a1100.eoliennes-ovh-energie-renouvelable'
f1 = 'a895.hubic_une_premiere_annee_epique'
f1 = 'a923.cybersecurite_a_vous_de_placer_le_curseur'
f1 = 'a955.nouvelle_api_chacun_son_160ovhworld160'

if len(sys.argv) == 2:
    f1 = sys.argv[1]

with open(f1) as line:
    file += line.read()

soup = BeautifulSoup.BeautifulSoup(file)

span = soup.findAll('span')
span = filter(None, map(valof, span))
span.sort(lambda x,y : int(x[0])-int(y[0]))
span = map(lambda e : e[1], span)
span = map(replace_value, span)
print ''.join(span)

La répétition des variables f1 est liée au fait que je faisais les wget à la main ... puis j'ai ajouté l'argument pour automatiser tout ça.

En mode pas très propre, je me suis dit qu'il fallait boucler jusqu'à ce que ça échoue. Soit.

#!/usr/bin/env perl

my $start = 'a955.nouvelle_api_chacun_son_160ovhworld160';
my $file_id = 'file001';

while (1)
{
    my $url = `python extract.py $start`;
    chomp $url;
    print $url;
    `wget $url -O $file_id`;
    $start = $file_id;
    print $start;
    $file_id++;
}

À la fin, on obtient encore un lien de la forme "ovh.to" mais qui cette fois, pointe vers hubic (le service de stockage cloud d'ovh), qui nous permet de télécharger (grâce à un browser, pas réussi avec wget :-() une image compressée, et un README.

$ cat README
$ md5sum nntp-dwn-03.img.bin.xz
e00a9c53306edd165534f4fa4a761737  nntp-dwn-03.img.bin.xz

==

We found this illegitimate server hidden in the servers room
of our company. It seems it was here for some time, as suggests
the dust that covers it ! Hell, was some competitor spying on
us from the inside ?

The strange thing is that, the server was actually crashed when
we found it, and we couldn't get it to even boot. The hardware
was pretty shitty, so it had to be expected somehow.

Bob plugged the disk in another machine and captured it, can you
have a look ? I fear there might be frigthening stuff on it...

$ file nntp-dwn-03.img.bin
nntp-dwn-03.img.bin: ; partition 1: ID=0xee, starthead 0, startsector 1, 2097151 sectors
$ parted nntp-dwn-03.img.bin print
Model:  (file)
Disk /home/nntp-dwn-03.img.bin: 1074MB
Sector size (logical/physical): 512B/512B
Partition Table: gpt

Number  Start   End     Size    File system  Name  Flags
1      1049kB  137MB   136MB  
2      137MB   274MB   136MB  
3      274MB   278MB   4194kB                     bios_grub
4      278MB   897MB   619MB   ext2               boot
5      897MB   1073MB  176MB

Bon. un coup de kpartx plus loin, on pouvait monter la partition 4 qui s'avère être la partition root.

# kpartx -a /home/nntp-dwn-03.img.bin
# mount -t ext2 /dev/mapper/loop0p3 /mnt/

Puis chercher un peu à quoi servait ce serveur, son schéma de partitions, etc ...

# cat /mnt/etc/passwd
charlie:x:10149:5000::/home/nntpusers/charlie:/bin/bash
joesun:x:10172:5000::/home/nntpusers/joesun:/bin/bash
bigdoh:x:10213:5000::/home/nntpusers/bigdoh:/bin/bash
crazy:x:10255:5000::/home/nntpusers/crazy:/bin/bash
antho:x:10267:5000::/home/nntpusers/antho:/bin/bash
tex0ra:x:10291:5000::/home/nntpusers/tex0ra:/bin/bash
smartfrac:x:10349:5000::/home/nntpusers/smartfrac:/bin/bash
miumphix:x:10370:5000::/home/nntpusers/miumphix:/bin/bash
finicsys:x:10422:5000::/home/nntpusers/finicsys:/bin/bash
smurg:x:10448:5000::/home/nntpusers/smurg:/bin/bash
demonluv:x:10483:5000::/home/nntpusers/demonluv:/bin/bash
eugene:x:10517:5000::/home/nntpusers/eugene:/bin/bash
tommy:x:10544:5000::/home/nntpusers/tommy:/bin/bash
kevin:x:10550:5000::/home/nntpusers/kevin:/bin/bash
gapnog:x:10628:5000::/home/nntpusers/gapnog:/bin/bash
sarinar:x:10678:5000::/home/nntpusers/sarinar:/bin/bash

# cat /mnt/etc/fstab
UUID=1fae0408-fbe9-45b3-bf96-705175c3a00c / ext2 defaults,auto 1 1
home    /home   zfs     defaults,auto   1 1

# mount /dev/mapper/loop0p1 /mnt/
mount: unknown filesystem type 'zfs_member'
# mount /dev/mapper/loop0p2 /mnt/
mount: unknown filesystem type 'zfs_member'

# cat /mnt/etc/apt/sources.list.d/zfsonlinux.list
## This file is installed by the zfsonlinux package.

deb http://archive.zfsonlinux.org/debian wheezy main
#deb-src http://archive.zfsonlinux.org/debian wheezy main contrib

Ok, donc un serveur de newsgroup, en ZFS ...
Installons ZFS "comme eux", http://zfsonlinux.org/debian.html et hop!

# zpool import -d /dev/mapper/
   pool: home
     id: 9557236098197187793
  state: ONLINE
action: The pool can be imported using its name or numeric identifier.
config:

        home         ONLINE
          mirror-0   ONLINE
            loop0p1  ONLINE
            loop0p2  ONLINE
# zpool import home
# zpool list
NAME   SIZE  ALLOC   FREE    CAP  DEDUP  HEALTH  ALTROOT
home   125M  51.0M  74.0M    40%  1.00x  ONLINE  -
# mount -t zfs home /tmp/home/
# ls /tmp/home/
nntpusers

Ok, on peut maintenant explorer ... On trouvea *beaucoup* d'images, de chats...
Une première approximation facile, afficher les jpg:

$ find . -name '*.jpg' -o -name '*.jpeg'
### Grosse liste
$ find . -name '*.jpg' -o -name '*.jpeg' > files
$ ( while read file ; do echo $file; feh $file; done ; ) < files
... ./kevin/downloads/ovh.jpg

On tombe sur une fameuse image coupée : http://www.fser.info/ovhack/ovh.jpg.

Après plusieurs tests pseudo crypto, incluant la recherche de l'image originale (http://www.ovh.com/fr/backstage/photos/large/201303-C-07.jpg) xorée (http://www.fser.info/ovhack/ovh-xored.jpg) .. il faut changer de stratégie.

Lors de mes essais d'import avec zfs, je me rappelle avoir vu passer un snapshot...

# zfs list -t snap
NAME          USED  AVAIL  REFER  MOUNTPOINT
home@backup  3.08M      -  48.7M  -

Il y a quoi dans ce snapshot? Beaucoup de chose! Et quoi qui matche "ovh" ?

# zfs diff home@backup | grep ovh
-       /tmp/home/nntpusers/kevin/downloads/ovh.par2
-       /tmp/home/nntpusers/kevin/downloads/ovh.par2/<xattrdir>
-       /tmp/home/nntpusers/kevin/downloads/ovh.par2/<xattrdir>/security.selinux
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol00+32.par2
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol00+32.par2/<xattrdir>
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol00+32.par2/<xattrdir>/security.selinux
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol32+28.par2
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol32+28.par2/<xattrdir>
-       /tmp/home/nntpusers/kevin/downloads/ovh.vol32+28.par2/<xattrdir>/security.selinux

Bon comment on monte ce truc ... bah on a qu'à rollbacker ...

# zfs rollback home@backup

Bon j'ai eu une erreur en faisant ça, ça a mouliné plusieurs heures avant que je ne l'arrête en me disant que pour 3 pauvres mega ...
Bref, la commande fonctionne quand même avec un peu d'insistance.
Il y a aussi plus subtile, `mount -t zfs home@backup /mnt` par exemple :p

Bon, malheuresement, cela ne corrige pas l'image ovh.jpg ...
Par contre, cela restaure d'autres fichiers!

# ls -1 /mnt/nntpusers/kevin/downloads/ovh.*
/mnt/nntpusers/kevin/downloads/ovh.jpg
/mnt/nntpusers/kevin/downloads/ovh.par2
/mnt/nntpusers/kevin/downloads/ovh.vol00+32.par2
/mnt/nntpusers/kevin/downloads/ovh.vol32+28.par2

Les fichiers par2 servent à reconstruire des fichiers qui pourraient être défectueux. Comme le codage avec un bit de parité ... Plus de détails ici par exemple: http://www-igm.univ-mlv.fr/~dr/XPOSE2004/poirot/.

On peut donc essayer de reconstruire l'image

# par2repair ovh.*.par2
par2cmdline version 0.4, Copyright (C) 2003 Peter Brian Clements.

par2cmdline comes with ABSOLUTELY NO WARRANTY.

This is free software, and you are welcome to redistribute it and/or modify
it under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version. See COPYING for details.

Loading "ovh.vol00+32.par2".
Loaded 36 new packets including 32 recovery blocks
Loading "ovh.vol32+28.par2".
Loaded 28 new packets including 28 recovery blocks

There are 1 recoverable files and 0 other files.
The block size used was 4456 bytes.
There are a total of 100 data blocks.
The total size of the data files is 443439 bytes.

Verifying source files:

Target: "ovh.jpg" - damaged. Found 45 of 100 data blocks.

Scanning extra files:

Repair is required.
1 file(s) exist but are damaged.
You have 45 out of 100 data blocks available.
You have 60 recovery blocks available.
Repair is possible.
You have an excess of 5 recovery blocks.
55 recovery blocks will be used to repair.

Computing Reed Solomon matrix.
Constructing: done.
Solving: done.

Wrote 443439 bytes to disk

Verifying repaired files:

Target: "ovh.jpg" - found.

Repair complete.

Repair complete. Simple non? Le résultat est donc http://www.fser.info/ovhack/ovh-testing.jpg

On remarque donc la chaîne "XgQ5CghZRBU8DRgEIlVCNwY3fylAIH8AVQ== "
qui est une chaîne en base64 valide, mais non ASCII.

Après avoir tourné ça dans tous les sens, cela doit avoir trait à du chiffrement xor.
Tient, que se passe-t-il si l'on xor cette chaîne décodée avec nos rustines d'épreuves: "http://ovh.to/" ?

On obtient la chaîne "6pMz2vkzJe6pMz*CrGEoO h{" ... Notez le début de répétition de "6pMz".
Ok prennons "6pMz2vkzJe" comme clé.
Le but est donc, maintenant que l'on a trouvé la clé, d'afficher ce qu'il y a "derrière" le ovh.to.

Logique: la clé fait 10 caractères, "http://ovh.to/" 14. On a en fait réalisé une attaque par "clair connu".
Et en effet, ça fonctionne!

$ python xor.py
http://ovh.to/pAmM5LvP2zg
$ curl -I http://ovh.to/pAmM5LvP2zg
HTTP/1.1 301 Moved Permanently
Server: ngx_openresty/1.2.1.14
Date: Thu, 20 Jun 2013 11:52:43 GMT
Content-Type: text/html
Connection: keep-alive
location: https://hubic.com/pub/?ruid=aHR0cDovLzUuMTM1LjIwNS44Nzo4ODg4L3YxL0FVVEhfNjdiNmQ0MzI4NGQ3ZmIyNTJjMDFlMjY0MDdjMjY4NTgvZGVmYXVsdC8ub3ZoUHViLzEzNzE0Nzk4NTdfMTM3NDA3MTg1Nz90ZW1wX3VybF9zaWc9MjE0NzY1YTliODFlNWY2MjBlNzg1ZWJhN2I0MGUyNzUzOTc1OTEyMSZ0ZW1wX3VybF9leHBpcmVzPTEzNzQwNzE4NTc=

On obtient alors un fichier, qui nous indique la marche à suivre pour contacter @jobsatovh sur twitter.

Voilà c'est terminé. N'hésitez pas à commenter. Les scripts ne sont pas géniaux, si vous voyez des raccourcis
pythoniques, n'hésitez pas. La manipulation de la variable "span" à répétition me semble fastidieuse par exemple...
Pour ceux qui ont également publié un writeup, je peux mettre les liens ici. J'ai eu accès à des writeup privés, je ne sais plus qui est quoi, les commentaires sont là pour ça.

Après avoir refait le tour du challenge, j'ai ouvert les yeux et vu l'image "ovhhacked.png".

En l'ouvrant avec emacs (et son copain hexl-mode), on se rend comte qu'il y a des données après le iend du png.
On coupe tout ça, on identifie cherche alors un "magic number" connu (une liste ici par exemple: http://www.astro.keele.ac.uk/oldusers/rno/Computing/File_magic.html) Tient, on remarque un gzip.

Bon, ça n'évoque pas grand chose ... Essayons quand même un base64_decode.
On obtient quelque chose de valide, mais qui ne ressemble pas à grand chose non plus.

(pour des raisons de lisibilité, je vous invite à explorer http://fser.info/ovhack/ onfaitquoideca étant le fichier de-gzipé, et decode.py la moulinette qui essaye)

On remarque qu'il manque des retours chariots. En itérant sur tous les possibles, on remarque qu'à 104 caractères par ligne, ça devient lisible. Euréka!
Bravo à ceux qui ont trouvé dès le début.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.