Struts 2 的学习记录就不放了,太杂乱了,而且内容很多 一时半会儿也整理不出来,也学不完,这里放一个 OGNL 的知识点总结吧,里面也包含了一些 Struts 2 的坑,比如 IDEA 怎么起一个 Struts 2 项目,后续会补充相关的漏洞 。

环境搭建

这里一点失败总结,很多问题实际上可能是 IDEA 环境配置出的问题,这里会先介绍一下我决定以后采用的方法,也是我尝试的无数种构建项目的方法里唯一好用的方法。

首先 安装 Struts 2 插件。

image.png

新建一个 Java EE 项目,选用我们的插件,这里选用第一个,我们自己来创建外部库。

image.png

选中这里的这些包,包的使用也是有讲究的,这里的包一定也是踩了无数的坑踩出来的,我在使用 pom.xml 的时候也是不断地报错,这里借鉴了一个 师傅的项目 ,因为后续的漏洞复现也是要频繁的变更依赖包版本的,所以这里的这种方式实际上也是很棒的。

image.png

确定,下一步,如果需要的话更改目录与名字,完成即可,接下来进入模块设置内,快捷键(Ctrl+Shift+Alt+S),或者右键找

image.png

进入 工件 内,双击这里我们一开始创建的项目库

image.png

image.png

此时我们的依赖包就会进入到 WEB-INF 下的 lib 文件夹下了。

前置知识

对象导航图语言(Object Graph Navigation Language),简称OGNL,是应用于Java中的一个开源的表达式语言(Expression Language),它被集成在Struts2等框架中,作用是对数据进行访问,它拥有类型转换、访问对象方法、操作集合对象等功能。

从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 ,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,java 中很多可以做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个“路径”来完成对象信息的导航,这个“路径”可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

OGNL 具有以下特点。

  • 支持 对象方法调用。如 objName.methodName()。
  • 支持 类静态方法调用和值访问,表达式的格式为 @[类全名(包括包路径)]@[方法名|值名]。如 @java.lang.String@format('fruit%s','frt')
  • 支持 赋值操作和表达式串联。如 price=100,discount=0.8,在方法 calculatePrice() 中进行乘法计算会返回 80。
  • 访问 OGNL 上下文(OGNL context)和 ActionContext
  • 操作集合对象。

Struts 中 OGNL 的三要素

  • 表达式(Expression)

    表达式(Expression)是整个 OGNL 的 核心内容,所有的 OGNL 操作都是针对表达式解析后进行的。通过表达式来告诉 OGNL 操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL 表达式支持大量的表达式,如“链式访问对象”、表达式计算、甚至还支持 Lambda 表达式。

  • Root 对象

    OGNL 的 Root 对象 可以理解为 OGNL 的 操作对象

    当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。

    OGNL 可以对根对象进行取值或写值等操作,表达式规定了“做什么”,而 根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境。

  • ActionContext

    有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。

    在 Struts2 中,OGNL 上下文即为 ActionContext ,而实际上存放内容是其中的 context,ActionContext 中的 get()/put() 方法实际上都在操作 ActionContext 中的 contex,我们在使用 debug 标签的时候就可以看到 。

    context 对象是一个 Map 类型的对象,在表达式中访问 context 中的对象,需要使用 # 号加对象名称,即“# 对象名称”的形式。例如要获取 context 对象中 user 对象的 username 值,可以如下书写:#user.username

值栈

OGNL 中的根对象即为 ValueStack(值栈),这个对象贯穿整个 Action 的生命周期(每个 Action 类的对象实例都会拥有一个 ValueStack 对象)。

当Struts 2接收到一个 .action 的请求后,会先建立Action 类的对象实例,但并不会调用 Action 方法,而是先将 Action 类的相应属性放到 ValueStack 的实现类 OgnlValueStack 对象 root 对象的顶层节点( ValueStack 对象相当于一个栈)。在处理完上述工作后,Struts2 就会调用拦截器链中的 拦截器,这些拦截器会根据用户请求参数值去更新 ValueStack 对象顶层节点的相应属性的值,最后会传到 Action 对象,并将 ValueStack 对象中的属性值,赋给 Action 类的相应属性。当调用完所有的拦截器后,才会调用 Action 类的 Action 方法。ValueStack 会在请求开始时被创建,请求结束时消亡。

基本语法

#,%$符号

