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

百家 作者:Chamd5安全团队 2022-05-10 21:47:51

简介

分块传输编码(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\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关闭我们的输出流,这里有三种解决方案:

  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\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/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接