Diggid's Blog

rwctf2022 Desperate Cat 复现记录

字数统计: 3.1k阅读时长: 13 min
2022/02/21 Share

前言

由于在回校的路上没怎么看RWCTF2022的题。有两道java的,先来复现总结一下Desperate Cat这题。题目的wp在https://mp.weixin.qq.com/s/QQ2xR32Fxj_nnMsFCucbCg

reload条件

Tomcat支持Context容器reload,有三种方式可以触发reload,前两种是通过backgroundProcess方法由tomcat后台进程监控某些变化从而触发reload,而第三种通常是在debug模式下通过特殊的servlet进行管理,类似actuator的reload。

  1. 委托WebappLoader,监控所有当前context已加载的class文件以及jar包,即对于/WEB-INF/classes下的class文件,时间戳改变;对于/WEB-INF/lib下的jar包,时间戳改变或增减。以上条件都会触发reload
  2. 委托HostConfig,从ContainerBase#backgroundProcess -> HostConfig#lifecycleEvent -> check -> checkResources,在该方法中,检查WatchedResource的时间戳是否改变
  3. 委托ManagerServletHTMLManagerServlet,访问/reload触发reload,需要web.xml中配置这两个servlet,通常是在调试环境下测试,生产环境几乎不会有,因此该利用点不考虑。

image-20220404130101478

使用EL表达式+上传

  • ${pageContext.servletContext.classLoader.resources.context.reloadable=true}

  • /WEB-INF/lib//WEB-INF/classes/发生改变,具体指的是满足以下任意一项:

    • /WEB-INF/classes/ 下已加载过的 class 文件内容发生了修改;

    • /WEB-INF/lib/ 下已加载过的 jar 文件内容发生了修改,或者写入了新的 jar 文件。

还有一个疑问是tomcat如何监控文件变化,这就涉及tomcat的后台线程监测模块,具体可以参考https://www.jianshu.com/p/1278c28d1f41

org.apache.catalina.loader.WebappClassLoaderBase#modified可以得出需要发生何种改变

image-20220221162403685

image-20220221162438256

上传(即修改Context的WatchedResource)

还可以通过修改Context的WatchedResource来触发reload,好处是不需要使用EL来修改Context的reloadable。在 Tomcat 开启 autoDeploy 的情况下(此值默认为 true,即默认开启 autoDeploy),一旦发现这些文件资源的 lastModified 时间被修改,也会触发 reload

WatchedResource - The auto deployer will monitor the specified static resource of the web application for updates, and will reload the web application if it is updated. The content of this element must be a string.

在 Tomcat 9 环境下,默认的 WatchedResource 包括:

  • WEB-INF/web.xml
  • ${CATALINA_HOME}/conf/web.xml
  • WEB-INF/tomcat-web.xml

tomcat 8没有第三个

