跳转到帖子

授权测试中如何判断 sudo systemctl 权限是否可突破 root 边界?

精选回复

发布于

问题背景

在一次已授权的 Linux 内网测试中,业务账号执行 sudo -l 后显示可免密运行 systemctl restart app.service。怎样判断这只是运维权限,还是能够间接影响 root 权限?验证时如何避免直接写入恶意服务,并保留可复核的证据?

这类问题不能只看 sudoers 中允许了哪个命令。关键是继续检查该服务引用的单元文件、启动脚本、环境文件和工作目录是否可由低权限账号修改。如果 root 启动的服务加载了账号可写内容,即使该账号只能执行 restart,也可能形成完整的权限边界突破链路。

问题定位

先确认测试身份、sudo 规则和命令参数约束。建议使用非交互方式,避免因输入密码改变现场状态。

id
sudo -n -l
sudo -V | head

# 只查询目标服务,不执行重启
systemctl show app.service \
  -p FragmentPath \
  -p DropInPaths \
  -p User \
  -p Group \
  -p ExecStart \
  -p ExecStartPre \
  -p ExecStartPost \
  -p EnvironmentFiles \
  -p WorkingDirectory

systemctl cat app.service

重点区分以下 sudoers 写法:

# 风险相对可控:服务名和动作均被固定
appuser ALL=(root) NOPASSWD: /usr/bin/systemctl restart app.service

# 风险较高:通配符可能匹配其他服务或额外参数
appuser ALL=(root) NOPASSWD: /usr/bin/systemctl * app*

# 高风险:允许编辑、链接、启停任意单元
appuser ALL=(root) NOPASSWD: /usr/bin/systemctl *

还要核对 systemctl 的实际路径。sudoers 中记录的是 /bin/systemctl,但系统实际调用 /usr/bin/systemctl 时,规则是否匹配取决于发行版、符号链接和 sudo 配置,不能仅凭文本推断。

技术分析

一条可成立的风险链通常需要同时满足三个条件:

  • 业务账号能够以 root 身份重启或启动目标服务。
  • 服务最终执行或加载的某个文件可被业务账号修改。
  • 相关进程以 root 运行,且修改内容会在重启时生效。

需要检查的对象不只是 app.service 本身,还包括:

  • FragmentPath 指向的主单元文件。
  • /etc/systemd/system/app.service.d/ 下的 drop-in 配置。
  • ExecStart、ExecStartPre、ExecStartPost 引用的脚本或二进制文件。
  • EnvironmentFile 引用的环境变量文件,尤其是变量参与命令、解释器参数或模块搜索路径时。
  • WorkingDirectory、插件目录、动态库目录和配置文件目录。
  • 路径上每一级父目录的写权限、ACL 和组权限。

操作步骤

第一步,对 systemd 展开后的配置进行确认。不要只读取 /etc/systemd/system/app.service,因为服务可能来自 /usr/lib/systemd/system,并被 drop-in 覆盖。

systemctl cat app.service
systemctl show app.service -p FragmentPath -p DropInPaths
systemd-analyze verify app.service 2>&1

第二步,提取所有执行入口和外部引用。以下命令只读取配置,不修改服务状态:

systemctl show app.service \
  -p ExecStart -p ExecStartPre -p ExecStartPost \
  -p EnvironmentFiles -p WorkingDirectory

systemctl cat app.service | grep -E \
  '^(ExecStart|ExecStartPre|ExecStartPost|EnvironmentFile|WorkingDirectory|User|Group)='

第三步,逐级检查路径权限。仅执行 ls -l 往往会漏掉可写父目录和 ACL。

# 示例路径应替换成实际取到的文件
namei -l /opt/app/bin/start.sh
namei -l /etc/app/app.env

stat -c '%A %a %U:%G %n' \
  /opt/app/bin/start.sh \
  /etc/app/app.env

getfacl -p /opt/app/bin/start.sh /etc/app/app.env

