主要有3个部分组成:
1、java的反省机制
2、java的序列化处理
3、java的远程代码执行
java的反射与代码执行
我们先看个简单的例子,使用java调用计算器程序:
import java.io.ioexception;import java.lang.runtime;public class test { public static void main(string[] args) { runtime env = runtime.getruntime(); string cmd = "calc.exe"; try { env.exec(cmd); } catch (ioexception e) { e.printstacktrace(); } }}
我们从java.lang包中导入runtime类,之后调用其getruntime方法得到1个runtime对象,该对象可以用于jvm虚拟机运行状态的处理。接着我们调用其exec方法,传入1个字符串作为参数。
此时,将启动本地计算机上的计算器程序。
下面我们通过java的反省机制对上述的代码进行重写。通过java的反省机制可以动态的调用代码,而逃过一些服务端黑名单的处理:
import java.lang.reflect.invocationtargetexception;import java.lang.reflect.method;public class test { public static void main(string[] args) { try { class<?> cls = class.forname("java.lang.runtime"); string cmd = "calc.exe"; try { method getruntime = cls.getmethod("getruntime", new class[] {}); object runtime = getruntime.invoke(null); method exec = cls.getmethod("exec", string.class); exec.invoke(runtime, cmd); } catch (nosuchmethodexception e) { e.printstacktrace(); } catch (securityexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } catch (illegalargumentexception e) { e.printstacktrace(); } catch (invocationtargetexception e) { e.printstacktrace(); } } catch (classnotfoundexception e1) { e1.printstacktrace(); } }}
上述代码看起来很繁琐,实际上并不是很难。首先,通过class.forname传入1个字符串作为参数,其返回1个class的实例。而其作用是根据对应的名称找到对应的类。
接着我们使用class实例的getmethod方法获取对应类的getruntime方法,由于该类没有参数,因此可以将其设置为null或使用匿名类来处理。
method getruntime = cls.getmethod("getruntime", new class[] {});
之后通过得到的方法的实例的invoke方法调用对应的类方法,由于没有参数则传入null即可。同理,我们再获取到exec方法。
java序列化处理
对于java中的序列化处理,对应的类需要实现serializable接口,例如:
import java.io.serializable;import java.io.objectinputstream;import java.io.objectoutputstream;import java.io.bytearrayinputstream;import java.io.bytearrayoutputstream;import java.io.ioexception;public class reader implements serializable { private static final long serialversionuid = 10l; private void readobject(objectinputstream stream) { system.out.println("foo...bar..."); } public static byte[] serialize(object obj) { //序列化对象 bytearrayoutputstream out = new bytearrayoutputstream(); objectoutputstream output = null; try { output = new objectoutputstream(out); output.writeobject(obj); output.flush(); output.close(); } catch (ioexception e) { e.printstacktrace(); } return out.tobytearray(); } public static object deserialize(byte[] bytes) { //反序列化处理 bytearrayinputstream in = new bytearrayinputstream(bytes); objectinputstream input; object obj = null; try { input = new objectinputstream(in); obj = input.readobject(); } catch (ioexception e) { e.printstacktrace(); } catch (classnotfoundexception e) { e.printstacktrace(); } return obj; } public static void main(string[] args) { byte[] data = serialize(new reader()); //对类自身进行序列化 object response = deserialize(data); system.out.println(response); }}
在这里我们重写了该类的readobject方法,用于读取对象用于测试。其中比较重要的2个函数是serialize和deserialize,分别用于序列化和反序列化处理。
其中,serialize方法需要传入1个对象作为参数,其输出结果为1个字节数组。在该类中,其中的对象输出流objectoutputstream主要用于bytearrayoutputstream进行包装,之后使用其writeobject方法将对象写入进去,最后我们通过bytearrayoutputstream实例的tobytearray方法得到字节数组。
而在deserialize方法中,需要传入1个字节数组,而返回值为1个object对象。与之前的序列化serialize函数类似,此时我们使用bytearrayinputstream接收字节数组,之后使用objectinputstream对bytearrayinputstream进行包装,接着调用其readobject方法得到1个object对象,并将其返回。
当我们运行该类时,将得到如下的结果:
java远程通信与传输
为了实现java代码的远程传输及远程代码执行,我们可以借助rmi、rpc等方式。而在这里我们使用socket进行服务端及客户端处理。
首先是服务器端,监听本地的8888端口,其代码为:
import java.net.socket;import java.io.ioexception;import java.io.inputstream;import java.net.serversocket;public class server { public static void main(string[] args) throws classnotfoundexception { int port = 8888; try { serversocket server = new serversocket(port); system.out.println("server is waiting for connect"); socket socket = server.accept(); inputstream input = socket.getinputstream(); byte[] bytes = new byte[1024]; int length = 0; while((length=input.read(bytes))!=-1) { string out = new string(bytes, 0, length, "utf-8"); system.out.println(out); } input.close(); socket.close(); server.close(); } catch (ioexception e) { e.printstacktrace(); } }}
我们通过传入1个端口来实例化serversocket类,此时得到1个服务器的socket,之后调用其accept方法接收客户端的请求。此时,得到了1个socket对象,而通过socket对象的getinputstream方法获取输入流,并指定1个长度为1024的字节数组。
接着调用socket的read方法读取那么指定长度的字节序列,之后通过string构造器将字节数组转换为字符串并输出。这样我们就得到了客户端传输的内容。
而对于客户端器,其代码类似如下:
import java.io.ioexception;import java.net.socket;import java.io.outputstream;public class client { public static void main(string[] args) { string host = "192.168.1.108"; int port = 8888; try { socket socket = new socket(host, port); outputstream output = socket.getoutputstream(); string message = "hello,java socket server"; output.write(message.getbytes("utf-8")); output.close(); socket.close(); } catch (ioexception e) { e.printstacktrace(); } }}
在客户端,我们通过socket对象传递要连接的ip地址和端口,之后通过socket对象的getoutputstream方法获取到输出流,用于往服务器端发送输出。由于这里只是演示,使用的是本地的主机ip。而在实际应用中,如果我们知道某个外网主机的ip及开放的端口,如果当前主机存在对应的漏洞,也是可以利用类似的方式来实现的。
这里我们设置要传输的内容为utf-8编码的字符串,俄日在输出流的write方法中通过字符串的getbytes指定其编码,从而将其转换为对应的字节数组进行发送。
正常情况下,我们运行服务器后再运行客户端,在服务器端可以得到如下输出:
server is waiting for connecthello,java socket server
java反序列化与远程代码执行
下面我们通过java反序列化的问题来实现远程代码执行,为了实现远程代码执行,我们首先在reader类中添加1个malicious方法,其代码为:
public object malicious() throws ioexception { runtime.getruntime().exec("calc.exe"); system.out.println("hacked the server..."); return this; }
在该方法中我们使用之前的介绍调用宿主机器上的计算器程序,然后输出1个相关信息,最后返回当前类。
之后是对服务器端的代码进行如下的修改:
while((length=input.read(bytes))!=-1) { reader obj = (reader) reader.deserialize(bytes); obj.malicious();}
我们在接收到客户端对应的字符串后对其进行反序列处理,之后调用某个指定的函数,从而实现远程代码的执行。而在客户端,我们需要对其进行序列化处理:
reader reader = new reader();byte[] bytes = reader.serialize(reader);string message = new string(bytes);output.write(message.getbytes());
下面我们在宿主机器上运行服务器端程序,之后在本地机器上运行客户端程序,当客户端程序执行时,可以看到类似如下的结果:
可以看到,我们成功的在宿主机器上执行了对应的命令执行。
总结
为了实现通过java的反序列问题来实现远程代码执行的漏洞,我们需要编写1个有恶意代码注入的序列化类。之后在客户端将恶意代码序列化后发送给服务器端,而服务器端需要调用我们期望的方法,从而触发远程代码执行。
为了避免服务器端进行一些安全处理,我们可以采用反射的方式来逃逸其处理。
这里只是1个简化的过程,更加实用的过程可以参考apache common collections的问题导致的weblogic漏洞cve-2015-4852及jboss的漏洞cve-2015-7501。
推荐相关文章教程:web安全教程
以上就是java反序列化引发的远程代码执行漏洞原理分析的详细内容。