Разбор демозадачи T-CTF 2025

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Проводим третий T-CTF для ИТ-специалистов, которые хорошо разбираются в коде и кибербезопасности. Соревнования пройдут онлайн и офлайн 19—20 апреля, так что пока есть время подготовиться. В статье расскажем, что нового будет в этом году, и разберем одно демозадание.

Если вы уже ко всему готовы, переходите на сайт мероприятия. Там можно узнать подробности и зарегистрироваться. Новичков в CTF и тех, кто хочет узнать решение интересных задач, приглашаем под кат.

Что такое T-CTF

CTF — соревнования по спортивному хакингу: как олимпиадное программирование, но в информационной безопасности. Команды получают набор заданий на криптографию, анализ скомпилированного кода, веб-уязвимости и не только — на все те направления, с которыми работают профессионалы-безопасники. 

Мы сделали соревнования в двух лигах — Разработки и Безопасности. В Лиге безопасности ждем опытных игроков и профи в информационной безопасности, а в Лиге разработки — опытных инженеров, SRE и QA, аналитиков и других ИТ-специалистов.

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

Капибарам не справиться без помощи ИТ-комьюнити, поэтому мы зовем на T-CTF всех, кто готов примерить на себя белую шляпу!

Как все устроено

Поучаствовать в соревнованиях и попробовать себя в роли белого хакера может любой ИТ-специалист, который хорошо разбирается в коде. Для новичков в CTF мы сделали демозадания — они помогут подготовиться и ближе познакомиться с форматом.

Соревнования проходят 36 часов, онлайн и офлайн в ИТ-хабах Т-Банка. Можно решать задачи все 36 часов или делать перерывы: пить кофе, знакомиться и общаться с другими игроками.

Максим Красножён, старший разработчик в Т-Банк Выгоде

CTF дает возможность на практике применить знания, которые в обычной жизни разработчики не используют: например, можно поломать что-нибудь во благо, получить новый опыт и шанс выиграть призы. Участвовать в CTF интереснее командой: напарник может привнести свое, поделиться идеями и знаниями. Даже если вы команда новичков, вместе сможете добиться большего, чем поодиночке

Федор Красильников, инженер в группе надежности Product & Business Platforms

Я поучаствовал в CTF в 2024 году — для меня это был первый опыт в таких соревнованиях. У ребят из моей команды было больше практики и навыков, поэтому вначале я переживал, что не смогу ничего привнести в командный результат. Но несмотря на недостаток опыта, мне удалось сделать значимый вклад в наше призовое место. Выходные прошли безумно весело и интересно

Поучаствовать офлайн можно в шести городах: Москве, Санкт-Петербурге, Казани, Иннополисе, Екатеринбурге и Новосибирске. На площадках будет все самое необходимое: рабочее место для прохождения соревнований, зоны отдыха, вкусная еда и напитки. 

На T-CTF вас будут ждать задания, связанные с безопасностью веба и приложений, инфраструктурой и криптографией. Лучшие команды получат призы — деньги и капибару. 

В каждой лиге — по три призовых места с денежными призами, которые делятся на команду. В Лиге Разработки максимальный приз — 360 000 ₽, в Лиге Безопасности — 420 000 ₽.

Разбор демозадачи «Капибиржа»

Биржа Capybit подверглась атаке, хакеры получили доступ к активам на 1,46 млрд капибаксов. Правда, они действовали не слишком скрытно, и умная система управления доступом заморозила все средства.

Но администраторы биржи теперь тоже не могут войти в систему, чтобы разморозить активы. Помогите восстановить доступ к деньгам и приведите биржу в порядок, пока котировки $CAPY не обвалились совсем из-за поднявшейся паники. https://t-capybit-kdot8z7j.spbctf.org/ 

Осматриваемся

В этом году на примере демотаска хотим рассказать, как работает бит-флиппинг — атака на шифрование, которая позволяет манипулировать зашифрованными данными, если приложение не позаботилось об их целостности.

Но прежде чем разбираться с криптографией, давайте сначала взглянем, что умеет сайт биржи.

Осматриваемся на сайте биржи
Осматриваемся на сайте биржи

На сайте всего две ручки: регистрация, куда мы вводим имя пользователя, и управление активами, которое нас не пускает из-за нехватки привилегий.

