Перейти к основному содержимому

Offzone 2024 райтапчики

·27 минут· loading · loading ·
Chillov3k
Cotsom
Avoidcode
Robert_sama
Bebra
Ezzh
Frakenbok
Автор
Chillov3k
cR4.sh / AppSec and average CTF enjoyer
Автор
cotsom
cR4.sh / Level 75 Mage
Автор
avoidcode
cR4.sh / Coding hater
Автор
robert 様です。
cR4.sh / чут чут рыв йор сер
Автор
Bebra
cR4.sh / Ношу кофе
Автор
ezzh
cR4.sh / Sweet ass
Автор
FrakenboK
cR4.sh / xss kernelovich
Оглавление

Сходили значит на офзон, много чего порешали там, делимся интересным.

Приятного чтения!

One day offer
#

Накануне OFFZONE 2024 на Хабре уже традиционно появилась статейка, представляющая розыгрыш проходок и One Day Offer от BI.ZONE.

Что ж, попробуем получить работу мечты бесплатную проходку на Offzone. Для этого всем желающим было предложено решить три таска с ранжированным уровнем сложности.

easy
#

Определимся с форматом полученного файла

Отлично, это симпатичная эльфийка! Запустим бинарь и посмотрим, что же такого он делает?

Программа требует ввести некоторую фразу, а после, видимо, выполняет ее валидацию.

Перейдем в IDA Pro для дальнейшго анализа.

Сразу можно заметить, что длинна ввода должна быть равна 32 символам.

Предварительный анализ показал, что введенная строка проходит через шквал ксора с перманентным ключём, а затем шифруется неустановленным блочным шифром.

Итоговая проверка происходит путем расшифровки флага с использованием заведомо известного шифротекста и ключа. Кроме ключа также требуется некое 4х байтное значение, по сути являющееся маленькой хэш-суммой от исходного сообщения.

Это значит, что у нас есть все, кроме конкретного значения 4х байтной хэш-суммы!

Ну и ладно, 4 байта это немного, можно и сбрутить

Тут я выбирал между эмуляцией машинного кода функции расшифровки и компиляцией псевдокода. В итоге решил использовать псевдокод, однако тут можно было получить поведение отличное от исходной функции, поэтому необходимо сверить результат выполнения функции путем отладки (я не сразу поправил типизацию, поэтому это был необходимый шаг)

Получилась такая вот простенькая прога для поиска магического числа:

#include <iostream>

void decrypt(unsigned int* data, unsigned int* key, int magic)
{
    unsigned int i; // [rsp+20h] [rbp-14h]
    unsigned int lo; // [rsp+24h] [rbp-10h]
    unsigned int hi; // [rsp+28h] [rbp-Ch]
    unsigned int temp; // [rsp+2Ch] [rbp-8h]

    lo = *data;
    hi = data[1];
    temp = magic << 6;
    for (i = 0; i < 64; ++i)
    {
        hi -= (((lo >> 5) ^ (16 * lo)) + lo) ^ (key[(temp >> 11) & 3] + temp);
        temp -= magic;
        lo -= (((hi >> 5) ^ (16 * hi)) + hi) ^ (key[temp & 3] + temp);
    }
    *data = lo;
    data[1] = hi;
}

bool readble(char* str, size_t size)
{
    for (size_t i = 0; i < size; i++)
        if (0x21 > str[i] || str[i] > 0x7e)
            return false;
    return true;
}

int main()
{
    uint64_t key[2];
    key[0] = 0x706050403020100LL;
    key[1] = 0xF0E0D0C0B0A0908LL;
    

    for (uint32_t magic = 0; magic < 0xffffffff; magic++)
    {
        char cipher[9];
        *(uint64_t*)&cipher = 0xEFBB45E15C46C3E3LL;
        cipher[8] = 0;

        decrypt((uint32_t*)&cipher, (uint32_t*)key, magic);

        if (cipher[0] == 'c' && cipher[1] == 't' && cipher[7] == '{' && readble(cipher, 8))
        {
            std::cout << "SOLVE: " << std::hex << magic << std::endl;
            std::cout << cipher << std::endl;
        }

        if (magic % 0xfffffff == 0)
            std::cout << "iter: " << std::hex << magic << std::endl;
    }
}

Через gdb поставим точку останова в оригинальном бинаре перед вызовом функции decrypt

Начнем выполнение и передадим произвольные данные на вход, проходящие базовую фильтрацию по длине

Проверим чем проинициализирован буффер с шифротекстом, а также вытащим значение магического числа

и зафиксируем состояние буффера после выполнения функции

Сверимся с функцией из псевдокода:

Отлично, мы получили тот же результат, а значит можно переходить к бруту!

Магическое число найдено!

Теперь расшифруем оставшуюся часть:

    uint64_t key[2];
    char cipher[33];
    key[0] = 0x706050403020100LL;
    key[1] = 0xF0E0D0C0B0A0908LL;

    *(uint64_t*)cipher = 0xEFBB45E15C46C3E3LL;
    *(uint64_t*)&cipher[8] = 0x7CA4AE528481F490LL;
    *(uint64_t*)&cipher[16] = 0x6CC3B2363BD62ED8LL;
    *(uint64_t*)&cipher[24] = 0x594A28956B2B39BDLL;

    uint32_t magic = 0x9e377989;

    for (int n = 0; n <= 3; ++n)
        decrypt((unsigned int*)&cipher[8 * n], (unsigned int*)key, magic);

    std::cout << cipher << std::endl;

Флаг: ctfzone{70u_e4s1l7_can_d0_1t}

medium
#

Следующий по уровню сложности таск. Разберемся в написании своего простенького деобфускатора для кастомного алгоритма обфускации кода платформы Microsoft .NET.

alt text

Анализ
#

