Java OGNL表达式注入漏洞原理研究 - 郑瀚Andrew
2023-10-27 11:38:0 Author: www.cnblogs.com(查看原文) 阅读量:15 收藏

0x1:什么是Java中的对象图

来看一个例子:

Class SchoolMaster{
    String name = "wanghua";
}

Class School
{
    String name = "tsinghua";
    SchoolMaster schoolMaster;
}

Class Student
{
    String name = "xiaoming";
    School school;
}

创建实例学校school = new School()、学生student = new Student()和校长schoolMaster = new SchoolMaster(),将学校校长指定为schoolMaster实例-school.schoolMaster = schoolMaster,学生的学校指定为school实例-student.school = school,那么三者就连接起来了形成了一个对象图,对象图基本可以理解为对象之间的依赖图。

通过对象图我们可以获取到对象的属性甚至对象的方法。

OGNL就是实现这个目的的一种语言,OGNL全称Object-Graph Navigation Language即对象导航图语言,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。

OGNL是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以

  • 存取对象的任意属性
  • 调用对象的方法
  • 遍历整个对象的结构图
  • 实现字段类型转化等功能

对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个 “路径” 来完成对象信息的导航,这个 “路径” 可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

OGNL表达式具有以下特点:

  • 支持对象方法调用,如objName.methodName()
  • 支持类静态方法调用和值访问,表达式的格式为 @[类全名(包括包路径)]@[方法名|值名],如@java.lang.String@format(‘fruit%s’,’frt’)
  • 支持赋值操作和表达式串联,如price=100、discount=0.8,calculatePrice(price*discount)这个表达式会返回80
  • 访问OGNL上下文(OGNL context)和ActionContext
  • 操作集合对象
  • 可以直接new一个对象

0x2:OGNL三要素

OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。

  • 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作
  • 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境
  • 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值

以下是一个OGNL的示例。

新建maven项目,

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>OGNL_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/ognl/ognl -->
        <dependency>
            <groupId>ognl</groupId>
            <artifactId>ognl</artifactId>
            <version>3.1.19</version>
        </dependency>
    </dependencies>

</project>

School.java

package org.example;

public class School {
    String name = "tsinghua";
    SchoolMaster schoolMaster;

    public void setName(String s) {
        name = s;
    }

    public String getName() {
        return name;
    }

    public void setSchoolMaster(SchoolMaster s) {
        schoolMaster = s;
    }

    public SchoolMaster getSchoolMaster() {
        return schoolMaster;
    }
}

Student.java

package org.example;

public class Student {
    String name = "xiaoming";
    School school;

    public void setName(String s) {
        name = s;
    }

    public String getName() {
        return name;
    }

    public void setSchool(School s) {
        school = s;
    }

    public School getSchool() {
        return school;
    }
}

SchoolMaster.java

package org.example;

public class SchoolMaster {
    String name = "wanghua";

    public SchoolMaster(String s) {
        name = s;
    }

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

    public String getName() {
        return name;
    }
}

Main.java

package org.example;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) throws OgnlException {
        // 创建Student对象
        School school = new School();
        school.setName("tsinghua");
        school.setSchoolMaster(new SchoolMaster("wanghua"));
        Student student1 = new Student();
        student1.setName("xiaoming");
        student1.setSchool(school);
        Student student2 = new Student();
        student2.setName("zhangsan");
        student2.setSchool(school);

        // 创建上下文环境
        OgnlContext context = new OgnlContext();
        // 设置根对象root
        context.setRoot(student1);
        context.put("student2", student2);
        // 获取ognl的root相关值
        Object name1 = Ognl.getValue("name", context, context.getRoot());
        Object school1 = Ognl.getValue("school.name", context, context.getRoot());
        Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());
        System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);
        // 获取ognl非root相关值
        Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());
        Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());
        Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());
        System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);
    }
}

在上面示例中,

  • 根对象是student1实例,context中设置了根对象和非根对象student2
  • 表达式有name、school.name、school.schoolMaster.name和student2.name、#student2.school.name、student2.school.schoolMaster.name,前三个是通过表达式获取root也就是student1对象的相关属性,后三个是通过表达式获取容器变量student2对象的相关属性。

