ECW - Data exfiltration

Posted on ven. 22 novembre 2019 in CTF

File : extracted.pcap

Merci à Killbit pour avoir trouver le writeup et les deux scripts python !

Data exfiltration (50 points)

It seems that some sensitive information has been compromised. The supervision teams have captured suspicious traffic and stored it in the FTP server of the Harbour's Master Office Secure (HMOS) LAN, but do not succeed to analyze it.

Help them.

Première étape, récupération des fichiers. Direction le serveur FTP ! Il faut d'abord trouvé son IP. Nous savons qu'il se trouve dans le HMOS (Harbour's Master Office Secure) LAN mais aucune information sur celui-ci. Cependant, nous pouvons faire des essais ciblés. Il y au un réseau en 10.0.10.0/24 et 10.0.30.0/24 d'annoncés. Essayons donc le milieu : 10.0.20.0/24.

Un scan NMAP ne renvoi rien car ce réseau ne semble pas être joignable.

Un scan de réseau sur le LAN nous indique un serveur en 10.0.10.254.

Nmap scan report for 10.0.10.254
Host is up (0.00067s latency).
Not shown: 99 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
MAC Address: 2E:0A:45:6B:CB:7F (Unknown)

On ajoute donc la route pour ce réseau via ce serveur : ip r a 10.0.20.0/24 via 10.0.10.254 dev eth0.

On peut donc refaire notre scan réseau :

Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-21 10:15 EST
Nmap scan report for 10.0.20.1
Host is up (0.00068s latency).
Not shown: 99 closed ports
PORT   STATE SERVICE
22/tcp open  ssh

Nmap scan report for 10.0.20.10
Host is up (0.0012s latency).
Not shown: 98 filtered ports
PORT   STATE SERVICE
21/tcp open  ftp
22/tcp open  ssh

Nmap done: 256 IP addresses (2 hosts up) scanned in 13.97 seconds

Bingo ! On a notre serveur FPT. On lance la connexion avec FileZilla. Comme on n'a pas d'information sur l'utilisateur et le mot de passe, on tente sans rien...

ftp.png

Et aussi surprennent qu'il soit, ça marche. Merci le FTP... On aspire alors tout le contenu.

On a alors ces fichiers à disposition :

├── Challenges
│   ├── Android
│   │   ├── base.apk
│   │   └── dump.pcap
│   └── Data exfiltration
│       ├── capture.pcap
├── Confidential patents
│   ├── 2162819600219719.jpg
│   ├── 3479819471660548.jpg
│   ├── 3725111495999545.jpg
│   ├── 5240616991475271.jpg
│   ├── 5459753873408937.jpg
│   ├── 7729689642204675.jpg
│   └── 8905726261278207.jpg
├── flag.txt
├── ftp.png
└── ship.txt

On a un flag.txt qui permet de récupérer quelques points :

cat flag.txt                
ECW{acb2[...]3c56}

Et notre fichier capture.pcap qui nous intéresse. On l'ouvre avec Wireshark. En le parcourant, on voit un certains nombres de requêtes DNS étranges :

dataexfiltration_2.png

Après un rapide recherche avec le contenu des requêtes, on trouve l'outil utilisé pour cette exfiltration : iodine.

Le but est donc de filtrer les requêtes à destination du domaine malicieux (.to.badland.com) et de déchiffrer l'échange.

En cherchant sur internet, on tombe sur un Writeup de Rawsec sur un challenge passé sur cette technologie : https://rawsec.ml/en/hxp-2017-write-ups.

Ils ne sont pas (encore) compatible Python3. Pensez donc à utiliser python2 si vous avez des erreurs.

"""
Horrible looking direct port of
https://github.com/yarrick/iodine/blob/master/src/base128.c
"""

cb128 = \
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" \
    "\274\275\276\277" \
    "\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317" \
    "\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337" \
    "\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357" \
    "\360\361\362\363\364\365\366\367\370\371\372\373\374\375"
rev128 = {ord(c): i for i, c in enumerate(cb128)}


def b128encode(data):
    data = map(ord, data)
    size = len(data)

    iin = 0
    buf = ''

    while 1:
        if iin >= size:
            break
        buf += cb128[((data[iin] & 0xfe) >> 1)]

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x01) << 6) | (((data[iin + 1] & 0xfc) >> 2) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x03) << 5) | (((data[iin + 1] & 0xf8) >> 3) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x07) << 4) | (((data[iin + 1] & 0xf0) >> 4) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x0f) << 3) | (((data[iin + 1] & 0xe0) >> 5) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x1f) << 2) | (((data[iin + 1] & 0xc0) >> 6) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[((data[iin] & 0x3f) << 1) | (((data[iin + 1] & 0x80) >> 7) if (iin + 1 < size) else 0)]
        iin += 1

        if iin >= size:
            break
        buf += cb128[(data[iin] & 0x7f)]
        iin += 1

    return buf


def b128decode(data):
    data = map(ord, data)
    size = len(data)

    iin = 0
    buf = ''
    while 1:
        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x7f) << 1) | ((rev128[data[iin + 1]] & 0x40) >> 6))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x3f) << 2) | ((rev128[data[iin + 1]] & 0x60) >> 5))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x1f) << 3) | ((rev128[data[iin + 1]] & 0x70) >> 4))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x0f) << 4) | ((rev128[data[iin + 1]] & 0x78) >> 3))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x07) << 5) | ((rev128[data[iin + 1]] & 0x7c) >> 2))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x03) << 6) | ((rev128[data[iin + 1]] & 0x7e) >> 1))
        iin += 1

        if iin + 1 >= size or data[iin] == 0 or data[iin + 1] == 0:
            break
        buf += chr(((rev128[data[iin]] & 0x01) << 7) | (rev128[data[iin + 1]] & 0x7f))
        iin += 2

    return buf