# 判断当前账号是否拥有实际写权限
for f in /opt/app/bin/start.sh /etc/app/app.env; do
  if test -w "$f"; then
    echo "WRITABLE: $f"
  else
    echo "not writable: $f"
  fi
done

第四步,检查脚本内部是否继续调用相对路径、可写解释器模块或外部配置。例如 root 服务执行 /opt/app/bin/start.sh,但脚本中使用 ./worker、source ./app.env 或未写绝对路径的 python,都需要继续追踪。

sed -n '1,200p' /opt/app/bin/start.sh
file /opt/app/bin/start.sh
head -n 1 /opt/app/bin/start.sh

# 查找相对调用和高风险环境变量引用
grep -nE '(^|[[:space:]])(source|\.|python|python3|node|java|bash|sh)[[:space:]]|PATH=|PYTHONPATH=|LD_' \
  /opt/app/bin/start.sh

第五步,确认服务运行身份和当前进程状态。单元未配置 User 时,系统级服务通常默认使用 root,但仍应以实际结果为准。

systemctl show app.service -p User -p Group -p DynamicUser
systemctl status app.service --no-pager
ps -eo user,group,pid,ppid,lstart,args | grep '[a]pp.service'

如何做最小化验证

在生产或共享环境中,不建议通过替换启动脚本、插入反弹命令或创建特权文件来证明问题。通常以下证据已经足以形成可复核结论:

  • sudo -n -l 证明账号可由 root 执行固定的 restart 操作。
  • systemctl show 或 systemctl cat 证明目标服务以 root 身份加载某个文件。
  • namei、stat 和 getfacl 证明当前账号可修改该文件或其父目录。
  • 服务配置证明该文件会在 restart 时被重新读取或执行。

如果项目明确要求动态验证,应先在隔离克隆环境中进行,并将动作限制为创建无敏感内容的临时标记文件。验证后恢复原始哈希、属主、权限和服务状态。不要在生产环境中修改真实启动链。

测试前可以记录关键文件基线:

date -Is
sha256sum \
  /etc/systemd/system/app.service \
  /opt/app/bin/start.sh \
  /etc/app/app.env 2>/dev/null

stat \
  /etc/systemd/system/app.service \
  /opt/app/bin/start.sh \
  /etc/app/app.env 2>/dev/null

日志留痕与事件复核

sudo 日志通常能够记录业务账号执行了 systemctl,但不一定完整记录 systemctl 后续启动的子进程。因此应同时检查认证日志、systemd 日志和 auditd 记录。

# Debian、Ubuntu 常见位置
grep -E 'sudo:.*(systemctl|app.service)' /var/log/auth.log

# RHEL、Rocky、AlmaLinux 常见位置
grep -E 'sudo:.*(systemctl|app.service)' /var/log/secure

# systemd 日志
journalctl -u app.service --since '24 hours ago' --no-pager
journalctl SYSLOG_IDENTIFIER=sudo --since '24 hours ago' --no-pager

# auditd 已部署时
ausearch -m USER_CMD,EXECVE,SERVICE_START,SERVICE_STOP \
  -ts recent -i | grep -E 'systemctl|app.service'

为了后续持续监控,可由管理员为关键单元和脚本添加 auditd 规则。规则部署前应评估日志量,并使用实际路径:

# /etc/audit/rules.d/app-service.rules
-w /etc/systemd/system/app.service -p wa -k app_unit_change
-w /etc/systemd/system/app.service.d/ -p wa -k app_unit_change
-w /opt/app/bin/start.sh -p wa -k app_exec_change
-w /etc/app/app.env -p wa -k app_env_change

# 管理员加载并检查规则
auditctl -R /etc/audit/rules.d/app-service.rules
auditctl -l | grep -E 'app_unit_change|app_exec_change|app_env_change'

需要注意,shell history 不是可靠审计来源:它可能未及时落盘,也可能被禁用或清理。正式报告应优先引用 sudo、journald、auditd 和文件元数据。

风险判定