参考链接: 

http://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/
https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/
https://xz.aliyun.com/t/10482
https://jueee.github.io/2020/08/2020-08-15-Ognl%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/

0x1:OGNL命令执行原理

基于以上OGNL表达式原理,OGNL可以访问静态方法、属性以及对象方法等,其中包含可以执行恶意操作如命令执行的类java.lang.Runtime等,当OGNL表达式外部可控时,攻击者就可以构造恶意的OGNL表达式来让程序执行恶意操作,这就是OGNL表达式注入漏洞。

我们可以由很容易写出Java执行命令的OGNL表达式。

package org.example;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) throws OgnlException {
        // 创建Student对象
        School school = new School();
        school.setName("tsinghua");
        school.setSchoolMaster(new SchoolMaster("wanghua"));
        Student student1 = new Student();
        student1.setName("xiaoming");
        student1.setSchool(school);
        Student student2 = new Student();
        student2.setName("zhangsan");
        student2.setSchool(school);

        // 创建上下文环境
        OgnlContext context = new OgnlContext();
        // 设置根对象root
        context.setRoot(student1);
        context.put("student2", student2);
        // 获取ognl的root相关值
        Object name1 = Ognl.getValue("name", context, context.getRoot());
        Object school1 = Ognl.getValue("school.name", context, context.getRoot());
        Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());
        System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);
        // 获取ognl非root相关值
        Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());
        Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());
        Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());
        System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);

        // OGNL命令执行
        // Object res = Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"open -a Calculator\")", context, context.getRoot());
        // 理论上只要存在一个OGNL注入点,就可以基于Java的内省和反射机制,实现命令执行
        Object res = Ognl.getValue("(new java.lang.ProcessBuilder(new java.lang.String[]{\"open\", \"-a\", \"Calculator\"})).start()", context, context.getRoot());
    }
}

一个更简单的POC如下,

import ognl.Ognl;
import ognl.OgnlContext;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个OGNL上下文对象
        OgnlContext context = new OgnlContext();

        // getValue()触发
        // @[类全名(包括包路径)]@[方法名|值名]
        Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')", context, context.getRoot());
        
        // setValue()触发
//        Ognl.setValue(Runtime.getRuntime().exec("calc"), context, context.getRoot());
    }
}

getValue()和setValue()都能成功解析恶意的OGNL表达式、触发弹计算器。

漏洞的触发点就在Ognl.getValue()这里。

Ognl.getValue()处理表达式时,会先生成一个tree,这个tree本质是SimpleNode实例,树的每个节点都是一个ASTChain实例,ASTChain继承自SimpleNode。

当调用node.getValue(ognlContext, root);时,会调用SimpleNode.getValue()进行处理,SimpleNode.getValue()会通过SimpleNode.evaluateGetValueBody()计算结果。 

SimpleNode.evaluateGetValueBody()在计算非常量情况的结果时会调用子类的getValueBody,Ognl在处理节点时分为多种情况进行处理:ASTChain、ASTConst、ASTCtor、ASTInstanceof、ASTList、ASTMethod、ASTStaticField、ASTStaticMethod等。 

ASTChain.getValueBody()在处理时,会迭代调用getValue处理子节点的结果,最终还是会调用ASTXXX方法处理节点的结果。

当Ognl计算@java.lang.Runtime@getRuntime()时,由于方法时静态方法会调用ASTStaticMethod.getValueBody。ASTStaticMethod.getValueBody通过OgnlRuntime.callStaticMethod处理方法的调用。 

通过OgnlRuntime.callAppropriateMethod()处理方法调用,最终会调用Method.invoke()进行方法调用并返回值。 

同样的,Ognl计算exec("calc")时,调用ASTMethod.getValueBody,最终也是在OgnlRuntime.callAppropriateMethod()中调用Method.invoke()处理。 

0x2:OGNL高版本下的黑名单 

OGNL在>=3.1.25、>=3.2.12的版本中增加了黑名单。我们将依赖更新为3.1.25,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>OGNL_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/ognl/ognl -->
        <dependency>
            <groupId>ognl</groupId>
            <artifactId>ognl</artifactId>
            <version>3.1.25</version>
        </dependency>
    </dependencies>

