Skip to main content

[mitm] HTTPS中间人劫持(被动扫描入口)

熟悉 xray 的同学可能非常熟悉被动扫描的模式,当我们测试爬虫失效的情况下,经常需要手动设置代理来收集更多的请求(用于纯记录或扫描)。

本功能库包含的内容:

  1. fn mitm.Bridge(var_1: interface {}, var_2: string, vars: ...yaklib.mitmConfigOpt): error 设置一个桥接模式的中间人
  2. fn mitm.Start(var_1: int, vars: ...yaklib.mitmConfigOpt): error 启动一个代理模式中间人
  3. fn mitm.callback(var_1: func(isHttps bool, url string, request *http.Request, response *http.Response)): yaklib.mitmConfigOpt【参数】:设置进入中间人的请求的入口(只劫持不修改)
  4. fn mitm.wscallback(var_1: func(data []byte, isRequest bool): var): yaklib.mitmConfigOpt【参数】:设置进入中间人的websocket请求的入口(通过返回值修改内容
  5. fn wsforcetext(var_1: bool): yaklib.mitmConfigOpt【参数】:设置wscallback中的返回值内容是否强制为文本类型(后文会细讲)
  6. fn mitm.hijack(var_1: func(bool, *http.Request, *http.Response)): yaklib.mitmConfigOpt【暂不开放:参数】:设置进入中间人的请求的入口(劫持)
  7. fn mitm.context(var_1: context.Context): yaklib.mitmConfigOpt【参数】:控制中间人生命周期的上下文
  8. fn mitm.host(var_1: string): yaklib.mitmConfigOpt【参数】:想要监听到本地的地址
  9. fn mitm.rootCA(var_1: []uint8, var_2: []uint8): yaklib.mitmConfigOpt【参数】:设置自定义根证书(可以使用tls 工具包
HTTPS 劫持成功的条件是信任根证书

大概在 2020 年,收约在 xray 社区博客分享过一篇文章,主要讲中间人劫持的一些神奇操作:

HTTPS 劫持漫谈:https://blog.xray.cool/post/HTTPS-hijacking/

本文关于中间人攻击原理就不再赘述了,有兴趣的同学可以查看上面链接,自行学习一下

代理模式中间人:mitm.Start#

代理模式中间人是一个启用 HTTP 代理协议的服务器,同时通过动态签发证书来做中间人劫持(无法劫持 x509 认证和估定证书/CA的)

参考资料:https://datatracker.ietf.org/doc/html/rfc2616

如何使用 mitm.Start ?#

// 启动一个 mitm 劫持服务器go mitm.Start(8084, mitm.callback(fn(isHttps, url, request, response){    if isHttps {        println("厉害了:我们劫持到一个 HTTPS!")        }        http.show(request)    http.showhead(response)}))
// 等待一秒:等待 mitm 完全启动sleep(1)
// 发送一个 Get 请求,设置代理:代理为我们刚启动的 http mitm 服务器rsp, err := http.Get("https://www.baidu.com", http.proxy("http://127.0.0.1:8084"))die(err)

在上面的例子中,我们通过 go 异步启动了一个 mitm 中间人服务器。对所有进入把 127.0.0.1:8084 作为 HTTP 代理的请求进行劫持,获取到信息。

当我们执行上述脚本之后,结果如下:

[WARN] 2021-06-17 17:59:54 +0800 [default:mitm.go:87] mitm proxy use the default cert and keyCONNECT www.baidu.com:443 HTTP/1.1Host: www.baidu.com:443User-Agent: Go-http-client/1.1

HTTP/1.1 200 OKContent-Length: 0

厉害了:我们劫持到一个 HTTPS!GET / HTTP/1.1Host: www.baidu.comConnection: closeConnection: closeUser-Agent: Go-http-client/1.1

HTTP/1.1 200 OKConnection: closeContent-Length: 227Accept-Ranges: bytesCache-Control: no-cacheContent-Type: text/htmlDate: Thu, 17 Jun 2021 09:59:56 GMTP3p: CP=" OTI DSP COR IVA OUR IND COM "P3p: CP=" OTI DSP COR IVA OUR IND COM "Pragma: no-cacheServer: BWS/1.1Set-Cookie: BD_NOT_HTTPS=1; path=/; Max-Age=300Set-Cookie: BIDUPSID=CA8E0B14CB5BB63F465E53D6A93AB589; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: PSTM=1623923996; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BAIDUID=CA8E0B14CB5BB63F2F9039F3FBF3EFDA:FG=1; max-age=31536000; expires=Fri, 17-Jun-22 09:59:56 GMT; domain=.baidu.com; path=/; version=1; comment=bdStrict-Transport-Security: max-age=0Traceid: 1623923996019334170611497956510311710358X-Ua-Compatible: IE=Edge,chrome=1

我们观察上述的劫持结果,发现:

  1. 回调函数中 isHttps 如果为 true,说明当前劫持到的请求是一个 HTTPS 请求
  2. http.show() 对 request 和 response 仍然有效

中间人劫持桥接模式: mitm.Bridge#

在 HTTP 代理协议中,我们可以设置下游代理。设置了下游代理,这个代理就成为了 HTTP/HTTPS 的一个中转站,可以劫持到里面 HTTP 和 HTTPS 的内容,作为代理服务器。

我们设置了下游代理,对一开始的用户并无感觉,我们可以在代理中做很多事情:

  1. 获取用户流量,进行作为扫描器的入口,进行分析构造扫描请求
  2. 修改用户流量,可以进行 IoT 设备的劫持安装包等操作,成功 GetShell,或者设置全局的 302 跳转到特定网站。

如何使用 mitm.Bridge#

我们对上述案例进行改造:我们保留原来 HTTPS 劫持的中间人服务器,在请求发送到 HTTPS 中间人之前,就把请求劫持下来,并且不影响后续的代理请求。

// 启动一个 mitm 劫持服务器go mitm.Start(8084, mitm.callback(fn(isHttps, url, request, response){    println("请求经过了代理服务器")}))
go mitm.Bridge(8086, "http://127.0.0.1:8084", mitm.callback(fn(    isHttps, url, request, response,){    if (!isHttps) {        return    }
    println("劫持到了 HTTPS!")    println("请求经过了桥接模式的中间人!你已经被黑了,Hacked by yaklang")    http.show(request)    http.showhead(response)}))
// 等待一秒:等待 mitm 代理服务器和桥接器完全启动sleep(1)
// 发送一个 Get 请求,设置代理:代理为我们刚启动的 http mitm 服务器rsp, err := http.Get("https://www.baidu.com", http.proxy("http://127.0.0.1:8086"))die(err)

当然,在我们完成上述代码执行的时候,我们如愿以偿得到了如下输出

[WARN] 2021-06-17 20:31:17 +0800 [default:mitm.go:87] mitm proxy use the default cert and key[WARN] 2021-06-17 20:31:17 +0800 [default:mitm.go:87] mitm proxy use the default cert and key请求经过了代理服务器请求经过了代理服务器劫持到了 HTTPS!请求经过了桥接模式的中间人!你已经被黑了,Hacked by yaklangGET / HTTP/1.1Host: www.baidu.comConnection: closeConnection: closeUser-Agent: Go-http-client/1.1

HTTP/1.1 200 OKConnection: closeContent-Length: 227Accept-Ranges: bytesCache-Control: no-cacheContent-Type: text/htmlDate: Thu, 17 Jun 2021 12:31:19 GMTP3p: CP=" OTI DSP COR IVA OUR IND COM "P3p: CP=" OTI DSP COR IVA OUR IND COM "Pragma: no-cacheServer: BWS/1.1Set-Cookie: BD_NOT_HTTPS=1; path=/; Max-Age=300Set-Cookie: BIDUPSID=0C95EED3D6AAB35100B443082722A2C9; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: PSTM=1623933079; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BAIDUID=0C95EED3D6AAB3511E2D3CDCA705D285:FG=1; max-age=31536000; expires=Fri, 17-Jun-22 12:31:19 GMT; domain=.baidu.com; path=/; version=1; comment=bdStrict-Transport-Security: max-age=0Traceid: 1623933079019163777015697755628909614426X-Ua-Compatible: IE=Edge,chrome=1

令人高兴的是,我们看到了 劫持到了 HTTPS!这句话,这句话意味着我们劫持成功了。

websocket的中间人劫持: mitm.wscallback#

随着时代的发展,越来越多的网站开始使用websocket技术,为此,yak也推出了对websocket劫持的支持。我们只需要简单地添加一个mitm.wscallback参数即可。

如何使用 mitm.wscallback#

我们使用一个新的例子来测试,首先编写一个yak脚本来启动一个能劫持websocket的server:

wg = sync.NewWaitGroup()wg.Add(1)go fn{    defer wg.Done()    mitm.Start(8084, mitm.wscallback(    fn(data, isRequest){        if isRequest {            data = "Hijack request"        } else {            data = "Hijack Response"        }        return data    }))}wg.Wait()

这里有2点需要注意:

  1. wscallback接受一个新的函数作为参数,该函数有两个参数:dataisRequests,其中data是[]byte类型(代表劫持到的websocket 请求/响应原始内容),而isRequest则是bool类型(代表劫持到的是请求还是响应,true代表劫持的是请求,false代表劫持的是响应)
  2. 函数返回值将会被用于修改请求/响应内容,如果无需修改也需要将原data返回。(无论如何都要return)

接下来我们使用go来启动一个websocket的测试服务器,这里需要安装依赖:"github.com/gorilla/websocket":

package main
import (    "fmt"    "net/http"    "os"    "time"
    "github.com/gorilla/websocket")
func main() {    var upgrader = websocket.Upgrader{}
    f, err := os.CreateTemp("", "test-*.html")    if err != nil {        panic(err)    }    f.Write([]byte(`<!DOCTYPE html><html><head>    <meta charset="UTF-8"/>    <title>Sample of websocket with golang</title>    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>    <script>        $(function() {            var ws = new WebSocket('ws://' + window.location.host + '/ws');            ws.onmessage = function(e) {                $('<li>').text(event.data).appendTo($ul);            ws.send('{"message":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}');            };            var $ul = $('#msg-list');        });    </script></head><body><ul id="msg-list"></ul></body></html>`))    index := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        http.ServeFile(w, r, f.Name())    })    http.Handle("/", index)    http.Handle("/index.html", index)    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {        // msg := &RecvMessage{}
        ws, err := upgrader.Upgrade(w, r, nil)        if err != nil {            panic(err)            return        }        defer ws.Close()
        go func() {            for {                _, msg, err := ws.ReadMessage()                if err != nil {                    panic(err)                    return                }                fmt.Printf("server recv from client: %s\n", msg)            }        }()
        for {            time.Sleep(time.Second)            ws.WriteJSON(map[string]interface{}{                "message": fmt.Sprintf("Golang Websocket Message: %v", time.Now()),            })        }    })
    err = http.ListenAndServe(":8884", nil)    if err != nil {        panic(err)    }}