满足“可 sudo 重启 root 服务”并不必然等于可提升权限。可以按以下方式定级:

  • 高风险:账号可修改 root 服务的单元文件、启动脚本、drop-in、环境文件或可执行文件,并可触发重启。
  • 中风险:账号不能修改执行链,但 sudoers 使用宽泛通配符,可能匹配其他服务、动作或参数。
  • 较低风险:命令和服务名被严格固定,所有加载文件均由 root 持有且低权限账号不可写,服务自身也不存在可利用的管理接口。

报告中应明确写出完整因果链,而不是只写“存在 sudo 提权”。例如:业务账号 appuser 可免密执行 systemctl restart app.service;该服务以 root 运行;ExecStart 引用 /opt/app/bin/start.sh;该脚本归 appgroup 所有且组可写;appuser 属于 appgroup。因此账号可控制 root 服务下一次启动时的执行内容。

修复建议

  • 将单元文件、drop-in、启动脚本、环境文件及其父目录统一设置为 root 持有,并移除业务组写权限。
  • 避免 root 服务从 /home、/tmp、共享发布目录或应用账号可写目录加载代码。
  • sudoers 中固定 systemctl 的绝对路径、动作和完整服务名,避免使用 * 通配符。
  • 更稳妥的做法是提供 root 持有的包装脚本,由脚本校验服务名、配置状态和调用来源,再执行单一操作。
  • 修改 sudoers 时使用 visudo -c 校验,避免语法错误导致权限异常。
  • 对关键服务启用 sudo 日志集中采集,并使用 auditd 或同类机制监控执行链文件的写入和属性变化。
  • 如果业务并不需要 root 身份,使用 User、Group、DynamicUser、NoNewPrivileges 和 ProtectSystem 等 systemd 隔离选项降低影响。
# 修复后的权限示例,具体路径和组应按业务调整
chown root:root /etc/systemd/system/app.service
chown root:root /opt/app/bin/start.sh /etc/app/app.env
chmod 0644 /etc/systemd/system/app.service /etc/app/app.env
chmod 0755 /opt/app/bin/start.sh

# 校验配置
systemd-analyze verify app.service
visudo -c
systemctl daemon-reload

总结

判断 sudo systemctl 是否突破 root 边界,核心不是“能不能重启服务”,而是“低权限账号能否控制该 root 服务加载的任何输入”。排查时应沿 sudo 规则、systemd 展开配置、执行文件、环境文件、父目录权限和审计日志逐层验证。授权测试中优先使用静态证据闭环,只有在隔离环境和明确批准下才进行动态验证。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

判断重点不是“能不能执行 systemctl”,而是 sudoers 放行了哪些子命令、哪些 unit,以及这些 unit 的依赖链里是否存在低权限用户可写内容。建议按下面顺序排查,尽量先做静态验证。

1. 确认 sudoers 的精确匹配范围

sudo -l
sudo -V | grep -E 'Environment|secure_path'
command -v systemctl
readlink -f "$(command -v systemctl)"

风险大致可以这样分:

  • systemctl restart fixed.service:通常只需要审计这个 unit 及其依赖链。
  • systemctl * fixed.service:需关注 edit、set-property 等子命令是否也被通配。
  • systemctl *:高风险,可能允许加载、修改或启动攻击者可控 unit。
  • sudoers 中参数使用了通配符:注意空格、实例化 unit、路径和额外参数是否都能被匹配。

同时确认是否有多个 systemctl 路径,避免只检查了 /usr/bin/systemctl,实际 sudoers 放行的是另一个路径。

2. 对允许操作的 unit 做完整展开

systemctl cat fixed.service
systemctl show fixed.service \
  -p FragmentPath -p DropInPaths \
  -p ExecStart -p ExecStartPre -p ExecStartPost \
  -p ExecReload -p ExecStop -p EnvironmentFiles \
  -p User -p Group -p WorkingDirectory

