Solution
On ouvre le fichier de traces et on regarde les statistiques. Le DNS est un cas classique d’exfiltration, mais un filtre rapide ne montre rien qui saute aux yeux.
On a beaucoup de HTTP, et en filtrant sur ce port, on repère beaucoup
de réponses avec le status 418 I'm a teapot
, ce qui est louche car
ce code est censé être une blague !
On filtre donc sur ce trafic à l’aide de la règle http and ip.src==192.168.1.26 and ip.dst==198.18.0.10
et on examine les requêtes qui provoquent
ces 418.
On a un premier GET /
qui retourne:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to my panel!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to my panel!</h1>
<p>If you see this page, the malware is successfully installed and
working. No further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://127.0.0.1/">localhost</a>.<br/>
Commercial support is available at
<a href="http://127.0.0.1/">localhost</a>.</p>
<p><em>Merci.</em></p>
</body>
</html>
On a trouvé notre client. Dans la suite, on observe des POST
qui envoient
des données x-www-form-urlencoded
comprenant deux champs :
data
contenant une suite de caractères hexadécimaux.uuid
qui semble être le même identifiant fixe pour toutes les requêtes.
Il est temps de coder un script qui va extraire ces différents éléments de la trace pcap.
#!/usr/bin/env python3
import dpkt
from urllib import parse
f = open('exfiltration.pcap','rb')
for ts, pkt in dpkt.pcap.Reader(f):
# Only TCP
eth=dpkt.ethernet.Ethernet(pkt)
if eth.type != dpkt.ethernet.ETH_TYPE_IP:
continue
ip = eth.data
if ip.p != dpkt.ip.IP_PROTO_TCP:
continue
# Filter source and destination
if ip.src != bytes([192, 168, 1, 26]):
continue
if ip.dst != bytes([198, 18, 0, 10]):
continue
# Only HTTP requests
tcp = ip.data
try:
request = dpkt.http.Request(tcp.data)
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
continue
# Only POST
if request.method != 'POST':
continue
# Parse form
form = parse.parse_qs(request.body.decode('utf-8'))
data = form['data'][0]
if len(data) % 2 != 0:
data = '0' + data
value = bytes.fromhex(data)
print(value)
Une partie des résultats :
b'5(pgqc{kmc\xb9\xc7\xf8-scecscecscecxcec,\x11\x00\x0f\x00LK\x11\x16'
b'\x00\xf1l\xee\x12\xe2\xe6\x03&\xfe8\xc8\xe7bh\xda\xe0\xf4\xee\xbb\xba\xd9'
b'\x03A1\x0cG\x96t\xe7\xfc\x8f\x9d\xb3\xabd@\n\xc8\x9c\xad"o\xd9#\xe9\xc5\xa4\xcf\x1a\x96\x93o\xb1\x08\xad\x8d'
# [...]
b'cqcesscecscecscecs\xc6lcs\x07\n\x00#\x11\n\x13\x00L\x04\x13\x03'
b"\x04\xd1\xd0\xe1\xf32\xe6'\x17vWw6\xb6\xd6\xb7:\x9c\x1f\xe3\xdd\x15\nn"
b'\x05\x00dcs\xb8gcsrecscecscecsceaxce\x07\x1c\x005\x11\x1c\x13\x16L\x10\x0c\x17\x06]\x1b'
b'\x00\x80\xf22\x86F\x16v7\x167\xb6\xb6\xd6;\x9c\x7f\x82\xdb$\x17\xf2\xa5\x96&V2'
b'\rgec`cecscecscecsc\xc1osc> \x1c\r\x11\x06\x1d\x17:7\n\x13\x00\x10.M\x1d\x0e\x1f3.fu'
b'cecsjejs_gcslkcsce'
On repère rapidement un motif qui se répète : cecs
. D’expérience, on peut poser l’hypothèse
d’un chiffrement par XOR
, avec une clef de 4 caractères. On peut même supposer que la clef vaut cecs
, et
qu’elle apparaît au moment de chiffrer une suite d’octets \x00
.
On essaie de XORer la première ligne 3528706771637b6b6d63b9c7f82d736365637363656373636563786365632c11000f004c4b1116
.
>>> ''.join([ chr(x^ord("cecs"[i%4])) for i, x in enumerate(bytes.fromhex("3528706771637b6b6d63b9c7f82d736365637363656373636563786365632c11000f004c4b1116")) ])
'VM\x13\x14\x12\x06\x18\x18\x0e\x06Ú´\x9bH\x10\x10\x06\x06\x10\x10\x06\x06\x10\x10\x06\x06\x1b\x10\x06\x06Obcjc?(tu'
Le résultat n’est pas intelligible. Mais peut-être un problème d’alignement ? Essayons des rotations de la clef…
>>> ''.join([ chr(x^ord("ecsc"[i%4])) for i, x in enumerate(bytes.fromhex("3528706771637b6b6d63b9c7f82d736365637363656373636563786365632c11000f004c4b1116")) ])
'PK\x03\x04\x14\x00\x08\x08\x08\x00ʤ\x9dN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00_rels/.re'
Ben tiens, comme par hasard, la clef est ecsc
(oui, ça paraît évident quand on l’a sous le nez…). D’autre part,
on constate qu’on a affaire à une archive ZIP (signature PK
) et même un document Word (le premier fichier de l’archive
pourrait être _rels/.rels
).
On modifie et complète alors le script Python ci-dessous pour ajouter le XOR et on vérifie le résultat :
$ file result.bin
result.bin: Microsoft Word 2007+
On ouvre le fichier docx déchiffré et il contient :
Confidentiel
ECSC{v3ry_n01sy_3xf1ltr4t10n}
Script Python de résolution
#!/usr/bin/env python3
import dpkt
from urllib import parse
start = None
f = open('exfiltration.pcap','rb')
result = ''
for ts, pkt in dpkt.pcap.Reader(f):
# Only TCP
eth=dpkt.ethernet.Ethernet(pkt)
if eth.type != dpkt.ethernet.ETH_TYPE_IP:
continue
ip = eth.data
if ip.p != dpkt.ip.IP_PROTO_TCP:
continue
# Filter source and destination
if ip.src != bytes([192, 168, 1, 26]):
continue
if ip.dst != bytes([198, 18, 0, 10]):
continue
# Only HTTP requests
tcp = ip.data
try:
request = dpkt.http.Request(tcp.data)
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
continue
# Only POST
if request.method != 'POST':
continue
# Parse form
form = parse.parse_qs(request.body.decode('utf-8'))
data = form['data'][0]
result += data
# Decrypt the whole content
key = "ecsc"
output = open('result.docx', 'wb')
result = bytes.fromhex(result)
decoded = bytearray( [ x^ord(key[i%len(key)]) for i, x in enumerate(result) ] )
output.write(decoded)