- PVSM.RU - https://www.pvsm.ru -
Эта история о том, как я нашел уязвимость в фреймворке Webasyst и, в частности, в ecommerce-движке Shop-Script 7.
Все началось с того, что вечером я решил приобрести мерч русскогоязычного рэп-исполнителя. После оплаты мне пришло письмо, содержащее ссылку на детали моего заказа:
Просмотр информации о заказе:
https://o***yshop.com/my/order/21311/9fe684d6508769ef213111ed917d1cce94088/ [1]
PIN: 3302
(примечание: ID заказа был видоизменен для публикации)
В глаза сразу бросился идентификатор заказа, встречающийся в середине строки хеша:
9fe684d6508769ef213111ed917d1cce94088
Мне стало интересно, как генерируется эта строка, а для этого нужно было взглянуть на исходники движка. Изучив html source страницы, я узнал какой движок используется в магазине, а немного погуглив нашел где его скачать [2].
Оказалось, что данная строка генерируется случайно и никакой закономерности не прослеживается. Но волею случая, пока я искал функцию отвечающую за этот хеш, я наткнулся на довольно любопытные участки кода.
wa-system/contact/waContact.class.php
/**
* Saves contact's data to database.
*
* @param array $data Associative array of contact property values.
* @param bool $validate Flag requiring to validate property values. Defaults to false.
* @return int|array Zero, if saved successfully, or array of error messages otherwise
*/
public function save($data = array(), $validate = false)
{
$add = array();
foreach ($data as $key => $value) {
if (strpos($key, '.')) {
$key_parts = explode('.', $key);
$f = waContactFields::get($key_parts[0]);
if ($f) {
$key = $key_parts[0];
if ($key_parts[1] && $f->isExt()) {
// add next field
$add[$key] = true;
if (is_array($value)) {
if (!isset($value['value'])) {
$value = array('ext' => $key_parts[1], 'value' => $value);
}
} else {
$value = array('ext' => $key_parts[1], 'value' => $value);
}
}
}
} else {
$f = waContactFields::get($key);
}
if ($f) {
$this->data[$key] = $f->set($this, $value, array(), isset($add[$key]) ? true : false);
} else {
if ($key == 'password') {
$value = self::getPasswordHash($value);
}
$this->data[$key] = $value;
}
}
$this->data['name'] = $this->get('name');
$this->data['firstname'] = $this->get('firstname');
$this->data['is_company'] = $this->get('is_company');
if ($this->id && isset($this->data['is_user'])) {
$c = new waContact($this->id);
$is_user = $c['is_user'];
$log_model = new waLogModel();
if ($this->data['is_user'] == '-1' && $is_user != '-1') {
$log_model->add('access_disable', null, $this->id, wa()->getUser()->getId());
} else if ($this->data['is_user'] != '-1' && $is_user == '-1') {
$log_model->add('access_enable', null, $this->id, wa()->getUser()->getId());
}
}
$save = array();
$errors = array();
$contact_model = new waContactModel();
foreach ($this->data as $field => $value) {
if ($field == 'login') {
$f = new waContactStringField('login', _ws('Login'), array('unique' => true, 'storage' => 'info'));
} else {
$f = waContactFields::get($field, $this['is_company'] ? 'company' : 'person');
}
if ($f) {
if ($f->isMulti() && !is_array($value)) {
$value = array($value);
}
if ($f->isMulti()) {
foreach ($value as &$val) {
if (is_string($val)) {
$val = trim($val);
} else if (isset($val['value']) && is_string($val['value'])) {
$val['value'] = trim($val['value']);
} else if ($f instanceof waContactCompositeField && isset($val['data']) && is_array($val['data'])) {
foreach ($val['data'] as &$v) {
if (is_string($v)) {
$v = trim($v);
}
}
unset($v);
}
}
unset($val);
} else {
if (is_string($value)) {
$value = trim($value);
} else if (isset($value['value']) && is_string($value['value'])) {
$value['value'] = trim($value['value']);
} else if ($f instanceof waContactCompositeField && isset($value['data']) && is_array($value['data'])) {
foreach ($value['data'] as &$v) {
if (is_string($v)) {
$v = trim($v);
}
}
unset($v);
}
}
if ($validate !== 42) { // this deep dark magic is used when merging contacts
if ($validate) {
if ($e = $f->validate($value, $this->id)) {
$errors[$f->getId()] = $e;
}
} elseif ($f->isUnique()) { // validate unique
if ($e = $f->validateUnique($value, $this->id)) {
$errors[$f->getId()] = $e;
}
}
}
if (!$errors && $f->getStorage()) {
$save[$f->getStorage()->getType()][$field] = $f->prepareSave($value, $this);
}
} elseif ($contact_model->fieldExists($field)) {
$save['waContactInfoStorage'][$field] = $value;
} else {
$save['waContactDataStorage'][$field] = $value;
}
}
// Returns errors
if ($errors) {
return $errors;
}
$is_add = false;
// Saving to all storages
try {
if (!$this->id) {
$is_add = true;
$storage = 'waContactInfoStorage';
if (wa()->getEnv() == 'frontend') {
if ($ref = waRequest::cookie('referer')) {
$save['waContactDataStorage']['referer'] = $ref;
$save['waContactDataStorage']['referer_host'] = parse_url($ref, PHP_URL_HOST);
}
if ($utm = waRequest::cookie('utm')) {
$utm = json_decode($utm, true);
if ($utm && is_array($utm)) {
foreach ($utm as $k => $v) {
$save['waContactDataStorage']['utm_'.$k] = $v;
}
}
}
}
$this->id = waContactFields::getStorage($storage)->set($this, $save[$storage]);
unset($save[$storage]);
}
foreach ($save as $storage => $storage_data) {
waContactFields::getStorage($storage)->set($this, $storage_data);
}
$this->data = array();
$this->removeCache();
$this->clearDisabledFields();
wa()->event(array('contacts', 'save'), $this);
} catch (Exception $e) {
// remove created contact
if ($is_add && $this->id) {
$this->delete();
$this->id = null;
}
$errors['name'][] = $e->getMessage();
}
return $errors ? $errors : 0;
}
Параметр $data содержит данные в формате ‘название поля’ => ‘значение поля’, в функции я не заметил защиты от Mass Assignment (https://cwe.mitre.org/data/definitions/915.html [3]), но не исключал, что фильтрация аргумента происходит до вызова самой функции. Мне стало лениво просматривать все места в коде, где вызывается save() и я решил проверить теорию экспериментальным путем.
Установив на локалку движок, первым делом я решил посмотреть структуру таблицы `wa_contact`.
Чтобы пользователь имел доступ к админ-панели (в движке она называется «бэкэндом») у покупателя должны быть заданы поля `login`, `password`, а поле `is_user` должно быть равно 1.
Добавляем товар в корзину, переходим на страницу оформления заказа, заполняем стандартные поля… и самое время добавить новые:
Отправляем запрос, пробуем зайти с нашими данными в админку (/wa/webasyst/). Авторизация проходит успешно, но… страница админки совершенно пустая: у нас нет никаких прав. Судорожно ищу поле в таблице, отвечающее за права доступа и понимаю, что такого поля нет, а все права как и подобает вынесены в отдельную таблицу.
Я уже почти смирился с фиаско, пока не заметил, что таблица `wa_contact_rights` содержит права для пользователей по id и для групп по id со знаком минус. В голову сразу же пришла идея присвоить нашему пользователю отрицательный id, тем самым получив права группы. Сказано – сделано, меняем customer[id] на -1 по аналогии с тем, как мы меняли остальные параметры ранее. Опять авторизуемся в админке и получаем все права, которые доступны группе «Администраторы».
Уязвимость, позволяющую на любом сайте на этом фреймворке, в любом интернет-магазине на этом движке получить полные права администратора, которые в свою очередь позволяют, например, получить конфединциальную информацию обо всех заказах и покупателях, менять статус заказов (скажем, помечать их оплаченными) и просто изменять настройки веб-сайта.
Условия для использования уязвимости нет, она работает и при отключенной регистрации на сайте (потому что по факту, при оформлении заказа регистрация все же происходит).
По данным PublicWWW более 17 000 сайтов используют данный фреймворк [4].
Об уязвимости было сообщено более двух месяцев назад, сайты обновились и никто не пострадал.
Хронология событий:
8 августа, 22:30 – купил футболку
9 августа, 08:00 – сообщил об уязвимости Webasyst, прикрепил видео с Proof of Concept
9 августа, 13:00 – получил подтверждение от службы поддержки
14 августа – получил вознаграждение, уязвимость была закрыта [5]
11 сентября – получил добро на публикацию данной статьи
Автор: Дмитрий Соболев
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/uyazvimost/265644
Ссылки в тексте:
[1] https://o***yshop.com/my/order/21311/9fe684d6508769ef213111ed917d1cce94088/ : #
[2] скачать: http://www.shop-script.ru/platform/download/
[3] https://cwe.mitre.org/data/definitions/915.html: https://cwe.mitre.org/data/definitions/915.html
[4] данный фреймворк: https://publicwww.com/websites/%2B%22%2Fwa-data%2Fpublic%2F%22/
[5] была закрыта: https://github.com/webasyst/webasyst-framework/commit/c6b96ab96720361f6144df7bf26b943a808e3e98
[6] Источник: https://habrahabr.ru/post/340066/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.