#!/usr/bin/env python
"""
Modified version of StalkR's script from
http://blog.stalkr.net/2010/10/hacklu-ctf-challenge-9-bottle-writeup.html

This version doesn't use any Popen calls, and ignores any errors while decoding
- krx
"""

import zlib
from base64 import b64encode, b64decode, b32encode, b32decode
from string import translate, maketrans
import chardet

from scapy.all import *

from base128_iodine import b128encode, b128decode

infile, outfile = "capture.pcap", "inception/extracted.pcap"
tld = "to.badland.com."

upstream_encoding = 128
# and no downstream encoding (type NULL)

# Translation tables for iodine's encoding
enctrans = {
    32: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'),
    64: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789+')
}

dectrans = {
    32: maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
    64: maketrans('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789+', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
}

# iodine encoders/decoders
encoders = {
    32: lambda x: translate(b32encode(x), enctrans[32]),
    64: lambda x: translate(b64encode(x), enctrans[64]),
    128: b128encode
}

decoders = {
    32: lambda x: b32decode(translate(x, dectrans[32])),
    64: lambda x: b64decode(translate(x, dectrans[64])),
    128: b128decode
}


def encoder(base, encode="", decode=""):  # base=[32,64,128]
    funcmap, data = (encoders, encode) if len(encode) > 0 else (decoders, decode)
    return funcmap[base](data)


def uncompress(s):
    try:
        return zlib.decompress(s)
    except zlib.error:
        return False


def b32_8to5(a):
    return "abcdefghijklmnopqrstuvwxyz012345".find(a.lower())


def up_header(p):
    return {
        "userid": int(p[0], 16),
        "up_seq": (b32_8to5(p[1]) >> 2) & 7,
        "up_frag": ((b32_8to5(p[1]) & 3) << 2) | ((b32_8to5(p[2]) >> 3) & 3),
        "dn_seq": (b32_8to5(p[2]) & 7),
        "dn_frag": b32_8to5(p[3]) >> 1,
        "lastfrag": b32_8to5(p[3]) & 1
    }


def dn_header(p):
    return {
        "compress": ord(p[0]) >> 7,
        "up_seq": (ord(p[0]) >> 4) & 7,
        "up_frag": ord(p[0]) & 15,
        "dn_seq": (ord(p[1]) >> 1) & 15,
        "dn_frag": (ord(p[1]) >> 5) & 7,
        "lastfrag": ord(p[1]) & 1,
    }


# Extract packets from DNS tunnel
# Note: handles fragmentation, but not packet reordering (sequence numbers)
dn_pkt, up_pkt = '', ''
datasent = False
E = []
i = 0
# modified from rdpcap to PcapReader
with PcapReader(infile) as pcap_reader:
    for pkt in pcap_reader:
        i+=1
        if i % 1000 == 0:  # Just for progress
            print i

        if not pkt.haslayer(DNS):
            continue
        if DNSQR in pkt:
            if DNSRR in pkt and len(pkt[DNSRR].rdata) > 0:  # downstream/server
                d = pkt[DNSRR].rdata
                if datasent:  # real data and no longer codec/fragment checks
                    dn_pkt += d[2:]
                    if dn_header(d)['lastfrag'] and len(dn_pkt) > 0:
                        u = uncompress(dn_pkt)
                        if u:
                            # Include the packet if decoding succeeded,
                            # ignore it and move on otherwise
                            E += [IP(u[4:])]
                        dn_pkt = ''
            else:  # upstream/client
                d = pkt[DNSQR].qname
                print(str(d[0]))
                if d[0].lower() in "0123456789abcdef":
                    datasent = True
                    up_pkt += d[5:-len(tld)].replace(".", "")
                    if up_header(d)['lastfrag'] and len(up_pkt) > 0:
                        u = uncompress(encoder(upstream_encoding, decode=up_pkt))
                        if u:
                            # Include the packet if decoding succeeded,
                            # ignore it and move on otherwise
                            E += [IP(u[4:])]
                        up_pkt = ''

wrpcap(outfile, E)
print "Successfully extracted %i packets into %s" % (len(E), outfile)

On obtions le résultat suivant :

$ python2 python2.py
Successfully extracted 11 packets into inception/extracted.pcap

On lui ensuite le fichier avec tshark :

$ tshark -q -r inception/extracted.pcap -z follow,tcp,ascii,0 

===================================================================
Follow: tcp,ascii
Filter: tcp.stream eq 0
Node 0: 10.0.0.2:50568
Node 1: 10.0.0.1:1664
1078
flag.txt............................................................................................000644 .000000 .000000 .00000000046 13542743565 012524. 0....................................................................................................ustar.00root............................root............................000000 .000000 ........................................................................................................................................................................ECW{E58B25A5943223922CA75B6AE6F06413}
................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
970
..........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
===================================================================

YEAH ! On a le flag. Il faut cependant le remettre en forme.

$ tshark -q -r inception/extracted.pcap -z follow,tcp,ascii,0 | tr '[:upper:]' '[:lower:]' | sed 's/\.//g' | sed 's/ecw/ECW/g'

===================================================================
follow: tcp,ascii
filter: tcpstream eq 0
node 0: 10002:50568
node 1: 10001:1664
1078
flagtxt000644 000000 000000 00000000046 13542743565 012524 0ustar00rootroot000000 000000 ECW{e58b25a5943223922ca75b6ae6f06413}

970

===================================================================

Voilà ! 50 points de faits :).