Chunk-Proxy:仅需一条http请求创建的Socks代理隧道
简介
分块传输编码(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个子类:
BufferedInputFilter 过滤器 负责读取和缓冲请求Body的 VoidInputFilter 空的输入过滤器,比如Body没有数据或者是请求方法是GET都是这个过滤器 读取返回空 IdentityInputFilter 过滤器 在请求包含content-length协议头并且指定的长度大于0时使用 ChunkedInputFilter 过滤器 Http Chunk请求会走这个过滤器读取 只要客户端有发数据,就可以一直读取 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\n";
????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\n";
????????????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关闭我们的输出流,这里有三种解决方案:
修改JDK源码(太费事了) 通过JavaAgent动态修补类(也太废事了) 反射修改类 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\n";
????????????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

end
招新小广告
ChaMd5?Venom?招收大佬入圈
新成立组IOT+工控+样本分析?长期招新
欢迎联系admin@chamd5.org

关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
关注网络尖刀微信公众号随时掌握互联网精彩
- 1 习近平听取李家超述职报告 7904680
- 2 明年生娃将实现“不花钱” 7809181
- 3 教育部:普通高中严格控制考试次数 7712781
- 4 回顾山东舰硬核名场面 7618537
- 5 健美冠军王昆去世 曾获职业赛8连冠 7520987
- 6 网红柴犬佩奇中毒死亡 7429354
- 7 收入分配制度或迎重大改革 7332551
- 8 小学一二年级不进行纸笔考试 7238533
- 9 福建舰入列后首次通过台湾海峡 7143406
- 10 感染甲流后该如何科学调养 7044482

Chamd5安全团队
