Skip to main content

函数库:poc - 专家 HTTP 库

"专家级" 的 HTTP 库指的是用户可以通过当前这个函数库获得如下一般 HTTP 库无法支持的场景:

  1. 直接使用原始 HTTP Request 报文发送数据包
  2. 构造畸形数据包
  3. 修复不符合 HTTP 协议的 HTTP Request 报文,让他能被合理接受
  4. 手动控制 chunk 等过程
  5. 自动处理 HTTP Response 的响应信息

...

在这些需求中,我们的视角将发生变化,在 基础 HTTP 通信 中,我们需要以遵循 HTTP 协议的角度思考问题;但是在本文中,我们视角将变为对 "HTTP 数据包"。

这个视角变化对安全领域非常重要。

除此之外,我们还要知道:

HTTP 的传输层协议是 TCP(Transmission Control Protocol)协议。

HTTP 协议通常使用 TCP 协议作为传输层协议,因为 HTTP 协议需要可靠的数据传输服务。当客户端发送 HTTP 请求时,它会使用 TCP 协议在客户端和服务器之间建立一个连接,并传输请求数据。服务器在收到请求后,使用相同的 TCP 连接向客户端发送 HTTP 响应数据。在传输数据时,TCP 协议会确保数据的完整性和顺序性,以及在网络拥塞和流量控制方面提供帮助,从而确保 HTTP 数据的可靠传输。

HTTP2 协议的传输层也是 TCP。HTTP2 数据传输基本单位是 Frame,而不是大家熟悉的 HTTP 报文,但是 HTTP2 在表现力上是完全兼容 HTTP 的,所以我们可以实现两种协议的互相转换。

通过了解当前文档,我们将会学习如下内容:

使用 URL 发送数据包:poc.Do#

最常见的做法是使用 URL 发送数据包,这个时候我们可以使用 poc.Do 函数来发送数据包。

其返回值是响应结构体,请求结构体和错误。参考后文获取更多信息。

以下是一个简单的例子,我们使用 poc.Do 来发送一个 GET 请求:

rsp, req = poc.Do("GET", "https://www.baidu.com")~
注意 ~ 是 Yaklang 的特殊语法

带有 ~ 结尾的函数调用将会自动忽略错误,如果错误不为空,将会造成当前程序抛出错误

详情我们可以参考如下两个章节:

  1. 函数简化调用:WavyCall
  2. 异常处理:try-catch

当然,poc 库也提供了更加简便地方法来快速发送其他的请求,例如 poc.Get, poc.Post。其本质上是对 poc.Do 的封装。

rsp, req = poc.Get("https://www.baidu.com")~ // 相当于 poc.Do("GET", "https://www.baidu.com")rsp, req = poc.Post("https://www.baidu.com", poc.replaceBody("a=1&b=2", false))~ //  相当于 poc.Do("POST", "https://www.baidu.com", poc.replaceBody("a=1&b=2", false))
专家 HTTP 库的额外参数

注意到在发送请求时,我们可以在第一个参数 (URL) 后传入若干个额外参数,例如上文中的 poc.replaceBody("a=1&b=2", false)。这个额外参数的作用即是指定请求的请求体。

这些额外参数可以帮助我们更好地控制原始请求包或请求的行为。可以参考后文

使用原始 HTTP 报文发送数据包: poc.HTTP#

使用 poc.HTTP 可以直接做到以一个数据包发送报文:

rsp, req = poc.HTTP(`GET / HTTP/1.1Host: www.baidu.com
`)~
/*rsp:([]uint8) (len=10511 cap=13076) { 00000000  48 54 54 50 2f 31 2e 31  20 32 30 30 20 4f 4b 0d  |HTTP/1.1 200 OK.| 00000010  0a 41 63 63 65 70 74 2d  52 61 6e 67 65 73 3a 20  |.Accept-Ranges: | 00000020  62 79 74 65 73 0d 0a 43  61 63 68 65 2d 43 6f 6e  |bytes..Cache-Con|......}
req:([]uint8) (len=39 cap=48) { 00000000  47 45 54 20 2f 20 48 54  54 50 2f 31 2e 31 0d 0a  |GET / HTTP/1.1..| 00000010  48 6f 73 74 3a 20 77 77  77 2e 62 61 69 64 75 2e  |Host: www.baidu.| 00000020  63 6f 6d 0d 0a 0d 0a                              |com....|}*/

在我们了解上述基本方式之后,可以很直观的了解到,使用这种方式我们发送出去的数据包非常容易被用户控制。

在任何地方加入任何数据都会被可能保留原义发送。