并且检测改变的方式是使用的lastModified时间戳,底层源码是File resource = new File(resources[i]);来表示这些资源(在org.apache.catalina.startup.HostConfig#checkResources中),因此很骚的一个操作是,将WatchedResource作为目录,往里面写文件,自然目录的lastModified也会改变。默认情况下是没有WEB-INF/tomcat-web.xml的,而其他两个均存在,因此创建WEB-INF/tomcat-web.xml/目录是比较保险简单的,至于Tomcat8下,覆盖其他两个也是可以的,但需要保证覆盖的内容能够正确的被tomcat解析

预期解 - 执行链的顺序

  • 第一步:基于正常重启或reload后保存session导致任意文件写,设置文件名+写入文件内容
  • 第二步:设置reloadable为true
  • 第三步:设置appBase为其他可用目录,如/。目的是防止当前部署的应用在第四部崩溃,从而无法访问部署应用目录下webshell

上述第二步和第三步的EL表达式可以调换顺序,上面三步合在一起作为成为一个EL表达式链。在tomcat中可以立即生效的是appBase的改变,即我们访问http://ip:8080/etc/passwd可以获取文件内容(appBase配置改变后,tomcat立即将新appBase目录下的子目录或.war包作为新的应用部署)

image-20220221194150815

  • 第四步:上传任意jar到对应的应用目录的WEB-INF/lib下,触发reload。需要注意:对应的目录位置是包含EL的jsp文件所在的应用目录,因为jsp中EL作用的范围是Context,一个应用对应一个Context,比如tomcat内置的docs和ROOT应用就对应着不同的Context,所以如果上传到不同的应用目录中,不会reload我们想要的应用

经过以上四步,可以实现将主机的任意目录添加作为应用目录,并写入jsp(如上appBase设置为/,则/etc/tmp等就可以使用http://[host]/[tmp|etc]/*来访问,意味着webshell的位置选择很多,不会受到原应用目录崩溃的影响)

1
2
3
4
${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}

非预期 - 硬刚ASCII JAR

非预期有两个解,两个解的总体思路相同,但是触发方式不同。总体思路都是硬刚ASCII JAR,即通过某种算法,构造满足条件的ASCII范围内的合法JAR包,然后直接上传到/META-INF/lib下,大体思路如下:

  1. 生成ASCII JAR并上传
    1. 非预期1:在jar的META-INF/resources目录下放入shell.jsp
    2. 非预期2:将Evil.class打包为jar,Evil类实现StringInterpreter接口,配合EL表达式触发实例化
  2. 触发reload:参考上面写的通过单纯上传的方式修改WatchedResource
  3. 触发RCE:
    1. 非预期1:直接访问shell.jsp
    2. 非预期2:通过EL表达式,调用StringInterpreterFactory#getStringInterpreter方法,触发恶意类的实例化

ASCII Jar

可参考:https://gv7.me/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/

p牛写的修复offset的工具:https://github.com/phith0n/PaddingZip,使用该工具,可以生成前后添加脏字符的合法zip,但是还是没办法实现ASCII jar,需要进一步改进

对于本题,对文件的写入限制很严格:

  • 文件前后有脏字符
  • 以字符串编码格式UTF-8写入(即String类型而不是byte[])。意味着即使前面可以生成不怕脏字符的jar包,但是jar包中的编码必须只能是ASCII字符,否则以字符串编码形式写入二进制数据就会出错。

整个制造前后padding脏字符的ascii jar的思路如下:

  1. ascii jar和以下四个参量有关系,而下面四个参量又由crcraw_datacompressed_data这三个参数制约,这三个参数之间都是互相影响的,能够找到一个数学公式表达其关系,但是实现成本较高。
1
2
3
4
struct.pack('<L', crc)
struct.pack('<L', len(raw_data) % pow(2, 32))
struct.pack('<L', len(compressed_data) % pow(2, 32))
struct.pack('<L', len(compressed_data) + len(zip_entry_filename) + 0x1e)
  1. 因此采用填充的方式,不断填充字符A,然后判断上面四个参量是否满足要求。但是在**@c0ny1**师傅的爆破思路中有一个小漏洞,就是尽管得到了填充A之后满足条件的ascii jar,但是经过padding前后脏字符,还是有可能出现最终padding脏字符后的jar包不是ascii jar。所以要多加一层判断
  2. 最终在@c0ny1基础上修改的项目:https://github.com/diggid4ever/ascii-jar

非预期1 - 在jar的META-INF/resources下写jsp

可以在tomcat的应用依赖目录/META-INF/lib下写入jar,该jar的/META-INF/resources目录下可以写jsp文件,该jsp可以直接访问。我们知道,tomcat对于jsp的解析是将.jsp文件转换为.class文件执行解析后,再封装成http response返回到页面中,除了我们熟知的直接写在应用目录中的jsp文件可以被解析访问,在jar中的jsp文件也可以被解析访问。

在浏览器中访问jsp页面的时候会检查jsp文件是否存在来避免创建垃圾文件夹和文件(而jar就是一种缓存)。

阅读源码,首先知道org.apache.jasper.servlet.JspServlet#service会处理访问jsp页面的请求,直接跟进到serviceJspFile

image-20220221224844914

首先会判断资源中是否可以找到这个jsp文件if (null == context.getResource(jspUri)),如果找不到,才像常规一样在webapp目录下直接取

跟进到org.apache.catalina.webresources.StandardRoot#getResource

image-20220221225448049

isCachingAllowed默认为true允许缓存,继续跟进到org.apache.catalina.webresources.CachedResource#validateResource,可以发现this.root变量是StandardRoot,存储了很多文件位置的信息

image-20220221230126353

可以发现显眼的JarResourceSet中的internalPathbase拼接起来正好是我们要找的xxx.jar/META-INF/resources。根据下面的代码获取webResource,这里就不继续跟进了,根据方法名应该能猜出来这里就是获取jsp文件的位置。

1
this.webResource = this.root.getResourceInternal(this.webAppPath, useClassLoaderResources);

image-20220221230443530

StandardRoot的构造方法中可以找到初始化的操作,添加了初始化含有的资源路径,包括jar、classes,大都是包含在/META-INF目录下的

image-20220221230603488

最后返回jsp文件路径

image-20220221230959013

注意我们还需要触发tomcat reload,才能使得tomcat重新扫描/WEB-INF/lib来初始化

非预期2 - EL配合StringInterpreter触发类实例化

applicationScope是ServletContext(具体实现是ApplicationContext)的属性集,具体解释可以参考https://yzddmr6.com/posts/tomcat-context/

1
2
3
${applicationScope[param.a]=param.b} 
相当于
pageContext.getServletContext().setAttribute("org.apache.jasper.compiler.StringInterpreter", "Pwn");

通过以上EL表达式,我们可以在访问jsp时给当前请求的ServletContext添加属性org.apache.jasper.compiler.StringInterpreter=Pwn,那么添加这个属性有什么用呢,我们的目的是访问jsp的同时执行jar包中的内容,因此可以猜测,该属性调用的代码能够触发类实例化。为什么要利用这种方式来使用lib包下的类呢?由于过滤了尖括号,所以我们不能直接import之前写入到/WEB-INF/lib中的jar包,所以我们没办法直接初始化类。

接下来就是要解释一下org.apache.jasper.compiler.StringInterpreter这个类到底有什么作用。如果先前对JSP的请求处理、jsp编译、执行流程有了解的话,可以在跟踪将jsp编译成java/class文件的过程中找到答案。

tomcat对访问jsp页面的整个处理流程:https://codeantenna.com/a/quqGVPLlrv

具体就不说了,直接定位到关键的地方,调用栈

1
2
3
4
5
6
7
8
9
10
11
getStringInterpreter:49, StringInterpreterFactory (org.apache.jasper.compiler)
<init>:3537, Generator (org.apache.jasper.compiler)
generate:3586, Generator (org.apache.jasper.compiler)
generateJava:258, Compiler (org.apache.jasper.compiler)
compile:392, Compiler (org.apache.jasper.compiler)
compile:368, Compiler (org.apache.jasper.compiler)
compile:352, Compiler (org.apache.jasper.compiler)
compile:605, JspCompilationContext (org.apache.jasper)
service:400, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:378, JspServlet (org.apache.jasper.servlet)
service:326, JspServlet (org.apache.jasper.servlet)

在编译jsp生成java文件时,使用的是Generator这个类,在初始化这个类时,会调用StringInterpreterFactory#getStringInterpreter。而StringInterpreterELInterpreter都是将jsp内容解析为java代码的编译器,StringInterpreter是把jsp页面中对象信息转换为String类型,ELInterpreter是把EL表达式转换为java代码。

image-20220222153558265

跟进org.apache.jasper.compiler.StringInterpreterFactory#getStringInterpreter,如果设置了前面所说的属性,则会把该属性值作为类来实例化(调用createInstance),最后转换为StringInterpreter类型返回(这里有点像SPI机制)。这里可以发现其直接实例化的属性值的类,没有任何检查。

image-20220222154059648

image-20220222154246342

所以我们可以借助一下EL表达式,来设置org.apache.jasper.compiler.StringInterpreter属性为恶意类名,再次访问该jsp,编译时触发StringInterpreterFactory#getStringInterpreter来实现恶意类的实例化。

1
2
3
${applicationScope[param.a]=param.b} 
发送请求:?a=org.apache.jasper.compiler.StringInterpreter&b=Exploit
相当于执行:pageContext.getServletContext().setAttribute("org.apache.jasper.compiler.StringInterpreter", "Evil");

调试tomcat启动和jsp解析

项目依赖如下,注意要排除一下依赖冲突,否则maven一直报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.46</version>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.46</version>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jsp-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-juli</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-util-scan</artifactId>
</exclusion>
</exclusions>
<scope>provided</scope>
</dependency>

参考

https://mp.weixin.qq.com/s/QQ2xR32Fxj_nnMsFCucbCg

https://cloud.tencent.com/developer/article/1054409

https://github.com/Arusekk/ascii-zip

https://www.jianshu.com/p/1278c28d1f41

https://yzddmr6.com/posts/tomcat-context/

https://codeantenna.com/a/quqGVPLlrv

Ascii jar:https://gv7.me/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/

CATALOG
  1. 1. 前言
  2. 2. reload条件
    1. 2.1. 使用EL表达式+上传
    2. 2.2. 上传(即修改Context的WatchedResource)
  3. 3. 预期解 - 执行链的顺序
  4. 4. 非预期 - 硬刚ASCII JAR
    1. 4.1. ASCII Jar
    2. 4.2. 非预期1 - 在jar的META-INF/resources下写jsp
    3. 4.3. 非预期2 - EL配合StringInterpreter触发类实例化
  5. 5. 调试tomcat启动和jsp解析
  6. 6. 参考