java實現http/https抓包攔截

最近在調試一個項目時常常需要對接口進行抓包查看,接口位於微信的公衆號內,目前每次調試時都是用的 fiddler進行抓包查看的。但每次打開fiddler去查看對應的接口並找到對應的參數感覺還是有點複雜,正好今天是週末,打算自己來研究下它的原理並自己通過java來寫一個(之所以知道java可以實現這個功能是因爲著名的web安全檢測工具 burpsuite 就是用java寫的)

 

分析

在使用fiddler或burpsuite時其抓包的原理都是通過代理服務器來實現的。fiddler或burpsuite通過自己創建一個代理服務器對需要攔截的socket請求進行一次中轉,其過程有點像中間人的方式,從而可以實現對請求和響應的攔截和修改。

知道了原理後,那麼通過JAVA編寫一個用於轉發socket的程序就可以實現請求的攔截了.

爲了開發的方便與高效,這裏採用netty框架來顯示代理服務器的開發

本文需要的依賴包爲:

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
   <groupId>io.netty</groupId>
   <artifactId>netty-all</artifactId>
   <version>4.1.42.Final</version>
</dependency>

http請求代理

微信公衆號內的接口目前全都是https的,直接開發https代理程序有一定難度,所以筆者決定在實現https的接口抓包之前還是先來搞定http的抓包攔截功能

在開發之前先梳理下思圖:這裏以谷歌瀏覽器訪問百度網站爲例,先畫下其訪問流程圖

http請求無代理

對於http請求無代理的情況其過程很簡單,客戶端向服務器發起請求,服務端響應此請求即可

http請求有代理

由於http請求太過簡單,其所有的數據傳輸也都是明文傳輸了。其最大的安全性是很容易受到中間人攻擊(MITM)。與MITM類比,那麼此處的http代理服務器也就是中間人了。作爲中間人服務器,它對於客戶端的請求可以進行攔截、查看、過濾、轉發、篡改等,由代理服務器處理完畢後再決定是否轉發給目標服務器。同時對於目標服務器的響應也由中間的代理服務器先進行處理一遍,再決定怎樣傳回給客戶端。

如果用netty來實現http的代理服務器其主要代碼如下:

public class HttpProxyHandler extends ChannelInboundHandlerAdapter implements IProxyHandler {
    private Logger logger = LoggerFactory.getLogger(HttpProxyHandler.class);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.debug("[HttpProxyHandler]");
        if (msg instanceof HttpRequest) {
            HttpRequest httpRequest = (HttpRequest) msg;
            //獲取客戶端請求
            ClientRequest clientRequest = ProxyRequestUtil.getClientRequest(ctx.channel());
            if (clientRequest == null) {
                //從本次請求中獲取
                Attribute<ClientRequest> clientRequestAttribute = ctx.channel().attr(CLIENTREQUEST_ATTRIBUTE_KEY);
                clientRequest = ProxyRequestUtil.getClientReuqest(httpRequest);
                //將clientRequest保存到channel中
                clientRequestAttribute.setIfAbsent(clientRequest);
            }
            //如果是connect代理請求,返回成功以代表代理成功
            if (sendSuccessResponseIfConnectMethod(ctx, httpRequest.method().name())) {
                logger.debug("[HttpProxyHandler][channelRead] sendSuccessResponseConnect");
                ctx.channel().pipeline().remove("httpRequestDecoder");
                ctx.channel().pipeline().remove("httpResponseEncoder");
                ctx.channel().pipeline().remove("httpAggregator");
                ReferenceCountUtil.release(msg);
                return;
            }
            if (clientRequest.isHttps()) {
                //https請求不在此處轉發
                super.channelRead(ctx, msg);
                return;
            }
            sendToServer(clientRequest, ctx, msg);
            return;
        }
        super.channelRead(ctx, msg);
    }

    /**
     * 如果是connect請求的話,返回連接建立成功
     *
     * @param ctx        ChannelHandlerContext
     * @param methodName 請求類型名
     * @return 是否爲connect請求
     */
    private boolean sendSuccessResponseIfConnectMethod(ChannelHandlerContext ctx, String methodName) {
        if (Constans.CONNECT_METHOD_NAME.equalsIgnoreCase(methodName)) {
            //代理建立成功
            //HTTP代理建立連接
            HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, Constans.CONNECT_SUCCESS);
            ctx.writeAndFlush(response);
            return true;
        }
        return false;
    }


    @Override
    public void sendToServer(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(ctx.channel().eventLoop())
                // 註冊線程池
                .channel(ctx.channel().getClass())
                // 使用NioSocketChannel來作爲連接用的channel類
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        //添加接收遠程server的handler
                        ch.pipeline().addLast(new HttpRequestEncoder());
                        ch.pipeline().addLast(new HttpResponseDecoder());
                        ch.pipeline().addLast(new HttpObjectAggregator(6553600));
                        //代理handler,負責給客戶端響應結果
                        ch.pipeline().addLast(new HttpProxyResponseHandler(ctx.channel()));
                    }
                });

        //連接遠程server
        ChannelFuture cf = bootstrap.connect(clientRequest.getHost(), clientRequest.getPort());
        cf.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    //連接成功
                    future.channel().writeAndFlush(msg);
                    logger.debug("[operationComplete] connect remote server success!");
                } else {
                    //連接失敗
                    logger.error("[operationComplete] 連接遠程server失敗了");
                    ctx.channel().close();
                }
            }
        });
    }

    @Override
    public void sendToClient(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {

    }
}