В прошлых разборах демозаданий на безопасность веб-приложений (2023, 2024) мы использовали Burp Suite для работы с HTTP-запросами. Это удобный инструмент, чтобы исследовать веб-аппки, и если раньше вы с ним не работали, посмотрите прошлые разборы. В этот же раз нам хватит и консоли браузера, будем использовать ее.

На вкладке Network в консоли видим, что происходит, когда мы регистрируемся: на сервер в поле username улетает введенное имя пользователя, а в ответ сервер ставит нам кукис с названием session и длинной строчкой внутри.

HTTP-запрос и ответ при регистрации
HTTP-запрос и ответ при регистрации

Экспериментируем с кукой

По кукису sessionприложение нас узнает. Мы ввели логин testuser123 и получили кукис28123600894ec0052e3abfa0039b98ec1efec27e389dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f3e9011fb96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88

Строчка целиком состоит из цифр 0—9 и букв a-f, значит, это хексы — байты, записанные в виде текста по два символа на байт, от 00 до ff.

Раскодируем хексы обратно в байты и посмотрим: может быть, где-то внутри данных прямо текстом лежит наш логин testuser123 или назначенные нам привилегии. 

Декодировать хексы удобно, например, в интерактивной консоли языка Python:

In [1]: b''.fromhex('28123600894ec0052e3abfa0039b98ec1efec27e389dcf65765bc4a5792bc8
   ...: f7a1ba2a6b431f0c7d3ae93c4924a7e804f3e9011fb96c68d141c53dd904c381f40e05e93fa
   ...: 4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0
   ...: d2b200680ccc88')
Out[1]: b'(\x126\x00\x89N\xc0\x05.:\xbf\xa0\x03\x9b\x98\xec\x1e\xfe\xc2~8\x9d\xcfev
[\xc4\xa5y+\xc8\xf7\xa1\xba*kC\x1f\x0c}:\xe9<I$\xa7\xe8\x04\xf3\xe9\x01\x1f\xb9lh\x
d1A\xc5=\xd9\x04\xc3\x81\xf4\x0e\x05\xe9?\xa4\x18\r\xc9\xa5\xdc\xc1\x83\x81\xdf\\\x
d9\x01\xc5\x86\xd1\x07Z\xe1\x11\xdb\x0b\xdf\xf1\xf7\xe1$w\xf6\xeaz\xc9\xe6\xfeY\x15
+\xd0\xd2\xb2\x00h\x0c\xcc\x88'

Мы получили нечитаемую мешанину. Большинство байтов вообще не обозначают символ текста, такие байты Python показывает нам снова в виде их хекс-значений: например, в начале строчки хексы 12 раскодировались в байт \x12. И раз в хексах мусор, данные внутри кукиса зашифрованы либо сжаты — это два преобразования, которые превращают данные в трэш.

Попробуем регистрироваться под разными логинами, чтобы собрать побольше информации:

username

Кукис session

testuser123

28123600894ec0052e3abfa0039b98ec1efe**c27e38**9dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f3e9011fb96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88

testuser234

28123600894ec0052e3abfa0039b98ec1efe**c17f3f**9dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f3e9011fb96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88

testuser234567890

28123600894ec0052e3abfa0039b98ec1efe**c17f3f8ad5726c0380**f3276288e3a1ab2b201b4a033a3ba46a0c7dbfba49eaff4657bb623d8047db7a9456808cfb0e46a17ef85748c9ea9287ddd48e5dd21195c982074df056cd0b89e7e7f46925a0e37ad7f3ae17586ccdc7fc5f3b0cc294afb1e428170c

Находить различия удобно, переключаясь по вкладкам
Находить различия удобно, переключаясь по вкладкам

Какие закономерности можно увидеть:

  1. Когда мы поменяли в логине три символа (123 → 234), в куке поменялись три байта (шесть хексов: c27e38 → c17f3f), а все остальные байты остались прежними.

  2. Каждый байт поменялся несильно: c2 → c1, 7e → 7f, 38 → 3f.

  3. Когда мы увеличили логин на +6 символов, кукис вырос на 6 байтов (12 хексов), при этом начало всей куки осталось тем же, но конец полностью изменился.

Пришло время попробовать самим что-нибудь поменять в куке и посмотреть, как на это отреагирует приложение. Возьмем последний длинный кукис и слегка поменяем хексы в месте, которое менялось при изменении логина: c17f3f8ad5726c0380 → c0703080d070600080

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

Вот наша новая кука с нулями в несколько байтов, скормим ее приложению: 28123600894ec0052e3abfa0039b98ec1efec0703080d070600080f3276288e3a1ab2b201b4a033a3ba46a0c7dbfba49eaff4657bb623d8047db7a9456808cfb0e46a17ef85748c9ea9287ddd48e5dd21195c982074df056cd0b89e7e7f46925a0e37ad7f3ae17586ccdc7fc5f3b0cc294afb1e428170c

Подставляем подменную куку на вкладке Application → Cookies и смотрим, что произошло:

Наш логин поменялся с testuser234567890 на testuser3<;?354:0 — мы смогли, вслепую изменив несколько мусорных непонятных байтов, повлиять на то, под каким логином нас узнает приложение!

Пора разбираться, как это работает под капотом и как с помощью этого поднять привилегии.

XOR, поточное шифрование и бит-флиппинг

Когда разработчик не хочет, чтобы какие-то данные мог прочитать или подменить тот, у кого они хранятся, он эти данные шифрует. Кукис session хранится у нас в браузере, и он зашифрован.

В алгоритмах шифрования часто используется операция XOR — она складывает два бита (A^B), получая бит на выходе (C):

A   B     C
0 ^ 0  => 0
0 ^ 1  => 1
1 ^ 0  => 1
1 ^ 1  => 0

В шифровании обычно ксорят биты данных на секретные биты, которые получаются из ключа шифрования. Ксор здесь удобен за счет того, что он:

  1. Работает на уровне отдельных битов — можно зашифровать сколь угодно маленькое или большое количество данных.

  2. Не дает никакой информации без знания ключа шифрования — зашифрованный бит «1» мог получиться при XOR бита данных «1» с битом ключа «0», а мог — при данных «0» с ключом «1». Без знания ключа не получится с немного большей вероятностью предположить, был бит данных 0 или 1. Операцию A^B можно также представить как инструкцию, какие биты в A поменять, а какие оставить: там, где бит у B «1» — переключаем бит у A, где бит у B «0» — не меняем.

  A  01001000 01100101 01101100 01101100 01101111    # "Hello"
^ B  01101011 00110011 01111001 00100001 00100001    # "k3y!!"
 =>  00100011 01010110 00010101 01001101 01001110    # "#V MN"

3. XOR обратим — чтобы получить исходные данные, достаточно просто поксорить еще раз на те же секретные биты — переключить биты данных в тех же местах.

  A  00100011 01010110 00010101 01001101 01001110    # "#V MN"
^ B  01101011 00110011 01111001 00100001 00100001    # "k3y!!"
 =>  01001000 01100101 01101100 01101100 01101111    # "Hello"

Целый класс алгоритмов шифрования сводится к этому простому действию: данные ксорятся на секретный поток битов. Это все поточные шифры (RC4, Chacha20) и блочные шифры в определенных режимах (AES-CTR, AES-OFB). Они работают одинаково: из секретного ключа генерируется секретный поток бит, этими битами накрываются данные.

Мы не знаем ключ шифрования, который используется в бирже Capybit, и видим только шифрованное сообщение — для нас ситуация выглядит так:

   userinfo  ******** ******** ******** ******** ******** ...   # *****
^ keystream  ???????? ???????? ???????? ???????? ???????? ...   # ?????
  => cookie  00101000 00010010 00110110 00000000 10001001 ...   # 28 12 36 00 89
             ↑↑↑ это мы видим в кукисе session
При заходе на сайт, приложение снова расшифровывает кукис:
     cookie  00101000 00010010 00110110 00000000 10001001 ...   # 28 12 36 00 89
^ keystream  ???????? ???????? ???????? ???????? ???????? ...   # ?????
=> userinfo  ******** ******** ******** ******** ******** ...   # *****

Свойства ксора сработали как надо: мы действительно не можем сказать ничего про содержимое нешифрованных данных, потому что не знаем ни бита из секретного потока.

Но целостность данных не защищена: мы можем менять (флипать) биты в шифрованном кукисе — и ровно этот же бит будет флипаться в расшифровываемых данных!

   original  00101000 00010010 00110110 00000000 10001001 ...   # 28 12 36 00 89
    flipped  00101000 00010010 00110110 00000100 10001001 ...   # 28 12 36 04 89
^ keystream  ???????? ???????? ???????? ???????? ???????? ...   # ?????
=> userinfo  ******** ******** ******** *****%** ******** ...   # ***%*

Именно это произошло, когда мы изменили в куке c17f3f8ad5726c0380 → c0703080d070600080 и получили смену логина testuser234567890 → testuser3<;?354:0 :

   userinfo  ... 00110010 00110011 00110100 00110101 00110110 ...   # user"23456"...
^ keystream  ... ???????? ???????? ???????? ???????? ???????? ...   # ?????
  => cookie  ... 11000001 01111111 00111111 10001010 11010101 ...   # c1 7f 3f 8a d5
Изменили в куке некоторые символы на нули:
    flipped  ... 11000000 01110000 00110000 10000000 11010000 ...   # c0 70 30 80 d0
^ keystream  ... ???????? ???????? ???????? ???????? ???????? ...   # ?????
=> userinfo  ... 00110011 00111100 00111011 00111111 00110011 ...   # user"3<;?3"...
            Эти же биты ↑ флипнулись в ↑↑↑ данных ↑       ↑ ↑

Мы все еще не можем расшифровать все данные в куке, но по реакции приложения на наш бит-флиппинг получили полезную информацию. Байт в кукисе, который мы поменяли с c1 на c0, был символом 2 из логина user234567890 в оригинальных нешифрованных данных и стал3, когда мы флипнули ему младший бит. Мы знаем, где в кукисе логин — если бы нам нужно было подделать логин, мы бы знали, у каких байтов флипать биты, чтобы на него влиять.

Но нам нужно подделать привилегии для доступа к управлению активами:

Бит-флипаем кукис

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

Сделаем это скриптом на Python, который попробует поменять младший бит у каждого байта кукиса и сходит с получившейся кукой на ручку управления активами:

#!/usr/bin/python3
import requests
sess = requests.Session()

cookie = bytearray(b''.fromhex("28123600894ec0052e3abfa0039b98ec1efec27e389dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f3e9011fb96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88"))

for i in range(len(cookie)):
    print(f'Position {i:3d}\t', end='')
    flipped = cookie[:]
    flipped[i] ^= 0b00000001  # флипаем у i-го байта младший бит — ксором на байт, у которого 1 только в младшем бите
    res = sess.get("https://t-capybit-kdot8z7j.spbctf.org/funds", cookies={"session": flipped.hex()})
    print(res.text)

Запускаем и смотрим на реакцию приложения:

root@vosus:/mnt/f# ./flipper.py
Position   0    JSON parsing error: Expecting value: line 1 column 1 (char 0)
Position   1    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
Position   2    rule role = 'root' not satisfied, your role = 'user'
Position   3    rule role = 'root' not satisfied, your role = 'user'
Position   4    rule role = 'root' not satisfied, your role = 'user'
Position   5    rule role = 'root' not satisfied, your role = 'user'
Position   6    JSON parsing error: Expecting ':' delimiter: line 1 column 11 (char 10)
Position   7    JSON parsing error: Expecting ':' delimiter: line 1 column 8 (char 7)
Position   8    JSON parsing error: Expecting value: line 1 column 9 (char 8)
Position   9    JSON parsing error: Expecting value: line 1 column 10 (char 9)
Position  10    rule role = 'root' not satisfied, your role = 'user'
<....>
Position  43    rule role = 'root' not satisfied, your role is not set
Position  44    JSON parsing error: Expecting ':' delimiter: line 1 column 49 (char 48)
Position  45    JSON parsing error: Expecting ':' delimiter: line 1 column 46 (char 45)
Position  46    JSON parsing error: Expecting value: line 1 column 47 (char 46)
Position  47    JSON parsing error: Expecting value: line 1 column 48 (char 47)
Position  48    rule role = 'root' not satisfied, your role = 'tser'
Position  49    rule role = 'root' not satisfied, your role = 'urer'
Position  50    rule role = 'root' not satisfied, your role = 'usdr'
Position  51    rule role = 'root' not satisfied, your role = 'uses'
Position  52    JSON parsing error: Expecting ',' delimiter: line 1 column 57 (char 56)
Position  53    JSON parsing error: Expecting ',' delimiter: line 1 column 54 (char 53)
Position  54    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 55 (char 54)
Position  55    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 56 (char 55)
Position  56    rule role = 'root' not satisfied, your role = 'user'
<....>

Богатый набор реакций! Что мы узнали:

  1. В оригинальных данных лежит JSON.

  2. На позиции [1] двойная кавычка ", на [6] :, на [52] , — похоже, зашифрован JSON-объект вида {"field":"value","field":1234}.

  3. На позициях [48…51] лежит наша роль user! Мы прошлись по ней флипами младшего бита: tser urer usdr uses, а приложение ругается, что у нас должна быть роль root.

Флипнем биты на позициях [48…51] так, чтобы из user там стал root. Можно посчитать вручную, какие биты для этого нужно флипнуть:

     cookie  11110011 11101001 00000001 00011111   # f3 e9 01 1f
   original  01110101 01110011 01100101 01110010   # "user"
Сравниваем оригинал с `root` и смотрим, какие биты менять:
     "root": 01110010 01101111 01101111 01110100   # "root"
Меняем эти биты в кукисе:
    flipped  11110100 11110101 00001011 00011001   # f4 f5 0b 19

Можно вычислить проще — поксорить то, что было (user), с тем, что нужно (root):

In [2]: xor = lambda A, B: bytes([a^b for a, b in zip(A, B)])  # вспомогательная функция, чтобы поксорить две строчки байт
   ...: cookie = b''.fromhex("f3e9011f")  # фрагмент изначального кукиса
   ...: flip = xor(b'user', b'root')  # ксорим "user" на "root" — получаем битовую разницу между ними
   ...: flipped = xor(cookie, flip)  # ксорим шифрованные данные на эту разницу — флипаем в них соответствующие биты
   ...: flipped.hex()
Out[2]: 'f4f50b19'  # получили те же модифицированные данные, что до этого руками

Меняем в куке f3e9011f → f4f50b19 и смотрим на реакцию приложения:

Отлично! Мы прошли проверку роли, успешно бит-флипнув себе user → root. Теперь приложение проверяет следующие условия. Зарядим новую куку с рутовой ролью в скрипт, который пробует флипать каждый байт, в надежде нащупать поле clearance:

Position  54    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 55 (char 54)
Position  55    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 56 (char 55)
Position  56    rule clearance >= 5 not satisfied, clearance must be an integer
Position  57    rule clearance >= 5 not satisfied, clearance must be an integer
Position  58    rule clearance >= 5 not satisfied, clearance must be an integer
Position  59    rule clearance >= 5 not satisfied, clearance must be an integer
Position  60    rule clearance >= 5 not satisfied, clearance must be an integer
Position  61    rule clearance >= 5 not satisfied, clearance must be an integer
Position  62    rule clearance >= 5 not satisfied, clearance must be an integer
Position  63    rule clearance >= 5 not satisfied, clearance must be an integer
Position  64    rule clearance >= 5 not satisfied, clearance must be an integer
Position  65    JSON parsing error: Expecting ':' delimiter: line 1 column 73 (char 72)
Position  66    JSON parsing error: Expecting ':' delimiter: line 1 column 67 (char 66)
Position  67    JSON parsing error: Expecting value: line 1 column 68 (char 67)
Position  68    rule clearance >= 5 not satisfied, your clearance = 3
Position  69    JSON parsing error: Expecting ',' delimiter: line 1 column 70 (char 69)

На позициях [56…64] ошибка говорит, что уровень допуска вообще отсутствует — похоже, в этом месте в JSON находится само слово "clearance", которое мы ломаем флиппингом.

На позиции [68] уровень допуска вместо 2 становится 3 — ожидаемо, вот где лежит значение этого параметра, наш скрипт флипает младший бит, b'2' ^ b'3' == 0b00000001.

Ксорим байт куки в позиции [68] на b'2' ^ b'5':

<....>
cookie = bytearray(b''.fromhex("28123600894ec0052e3abfa0039b98ec1efec27e389dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f4f50b19b96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88"))
xor = lambda A, B: bytes([a^b for a, b in zip(A, B)])
cookie[68:68+1] = xor(cookie[68:68+1], xor(b'2', b'5'))
<....>

Запускаем снова:

Position   2    rule untrusted = false not satisfied, your untrusted = true

Position  69    JSON parsing error: Expecting ',' delimiter: line 1 column 70 (char 69)
Position  70    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 71 (char 70)
Position  71    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 72 (char 71)
Position  72    rule untrusted = false not satisfied, your untrusted is not set
Position  73    rule untrusted = false not satisfied, your untrusted is not set
Position  74    rule untrusted = false not satisfied, your untrusted is not set
Position  75    rule untrusted = false not satisfied, your untrusted is not set
Position  76    rule untrusted = false not satisfied, your untrusted is not set
Position  77    rule untrusted = false not satisfied, your untrusted is not set
Position  78    rule untrusted = false not satisfied, your untrusted is not set
Position  79    rule untrusted = false not satisfied, your untrusted is not set
Position  80    rule untrusted = false not satisfied, your untrusted is not set
Position  81    JSON parsing error: Expecting ':' delimiter: line 1 column 92 (char 91)
Position  82    JSON parsing error: Expecting ':' delimiter: line 1 column 83 (char 82)
Position  83    JSON parsing error: Expecting value: line 1 column 84 (char 83)
Position  84    JSON parsing error: Expecting value: line 1 column 85 (char 84)
Position  85    JSON parsing error: Expecting value: line 1 column 85 (char 84)
Position  86    JSON parsing error: Expecting value: line 1 column 85 (char 84)
Position  87    JSON parsing error: Expecting value: line 1 column 85 (char 84)
Position  88    JSON parsing error: Expecting ',' delimiter: line 1 column 89 (char 88)
Position  89    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 90 (char 89)
Position  90    JSON parsing error: Expecting property name enclosed in double quotes: line 1 column 91 (char 90)

По этим ошибкам мы можем узнать, что:

В данных содержится "untrusted":true, а нам нужно сделать false.

Судя по тому, что приложение:

  • ругается на запятую на позиции [69]

  • ожидает появления кавычки на [70…71]

  • ломает название поля «untrusted» на [72…80] 

  • ожидает двоеточие на [82], а на [81] — видимо, ломается вторая кавычка

  • ожидает значения поля на [83…87]

  • снова встречает запятую на [88] 

данные имеют вид , "untrusted": true, " с позиции [69].

Чтобы нафлипать себе false, превратим в него true вместе с пробелом (позиции [83…87]):

cookie[83:87+1] = xor(cookie[83:87+1], xor(b' true', b'false'))

Запускаем:

Position   2    rule allow-flag = 'yes' not satisfied, your allow-flag is not set

Судя по сообщению, поля allow-flag не существует — нам не у чего флипать значение.

Расшифровываем кукис целиком

Воспользуемся еще одним трюком: посмотрим, что будет, если флипать не один младший бит (^ 0b00000001), а сразу все биты байта (^ 0b11111111)

flipped[i] ^= 0b11111111  # флипаем у i-го байта все биты
root@vosus:/mnt/f# ./flipper.py
Position   0    Decryption error: 'utf-8' codec can't decode byte 0x84 in position 0: invalid start byte
Position   1    Decryption error: 'utf-8' codec can't decode byte 0xdd in position 1: invalid continuation byte
Position   2    Decryption error: 'utf-8' codec can't decode byte 0x8a in position 2: invalid start byte
Position   3    Decryption error: 'utf-8' codec can't decode byte 0x8c in position 3: invalid start byte
Position   4    Decryption error: 'utf-8' codec can't decode byte 0x9a in position 4: invalid start byte
Position   5    Decryption error: 'utf-8' codec can't decode byte 0x8d in position 5: invalid start byte
Position   6    Decryption error: 'utf-8' codec can't decode byte 0xdd in position 6: invalid continuation byte
Position   7    Decryption error: 'utf-8' codec can't decode byte 0xc5 in position 7: invalid continuation byte
Position   8    Decryption error: 'utf-8' codec can't decode byte 0xdf in position 8: invalid continuation byte
Position   9    Decryption error: 'utf-8' codec can't decode byte 0xdd in position 9: invalid continuation byte
Position  10    Decryption error: 'utf-8' codec can't decode byte 0x8b in position 10: invalid start byte
Position  11    Decryption error: 'utf-8' codec can't decode byte 0x9a in position 11: invalid start byte
Position  12    Decryption error: 'utf-8' codec can't decode byte 0x8c in position 12: invalid start byte

Теперь одинаковая ошибка возникает при изменении любого байта и еще до парсинга JSON — на этапе декодирования кодировки UTF-8, которая обычно используется в вебе. В UTF-8 буквы латинского алфавита, цифры, знаки препинания совпадают с кодировкой ASCII и кодируются одним байтом от 00 до 7f, а вот вторая половина байтов (80—ff) занята под символы из нескольких байтов (буквы других алфавитов, значки, эмодзи и так далее.)

JSON целиком состоит из обычных латинских символов, и у них старший бит всегда 0: 0x00 == 0b00000000, 0x7f == 0b01111111. Когда мы флипаем у такого символа все биты, мы всегда получим байт из второй половины: 0x80 == 0b10000000, 0xff == 0b11111111.

Байты из второй половины используются в UTF-8 для символов из нескольких байтов и никогда не стоят по одиночке, поэтому декодер ругается, встретив такой одиночный байт. И даже говорит, какой именно байт ему попался!

Возьмем те байты, что нам сообщает декодер, и флипнем у них все биты обратно:

In [3]: chr(0x84 ^ 0b11111111)
Out[3]: '{'
In [4]: chr(0xdd ^ 0b11111111)
Out[4]: '"'
In [5]: chr(0x8a ^ 0b11111111)
Out[5]: 'u'
In [6]: chr(0x8c ^ 0b11111111)
Out[6]: 's'
In [7]: chr(0x9a ^ 0b11111111)
Out[7]: 'e'
In [8]: chr(0x8d ^ 0b11111111)
Out[8]: 'r'

Декодер UTF-8 слил нам байты из незашифрованного кукиса, потому что не смог их распарсить. Модифицируем скрипт, чтобы он брал сообщения от декодера и выводил нам все байты:

#!/usr/bin/python3
import requests
sess = requests.Session()

cookie = bytearray(b''.fromhex("28123600894ec0052e3abfa0039b98ec1efec27e389dcf65765bc4a5792bc8f7a1ba2a6b431f0c7d3ae93c4924a7e804f4f50b19b96c68d141c53dd904c381f40e05e93fa4180dc9a5dcc18381df5cd901c586d1075ae111db0bdff1f7e12477f6ea7ac9e6fe59152bd0d2b200680ccc88"))
xor = lambda A, B: bytes([a^b for a, b in zip(A, B)])
cookie[68:68+1] = xor(cookie[68:68+1], xor(b'2', b'5'))
cookie[83:87+1] = xor(cookie[83:87+1], xor(b' true', b'false'))

for i in range(len(cookie)):
    flipped = cookie[:]
    flipped[i] ^= 0b11111111  # флипаем у i-го байта все биты
    res = sess.get("https://t-capybit-kdot8z7j.spbctf.org/funds", cookies={"session": flipped.hex()})
    leaked = res.text[res.text.find('0x'):][:4]  # берём байт 0x** из сообщения декодера utf-8
    leaked = int(leaked, 16) ^ 0b11111111  # флипаем биты обратно
    print(chr(leaked), end='', flush=True)
root@vosus:/mnt/f# ./flipper.py
{"user": "testuser123", "attributes": {"role": "root", "clearance": 5, "untrusted":false, "department": "sales"}}

Теперь мы знаем целиком содержимое куки, на какой позиции какой текст находится в данных, и можем легко нафлипать себе последний оставшийся элемент привилегий "allow-flag":"yes" — например, на месте ненужного "department": "sales"

Источник: https://habr.com/ru/companies/tbank/articles/891944/


Интересные статьи

Интересные статьи

Учёные Томского политехнического университета придумали новый способ обработки лазерно-индуцированных композитов на основе оксида графена и полимера. Он даёт возможность проектировать и динамически из...
Привет, Хабр!Представляю вам свою первую статью о программировании на Rust. Я только начинаю изучать этот язык, и в качестве первого задания выбрал написание генератора паролей.Процесс разработки кон...
Битрикс24 — корпоративное SaaS-решение (Software as a Service, программное обеспечение как услуга), которым пользуются компании разного масштаба и профиля для коммуникации между сотрудниками, хран...
Один из ключевых сценариев работы в CRM это общение с клиентом в удобном для него канале. По почте, по телефону, по SMS или в мессенджере. Особенно выделяется WhatsApp — интеграцию с ...
С версии 12.0 в Bitrix Framework доступно создание резервных копий в автоматическом режиме. Задание параметров автоматического резервного копирования производится в Административной части на странице ...