折腾CTF靶场这几年,我最喜欢的环节不是初始拿shell那一刻,而是从普通用户一路摸到root的过程。很多时候,你手里就一个低权限的用户,粗糙的命令都跑不了,但只要能读到sudo -l的输出,整个局面就可能彻底反转。sudo几乎算是CTF里出镜率最高的提权入口,没有之一。这篇文章就围绕sudo命令实现权限提升这条路,把常见手法、原理、配置细节和踩坑点一次性说透,适合刚开始打CTF的选手,也适合想回头补一补系统权限管理知识的运维朋友。
很多人会觉得sudo不就是“临时借管理员权限跑一条命令”吗,有什么可讲的?真到了靶场里你才发现,一条配置不当的sudo规则,就是给你留好的登云梯。与其到处碰运气,不如把sudo提权的底层逻辑吃透,让它变成你工具箱里最顺手的一把钥匙。
1. 认识sudo:一把通往最高权限的钥匙
1.1 sudo到底在做什么
简单来说,sudo是Linux/Unix系统里用来临时提升执行权限的命令。默认情况下,系统管理员可以通过配置,允许指定用户以root或其他用户身份执行某些命令。放在CTF场景里就很好理解了:你通过某个漏洞拿到一个低权限webshell,下一步通常想读/root/flag,或者执行只有root才能跑的指令,这时候如果当前用户恰好被sudo配置允许执行某些命令,提权就变成了一道现成的程序题。
我习惯把sudo机制比作小区的门禁卡。普通用户只有单元门卡,只能在公共区域活动;sudo是物业发的临时授权卡,能进机房、能开设备间,但具体能进哪个房间、能操作哪台机器,全写在授权表里。CTF出题人往往就是在授权表上做文章,故意给你留几个“看起来不太危险”的命令入口,实际操作空间却很大。
1.2 sudoers规则怎么看
授权表就是/etc/sudoers文件,正常不会让你直接编辑,系统也提供了visudo命令专门改它,目的就是防止语法错误把人锁死。规则的通用格式长这样:
用户名 主机列表=(被切换的用户) 标签:命令比如:
ctf ALL=(ALL) NOPASSWD: /usr/bin/vim这句话的意思是:用户ctf在任意主机上,可以不需要输入密码,以任何用户的身份执行/usr/bin/vim。这里的ALL是最常见的通配符,第一个ALL是主机名,第二个ALL是目标用户。NOPASSWD是标签,代表执行sudo时免密,CTF里特别常见,因为出题人通常不希望你卡在密码上,而是希望你关注命令本身能不能做文章。
还有一种特殊写法要格外留意,就是(ALL, !root)、(root)这类权限范围限定。有些规则看似限制了你能以谁的身份执行,实际上可能被某些参数绕过。后边讲CVE的时候会提到。
我们看自己的sudo权限,最实用的命令就是sudo -l。它会列出当前用户被允许执行的所有命令以及对应规则。如果加了-U参数,还可以查看其他用户的sudo权限,不过通常没有权限这么做。
1.3 为什么CTF里绕不开sudo提权
不止一个朋友问过我,CTF里提权手段那么多,为什么偏要看sudo?原因有三点:
第一,sudo配置错误太常见了。靶机作者为了让题目有解,往往会在业务脚本里顺手给某用户加一条sudo规则,比如“允许运行某个备份脚本”“允许运行某个编辑器”,这正好变成了考点。
第二,sudo的可利用面很大。绝大多数常用命令都可以在root权限下逃逸出shell,不需要你懂内核漏洞,也不需要写很复杂的exploit,一条命令就够。
第三,sudo提权是很多复杂利用链的基础设施。你可能先用sudo拿到了root shell,之后才方便去读敏感文件、改内核模块、甚至横向移动。没有这一步,后面的戏全都唱不了。
2. 从sudo -l开始:找到你脚下那块垫脚石
2.1 sudo -l的输出不是天书
打靶时我拿到低权限shell的第一件事,就是敲sudo -l。很多人不看重这一步,以为输出就是几条路径,其实里面藏着大量信息。
假设你看到这样一个输出:
User ctf may run the following commands on this host: (root) NOPASSWD: /usr/bin/vim这就意味着你可以用root身份执行vim。看起来好像没什么用,但vim本身有执行shell的能力。进入vim之后,按下:!sh,它会直接起一个以root权限运行的shell。因为命令是sudo启动的,进程已经是root,而vim只是替你打开了一扇门。
更普遍的思路是,看到sudo -l列出了某个命令,先别急着跑,想想这条命令有没有可能交互式执行其他程序,或者有没有参数可以直接调用系统命令。凡是能打开shell的工具,都有可能成为你的跳板。
2.2 命令逃逸到shell的经典姿势
我把常见的几条命令和对应的逃逸方式整理成了一个速查表,基本覆盖了CTF里最常见的套路:
| 命令 | 逃逸方法 | 说明 |
|---|---|---|
| vim | :!sh | 进入vim后,冒号模式下执行外部命令 |
| less / more | 输入!sh | 分页器内部支持执行shell命令 |
| find | find / -exec /bin/sh \; | 用-exec执行任意命令 |
| python | python -c 'import os; os.system("/bin/sh")' | 脚本解释器天然能执行系统调用 |
| perl | perl -e 'exec "/bin/sh";' | 同上 |
| awk | awk 'BEGIN {system("/bin/sh")}' | awk里直接调system函数 |
| git | git help -a后输入!/bin/sh | git pager机制也能逃逸 |
| env | env /bin/sh | sudo运行env本身无意义,但可以带命令 |
| nmap | 交互模式或--interactive,执行!sh | 老版本nmap适用,新版有限制 |
这些逃逸手法的底层逻辑其实都一样:sudo以root身份启动了某个程序,而这个程序内部又提供了执行外部命令的接口,于是你在当前进程里拿到root权限的shell。它不是“爆破”,更像“借壳”。
2.3 不会ESC,这些组合就是你的盾牌
光记住几个命令还不够,你得掌握怎么判断“这条命令有没有戏”。我提供一个简单的概念性判断流程:
先看命令类型。编辑器、分页器、脚本解释器、包安装工具、搜索工具,这些大概率都有逃逸方式。再看参数面。命令是否支持-exec、--interactive、-c这类执行参数?最后看交互面。有些工具需要进入交互模式后才能输入shell指令,比如less里的!。如果你对这些命令不够熟,就去搜一下“GTFOBins”,这是一个专门汇总Unix二进制文件提权方式的公开项目,CTF玩家应该人手一份。
有意思的是,很多选手到了这一步反而不习惯“一条命令直接拿root”的感觉,总觉得是不是漏了什么。实际上这就是sudo提权的魅力:出题人想考的就是你对命令细节的熟悉程度。所以平时多练习,多尝试不同的参数组合,才会越来越有感觉。
3. 环境变量与动态库:隐藏的提权后门
3.1 sudo保留的环境变量机制
有时候sudo -l里列出的命令看起来平平无奇,比如只有/usr/bin/find,但你还是可以提权。原因不在命令本身,而在环境变量配置。默认情况下访问控制会把环境变量重置,避免恶意变量污染高权限进程,但如果在sudoers里配置了SETENV标签,或者某些变量被env_keep保留,情况就完全不同了。
你可以把环境变量理解成进程的“随身行李”。sudo在过安检时默认把大部分行李扣下,清空成一套安全的默认值,但有些特殊情况下,出题人允许你带着自己的行李过安检,甚至允许在行李里夹层藏了动态库。这个时候,就能利用动态库加载机制做文章了。
3.2 LD_PRELOAD劫持提权原理与实操
Linux有一个环境变量叫LD_PRELOAD,它可以让一个动态库被提前加载到进程里,抢先执行其中的构造函数。如果我们能够通过sudo以root身份运行任何一条命令,同时sudo又允许保留LD_PRELOAD环境变量,那么加载一个我们自己写好的恶意动态库,就能让root进程替我们执行任意代码。
这个过程我拿一个经典例子分解一下。先写一个C文件,比如shell.c:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> void _init() { unsetenv("LD_PRELOAD"); setresgid(0,0,0); setresuid(0,0,0); system("/bin/sh"); }然后编译成动态库:
gcc -fPIC -shared -o shell.so shell.c -nostartfiles最后在sudo执行命令时手动指定这个动态库:
sudo LD_PRELOAD=./shell.so find / -type f如果你看到find真的以root权限执行了,同时动态库里的_init函数也被调用,你就会直接掉进一个root shell。为什么能生效?因为sudo在高权限状态下启动进程时,加载器先加载了LD_PRELOAD指定的库,构造函数抢先执行,而后才运行find这个程序。我们借的是加载机制,跟find本身无关。
当然有个前提必须反复强调:sudo规则里得允许设置环境变量,比如Defaults env_keep+=LD_PRELOAD,或者规则里带有SETENV标签。如果默认清空环境变量,这招就是无效的。所以我在实际打靶时,会刻意检查sudo -l的输出里是否出现SETENV字样,有的话就先想到LD_PRELOAD。
3.3 PATH劫持与脚本类命令的玩法
另一个利用环境变量的思路是PATH劫持。假设sudo保证你可以运行某个系统脚本,而脚本内部又调用了外部命令,比如:
#!/bin/bash ls -la /root/data如果这个脚本没有使用绝对路径调用ls,而我们又可以通过sudo设置PATH环境变量,就可以制造一个假的ls放到当前目录,里面写上/bin/sh,再把PATH改成优先搜索当前目录。这样sudo运行脚本时,找到的ls实际是我们的恶意脚本,用户虽然原本没权限执行任意命令,但通过脚本间接达到了目的。
不过PATH劫持在CTF里成功率往往没有LD_PRELOAD那么高,因为sudo默认对PATH做了严格限制,常常会重置成系统安全路径。所以第七感告诉我:看到脚本提权时,先看脚本能不能写,再看环境变量能不能控制,最后才考虑路径劫持。顺序反了会浪费不少时间。
4. 真实场景中的组合拳:从普通用户到root
4.1 一个模拟靶场里的完整流程
理论说再多,不如走一遍流程。这里我虚构一个某常见模拟靶场里反复出现的套路,用来展示sudo提权的组合打法。
场景是这样的:通过Web漏洞拿到的webshell。反弹回来之后,当前用户是dummy,家目录里有一个user.txt,但没有读取/root/flag的权限。老规矩,先执行sudo -l,得到如下输出:
User dummy may run the following commands on this host: (root) NOPASSWD: /usr/bin/python3 /opt/config.py这里能执行python3,但是限定了只能执行/opt/config.py这个脚本。乍一看好像把代码固定死了,但脚本内容往往有可乘之机。我赶紧看/opt/config.py,果然它只是做了配置解析,支持一个--include参数,用到了Python的某个模块加载。这不就是“间接命令执行”吗?
如果你能在脚本的运行路径下放置一个有特定文件名的恶意模块,或者通过参数影响搜索路径,最后就能让python3以root身份加载我们的恶意代码。类似地,常见的还有sudo限定只能执行某个备份脚本,但脚本里有tar、zip等命令调用,同样可以借机绕过。
我在这个模拟场景里,最后是通过构造恶意模块文件,再利用sudo运行/usr/bin/python3 /opt/config.py,成功以root身份执行了反弹shell命令,直接拿到root权限并读取了flag。整个过程只用了不到十分钟,关键就在于充分利用了配置文件对模块路径的控制。
4.2 版本漏洞带来的意外惊喜
除了配置层面的滥用,sudo自身的历史漏洞也是CTF里完全绕不开的考点。其中比较有名的两个,我建议你把原理记牢。
第一个是CVE-2019-14287。这个漏洞本质上是对sudo用户ID处理不当。当sudoers规则里写的是(ALL, !root)这种排除root的授权时,攻击者如果指定用户ID为-1(即4294967295的无符号整数),sudo会在某些版本里错误地把它当成root来处理,从而绕过“不允许root身份执行”的限制。利用方式很简单:
sudo -u#-1 id或者:
sudo -u#4294967295 id只要规则中存在排除root的写法,就能以root身份运行命令。这个洞在好几年前很火,直到现在一些靶机还喜欢把它当彩蛋。
第二个是CVE-2021-3156,代号叫Baron Samedit。它是sudo堆缓冲区溢出漏洞,影响了很多年前的多个版本,利用方式比前者复杂,但效果直接,可以完全绕过配置限制获得root shell。CTF里遇到老旧系统时,如果找不到任何配置层面的提权点,我就会去查sudo版本,然后对应排查漏洞。
这些漏洞的共性在于:它们不是配置错误,而是程序本身的逻辑缺陷。但从攻击者视角看,它们和配置错误是同样的资产,都属于“拿来就能用”的提权路径。不过我要多说一句,了解漏洞是为了防护和打靶,千万不要拿到真实生产环境里去乱试。
4.3 提权的防御建议与自查清单
自己搭靶场或者做系统加固时,我会按下面这套清单过一遍,能挡掉大部分sudo提权问题:
- 坚持最小权限原则。不该给sudo的普通用户,一条命令都不给。
- 可执行命令列表里,尽量避免vim、less、python、find这类能逃逸到shell的工具。
- 如果业务必须用到某些命令,能用绝对路径就用绝对路径,并在规则里限定参数,减少灵活性。
- 避免滥用
NOPASSWD关键字,每一条免密规则都是风险点。 - 保留环境变量重置机制,不要轻易配置
SETENV或env_keep。 - 定期更新sudo版本,至少关注安全公告,别让系统腐烂在远古版本上。
- 定期让运维同事跑一遍
sudo -l,检查每个用户的实际权限面。
这套清单是我自己每次配置完系统后会立刻执行的“安全检查”,虽然简单,但很多入侵者撞到这些点都会被卡住。
5. 常见坑位与排错手册
5.1 为什么提权经常翻车
sudo提权看似简单,实战时踩坑的几率却不低。我粗略总结过,大部分人翻车集中在下面几个地方:
一是sudo -l的信息没有被充分解读。输出里明明写了SETENV,你没看到;明明写的是(ALL) NOPASSWD,你却在纠结密码。二是场景不对。LD_PRELOAD并不是所有sudo配置下都能用,很多人拿着这个技巧硬套,结果失败。三是对shell版本和工具版本不敏感。比如老版本的nmap有交互模式,新版本早就砍掉了这个功能,拿老命令去新环境当然不灵。
5.2 一条条对号入座的排查技巧
我把自己在靶场里踩过、甚至在某些真实系统加固时复盘过的典型问题,整理成了一张排错表,看起来会更直观:
| 症状 | 可能原因 | 排查与处理 |
|---|---|---|
sudo -l提示找不到命令 | 当前用户没有sudo权限,或命令路径不在PATH里 | 先确认用户是否在sudo组,再看/etc/sudoers语法 |
| 提示必须输入密码,但不知道密码 | 规则中没有NOPASSWD,或者sudo缓存已过期 | 回看信息收集阶段拿到的密码哈希,尝试密码复用 |
| LD_PRELOAD提权没生效 | 环境变量被重置,或规则不允许保留该变量 | 检查sudoers里的env_keep和SETENV标签 |
| 逃逸命令执行后没反应 | 有些程序进入的是受限交互模式 | 尝试换一种逃逸方式,或查看/bin/sh是否被替换成受限shell |
| 拿到了shell但操作异常 | 进程权限可能还是普通用户,或目标系统限制了shell | 立刻执行id确认uid,必要时用exec bash替换当前进程 |
规则里写了/usr/bin/find却跑不了 | sudoers可能限制了精确参数,比如只允许特定路径 | 查看完整规则,不要只看命令名 |
排错的核心思路永远是:先确认权限面,再确认环境变量,再确认版本和交互方式。不要在一个点上死磕,换一条路径往往更快。
另外一个细节,sudo本身会记录执行日志到/var/log/auth.log(不同发行版路径略有差异),有时候日志里的格式能帮你反推规则细节。靶场里这不是你该操心的,但做防御视角自查时,这些日志是判断是否有异常sudo调用的重要依据。
5.3 我个人最想提醒你的三件事
第一,不要把提权限定在“必须拿到root shell”这一个目标里。很多时候,只要你能通过sudo读取某个敏感文件,甚至执行一条特定管理命令,任务就已经完成了。比如直接sudo cat /root/flag.txt,虽然不算“获得shell”,但在CTF里你已经得分了。
第二,多看sudo -l里的细节。NOPASSWD、SETENV、(root)、(ALL:ALL)这些关键词直接决定走哪条路。用习惯之后,我会下意识地先把输出复制到本地,高亮这些关键词再动手。
第三,别忘了sudo其实还有“临时切换用户”的作用。如果你能sudo到某个服务账户,而这个服务账户本身又有特殊权限,那也算一种提权路径。有时候横向比纵向更好用,视野打开一点,思路就不会被“只能变root”限制死。
说实话,玩了这么多年CTF,sudo提权是我觉得性价比最高的内容之一。它不像内核漏洞那样高门槛,也不像密码爆破那样看运气,它纯粹考验你对系统配置和命令细节的理解。把这套东西吃透,你在靶场里的操作会顺畅很多,同时对真实系统的权限管理风险也会有更具体的感知。
最后再唠叨一句:所有技巧都是在授权范围内做安全测试时才值得练习。别拿到生产的机器去试,也别把靶场里的经验变成灰色的习惯。守住这条线,技术这条路才走得更稳、更安心。