Перейдём по заветной ссылке из статьи и получим файлик, с которым нам далее предстоит работать.

alt text

Внушает доверие! Сразу же запустим бинарь, ведь мы верим разработчикам таска (Не делайте так, пожалуйста).

Бинарь нагло выпрашивает флаг - дадим ему что просит.

alt text

Таким поворотом событий он явно не впечатлен. Что ж, попробуем разобраться как он работает.

Определим тип файлика через DIE (Detect It Easy, утилиту, позволяющую путем сигнатурного анализа отпределить компилятор, линковщик, наличие пакера(-ов) и другую важную инфу, которая может пригодится при ковырянии очередного бинарника).

alt text

Ладно, про это мы уже говорили в предыдущих статьях. Определить тип файлика можно и без всяких даев.

alt text

Mono/.Net assembly... - имеем дело со сборкой Microsoft .NET, скомпилированной под ОС Windows. Для анализа и реверс-инжиниринга подобных бинарей существует несколько инструментов, самыми известными из которых являются ILSpy и dnSpy. Первый по сравнению с последним курит в сторонке: dnSpy имеет возможность модификации кода на лету (патчинга), отладки, модификации ресурсов и пересборки. Правда, его поддержка была прекращена в 2020, однако использовать его все ещё можно (из DIE видим версию .NET Framework 4). В случае необходимости, можно использовать dnSpy от dnSpyEx, это развитие форка проекта сторонним разработчиком.

Закинув бинарь в dnSpy видим следующую сборку с милым названием, заставляющую нас страдать решать: sharpon <3.

alt text

Видим, что в сборке присутствует два класса с префиксом Pon_ в названии. Далее в названии типа идет что-то малопонятное, очевидно над бинарем потрудился обфускатор. Кроме двух классов видим два ресурса realpongo_1 и realpongo_2. Их назначение разберем позже. Посмотрим на декомпиль кода каждого из типов (классов). В первом найдём страшное месиво из вызовов методов с префиксом pon_.

alt text

В модуле же, без указания неймспейса разместилось бесчисленное множество объявлений делегатов одного вида (типы, наследуемые от MulticastDelegate). Именно их методы вызываются в коде выше.

alt text

Делегат (Delegate) в платформе .NET (и .NET Framework) - type-safe указатель на метод, либо множество методов.

