1. 拿到低权限Shell后,优先排查的"隐形提权面"
先聊个很实际的问题:当你费了九牛二虎之力拿下一个低权限Shell,下一步干什么?大多数人第一反应是翻数据库连接配置、找SUID文件、查内核漏洞。这些思路本身没问题,但容易忽略真正的"捷径"——很多管理员图省事留下的配置习惯,反而成了比内核漏洞稳定得多的提权通道。
我复盘过不少真实项目,发现一个规律:Cron任务、PATH变量、NFS服务这三类问题,出现的频率比想象中高得多。它们有一个共同点:都不是靠某个复杂漏洞打进去的,而是利用了系统运维层面"想当然"的信任关系。更关键的是,这些问题在漏洞扫描器里往往不显眼,但人工排查时几乎一抓一个准。
先交代一下背景:本文讨论的"提权",特指在已获得一个低权限账户(比如www-data或普通业务用户)的前提下,通过系统配置缺陷将权限提升到root。整个过程都依赖于系统本身的设计特性,不涉及任何0day,也不依赖内核漏洞。为了把这几个问题讲透,我从三个方面展开:Cron任务为什么是提权重灾区、PATH变量劫持的技术原理、NFS服务配置不当的利用链,最后给出防御侧的排查加固清单。
如果你是第一次接触提权,也不用慌。前两年我在整理内网安全测试笔记时,专门把这三类问题归为"配置类提权三板斧"——因为它们不要求你有多深的底层功底,但对细节的观察力要求很高。只要耐心把下面的每个场景在测试环境里过一遍,后面遇到真实系统时基本能形成肌肉记忆。
2. Cron定时任务里的权限继承陷阱
2.1 Cron任务提权的三个先决条件
Cron任务提权的本质,是root用户通过cron机制创建了周期性执行的脚本,但脚本或脚本依赖的文件存在可被低权限用户利用的缺陷。这句话拆开看,至少要同时满足三个条件:
- 脚本以root权限运行(crontab里没有指定用户,默认是root)
- 脚本中调用的某个文件、命令或通配符展开逻辑存在可控点
- 低权限用户对可控点具备写入或影响能力
先看一个最常见的场景。管理员习惯把备份、日志清理等任务写在/etc/crontab或/etc/cron.d/中,脚本放在/usr/local/bin/或/opt/scripts/下,权限设置随意。比如:
*/5 * * * * root /usr/local/bin/backup.sh此时如果你能拿到该脚本的写权限(比如管理员用了chmod 777或者脚本所在目录权限是/tmp这类世界可写的),提权几乎就完成了一半。方法非常简单:直接把脚本内容替换成你要执行的命令,等下一个周期来临,root就会带着你的命令执行。
这种场景的排查思路其实不难:拿到shell后,先ls -la看脚本权限,如果发现owner是root但other用户有写权限,基本就是白送的分。但实际项目中更常见的是下面这种情况——脚本权限看着很安全,root才可读写,但脚本内部调用了不存在的"外部命令"或者使用了不安全的通配符。
2.2 通配符注入:tar、chmod 和 rsync 的隐藏风险
通配符注入(Wildcard Injection)是个被低估的技巧。很多人以为脚本限制了写权限就万事大吉,但脚本中如果有tar czf /backup/*.tar.gz /data/*这样的写法,配合文件系统允许低权限用户创建文件名,一样存在提权可能。
原理很简单:tar命令支持--checkpoint-action参数,该参数可以指定在归档过程中执行系统命令。而tar在展开通配符时,会把文件名当作参数解析。攻击者只需要在/data/目录下创建一个名为--checkpoint=1的文件,再创建一个名为--checkpoint-action=exec=sh -c '命令'的文件,当root用户的cron脚本执行到这一行时,tar就会把这两个文件名当作选项解析,从而执行攻击者注入的命令。
当年实战中我用这个法子,在一台备份服务器上拿到了root权限。管理员备份脚本长下面这样:
#!/bin/bash tar czf /backup/$(date +%Y%m%d).tar.gz /var/www/html/* cd /tmp && tar czf /tmp/upload.tar.gz *注意第二个脚本:cd /tmp后打整个tmp目录,而/tmp是任何人都能写入的。拿到低权限shell后,我做了两件事:
cd /tmp touch -- "--checkpoint=1" touch -- "--checkpoint-action=exec=sh -c 'echo \"www-data ALL=(ALL) NOPASSWD:ALL\" >> /etc/sudoers'"等cron周期触发后,sudoers被成功写入,之后直接sudo -i就拿到了root。这个攻击路径的关键在于:echo在tar进程(root权限)上下文中执行,读写文件的权限也继承为root,不需要任何交互。
防御这道攻击最直接的方式是让脚本不依赖通配符,或者用find -exec配合-print0替代裸通配符,另一道防线是tar --wildcards前先对参数做引号处理,但引号只保护了路径本身,没有实际拦截选项注入的逻辑。
2.3 Cron环境变量和绝对路径缺失
写Cron脚本时,很多人默认脚本里用到的命令(如python、curl、wget)都能被找到。但Cron有一个和用户登录Shell不同的地方:cron执行脚本的环境变量非常精简,PATH一般只有/usr/bin:/bin,而且脚本不会加载用户登录时的.bash_profile或.bashrc。
这个"精简PATH"环境,本身就是提权的温床。如果root的cron脚本里写的是:
* * * * * root /usr/local/sbin/test.py而test.py第一行是#!/usr/bin/python3,注意这个shebang强制指定了完整路径,那还好。但如果脚本内部调用了system("curl ...")或者os.system("tar ...")这类依赖环境变量找命令的方式,而该目录下恰好有一个同名的可执行文件可被低权限用户写入,系统就会优先执行攻击者植入的版本。
这就引出了本文第三节要展开的PATH变量问题。CRON和PATH变量往往是耦合在一起的,实战中看到cron任务先别急着改脚本,先扫一眼脚本调用的外部命令是否使用了绝对路径,这是一个非常关键的习惯。
3. PATH变量劫持:环境变量里的"同名骗局"
3.1 为什么PATH变量能决定提权成败
PATH变量决定了shell在查找命令时,按什么顺序去哪些目录寻找。以root身份执行某个命令时,如果命令没有写全路径,系统会按照PATH排列的目录顺序逐一查找,先找到哪个就用哪个。
这里有三个核心事实:
- 不是所有程序启动时都继承当前Shell的PATH,cron、systemd等服务都有自己的默认PATH
- 即使继承当前Shell的PATH,如果攻击者能把一个高权限目录前面的目录变成可写,就能通过放置同名可执行文件"劫持"后续的命令查找
- 网络服务(比如web服务)传给CGI脚本的环境变量,往往也包含可以被控制的内容
在真实项目中,我遇到次数最多的是两种场景。第一种是root通过cron跑一个脚本,脚本调用了ssh或scp,而攻击者在/tmp下放置了一个同名ssh脚本;第二种是程序里用system("ps aux")这类函数,而$PATH的第一个目录恰好是当前用户可写的。
3.2 反向利用脚本内相对路径
来看一个具体的例子。假设管理员写了一个cron脚本,每五分钟统计一次系统进程并生成报告:
#!/bin/bash ps aux > /var/www/html/report.txt这个脚本本身是root所有,权限是755,不可写。但它调用ps命令时没有写绝对路径/bin/ps,而/tmp恰好被管理员错误地加入了root的PATH(这种情况不多,但真的存在),或者脚本没有在开头声明PATH=/usr/bin:/bin,而Cron的PATH又包含了一些可以被利用的目录,此时攻击者就可以这样做:
cat > /tmp/ps << 'EOF' #!/bin/bash cp /bin/bash /tmp/bashroot chmod 4755 /tmp/bashroot /bin/ps "$@" EOF chmod 755 /tmp/ps当cron再次执行脚本时,实际运行的是攻击者放在/tmp下的ps,它会先完成提权操作(复制bash并设置SUID位),再正常输出进程列表,不让管理员察觉异样。几分钟后,攻击者只需/tmp/bashroot -p来获得root shell。
这里要注意几个细节:-p参数是为了在bash以SUID方式运行时保持euid为root,否则bash会自动把euid降回普通用户。很多人以为只要复制了bash就万事大吉,结果运行后还是普通用户,就是没加-p。这种坑我在测试时踩过不止一次。
3.3 两种PATH劫持的触发场景对比
为了帮你快速判断一个PATH类问题是否可以利用,我用表格总结一下最常见的触发条件和排查方向:
| 触发场景 | 脚本/程序示例 | 利用关键点 | 防御建议 |
|---|---|---|---|
| Cron运行的脚本依赖PATH查找命令 | ps aux,scp,curl | 确认Cron的PATH是否包含世界可写目录,确认脚本内是否使用绝对路径 | 脚本开头声明PATH=/usr/bin:/bin,或者调用命令一律写绝对路径 |
程序调用了system()/exec()但未指定可执行文件全路径 | system("ls -la") | 能否控制当前用户的环境变量PATH,以及PATH里是否包含/tmp、. | 开发时使用/bin/ls这类全路径;启动服务时使用干净的env |
| SUID程序内部调用其他命令 | 老版本nmap的交互模式 | SUID程序运行时环境变量是否被过滤 | 系统更新,或对SUID列表做白名单审核 |
表格里的第三个场景值得多说一句。很多SUID程序为了避免环境变量攻击,会主动清空或重设环境变量,比如删除LD_PRELOAD、LD_LIBRARY_PATH等。但PATH本身有没有被清空,取决于程序实现。一个典型的例子是某些版本的screen和find曾被曝出可以通过PATH劫持来提权。所以在测试环境里,遇到有SUID位但不知道原理的程序,可以先strings一下看看有没有调用外部命令,再决定是否以PATH注入试探。
4. NFS服务配置不当:远程目录映射出的root权限
4.1 no_root_squash参数意味着什么
NFS(Network File System)本身是网络文件系统,用于在局域网内共享目录。但由于NFS有一套独特的"用户映射"机制,配置不当时,它会成为一条隐蔽的提权通道。
NFS的默认行为是root_squash:当客户端以root身份(uid 0)访问共享目录时,服务端会把这个root强制映射成一个低权限用户(如nobody),这样客户端root就无法在共享目录里创建拥有root权限的文件。这本来是极好的安全机制。
但偏偏有管理员为了方便,在/etc/exports里加上了no_root_squash:
/data 192.168.1.0/24(rw,sync,no_root_squash)这时候,任何在192.168.1.0/24网段内的客户端,以root身份挂载/data后,创建的文件在服务端看来也是root所有。如果这个共享目录里面再有一个可被root执行的脚本或SUID文件,提权路径就完全打通了。
我在一次授权测试中遇到的情况是:内网一台文件服务器的/etc/exports配置了no_root_squash,共享的是/srv/nfs目录。由于NFS默认允许root写文件,我直接在自己的攻击机上挂载该目录:
mkdir /mnt/nfs mount -t nfs 192.168.1.10:/srv/nfs /mnt/nfs挂载成功后,在/mnt/nfs下创建了一个SUID shell:
cp /bin/bash /mnt/nfs/pwn chmod 4755 /mnt/nfs/pwn然后回到被攻击服务器(已经拿到低权限shell),执行/srv/nfs/pwn -p,直接获得root。
这里有个前提要交代清楚:NFS服务本身需要有一个能读取共享目录的本地账户。如果共享目录的权限是root:root 700,那么低权限用户也访问不了。但实际中,管理员为了方便,经常把共享目录权限设计成777或755,这就给了利用空间。所以碰到NFS场景,别只顾着挂载,挂载完还要注意目录的写权限是否对目标系统上的低权限用户开放。
4.2 利用匿名映射的另一种形态
如果NFS没有配置no_root_squash,但配置了all_squash+anonuid/anongid,也存在另一种风险。管理员可能为了让所有客户端都以同一个用户身份访问共享资源,这样"省事"地配置:
/public *(rw,sync,all_squash,anonuid=1000,anongid=1000)此时即使客户端以root身份写入,在服务端看来也是uid 1000(普通用户)。但如果共享目录里存在原本属于root的、权限配置不当的文件,而这个uid 1000恰好能修改它们,风险依然存在。
举一个实际场景:如果共享目录下有一个root所有的脚本,其他用户具有写权限,攻击者以自己的普通用户身份挂载NFS目录,修改该脚本内容。等目标服务器上root执行该脚本时,提权完成。
4.3 判断和管理NFS共享的排查清单
遇到NFS时,不要急着尝试利用,先做下面这几步,梳理清楚目录权限和映射规则:
- 在被攻击机器上执行
showmount -e localhost查看当前开放了哪些NFS共享 - 查看
/etc/exports确认挂载选项,重点看是否有no_root_squash、all_squash、rw等敏感设置 - 挂载后
ls -lan看实际文件权限,确认当前登录用户能能不能写 - 如果目标机器无法执行
mount,可以在攻击机上挂载,再从目的服务器上读取共享内容
这套链路走完后,你会对NFS配置的风险有非常直观的认识。
5. 防御视角:如何把这三个提权通道一次堵死
写到这里,可能已经有读者开始紧张:那我自己的服务器是不是也有类似问题?别急,前面是攻击视角,现在切换到防御侧,把排查和加固的具体手段都过一遍。
5.1 Cron任务的加固清单
对Cron任务做审计,我最推荐的做法是写一个脚本循环遍历所有crontab文件,检查每一行任务的脚本路径是否存在权限异常:
for crontab_file in /etc/crontab /etc/cron.d/* /var/spool/cron/*; do echo "=== $crontab_file ===" grep -v '^#' "$crontab_file" 2>/dev/null | grep -v '^$' | while read line; do script=$(echo "$line" | awk '{print $NF}') # 这里简单检查脚本或指向路径的权限 if [ -f "$script" ]; then stat -c "%a %U %n" "$script" fi done done在实际运维中,我一般对Cron任务做三条硬性规定:
- 脚本内所有外部命令统一使用绝对路径
- 脚本目录和脚本本身的权限控制在
750或更严,owner为root,group为专用管理组 - 禁止把Cron任务指向
/tmp、/var/tmp、/home/*这些低权限用户可写的目录
另外,通配符注入的防御一定要单独说明。不要在Cron脚本里用裸通配符对接tar、chmod、rsync这类支持选项注入的命令。如果需要处理目录下所有文件,更安全的写法是使用find配合-exec和+,或者把文件名循环读出来再传给命令:
find /data -type f -name "*.txt" -print0 | xargs -0 chmod 644这种写法从机制上杜绝了文件名被当作命令行选项解析的可能。
5.2 PATH变量风险的加固思路
PATH变量劫持的防御,核心原则只有一条:别让"当前目录"和"世界可写目录"出现在高权限执行的PATH中。
具体检查时,我一般会分三块:
- 查看
/etc/crontab中是否有PATH声明。如果系统默认PATH很长而且包含了如/usr/local/bin、/usr/bin这类正常目录,还要继续检查这些目录本身是否可以被低权限用户写入 - 对现有的bin目录(
/usr/local/bin,/usr/bin,/usr/sbin)抽查,重点看是否有"普通用户具有写权限"的文件或目录 - 对SUID程序做记录,并定期执行
find / -perm -4000 -type f做diff对比,新出现的SUID文件要追查来源
如果真的有管理员误将当前目录(.)或/tmp写进了root的PATH,需要立即修正。临时在Shell环境里设置PATH进行覆盖测试的教训是:只改了登录Shell的PATH是没用的,因为cron/systemd等并不读你改的配置。要改,必须在/etc/crontab和/etc/systemd/system/相关服务里显式声明。
5.3 NFS服务的安全配置建议
对NFS的加固,按危险程度优先级排序,我的建议是:
- 优先检查
/etc/exports中是否包含no_root_squash,如有且业务不依赖,立即移除并重载NFS服务 - 如果业务确实需要客户端root保留权限,至少要将共享目录限定在白名单IP段,不能使用
*通配,同时在防火墙层面做端口限制 - 谨慎使用
all_squash+anonuid组合,确认这些uid在服务端有严格的文件权限控制
下面给一份安全基线示例:
/data/app1 10.10.10.0/24(rw,sync,root_squash,no_subtree_check) /data/upload 10.10.20.10(rw,sync,all_squash,anonuid=1000,anongid=1000,no_subtree_check)改成root_squash后,即使客户端root挂载了共享目录,创建的文件的owner也会被映射成nobody,无法在服务端生成高权限文件,提权链从源头被掐断。
5.4 快速风险自查的命令集合
在真实环境里,如果时间有限,没法做全面的人工审计,可以先跑下面这套命令,把高危点扫出来:
# 查看所有cron任务及其脚本权限 for f in /etc/crontab /etc/cron.d/* /var/spool/cron/*; do echo "--- $f ---" cat "$f" 2>/dev/null | grep -v '^#' done # 找出所有目录和文件权限异常(其他用户可写) find /etc/cron* /var/spool/cron /usr/local/bin /usr/sbin -type f -perm -o+w -ls find /etc/cron* /var/spool/cron /usr/local/bin /usr/sbin -type d -perm -o+w -ls # 查看当前所有SUID文件 find / -perm -4000 -type f -ls 2>/dev/null # 检查NFS导出配置 cat /etc/exports showmount -e localhost 2>/dev/null # 查看所有系统用户cron任务 ls -la /var/spool/cron/ # 检查PATH变量中是否有危险目录 echo $PATH cat /etc/crontab这套命令跑完后,基本能把三个高风险区域的核心风险点覆盖住。如果输出结果里有上文提到的问题,建议优先处理。
6. 实战复盘:一个完整提权过程中的思路链
说了这么多,我把一个典型的内网渗透场景串起来讲一遍,方便你把Cron、PATH、NFS串成一个整体的思路链。
假设你拿到的是一台部署在内网的文件服务器,低权限用户是svc_app,系统是CentOS 7。你的目标很明确:从svc_app提权到root。
第一步,先看cron。执行cat /etc/crontab,发现root有一个定时任务,每10分钟执行一次/opt/scripts/backup.sh。再查看该脚本权限,发现脚本是root所有,权限750,无法篡改。但你打开脚本一看,里面内容是:
#!/bin/bash tar czf /backup/backup_$(date +%Y%m%d).tar.gz /var/www/html/*通配符注入的机会来了。你在/var/www/html目录下找到了可写权限(apache用户可写,而svc_app所在的用户组碰巧对这个目录有写权限)。于是创建两个bad文件:
cd /var/www/html touch -- "--checkpoint=1" touch -- "--checkpoint-action=exec=sh -c 'echo \"svc_app ALL=(ALL) NOPASSWD:ALL\" >> /etc/sudoers'"等cron执行后,sudo -i直接进入root。
但换个场景,如果目标机器上cron脚本权限很严,脚本内容又是纯绝对路径,没有可利用的通配符呢?此时不要灰心,转去看PATH和NFS。
假设这台机器上/etc/exports有如下内容:
/home/svc_app/data 192.168.1.0/24(rw,sync,no_root_squash)与此同时,svc_app的主目录正好就是NFS共享目录。你在攻击机上以root挂载该目录,在挂载目录下创建一个SUID shell:
mkdir /mnt/nfs mount -t nfs <目标IP>:/home/svc_app/data /mnt/nfs cp /bin/bash /mnt/nfs/.hidden_shell chmod 4755 /mnt/nfs/.hidden_shell回到目标机器,在svc_app的shell中执行:
/home/svc_app/data/.hidden_shell -p得到root。整个流程不需要任何漏洞利用框架,纯粹依赖管理员对NFS参数的误配置。
第三个场景,如果这两条路都不通,再看PATH。假设系统里有一个root经常手动运行的管理脚本,写在了/usr/local/bin/status,调用时执行了:
ps -ef | grep java如果你能在/usr/local/bin(注意这个目录是否是用户可写)或者脚本使用相对路径运行,而且当前用户的PATH里包含了一个可写目录,就能用同名脚本替换ps。但大多数情况下,这种场景部署在默认安装的服务器上并不多见,需要更细致地检查。
这三个场景串起来,你会发现Cron + PATH + NFS三条支线互不依赖,任何一条打通都能完成提权。这也是为什么我坚持把它们放在一起讲——配置类提权永远不是单点问题,而是一个"面"的问题,需要你同时考虑定时任务、环境变量、网络服务三个维度。
7. 一点实操体会
整理这篇文章时,我又把这三类问题在虚拟机环境里完整复现了一遍。每次复现都会有一些新的细节冒出来,比如tar通配符注入时,如果文件名以--开头,touch命令本身也要注意防止被解释为选项;再比如NFS挂载时,如果目标服务器的rpcbind有防火墙拦截,mount操作会莫名超时,需要先确认网络层放行。
真心建议每一位做安全测试或系统运维的朋友,都找个周末环境把这几个实验从头到尾做一遍。不需要攻击真实用户,只需要准备两台虚拟机(一台攻击机、一台目标机),就能完整走完从低权限到root的每一个步骤。只有亲手在Cron脚本里成功执行了注入命令,你才能真正理解为什么安全基线上反复强调"绝对路径"和"禁用no_root_squash"。纸上得来终觉浅,这句话在提权研究上尤其适用。