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
[email protected] 3.08M - 48.7M -
Il y a quoi dans ce snapshot? Beaucoup de chose! Et quoi qui matche "ovh" ?
# zfs diff [email protected] | 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 [email protected]
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 [email protected] /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