现在,我们访问http://127.0.0.1:8884,会发现屏幕会每秒输出一条json内容,例如:

{"message":"Golang Websocket Message: 2022-09-05 15:17:22.497926 +0800 CST m=+7.689153001"}

同时,在终端中会每秒输出一条以下内容:

server recv from client: {"message":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}

这时候我们挂上代理http://127.0.0.1:8084/再进行访问,如果顺利的话,你会发现上述的内容都会发生改变,屏幕输出的内容变为:

Hijack Response

同时,终端输出的内容变为:

server recv from client: Hijack request

这说明我们成功对websocket的请求与响应进行了劫持!

如何使用 mitm.wsforcetext#

mitm.wsforcetext的使用非常简单,我们只需要对上述的例子进行稍微的修改即可:

wg = sync.NewWaitGroup()wg.Add(1)go fn{    defer wg.Done()    mitm.Start(8084, mitm.wsforcetext(true),mitm.wscallback(    fn(data, isRequest){        if isRequest {            data = "Hijack request"        } else {            data = "Hijack Response"        }        return data    }))}wg.Wait()

在说明如何使用mitm.wsforcetext之后,我们来说说为什么需要这个函数,这涉及到了websocket协议。

websocket协议头中有4 bit(opcode)来决定了操作代码,Opcode的值决定了应该如何解析后续的data。如果操作代码是不认识的,那么接收端应该断开连接。可选的操作代码如下:

  • %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
  • %x1:表示这是一个文本帧(frame)
  • %x2:表示这是一个二进制帧(frame)
  • %x3-7:保留的操作代码,用于后续定义的非控制帧。
  • %x8:表示连接断开。
  • %x9:表示这是一个ping操作。
  • %xA:表示这是一个pong操作。
  • %xB-F:保留的操作代码,用于后续定义的控制帧。

在默认情况下,yak启动的mitm server是不会对websocket的opcode进行修改的,而在某些情况下,我们需要强制让修改后的内容变为文本帧(%x1),这也是为什么会有mitm.wsforcetext函数。