Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .docker/Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ RUN apk add --no-cache \

RUN adduser -D secator

# Allow runing 'nmap' with sudo; for priviledged scans
RUN echo "secator ALL=(ALL) NOPASSWD: /usr/bin/nmap *" >> /etc/sudoers && \
echo "secator ALL=(ALL) NOPASSWD: /usr/bin/arp-scan *" >> /etc/sudoers && \
echo "secator ALL=(ALL) NOPASSWD: /sbin/arp *" >> /etc/sudoers
# Raw-socket tools (nmap/arp-scan/arp) need CAP_NET_RAW/CAP_NET_ADMIN.
# full flavor: no baked-in sudoers grant here -- tools are installed below, then
# switched to file capabilities (see the setcap block after tool install). A
# `NOPASSWD: /usr/bin/nmap *` sudoers entry is a GTFObins root shell (a shell
# injection in a worker can `sudo nmap --script=/tmp/x.nse ...` -> root).
# lite flavor: tools are installed at runtime, so there are no baked binaries to
# setcap yet -- keep the old sudo grants here. Prefer the full image in prod.
RUN if [ "$flavor" = "lite" ]; then \
echo "secator ALL=(ALL) NOPASSWD: /usr/bin/nmap *" >> /etc/sudoers && \
echo "secator ALL=(ALL) NOPASSWD: /usr/bin/arp-scan *" >> /etc/sudoers && \
echo "secator ALL=(ALL) NOPASSWD: /sbin/arp *" >> /etc/sudoers; \
fi

# Allow user to install commands
RUN echo "secator ALL=(ALL) NOPASSWD: /sbin/apk add *" >> /etc/sudoers && \
Expand Down Expand Up @@ -74,6 +82,19 @@ RUN if [ "$flavor" != "lite" ]; then \
sed -i '/secator ALL=(ALL) NOPASSWD: \/usr\/bin\/flock \/tmp\/install.lock apk add \*/d' /etc/sudoers && \
sed -i '/secator ALL=(ALL) NOPASSWD: \/usr\/bin\/flock \/tmp\/install.lock ln -sf \/usr\/lib\/libpcap.so/d' /etc/sudoers; \
fi

# full flavor: grant CAP_NET_RAW/CAP_NET_ADMIN directly on the raw-socket tool
# binaries instead of sudo (no baked sudoers entry to abuse via GTFObins).
RUN if [ "$flavor" != "lite" ]; then \
apk add --no-cache libcap && \
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
setcap cap_net_raw,cap_net_admin+eip /usr/bin/arp-scan && \
setcap cap_net_raw,cap_net_admin+eip /sbin/arp && \
setcap cap_net_raw,cap_net_admin+eip /home/secator/.local/bin/naabu; \
fi
USER secator

# full flavor: switch secator to caps mode so requires_sudo commands run unprivileged.
RUN if [ "$flavor" != "lite" ]; then secator config set security.privileged_mode caps; fi

ENTRYPOINT ["secator"]
1 change: 1 addition & 0 deletions secator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Security(StrictModel):
auto_install_commands: bool = True
force_source_install: bool = False
prompt_sudo_password: bool = True
privileged_mode: str = 'sudo' # 'sudo' (prepend sudo) or 'caps' (rely on file capabilities)


class HTTP(StrictModel):
Expand Down
4 changes: 2 additions & 2 deletions secator/runners/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ def __init__(self, inputs=[], **run_opts):
# Run on_cmd hook
self.run_hooks('on_cmd', sub='init')

# Add sudo to command if it is required
if self.requires_sudo:
# Add sudo to command if it is required (skip when relying on file capabilities instead)
if self.requires_sudo and CONFIG.security.privileged_mode != 'caps':
self.cmd = f'sudo {self.cmd}'
if self._has_sensitive_cmd_opts:
self.cmd_redacted = f'sudo {self.cmd_redacted}'
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,41 @@ class TaskOverrideB(Command):
self.assertEqual(cmd_b.input_chunk_size, 100)
finally:
CONFIG.tasks.overrides = original_overrides


class TestCommandPrivilegedMode(unittest.TestCase):
"""Gate test for security.privileged_mode: 'caps' must not prepend sudo, 'sudo' (default) must."""

def test_sudo_mode_prepends_sudo(self):
from secator.config import CONFIG

class SudoTestCmd(Command):
cmd = 'test'
requires_sudo = True
file_flag = None

original_mode = CONFIG.security.privileged_mode
CONFIG.security.privileged_mode = 'sudo'
try:
with unittest.mock.patch('secator.runners.task.Task.get_task_class', return_value=SudoTestCmd):
cmd = SudoTestCmd(['target1'])
self.assertTrue(cmd.cmd.startswith('sudo '))
finally:
CONFIG.security.privileged_mode = original_mode

def test_caps_mode_does_not_prepend_sudo(self):
from secator.config import CONFIG

class CapsTestCmd(Command):
cmd = 'test'
requires_sudo = True
file_flag = None

original_mode = CONFIG.security.privileged_mode
CONFIG.security.privileged_mode = 'caps'
try:
with unittest.mock.patch('secator.runners.task.Task.get_task_class', return_value=CapsTestCmd):
cmd = CapsTestCmd(['target1'])
self.assertFalse(cmd.cmd.startswith('sudo '))
finally:
CONFIG.security.privileged_mode = original_mode
Loading