这三种符号在表达式注入中都有用到,由于被广泛应用于 EL 中,所以本小节将重点介绍%#的用法。

# 符号

在 Struts2 框架中,#符号有三种用途,分别如下:

  • 访问非根对象的属性

    如访问 OGNL 上下文和 Action 上下文。由于 Struts2 中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀。

    #相当于 ActionContext.getContext()。例如 #session.user 表达式相当于 ActionContext.getContext().getSession().getAttribute("user")request.userName 表达式相当于 request.getAttribute("userName")

  • 用于过滤和投影集合

    books.{?#this.price>25}。

  • 构造 Map

    #{key1:value1,key2:value2},这种方式常用于给 radio 或 select、checkbox 等标签赋值。如果要在页面中取一个 Map 的值可以如下书写:<s:property value="myMap['key']"/>

% 符号

% 是在标签的属性值被理解为 字符串类型 时,告诉执行环境 %{} 中的是 OGNL 表达式,并计算 OGNL 表达式的值。

符号

$ 符号主要用于在 Struts2 配置文件中引入 OGNL 表达式。

例如:

<action name="userAction_*" class="userAction" method="{1}">
<result name="add" type="redirect">
userAction_findById?userId=${User.userId}
</result>
</action>

具体的用法我们来看下面的示例。

OGNL Test

这里也有大坑,网上的资料都太老了,甚至连 property 标签内的 var 是旧版本的我们都要找好一会儿,在找到之前只能对着报错的 var 和全都是用 var 的教程发呆 …

这里直接贴出我的测试代码,然后再在下面写一下学习到的知识。

OgnlAction.class

package test;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import test.User;

public class OgnlAction extends ActionSupport{
private static final long serialVersionUID = 1L;
private List<User> users;
private static String str = "ognl";

public String execute(){
//获取web元素,以便访问Servlet API
ActionContext context = ActionContext.getContext();
//获取request对象
@SuppressWarnings("unchecked")
Map<String, String> request = (Map<String, String>) context.get("request");
//获取session对象
Map<String, Object> session = context.getSession();
//获取Application对象
Map<String, Object> application = context.getApplication();
//分别在三个域对象中设置一个值
request.put("msg", "我是request元素");
session.put("msg", "我是session元素");
application.put("msg", "我是application元素");

//创建两个User对象
users = new ArrayList<User>();
User u1 = new User();
u1.setName("zhaoyun");
u1.setAge(23);
User u2 = new User();
u2.setName("zhangfei");
u2.setAge(26);
//添加到集合中
users.add(u1);
users.add(u2);

return SUCCESS;
}

public List<User> getUsers() {
return users;
}

public void setUsers(List<User> users) {
this.users = users;
}

}

User.class

package test;

import com.sun.xml.internal.ws.wsdl.writer.document.http.Address;

public class User {

private Address address;
private String name;
private int age;
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;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}

struts.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">

<struts>
<package name="Hello" extends="struts-default">
<action name="ognl" class="test.OgnlAction" method="execute">
<result>
ognl.action
</result>
</action>
</package>
</struts>

这里并不能加反斜杠,仔细看,我们这里使用的是 ognl.action 而不是指向的 jsp 界面,这种用法也挺少见的,看了半天 result 也没有找到有这么用的 。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">

<struts>
<package name="Hello" extends="struts-default">
<action name="ognl" class="test.OgnlAction" method="execute">
<result>
/ognl.jsp
</result>
</action>
</package>
</struts>

通常情况下还是这种写法,两种写法的结果是一样的,这里加不加反斜杠也是有讲究的。

(找测试代码的时候发现基本上都没有加这里的 method="execute" ,垃圾海使人心累)

ognl.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">

<title>OGNL表达式</title>

<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">

</head>

<body>
<p>1、访问web元素中的值</p>
<p>request:<s:property value="#request.msg"/></p>
<p>session:<s:property value="#session.msg"/></p>
<p>application:<s:property value="#application.msg"/></p>
<hr>
<p>2、访问list</p>
<p>
<s:iterator value="users" id="u">
<s:property/><br>
</s:iterator>
</p>
<p>3、通过OGNL表达式直接创建列表并访问</p>
<p>
创建一个列表:{'赵云','刘备','张飞'}<br>
<s:set name="list" value="{'赵云','刘备','张飞'}"></s:set>
<!-- 遍历该创建的列表 -->
<s:iterator value="#list" id="o">
<s:property/><br>
</s:iterator><br>
访问列表的元素个数:<s:property value="#list.size()"/><br>
访问列表的第二个元素:<s:property value="#list[1]"/>
</p>
<p>4、创建一个数组并访问</p>
<p>
<s:set name="array" value="new int[]{1,2,3}"></s:set>
<s:iterator value="#array" id="o">
<s:property/>&nbsp;
</s:iterator>
</p>
<p>5、构造map并访问</p>
<p>
<s:set name="map" value="#{'中国':'北京','美国':'纽约','俄罗斯':'莫斯科' }"></s:set>
<!-- 遍历该map -->
遍历该map(第一种方式):
<s:iterator value="#map" id="m">
<s:property/>&nbsp;
</s:iterator>
<br>
遍历该map(第二种方式):
<s:iterator value="#map" id="m">
<s:property value="key"/>=<s:property value="value"/>&nbsp;
</s:iterator>
<br>
访问第一个元素(不使用%):<s:property value="#map['中国']"/><br>
访问第一个元素(使用%):<s:property value="%{#map['中国']}"/><br>
获取该map所有的key:<s:property value="#map.keys"/><br>
获取该map所有的value:<s:property value="#map.values"/>
</p>
<p>6、访问静态方法和字段</p>
<p>
<code>访问Math类的floor方法(由于Math类是Struts2中默认的类<br>
所以在调用此类方法的时候,其中的class可以不写出)</code><br>
访问静态方法::
<s:property value="@java.lang.Math@floor(32.56)"/>
等于
<s:property value="@@floor(32.56)"/>
<br>
<s:property value="@java.util.Calendar@getInstance()"/><br>
访问静态属性:
<s:property value="@test.OgnlAction@str"/>
</p>
<p>7、访问构造方法</p>
<p>
访问User类的有参构造方法:
<s:property value="new test.User('张角',43).name"/>
</p>
<p>8、if/else判断语句</p>
<p>
<s:if test="'关羽'in {'关羽','赵云','黄忠'}">
关羽在集合中
</s:if>
<s:elseif test="'赵云' in {'赵云','关羽','黄忠'}">
赵云在集合中
</s:elseif>
<s:else>
关羽不在集合中
</s:else>
<br>
<s:if test="#request.msg not in #list">
请求消息不在集合中
</s:if>
</p>
<p>9、迭代标签完整版</p>
<p>
创建一个list列表:abcde<br><s:set name="list2" value="{'a','b','c','d','e'}"></s:set>
<s:iterator value="#list2" id="c" status="s">
索引:<s:property value="#s.getIndex()"/>&nbsp;值:<s:property/>
&nbsp;当前迭代数量:<s:property value="#s.getCount()"/><br>
</s:iterator>
</p>
<p>10、获取属性范围内的值</p>
<p>
<% request.setAttribute("req", "request对象");
request.getSession().setAttribute("ses", "session对象");
request.getSession().getServletContext().setAttribute("app", "application对象");
%>
request:<s:property value="#request.req"/><br>
session:<s:property value="#session.ses"/><br>
application:<s:property value="#application.app"/>

</p>
</body>
</html>

坑在前面也说过了,主要就是 id 标签这一个改变,还有 map 中在 IDEA 里会报一个错,但是我们编译后是可以正常运行的,我这里运行后返回的结果如下:

image.png

image.png

用法介绍

标签

iterator 标签

iterator 标签主要用于对集合中的数据进行迭代,它可以根据条件遍历集合中的数据 。

属性 是否必须 默认值 类型 描 述
begin 0 Integer 迭代数组或集合的起始位置
end 数组或集合的长度大小减 1,若 Step 为负,则为 0。 Integer 迭代数组或集合的结束位置
status false Boolean 迭代过程中的状态
step 1 Integer 指定每一次迭代后索引增加的值
value String 迭代的数组或集合对象
var String 将生成的 Iterator 设置为 page 范围的属性
id String 指定了集合元素的 id,现已用 var 代替

在表 1 中,如果在 <s:iterator> 标签中指定 status 属性,就可以通过该属性获取迭代过程中的状态信息,如元素数、当前索引值等。通过 status 属性获取信息的方法如表 2 所示(假设其属性值为 sp)。

方 法 说 明
sp.count 返回当前已经遍历的集合元素的个数
sp.first 返回当前遍历元素是否为集合的第一个元素
sp.last 返回当前遍历元素是否为集合的最后一个元素
sp.index 返回遍历元素的当前索引值

在我们 S2 系列漏洞的环境下使用的是 id,var 并不存在。

property 标签

property 标签用于获取一个值的属性,如果没有指定,它将默认为在 值栈 的顶部,通常输出的是 value 属性指定的值。

属性 是否必须 描述
value 指定需要输出的属性值,如果没有指定该属性,则默认输出 ValueStack 栈顶的值
id 指定该元素的标识
default 如果要输出的属性值为 null,则显示 default属性的指定值
escape 指定是否忽略 HTML 代码。默认值是 true,即忽略输出值中的 HTML 代码
debug 标签

很酷的标签 <s:debug/> ,会在页面生成一个 debug 链接,展开能看到ValueStack中的内容,该页面有显示用 #key 能获取到Stack Context中的值

image.png

里面一些参数的意义如下:

key 存放内容
com.opensymphony.xwork2.ActionContext.locale LOCALE 常量
struts.actionMapping ActionMapping 引用对象,其中包括name/namespace/method/params/result
com.opensymphony.xwork2.util.ValueStack.ValueStack ValueStack 引用对象
attr 按照 request > session > application 顺序访问 attribute
application
com.opensymphony.xwork2.ActionContext.application
当前应用 ServletContext 中的attribute
request HttpServletRequest中的attribute
com.opensymphony.xwork2.dispatcher.HttpServletRequest request 引用对象
com.opensymphony.xwork2.dispatcher.HttpServletResponse response 引用对象
session/
com.opensymphony.xwork2.ActionContext.session
HttpSession 中的attribute
parameters/
com.opensymphony.xwork2.ActionContext.parameters
请求参数 HashMap
com.opensymphony.xwork2.dispatcher.ServletContext ApplicationContext 对象
com.opensymphony.xwork2.ActionContext.name 当前 action 的 name
set 标签

set 标签用于定义一个变量。通过此标签可以给定义的变量赋值,以及设置变量的作用域(application、request、session)。在默认情况下,通过set标签所定义的变量被放置到值栈中,如下

<s:set name="list" value="{'赵云','刘备','张飞'}"></s:set>

set 标签的属性说明如表所示:

名称 是否必须 类型 说明
scope 可选 String 设置变量的作用域,它的值可以是application、request、session、page或action,默认值为action
value 可选 String 设置变量的值
var 可选 String 定义变量的名称

测试代码分析

1 访问对象中的属性

从头开始看,这里的第一处就是 # 的使用,这里表示的就是访问 Action 上下文,例如 #session.msg 表达式相当于 ActionContext.getContext().getSession().getAttribute("msg")request.msg 表达式相当于 request.getAttribute("msg")

image.png

这里我们在 execute 中对它的值进行了设置。

2 访问 List

这里要访问的是 List 类型的参数 users

image.png

可以看到定义如下:

image.png

这里返回的正是两个 User 对象,这里设置 id 也是有讲究的,翻到了 一篇文章 (找为什么没有 var 的时候翻到的)

不指定 var id 时,会将要遍历的集合中每个元素压入根栈栈顶.

3 创建与访问列表

这里还是遍历并输出 list,只不过这里的 list 是 我们在上面用 set 标签设置的。

image.png

同时可以发现,这里还可以进行一些对我们访问到的对象的操作,比如方法的调用、取值。

4 创建与访问数组

image.png

这里是数组的输出,用了转义字符 &nbsp 的方式来表示空格。

5 构造与访问 map

这里则是构造的 map,有趣的是这里在 IDEA 里实际上是有一个报错

image.png

我们可以使用 #{} 的方式来在 set 中定义数组,: 来表示键值关系

在遍历的时候我们当然还是使用 iterator 标签。

% 符号的用途是在标志的属性为字符串类型时,计算 OGNL 表达式的值,这里在结果上看不出来差异,实际上这个 % 很暴力,我们后面的好多 漏洞的利用都是利用的 ognl 中的 %

可以利用 #map.keys , #map.values 获取 map 的键和值,这里涉及到了 OGNL 中的伪属性。

OGNL针对集合提供了一些伪属性(如size,isEmpty),让我们可以通过属性的方式来调用方法(本质原因在于集合当中的很多方法并不符合JavaBean的命名规则),但我们依然还可以通过调用方法来实现与伪属性相同的目的。

6 访问静态方法和字段

这里是 @ 的使用,也就是 类静态方法调用和值访问 ,格式主要为 @[类全名(包括包路径)]@[方法名|值名]

image.png

还有一个小 tips : 由于Math类是Struts2中默认的类,所以在调用此类方法的时候,其中的class可以不写出

7 访问构造方法 实例化

可以直接访问User类的有参构造方法

image.png

8 if else elseif

image.png

if else elseif 标签的一个应用

9 完整迭代

image.png

详细的迭代,我们使用了 status 属性

10 获取属性范围内的值

# 获取属性范围内的值

image.png

总结

我们可以利用 OGNL 表达式来实现这样一些操作:

  • 访问对象中的属性、调用对象中的方法

  • 获取属性范围内的值

  • 访问、调用 静态的 属性、方法 ,例如 @java.lang.Math@floor(32.56) (这里还有一个小知识点,由于 Math 类是 Struts2 中默认的类,这里我们可以简写为 @@floor(32.56)

  • 创建 java 实例对象

  • 创建 数组、列表、Map ,以及访问其中元素(通过下标的方式 #map['1']

  • 通过 OGNL 提供的 伪属性 来实现方法的调用
    OGNL能够引用集合的一些 特殊的属性,这些属性并不是 JavaBean 模式,例如size()、length()
    当表达式引用这些属性时,OGNL会调用相应的方法,这就是伪属性
    比如获取List的大小:<s:property value="testList.size"/>

    • List 的伪属性:size、isEmpty、iterator
    • Set 的伪属性:size、isEmpty、iterator
    • Map 的伪属性:size、isEmpty、keys、values
    • Iterator 的伪属性:next、hasNext
    • Enumeration 伪属性:next、hasNext、nextElement、hasMoreElements

其他用法

在测试代码之外我们还有很多其他的用法。

创建 map 对象

Map使用特殊的语法来创建 #{'key' : 'value', ……} 要搭配 set 标签

如果想指定创建的Map类型,可以在左花括号前利用 @ 来指定 Map 实现类的类名。示例:#@java.util.LinkedHashMap@{”key”:”value”,….}

过滤(filtering)

首先要明确:无论过滤还是投影都是针对于 数组、集合和 Map 而言的。

过滤指的是将原集合中不符合条件的对象过滤掉,然后将满足条件的对象,构建一个新的集合对象返回,与过滤相关的主要有三个符号 ? , ^ , &

  • ? 用于获得所有符合逻辑的元素,示例: collection.{? expression}
  • ^ 用于获得符合逻辑的第一个元素,示例: collection.{^ expression}
  • $ 用于获得符合逻辑的最后一个元素,示例: collection.{& expression}

同时,在使用过滤操作时,我们通常都会使用 #this,该表达式用于代表 当前正在迭代 iterator 的集合中的对象(联想增强的for循环)

投影(projection)

过滤与投影之间的差别:类比于数据库中的表,过滤是取行的操作,而投影是取列的操作。

投影指的是将 原集合中 所有对象的某个属性 抽取出来,单独构成一个 新的集合对象 返回,示例: collection.{expression}

Lambda 表达式

Lambda 是一个匿名函数,可以把 Lambda表达式 理解为是一段可以传递的代码 (将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升

Lambda 表达式的基础语法 : Java8 中引入了一个新的操作符 “->” 该操作符称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分 :

  • 左侧 : Lambda 表达式的参数列表
  • 右侧 : Lambda 表达式中所需执行的功能, 即 Lambda 体

在 OGNL 表达式中我们也可以使用 Lambda 表达式,语法为 :[...] ,不过 OGNL 中的 Lambda 表达式只能使用一个参数,这个参数通过 #this 引用。

例子:

   #fact= :[ #this<=1 ? 1 : #this* #fact ( #this-1) ], #fact(30)
   #fib= :[#this==0 ? 0 : #this==1 ? 1 : #fib(#this-2)+#fib(#this-1)], #fib(11)
数学表达式

我们可以在其中使用 Java 所支持的数学运算,例如:<s:property value="8+8"/> ,就会输出 16 。

逗号表达式

类似于 C 语言中的都好表达式,求值过程是分别求两个表达式的值,并以表达式2(即最右边的一个表达式)的值作为整个逗号表达式的值。

相关漏洞

后续再进行补充了,要搞一段时间别的东西。