WPICTF 2019 Write-up
- はじめに
- 成績
- can you read [Intro, 1pts, 496solves]
- Discord [Intro, 5pts, 370solves]
- Source pt1 [Pwn, 100pts, 132solves]
- strings [Reversing, 50pts, 398solves]
- WebInspect [Web, 25pts, 465solves]
- suckmore-shell [Linux, 100pts, 210solves]
- zoomercrypt [Cryptography, 50pts, 91solves]
- jocipher [Cryptography, 100pts, 190solves]
- bogged [Cryptography, 150pts, 23solves]
- まとめ
はじめに
2019/04/13 ~ 2019/04/15 に開催されたWPICTFに1人で参加しました.
成績
9問解いて88位(1問以上正解した586チーム中)でした.
can you read [Intro, 1pts, 496solves]
WPI{y3s_y0u_cAN_r33d}
All flags, unless otherwise stated, will be in the form WPI{S0M3_flag_here}
WPI{y3s_y0u_cAN_r33d}
Discord [Intro, 5pts, 370solves]
Look at the pinned msgs.
WPI{Welcome_to_our_discord}
Source pt1 [Pwn, 100pts, 132solves]
ssh source@source.wpictf.xyz -p 31337 (or 31338 or 31339).
Password is sourcelockerHere is your babybuff.
アプローチ:BOF
ssh
するとhttps://www.imdb.com/title/tt0945513/にアクセスするためのパスワードの入力を求められます.
> ssh source@source.wpictf.xyz -p 31337 source@source.wpictf.xyz's password: Enter the password to get access to https://www.imdb.com/title/tt0945513/ A Pasword auth failed exiting Connection to source.wpictf.xyz closed.
babybuff
とか書かれているので何も考えずにBOFしそうな文字列を流します.
> ssh source@source.wpictf.xyz -p 31337 source@source.wpictf.xyz's password: Enter the password to get access to https://www.imdb.com/title/tt0945513/ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
予想通りソースコードが表示されます.
#define _GNU_SOURCE #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> //compiled with gcc source.c -o source -fno-stack-protector -no-pie //gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 //flag for source1 is WPI{Typos_are_GrEaT!} int getpw(void){ int res = 0; char pw[100]; fgets(pw, 0x100, stdin); *strchrnul(pw, '\n') = 0; if(!strcmp(pw, getenv("SOURCE1_PW"))) res = 1; return res; } char *lesscmd[] = {"less", "source.c", 0}; int main(void){ setenv("LESSSECURE", "1", 1); printf("Enter the password to get access to https://www.imdb.com/title/tt0945513/\n"); if(!getpw()){ printf("Pasword auth failed\nexiting\n"); return 1; } execvp(lesscmd[0], lesscmd); return 0; }
fgets(pw, 0x100, stdin);
に不自然なtypoがありますね.
WPI{Typos_are_GrEaT!}
strings [Reversing, 50pts, 398solves]
A handy tool for your RE efforts!
アプローチ:strings
> file strings strings: ELF 64-bit LSB shared object x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4e539e560b4d7729f7926e1b594dc623ce4c8e0d, not stripped
> strings strings | grep WPI WPI{ warbleglarblesomejunkWPI{What_do_you_mean_I_SEE_AHH_SKI}0x13376969
WPI{What_do_you_mean_I_SEE_AHH_SKI}
WebInspect [Web, 25pts, 465solves]
Something is lurking at https://www.wpictf.xyz
WPI{Inspect0r_Gadget}
suckmore-shell [Linux, 100pts, 210solves]
Here at Suckmore Software we are committed to delivering a truly unparalleled user experience. Help us out by testing our latest project.
- ssh ctf@107.21.60.114
- pass: i'm a real hacker nowBrought to you by acurless and SuckMore Software, a division of WPI Digital Holdings Ltd.
アプローチ:unalias
ssh
するとsuckmore shell
に繋がります.
> ssh ctf@107.21.60.114 ctf@107.21.60.114's password: SuckMORE shell v1.0.1. Note: for POSIX support update to v1.1.0 suckmore>env HOSTNAME=7eaed2c0fee9 PWD=/ HOME=/home/ctf FBR=f DISTTAG=fcontainer FGC=f TERM=xterm SHLVL=2 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PS1=suckmore> _=/usr/bin/env
いくつかのコマンドを実行すると挙動がおかしいことに気が付きます(ls
してるのにsleep
のhelp
を見ろとか言われているので).
suckmore>ls suckmore>ls -a sleep: invalid option -- 'a' Try 'sleep --help' for more information. suckmore>pwd Linux suckmore>uname Linux
alias
を確認します.
suckmore>alias alias bash='sh' alias cat='sleep 1 && vim' alias cd='cal' alias cp='grep' alias dnf='' alias find='w' alias less='echo "We are suckMORE, not suckless"' alias ls='sleep 1' alias more='echo "SuckMORE shell, v1.0.1, (c) SuckMore Software, a division of WPI Digital Holdings Ltd."' alias nano='touch' alias pwd='uname' alias rm='mv /u/' alias sh='echo "Why would you ever want to leave suckmore shell?"' alias sl='ls' alias vi='touch' alias vim='touch' alias which='echo "Not Found"'
unalias
して元に戻します.
suckmore>unalias pwd suckmore>unalias cd suckmore>unalias ls
$HOME
に移動してflag
を探します.
suckmore>pwd / suckmore>cd $HOME suckmore>pwd /home/ctf suckmore>ls bash: /usr/bin/ls: Permission denied
ls
が使えないらしいのでgrep
で探します.
suckmore>grep -r "WPI{" ./ ./flag:WPI{bash_sucks0194342}
WPI{bash_sucks0194342}
zoomercrypt [Cryptography, 50pts, 91solves]
My daughter is using a coded language to hide her activities from us!!!! Please, help us find out what she is hiding!
アプローチ:換字式暗号 + エスパー
絵文字が換字式暗号に見えるのでdecode
します.
以下のスクリプトで絵文字をASCII
に置き換えます.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- if __name__ == '__main__': emoji_cipher = '😃😁😕😗😈😗😇😋😄😗😆😓😄😓😂😈😎😃😃😁😓😆😇' emoji_translated = {} ascii_cipher = '' x = 0 for emoji in emoji_cipher: if emoji not in emoji_translated: emoji_translated[emoji] = chr(65 + x) x += 1 for emoji in emoji_cipher: ascii_cipher += emoji_translated[emoji] print(ascii_cipher)
> python solve.py ABCDEDFGHDIJHJKELAABJIF
換字式暗号のソルバを使うためにスペースを挿入してあげます.
ABC DE DF GHD IJHJKE LAABJIF
ここまで来たらquipqiup
で解けます.
OMG IT IS WPI REPENT BOOMERS
あとはフラグの形式に直してあげます.
WPI{REPENT_ZOOMERS}
ZOOMER
はスラングっぽいのでquipqiup
では出力されないっぽい
jocipher [Cryptography, 100pts, 190solves]
Decrypt PIY{zsxh-sqrvufwh-nfgl} to get the flag!
アプローチ:uncompyle
バイトコンパイルされたPython
ファイルが渡されます.
> file jocipher.pyc jocipher.pyc: python 2.7 byte-compiled
Python
のバイトコンパイルは簡単に復元できるのでuncompyle
を使って元に戻します(pyc
にソース保護は期待できない)
> uncompyle6 jocipher.pyc # uncompyle6 version 3.2.6 # Python bytecode 2.7 (62211) # Decompiled from: Python 2.7.15 |Anaconda, Inc.| (default, May 1 2018, 18:37:05) # [GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] # Embedded file name: ./jocipher.py # Compiled at: 2019-03-02 02:41:21
import argparse, re num = '' first = '' second = '' third = '' def setup(): global first global num global second global third num += '1' num += '2' num += '3' num += '4' num += '5' num += '6' num += '7' num += '8' num += '9' num += '0' first += 'q' first += 'w' first += 'e' first += 'r' first += 't' first += 'y' first += 'u' first += 'i' first += 'o' first += 'p' second += 'a' second += 's' second += 'd' second += 'f' second += 'g' second += 'h' second += 'j' second += 'k' second += 'l' third += 'z' third += 'x' third += 'c' third += 'v' third += 'b' third += 'n' third += 'm' def encode(string, shift): result = '' for i in range(len(string)): char = string.lower()[i] if char in num: new_char = num[(num.index(char) + shift) % len(num)] result += new_char elif char in first: new_char = first[(first.index(char) + shift) % len(first)] if string[i].isupper(): result += new_char.upper() else: result += new_char elif char in second: new_char = second[(second.index(char) + shift) % len(second)] if string[i].isupper(): result += new_char.upper() else: result += new_char elif char in third: new_char = third[(third.index(char) + shift) % len(third)] if string[i].isupper(): result += new_char.upper() else: result += new_char else: result += char print result return 0 def decode(string, shift): result = '' shift = -1 * shift for i in range(len(string)): char = string.lower()[i] if char in num: new_char = num[(num.index(char) + shift) % len(num)] result += new_char elif char in first: new_char = first[(first.index(char) + shift) % len(first)] if string[i].isupper(): result += new_char.upper() else: result += new_char elif char in second: new_char = second[(second.index(char) + shift) % len(second)] if string[i].isupper(): result += new_char.upper() else: result += new_char elif char in third: new_char = third[(third.index(char) + shift) % len(third)] if string[i].isupper(): result += new_char.upper() else: result += new_char else: result += char print result return 0 def main(): parser = argparse.ArgumentParser() parser.add_argument('--string', '-s', type=str, required=True, help='the string to encode or decode') parser.add_argument('--shift', '-t', type=int, required=True, help='the shift value to use') parser.add_argument('--encode', '-e', required=False, action='store_true', help='encode the string') parser.add_argument('--decode', '-d', required=False, action='store_true', help='decode the string') args = parser.parse_args() setup() p = re.compile('[a-zA-Z0-9\\-{}]') if p.match(args.string) is not None: if args.encode: ret = encode(args.string, args.shift) else: if args.decode: ret = decode(args.string, args.shift) if ret is not 0: print 'Sorry, this cipher only uses the [a-zA-Z0-9\\-{}]' else: print 'Sorry, this cipher only uses the [a-zA-Z0-9\\-{}]' return if __name__ == '__main__': main()
なんとなくやってることは分かりました.
ただ,encoder
, decoder
を完全に理解するよりもシフトを総当たりした方が速く解けそうなので今回は総当たりします.
> for x in `seq 10`; do echo "$x"; python uncompyle_jocipher.py -s PIY{zsxh-sqrvufwh-nfgl} -t $x -d; done 1 OUT{mazg-apecydqg-bdfk} 2 IYR{nlmf-lowxtspf-vsdj} 3 UTE{bknd-kiqzraod-cash} 4 YRW{vjbs-jupmelis-xlag} 5 TEQ{chva-hyonwkua-zklf} 6 RWP{xgcl-gtibqjyl-mjkd} 7 EQO{zfxk-fruvphtk-nhjs} 8 WPI{mdzj-deycogrj-bgha} 9 QOU{nsmh-swtxifeh-vfgl} 10 PIY{bang-aqrzudwg-cdfk}
シフトを100まで増やしてWPI
でgrep
します.
> for x in `seq 100`; do echo "$x"; python uncompyle_jocipher.py -s PIY{zsxh-sqrvufwh-nfgl} -t $x -d; done | grep WPI WPI{mdzj-deycogrj-bgha} WPI{vsbh-seymofrh-xfgl} WPI{zaxg-aeyvodrg-ndfk} WPI{blnf-leyzosrf-csdj} WPI{xkcd-keyboard-mash} WPI{njms-jeyxolrs-vlag} WPI{chva-heynokra-zklf} WPI{mgzl-geycojrl-bjkd} WPI{vfbk-feymohrk-xhjs} WPI{zdxj-deyvogrj-ngha}
さらにここから目grep
でflag感
のある文字列を探します.
WPI{xkcd-keyboard-mash}
フラグを見てから元ネタを知りました.
bogged [Cryptography, 150pts, 23solves]
Two strange men called me last night. They call themselves the Bogdanoff twins. I don't know much about cryptocurrency can you help them with their scheme?
nc bogged.wpictf.xyz 31337 (or 31338 or 31339)
アプローチ:Length Extension Attack
nc
するとcryptowojak123
の通貨をnot_b0gdan0ff
に送金しろと言われます.
> nc bogged.wpictf.xyz 31337 BOGDANOFF: Bonjour... We have access to the Binance backdoor, and got you into a compromised teller station. We need you to steal tethered cryptocurrency from people's wallets. We were halted by an unfortunate countermeasure in the teller system, but we have an account ready to recieve the stolen crypto. Steal the currency from cryptowojak123. Transfer it to not_b0gdan0ff. Transfer everything... then we will kill him, and find another. Do not fail us. Welcome to the Binance Teller Terminal! Please remember to use admin-issued auth tokens with each account transfer! Either enter a command or one of the following keywords: accounts: List of accounts currently on the system. history: A history of prior terminal commands. help: A reminder on how to use this terminal. Command: >>>
とりあえずhelp
コマンドで概要を把握します.
Command: >>>help You may either withdraw funds from an account or deposit funds to an account. Withdraw with the following command: withdraw ACCOUNT_NAME Deposit with the following command: deposit ACCOUNT_NAME Commands may be chained, as follows: withdraw ACCOUNT_NAME;deposit ACCOUNT_NAME;... An authorization token unique to the command contents must exist for the transaction to succeed! (Sorry, but we have to protect from malicious employees.) Contact admin@dontactuallyemailthis.net to get auth tokens for different transfer commands!
help
を読む限り,withdraw cryptowojak123;deposit not_b0gdan0ff
を実行すればフラグが取れそうです.
Command: >>>withdraw cryptowojak123;deposit not_b0gdan0ff Auth token: >>>a Error: Auth token does not match provided command..
Auth token
が必要みたいです(helpにも書かれているのでそれはそう).
ここで,問題に添付されていたleaked_source.py
を確認してみます.
import hashlib secret = "" def generate_command_token(command, secret): hashed = hashlib.sha1(secret+command).hexdigest() return hashed def validate_input(command, token_in): token = hash_command(command, secret) if token == token_in: return True else: return False while(True): print("Command:") command = raw_input(">>>") print('Auth token:') token = raw_input(">>>") print if validate_input(command, token) == False: print("Error: Auth token does not match provided command..") else: execute_command(command) print
hash(salt || command)
が Auth token
になっていることが分かります.
つまり,withdraw cryptowojak123;deposit not_b0gdan0ff
を実行するには
hash(salt || 'withdraw cryptowojak123;deposit not_b0gdan0ff')
が必要になります.
しかし,salt
はsecret
になっているため,Auth token
は簡単には求められません.
このままではまだちょっと情報が足りないのでhistory
コマンドの結果を確認します.
Command: >>>history ///// TRANSACTION HISTORY ////////////////////////// Command: >>>withdraw john.doe Auth token: >>>b4c967e157fad98060ebbf24135bfdb5a73f14dc Action successful! Command: >>>withdraw john.doe;deposit xXwaltonchaingangXx Auth token: >>>455705a6756fb014a4cba2aa0652779008e36878 Action successful! Command: >>>withdraw cryptowojak123;deposit xXwaltonchaingangXx Auth token: >>>e429ffbfe7cabd62bda3589576d8717aaf3f663f Action successful! Command: >>>withdraw john.doe Auth token: >>>b4c967e157fad98060ebbf24135bfdb5a73f14dc Action successful! ////////////////////////////////////////////////////
コマンドの履歴だけではなく,Auth token
まで見れますね.
ここで怪しい点をまとめると以下のようになります.
- チェインルール
Auth token
の生成方法- 過去の
Auth token
が閲覧可能
したがって,Length Extension Attack
ができることが分かります.
Length Extension Attack
とはhash(salt || message1)
とmessage1
が既知のとき,hash(salt || message1 || message2)
が求められる攻撃です.
具体的には
message1
:withdraw john.doe
hash(salt || message1)
:b4c967e157fad98060ebbf24135bfdb5a73f14dc
message2
:;withdraw cryptowojak123;deposit not_b0gdan0ff
hash(salt || message1 || message2)
: 求めたいAuth token
のようになります.
実際にLength Extension Attack
を行うツールとしてはHashPump
を利用します.
以下ソルバです.
HashPump
を利用する際(Length Extension Attack
を行う際)にはsalt
の文字列長が必要になるのでそこは総当たりしてます.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- from socket import * import subprocess import binascii def encodeHex(s): enc = '' xcode = '' for i in range(len(s)): if s[i] == '\\' and len(xcode) == 0: xcode += s[i] elif s[i] == 'x' and len(xcode) == 1: xcode += s[i] elif len(xcode) == 2: xcode += s[i] elif len(xcode) == 3: xcode += s[i] enc += xcode[2:] xcode = '' else: enc += '%02x' % ord(s[i]) return enc def LEA(cmd): proc = subprocess.run(cmd,stdout = subprocess.PIPE, stderr = subprocess.PIPE) ret = proc.stdout.decode('utf-8').split('\n') token = ret[0].encode('utf-8') init = ret[1][:17].encode('utf-8') lines = ret[1][17:].split(';') encHex = encodeHex(lines[0]) pad = binascii.unhexlify(encHex) append = (';' + lines[1] + ';' + lines[2]).encode('utf-8') data = init + pad + append print(data) return (data,token) def main(): cmd_format = ['hashpump', '-s', 'b4c967e157fad98060ebbf24135bfdb5a73f14dc', '-d', 'withdraw john.doe', '-k', 'n', '-a', ';withdraw cryptowojak123;deposit not_b0gdan0ff'] s = socket(AF_INET, SOCK_STREAM) s.connect(('bogged.wpictf.xyz', 31337)) # recv 'description' for _ in range(2): temp_rec = s.recv(1024).decode('utf-8') for n in range(1, 33): print('SALT Length: {}'.format(n)) cmd_format[6] = str(n) LEA_data, token = LEA(cmd_format) s.send(LEA_data + b'\n') # recv 'Auth token' temp_rec = s.recv(1024).decode('utf-8') print(temp_rec) s.send(token + b'\n') print(token) # recv '>>>' temp_rec = s.recv(1024).decode('utf-8') print(temp_rec) # recv 'result' temp_rec = s.recv(1024).decode('utf-8') print(temp_rec) if 'Error' not in temp_rec: break if __name__ == '__main__': main()
> python solve.py [snip] Command: >>> SALT Length: 16 b'withdraw john.doe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x08;withdraw cryptowojak123;deposit not_b0gdan0ff' Auth token: b'050a162f6ee310d345821b402f436b19677495dc' >>> A subcommand was unreadable... Action successful! Action successful! BOGDANOFF: The money is transferred. You have done... well. Your service has demonstrated your loyalty. You have truly swallowed the bogpill. You will be among the first to behold the enlightenment we will soon unleash. ... Quoi? You want more? ... Somewhere in the cosmos, a secret calls out to us, lost in the wrinkles of time. We shall relay this secret to you. Au revoir. WPI{duMp_33t_aNd_g@rn33sh_H1$_wAg3$}
WPI{duMp_33t_aNd_g@rn33sh_H1$_wAg3$}
まとめ
- 簡単な問題しか解けなくて悲しくなった
- Web, Cryptoがあと1問ずつ解けそうだったけど解けなかった(Write-up読んでべんきょうする)
- PlaidCTFが難しかったのでこっちに参加した