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以后。

0x04 環境

  • tomcat7
  • jk8
  • struts2.3.7-src

0x05 源碼部署

在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由基類StrutsBodyTagSupportgetStack()獲得,request和response對象在PageContext實例中獲取。然后調用populateParams();進行初始參數值的填充,populateParams()也將調用具體類中的populateParams()對自己的屬性成員進行初始化。

所以我們直接debug斷點在解析標簽的ComponentTagSupport類的doStartTag()函數的第一行。
image.png
可以看到this的href屬性就是xxx.jsp,因為文章說的在this.populateParams();里進行填充,所以我們跟進populateParams()看看,一直到抽象類的父類AbstractUITag類的populateParams操作里看到了uiBean.setId(this.id);,對id里的值進行了填充。
image.png
通過該類:

        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);

進入setId操作
image.png

    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
image.png

在這個while里,

while(true) {
                int start = expression.indexOf(lookupChars, pos);
                if (start == -1) {
                    ++loopCount;
                    start = expression.indexOf(lookupChars);
                }

                if (loopCount > maxLoopCount) {
                    break;
                }

image.png

Object o = evaluator.evaluate(var);里進行了值計算,
image.png

但是如果%{%{1+1}+1},
image.png
因為start=-1,所以執行++loopCount
因為loopCount=2>maxLoopCount=1,所以會break這個while,導致不能執行Object o = evaluator.evaluate(var);,所以會返回值為空。
這是一個埋點。

所以我們繼續追蹤,來到boolean evalBody = this.component.start(this.pageContext.getOut());
image.png
我們看下這時的this.component變量,
image.png
可以看到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
image.png
可以看到大量熟悉的findString函數,也就是說這邊是有可能再次進行ognl表達式計算的。

一路跟蹤:
image.png
進入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;
            }
        }

image.png

來到tryId = this.findStringIfAltSyntax(this.id);

image.png

因為altSyntax()默認true,所以來到熟悉的findString計算操作。所以this.id在第一次ognl計算成功后還會進行一次ognl計算,假如第一次ognl計算表達式的結果是%{1+2}
image.png

那么此處就存在一定的風險。但是實際情況下,不管是第一次ognl計算還是第二次ognl計算,均都沒法執行復雜計算。等有進一步的poc消息。

0x6 參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM