猫おいしいです(^o^)

個人的な記録および備忘録

高専SECCON2018 参加記録 Write-Up

高専セキュリティコンテスト2018に参加しました。
多くのwrite-upが既に上がっていますが、折角参加したので残しておこうと思います。

はじめに

僕は”おやまようちえん(おやまどうぶつえん)”というチームで参加しました。
結果は7位でした。僕自身CTF初心者なので10位以内に入れてよかったです。次参加するとしたらもう少しweb問題を練習しておきます。。。

[Binary 100]ログインしたい!

ltraceを実行するとstrcmpでパスワードを比較していることがわかる。

$ ltrace ./login
__libc_start_main(0x565f165d, 1, 0xffdd6b94, 0x565f1840 <unfinished ...>
puts("hoge\351\253\230\345\260\202 \346\210\220\347\270\276\347\256\241\347\220\206\343\202\267\343\202\271\343\203\206"...hoge高専 成績管理システム version 1.0
) = 48
printf("\343\203\255\343\202\260\343\202\244\343\203\263\343\203\221\343\202\271\343\203\257\343\203\274\343\203\211: ") = 29
fgets(ログインパスワード: abcdef
"abcdef\n", 128, 0xf7f8e5a0)               = 0xffdd69cc
strtok("abcdef\n", "\n")                         = "abcdef"
strcmp("abcdef", "SCKOSEN{P@SSW0RD!}")           = 1
puts("\343\203\221\343\202\271\343\203\257\343\203\274\343\203\211\343\201\214\351\226\223\351\201\225\343\201\210\343\201\246\343\201"...パスワードが間違えています
) = 40
+++ exited (status 0) +++

[Crypto 100]exchangeable if

問題の画像を開くと、4文字分抜け落ちたフラグがある。
問題の画像のexif情報にはmd5ハッシュ値が書いてある。
正しいフラグのmd5ハッシュ値exif情報にあるハッシュと一致すると考え、抜け落ちた4文字分を総当たりで検索した。

import hashlib

chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~"
ans = "2009d1c114ed83f57cf8adde69fd6ca8"
s0 = "SCKOSEN{sHDtF1"
s1 = "NLTIWp}"

for c0 in chars:
    for c1 in chars:
        for c2 in chars:
            for c3 in chars:
                string = s0 + c0 + c1 + c2 + c3 + s1
                has = hashlib.md5(string.encode('utf-8')).hexdigest()
                if has == ans:
                    print(string)

[Crypto 200]シンプルなQRコード

右半分のみのQRコードが与えられる。
与えられた部分からでもマスクパターンがわかる(チームメイトが見つけた)のであとはstrong-qr-decoderでマスクパターンを指定してデコードする。

[Crypto 250]CMA

2つのPublic Keyと、暗号文c1とc2が与えられる。
おそらく異なるeで同一の平文を暗号化しているのでタイトルからも明らかだが、Common Modulus Attackが可能。

import gmpy2
import math

n = 69716374785・・・

c1 = 3565707597・・・
c2 = 5756696653・・・

e1 = 28403731
e2 = 17150467

# pow マイナス乗 対応版


def pow_ex(a, b, c):
    if b < 0:
        return pow(gmpy2.invert(a, c), -b, c)
    else:
        return pow(a, b, c)


def main():
    gcd, s1, s2 = gmpy2.gcdext(e1, e2)
    print("s1=%d,s2=%d" % (s1, s2))

    v = pow_ex(c1, s1, n)
    w = pow_ex(c2, s2, n)

    m = v * w % n
    m = int(m)
    s = "".join(map(chr, int(m).to_bytes(
        math.ceil(m.bit_length() / 8), "big")))
    print(s)


if __name__ == "__main__":
    main()

[Network 100]Basic認証

Wiresharkでpcapを見ると、No14の部分でbasic認証の情報を送信していて、それに対してサーバからOKが返ってきているので、basic認証で用いたユーザIDとパスワードがここから手に入る。
更に読み進めるとNo16にはzipファイルのパスワードはjohnのパスワードであると書かれている。
そこでwiresharkのオブジェクトエクスポート機能でflag.zipを取得し、johnのパスワードで展開すると、flagが書かれたテキストファイルが手に入る。

[Network 150]ログインしてフラグを入手せよ

Digest認証の問題。問題文で与えられたmd5のハッシュとそのハッシュ化前の値はDigest認証のresponseである。
nonceが通信するたびに変化するため、pcapファイル内にあるresponseと異なる。
responseの構造は以下に示す。

A1 = username : realm : password
A2 = http method : URI
response = MD5( MD5( A1 ) : nonce : nc : cnonce : qop : MD5( A2 ) )

通信するたびに変化するのはnonceのみなので、サーバから受け取ったnonceを元にresponseを計算すればよい。

import urllib
import urllib.request
import hashlib

url = 'http://digest.kosensc2018.tech/flag.txt'

username = "tanaka"
realm = "Digest Auth"
nonce = ""
uri = "/flag.txt"
algorithm = "MD5"
response = ""
qop = "auth"
nc = "00000001"
cnonce = "ZmMyNDM3NzcyNWI3ZGI5NjQyNjhiNTAwZDkxZjM4YzQ="
md5a1 = "c932836c1feff27841c03453e81d5b13"
a2 = 'GET:'+uri


def getNonce():
    try:
        data = urllib.request.urlopen(url)
        html = data.read()
    except Exception as e:
        res = e.info()['WWW-Authenticate'].split('"')
        nonce = res[3]
        return nonce


def genMD5(str):
    return hashlib.md5(str.encode()).hexdigest()


def genResponse(nonce):
    response = genMD5(md5a1 + ':' + nonce + ':' + nc + ':' +
                      cnonce + ':' + qop + ':' + genMD5(a2))
    return response


def genAuthorized(nonce, response):
    authorized = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", cnonce=\"%s\", nc=%s, qop=%s, response=\"%s\", algorithm=\"%s\"" % (username, realm, nonce, uri, cnonce, nc, qop, response, algorithm)
    return authorized


def main():
    nonce = getNonce()
    response = genResponse(nonce)
    auth = genAuthorized(nonce, response)
    header = {'Authorization': auth}
    req = urllib.request.Request(url, None, header)
    try:
        data = urllib.request.urlopen(req)
        html = data.read()
        print("success")
        print (html.decode('utf-8'))
    except Exception as e:
        print (e.code)
        print (e.info())


if __name__ == '__main__':
    main()

[Web 100]サーバーから情報を抜き出せ!

画像が3つ掲載されたウェブページが表示される。
画像の参照先が”/image?filename=1.jpeg”となっていることから"1.jpeg"の部分をいじると"flag.txt"が表示されると推定。
しかし、"/image?filename=flag.txt"ではエラーになってしまうため、ディレクトリトラバーサルが有効だと考え、"/image?filename=../flag.txt"としたところ、flagが表示された。
どうやら"flag.txt"は画像の1つ上のディレクトリにあったらしい。

[Misc 50]サイトを見ていただけなのに

Webサイトを見ていただけなのに、謎のウイルスがダウンロードされて起動させられてしまった!

ググるIPAのページが見つかったのでその単語を入力した。

[Misc 100]謎のファイル

与えられたzipを展開すると"_rels"、"docProps"、"rename_me.xml"、"word"フォルダが出てきた。
何かのファイル構造で、”rename_me.xml”の名前を正しく直し、zip圧縮し直すことでファイルが読めるようになると考え、更に"word"フォルダがあるため、docxファイルであると推定した。
"rename_me.xml"について、docxのファイル構造を調べると、"[Content-Type].xml"という名前で保存されているべきなのでリネームした。
最後にもう一度zip(フォルダで圧縮ではなくすべてのフォルダとファイルを指定して)圧縮し、拡張子をdocxにすることでflagが表示される。