Struts2漏洞系列之S2-001利用表单错误进行远程代码执行
Posted 安恒信息安全研究院
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Struts2漏洞系列之S2-001利用表单错误进行远程代码执行相关的知识,希望对你有一定的参考价值。
Smi1e@Pentes7eam
漏洞信息: https://cwiki.apache.org/confluence/display/WW/S2-001
当我们提交表单并验证失败时,由于Strust2默认会原样返回用户输入的值而且不会跳转到新的页面,因此当返回用户输入的值并进行标签解析时,如果开启了
altSyntax
,会调用translateVariables
方法对标签中表单名进行OGNL
表达式递归解析返回ValueStack
值栈中同名属性的值。因此我们可以构造特定的表单值让其进行OGNL
表达式解析从而达到任意代码执行。
漏洞复现
altSyntax
功能允许将 OGNL
表达式插入到文本字符串中并以递归方式处理。
使用struts2的 s
标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。
影响范围
Struts 2.0.0 - Struts 2.0.8 , WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5
漏洞分析
首先Struts2的运行流程是
HTTP请求经过一系列的过滤器,最后到达
FilterDispatcher
过滤器。FilterDispatcher
将请求转发给ActionMapper
,判断该请求是否需要处理。如果该请求需要处理,
FilterDispatcher
会创建一个ActionProxy
来进行后续的处理。ActionProxy
拿着HTTP请求,询问struts.xml
该调用哪一个Action
进行处理。当知道目标
Action
之后,实例化一个ActionInvocation
来进行调用。然后运行在
Action
之前的拦截器。运行
Action
,生成一个Result
。Result
根据页面模板和标签库,生成要响应的内容。根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。
在漏洞关键位置 xwork-2.0-beta-1.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
中的translateVariables
方法下断点。
前面都是一些应用初始化、调度、Struts2 拦截器执行、Action执行等操作,直接跳过。
因为 Action
的返回值为error,所以doForward
会跳转到 index.jsp
,后面再进行一系列过滤器调用、JspServlet
调用解析jsp等操作。
我们的 index.jsp
文件
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>
直接跟到 org.apache.struts2.views.jsp.ComponentTagSupport
的 doStartTag
方法,这里会对jsp标签进行解析,从 form
标签开始,我们步入到解析 textfield
标签时。
执行完 doStartTag
后会再次回到 index.jsp
,此时遇到了相应的闭合标签 />
,会跳转到 doEndTag()
:
public int doEndTag() throws JspException {
this.component.end(this.pageContext.getOut(), this.getBody());
this.component = null;
return 6;
}
跟进 component.end()
,到达org.apache.struts2.components.UIBean#end
继续跟入this.evaluateParams();
,由于开启了 altSyntax
,expr = %{password}
altSyntax
功能是 Struts2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的 OGNL
表达式进行解析并执行。altSyntax
功能在处理标签时,对 OGNL
表达式的解析能力实际上是依赖于开源组件 XWork
。
继续跟入this.findValue(expr, valueClazz);
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);
}
}
由于开启了 altSyntax
,toType 为 class.java.lang.string
,跟入com.opensymphony.xwork2.util.TextParseUtil
中的translateVariables('%', expr, this.stack);
public static String translateVariables(char open, String expression, ValueStack stack) {
return translateVariables(open, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();
}
最终来到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}
,先经过while循环确定出表达式的start和end,然后取出来password
赋值给var
变量
接着调用stack.findValue(var, asType);
,这里的stack为OgnlValueStack
,它是ValueStack
的实现类。
ValueStack
是 Struts2
的一个接口,字面意义为值栈,类似于一个数据中转站,Struts2
的数据都保存在 ValueStack
中。客户端发起一个请求 struts2
会创建一个 Action
实例同时创建一个 OgnlValueStack
值栈实例,OgnlValueStack
贯穿整个 Action
的生命周期。Struts2
中使用 OGNL
将请求 Action
的参数封装为对象存储到值栈中,并通过 OGNL
表达式读取值栈中的对象属性值。
ValueStack
中有两个主要区域
CompoundRoot 区域:是一个ArrayList,存储了
Action
实例,它作为OgnlContext
的Root
对象。获取root数据不需要加#
context 区域:即
OgnlContext
上下文,是一个Map,放置web开发常用的对象数据的引用。request、session、parameters、application
等。获取context
数据需要加#
操作值栈,通常指的是操作 ValueStack
中的 root
区域。
OgnlValueStack
的 findValue
方法可以在 CompoundRoot
中从栈顶向栈底找查找对象的属性值。
跟进 OgnlUtil.getValue(expr, this.context, this.root, asType);
public static Object getValue(String name, Map context, Object root, Class resultType) throws OgnlException {
return Ognl.getValue(compile(name), context, root, resultType);
}
Ognl.getValue(compile(name), context, root, resultType);
会根据 root
和 context
对象对表达式 compile(name)
进行解析。
这里因为 name
为 password
,会从 root
栈中 LoginAction
对象中 获取到我们提交的参数 %{1+1}
并返回。
返回值 o
进行一系列处理后,最后又赋值给 expression
,接着会再次进入 translateVariables
进行解析,然后进入下一次 while
循环
这次是 1+1
进入到 stack.findValue
,从而在 Ognl.getValue(compile(name), context, root, resultType);
中进行表达式解析也就是执行我们的payload最终返回 2
漏洞修复
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) {
int start = expression.indexOf(open + "{", pos);
if (start == -1) {
pos = 0;
loopCount++;
start = expression.indexOf(open + "{");
}
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;
......
}
}
漏洞的原因是当我们提交表单并验证失败时,由于Strust2默认会原样返回用户输入的值而且不会跳转到新的页面,因此当返回用户输入的值并进行标签解析时,如果开启了 altSyntax
,会调用 translateVariables
方法对标签中表单名进行 OGNL
表达式递归解析返回 ValueStack
值栈中同名属性的值。
例如我们提交的表单名为 password
值为%{1+1}
,由于开启了altSyntax
会使用 %{}
包裹,并调用 translateVariables
对 %{password}
进行 OGNL
表达式解析,此时值栈中栈顶为 LoginAction
,他的 password
属性值为我们提交的 %{1+1}
,在执行 ParametersInterceptor
拦截器时就已经 set
进去了,解析出来以后发现有%{}
标签包裹,因此继续下一个while循环进行解析,直到返回没有 %{}
标签包裹的值 2
才跳出while循环并返回。
可以看到修复代码中加入了一个 maxLoopCount
参数和 if 判断条件来限制递归解析次数。
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
表单验证错误并不是该漏洞的产生的原因
Struts2-命令-代码执行漏洞分析系列 S2-001
当我们把 Action
的 password
属性硬编码为 %{1+1}
时,依然会执行payload,因此表单验证错误并不是该漏洞产生的原因,不过表单验证错误是这个漏洞出现的场景之一。
在struts2框架中,配置了
Validation
,倘若验证出错往往默认会原样返回用户输入的值而且不会跳转到新的页面,而在最后解析页面时解析了用户输入的值,从而执行payload。在实际场景中,比如登陆等处,往往会配置了Validation,比如限制用户名长度,数字的范围等等,从而成为了该漏洞的高发区。
关于我们
人才招聘
一、高级攻防研究员
工作地点:
1.杭州/重庆/上海/北京;
岗位职责:
1.前沿攻防技术研究;
2.负责完成定向渗透测试任务;
3.负责红队工具的研发。
任职要求:
1.三年以上相关工作经验,若满足以下所有条件,则可忽略此要求;
2.熟练掌握Cobalt Strike、Empire、Metasploit等后渗透工具的使用;
3.熟练掌握工作组/域环境下的各种渗透思路、手段;
4.具有大型、复杂网络环境的渗透测试经验;
5.具有独立的漏洞挖掘、研究能力;
6.熟练至少一门开发语言,不局限于C/C++、Java、php、Python等;
7.良好的沟通能力和团队协作能力。
加分项:
1.红队工具开发经验;
2.有良好的技术笔记习惯。
联系人:姜女士
邮箱:double.jiang@dbappsecurity.com.cn
手机;15167179002,微信同号
以上是关于Struts2漏洞系列之S2-001利用表单错误进行远程代码执行的主要内容,如果未能解决你的问题,请参考以下文章