From bbf49e4c72c57ec280c8efe27598e9edc95990bb Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Thu, 4 Jun 2020 19:35:57 -0400 Subject: [PATCH] Updated password enumeration --- pwncat/enumerate/passwords.py | 77 +++++++++++++++++++---------------- pwncat/enumerate/suid.py | 5 ++- pwncat/remote/victim.py | 41 +++++++++++++++++-- 3 files changed, 83 insertions(+), 40 deletions(-) diff --git a/pwncat/enumerate/passwords.py b/pwncat/enumerate/passwords.py index 21be878..51acd12 100644 --- a/pwncat/enumerate/passwords.py +++ b/pwncat/enumerate/passwords.py @@ -50,6 +50,8 @@ def enumerate() -> Generator[FactData, None, None]: # The locations we will search in for passwords locations = ["/var/www", "$HOME", "/opt", "/etc"] + # Known locations which match this search but don't contain useful entries + blacklist = ["openssl.cnf", "libuser.conf"] # The types of files which are "code". This means that we only recognize the # actual password if it is a literal value (enclosed in single or double quotes) code_types = [".c", ".php", ".py", ".sh", ".pl", ".js", ".ini", ".json"] @@ -71,46 +73,51 @@ def enumerate() -> Generator[FactData, None, None]: password = None - # Check for simple assignment - match = re.search(r"password\s*=(.*)", content, re.IGNORECASE) - if match is not None: - password = match.group(1).strip() + # Ensure this file isn't in our blacklist + # We will still report it but it won't produce actionable passwords + # for privesc because the blacklist files have a high likelihood of + # false positives. + if os.path.basename(path) not in blacklist: + # Check for simple assignment + match = re.search(r"password\s*=(.*)", content, re.IGNORECASE) + if match is not None: + password = match.group(1).strip() - # Check for dictionary in python with double quotes - match = re.search(r"password[\"']\s*:(.*)", content, re.IGNORECASE) - if match is not None: - password = match.group(1).strip() + # Check for dictionary in python with double quotes + match = re.search(r"password[\"']\s*:(.*)", content, re.IGNORECASE) + if match is not None: + password = match.group(1).strip() - # Check for dictionary is perl - match = re.search(r"password[\"']?\s+=>(.*)", content, re.IGNORECASE) - if match is not None: - password = match.group(1).strip() + # Check for dictionary is perl + match = re.search(r"password[\"']?\s+=>(.*)", content, re.IGNORECASE) + if match is not None: + password = match.group(1).strip() - # Don't mark empty passwords - if password is not None and password == "": - password = None - - if password is not None: - _, extension = os.path.splitext(path) - - # Ensure that this is a constant string. For code file types, - # this is normally indicated by the string being surrounded by - # either double or single quotes. - if extension in code_types: - if password[-1] == ";": - password = password[:-1] - if password[0] == '"' and password[-1] == '"': - password = password.strip('"') - elif password[0] == "'" and password[-1] == "'": - password = password.strip("'") - else: - # This wasn't assigned to a constant, it's not helpful to us - password = None - - # Empty quotes? :( - if password == "": + # Don't mark empty passwords + if password is not None and password == "": password = None + if password is not None: + _, extension = os.path.splitext(path) + + # Ensure that this is a constant string. For code file types, + # this is normally indicated by the string being surrounded by + # either double or single quotes. + if extension in code_types: + if password[-1] == ";": + password = password[:-1] + if password[0] == '"' and password[-1] == '"': + password = password.strip('"') + elif password[0] == "'" and password[-1] == "'": + password = password.strip("'") + else: + # This wasn't assigned to a constant, it's not helpful to us + password = None + + # Empty quotes? :( + if password == "": + password = None + # This was a match for the search. We may have extracted a # password. Either way, log it. yield Password(path, password, lineno, ":".join(line), []) diff --git a/pwncat/enumerate/suid.py b/pwncat/enumerate/suid.py index f6bdc45..e56eddb 100644 --- a/pwncat/enumerate/suid.py +++ b/pwncat/enumerate/suid.py @@ -45,7 +45,10 @@ def enumerate() -> Generator[Binary, None, None]: # Spawn a find command to locate the setuid binaries with pwncat.victim.subprocess( - "find / -perm -4000 -printf '%U %p\\n' 2>/dev/null", mode="r", no_job=True + ["find", "/", "-perm", "-4000", "-printf", "%U %p\\n"], + stderr="/dev/null", + mode="r", + no_job=True, ) as stream: for path in stream: # Parse out owner ID and path diff --git a/pwncat/remote/victim.py b/pwncat/remote/victim.py index 2be0365..c424267 100644 --- a/pwncat/remote/victim.py +++ b/pwncat/remote/victim.py @@ -1030,6 +1030,9 @@ class Victim: exit_cmd: str = None, no_job=False, name: str = None, + env: Dict[str, str] = None, + stdout: str = None, + stderr: str = None, ) -> Union[io.BufferedRWPair, io.BufferedReader]: """ Start a process on the remote host and return a file-like object @@ -1053,11 +1056,41 @@ class Victim: delimeter. :param no_job: whether to run as a sub-job in the shell (only used for "r" mode) :param name: the name assigned to the output file object + :param env: environment variables to set for this command + :param stdout: a string specifying the location to redirect standard out (or None) + :param stderr: a string specifying where to redirect stderr (or None) :return: Union[BufferedRWPair, BufferedReader] """ + # Join the command if it was supplied as a list if isinstance(cmd, list): - cmd = shlex.join(cmd) + if self.which(cmd[0]) is not None: + # We don't want to prevent things like aliases from working + # but if the given command exists in our database, replace + # it with the path retrieved from which. + cmd[0] = self.which(cmd[0]) + cmd = util.join(cmd) + + # Add environment to the beginning of the command if requested + if env is not None: + cmd = ( + " ".join( + [ + f"{util.quote(name)}={util.quote(value)}" + for name, value in env.items() + ] + ) + + cmd + ) + + # Redirect stdout, this is useful when the command + # is passed as a list vice a command string + if stdout is not None: + cmd += f" >{stdout}" + + # Redirect stderr for the same reason as above + if stderr is not None: + cmd += f" 2>{stderr}" for c in mode: if c not in "rwb": @@ -1241,7 +1274,9 @@ class Victim: if util.Access.EXECUTE not in access: raise PermissionError - with self.subprocess("ls --color=never --all -1", "r") as pipe: + with self.subprocess( + ["ls", "--color=never", "--all", "-1"], stderr="/dev/null", mode="r" + ) as pipe: for line in pipe: line = line.strip().decode("utf-8") yield line @@ -1368,8 +1403,6 @@ class Victim: if method.stream is Stream.RAW: sub_mode += "b" - print("payload", payload) - # Run the payload on the remote host. pipe = self.subprocess( payload,