不符合规范的数据包将会被尽力修复

修复数据包是一个极其复杂的过程,我们要考虑包括但不限于如下内容:

  1. 数据包的 CRLF 是否被正确设置?
  2. 数据包是否是合理的 chunk?
  3. 针对 multipart/form-data 的数据,boundary 是否合理?如果不合理应该怎么修复?
  4. Content-Length 和 Content-Encoding 之间的关系是什么?
  5. 如果用户规定了 Content-Length 不被修复应该怎么处理?
  6. 不规整的请求往往对应不规整的响应,这种情况应该如何处理?

使用原始 HTTP 报文发送数据包,获取更多请求与响应数据: poc.HTTPEx#

使用poc.HTTP发送数据包时返回三个值:responseRaw, requestRaw, error,也就是原始响应报文,原始请求报文和错误,这在某些情况下是不够用的,我们可能还需要更多的信息,比如:服务器响应时间,服务器的URL...

发现了这个缺陷之后,我们为poc标准库添加了一个新的函数:poc.HTTPEx,这个函数的参数与原有的poc.HTTP一样,但是返回值不一样:是响应结构体,请求结构体和错误,通过返回结构体,我们可以获取到这次请求的更多信息。

举一个简单的例子,响应结构体的结构如下:

type LowhttpResponse struct {    RawPacket              []byte // 原始响应报文    RedirectRawPackets     []*RedirectFlow // 所有重定向的请求与响应流信息    PortIsOpen             bool // 端口是否开放    TraceInfo              *LowhttpTraceInfo // 与时间相关的跟踪信息    Url                    string // 这次请求的URL    RemoteAddr             string // 这次请求的远程主机地址    Proxy                  string // 这次请求所使用的代理    Https                  bool // 这次请求是否是HTTPS    Http2                  bool // 这次请求是否是HTTP2    RawRequest             []byte // 原始请求报文    MultiResponse          bool // 是否返回了多个响应, 常见于pipeline请求与响应    MultiResponseInstances []*http.Response // 返回的多个响应的原始响应结构体
    TooLarge         bool    TooLargeLimit    int64    ResponseBodySize int64}