Вызов делегата (в C# выглядит буквально как вызов метода) приведет в вызову всех методов, ассоциированных с ним.

Деобфускация
#

Обратим внимание на реализацию наследников класса MulticastDelegate с префиксом pon_. Все они имеют статический конструктор (будет вызван при первом обращении к любому методу, полю или свойству объекта данного класса). А кроме него, публичный статический метод-прокси, принимающий последним аргументом объект делегата данного типа и вызывающий его со всеми аргументами до последнего (в примере - метод pon_CV4ETLEOJK). Также каждый такой тип имеет non-public (internal) поле, в данном примере pon_E5PZZ7Z670. Эти замечания нам понадобятся позже. В статическом конструкторе каждого из этих типов увидим вызов метода второго класса сборки Pon_W17P33WDUSJ731VRYYBVFNL3YWO3XD6A.pongo_69OQU310OO. Посмотрим на его реализацию.

alt text

С первого взгляда это наглядный пример нарушения принципа 15-строчного идеального метода выглядит страшно. Но начнем по порядку. Все, что делаем данный метод - вычитывает бинарный ресурс realpongo_1 как словарь int -> int (хеш-мапу для джавистов), а далее проходится по полям переданного в аргументе метода типа и находит соответствие MetadataToken‘у поля в словаре. После, по полученному из словаря значению резолвит метод в сборке и в зависимости от признака статичности метода определяет делегат (поле) указателем на этот метод.

MetadataToken - уникальный идентификатор ресурса, метода, поля и другого объекта CIL в сборке.

Всё это - реализация возможности запуска кода в рантайме. Когда бинарь запущен и происходит обращение к какому-либо из делегатов с префиксом pon_, сначала происходит его определение (присвоение ссылки на реальный метод, имеющий функционал). Так в реальном времени бинарь исполняется без проблем, однако статический анализ кода приложения крайне затруднителен.

Попробуем решить эту проблему, прогнав данный код и заменив вызовы делегатов в коде на вызов реальных методов. Запишем готовую сборку, которую потом можно будет анализировать в статике. Для этого напишем небольшой деобфускатор на базе библиотеки dnLib, именно её для работы использует dnSpy - чем мы хуже?

Загрузим бинарь, используя

var ponModule = ModuleDefMD.Load("pon.exe")

и прочитаем ресурс realpongo_1, содержащий маппинги на реальные методы.

Dictionary<int, int> tokensMap = ReadResourceDictionary<int>(ponModule, "realpongo_1");

Далее пройдемся каждому методу в каждом типе сборки и заменим вызовы делегатов на прямые.

Операции придется проводить на уровне инструкций стековой машины .NET - языке IL (CIL, Common Intermediate Language - одно и то же), в него компилируется любой язык, поддерживаемый платформой (Visual Basic, C#, F# и проч.) В рантайме этот код прогоняется через JIT-компилятор и исполняется.

Нас интересуют вызовы методов - инструкции call, а точнее - их аргументы. Посмотрим на инструкции вызова одного из методов-прокси.

alt text

Как видим, вызываемый делегат загружается на стек сразу перед инструкцией call при помощи ldsfld (Load Static Field). При первом обращении к данному статическому полю класса будет выполнен код конструктора, который определит делегат ссылкой на реальный метод. Мы же удалим загрузку этого поля, а вместо вызова прокси-метода подставим вызов метода-цели напрямую, ведь у нас есть маппинги в tokensMap.

Получится что-то типа этого.

foreach (TypeDef typedef in ponModule.GetTypes().Where(t => t.FullName.StartsWith("pon_")))
{
    int keyFieldToken = typedef.Fields.Where(f => f.IsStatic && !f.IsPublic).Single()
        .MDToken.ToInt32();
    int obfMethodToken = typedef.Methods.Where(f => f.IsStatic && f.IsPublic).Single()
        .MDToken.ToInt32();

    int deobfMethodToken = tokensMap[keyFieldToken];
    MethodDef targetMethod = ponModule.ResolveMethod(MDToken.ToRID(deobfMethodToken));

    foreach (TypeDef type in ponModule.GetTypes())
    {
        foreach (MethodDef typeMethod in type.Methods.Where(m => m.Body != null))
        {
            for (int instrIndex = 0; instrIndex < typeMethod.Body.Instructions.Count; instrIndex++)
            {
                Instruction instr = typeMethod.Body.Instructions[instrIndex];
                if (instr.OpCode == OpCodes.Call)
                {
                    Instruction prevInstr = typeMethod.Body.Instructions[instrIndex - 1];
                    if (prevInstr.OpCode == OpCodes.Ldsfld &&
                        (instr.Operand as IMethodDefOrRef).MDToken.ToInt32() == obfMethodToken)
                    {
                        typeMethod.Body.Instructions[instrIndex].Operand = targetMethod;
                        typeMethod.Body.Instructions.RemoveAt(instrIndex - 1);
                        instrIndex--;
                    }
                }
            }
        }
    }
}

Запишем модифицированную сборку в новый файлик, назовем оригинально: depon.exe. Оценим результат.

alt text

Огонь! Теперь не декомпилируется практически ни один метод. Придется разбираться.

Запихаем полученный бинарь в ILSpy, может он сможет переварить всю грусть наших модификаций.

alt text

И действительно, что-то вменяемое в коде разглядеть можно. Однако вместо вызовов некоторых методов явно просматриваются ошибки. Например строка s точно хранит ввод пользователя, стало быть перед ней должен быть вызов чего-то вроде Console.ReadLine(), а видим мы Pon_W17P33WDUSJ731VRYYBVFNL3YWO3XD6A.pongo_POLTBZD0VO. ILSpy же ругается на несовпадение типов аргументов методов. В других местах можно увидеть совершенно читаемый код, значит некоторые методы (большая часть) были заменены корректно. Можно заметить, что в замененных методах нет ссылок на внешние относительно нашей сборки типы. Ошибка оказалась в резолвинге методов здесь:

MethodDef targetMethod = ponModule.ResolveMethod(MDToken.ToRID(deobfMethodToken));

Таблицы токенов TypeDef (объявленных в сборке типов) и TypeRef (ссылок на типы из внешних сборок) различны.

alt text

А потому резолвить методы “свои” и “чужие” нужно несколько иначе. Радует, что указатель на нужную таблицу содержится в самом токене, а в dnLib есть удобные методы работы с ними.

static IMethodDefOrRef ResolveMethodDefOrRef(ModuleDefMD module, int token)
{
    uint rid = MDToken.ToRID(token);
    if (MDToken.ToTable(token) == Table.Method)
        return module.ResolveMethod(rid);
    else
        return module.ResolveMemberRef(rid);
}

Заменим

MethodDef targetMethod = ponModule.ResolveMethod(MDToken.ToRID(deobfMethodToken));

на

IMethodDefOrRef targetMethod = ResolveMethodDefOrRef(ponModule, deobfMethodToken);

Запустим и откроем полученный файлик в dnSpy.

alt text

Этот код уже гораздо более читаем, а странные названия методов можно поменять исходя из их реализации. Однако, осталось еще кое-что. В коде совершенно отсутствуют строковые константы, которые точно где-то хранятся ("Input Flag:", например). Посмотрим на первый вызов на скрине выше, в Console.Write определенно должна передаваться строка. Смотрим реализацию Pon_W17P33WDUSJ731VRYYBVFNL3YWO3XD6A.pongo_POLTBZD0VO.

alt text

Ага, строки хранятся во втором ресурсе realpongo_2, представляющего собой хешмапу int -> string. Допишем наш деобфускатор. Пусть заменяет вызов данного метода на загрузку соответствующей строковой константы.

if (instr.Operand is MethodDef callee &&
    callee.Name == "pongo_POLTBZD0VO")
{
    int value = (int)typeMethod.Body.Instructions[instrIndex - 1].Operand;
    typeMethod.Body.Instructions[instrIndex] = new Instruction(OpCodes.Ldstr, stringsMap[value]);
    typeMethod.Body.Instructions.RemoveAt(instrIndex - 1);
    instrIndex--;
}

Результат.

alt text

И оно даже запускается! :)

Реверс
#

Дело за малым, переименовать методы и разобрать алгоритм. Для удобства я переписал его на Python.

import hashlib

def rotate32_right(value, offset):
    return ((value >> offset) | (value << (32 - offset))) & 0xFFFFFFFF

def rotate32_left(value, offset):
    return ((value << offset) | (value >> (32 - offset))) & 0xFFFFFFFF

def reverse32(value):
    res = 0
    for i in range(32):
        res <<= 1
        res |= (value & 1)
        value >>= 1
    return res & 0xFFFFFFFF

def mix(flag, key):
    num = flag
    for i in range(5):
        num = rotate32_right(num, 3)
        num = reverse32(num)
        x1 = rotate32_left(key, i + 1)
        x2 = rotate32_right(key, i + 1)
        num ^= x1
        num ^= x2
    return num

TARGET = "<HUGE_HEX_VALUE_HERE>"

def main():
    flag_bytes = input("Flag: ").encode()
    key = 0x13371337
    hashed = ""
    while len(flag_bytes) % 4 != 0:
        flag_bytes += b"\x00"
    for i in range(0, len(flag_bytes), 4):
        val = int.from_bytes(flag_bytes[i:i + 4], byteorder='little')
        mixed = mix(val, key)
        lsb = (mixed % 65536) & 0xFFFF
        msb = (mixed >> 16) & 0xFFFF
        hashed += hashlib.sha256(lsb.to_bytes(2, 'little')).hexdigest() + hashlib.sha256(msb.to_bytes(2, 'little')).hexdigest()
    print("HAROSH" if hashed == TARGET else "PLOH(N)")

if __name__ == "__main__":
    main()

Алгоритм заключается в следующем: флаг, представленный в виде байтовой поcледовательности дополняется нулями до длины, кратной четырем. Далее каждая четверка байт представлеятся little-endian закодированныим числом, которое передается в функцию “шифрования”. Выхлоп функции делится на 2 двухбайтных числа, старшую и младшую части. Их байтовое представление хешируется SHA256, а затем конкатенируется с результирующей hex-строкой. В конце проверяется совпадение с захардкоженым результатом.

<Страдания>
#

Увидев таск впервые, я решил, что функция mix не может быть обращена и начал брутить возможные входные значения. Позже, взглянув внимательнее на функцию, стало понятно, что можно просто развернуть последовательность вызовов в функции, а 32-х битное вращения вправо заменить на вращение влево. Функция никак не меняет значение key и не теряет данные по выходу.

</Страдания>
#

В результате имеем что-то такое:

import hashlib

def rotate32_right(value, offset):
    return ((value >> offset) | (value << (32 - offset))) & 0xFFFFFFFF

def rotate32_left(value, offset):
    return ((value << offset) | (value >> (32 - offset))) & 0xFFFFFFFF

def reverse32(value):
    res = 0
    for i in range(32):
        res <<= 1
        res |= (value & 1)
        value >>= 1
    return res & 0xFFFFFFFF

def unmix(num, key):
    for i in range(5):
        num ^= rotate32_left(key, i + 1)
        num ^= rotate32_right(key, i + 1)
        num = reverse32(num)
        num = rotate32_left(num, 3)
    return num

TARGET = "<HUGE_HEX_VALUE_HERE>"
KEY = 0x13371337

def main():
    flag = ""
    for i in range(0, len(TARGET), 128):
        h1, h2 = TARGET[i:i+64], TARGET[i+64:i+128]
        print(h1, h2)
        for i in range(65536):
            hash = hashlib.sha256(i.to_bytes(2, 'little')).hexdigest()
            if hash == h1:
                v1 = i
            if hash == h2:
                v2 = i
        num = (v2 << 16) | v1
        flag += unmix(num, KEY).to_bytes(4, 'little').decode()
    print(flag)

if __name__ == "__main__":
    main()

Флаг
#

Запустим решалку и получим заветный флаг: ctfzone{d3l3g@t3d_m@lw@r3_t0_y0u,_ch3ck_7927c8b3f2860394fb6159c688f31625}

Полный исходный код проекта деобфускатора можно найти тут.

Offzone
#

40bit
#

Описание

В большой корпорации произошел инцидент ИБ. В Wi-Fi сети использовался слабый протокол защиты, был похищен архив с ключевой информацией к критичным данным. Копий не осталось. При опросе системный администратор указал, что проводил работы по отладке проблемы с авторизацией в домене. После восстановления не удалил файлы отладки. Это привело к компрометации всего домена. В ходе сбора данных удалось получить дамп сетевого трафика с ядра сети. Проведи анализ и найди что похитили злоумышленники.

К таску прикреплен дамп трафика размером около 90 Мб. Стоит сказать, что таск не совсем стфный, потому что он сделан по мотивам реального инцидента. Так что, решая его, можете почувствовать себя настоящим форензиком и примерить эту роль.

Открываем wireshark и осматриваемся.

dump1
dump2

Если коротко, до в дампе происходит буквально то, о чем говорится в описании таска. Нас просят найти файл, который угнали хацкеры. Импортируем из трафика всё, что обнаружит wireshark

import
import2

Отсортировав по размеру, видим, что мы можем извлечь файл ntds.dit, а также SYSTEM и SECURITY.

ntds.dit — Это файл базы данных Active Directory (AD). Он хранит всю информацию и данные конфигурации для доменных контроллеров в среде Windows Server, включая учетные записи пользователей, пароли и другие данные AD. Он то и является тем самым лакомым куском для хакера.

SYSTEM — файл часть реестра Windows. Он содержит информацию о конфигурации системы, такую как установленные драйверы и параметры аппаратного обеспечения.

SECURITY — ещё один файл реестра Windows. Он содержит информацию, связанную с политиками безопасности системы, включая учетные записи пользователей и разрешения.

С помощью этих трех файлов и тулзы secretsdump из набора impacket, мы можем извлечь все хэши паролей от всех учеток.

impacket-secretsdump -ntds ntds.dit -security SECURITY -system SYSTEM LOCAL

Внимание сразу привлекает учетка, связаная с wifi. Почему? Прочти описание таска еще раз.

big.corp\wifi_test:1113:aad3b435b51404eeaad3b435b51404ee:de32ca82da965f21d66757ddbb7f0e09:::

Это NTLM хэш, будем брутить.

hashcat -a 0 -m 1000 hash /usr/share/wordlists/rockyou.txt

Результат - de32ca82da965f21d66757ddbb7f0e09:Qwert

Пароль есть, но что с ним делать? Спросив у придурка ChatGPT что-то типа “40bit password wifi что это может означать”. Получаем

pridurok

Гуглим 40 bit wep key generator, заходи на рандомный сайт, вставляем наш пароль, получаем WEP ключ

key

Идем в вайршарк и вставляем его вот так:

wep

Далее вкладка протоколы и 802.11

wep2

Применяем наш ключ и идем во вкладку импортов, как делали в самом начале. Тут стало уже намного больше объектов для импорта. Есть много всего, но нужен нам файл key

flag

При открытии сразу видим, что это html. Откроем его в браузере и встретим флаг.

flag2

Kube cicada8
#

В общем, есть такое дело… Ко мне человек один шел, Шустрый, флаг нёс по договору с нужными людьми. Да вот незадача: пропал он. Последний раз неподалёку от порта видели, там как раз бандиты обосновались. Уговор простой: ты мне этот флаг, я тебе - материальную благодарность, хе-хе-хе, и обоим хорошо. Координаты порта сейчас тебе отправлю.

По наводке Сидоровича Цикадовича, мы оказываемся на подходе к порту, который (о, как символично) является kubernetes кластером.

alt text

Как только мы попали на тачку, нас встречает следующий баннер: Дорога, ведущая в порт. На обочине стоит грузовой контейнер, рядом с которым сидит группа бандитов. Неподалёку стоит машина, за которой можно спрятаться и подслушать их разговор, оставшись незамеченным.

Проведя небольшую разведку, мы понимаем, что оказались в pod’е

alt text

Наша задача “подслушать” разговор ничего не подозревающих бандитов, а подсказки в баннере намекают нам на сайдкар контейнер, траффик которого мы должны послушать. Вспоминаем write up на k8s lan party, а именно второй таск, в котором перед нами стояла точно такая же задача.

Первым делом нам нужно узнать адрес контейнера, который с нами общается. Сделать это можно выполнив команду netstat, и просмотрев все активные подключения.

alt text

Думаю, что адрес razgovor.offzone.s:1337 является искомым. Далее с помощью утилиты tcpdump начинаем слушать весь трафик исходящий от нашего сайдкара. tcpdump host razgovor.offzone -A

Перехватив увлекательный разговор бандитов, нам удалось получить информацию о новом ресусре hackerdva.ctf.cicada8.ru и пароле Tolya_v_otpuske (Также благодаря dns имени сайдкара razgovor.offzone.svc.cluster.local.1337 мы узнаем namespace, в котором все крутится - offzone. Он нам понадобится чуть позже)

alt text

Попадаем на ресурс, используя полученный пароль

alt text

На странице помимо алерта нас встречает комментарий в html’е

<!-- Лавка торговца. Никого нет, но дверь оказалась не заперта. На столе лежит записка: "Для посыльного: код доступа найдешь там же, где и всегда, в продуктовой комнате" -->

Профазим роуты, подставив нашу куку и найдем /market/products.php

alt text

Перед нами на мгновение мелькнет страничка, но злой js редиректнет нас обратно. Чтобы избежать его мы пошлем простой curl.

curl -H "Cookie: PHPSESSID=37c1c1574b402bd21cf71a2efbd738c3" http://hackerdva.ctf.cicada8.ru/market/products.php

alt text

Быстро осмотрев респонс, мы замечаем, что картинки тягаются с помощью запроса /market/products.php?file=image.png

Знакомая конструкция сразу наталкивает на мысли о lfr

Догадки оказываются верны, и мы получаем содержимое файла /etc/passwd

alt text

Не забываем, что все наши приключения происходят внутри куб кластера, а значит мы должны попробовать получить токен сервис аккаунта, который располагается по пути /var/run/secrets/kubernetes.io/serviceaccount/token.

alt text

Получив новый токен, а вместе с ним и новые привилегии, мы отправляемся обратно откуда начали, чтобы осмотреться в кластере.

Используем утилиту kubectl для общения с kube api, чтобы просмотреть права нового сервис аккаунта, подставляя его токен, адрес апи сервера, offzone namespace, а также insecure флаг, чтобы на серт не ругались

./kubectl --token=${TOKEN} --server=https://10.233.0.1:443 --insecure-skip-tls-verify=true -n offzone auth can-i --list

alt text

В ответе мы видим, что у нас имеется привелегия на чтение секрета с именем dlya-posylnogo. Давайте прочитаем его ./kubectl --token=${TOKEN} --server=https://10.233.0.1:443 --insecure-skip-tls-verify=true -n offzone describe secret dlya-posylnogo

Забираем очередной токен и проделываем всю операцию заново

alt text

На этот раз мы имеем привилегию impersonate для service account’а с названием fraer. Данная привилегия дает нам право исполнять команды от его имени.

alt text

Попробуем все так же посмотреть наши права в кластере, только вместо использования нового токена притворимся другим SA.

./kubectl --token=${TOKEN} --server=https://10.233.0.1:443 --insecure-skip-tls-verify=true --namespace=offzone --as=system:serviceaccount:offzone:fraer auth can-i --list

alt text

У нас имеются права на выполнение команд в другом поде, а значит получим в нем шелл.

./kubectl --token=${TOKEN} --server=https://10.233.0.1:443 --insecure-skip-tls-verify=true --namespace=offzone --as=system:serviceaccount:offzone:fraer exec sklad -it -- sh

В новом поде нас встречает новый баннер

Большое портовое помещение, переоборудованное под склад. Над входом висит огромный плакат "Будь внимателен к окружающей среде!". Тебя довели досюда и оставили в одиночестве. Почему-то ты чувствуешь сильное дежавю, как будто вновь оказался в лавке торговца. Интересно, к чему бы это?... Стоит осмотреться.

не будем долго думать и просто снова посмотрим права текущего service account’а

alt text

на этот раз в колонке ресурсов нас встречает nodes/log, после тщетных попыток заглянуть наконец в эти логи, нужно немного погуглить и найти замечательную страничку на hacktricks , из которой мы узнаем “If the attacker controls any principal with the permissions to read nodes/log , he can just create a symlink in /host-mounted/var/log/sym to / and when accessing https://<gateway>:10250/logs/sym/ he will lists the hosts root filesystem (changing the symlink can provide access to files).”

Более подробно прочитать, почему так получается вы можете по этой ссылке

Немного осмотревшись, можно заметить в корне директорию /flag, а в ней zapiska

По вопросам флагов смотрите в журнале у Рута Андреевича (вашего босса, если забыли), здесь их больше не будет. Меня не беспокоить, сами до этого довели

После данного сообщения отправляемся в директорию /var/log и обнаруживаем, что /var/log/archive/flag является symlink’ом на /flag, нужным нам для эксплуатации данной привилегии.

alt text

Отправляем запрос так, как это показано в статье выше, подставляя новый токен и получаем флаг

curl -k -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InJNU3FMeDcyVHlmVTRWYkFMdzlqQmhkVTgxa29fUXRFdVZWcktIQWZw[...]' 'https://10.110.81.4:10250/logs/flag/flag.txt'

alt text
Дополнительно приложу авторское решение

Green Cube
#

В тематической зоне CUB_3 как всегда было представлено несколько заданий, решив которые участник мероприятия мог получить внушительное кол-во внутренней валюты конференции - Offcoin. Разберем одно из них.

Зелёный куб попытались надёжно спрятать от посторонних глаз.
Кажется у них даже это получилось.
У нас есть дамп интерактивной консоли и команды которые были введены в неё, для того чтобы спрятать куб.
Получится ли у вас найти его и узнать секрет?

С описанием к заданию получаем пару файлов: commands.txt и green_dump. Первый имеет следующее содержание:

>>> from Crypto.Cipher import AES
>>> import os
>>> key = os.urandom(16)
>>> iv = os.urandom(16)
>>> key, iv
>>> ctx = AES.new(key, AES.MODE_CBC, iv=iv)
>>> cube_file_data = open('green_cube.png','rb').read()
>>> cube_file_data += (16 - len(cube_file_data)%16)*b'\x00'
>>> cube_file_data = ctx.encrypt(cube_file_data)
>>> key = b''
>>> iv = b''

Прочитав предложенный код, несложно догадаться о цели наших действий - необходимо получить изображение green_cube.png, содержимое которого было зашифровано случайным ключем с помощью алгоритма шифрования AES в режиме Cipher Block Chaining (CBC). Зашифрованного файла нам не предоставили, однако у нас есть полный дамп памяти процесса интерпретатора Python в момент ввода данных команд. Осталось только отыскать нужные данные в бесконечном потоке ненужного … в файле дампа.

Обратим внимание на команду интерпретатора в 5 строке - ключ и вектор инициализации, сгенерированные рандомом, выводятся в терминал. А это значит, что в дампе эти данные представлены не только простым набором байтов, но и в человекочитаемом виде. Кроме того, выводятся они записью через запятую, что говорит интерпретатору сформировать кортеж. Таким образом, для поиска ключа и вектора iv нужно просто найти ascii-строки в дампе, начинающиеся со следующих симолов (кортеж, имеющий байтовый массив первым элементом): (b'. Например, так: strings green_dump | grep "(b'". Нас радостно приветствует необходимый кортеж, аж в двух экземплярах.

key and iv

Лутаем и идем далее. Ключ и iv у нас уже есть, теперь необходимо отыскать данные для дешифровки.

На этом моменте в процессе решения у меня случилось озарение и осознание - файл читали в чистом виде, значит в оперативке он 100% должен присутствовать. Однако, поиск по хедеру и binwalk оказались бессильны - создатели таска заботливо вырезали нужные байтики. Ну еще бы, тогда бы таск не был таском.

Попробуем найти зашифрованное изображение. Мы знаем параметры шифрования - это может натолкнуть нас на следующую мысль. Для поиска изображения можно пройтись по всему файлу дампа, выделяя блоки по 16 байт (размер одного блока AES) и пытаясь их расшифровать. В полученном блоке будем искать 8 байт заголовка PNG: '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a'. В случае, когда найдено смещение от начала файла, расшифруем оставшийся дамп. Картинка будет в самом начале полученных данных, а все что превратится в мусор в результате “расшифровки” будет игнорировано средством просморта (читать хедер и выпиливать мусор - лень долго).

from Crypto.Cipher import AES

key, iv = (b'\x14\xfd\x0bM`\xef\t\x07*\xb4\x9b\x97PXc\xa4', b'\x83\xd7\xb2\x99\xb6\n\xe6Mn9\xe8)^\xd0\x871')

def find_enc_offset(haystack, needle):
    for i in range(len(haystack) - 16):
        try:
            ctx = AES.new(key, AES.MODE_CBC, iv=iv)
            dec = ctx.decrypt(haystack[i:i+16])
            if (needle in dec):
                print(f"Found offset: {i}")
                return i
        except:
            pass

def main():
    with open('green_dump', 'rb') as src:
        data = src.read()
    offset = find_enc_offset(data, b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")
    assert offset != None
    img_data = data[offset:]
    img_data += b'\x00' * (16 - len(img_data) % 16)
    ctx = AES.new(key, AES.MODE_CBC, iv=iv)
    dec = ctx.decrypt(img_data)
    with open("cube.png", 'wb') as dst:
        dst.write(dec)
    print("Done!")

if __name__ == "__main__":
    main()

Данный код реализует эту идею. Реинициализация AES в функции поиска смещения необходима, так как используется режим CBC. В случае использования ECB можно было бы использовать 1 инстанс (подробнее про режимы AES тут). Запустим скрипт, и после некоторого раздумья компа, получим искомую картинку, а вместе с ней и флаг.

cub_3

Web cicada8
#

Я в последнее время проводил свое маленькое расследование и узнал, что есть один интересный тайник с флагом. Только вот где он, до сих пор неясно. Недавно неподалеку появился информатор - болтает о именах всяких и прочей мути, но знает явно больше. Попробуй вытянуть из него информацию, сходи за тайником и принеси мне. Контакт отправляю: http://generator.ctf.cicada8.ru:8080

Еще один таск от cicada8. Сканим данный узел на наличие других портов.

cicada_web1
fscan помимо базового скана открытых портов по ним прошелся разными поками. Можно заметить - успешно, веб на спринге, и случается, что по ручке /actuator/heapdump выплюнется дамп кучи. Можно еще заметить открытую на внешку базу данных postgresql (запоминаем эту деталь). Получается имеем хипдамп (дамп памяти текущего java процесса, в данном случае), который представляет собой снимок текущего состояния памяти, включая объекты, их связи и прочую информацию. Для анализа хипдампов можно использовать анализатор от eclipse, а можно, зная, что нас интересует поискать в стрингсах интересующую информацию. Вернемся к тому, что торчит постгря, креды для которой по любому хранятся в памяти процесса приложения. По общей практике использоваться для подключения к бд url вида jdbc:postgresql://hostname:5432/dbname, можно грепать по схеме данного урла.
cicada_web2
Получаем креды, осталось подключиться к постгре, не найти флага в таблицах, и приступить к эксалированию импакта подключения к postgresql. Одним из вариантов является использование функции pg_read_file() и pg_ls_dir(), приводящей к чтению локальных файлов и листингу директорий соответственно. Еще вариант - фича PROGRAM, которая приводит к RCE на хосте от имени postgresql, однако для этого нужны права суперюзера в базе данных, чего у нас нет :( По первому варианту читаем флаг.
cicada_web3
Дополнительно приложу авторское решение

MagickMath
#

После достаточно интересных тасков предлагаю отдохнуть и посмотреть на миск со стенда совкомбанк технологий(они проводили стф во время офзона), который судя по названию как то связан с математикой.

Alt Text

Нам дано приложение, которое требует нас ввести прикольную строчку в eval, чтобы на выходе с этого эвала появилось нужное нам число. И так 10 раз подряд.

from random import randint
bad = '0123456789+-*/euioa' # Нельзя эти символы


while True:
	print("Okk, lets do it, ")
	flag = 1

	for i in range(1, 10+1):
		num = randint(1, 100)
		print("I want {}".format(num)) # Вывод запрашиваемого числа
		your = input("Send me, what I want!\n")

		for char in your:
			if not (32 <= ord(char) <= 126): # проверка что символ в читаемом диапозоне
				print("nonono, that isn't good!")
				flag = 0
				break

		for plox in bad: # проверка на присуствие запрещенных символов
			if plox in your:
				print("nonono, that isn't good!")
				flag = 0
				break

		if not flag: break	
		if eval(your) == num: # ну и наконец-то исполнение нашей строчки
			print("good {}".format(i))
		else:
			print("bad.")
			flag = 0
	
	if flag:
		print("SCBCTF{fake_flag_for_testing}")

Судя по коду, ну или же после бесчисленных эксперементов, мы поймем что все плохо( Так как забанены все гласные (кроме буквы “y”, не знаю зачем), использование каких-либо функций в питончике сильно ограничено. Какие-то простые арифметические операции тоже недоступны, как и все цифры, что же делать????

Так как цифры недоступны, надо думать в сторону того, что питончик может превратить в цифру и при каких обстоятельствах, не используя при этом int. Сразу на ум приходит работа с байтами. Но байты, например b"d",перевести в число не используя те же числа у меня не получилось.

Вдруг на ум приходят False и True, которые питон иногда удобно заменяет на 0 и 1. Например True + True = 2. Но нам запрещено использовать +,-,/,*, но никто не запрещал использовать операции >> (битовый сдвиг вправо), << (битовый сдвиг влево), & (побитовое И), | (побитовое ИЛИ). Например True << True << True = 4. Использование этих операций достаточно, чтобы получить любое число, позже покажу как.

Нам осталось решить всего одну проблему, нам не доступно True или False, так как там есть запрещенные символы. И чтобы решить эту проблему будем просто использовать сравнение, например “w” == “w” будет True, а “w” > “w” будет False.

Теперь, когда у нас все есть, будем писать солв:

import pwn


c = pwn.connect("127.0.0.1", 9005) # Подключение к таску

def magick(num): # Функция получения строчки, которая на выходе дает нужное число
    f = bin(num)[3:]
    result = "(" * len(f) + "('w'=='w')"
    for i in f:
        if i == '1':
            result += "<<('w'=='w')|('w'=='w'))"
        else:
            result += "<<('w'=='w')|('w'>'w'))"
    return result

for i in range(10):
    a = c.recvlines(3) 
    num = int(a[1].decode().replace("I want ", "")) # получение числа
    c.send(magick(num).encode()+ b"\n") # отправка нашей прикольной строчки

print(c.recv(1024)) # ну и получаем флаг

Как работает функция magick?
#

Любое число можно представить в виде битов, например нам нужно получить 5 = 0b101. Стартуем с 1, в битовом представлении она так же будет 0b1. Сдвинув на 1 бит влево, получим 0b10, дальше смотрим на второй бит нужного нам числа, и если там 0, делаем побитовое ИЛИ с 0, а если там 1 делаем побитовое ИЛИ с 1. Там ноль, следовательно делаем побитовое ИЛИ с 0, и у нас получается так же 0b10. Снова сдвигаем на 1 и получаем 0b100. Следующее число у нас 1, поэтому делаем побитовое ИЛИ с 1 и получаем 0b101, число которое нам и нужно. Пишем выражение для получение 5 и не забываем про скобки: (1 << 1 | 0) << 1 | 1 Все что остается сделать это заменить 1 на “w” == “w”, а ноль на “w” < “w”. (Можно было бы вообще не делать побитовое ИЛИ при 0, но мне показалось, что так код будет более понятным и простым)

Запускаем сплоит, радуемся флагу и получаем пятерку по математике)

Django Blog
#

django unchained

Описание таска и его идеи
#

На стенде Совкомбанка мы нашли довольно интересный whitebox-таск на веб - он представлял из себя блог, написанный с помощью фреймворка django, в котором пристутствовал функционал создания (доступный только залогиненному пользователю) и просмотра статей, а также существовала возможность входа по токену вместо кредов - эту логику написал автор задания. Еще существали дефолтные форма логина и админка django (django.contrib.auth и django.contrib.admin соотвтественно). Флаг можно было получить на ручке /get_flag/, которая требовала авторизации:

@login_required
def get_flag(request):
    try:
        with open('flag.txt', 'r') as file:
            flag = file.read()

        return HttpResponse(flag, content_type='text/plain')
    ...

Функциионала для регистрации, конечно же, нет (было бы слишком просто .-.) - это означает лишь то, что для решения таска требовалось заполучить учетку админа, ибо других пользователей, судя по коду, нет, а для обращения на ручку с флагом нам нужна абсолютно любая учетка.

Разбираемся с сущностями
#

gnomiq

Для хранения в сервисе использовался sqlite и движок django.db.backends.sqlite3, соотетственно везде применялся orm вместо голых sql-запросов.

Как было описано выше, в приложении присутствовали функционал для просмотра и создания статей - для их хранения использовалась следующая моделька:

class Post(models.Model):
    PUBLIC = 'Public'
    PRIVATE = 'Private'
    STATUS_CHOICES = [
        (PUBLIC, 'Public'),
        (PRIVATE, 'Private'),
    ]

    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default=PUBLIC,
    )

    def __str__(self):
        return self.title

Как видно из примера выше, у класса Post пристутствал атрибут для обращения к дефолтной сущности пользователя User из django.contrib.auth.models.

Еще в сервисе также присутствовала логика для входа в учетку пользователя по токену, который высылается на почту в случае утраты кред. Для хранения таких токенов сущестовала сущность MagicLink:

class MagicLink(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    expires_at = models.DateTimeField()

    def is_valid(self):
        return timezone.now() < self.expires_at

Эта сущность также связана с сущностью пользователя через Foreign Key.

Ищем баги
#

При подробном рассмотрении функционала приложения можно было наткнуться на довольно странную конструкцию фильтрации постов на корневой ручке:

def post_list(request):
    filter_params = request.GET.dict()
    posts = Post.objects.all()

    if filter_params:
        if 'title' in filter_params:
            filter_params['title__contains'] = filter_params['title']
            del filter_params['title']

        posts = posts.filter(**filter_params)

    form = PostSearchForm(request.GET or None)
    return render(request, 'blog/post_list.html', {
        'posts': posts,
        'form': form
    })

Как видно из примера выше, пользователь мог целиком контрольровать содержимое filter_params, передавая произвольные параметры для фильтрации при обращении в БД во время вызова posts = posts.filter(**filter_params). Также конструкция title__contains сразу наталкивает нас на то, что у фильтров в django.db может быть дополнительный функционал, который, вероятно, сможет нам помочь в получении админской учетки.

Как работают фильтры в django.db?
#

Прошерстев документацию django можно было прийти в нескольким наиважнейшим выводам:

  • Во-первых, у атрибутов фильтрации в django есть дополнительные методы - наиболее инетесными из них являются startswith, exact и regex. Например, конструкция вида title__startswith вернет нам лишь те статьи, которые начинаются с переданного паттерна. Наглядный пример: Без фильтрации 5 статей:
    not filtered title
    С фильтрацией всего 3:
    filtered title
  • Во-вторых, через конструкцию вида attribute1__attribute2 можно обращаться к атрибутам атрибутов сущностей, привем цепочка таких обращений не ограничена, а от конечных атрибутов можно также вызывать фильтры из пункта 1. Таким образом, например, можно было обратиться к атрибутам объекта User через конструкцию author__username__exact=admin, ведь объект автора, как было видно ранее, является атрибутом класса Post: Автор админ:
    admin author
    Автор robert_sama (ну и логично, что ничего нет, он же статью не сюда писал, а на mireactf.ru):
    robert sama author

Балагодаря этим двум пунктам можно вытащить, например, хеш админского пароля через конструкцию author__password__startswith, написв сплойт на перебор читаемых символов (покажу чуть позже), однако, пароль в базе хранится в виде хеша pbkdf2_sha256, который тяжело брутится. Это не очень похоже на конечное решение, поэтому продолжаем копать.

Foreign Key moment
#

Для нахождения последнего шага к решению таска мог очень помочь вот этот пост.

Как можно было заметить из кода, атрибут с Foreign Key, вроде как, направлен только в одну сторону от сущности MagicLink до сущности Post, а не наоборот, и только у объекта MagicLink по коду виден явный атрибут для обращения к объекту пользователя, поэтому на первый взгляд может показаться, что достучаться от пользователя до магической ссылки напрямую не представляется возможнвым. Однако под капотом фильтры в django умеют делать такие обращения в обе стороны, несмотря на то, что явно foreign key атрибут явно прописан только для одного из объектов.

Таким образом, через конструкцию author__magiclink__token__startswith=<здесь перебираем> можно было достучаться до адинского токена для авторизации через кривую фильтрацию совершенно других сущностей, написав небольшой эксплойт для перебора. Вытащив токен, можно было залогиниться и получить заветный флаг:)

Спройт можно глянуть тут - тык. Исходники тут - тык.