📜 ⬆️ ⬇️

How to hack more than 17,000 sites in one night

This story is about how I found a vulnerability in the Webasyst framework and, in particular, in the e-commerce engine Shop-Script 7.


It all started with the fact that in the evening I decided to get a merch of a Russian-language rap artist. After payment I received a letter containing a link to the details of my order:
View order information:
https: //o***yshop.com/my/order/21311/9fe684d6508769ef213111ed917d1cce94088/
PIN: 3302
(note: order id has been modified for publication)

The order identifier immediately occurring in the middle of the hash string:
')
9fe684d6508769ef 21311 1ed917d1cce94088

I wondered how this line is generated, and for this it was necessary to look at the source of the engine. Having studied the html source of the page, I found out which engine is being used in the store, and a little googling I found where to download it .

We study the source code


It turned out that this string is generated randomly and no pattern can be traced. But by chance, while I was looking for a function responsible for this hash, I came across some rather curious sections of code.

wa-system / contact / waContact.class.php

The save ($ data, $ validate) function - be careful, a lot of code!
/** * 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; } 


The $ data parameter contains data in the format 'field name' => 'field value' , in the function I did not notice any protection from Mass Assignment , but did not rule out that the filtering of the argument occurs before calling the function itself. I became lazy to look through all the places in the code where save () is called and I decided to test the theory experimentally.

Having installed the engine on LAN, first of all I decided to look at the structure of the `wa_contact` table.

Table structure wa_contact`


In order for a user to have access to the admin panel (in the engine it is called a “backend”), the buyer must have the login, password fields set, and the is_user field must be equal to 1.

We are testing


Add the item to the cart, go to the checkout page, fill in the standard fields ... and it's time to add new ones:



We send a request, try to go with our data to the admin area (/ wa / webasyst /). Authorization is successful, but ... admin page is completely empty: we do not have any rights. I frantically search for a field in the table that is responsible for access rights and I understand that there is no such field, and all the rights are in a separate table as appropriate.



I have almost come to terms with the fiasco, until I noticed that the table `wa_contact_rights` contains the rights for users by id and for groups by id with a minus sign. An idea immediately occurred to us to assign a negative id to our user, thereby obtaining the rights of a group. No sooner said than done, change customer [id] to -1 by analogy with how we changed the other parameters earlier. Again, log in to the admin and get all the rights that are available to the "Administrators" group.



What we have in the end


Vulnerability allowing on any website on this framework, in any online store on this engine to get full administrator rights, which in turn allow, for example, to receive confidential information about all orders and customers, change the status of orders (say, mark them paid) and just change the website settings.

There are no conditions for the use of the vulnerability, it also works when registration on the site is disabled (in fact, registration does occur at the time of ordering).

According to PublicWWW, more than 17,000 sites use this framework .

The vulnerability was reported more than two months ago, the sites were updated and no one was hurt.

Chronology of events:

August 8, 10:30 pm - bought a t-shirt
August 9, 08:00 - reported on the Webasyst vulnerability, attached a video with the Proof of Concept
August 9, 13:00 - received confirmation from the support service
August 14 - received a reward, the vulnerability was closed
September 11 - got the go-ahead to publish this article.

Source: https://habr.com/ru/post/340066/


All Articles