当前位置:网站首页 > 网络安全培训 > 正文

Chunk-Proxy:仅需一条http请求创建的Socks代理隧道

freebuffreebuf 2022-05-11 386 0

本文来源:

简介

分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许 HTTP 由应用服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在 HTTP 协议 1.1 版本(HTTP/1.1)中提供。

通常,HTTP 应答消息中发送的数据是整个发送的,Content-Length 消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。

通过 Request获得Socket

在2020年看先知帖子搞反序列化回显的时候,发现可以通过request对象获取到真实的Socket套接字流,获得真实套接字流之后可以直接做Socks代理。

测试代码

主要是从request.getInputStream() 获取输入流,然后读取到buf。

获取Socket的真实输入流与输出流

断点下到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,那有没有更加通用的方法呢?请看下面的内容。

通用HTTP Chunk Socks代理

我们继续查看inputstream.read的调用堆栈 发现是ChunkedInputFilter类调用的SocketInputSteam类的read方法

我们再来看一下ChunkedInputFilter类,看看它实现了哪些接口。

发现它实现了InputFilter接口,我们发现它一共有5个子类:

  1. BufferedInputFilter 过滤器 负责读取和缓冲请求Body的
  2. VoidInputFilter 空的输入过滤器,比如Body没有数据或者是请求方法是GET都是这个过滤器 读取返回空
  3. IdentityInputFilter 过滤器 在请求包含content-length协议头并且指定的长度大于0时使用
  4. ChunkedInputFilter 过滤器 Http Chunk请求会走这个过滤器读取 只要客户端有发数据,就可以一直读取
  5. ChunkedInputFilter 过滤器 负责在FORM认证后恢复保存的请求时重放请求的正文

从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关闭我们的输出流,这里有三种解决方案:

  1. 修改JDK源码(太费事了)
  2. 通过JavaAgent动态修补类(也太废事了)
  3. 反射修改类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代理了。

通过Http Chunk编写的Socks代理(仅需一条Http请求)

在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 

转载请注明来自网盾网络安全培训,本文标题:《Chunk-Proxy:仅需一条http请求创建的Socks代理隧道》

标签:sockethttp代理stringsocks代理http请求

关于我

欢迎关注微信公众号

关于我们

网络安全培训,黑客培训,渗透培训,ctf,攻防

标签列表