Struts2 漏洞分析系列 - S2-009/003與005的補丁繞過

漏洞概述
S2-009是S2-003與S2-005的補丁繞過,當時的補丁是增加了正則以及相關的限制(這些限制可以通過執行OGNL表達式進行修改),主要的防御還是正則。
這次的問題還是出現在ParameterInterceptor這個攔截器上,其漏洞原理類似于二次注入,先將Payload注入到上下文中,取出來時通過某個特定語法就可以執行之前設置過的Payload。
影響版本:2.0.0 – 2.3.1.1
官方issue地址:https://cwiki.apache.org/confluence/display/WW/S2-009
環境搭建
首先編寫一個最簡單的Action類,其中只需要存在一個屬性即可:
public class TestAction { private String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String execute() throws Exception { return "success"; }}
接著編寫struts.xml用于定義路由:
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd">
index.jsp
最后老規矩,定義一個Filter:
struts2 org.apache.struts2.dispatcher.FilterDispatcher
struts2 /*
漏洞分析
ParameterInterceptor的作用就是將當前請求中的參數與Bean中的屬性綁定在一起,所以http://127.0.0.1:8080/test.action?message=xxx會將xxx這個值賦到當前請求對象TestAction的message屬性中,在setValue調用完畢后可以通過getValue取出來:

接下來會繼續對下一個參數進行解析,通過S2-003與S2-005的分析中我們得知,如果能夠通過ParameterInterceptor的相關驗證邏輯,那么是會對參數名進行一次OGNL表達式解析的,S2-003與S2-005的漏洞也出于此,后續的修復方案是增加了靜態方法相關的禁用以及一個用于驗證參數名的正則。
但是在S2-009中我們可以通過top['message']的方式獲取到剛剛賦到message屬性上的值,并通過()執行OGNL表達式解析,并且top['message']是符合正則條件的:

所以完整的Payload如下(實際情況利用時需要進行URL編碼):
http://localhost:8082/test.action?message=#context['xwork.MethodAccessor.denyMethodExecution']=false,#_memberAccess["allowStaticMethodAccess"]=new java.lang.Boolean(true),@java.lang.Runtime@getRuntime().exec('open -a /System/Applications/Calculator.app'),#request&top['message'](0)
修復方案
這次漏洞修復體現在多處代碼,首先就是ParametersInterceptor中,其將原先調用的setValue修改為setParameter,將兩者作為兩個模塊區分開了。
區別是什么呢?重點就是傳給setValue的第四個參數:
public void setParameter(String expr, Object value) { this.setValue(expr, value, this.devMode, false);}
public void setValue(String expr, Object value, boolean throwExceptionOnFailure) { this.setValue(expr, value, throwExceptionOnFailure, true);}
可以發現,setParameter的第四個參數為false,而setValue的第四個參數為true,這影響到了后續的調用流程,讓我們接著跟到最后的調用流程中:
protected void setValue(String name, Map<String, Object> context, Object root, Object value, boolean evalName) throws OgnlException { Object tree = this.compile(name); if (!evalName && this.isEvalExpression(tree, context)) { throw new OgnlException("Eval expression cannot be used as parameter name"); } else { Ognl.setValue(tree, context, root, value); } }
這里會判斷當前的name是否為evalName,此處為setParameter,因此evalName為false,所以這里為true,接著會通過isEvalExpression來判斷當前的name是否符合要求。
private boolean isEvalExpression(Object tree, Map<String, Object> context) throws OgnlException { if (tree instanceof SimpleNode) { SimpleNode node = (SimpleNode)tree; return node.isEvalChain((OgnlContext)context); } else { return false; } }
isEvalExpression中會通過isEvalChain來判斷當前的node是否為鏈式調用(先取值再執行就是鏈式調用),Debug一下會發現之前的Payload在此處已經返回true了,被標為危險的name,因此這里會直接拋出異常而不會進行接下來的OGNL表達式解析:

由于我們的Node會被解析為ASTEvalNode,其isEvalChain相關邏輯如下:
public boolean isEvalChain(OgnlContext context) throws OgnlException { return true; }
可以發現是直接返回true的,因此所有EvalNode都不能在這個漏洞點中使用了,如果想要繼續挖掘只能換一個Node看看是否能進行二次解析或是能夠達到與EvalNode相同作用(通過繼承邏輯)。
上面是一個修復點,還有另外一個修復點在2.3.1.2中似乎沒有啟用,就是xx中的正則被修改為了:
\w+((\\.\\w+)|(\\[\\d+\\])|(\\(\\d+\\))|(\\['\\w+'\\])|(\\('\\w+'\\)))*
這個正則的作用是匹配name中的字母與數字,匹配不了特殊符號,我認為Struts2官方應該是想在這里取出name中不包含特殊符號的部分,接著通過setValue進行一個賦值,如下:

但不知道為什么在這個版本沒有啟用,我認為這算是一個比較好的修復方案,不會太影響后面的業務邏輯,直接從漏洞點出發而不是直接在底層封死了,可能Struts2有它們自己的考究吧。