Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
n3rada committed Dec 3, 2024
1 parent 1dde01a commit 72611ae
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 284 deletions.
183 changes: 108 additions & 75 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[tool.poetry]
name = "toboggan"
version = "1.0"
description = "Toboggan is a python3 module wrapper for your RCEs that can be leveraged to interactive shell."
version = "2.0"
description = "CLI tool designed to wrap modules for Remote Command Execution (RCE). It enables interactive shell functionality, making it ideal for constrained environments (e.g., firewalls) where establishing a reverse shell is challenging."
authors = ["n3rada"]
license = "MIT license"
readme = "README.md"

[tool.poetry.dependencies]
python = ">=3.11"
python = ">=3.10"
prompt-toolkit = ">=3.0.39"
tqdm = ">=4.66.0"
httpx = ">=0.24.1"
Expand Down
128 changes: 77 additions & 51 deletions toboggan/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@


def banner() -> None:
banner = r"""
print(
r"""
_____ _
/__ \___ | |__ ___ __ _ __ _ __ _ _ __
/ /\/ _ \| '_ \ / _ \ / _` |/ _` |/ _` | '_ \
Expand All @@ -20,139 +21,164 @@ def banner() -> None:
Remote command execution module wrapping tool.
@n3rada
"""
print(banner)
)


def run() -> None:
parser = argparse.ArgumentParser(
prog="toboggan",
add_help=True,
description="A python3 module wrapper for your RCEs that can be leveraged to an interactive shell.",
description="A Python module wrapper for RCEs that can be leveraged to an interactive shell.",
)

# Argument Groups
module_group = parser.add_argument_group(
"Module Configuration", "Options to configure the execution module."
)
request_group = parser.add_argument_group(
"Request Configuration", "Options for crafting and sending requests."
)
interactive_group = parser.add_argument_group(
"Interactive Settings", "Options to manage interactive sessions."
)
advanced_group = parser.add_argument_group(
"Advanced Options", "Additional advanced or debugging options."
)

parser.add_argument(
# Module configuration arguments
module_group.add_argument(
"-m",
"--module",
type=str,
default=None,
help="Module path to be imported and executed or built-in module name.",
)

parser.add_argument(
module_group.add_argument(
"-o",
"--os",
type=str,
default=None,
help="OS command with placeholder ||cmd||.",
)

parser.add_argument(
# Request configuration arguments
request_group.add_argument(
"-u",
"--url",
type=str,
default=None,
help="URL to use if a built-in module is specified. Replace ||URL|| placeholder.",
)

parser.add_argument(
request_group.add_argument(
"--post",
action="store_true",
required=False,
help="Specify that the URL request should be a POST request (only with -u/--url).",
)
request_group.add_argument(
"-p",
"--password",
"--params",
nargs="*",
help="Additional parameters as key=value pairs.",
)
request_group.add_argument(
"--cmd-param",
type=str,
default=None,
help="Password in 'key=value' format. If only value is provided, a default key 'ps' will be used.",
default="cmd",
help="Specify the name of the command parameter (e.g., cmd, command, c).",
)

parser.add_argument(
# Interactive session arguments
interactive_group.add_argument(
"-i",
"--interactive",
action="store_true",
required=False,
help="Start an interactive session.",
)
parser.add_argument(
interactive_group.add_argument(
"-s",
"--session",
required=False,
type=str,
default=None,
help="Session to connect.",
)
parser.add_argument(
interactive_group.add_argument(
"-r",
"--read-interval",
required=False,
type=float,
default=None,
help="Reading interval for interactivity.",
)
parser.add_argument(

# Advanced options
advanced_group.add_argument(
"-a",
"--alias-prefix",
required=False,
type=str,
default=None,
help="Desired alias prefix to use.",
)
parser.add_argument(
advanced_group.add_argument(
"-c",
"--clear-commands",
action="store_true",
required=False,
help="Send unobfuscated commands.",
)

parser.add_argument(
advanced_group.add_argument(
"-b",
"--burp",
action="store_true",
required=False,
help="Pass the traffic through burp if '# ||BURP||' placeholder is present inside choosen module.",
help="Pass the traffic through Burp Suite if '# ||BURP||' placeholder is present in the module.",
)

banner()

# Parse arguments
args = parser.parse_args()

if ("-h" in sys.argv or "--help" in sys.argv) or (
args.module is None and args.url is None and args.os is None
):
parser.print_help()
return

# Check for the presence of '-i' when '-s' or '-r' is specified
if (args.session or args.read_interval) and not args.interactive:
parser.error(
"[Toboggan] The -s and -r arguments require the -i (interactive) argument."
)

password_param = None
password_content = None

if args.password:
# Try the formats `"key"="value"` or `'key'='value'`
if match := re.match(r'["\']?(.*?)["\']?=["\']?(.*?)["\']?$', args.password):
password_param, password_content = match.groups()
else:
# Try the second format `key=value`
password_parts = args.password.split("=", 1)
if len(password_parts) == 2:
password_param, password_content = password_parts
# Add validation for grouped arguments
if args.url:
if not args.params and not args.cmd_param:
parser.error(
"URL-based execution requires parameters (--params) or a command parameter (--cmd-param)."
)
if args.post and not args.url:
parser.error("The --post argument can only be used with --url.")

if args.session and not args.interactive:
parser.error("The --session argument requires --interactive.")

if args.read_interval and not args.interactive:
parser.error("The --read-interval argument requires --interactive.")

# Parse parameters
request_parameters = {}
if args.params:
for param in args.params:
if "=" in param:
key, value = param.split("=", 1)
request_parameters[key] = value
else:
password_param = "ps"
password_content = password_parts[0]
parser.error(f"Invalid parameter format: {param}. Use key=value.")

# Module handling
module_path_or_name = args.module
if args.os:
module_path_or_name = "snippet"
elif args.url:
module_path_or_name = "webshell"
module_path_or_name = "webshell__"
module_path_or_name += "POST" if args.post else "GET"

# Load the module
module_instance = executor.Module(
module_path=module_path_or_name,
url=args.url,
password_param=password_param,
password_content=password_content,
request_parameters=request_parameters,
command_parameter=args.cmd_param,
burp_proxy=args.burp,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
"""
webshell.py
------------------------
Module containing utilities to interact with basic webshells.
This module currently provides a single function that sends commands to a specific webshell URL and
parses the outputs to sanitize common unwanted escape sequences. It uses the httpx library to make HTTP requests
and the built-in 're' library for regular expressions.
Functions:
- execute(command: str, timeout: float = None) -> str
"""

# Buit-in imports
import re

Expand All @@ -23,8 +9,8 @@ def execute(command: str, timeout: float = None) -> str:
response = httpx.get(
url="||URL||",
params={
# ||PARAM_PASSWORD||
"||PARAM_CMD||": command,
# ||PARAMS||
},
# ||BURP||
timeout=timeout,
Expand All @@ -44,7 +30,4 @@ def execute(command: str, timeout: float = None) -> str:
# If there's meaningful content, strip only the trailing escape sequences
output = re.sub(r"(\\[nt]|[\n\t])+$", "", output, flags=re.IGNORECASE)

# Add a new line at the end
output += "\n"

return output
33 changes: 33 additions & 0 deletions toboggan/modules/webshell__POST.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Buit-in imports
import re

# Third party library imports
import httpx


def execute(command: str, timeout: float = None) -> str:
response = httpx.post(
url="||URL||",
data={
"||PARAM_CMD||": command,
# ||PARAMS||
},
# ||BURP||
timeout=timeout,
verify=False,
)

# Check if the request was successful
response.raise_for_status()

# Trying to sanitize most of the webshells outputs
output = response.text

# Check if entire output consists only of escape sequences
if re.fullmatch(r"(\\[nt]|[\n\t])+", output, flags=re.IGNORECASE):
return ""

# If there's meaningful content, strip only the trailing escape sequences
output = re.sub(r"(\\[nt]|[\n\t])+$", "", output, flags=re.IGNORECASE)

return output
7 changes: 4 additions & 3 deletions toboggan/src/commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Built-in imports
import sys
from typing import TYPE_CHECKING
from pathlib import Path
import secrets
Expand All @@ -18,7 +19,7 @@

# Module variables definition

DEFAULT_PREFIX = "\\"
DEFAULT_PREFIX = "/"

INTERACTIVITY_CLASSES = {
"unix": [
Expand Down Expand Up @@ -166,10 +167,10 @@ def terminate(self) -> None:
"""
if self.__interactivity is not None:
keeping = utils.yes_no_query(
prompt=f"[Toboggan] Would you like to save the current session?",
prompt="[Toboggan] Would you like to save the current session?",
)
self.__interactivity.stop(keep_session=keeping)
exit(0)
sys.exit(0)

def get_prompt(self) -> str:
"""
Expand Down
Loading

0 comments on commit 72611ae

Please sign in to comment.