projectdiscovery之httpx源码学习 - w9ay

admin 2021年12月31日15:58:08评论328 views字数 8461阅读28分12秒阅读模式

httpx is a fast and multi-purpose HTTP toolkit allows to run multiple probers using retryablehttp library, it is designed to maintain the result reliability with increased threads.

httpx是一个多功能的http请求工具包,通常可以用它来批量探测资产的一些基本信息。

源码地址:https://github.com/projectdiscovery/httpx

支持的探针有

通常情况下,实现一个类似的工具是很容易的,如果用python来写

import requests
url = "https://x.hacking8.com"
r = requests.get(url)
resp = r.text
status_code = r.status_code

就可以获得大部分探针的信息,但要做成一个工程化的软件,就要考虑更多问题。

如何处理并发,如何加快请求速度,如何进行原生http请求以适应更多探针,以及如何工程化的组织go的源码框架,看了httpx的源码,这些都挺有收获的,所以写一篇源码记录。

基础结构

源码目录

.
├── cmd
│   └── httpx # 程序编译目录
├── common # 公共函数
│   ├── customheader # 自定义header的一些处理
│   ├── customports # 自定义端口的处理
│   ├── fileutil # 文件处理
│   ├── httputilz # http处理
│   ├── httpx # 一些探针
│   ├── iputil # ip处理
│   ├── slice # go切片处理
│   └── stringz # string函数
├── internal # 运行处理主目录
│   └── runner
├── scripts # 不重要
└── static # 不重要

cmd/httpx/httpx.go是整个程序的主入口,流程一目了然,先解析命令,做初始化操作,按照命令运行,最后关闭。

package main

import (
    "github.com/projectdiscovery/gologger"
    "github.com/projectdiscovery/httpx/internal/runner"
)

func main() {
    // 解析参数
    options := runner.ParseOptions()

    // 启动,初始化工作
    run, err := runner.New(options)
    if err != nil {
        gologger.Fatalf("Could not create runner: %s\n", err)
    }

    // 运行
    run.RunEnumeration()

    // 关闭
    run.Close()
}

Go的并发处理

在http的请求中,最好对go协程的并发做一些限制,类似于线程池的概念。

httpx使用了https://github.com/remeh/sizedwaitgroup的库

使用也很简单,初始化时定义协程的数量,初始化之后,在要使用协程之前wg.Add(),使用协程后wg.Done()代表结束即可。

以上只是对协程数做了限制,如果还想对每秒并行的协程数量做限制,可以参考projectdiscover另一个项目nuclei,它在使用了sizedwaitgroup的基础上,还使用了go.uber.org/ratelimit

使用更加简单

package main

import (
    "fmt"
    "time"

    "go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(1) // per second

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take() // 重点是它
        if i > 0 {
            fmt.Println(i, now.Sub(prev))
        }
        prev = now
    }

}

初始化后在协程中调用Take()方法即可。

探针

httpx支持了很多基于http的基础探针,看几个有意思的。

探针的实现大部分在common/httpx目录。

Cdn check

common/httpx/cdn.go

在以前看naabu时也看到过 https://x.hacking8.com/post-406.html

顾名思义是检测是否是CDN,跟踪一下,发现cdn检查调用的是github.com/projectdiscovery/cdncheck中的项目。

主要是通过接口获取一些CDN的ip段,判断ip是否在这些ip段中

CSP

common/httpx/csp.go

主要是获取header中的响应头

// CSPHeaders is an incomplete list of most common CSP headers
var CSPHeaders []string = []string{
    "Content-Security-Policy",               // standard
    "Content-Security-Policy-Report-Only",   // standard
    "X-Content-Security-Policy-Report-Only", // non - standard
    "X-Webkit-Csp-Report-Only",              // non - standard
}

HTTP2.0

httpx支持http2.0的探针

httpx.client2 = &http.Client{
        Transport: &http2.Transport{
            TLSClientConfig: &tls.Config{
                InsecureSkipVerify: true,
            },
            AllowHTTP: true,
        },
        Timeout: httpx.Options.Timeout,
    }

Pipeline 探针

http 1.1支持管道传输,例如在一个数据包中发送这样的包

GET / HTTP/1.1
HOST: 127.0.0.1

GET / HTTP/1.1
HOST: 127.0.0.1

GET / HTTP/1.1
HOST: 127.0.0.1

检测最后的返回是否合法。

package httpx

import (
    "crypto/tls"
    "fmt"
    "net"
    "strings"
    "time"
)

