Struts2漏洞笔记之S2-001
前置知识
Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。 Struts 2是Struts的下一代产品,是在struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。
1.OGNL(Object Graph Navigation Language)对象导航图语言
Struts2框架使用OGNL作为默认的表达式语言,OGNL(Object Graph Navigation Language),是一种表达式语言,目的是为了在不能写Java代码的地方执行java代码;主要作用是用来存数据和取数据的。
2.关于Xwork、ActionContext、OgnlValueStack相关知识可以参考链接:https://milkfr.github.io/java/2019/02/04/java-struts2-4/
版本影响
2.0.1 ~ 2.0.8
漏洞原理
处理登陆问题上,验证失败返回原界面,在处理回显时,框架解析JSP页面标签时会对用户输入的Value值获取,在获取对应的Value值中递归解析%{、}
造成了二次解析,最终触发表达式注入漏洞,执行任意代码。
程序入口
struts 2.0.8中Web.xml配置org.apache.struts2.dispatcher.FilterDispatcher
为程序入口点,执行doFilter方法.在其中较关键创建OgnlValueStack,并添加相应的数据.
OgnlValueStack创建和数据载入
在Ognl解析表达式中存在关键的三要素expr、root、Context
,在expr为可解析的表达式需要符合相关语法。接着需要关注root、Context如何载入到对象中。根据框架的分析可知Struts2中利用OgnlValueStack存储数据栈,而在创建之后将相关参数插入进root与Context.
跟进程序入口点FilterDispatcher.doFilter->this.dispatcher.serviceAction(....)
中会先调用this.createContextMap
Dispatcher.createContextMap
会获取当前请求的参数并以Map
形式保存,最后载入extracontext
中.
接着会获取当前访问的命名空间、文件名、方法名
在初始化ActionProxy
时会创建一个OgnlValueStack
实例(DefaultActionInvocation.createContextMap()
)
接着会将extraContext通过putAll存放进stack.Context中.
调用push将当前访问生成的实例化Action存入stack.root中.但是这时生成的Action并没有设置上username
与password
ParametersInterceptor载入参数
ParametersInterceptor拦截器又继承自MethodFilterInterceptor,其主要功能是把ActionContext中的请求参数设置到ValueStack中,如果栈顶是当前Action则把请求参数设置到了Action中,如果栈顶是一个model(Action实现了ModelDriven接口)则把参数设置到了model中。
跟进代码看下究竟
ParametersInterceptor.doIntercept
会从ActionContext上下文中取出parameters
跟进ParametersInterceptor.setParameters
一路跟进在OgnlRuntime.setMethodValue
中根菌propertyName获取该属性的set方法.接着执行OgnlRuntime.callAppropriateMethod
反射执行setPassword
方法
执行Action
在一系列拦截器执行完毕后,调用DefaulActionInvocation.invokeActionOnly()执行Action操作.继续跟进DefaulActionInvocation.invokeAction
会先获取需要执行该Action实例的方法,该方法在创建ActionProxy获取,没有制定方法时,会默认调用execute
方法,接着会反射执行LoginAction.execute()
Result处理
当执行execute返回”error”作为ResultCode返回,(可以看作账号验证失败仍然停留在登陆界面),执行StrutsResultSupport.doExcute()后框架将会开始处理页面回显,而其中会调用中间件tomcat调度器ApplicationDispatcher由于访问jsp文件,会调用JspServlet
处理请求。接着Struts2会利用doStartTag、doEndTag解析标签.
通过向页面请求
username=1&password=%25{%40java.lang.System%40getProperty("user.dir")}
进入doEndTag解析标签
<s:textfield name="password" label="password" />
进入UIBean解析公共标签,满足IF语句后会对password拼接%{
字符串为%{password}
.
之后会进入TextParseUtil.translateVariables
递归判断当前返回字符串是否含有%{}
字符串,满足的话会剔除掉%{}
,执行findValue方法,从当前值栈中找到password
获得对应的值%{@java.lang.System@getProperty("user.dir")}
由于TextParseUtil.translateVariables
的递归判断,会再一次执行获得的值%{@java.lang.System@getProperty("user.dir")}
造成二次解析,最后将结果保存值parameters.nameValue
.在解析模版时会获取parameters.nameValue
值,将执行代码的结果输出到浏览器上.
漏洞复现
修复
TextParseUtil.translateVariables
限制递归深度,仅解析一次表达式
总结
其实这是第二遍分析s2-001漏洞,在调试一遍学到很多,主要从框架出发来看待这个问题.
1.ThreadLocal设计模式,保证线程安全,使得每次拿到的ActionContext不受影响.
2.二次解析漏洞挖掘思路,分析至此,究其原因在于递归调用,最后在调用stack.findValue时会解析表达式.(或许写个全局搜findValue有惊喜呢)
3.过一遍文档和框架的生命周期在搭环境和理解代码也会有帮助.
该篇文章没有对Ognl如何解析表达式进一步分析,感觉有点麻烦,后续单独切一个知识点来学习.
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!