简介
分块传输编码(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 += "rn";
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 += "rn";
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 += "rn";
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+工控+样本分析 长期招新
原文始发于微信公众号(ChaMd5安全团队):Chunk-Proxy:仅需一条http请求创建的Socks代理隧道