satto1237’s diary

s4tt01237’s diary

ラーメンとかCTFとかセキュリティとか

WPICTF 2019 Write-up

はじめに

2019/04/13 ~ 2019/04/15 に開催されたWPICTFに1人で参加しました.

成績

9問解いて88位(1問以上正解した586チーム中)でした.

f:id:satto1237:20190415141833p:plain f:id:satto1237:20190415141857p:plain

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]

https://discord.gg/pqg8qjw

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 sourcelocker

Here 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

アプローチ:Chromeデベロッパーツール

リンク先でChromeデベロッパーツールを開きます.

f:id:satto1237:20190415144555p:plain

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 now

Brought 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してるのにsleephelpを見ろとか言われているので).

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!

f:id:satto1237:20190415150158p:plain

アプローチ:換字式暗号 + エスパー

絵文字が換字式暗号に見えるので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で解けます.

quipqiup.com

OMG IT IS WPI REPENT BOOMERS

あとはフラグの形式に直してあげます.

WPI{REPENT_ZOOMERS}

ZOOMERスラングっぽいのでquipqiupでは出力されないっぽい

www.urbandictionary.com

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にソース保護は期待できない)

pypi.org

> 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まで増やしてWPIgrepします.

> 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}

さらにここから目grepflag感のある文字列を探します.

WPI{xkcd-keyboard-mash}

フラグを見てから元ネタを知りました.

xkcd.com

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')が必要になります.
しかし,saltsecretになっているため,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を利用します.

github.com

以下ソルバです.
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が難しかったのでこっちに参加した