
打CTF或者做授权渗透测试的时候我最怕的不是拿不下初始shell而是卡在权限提升那一层。明明是同一个靶机别人一条命令就root了我还在翻几十个CVE心态直接崩。Linux提权的路径很多内核漏洞、sudo配置、定时任务、docker组滥用但SUID提权绝对是最常见也最适合新手入门的切入点。它不需要读几百页漏洞文档不需要交叉编译一条find命令扫过去答案往往就写在文件权限位里。这篇文章是围绕10道SUID提权例题整理的实战Writeup本文是[一]先把SUID的底层原理和5道经典例题讲透再附上我每次打靶都会敲的探测命令清单。适合刚接触Linux提权、想在靶场上把SUID这条路彻底走通的读者。1. 先从权限位说起为什么一个小写s能改变进程身份1.1 一个大家都用过、却没注意过的SUID程序你在终端里敲过passwd命令改密码吧比如修改普通用户密码时要写/etc/shadow这个文件。但你看一下/etc/shadow的权限ls -l /etc/shadow ---------- 1 root shadow 1843 3月 17 10:22 /etc/shadow权限位是----------普通用户对它是没有任何读写权限的。那普通用户凭什么能通过passwd命令把自己的密码写进去答案就在passwd命令自身的权限位上ls -l /usr/bin/passwd -rwsr-xr-x 1 root root 59976 5月 27 2023 /usr/bin/passwd注意看属主权限位不是rwx而是rws。这个s就是setuid位也叫SUID。当一个可执行文件设置了SUID位普通用户运行它时进程的有效身份会临时切换成文件属主。passwd命令属主是root所以普通用户一运行它内核就让这个进程以root身份去执行逻辑于是它可以合法地修改/etc/shadow且不需要把所有权限都下放给普通用户。这就是SUID的经典设计。想复现这个现象可以自己找一个没有SUID的程序对照。比如常人都会用的ls无论谁执行它进程身份就是执行者自己所以普通用户无法用ls查看/root目录。这就是有没有SUID的区别。1.2 真正起作用的是euid不是uid要理解SUID提权必须分清两个概念ruid和euid。ruidreal uid发起这个进程的用户ID也就是当前登录用户的身份。euideffective uid内核做权限检查时使用的用户ID。Linux对文件访问权限的判断看的是euid不是ruid。普通程序运行时ruid和euid是同一个值。但一旦二进制文件带SUID位内核会把进程的euid设置为文件属主的uidruid保持原样。于是进程就同时拥有两个身份看起来是普通用户启动的实际操作权限却是root。所谓“SUID提权”本质上就是找到一个带SUID位、属主是root、且其功能可以被我们利用来执行任意命令或读写任意文件的程序。它越过了sudo密码验证也不需要知道root密码直接把普通用户进程的euid抬到0。这里有一个非常常见的坑为什么拿到一个SUID bash以后直接输入/bin/bash没有变成root反而还是普通用户因为bash在启动时发现euid和ruid不一致出于安全考虑会主动把euid重置回ruid防的就是用户借SUID bash直接提权。要绕过这个逻辑必须加-p参数让bash保留euid。所以你在很多提权Payload里会看到bash -p原因就在这里。1.3 不是所有SUID都能弹shell系统内置程序与自定义程序的差异有人刚学会find命令扫描SUID后看到靶机上有一堆系统程序比如/usr/bin/mount、/usr/bin/su、/usr/bin/umount就挨个尝试结果一个都弹不出来于是怀疑自己姿势不对。其实不是姿势不对而是系统内置SUID程序在设计上就防着提权。passwd只允许改自己的密码mount只允许按要求挂载su需要验证密码它们内部都做了权限校验不会给你任意执行命令的机会。真正危险的是那些本身就能执行命令、加载库、读文件的工具比如find、vim、python、perl、tee或者是管理员自己写的、没做安全处理的自定义程序。分类可以这样记忆可利用型程序本身的能力允许执行任意命令比如find、vim、python3、perl、awk、env、nmap老版本带交互模式。有条件可利用型程序能读写文件或加载配置比如tee可以追加写文件vim可以直接编辑root文件tar存在通配符参数注入。设计安全型passwd、su、mount这类内部有鉴权常规手法很难利用。怎么快速判断最实用的办法就是看看目标程序有没有出现在GTFOBins网站上。GTFOBins收集了大量Unix二进制文件在sudo、SUID场景下绕过限制的用法每一条都写得很清楚但它只是一个知识库真正能不能用还要看目标二进制编译时开启了哪些功能、靶机是什么发行版。1.4 扫描SUID文件的命令必须背下来不管例题多花哨第一步永远是先找全盘有哪些SUID文件。我常用的三条find / -perm -4000 -type f 2/dev/null find / -user root -perm -4000 -type f 2/dev/null find / -perm -4000 -type f -exec ls -la {} \; 2/dev/null-perm -4000表示匹配所有“包含SUID位”的文件4000是SUID位的八进制值。-type f只留普通文件过滤目录和特殊文件。2/dev/null把权限错误丢掉不然系统里一堆/proc、/sys的报错会刷屏。如果你想保留错误日志便于排查可以改成这样find / -perm -4000 -type f 2/tmp/finderr.txt | tee /tmp/suidlist.txt上述第一条扫描的是“任意属主的SUID”实际提权关注的是属主为root的SUID所以第二条更贴合场景。如果靶机上有非root属主的SUID通常价值不大但也可以留意比如属主是某个服务用户提权到该用户后可能成为后续攻击链的一环。2. 例题一到例题三三个现成工具直接弹root2.1 例题一find的-exec参数像是一个后门这是一台模拟Debian环境的最简靶机我的初始权限是一个普通的connect用户。常规信息收集后用find扫描SUID文件connecttarget:~$ find / -perm -4000 -type f -exec ls -la {} \; 2/dev/null -rwsr-xr-x 1 root root 381K 9月 12 14:22 /usr/bin/find -rwsr-xr-x 1 root root 1.2M 9月 12 14:22 /usr/bin/python3看到/usr/bin/find是root属主且带SUID基本可以提前宣告游戏结束。find本身是一个命令行工具它的-exec参数就是用来执行外部命令的。当find以root euid运行时它fork出来的子进程也会继承root euid。于是执行find /home -exec /bin/sh \;界面直接进入一个sh会话whoami输出root。如果你用的是/bin/bash记得加-pfind / -exec /bin/bash -p \;为什么这里用/bin/sh也有效因为Debian的/bin/sh是dashdash没有bash那种“检测到euid和ruid不一致就降权”的逻辑它会老老实实保留继承过来的euid。所以用sh通常也能直接拿到root shell。这条命令后面的分号是给find看的表示-exec命令到此结束shell会把它转义给find。少了转义符命令不会被正确解析这也是新手经常搞不定的点。利用完毕之后可以用exit退出shell回到普通用户状态不影响后续继续排查其他漏洞点。2.2 例题二vim不只是编辑器还是一个root文件读写器第二道例题的环境是/usr/bin/vim.basic带SUID。同样是先扫描确认-rwsr-xr-x 1 root root 2.3M 9月 12 14:22 /usr/bin/vimvim带SUID常见的利用方式有三种按攻击路径的“动静”从大到小排序。方式一在vim里直接切shell。vim -c :set shell/bin/bash -c :shell或者进入vim后执行:shell默认shell可能被重置权限所以先把shell显式指定为/bin/bash再调用shell命令。由于vim以root euid运行这里弹出的就是一个root shell。方式二借助vim的python接口。vim -c :python3 import os; os.setuid(0); os.system(/bin/bash -p)前提是vim编译时带python3支持。这个方法会把进程的uid和euid都置成0再启动bash能拿到比较“干净”的root shell。方式三直接用vim改系统文件。这一点在实战中最灵活。vim能读写任意文件那就把当前用户加入/etc/passwd或者直接改/root/.ssh/authorized_keys又或者写一个计划任务。比如:!/usr/bin/openssl passwd -1 -salt override 123456把生成的密文记录好然后vim /etc/passwd把root那一行的密码占位符x替换成密文保存退出后执行su root密码就是123456。当然在CTF靶场里更稳妥的一般是直接从vim弹出root shell少改系统文件避免把靶机搞坏。2.3 例题三python3解释器一旦带SUID等于白给第三道例题的特权文件是/usr/bin/python3权限位同样是root:s并且带s位。Python这类的解释器只要它以root身份运行导入os模块、设置uid、启动shell一步到位python3 -c import os; os.setuid(0); os.system(/bin/bash -p)如果你想要一个带TTY的交互式shell可以用pty模块python3 -c import pty,os; os.setuid(0); pty.spawn(/bin/bash)os.setuid(0)的作用是把进程的ruid和euid同时设为0。前面的例子里进程的euid已经是0但ruid还是普通用户这会带来一些细微的权限检查问题。setuid(0)之后ruid和euid统一为0执行后续操作会更顺畅。如果题目环境是python2语法略有区别但写法基本一致python2 -c import os; os.setuid(0); os.system(/bin/bash)这几条命令建议直接存到本地备忘录里。CTF和实战里一旦看到SUID的解释器类工具思路都是同一个方向想办法让解释器执行setuid(0)并拉起shell。3. 例题四环境变量劫持——system函数里那个ls不是你以为的ls3.1 靶机现场与初步发现第四道例题的靶机环境变了个花样。扫描SUID时没有看到find、vim、python这类现成工具而是多了一个看起来身份不明的二进制文件connecttarget:~$ find / -perm -4000 -type f 2/dev/null /usr/local/bin/backup这个路径以及文件名明显是管理员自己放的。先查看它是什么connecttarget:~$ file /usr/local/bin/backup /usr/local/bin/backup: ELF 64-bit LSB executable, x86-64再用strings把里面的可读字符串拉出来重点关注它调用了什么外部命令connecttarget:~$ strings /usr/local/bin/backup | grep -E system|popen|exec|/bin/ system(ls -la /home); system(tar cf /backup/backup.tar /home);它内部调用了系统命令ls和tar而且用的是命令名不是绝对路径。这就是突破口。3.2 system函数内部发生了什么C语言里的system函数实际上会启动一个/bin/sh -c 字符串来做命令解析。也就是说程序里写system(ls -la /home)shell会去PATH环境变量指定的目录里逐个查找名为ls的可执行文件。如果攻击者能控制PATH环境变量再伪造一个名为ls的恶意程序放到PATH路径的前面那么程序运行时就会被骗着执行攻击者的程序。很多新手会问为什么我不直接改PATH让系统把ls解析成/bin/bash因为shell查找到ls后会把它当作一个可执行程序来加载最终还是以ls的命令名启动。我们伪造的那个“ls”文件内容才是真正会被执行的代码。所以思路是伪造文件不是改PATH指向bash。3.3 完整利用过程与解释在/tmp目录下创建恶意脚本命名为程序即将调用的命令名cd /tmp echo /bin/bash -p ls chmod x ls export PATH/tmp:$PATH /usr/local/bin/backup执行backup后它的system(ls -la /home)会去PATH里按顺序找ls。由于/tmp被排在最前面系统找到的第一个ls是/tmp/ls也就是我们的恶意脚本。此时backup进程的euid是root它fork出的shell继承这个euid于是一个root shell就出现了。还有一个变体值得提一下如果目标程序调用的外部命令是绝对路径比如system(/bin/ls -la /home)PATH劫持就失效了。这时候要观察它是否调用了多个命令、是否能通过参数注入绕过或者有没有其他脆弱点。盲目的PATH劫持只对没有写绝对路径的程序有效。3.4 为什么LD_PRELOAD这条路走不通有人可能想到环境变量劫持的进阶玩法LD_PRELOAD。既然能控制环境变量为什么不设置LD_PRELOAD让程序加载我们的恶意共享库这个思路单独对普通程序成立但对SUID程序基本无效。原因是glibc在加载器里开启了AT_SECURE安全模式当进程是setuid/setgid程序时动态链接器会直接忽略LD_PRELOAD、LD_LIBRARY_PATH等环境变量防止攻击者注入代码。系统设计者早就防了这一手。PATH变量不受这个限制吗实测下来glibc的安全模式确实不会过滤PATH。所以PATH劫持是SUID提权里一个经典且仍然有效的手法而LD_PRELOAD则不能直接用在真SUID场景下。这个知识点面试答出来能加分实战里也能帮你少走弯路。4. 例题五一个带SUID的自定义C程序怎样的攻击路线4.1 没有源码时怎么快速分析二进制第五道例题拿到的是/usr/local/bin/vuln_prog同样带root SUID位。这次字符串里没有直接暴露system调用所以我按顺序做了几步基础分析。file /usr/local/bin/vuln_prog strings /usr/local/bin/vuln_prog | less ltrace /usr/local/bin/vuln_progfile确认它是64位ELFstrings看有没有关键路径、用户命名字符串ltrace看它会调用哪些库函数。如果程序是动态编译且没做静态混淆ltrace的效果最好connecttarget:~$ ltrace /usr/local/bin/vuln_prog getuid() 1000 system(id) ...结果直接暴露了system(id)。这就和上一道题异曲同工程序里调用了外部命令id而且没有写绝对路径。如果程序稍微复杂一点用strace跟踪子进程的系统调用也很有用strace -f -e execve /usr/local/bin/vuln_prog-f参数表示跟踪fork出来的子进程-e execve只看程序执行了哪些外部文件。这比strings更准确能看出实际执行的命令路径。4.2 找到可劫持的命令之后的操作同样的思路在/tmp里伪造一个id脚本修改PATH然后运行vuln_progcd /tmp echo /bin/bash -p id chmod x id export PATH/tmp:$PATH /usr/local/bin/vuln_prog程序执行system(id)时实际执行的是/tmp/id启动一个root shell。和上一道题的区别在于这次不是管理员脚本带SUID而是经过编译的C程序很多人一看到ELF文件就发怵其实利用方式完全一致。4.3 如果程序里调用了多个命令怎么办这道题我故意说一个加深理解的变化。如果漏洞程序是这样写的system(date /tmp/timestamp.txt); system(id);那我只伪造一个id就不够因为date那一步会正常执行并输出重定向不影响提权但我必须在PATH里同时放一个id的恶意脚本。如果程序先调用ls再调用id那就在/tmp里同时放两个恶意文件一个叫ls一个叫id。等程序运行到哪个命令哪个命令就会被劫持。这个细节看着简单实战里我见过有人只伪造了其中一个命令导致前半段执行正常、后半段提权失败排查了半天才反应过来。4.4 setuid(0)在SUID程序中扮演的角色还有一个问题要澄清。有的自定义C程序源码里会有setuid(0)、setgid(0)这种调用比如int main() { setuid(0); setgid(0); system(id); }在程序自身没有SUID位的情况下普通用户运行它setuid(0)会直接失败因为普通用户无权把自己的uid改成0。一旦程序被设置成root属主且带SUID位运行时进程的euid已经是0setuid(0)的作用是确保ruid也变成0。这道例题的二进制分析里如果看到setuid(0)可以认为它本来就是管理员写出来准备以root身份执行任务的只是错误地调用了外部命令。5. 每次打靶前我都会敲的探测命令清单5.1 最好的第一步永远是信息收集SUID提权不是上来就找Payload而是先把靶机的基础信息收集清楚。下面这套流程我几乎每次都会执行id whoami uname -a cat /etc/os-release find / -perm -4000 -type f 2/dev/nullid和whoami确认当前身份uname -a看内核版本/etc/os-release看发行版信息最后再用find扫SUID。如果发现可疑目标用file、strings、ltrace做进一步分析。我习惯把输出保存下来尤其是SUID列表因为后续对比判断哪些是利用点需要反复查看。5.2 常用直接利用命令速查表以下是我在5道例题里反复使用、以及日常打靶中验证过的SUID利用命令。这里的“能否成功”高度依赖目标二进制带哪些功能建议配合GTFOBins食用。特权程序利用思路典型命令findexec子进程find /home -exec /bin/sh ;vimshell或编辑任意文件vim -c :shell 或直接改/etc/passwdpython3os.setuid后拉起shellpython3 -c import os;os.setuid(0);os.system(/bin/bash -p)perlPOSIX模块设置uid后提权perl -e use POSIX qw(setuid); setuid(0); system(/bin/bash);awk直接执行系统命令awk BEGIN{system(/bin/bash)}bash保留euid启动/bin/bash -penv以root身份执行任意命令env /bin/bash -ptee利用root权限追加写passwdecho backdoor:密文:0:0:root:/root:/bin/bash | tee -a /etc/passwd自定义ELFPATH劫持或参数注入伪造同名命令并 export PATH/tmp:$PATH关于tee我想多说一句。tee本身只是把标准输入写到文件但它一旦带SUID就具备了以root身份追加写文件的能力。最常见的用法是往/etc/passwd追加一个uid0的用户。生成密码密文用openssl passwd -1 -salt hack 123456然后组合成一行追加进去su切到新用户root到手。这条链路在真实环境下比直接弹shell更隐蔽适合目标禁止交互式shell的场景。5.3 提权成功之后别急着庆祝拿到root之后第一步是确认自己真的变成了rootid如果uid0说明提权成功。接着看当前目录有没有flag.txt之类的目标文件读一下完成任务。如果还想做深一步的横向移动可以查看root的历史命令、/root目录下的脚本、数据库连接配置这些往往能揭示其他主机或服务的账号信息。有一点提醒如果是CTF答题拿到flag就可以收工如果是授权的渗透测试一定记得把环境变量、临时文件恢复原样避免影响靶机其他环节。我通常会在提权后记录一条返回普通用户的路径方便清理。PATH可以这样恢复export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin6. 这5道题背后我踩过的坑和给新手的排查建议6.1 最常见的5个坑每一个都是血泪教训第一个坑是bash不加-p。前面反复强调过bash检测到euid和ruid不一致时会主动降权。用bash姿势的时候不带-p会看到明明执行的是root SUID程序弹出来的shell却还是普通用户。第二个坑是find -exec的结尾符号丢失。find命令的-exec必须以空格加上分号结尾而且分号要转义成;。有人写成了find /home -exec /bin/sh ;find根本不会结束-exec直接报错。这种错误在键盘上就是一瞬间的事但排查起来很烦。第三个坑是伪造命令时权限没加执行位。PATH劫持的恶意脚本如果忘记chmod x系统会提示Permission denied然后继续去PATH的下一个目录寻找同名命令导致利用失败。伪造文件后先ls -l确认权限再执行目标程序。第四个坑是/tmp目录被noexec挂载。有些靶机或生产环境为了防止临时文件被执行会把/tmp用noexec选项挂载。这会导致我们在/tmp下伪造的脚本根本无法运行。遇到这种情况优先尝试其它可写目录比如/var/tmp、/dev/shm或者根据当前用户HOME目录比如/home/connect通常这些目录没有noexec限制。可以用mount命令确认mount | grep /tmp第五个坑是32位和64位架构不匹配。如果目标二进制是32位而你伪造的脚本或Payload依赖64位环境可能运行不起来。一般先file看一下目标程序再用uname -m确认系统架构。6.2 提权失败时的排查链路如果你扫到了SUID文件、试了命令却没成功先别急着换思路按下面的顺序排查确认这个SUID文件属主是不是rootls -l查看第三列。属主不是root的话提权价值大打折扣。确认文件是否真的带s位注意rws和rwx的区别有时候管理员配置了chmod x但没加s位看起来像SUID实际不是。确认你选的利用方式是否匹配程序能力比如vim要看是否编译了python接口nmap要看是否支持交互模式。GTFOBins上每条命令旁边都标注了依赖条件。确认环境变量是否生效echo $PATH看看你的恶意脚本目录是否排在前面。如果之前实验改乱了PATH先恢复。如果目标程序调用了多个外部命令逐个检查每个命令是否都伪造了对应文件。我在靶机上实操时会在每步之后用id、pwd、echo验证当前状态保证每一步都落在预期位置再进入下一步。这样即使失败也知道问题具体出在哪一环而不是从头再来。6.3 防守视角如何让SUID提权失效聊完攻击再看防守。管理员如果想让SUID提权在系统里失效通常从这几个方向入手定期审计全盘SUID文件find / -perm -4000 -type f 2/dev/null逐个确认是否必要。移除不必要的SUID位chmod -s /usr/local/bin/backup把管理员自己写但没写安全的二进制恢复为普通程序。对可写目录使用nosuid挂载比如在/etc/fstab里给/tmp、/var/tmp加nosuid选项使挂载在这些目录下的文件即使被设置SUID也不生效。注意这只对挂载点内的文件有效对/usr/bin这类系统目录不适用。用capabilities替代部分SUID现代Linux支持文件capabilities比如cap_net_bind_service可以让普通用户绑定低端口而不需要给整个程序root权限。但capabilities配置不当也可能变成新的提权点。代码层拦截审查程序里的system、popen调用尽量改execve并写绝对路径避免环境变量劫持。我个人在实际防御中体会最深的是“最小权限”这四个字。很多SUID漏洞本质上不是Linux机制错了而是系统里存在一个本不需要root权限却以root身份运行的程序。把SUID文件数量控制在个位数攻击面自然就小了。另外如果你是带着学习目的在练SUID提权强烈建议准备一个本地速查文档把我上面列的命令按自己的话重写一遍。提权这个东西看十篇Writeup不如动手打一台靶机。先把这5道题在模拟环境里完整复现一遍再进入后面的内容会顺手很多。下一篇[二]我会接着拆解剩下5道例题包括tar通配符注入、共享库劫持、内核capability滥用以及和sudo提权的组合技。