From a0c2713899509b95d49821cf84139b80a25b0462 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:04:44 +0200 Subject: [PATCH 1/5] Added homedir, Added Parameter Parser --- pwncat/commands/__init__.py | 51 ++++++++++++++++++++++++++++++++++--- pwncat/commands/upload.py | 4 +-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index 3d7dc2bd..c10ddad0 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -46,6 +46,7 @@ def run(self, manager: "pwncat.manager.Manager", args: "argparse.Namespace"): from io import TextIOWrapper from enum import Enum, auto from typing import Dict, List, Type, Callable, Iterable +from pathlib import Path from functools import partial import rich.text @@ -97,6 +98,31 @@ class Complete(Enum): """ Do not provide argument completions """ +class ParseType(Enum): + """ + Command type. This defines how command parameter arguments are parsed + """ + + NONE = auto() + """ No specific type given, so no interpreter needed """ + LOCAL_FILE = auto() + """ File type """ + + +class ParameterParse: + def parse(value): + """This is where the parameter will be parsed""" + raise NotImplementedError + + +class LocalFileParse: + def parse(value): + if value.startswith("~"): + homedir = str(Path.home()) + return homedir + value[1:] + return value + + class StoreConstOnce(argparse.Action): """Only allow the user to store a value in the destination once. This prevents users from selection multiple actions in the privesc parser.""" @@ -180,6 +206,8 @@ class Parameter: :param complete: the completion type :type complete: Complete + :param parser: the parsing type + :type parser: ParseType :param token: the Pygments token to highlight this argument with :type token: Pygments Token :param group: true for a group definition, a string naming the group to be a part of, or none @@ -191,12 +219,14 @@ class Parameter: def __init__( self, complete: Complete, + parser=ParseType.NONE, token=token.Name.Label, group: str = None, *args, **kwargs, ): self.complete = complete + self.parser = parser self.token = token self.group = group self.args = args @@ -338,6 +368,18 @@ def __iter__(wself): parser.set_defaults(**self.DEFAULTS) + def parse_args(self, args, fallback): + if not self.parser: + return fallback + + parsed = vars(self.parser.parse_args(args)) + for [argkey, argobj] in self.ARGS.items(): + if argkey not in parsed or argobj.parser is ParseType.NONE: + continue + if argobj.parser is ParseType.LOCAL_FILE: + parsed[argkey] = LocalFileParse.parse(parsed[argkey]) + return argparse.Namespace(**parsed) + def resolve_blocks(source: str): """This is a dumb lexer that turns strings of text with code blocks (squigly @@ -659,10 +701,7 @@ def dispatch_line(self, line: str, prog_name: str = None): prog_name = temp_name # Parse the arguments - if command.parser: - args = command.parser.parse_args(args) - else: - args = line + args = command.parse_args(args, line) # Run the command command.run(self.manager, args) @@ -873,6 +912,10 @@ def get_completions(self, document: Document, complete_event: CompleteEvent): if path == "": path = "." + if path.startswith("~"): + homedir = str(Path.home()) + path = homedir + path[1:] + # Ensure the directory exists if not os.path.isdir(path): return diff --git a/pwncat/commands/upload.py b/pwncat/commands/upload.py index 13e547d1..91205163 100644 --- a/pwncat/commands/upload.py +++ b/pwncat/commands/upload.py @@ -13,7 +13,7 @@ import pwncat from pwncat.util import console, copyfileobj, human_readable_size, human_readable_delta -from pwncat.commands import Complete, Parameter, CommandDefinition +from pwncat.commands import Complete, Parameter, ParseType, CommandDefinition class Command(CommandDefinition): @@ -21,7 +21,7 @@ class Command(CommandDefinition): PROG = "upload" ARGS = { - "source": Parameter(Complete.LOCAL_FILE), + "source": Parameter(Complete.LOCAL_FILE, parser=ParseType.LOCAL_FILE), "destination": Parameter( Complete.REMOTE_FILE, nargs="?", From 67f96454781ce7e19e85361be8f08bc9a58c5d67 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:04:53 +0200 Subject: [PATCH 2/5] Fixed test not running --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b From 1902ede5e348cba3fe2d2cb75a5318bc702d37ec Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Thu, 5 Aug 2021 00:09:21 +0200 Subject: [PATCH 3/5] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d18d09b..686666a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and simply didn't have the time to go back and retroactively create one. - Added licensing for pwncat (MIT) - Added background listener API and commands ([#43](https://github.com/calebstewart/pwncat/issues/43)) - Added Windows privilege escalation via BadPotato plugin ([#106](https://github.com/calebstewart/pwncat/issues/106)) +- Added command parameter parsers +- Added homedir (`~`) support on local file completer and parser ### Removed - Removed `setup.py` and `requirements.txt` From 28a71b4b7015eb149f6341afc21779c28338079f Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:28:49 +0200 Subject: [PATCH 4/5] Resolved feedback, Added remote file type --- CHANGELOG.md | 1 + pwncat/commands/__init__.py | 48 +++++++++++++++++++++++++++++-------- pwncat/commands/connect.py | 2 -- pwncat/commands/download.py | 6 ++--- pwncat/commands/upload.py | 1 + 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 686666a6..d1fbde86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and simply didn't have the time to go back and retroactively create one. - Added Windows privilege escalation via BadPotato plugin ([#106](https://github.com/calebstewart/pwncat/issues/106)) - Added command parameter parsers - Added homedir (`~`) support on local file completer and parser +- Added homedir (`~`) support on remote file completer and parser ### Removed - Removed `setup.py` and `requirements.txt` diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index c10ddad0..505f6c14 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -106,20 +106,32 @@ class ParseType(Enum): NONE = auto() """ No specific type given, so no interpreter needed """ LOCAL_FILE = auto() - """ File type """ + """ Local file type """ + REMOTE_FILE = auto() + """ Remote file type """ class ParameterParse: - def parse(value): + def parse(value, session: "pwncat.manager.Session"): """This is where the parameter will be parsed""" raise NotImplementedError -class LocalFileParse: - def parse(value): +class LocalFileParse(ParameterParse): + def parse(value, session: "pwncat.manager.Session"): if value.startswith("~"): - homedir = str(Path.home()) - return homedir + value[1:] + return os.path.expanduser(value) + return value + +class RemoteFileParse(ParameterParse): + def parse(value, session: "pwncat.manager.Session"): + if value.startswith("~/") or value.startswith("~\\"): + homedir = session.platform.getenv("HOME") + if not homedir: + """ Windows support """ + homedir = session.platform.getenv("USERPROFILE") + if homedir: + return homedir + value[1:] return value @@ -376,8 +388,17 @@ def parse_args(self, args, fallback): for [argkey, argobj] in self.ARGS.items(): if argkey not in parsed or argobj.parser is ParseType.NONE: continue - if argobj.parser is ParseType.LOCAL_FILE: - parsed[argkey] = LocalFileParse.parse(parsed[argkey]) + + if argobj.parser is not ParseType.NONE: + parser = None + if argobj.parser is ParseType.LOCAL_FILE: + parser = LocalFileParse + elif argobj.parser is ParseType.REMOTE_FILE: + parser = RemoteFileParse + + if parser is not None: + parsed[argkey] = parser.parse(parsed[argkey], self.manager.target) + return argparse.Namespace(**parsed) @@ -891,6 +912,14 @@ def get_completions(self, document: Document, complete_event: CompleteEvent): if path == "": path = "." + + if path.startswith("~"): + homedir = self.manager.target.platform.getenv("HOME") + if not homedir: + """ Windows support """ + homedir = self.manager.target.platform.getenv("USERPROFILE") + if homedir: + path = homedir + path[1:] for name in self.manager.target.platform.listdir(path): if name.startswith(partial_name): @@ -913,8 +942,7 @@ def get_completions(self, document: Document, complete_event: CompleteEvent): path = "." if path.startswith("~"): - homedir = str(Path.home()) - path = homedir + path[1:] + path = os.path.expanduser(path) # Ensure the directory exists if not os.path.isdir(path): diff --git a/pwncat/commands/connect.py b/pwncat/commands/connect.py index 11f2d7ae..07a52045 100644 --- a/pwncat/commands/connect.py +++ b/pwncat/commands/connect.py @@ -203,8 +203,6 @@ def run(self, manager: "pwncat.manager.Manager", args): console.log("[red]error[/red]: multiple ports specified") return - console.log(args.pos_port) - if args.port is not None: query_args["port"] = args.port if args.pos_port is not None: diff --git a/pwncat/commands/download.py b/pwncat/commands/download.py index 002636ce..9b14576c 100644 --- a/pwncat/commands/download.py +++ b/pwncat/commands/download.py @@ -14,7 +14,7 @@ import pwncat from pwncat import util from pwncat.util import console -from pwncat.commands import Complete, Parameter, CommandDefinition +from pwncat.commands import Complete, Parameter, CommandDefinition, ParseType class Command(CommandDefinition): @@ -22,8 +22,8 @@ class Command(CommandDefinition): PROG = "download" ARGS = { - "source": Parameter(Complete.REMOTE_FILE), - "destination": Parameter(Complete.LOCAL_FILE, nargs="?"), + "source": Parameter(Complete.REMOTE_FILE, parser=ParseType.REMOTE_FILE), + "destination": Parameter(Complete.LOCAL_FILE, nargs="?", parser=ParseType.LOCAL_FILE), } def run(self, manager: "pwncat.manager.Manager", args): diff --git a/pwncat/commands/upload.py b/pwncat/commands/upload.py index 91205163..7d6d3bea 100644 --- a/pwncat/commands/upload.py +++ b/pwncat/commands/upload.py @@ -25,6 +25,7 @@ class Command(CommandDefinition): "destination": Parameter( Complete.REMOTE_FILE, nargs="?", + parser=ParseType.REMOTE_FILE ), } From 642bed5e803bbbbe7545bc48d241ec2d27bac6d8 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:31:42 +0200 Subject: [PATCH 5/5] Fixed pre-merge issues --- pwncat/commands/__init__.py | 8 ++++---- pwncat/commands/download.py | 6 ++++-- pwncat/commands/upload.py | 4 +--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index 505f6c14..0b0693f6 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -46,7 +46,6 @@ def run(self, manager: "pwncat.manager.Manager", args: "argparse.Namespace"): from io import TextIOWrapper from enum import Enum, auto from typing import Dict, List, Type, Callable, Iterable -from pathlib import Path from functools import partial import rich.text @@ -123,12 +122,13 @@ def parse(value, session: "pwncat.manager.Session"): return os.path.expanduser(value) return value + class RemoteFileParse(ParameterParse): def parse(value, session: "pwncat.manager.Session"): if value.startswith("~/") or value.startswith("~\\"): homedir = session.platform.getenv("HOME") if not homedir: - """ Windows support """ + """Windows support""" homedir = session.platform.getenv("USERPROFILE") if homedir: return homedir + value[1:] @@ -912,11 +912,11 @@ def get_completions(self, document: Document, complete_event: CompleteEvent): if path == "": path = "." - + if path.startswith("~"): homedir = self.manager.target.platform.getenv("HOME") if not homedir: - """ Windows support """ + """Windows support""" homedir = self.manager.target.platform.getenv("USERPROFILE") if homedir: path = homedir + path[1:] diff --git a/pwncat/commands/download.py b/pwncat/commands/download.py index 9b14576c..16e6ad39 100644 --- a/pwncat/commands/download.py +++ b/pwncat/commands/download.py @@ -14,7 +14,7 @@ import pwncat from pwncat import util from pwncat.util import console -from pwncat.commands import Complete, Parameter, CommandDefinition, ParseType +from pwncat.commands import Complete, Parameter, ParseType, CommandDefinition class Command(CommandDefinition): @@ -23,7 +23,9 @@ class Command(CommandDefinition): PROG = "download" ARGS = { "source": Parameter(Complete.REMOTE_FILE, parser=ParseType.REMOTE_FILE), - "destination": Parameter(Complete.LOCAL_FILE, nargs="?", parser=ParseType.LOCAL_FILE), + "destination": Parameter( + Complete.LOCAL_FILE, nargs="?", parser=ParseType.LOCAL_FILE + ), } def run(self, manager: "pwncat.manager.Manager", args): diff --git a/pwncat/commands/upload.py b/pwncat/commands/upload.py index 7d6d3bea..e6da9c68 100644 --- a/pwncat/commands/upload.py +++ b/pwncat/commands/upload.py @@ -23,9 +23,7 @@ class Command(CommandDefinition): ARGS = { "source": Parameter(Complete.LOCAL_FILE, parser=ParseType.LOCAL_FILE), "destination": Parameter( - Complete.REMOTE_FILE, - nargs="?", - parser=ParseType.REMOTE_FILE + Complete.REMOTE_FILE, nargs="?", parser=ParseType.REMOTE_FILE ), }