</project>

然后再次运行,就会得到一个报错信息,如下: 

根据报错信息,跟进到OgnlRuntime#invokeMethod,可以看到如下的黑名单:

0x3:HTTP请求中常见的注入点

参考链接:

https://boogipop.com/2023/04/25/Struct2%20OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/ 
http://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/ 

0x1:Confluence CVE-2021-26084

1、Confluence简介

Confluence是一个专业的企业知识管理与协同软件,也可以用于构建企业wiki。使用简单,它强大的编辑和站点管理特征能够帮助团队成员之间共享信息、文档协作、集体讨论,信息推送。

2、Confluence velocity模板引擎语法

Confluence velocity模板引擎基本语法如下:

"#"标识velocity的脚本语句
"$"获取一个对象或变量
"{}"用来标识velocity变量
"!"对变量为null的情况在页面显示为空白字符串
用双引号还是单引号表示,默认“双引号,可以在stringliterals.interpolate=false改变默认处理方式

一个简单示例如下,

## 1、变量引用
$name
## 2、语句/指令-变量赋值
#($name="test")
#set($value= 123)
## 3、#include和#parse的作用都是引入本地文件。#include引入的文件内容不会被velocity模板引擎解析。#parse引入的文件内容,将解析其中的velocity并交给模板,相当于把引入的文件内容copy到文件中。
#parse ( "/template/includes/actionerrors.vm" )
#include ( "/template/includes/actionerrors.vm" )

更多语法可参考:http://velocity.apache.org/engine/1.7/user-guide.html

3、漏洞原理分析

confluence处理velocity模板,将velocity语法转为字符串输出到页面,其中涉及到的一些表达式计算会调用ognl.getValue()处理。

confluence在处理vm文件时,首先将vm内容转为AST语法树,然后分别处理每一个节点的内容,将每个节点的内容拼接输出。

Confluence的Velocity模板引擎处理vm文件流程主要在com.opensymphony.webwork.dispatcher.VelocityResult.doExecute(),首先获取OgnlValueStack、context上下文、getTemplate获取vm文件,接下来用merge处理合并页面结果,将结果输出给writer。

0x2:Struts OGNL注入漏洞 

webwork2和现在的Struts2.x中使用OGNL取代原来的EL来做界面数据绑定,所谓界面数据绑定,也就是把界面元素(例如一个textfield,hidden)和对象层某个类的某个属性绑定在一起,修改和显示自动同步。而Struts2框架正是因为滥用OGNL表达式,使之成为了“漏洞之王”。

0x3:Mybatis SQL解析OGNL注入漏洞

Mybatis在动态SQL中,可以解析OGNL表达式,如果我们控制了一个变量,并且该变量可以被解析成OGNL表达式,就能够实现OGNL表达式注入。

参考链接:

https://www.cnpanda.net/sec/1227.html
https://github.com/Mr-xn/Penetration_Testing_POC/blob/master/%E6%B3%9B%E5%BE%AEe-mobile%20ognl%E6%B3%A8%E5%85%A5.md
https://tttang.com/archive/1583/#toc_0x06-s2-045 
https://blog.csdn.net/GalaxySpaceX/article/details/132364381
https://baike.baidu.com/item/confluence/452961
https://blog.csdn.net/Kevin__Durant/article/details/123147336
https://xz.aliyun.com/t/10482#toc-10
https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/
https://static.anquanke.com/download/b/security-geek-2019-q1/article-19.html 

OGNL漏洞的修复基本都是采用黑名单来限制OGNL注入,开发人员在使用ognl时,除了ognl需要注意使用较高版本,还要注意添加额外的防护措施。

当然,使用黑名单的防护方式也许一时可以防住OGNL的RCE,但总有被绕过的风险,另外除了命令执行,文件操作、SSRF也同样存在风险敞口。


文章来源: https://www.cnblogs.com/LittleHann/p/17788847.html
如有侵权请联系:admin#unsafe.sh