OGNL (Object Graph Navigation Language) 是一个开源的表达式引擎。通过使用OGNL,我们能够通过表达式存取Java对象树中的任意属性和调用Java对象树的方法等。也就是说,如果我们把表达式看成是一个带有语义的字符串,那么OGNL就是这个语义字符串与Java对象之间沟通的催化剂,通过OGNL,我们可以轻松解决在数据流转的过程中所碰到的各种问题。
/**
* 通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值
*/
public static Object getValue(String expression, Map context, Object root) throws OgnlException {
return getValue(expression, context, root, null);
}
/**
* 通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值
*/
public static void setValue(String expression, Map context, Object root, Object value) throws OgnlException {
setValue(parseExpression(expression), context, root, value);
}
OGNL的API其实相当简单,上面的2个方法,分别针对对象的“取值”和“写值”操作。因而,OGNL的基本操作实际上是通过传入上述这2个方法的三个参数来实现的。OGNL同时编写了许多其他的方法来实现相同的功能,上述的2个接口只是其中最简单并最具代表性的2个方法。读者可以通过阅读Ognl.java来获取更多的信息。
<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.21</version>
</dependency>
public class Main {
public static void main(String[] args) throws OgnlException {
User user = new User();
user.setId(1);
user.setName("downpour");
// 创建上下文环境
Map context = new HashMap();
context.put("introduction", "My name is ");
// 测试从Root对象中进行表达式计算并获取结果
Object name = Ognl.getValue(Ognl.parseExpression("name"), user);
System.out.println(name.toString());
// 测试从上下文环境中进行表达式计算并获取结果
Object contextValue = Ognl.getValue(Ognl.parseExpression("#introduction"), context, user);
System.out.println(contextValue);
// 测试同时从将Root对象和上下文环境作为表达式的一部分进行计算
Object hello = Ognl.getValue(Ognl.parseExpression("#introduction + name"), context, user);
System.out.println(hello);
// 对Root对象进行写值操作
Ognl.setValue("group.name", user, "dev");
Ognl.setValue("age", user, "18");
System.out.println(user.getGroup().getName());
}
}
OGNL三要素
表达式(Expression)
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。
OGNL支持大量的表达式语法,不仅支持“链式”描述对象访问路径,还支持在表达式中进行简单的计算,甚至还能够支持复杂的Lambda表达式等。我们可以在接下来的章节中看到各种各样不同的OGNL表达式。
Root对象(Root Object)
OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。
上下文环境(Context)
有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。
不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干。
OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。
OGNL的基本操作
对Root对象(Root Object)的访问
针对OGNL的Root对象的对象树的访问是通过使用“点号”将对象的引用串联起来实现的。通过这种方式,OGNL实际上将一个树形的对象结构转化成了一个链式结构的字符串结构来表达语义。
// 获取Root对象中的name属性的值
name
// 获取Root对象中department属性中的name属性的实际值
department.name
// 获取Root对象中department属性中manager属性中name属性的实际值
department.manager.name
对上下文环境(Context)的访问
由于OGNL的上下文是一个Map结构,在OGNL进行计算时可以事先在上下文环境中设置一些参数,并让OGNL将这些参数带入进行计算。有时候也需要对这些上下文环境中的参数进行访问,访问这些参数时,需要通过#符号加上链式表达式来进行,从而表示与访问Root对象(Root Object)的区别。
// 获取OGNL上下文环境中名为introduction的对象的值
#introduction
// 获取OGNL上下文环境中名为parameters的对象中user对象中名为name的属性的值
#parameters.user.name
对静态变量的访问
在OGNL中,对于静态变量或者静态方法的访问,需要通过@[class]@[field / method]的表达式语法来进行。
// 访问com.example.core.Resource 类中名为ENABLE的属性值
@com.example.core.Resource@ENABLE
// 调用com.example.core.Resource 类中名为get的方法@com.example.core.Resource@get()
方法调用
在OGNL中调用方法,可以直接通过类似Java的方法调用方式进行,也就是通过点号加方法名称完成方法调用,甚至可以传递参数。
// 调用Root对象中的group属性中users的size()方法
group.users.size()
// 调用Root对象中的group中的containsUser的方法,并将上下文环境中名为requestUser的值作为参数传入
group.containsUser(#requestUser)
【OGNL带来的潜在问题】
*我们已经能够看到OGNL在语法层面所表现出来的强大之处。然而,越强大的东西,其自身也一定存在着致命的弱点,这也就是所谓的“物极必反”。正是由于OGNL能够支持完整的Java对象创建、读写过程,它就能被作为一个潜在的切入点,成为黑客的攻击目标。
exploit-db网站在2010年的7月14日就爆出了一个Struts2的远程执行任意代码的漏洞。具体的声明链接为:http://www.exploit-db.com/exploits/14360/。
细心的读者会发现,这个漏洞的基本原理实际上就是利用了OGNL可以任意构造对象,并执行对象中方法的特性,构造了一个底层命令调用的Java类,并执行操作系统命令进行系统攻击。
在Struts2.2.X之后的版本中,这个漏洞被修复,其主要的方法也是通过限制参数名称的方式,拒绝类似的代码执行方式。*