需要重点看:

  • ExecStart*、ExecReload、ExecStop* 指向的脚本或二进制是否可写。
  • EnvironmentFile=、WorkingDirectory=、配置文件、插件目录是否可写。
  • 是否通过 shell 执行脚本,脚本内部又调用了相对路径命令或加载可写文件。
  • 服务是否默认以 root 运行;未设置 User= 时通常就是 root。
  • 是否存在模板 unit,例如放行 app@*.service 后,实例名会不会进入脚本参数或路径。

对所有相关路径检查整条目录链,而不只是文件本身:

namei -l /path/to/service-script
stat -c '%A %U:%G %n' /path/to/service-script
getfacl -p /path/to/service-script
findmnt -T /path/to/service-script -o TARGET,SOURCE,FSTYPE,OPTIONS

常见误判是脚本本身不可写,但其父目录可写,用户可以替换文件;或者文件有 ACL,单看 ls -l 看不出来。

3. 检查 drop-in、unit 搜索路径和生成器

systemd-analyze unit-paths
systemctl show fixed.service -p FragmentPath -p DropInPaths
find /etc/systemd/system /run/systemd/system /usr/local/lib/systemd/system \
  -maxdepth 3 -type f -o -type d 2>/dev/null | while read p; do
    test -w "$p" && printf 'writable: %s\n' "$p"
  done

find /etc/systemd/system-generators /run/systemd/system-generators \
     /usr/local/lib/systemd/system-generators \
  -maxdepth 1 -type f -perm -0020 2>/dev/null

如果低权限用户能写入 unit 文件、drop-in 目录、systemd generator,且又能执行 daemon-reload 和启动/重启相应服务,基本就已经跨过 root 边界。这里还要检查符号链接最终落点,不能只看 unit 表面路径。

4. 区分各子命令的实际能力

  • start/restart/reload:主要风险来自现有 unit 的可写执行链。
  • daemon-reload:自身通常不执行普通服务,但会重新读取 unit、drop-in 和 generator;与可写配置组合时风险很高。
  • edit:若能以 root 创建 override,通常可改变服务执行行为。
  • link/enable:可能把攻击者控制的 unit 纳入 systemd 管理范围,需结合允许的后续启动命令判断。
  • set-property:可能创建持久化或运行时 drop-in,具体可设置项要在目标版本实测。
  • status:重点检查 pager。不要只根据网上的 pager escape 结论判断,sudo 的环境清理、systemd 版本及 SYSTEMD_PAGERSECURE 都会影响结果。

可以在不进入交互 pager 的情况下检查输出行为:

sudo SYSTEMD_PAGER=cat systemctl status fixed.service
sudo -l | grep -E 'SETENV|NOSETENV|systemctl'

5. 别漏掉间接触发链

允许重启一个固定服务,也可能间接触发其他 root 单元。建议查看依赖和触发关系:

systemctl list-dependencies fixed.service --all
systemctl show fixed.service \
  -p Requires -p Wants -p BindsTo -p PartOf \
  -p Triggers -p TriggeredBy
systemctl list-unit-files | grep -E '\.(path|timer|socket|service)'

尤其关注 .path、.timer、.socket 激活,以及服务脚本是否会处理用户可写目录中的文件。

6. 给出结论时建议采用“能力组合”而不是只报命令

例如:

  • 只有固定 unit 的 restart,且 unit、脚本、环境文件、父目录均不可写,参数不受用户输入影响:通常不能直接突破。
  • 固定 unit 可重启,但 root 服务调用了用户可写脚本或可替换二进制:可突破,根因是执行链权限错误。
  • 允许通配 systemctl 子命令,并可执行 edit/link/enable/daemon-reload/start 的有效组合:应按可获得 root 代码执行处理。
  • 仅允许 status:需单独验证 pager 和环境保留策略,不能直接定性。

授权测试中,证明阶段最好在快照或隔离主机上完成,并使用约定的无副作用证据,例如记录审计日志、检查 unit 解析结果和文件写权限。生产机上不要通过修改现有业务 unit 来“验证”,因为一次 daemon-reload 或 restart 就可能影响实际服务。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。