// SupportPipeline checks if the target host supports HTTP1.1 pipelining by sending x probes
// and reading back responses expecting at least 2 with HTTP/1.1 or HTTP/1.0
func (h *HTTPX) SupportPipeline(protocol, method, host string, port int) bool {
    addr := host
    if port == 0 {
        port = 80
        if protocol == "https" {
            port = 443
        }
    }
    if port > 0 {
        addr = fmt.Sprintf("%s:%d", host, port)
    }
    // dummy method while awaiting for full rawhttp implementation
    dummyReq := fmt.Sprintf("%s / HTTP/1.1\nHost: %s\n\n", method, addr)
    conn, err := pipelineDial(protocol, addr)
    if err != nil {
        return false
    }
    // send some probes
    nprobes := 10
    for i := 0; i < nprobes; i++ {
        if _, err = conn.Write([]byte(dummyReq)); err != nil {
            return false
        }
    }
    gotReplies := 0
    reply := make([]byte, 1024)
    for i := 0; i < nprobes; i++ {
        err := conn.SetReadDeadline(time.Now().Add(1 * time.Second))
        if err != nil {
            return false
        }

        if _, err := conn.Read(reply); err != nil {
            break
        }

        // The check is very naive, but it works most of the times
        for _, s := range strings.Split(string(reply), "\n\n") {
            if strings.Contains(s, "HTTP/1.1") || strings.Contains(s, "HTTP/1.0") {
                gotReplies++
            }
        }
    }

    // expect at least 2 replies
    return gotReplies >= 2
}

func pipelineDial(protocol, addr string) (net.Conn, error) {
    // http
    if protocol == "http" {
        return net.Dial("tcp", addr)
    }

    // https
    return tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true})
}

Title

httpx里面的title单独对中文编码做了优化

但是不太完美,只对返回头的Content-Type做了判断,但有的网站是从html的meta中定义编码的,有兴趣的老哥可以去提个pr。

HTTP 证书信息

go内置的网络库已经把tls信息获取并解析了,直接获取即可。

package httpx

import (
    "net/http"
)

// TLSData contains the relevant Transport Layer Security information
type TLSData struct {
    DNSNames         []string `json:"dns_names,omitempty"`
    Emails           []string `json:"emails,omitempty"`
    CommonName       []string `json:"common_name,omitempty"`
    Organization     []string `json:"organization,omitempty"`
    IssuerCommonName []string `json:"issuer_common_name,omitempty"`
    IssuerOrg        []string `json:"issuer_organization,omitempty"`
}

// TLSGrab fills the TLSData
func (h *HTTPX) TLSGrab(r *http.Response) *TLSData {
    if r.TLS != nil {
        var tlsdata TLSData
        for _, certificate := range r.TLS.PeerCertificates {
            tlsdata.DNSNames = append(tlsdata.DNSNames, certificate.DNSNames...)
            tlsdata.Emails = append(tlsdata.Emails, certificate.EmailAddresses...)
            tlsdata.CommonName = append(tlsdata.CommonName, certificate.Subject.CommonName)
            tlsdata.Organization = append(tlsdata.Organization, certificate.Subject.Organization...)
            tlsdata.IssuerOrg = append(tlsdata.IssuerOrg, certificate.Issuer.Organization...)
            tlsdata.IssuerCommonName = append(tlsdata.IssuerCommonName, certificate.Issuer.CommonName)
        }
        return &tlsdata
    }
    return nil
}

检测virtualhost

检测是否是虚拟主机,方法很简单,将host换成一个随机值,和原来的返回进行比对,不一样则为虚拟主机。

比对的方法有很多,状态码,长度,单词数,行数,文本相似度对比等等。

package httpx

import (
    "fmt"

    "github.com/hbakhtiyor/strsim"
    retryablehttp "github.com/projectdiscovery/retryablehttp-go"
    "github.com/rs/xid"
)

const simMultiplier = 100

// IsVirtualHost checks if the target endpoint is a virtual host
func (h *HTTPX) IsVirtualHost(req *retryablehttp.Request) (bool, error) {
    httpresp1, err := h.Do(req)
    if err != nil {
        return false, err
    }

    // request a non-existing endpoint
    req.Host = fmt.Sprintf("%s.%s", xid.New().String(), req.Host)

    httpresp2, err := h.Do(req)
    if err != nil {
        return false, err
    }

    // Status Code
    // 比对状态码,不相同则返回True
    if !h.Options.VHostIgnoreStatusCode && httpresp1.StatusCode != httpresp2.StatusCode {
        return true, nil
    }

    // Content - Bytes Length
    // 比对content,不等则返回True
    if !h.Options.VHostIgnoreContentLength && httpresp1.ContentLength != httpresp2.ContentLength {
        return true, nil
    }

    // Content - Number of words (space separated)
    // 比对单词数量,不等则返回True
    if !h.Options.VHostIgnoreNumberOfWords && httpresp1.Words != httpresp2.Words {
        return true, nil
    }

    // Content - Number of lines (newline separated)
    // 比对行数
    if !h.Options.VHostIgnoreNumberOfLines && httpresp1.Lines != httpresp2.Lines {
        return true, nil
    }

    // Similarity Ratio - if similarity is under threshold we consider it a valid vHost
    // 文本相似度对比
    if int(strsim.Compare(httpresp1.Raw, httpresp2.Raw)*simMultiplier) <= h.Options.VHostSimilarityRatio {
        return true, nil
    }

    return false, nil
}

