Solution de lrstx pour Exfiltration

forensics réseau

3 mai 2024

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)