上面的代碼爲轉發部分的處理代碼,其具體完整實現可以查看文末的github地址

對於http請求響應的處理代碼爲:

public class HttpProxyResponseHandler extends ChannelInboundHandlerAdapter {
    private Logger logger = LoggerFactory.getLogger(HttpProxyResponseHandler.class);
    private Channel clientChannel;

    public HttpProxyResponseHandler(Channel clientChannel) {
        this.clientChannel = clientChannel;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpResponse) {
            FullHttpResponse response = (FullHttpResponse) msg;
            logger.debug("[channelRead][FullHttpResponse] 接收到遠程的數據1 content:{}", response.content().toString(Charset.defaultCharset()));
        } else if (msg instanceof DefaultHttpResponse) {
            DefaultHttpResponse response = (DefaultHttpResponse) msg;
            logger.debug("[channelRead][FullHttpResponse] 接收到遠程的數據 content:{}", response.toString());
        } else if (msg instanceof DefaultHttpContent) {
            DefaultHttpContent httpContent = (DefaultHttpContent) msg;
            logger.debug("[channelRead][DefaultHttpContent] 接收到遠程的數據 content:{}", httpContent.content().toString(Charset.defaultCharset()));
        } else {
            logger.debug("[channelRead] 接收到遠程的數據 " + msg.toString());
        }
        //發送給客戶端
        clientChannel.writeAndFlush(msg);
    }
}

https請求攔截

https的請求相對於http的請求流程稍微複雜一點,目前的瀏覽器主要採用tls1.2版本和tls1.3版本,在開發https的代理之前,先看一下https採用tls1.2的握手過程是怎麼樣的

https tls1.2無代理

其過程可以通過wireshark抓包進行分析

通過tls and ip.addr=[目錄ip]對https通信過程中的數據進行過濾

以下爲我分析的https中使用的tls1.2版本客戶端與服務端的握手過程簡要分析,其中參數了一些大牛的文章

這其中的知識點比較多,如感興趣可以仔細看下上面我在梳理過程中所畫的時序圖,如想深入研究可以進入上方的鏈接進行深入學習。

我的理解後,其主要就是這樣的:CA保證了通信雙方的身份的真實性,基於公私鑰交換確保了通信過程中的安全性

對https請求進行代理分析

回到本文主題,那麼想要對https請求進行代理應該如何實現呢?

在瞭解了https的通信過程後,那麼我們有兩種辦法可以對https的請求進行代理:

  1. 獲取到所要代理網站https證書頒發機構的私鑰,也就是ca根證書的私鑰,然後自己再重新頒發一個新的證書返回給被代理的客戶端
  2. 自己生成一個ca證書,然後導入到將要被代理的客戶端中,讓其信任,隨後再針對將要代理的請求動態生成https證書

通過分析後我們可以知道,想要獲取到ca根證書的私鑰是不太可能的,據說ca根證書都是離線存儲的,一般人拿不到的(一個https證書一年收費上千塊不是開玩笑的),ca的代理機構的證書也是這個道理。

那麼通過上面的再次分析後通過方案1來進行請求代理的可行性還高一些,其代理過程可以簡單如下圖:

在分析過後並自己畫一個流程圖後對於https的代理實現流程清晰多了,其實目前市面上的許多支持https的代理軟件都是採用的這種方式來實現的,無論是常見的抓包利器fidder還是大名鼎鼎的安全測試工具BurpSuite都是基於此種方式來做的實現

https代理基於netty的實現

在有了上面的分析後,其實想要自己去實現一個https的代理服務器還是有一定難度的,https握手的細節實現就足以讓人費事費力了。但在同樣大名鼎鼎的netty框架面前這些都是小事兒!netty中的SslContext類幫我們完成了這些細節的實現,我們只管如何調用它遍可完成對https的握手了,框架就是框架,強大哇!

由於時間關係,對於其實現的具體代碼這裏不做詳細分析了,我已把代碼提交到github上了.

開源項目easyHttpProxy

其具體的實現可以參考源碼:https://github.com/puhaiyang/easyHttpProxy

爲了使用的方便,我也將此項目上傳到了maven公網,其maven爲:

<dependency>
  <groupId>com.github.puhaiyang</groupId>
  <artifactId>easy-http-proxy</artifactId>
  <version>0.0.1</version>
</dependency>

使用時添加依賴包後,調用

EasyHttpProxyServer.getInstace().start(6667);

即可,其中6667爲代理服務器監聽的端口號,目前已支持了http/https並針對其他請求直接進行轉發.

如果不想自己生成證書,記得將jar包中的ca.crt、ca.key、ca_private.der拷貝的項目的運行根目錄下,即classes path下,要不然https代理時會找不到ca根證書會出錯。

同時,記得將ca.crt導入到根證書

具體步驟可見此截圖:

歡迎留言評論,共同學習共同進步!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章