CVE-2019-0230 s2-059 漏洞分析
0x01 問題原因
Apache Struts 框架在強制時對分配給某些標記屬性的屬性值執行雙重計算,以便可以傳遞一個值,該值將在呈現標記的屬性時再次計算。對於精心設計的請求,這可能會導致遠端程式碼執行 (RCE)。
只有在 Struts 標記屬性內強制使用OGNL表示式時,當表示式用於計算攻擊者可以通過建立相應請求直接修改的原始未驗證輸入時,問題才適用。
0x02 例子
<s:url var="url" namespace="/employee" action="list"/><s:a id="%{skillName}" href="%{url}">List available Employees</s:a>
如果攻擊者能夠修改請求中的屬性,使原始 OGNL 表示式傳遞到屬性而無需進一步驗證,則當作為請求呈現標記時,屬性中包含的提供的 OGNL 表示式將計算。
0x03 影響範圍
Struts 2.0.0 - Struts 2.5.20
官方建議升級到2.5.22以後。
0x03 環境
- tomcat7
- jk8
- struts2.3.7-src
0x04 原始碼部署
在showcase專案中,在首頁showcase.jsp的head標籤中加入
<head> <title>Struts2 Showcase</title> <%-- <s:head theme="simple"/>--%> <s:a id="%{1+1}" href="xxx.jsp"></s:a> </head>
在struts專案中,view裡需要加入<%@ taglib prefix="s" uri="/struts-tags" %>
才可解析jsp裡的struts標籤。
參考https://blog.csdn.net/sho_ko/article/details/84175306
對struts的標籤原始碼解析過程。
當用戶發出請求時,doStartTag()
開始執行,首先就呼叫getBean獲取對應的標籤元件類例項,建構函式引數值棧stack由基類StrutsBodyTagSupport
的getStack()
獲得,request和response物件在PageContext例項中獲取。然後呼叫populateParams();
populateParams()
也將呼叫具體類中的populateParams()對自己的屬性成員進行初始化。
所以我們直接debug斷點在解析標籤的ComponentTagSupport
類的doStartTag()
函式的第一行。
可以看到this的href屬性就是xxx.jsp,因為文章說的在this.populateParams();
裡進行填充,所以我們跟進populateParams()看看,一直到抽象類的父類AbstractUITag
類的populateParams操作裡看到了uiBean.setId(this.id);
,對id裡的值進行了填充。
通過該類:
UIBean uiBean = (UIBean)this.component;
uiBean.setCssClass(this.cssClass);
uiBean.setCssStyle(this.cssStyle);
uiBean.setCssErrorClass(this.cssErrorClass);
uiBean.setCssErrorStyle(this.cssErrorStyle);
uiBean.setTitle(this.title);
uiBean.setDisabled(this.disabled);
uiBean.setLabel(this.label);
uiBean.setLabelSeparator(this.labelSeparator);
uiBean.setLabelposition(this.labelposition);
uiBean.setRequiredPosition(this.requiredPosition);
uiBean.setErrorPosition(this.errorPosition);
uiBean.setName(this.name);
uiBean.setRequiredLabel(this.requiredLabel);
uiBean.setTabindex(this.tabindex);
uiBean.setValue(this.value);
uiBean.setTemplate(this.template);
uiBean.setTheme(this.theme);
uiBean.setTemplateDir(this.templateDir);
uiBean.setOnclick(this.onclick);
uiBean.setOndblclick(this.ondblclick);
uiBean.setOnmousedown(this.onmousedown);
uiBean.setOnmouseup(this.onmouseup);
uiBean.setOnmouseover(this.onmouseover);
uiBean.setOnmousemove(this.onmousemove);
uiBean.setOnmouseout(this.onmouseout);
uiBean.setOnfocus(this.onfocus);
uiBean.setOnblur(this.onblur);
uiBean.setOnkeypress(this.onkeypress);
uiBean.setOnkeydown(this.onkeydown);
uiBean.setOnkeyup(this.onkeyup);
uiBean.setOnselect(this.onselect);
uiBean.setOnchange(this.onchange);
uiBean.setTooltip(this.tooltip);
uiBean.setTooltipConfig(this.tooltipConfig);
uiBean.setJavascriptTooltip(this.javascriptTooltip);
uiBean.setTooltipCssClass(this.tooltipCssClass);
uiBean.setTooltipDelay(this.tooltipDelay);
uiBean.setTooltipIconPath(this.tooltipIconPath);
uiBean.setAccesskey(this.accesskey);
uiBean.setKey(this.key);
uiBean.setId(this.id);
uiBean.setDynamicAttributes(this.dynamicAttributes);
側面我們也可以瞭解到這些屬性都是可以進行ognl表示式的。
public void setId(String id) {
if (id != null) {
this.id = this.findString(id);
}
}
跟進findString
:
protected String findString(String expr) {
return (String)this.findValue(expr, String.class);
}
在跟進findValue
:
protected Object findValue(String expr, Class toType) {
if (this.altSyntax() && toType == String.class) {
return ComponentUtils.containsExpression(expr) ? TextParseUtil.translateVariables('%', expr, this.stack) : expr;
} else {
expr = this.stripExpressionIfAltSyntax(expr);
return this.getStack().findValue(expr, toType, this.throwExceptionOnELFailure);
}
}
然後可以看到TextParseUtil
類了,根據if條件可知,this.altSyntax()
需要ture,預設是true的。
所以預設是進入TextParseUtil.translateVariables('%', expr, this.stack)
。
一直跟入,來到translateVariables
在這個while裡,
while(true) {
int start = expression.indexOf(lookupChars, pos);
if (start == -1) {
++loopCount;
start = expression.indexOf(lookupChars);
}
if (loopCount > maxLoopCount) {
break;
}
在 Object o = evaluator.evaluate(var);
裡進行了值計算,
但是如果%{%{1+1}+1}
,
因為start=-1,所以執行++loopCount
,
因為loopCount=2>maxLoopCount=1,所以會break這個while,導致不能執行Object o = evaluator.evaluate(var);
,所以會返回值為空。
這是一個埋點。
所以我們繼續追蹤,來到boolean evalBody = this.component.start(this.pageContext.getOut());
我們看下這時的this.component
變數,
可以看到id已經計算出來了。
進入start函式,最終來到:
public boolean start(Writer writer) {
boolean result = super.start(writer);
try {
this.evaluateParams();
this.mergeTemplate(writer, this.buildTemplateName(this.openTemplate, this.getDefaultOpenTemplate()));
} catch (Exception var4) {
LOG.error("Could not open template", var4, new String[0]);
}
進入evaluateParams
,
可以看到大量熟悉的findString
函式,也就是說這邊是有可能再次進行ognl表示式計算的。
一路跟蹤:
進入this.populateComponentHtmlId(form);
protected void populateComponentHtmlId(Form form) {
String tryId;
if (this.id != null) {
tryId = this.findStringIfAltSyntax(this.id);
} else {
String generatedId;
if (null == (generatedId = this.escape(this.name != null ? this.findString(this.name) : null))) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot determine id attribute for [#0], consider defining id, name or key attribute!", new Object[]{this});
}
tryId = null;
} else if (form != null) {
tryId = form.getParameters().get("id") + "_" + generatedId;
} else {
tryId = generatedId;
}
}
來到tryId = this.findStringIfAltSyntax(this.id);
因為altSyntax()預設true,所以來到熟悉的findString計算操作。所以this.id在第一次ognl計算成功後還會進行一次ognl計算,假如第一次ognl計算表示式的結果是%{1+2}
,
那麼此處就存在一定的風險。但是實際情況下,不管是第一次ognl計算還是第二次ognl計算,均都沒法執行復雜計算。等有進一步的poc訊息。