type RedirectFlow struct {    IsHttps    bool    Request    []byte    Response   []byte    RespRecord *LowhttpResponse}
type LowhttpTraceInfo struct {    AvailableDNSServers []string    DNSTime time.Duration // DNS的耗时    ConnTime time.Duration // 获取一个连接的耗时    ServerTime time.Duration // 服务器处理耗时,即从连接建立到客户端收到第一个字节的时间间隔    TotalTime time.Duration // 完整请求的耗时}

在该结构体中,我们就可以拿到更多与这次请求响应相关的信息,如服务器处理耗时,重定向次数,重定向请求及响应。除此之外,我们通常可以访问MultiResponseInstances[0]这个原始响应结构体来拿到这次响应的各种信息,可以参考后文

响应与更多请求控制#

获取响应信息#

我们通过 poc.HTTP 拿到的原始响应报文是 bytes 类型的,这固然很直观,但是我们希望拿到响应中的具体信息(如响应状态码,响应体)应该怎么做呢?

方法一:解析为原始响应结构体#

第一个方法是使用 poc.ParseBytesToHTTPResponse将原始响应报文解析为原始响应结构体,在此结构体中我们就可以拿到许多响应信息。

一个例子如下:

rsp, req = poc.HTTP(`GET / HTTP/1.1Host: www.baidu.com
`)~rspIns = poc.ParseBytesToHTTPResponse(rsp)~println(rspIns.StatusCode) // 响应状态码body = io.ReadAll(rspIns.Body)~println(string(body)) // 响应体

方法二:使用辅助函数#

第二个方法是使用poc库提供的辅助函数,这些函数可以帮助我们快速获取响应信息,如响应状态码,响应体等。

一个例子如下:

rsp, req = poc.HTTP(`GET / HTTP/1.1Host: www.baidu.com
`)~proto, statusCode, _ = poc.GetHTTPPacketFirstLine(rsp)statusCode = int(statusCode) // 响应状态码body = poc.GetHTTPPacketBody(rsp) // 响应体

这两种方法没有好坏之分,用户可以根据需求自行选择。

修改数据包#

使用 poc.HTTP 时,我们直接传入的是原始请求数据包,这样的方式对于一些简单的场景来说已经足够了,但是对于一些复杂的场景,我们可能需要对原始请求数据包进行修改,这时我们可以使用 poc.HTTP 提供的一些额外参数或者辅助函数来对原始请求数据包进行修改。

方法一:请求时使用额外参数#

有一些额外参数可以辅助我们对原始请求数据包进行修改,以下是提供的接口:

poc.replaceFirstLinepoc.replaceMethodpoc.replaceHeaderpoc.replaceHostpoc.replaceBasicAuthpoc.replaceCookiepoc.replaceBodypoc.replaceAllQueryParamspoc.replaceAllPostParamspoc.replaceQueryParampoc.replacePostParampoc.replacePathpoc.appendHeaderpoc.appendCookiepoc.appendQueryParampoc.appendPostParampoc.appendPathpoc.appendFormEncodedpoc.appendUploadFilepoc.deleteHeaderpoc.deleteCookiepoc.deleteQueryParampoc.deletePostParampoc.deleteForm

一些简单的例子如下:

修改请求方法#
// 将请求方法改为POST,并增加bodyrsp, req = poc.HTTP(`GET /post HTTP/1.1Host: pie.dev`, poc.replaceMethod("POST"), poc.replaceBody("a=1&b=2", false))~printf("%s", rsp)
修改path#
// 将path改为getrsp, req = poc.HTTP(`GET /post HTTP/1.1Host: pie.dev`, poc.replacePath("/get"))~printf("%s", rsp)
修改Host#
// 将Host改为pie.devrsp, req = poc.HTTP(`GET /get HTTP/1.1Host: baidu.com`, poc.replaceHost("pie.dev"))~printf("%s", rsp)
修改所有GET参数#
rsp, req = poc.HTTP(`GET /get?a=1&b=2 HTTP/1.1Host: pie.dev`, poc.replaceAllQueryParams({"a":"3", "b":"4"}))~printf("%s", rsp)
增加一个form表单,上传文件#
// 新增一个form,上传文件,poc.appendUploadFile 第一个参数为fieldName,第二个参数为fileName,第三个参数为文件内容,第四个参数为contentTypersp, req = poc.HTTP(`POST /post HTTP/1.1Host: pie.devContent-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gWContent-Disposition: form-data; name="submit"
true------WebKitFormBoundary7MA4YWxkTrZu0gW--`, poc.appendUploadFile("file", "upload.php", "<?php phpinfo();?>", "image/png"))~printf("%s", rsp)

方法二:使用辅助函数#

与额外参数类似,用户可以直接使用辅助函数对原始请求数据包进行修改,而不进行请求:

poc.ReplaceHTTPPacketMethodpoc.ReplaceHTTPPacketFirstLinepoc.ReplaceHTTPPacketHeaderpoc.ReplaceHTTPPacketBodypoc.ReplaceHTTPPacketCookiepoc.ReplaceHTTPPacketHostpoc.ReplaceHTTPPacketBasicAuthpoc.ReplaceAllHTTPPacketQueryParamspoc.ReplaceAllHTTPPacketPostParamspoc.ReplaceHTTPPacketQueryParampoc.ReplaceHTTPPacketPostParampoc.ReplaceHTTPPacketPathpoc.AppendHTTPPacketHeaderpoc.AppendHTTPPacketCookiepoc.AppendHTTPPacketQueryParampoc.AppendHTTPPacketPostParampoc.AppendHTTPPacketPathpoc.AppendHTTPPacketFormEncodedpoc.AppendHTTPPacketUploadFilepoc.DeleteHTTPPacketHeaderpoc.DeleteHTTPPacketCookiepoc.DeleteHTTPPacketQueryParampoc.DeleteHTTPPacketPostParampoc.DeleteHTTPPacketForm

一些简单的例子如下:

修改请求方法#
// 将请求方法改为POSTreq = poc.ReplaceHTTPPacketMethod(`GET /post HTTP/1.1Host: pie.dev`, "POST")
修改path#
// 将path改为getreq = poc.ReplaceHTTPPacketPath(`GET /post HTTP/1.1Host: pie.dev`, "/get")

方法三: poc.BuildRequest#

这是poc标准库最近新增的一个工具函数,经常在构建请求时使用,一个简单的例子如下:

packet = `GET /post HTTP/1.1Host: pie.dev`// 修改请求方法为POST,并添加bodypacket = poc.BuildRequest(packet, poc.replaceMethod("POST"), poc.replaceBody("a=1&b=2", false))

额外参数:是否使用 TLS(HTTPS)#

当我们使用上述内容之后,很自然地会有疑问: 通过 URL 传递的目标,会在 schema 部分写明 https:// 还是 http://,那么 poc 这个库如何指定访问目标是否是 HTTPS 呢?

这时就可以使用额外参数了。其类似于 http 库中的额外参数。

使用 poc.https 设置 TLS 加密通信
packet = `GET / HTTP/1.1Host: www.baidu.com
`rsp, req = poc.HTTP(packet, poc.https(true))~
大多数参数都可以这么设置

这种可变参数的设置方法在 Yak 中非常常用,它允许用户拥有好的代码提示,并且能在动态类型中很好地配合强类型限制去限制参数内容。

同时使用多个参数
rsp, req = poc.HTTP(    packet,    poc.https(true),    poc.http2(true),    poc.proxy("https://127.0.0.1:7890"),)~
我们可以同时限制若干参数,这些都可以稳定同时生效。

额外参数:其他参数#

下文表格以过时,访问此处获取最新的额外参数

参数名说明使用案例
poc.https(bool)指定 HTTPSpoc.HTTP(packet, poc.https(true))
poc.http2(bool)指定使用 HTTP2 访问(一般 HTTPS 也需要同时开启)poc.HTTP(packet, poc.https(true), poc.http2(true))
poc.host(string)发起 HTTP 请求之前,TCP 连接的 Hostpoc.HTTP(packet, poc.host("192.168.1.1"))
poc.port(int)发起 HTTP 请求之前,TCP 连接的 Port, 一般配合 poc.host 一起使用poc.HTTP(packet, poc.host("192.168.1.1"), poc.port(80))
poc.retryTimes(int)如果访问失败,重试的次数,这个访问失败指的是网络原因导致错误(超时等)poc.HTTP(packet, poc.retryTimes(3))
poc.retryInStatusCode(...int)如果访问符合用户定义的状态码,则重试该请求poc.HTTP(packet, poc.retryInStatusCode(200,300), poc.retryTimes(3))
poc.retryNotInStatusCode(...int)如果访问不符合用户定义的状态码,则重试该请求poc.HTTP(packet, poc.retryNotInStatusCode(404), poc.retryTimes(3))
poc.redirectTimes(int)允许网站进行重定向的次数poc.HTTP(packet, poc.redirectTimes(3))
poc.noRedirect()禁用重定向(等价于 poc.redirectTimes(0))poc.HTTP(packet, poc.noRedirect())
poc.jsRedirect(bool)启用 JS 重定向,启动从 meta 或者 js 中提取重定向执行。poc.HTTP(packet, poc.jsRedirect(true))
poc.proxy(...string)使用代理访问,支持: Socks5,Socks4a,HTTP,HTTPSpoc.HTTP(packet, poc.proxy("http://127.0.0.1:7890"))
poc.timeout(float)为请求增加超时限制poc.HTTP(packet, poc.timeout(5))
poc.noFixContentLength(bool)不修复 Content-Length 长度,一般用来测试请求走私的情况或其他畸形数据poc.HTTP(packet, poc.noFixContentLength(true))
poc.save(bool)自动保存当前流量到数据库(默认为 True)poc.HTTP(packet, poc.save(bool))
poc.session(any)自动保留当前 session (根据 session 缓存 Cookie 等认证信息)poc.HTTP(packet, poc.session("abc"))
poc.params(map)自动渲染数据包中 fuzz 参数标签poc.HTTP(packet, poc.params({"target": "baidu.com:80"}))

目标地址#

一般情况下,我们并不需要手动指定目标地址,poc.HTTP 会自动识别 Host 请求头进行数据包发送。假如 Host 请求头中没有指定端口,则使用默认端口(HTTP为80,HTTPS为443)。假如没有 Host 请求头且没有手动指定 Host 则会报错。

我们以下面的例子来说明:

不指定 HTTPS && 不指定端口
packet = `GET / HTTP/1.1Host: www.baidu.com
`rsp, req = poc.HTTP(    packet,)~

此时目标地址为:www.baidu.com:80

指定 HTTPS && 不指定端口
packet = `GET / HTTP/1.1Host: www.baidu.com
`rsp, req = poc.HTTP(    packet,    poc.https(true))~

此时目标地址为:www.baidu.com:443

指定端口
packet = `GET / HTTP/1.1Host: www.baidu.com:8080
`rsp, req = poc.HTTP(    packet,    poc.https(true))~

此时目标地址为:www.baidu.com:8080

这个请求无论是否是HTTPS,端口都会被自动设置为 8080

手动指定 Host (常用于 Host 碰撞)#

另外,用户也可以使用 poc.host(string)poc.port(int) 来直接指定目标地址。

tip

这个功能在安全实践中非常有用,一般用于进行 Mock 或者 Host 碰撞。

指定 Host 和 Port 来发送数据包
packet = `GET / HTTP/1.1Host: www.baidu.com:8080
`rsp, req = poc.HTTP(    packet,    poc.https(true),    poc.host("127.0.0.1"),    poc.port(8081),)~

数据包修复和分割#

修复数据包: poc.FixHTTPRequestpoc.FixHTTPResponse#

除了正常发送数据包之外,我们如果觉得自己构造的数据包有问题,可以通过修复数据包的方式格式化自己的数据包,修复的方便如下:

  1. 移除数据包前无谓的空格
  2. 修复数据包的 Content-Length 保证 Body 被正确读取。
  3. 如果数据包是 multipart/form-data 形式的,自动校验并修复 Boundary
  4. 自动修复 CRLF
修复 Content-Length
packet = `GET / HTTP/1.1Host: www.example.comUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
abc123123`
req = poc.FixHTTPRequest(packet)/*println(string(req)):
GET / HTTP/1.1Host: www.example.comUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)Content-Length: 9
abc123123*/

除了修复 Content-Length 的基础情况,我们也可以修复复杂的 Boundary,我们观察如下数据包,大致存在的问题有:

  1. 数据包前有无用空格空行
  2. 数据包中 Content-Type 包含 boundary 和实际 Body 中对应不上
  3. 数据包 Content-Length 无法对应
  4. 数据包中结尾缺失 CRLF*2
修复上传文件数据包,同时也能修复 Content-Length
packet = `POST / HTTP/1.1Host: localhost:8000User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0Connection: keep-aliveContent-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266Content-Length: 554
-----------------------------abcContent-Disposition: form-data; name="text"
abc-----------------------------abcContent-Disposition: form-data; name="file1"; filename="a.txt"Content-Type: text/plain
Content of a.txt.-----------------------------abc--`
req = poc.FixHTTPRequest(packet)

修复后数据包的中内容如下:

修复后的数据包
POST / HTTP/1.1Host: localhost:8000User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0Connection: keep-aliveContent-Type: multipart/form-data; boundary=---------------------------abcContent-Length: 267
-----------------------------abcContent-Disposition: form-data; name="text"
abc-----------------------------abcContent-Disposition: form-data; name="file1"; filename="a.txt"Content-Type: text/plain
Content of a.txt.-----------------------------abc--

分割数据包 Header 和 Body: poc.Split#

一般来说,我们在 poc.HTTP 中获得的数据包是 bytes 类型的,实际上在进行数据处理过程中,我们有时并不希望整体进行处理,需要单独进行处理 header 与 body。

rsp, req, err := poc.HTTP(`GET / HTTP/1.1Host: www.example.comUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
`)die(err)
// 分割响应数据包的 Header 和 Body 部分header, body = poc.Split(rsp)println(header)                             // 打印数据包 Headerprintf("Body MD5: %v\n", codec.Md5(body))   // 计算数据包响应的 md5
简化上述代码

我们在上述代码中,使用 err 去尝试接收 poc.HTTP (这是 Golang 的错误处理风格)。

这种处理方式没有任何问题,可以很好的让 "错误" 变成代码逻辑,那么如果不想处理错误的话,我们可以使用

简化错误处理
rsp, req := poc.HTTP(`GET / HTTP/1.1Host: www.example.comUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
`)~
// 分割响应数据包的 Header 和 Body 部分header, body = poc.Split(rsp)println(header)                             // 打印数据包 Headerprintf("Body MD5: %v\n", codec.Md5(body))   // 计算数据包响应的 md5
/*[INFO] 2023-02-21 12:28:32 +0800 [exec.go:489] should save url: http://www.example.com/HTTP/1.1 200 OKAge: 345812Cache-Control: max-age=604800Content-Type: text/html; charset=utf-8Date: Tue, 21 Feb 2023 04:28:36 GMTEtag: "3147526947+ident"Expires: Tue, 28 Feb 2023 04:28:36 GMTLast-Modified: Thu, 17 Oct 2019 07:18:26 GMTServer: ECS (sab/5799)Vary: Accept-EncodingX-Cache: HITContent-Length: 1256

Body MD5: 84238dfc8092e5d9c0dac8ef93371a07*/

如何批量发送请求?#

方法一:并发编程#

手动实现并发编程和批量发送请求,需要有前置的一些知识,您可以查看如下文档:

  1. yak 中的并发编程

方法二:学习使用 fuzz - 模糊测试工具包#

当然,您可以继续学习更强大的模糊测试技术 fuzz 模块来掌握如何测试多个数据包。

高级话题#

TBD

  1. HTTP2 支持
  2. Websocket 支持