不为人知的优化

autofdmax

// automatic fd max increase if running as root
    _ "github.com/projectdiscovery/fdmax/autofdmax

之前看naabu时也说过哦

大多数类UNIX操作系统(包括Linux和macOS)在每个进程和每个用户的基础上提供了系统资源的限制和控制(如线程,文件和网络连接)的方法。 这些“ulimits”阻止单个用户使用太多系统资源。

这个库的作用是自动将限制调到最大。

dns缓存

httpx调用了github.com/projectdiscovery/fastdialer/fastdialer

会缓存dns记录(文件式),加快请求速度

http/https的识别转换

httpx默认会请求http/https两种协议,默认先请求https,如果失败了会再请求http,这样就能识别出使用了http还是https了。

internal/runner/runner.go

识别指纹

之前给httpx提交过一个pr,基于wappalyzer的指纹。

PR详情: https://github.com/projectdiscovery/httpx/pull/205

若干天后,projectdiscover官方自己实现了一个这个库的,并作为go包的形式调用了。https://github.com/projectdiscovery/wappalyzergo

最新版已经可以使用,使用命令

-tech-detect
chaos -d hackerone.com -silent | ./httpx -tech-detect

    __    __  __       _  __
   / /_  / /_/ /_____ | |/ /
  / __ \/ __/ __/ __ \|   /
 / / / / /_/ /_/ /_/ /   |
/_/ /_/\__/\__/ .___/_/|_|
             /_/              v1.0.4-dev

        projectdiscovery.io

Use with caution. You are responsible for your actions
Developers assume no liability and are not responsible for any misuse or damage.
https://mta-sts.hackerone.com [GitHub Pages,Ruby on Rails,Varnish]
https://mta-sts.managed.hackerone.com [Ruby on Rails,Varnish,GitHub Pages]
https://mta-sts.forwarding.hackerone.com [Varnish,GitHub Pages,Ruby on Rails]
https://www.hackerone.com [Varnish,Amazon EC2,Cloudflare,React,Drupal,PHP,Acquia Cloud Platform,Apache,Percona]
https://api.hackerone.com [Cloudflare,jsDelivr]
https://resources.hackerone.com
https://support.hackerone.com [Cloudflare]
https://docs.hackerone.com [jsDelivr,React,Gatsby,webpack,Varnish,GitHub Pages,Ruby on Rails]

可以学习的

结合GitHub action的自动编译发布

实现打个tag,github就自动帮你编译发布,在开源软件中还是比较方便的

新建.github/workflows目录,新建release.yml

name: Release
on:
  create:
    tags:
      - v*

jobs: 
  release: 
    runs-on: ubuntu-latest
    steps: 
      - 
        name: "Check out code"
        uses: actions/[email protected]
        with: 
          fetch-depth: 0
      - 
        name: "Set up Go"
        uses: actions/[email protected]
        with: 
          go-version: 1.14
      - 
        env: 
          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
        name: "Create release on GitHub"
        uses: goreleaser/[email protected]
        with: 
          args: "release --rm-dist"
          version: latest

编译分为三步,第一步签出代码,第二部安装go环境,第三部使用goreleaser/goreleaser-action编译上传。

说下第三步,在根目录新建.goreleaser.yml文件,文件内容用于配制编译以及上传选项,httpx的配置如下

builds:
    - binary: httpx
      main: cmd/httpx/httpx.go
      goos:
        - linux
        - windows
        - darwin
      goarch:
        - amd64
        - 386
        - arm
        - arm64

archives:
    - id: tgz
      format: tar.gz
      replacements:
          darwin: macOS
      format_overrides:
          - goos: windows
            format: zip

具体字段的介绍可查看:https://goreleaser.com/intro/

最后

projectdiscover为资产探测/收集造了很多go的基础轮子,用起来很方便,如果想学习go&&写扫描器,这个项目真是非常好的学习资料。

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日15:58:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   projectdiscovery之httpx源码学习 - w9ayhttps://cn-sec.com/archives/713216.html

发表评论

匿名网友 填写信息