服务热线
15527777548/18696195380
发布时间:2022-05-11
简要描述:
简介分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许 HTTP 由应用服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。...
分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许 HTTP 由应用服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在 HTTP 协议 1.1 版本(HTTP/1.1)中提供。
通常,HTTP 应答消息中发送的数据是整个发送的,Content-Length 消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。
在2020年看先知帖子搞反序列化回显的时候,发现可以通过request对象获取到真实的Socket套接字流,获得真实套接字流之后可以直接做Socks代理。
主要是从request.getInputStream() 获取输入流,然后读取到buf。
断点下到Socket的InputStream类 会断到org.apache.coyote.http11.InternalInputBuffer
类的fill方法,这个类是一个输入流的包装类。
其中最主要的就是它的inputStream变量,它是Socket套接字的输入流
通过堆栈回溯我们可以通过request.request.coyoteRequest.inputBuffer.inputStream
获取Socket的输入流,同时可以看到SocketInputStream类里面有一个socket字段存放着这个输入流所属的套接字(Socket)
request.request.coyoteRequest.inputBuffer.inputStream
获取到Socket之后我们就可以直接操作Socket的输入与输出流做一个Socks代理。
代码:
<%@ page import="java.io.InputStream" %> <%@ page import="java.io.OutputStream" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.util.StringTokenizer" %> <%@ page import="java.net.Socket" %> <%! public static Object getFieldValue(Object obj,String fieldName){ if (obj!=null){ Class clazz = obj.getClass(); while (clazz!=null){ try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return field.get(obj); } catch (Exception e) { clazz = clazz.getSuperclass(); } } } return null; } public static Object getFieldValueEx(Object obj,String fieldName){ StringTokenizer stringTokenizer = new StringTokenizer(fieldName,"->"); while (stringTokenizer.hasMoreTokens()){ String realFieldName = stringTokenizer.nextToken(); obj = getFieldValue(obj,realFieldName); } return obj; } %> <% Socket socket = (Socket) getFieldValueEx(request,"request->coyoteRequest->inputBuffer->inputStream->socket"); socket.getOutputStream().write("hacker".getBytes()); socket.getOutputStream().flush(); socket.close(); System.out.println(socket); %>
查看流量,我们成功劫持了Socket,并输出了我们想要的内容。
现在我们已经控制了Socket可以用来做Socks代理了,不过这种方法只适用于Tomcat,那有没有更加通用的方法呢?请看下面的内容。
我们继续查看inputstream.read的调用堆栈 发现是ChunkedInputFilter类调用的SocketInputSteam类的read方法
我们再来看一下ChunkedInputFilter类,看看它实现了哪些接口。
发现它实现了InputFilter接口,我们发现它一共有5个子类:
从InputFilter接口的实现类来看,如果要实现一个Socks代理,ChunkedInputFilter是我们唯一的选择。
如何让我们的请求走到ChunkedInputFilter呢?只要添加一个Transfer-Encoding协议头并且值为chunked即可。
接下来我们写一个测试代码,看看能不能行得通,看一下能否同时读取并写出数据呢?
下面是一个例子,服务端写出服务端的时间并读取输出客户端发送的时间,客户端写出客户端的时间并读取输出服务端发送的时间。
server jsp
<%@ page import="java.io.InputStream" %> <%@ page import="java.io.OutputStream" %> <%@ page import="java.text.DateFormat" %> <%@ page import="java.util.Locale" %> <%@ page import="java.util.Date" %> <%@ page import="java.util.Arrays" %> <% InputStream inputStream = request.getInputStream(); response.setHeader("Transfer-Encoding","chunked");//设置响应也是HTTP CHUNK response.setBufferSize(1024); OutputStream outputStream = response.getOutputStream(); byte[] buf = new byte[1024]; for (int i = 0; i < 10; i++) { //通过chunk 写出当前的时间 String currentTime = DateFormat.getTimeInstance( DateFormat.FULL, Locale.getDefault()).format(new Date()); currentTime += "\r"; outputStream.write(currentTime.getBytes()); outputStream.flush(); //读取客户端发来的时间并输出 int read = inputStream.read(buf); System.out.println("server read " + new String(Arrays.copyOf(buf,read))); Thread.sleep(1000); } outputStream.close(); %>
client
import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.text.DateFormat; import java.util.Arrays; import java.util.Date; import java.util.Locale; public class Main { public static void main(String[] args) throws Throwable { //创建HTTP连接 URL url = new URL("http://localhost:8080/chunk/index.jsp"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); //设置请求方法为POST httpURLConnection.setRequestMethod("POST"); //允许写出数据 httpURLConnection.setDoOutput(true); //允许读取数据 httpURLConnection.setDoInput(true); //设置请求body发送方式为chunk httpURLConnection.setRequestProperty("Transfer-Encoding","chunked"); //设置请求body为二进制流 httpURLConnection.setRequestProperty("Content-Type", "application/octet-stream"); //设置Chunk的块大小 httpURLConnection.setChunkedStreamingMode(1024); //发送连接 httpURLConnection.connect(); //获取写到服务端的输出流 我们设置了chunk就可以一直向服务端写数据 OutputStream outputStream = httpURLConnection.getOutputStream(); //获取服务器发送来的数据 服务端设置了chunk就可以一直读 直到服务端关闭输出流 InputStream inputStream = httpURLConnection.getInputStream(); byte[] buf = new byte[1024]; for (int i = 0; i < 10; i++) { //通过chunk 写出当前的时间 String currentTime = DateFormat.getTimeInstance( DateFormat.FULL, Locale.getDefault()).format(new Date()); currentTime += "\r"; outputStream.write(currentTime.getBytes()); outputStream.flush(); //读取服务端发来的时间并输出 int read = inputStream.read(buf); System.out.println("client read " + new String(Arrays.copyOf(buf,read))); Thread.sleep(1000); } } }
运行后发现客户端报错了,异常消息说输出流已经被关闭了,但是我们的代码并没有关闭输出流。
Exception in thread "main" java.io.IOException: Stream is closed at sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream.checkError(HttpURLConnection.java:3591) at sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream.write(HttpURLConnection.java:3580) at sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream.write(HttpURLConnection.java:3575) at Main.main(Main.java:39)
我们在类sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream
的close方法下一个断点,看看是谁关闭了我们的输出流。
我们发现在我们调用HttpURLConnection类的getInputStream方法时,在getInputStream0方法会关闭我们打开的输出流,我们要想办法绕过去不让JDK关闭我们的输出流,这里有三种解决方案:
sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream
的closed字段设置为flase获得输入流之后再设置成true综上所述1和2方法过于繁琐,所以我们直接采用第三种方法反射修改closed字段的值(在调用类sun.net.www.protocol.http.HttpURLConnection$StreamingOutputStream
的close方法时,方法会先检查是否已经关闭如果已经关闭就直接返回)
修改后的代码
import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; import java.text.DateFormat; import java.util.Arrays; import java.util.Date; import java.util.Locale; public class Main { public static void main(String[] args) throws Throwable { //创建HTTP连接 URL url = new URL("http://localhost:8080/chunk/index.jsp"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); //设置请求方法为POST httpURLConnection.setRequestMethod("POST"); //允许写出数据 httpURLConnection.setDoOutput(true); //允许读取数据 httpURLConnection.setDoInput(true); //设置请求body发送方式为chunk httpURLConnection.setRequestProperty("Transfer-Encoding","chunked"); //设置请求body为二进制流 httpURLConnection.setRequestProperty("Content-Type", "application/octet-stream"); //设置Chunk的块大小 httpURLConnection.setChunkedStreamingMode(1024); //发送连接 httpURLConnection.connect(); //获取写到服务端的输出流 我们设置了chunk就可以一直向服务端写数据 OutputStream outputStream = httpURLConnection.getOutputStream(); //设置输出流的状态为关闭 Field closedField = outputStream.getClass().getDeclaredField("closed"); closedField.setAccessible(true); closedField.set(outputStream,true); //获取服务器发送来的数据 服务端设置了chunk就可以一直读 直到服务端关闭输出流 InputStream inputStream = httpURLConnection.getInputStream(); //设置输出流的状态为开启 closedField.set(outputStream,false); byte[] buf = new byte[1024]; for (int i = 0; i < 10; i++) { //通过chunk 写出当前的时间 String currentTime = DateFormat.getTimeInstance( DateFormat.FULL, Locale.getDefault()).format(new Date()); currentTime += "\r"; outputStream.write(currentTime.getBytes()); outputStream.flush(); //读取服务端发来的时间并输出 int read = inputStream.read(buf); System.out.println("client read " + new String(Arrays.copyOf(buf,read),"gbk")); Thread.sleep(1000); } } }
我们可以看到服务端和客户端都是双工流输出(同时读取并且输出)
客户端成功读取服务端每隔一秒发送的时间
服务端成功读取客户端每隔一秒发送的时间
我们来看一下流量 从流量中也可以看出来 不论是服务端还是客户端都在读取的同时也在发送,真正的全双工流(红色是我们发送给服务端的,蓝色是服务端发送给我们的),有了全双工流我们就可以做Socks代理了。
在tomcat6-10、weblogic、jetty、树脂、iis上均已经过测试。这里已经写好并开源了,大家下载下来就可以使用了。欢迎Star下~
Github https://github.com/BeichenDream/Chunk-Proxy/releases/tag/jar-v1.10
usage: java -jar chunk-Proxy.jar type listenPort targetUrl type: .net|java example: java -jar chunk-Proxy.jar java 1088 http://10.10.10.1:8080/proxy.jsp
上一篇:人脸识别技术及风险研究
如果您有任何问题,请跟我们联系!
联系我们