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";
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關閉我們的輸出流,這里有三種解決方案:
- 修改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";
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
