作者 修订时间
wjlin0 2024-12-31 16:45:11
作者 修订时间
wjlin0 2024-03-26 18:14:03

FoFa爬虫API进一步研究

前沿

几年前我在博客上发表一个文章,就是 基于fofa爬虫获取URL(感兴趣可以去回看一下) ,但是那个是基于Selenium,由于这个东西爬起来过慢,而且内存开销很大,还是python实现的,我满脸的嫌弃,为了提升爬取的速度,所以就有了这篇文章的诞生。

分析

在上一篇文章中提到,由于匿名用户只能访问前五页(现在只能获取前面一页的内容了,真该死)的内容,所以无法获取到更多内容,然而由于资源会分布在不同城市,那么可以利用这一特点,只要获取到这个城市列表,利用链接依次爬取就可以了,如下图所示。

image-20240318224918094

之前是利用的Selenium来获取的页面内容,然而这种速度太慢了,于是我分析了它的JS发现,于是发现了这个接口https://api.fofa.info/v1/search/stats,如下图

image-20240318225530820

但是你可以发现它的参数进行了校验的。

image-20240318225556649

当你找寻着个源码,你会发现,这个是rsa的签名认证。而且私钥也是存在的,所以说这里看一下他是如何加密的就可以了。

image-20240318230212922

我们跟进这个 i.$sortFun函数看一下

他是按照顺序排列的这个e ,然后i += "".concat(e).concat(n[e])进行拼接,最后得出结果

image-20240318230525478

fullfalseqbase64ImJhaWR1LmNvbSI=ts1710774077895

这个结果你会发现,ts是当前的时间戳,而这个qbase64就是我们查询的语句,所以说只需要写一下rsa-sha256加密就可以了

如下go语言简单实现

package main

import (
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "github.com/projectdiscovery/retryablehttp-go"
    "net/http"
    "sort"
    "strconv"
    "time"
)

const (
    private = "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEogIBAAKCAQEAv0xjefuBTF6Ox940ZqLLUFFBDtTcB9dAfDjWgyZ2A55K+VdG\r\nc1L5LqJWuyRkhYGFTlI4K5hRiExvjXuwIEed1norp5cKdeTLJwmvPyFgaEh7Ow19\r\nTu9sTR5hHxThjT8ieArB2kNAdp8Xoo7O8KihmBmtbJ1umRv2XxG+mm2ByPZFlTdW\r\nRFU38oCPkGKlrl/RzOJKRYMv10s1MWBPY6oYkRiOX/EsAUVae6zKRqNR2Q4HzJV8\r\ngOYMPvqkau8hwN8i6r0z0jkDGCRJSW9djWk3Byi3R2oSdZ0IoS+91MFtKvWYdnNH\r\n2Ubhlnu1P+wbeuIFdp2u7ZQOtgPX0mtQ263e5QIDAQABAoIBAD67GwfeTMkxXNr3\r\n5/EcQ1XEP3RQoxLDKHdT4CxDyYFoQCfB0e1xcRs0ywI1be1FyuQjHB5Xpazve8lG\r\nnTwIoB68E2KyqhB9BY14pIosNMQduKNlygi/hKFJbAnYPBqocHIy/NzJHvOHOiXp\r\ndL0AX3VUPkWW3rTAsar9U6aqcFvorMJQ2NPjijcXA0p1MlZAZKODO2wqidfQ487h\r\nxy0ZkriYVi419j83a1cCK0QocXiUUeQM6zRNgQv7LCmrFo2X4JEzlujEveqvsDC4\r\nMBRgkK2lNH+AFuRwOEr4PIlk9rrpHA4O1V13P3hJpH5gxs5oLLM1CWWG9YWLL44G\r\nzD9Tm8ECgYEA8NStMXyAmHLYmd2h0u5jpNGbegf96z9s/RnCVbNHmIqh/pbXizcv\r\nmMeLR7a0BLs9eiCpjNf9hob/JCJTms6SmqJ5NyRMJtZghF6YJuCSO1MTxkI/6RUw\r\nmrygQTiF8RyVUlEoNJyhZCVWqCYjctAisEDaBRnUTpNn0mLvEXgf1pUCgYEAy1kE\r\nd0YqGh/z4c/D09crQMrR/lvTOD+LRMf9lH+SkScT0GzdNIT5yuscRwKsnE6SpC5G\r\nySJFVhCnCBsQqq+ohsrXt8a99G7ePTMSAGK3QtC7QS3liDmvPBk6mJiLrKiRAZos\r\nvgPg7nTP8VuF0ZIKzkdWbGoMyNxVFZXovQ8BYxECgYBvCR9xGX4Qy6KiDlV18wNu\r\nElYkxVqFBBE0AJRg/u+bnQ9jWhi2zxLa1eWZgtss80c876I8lbkGNWedOVZioatm\r\nMFLC4bFalqyZWyO7iP7i60LKvfDJfkOSlDUu3OikahFOiqyG1VBz4+M4U500alIU\r\nAVKD14zTTZMopQSkgUXsoQKBgHd8RgiD3Qde0SJVv97BZzP6OWw5rqI1jHMNBK72\r\nSzwpdxYYcd6DaHfYsNP0+VIbRUVdv9A95/oLbOpxZNi2wNL7a8gb6tAvOT1Cvggl\r\n+UM0fWNuQZpLMvGgbXLu59u7bQFBA5tfkhLr5qgOvFIJe3n8JwcrRXndJc26OXil\r\n0Y3RAoGAJOqYN2CD4vOs6CHdnQvyn7ICc41ila/H49fjsiJ70RUD1aD8nYuosOnj\r\nwbG6+eWekyLZ1RVEw3eRF+aMOEFNaK6xKjXGMhuWj3A9xVw9Fauv8a2KBU42Vmcd\r\nt4HRyaBPCQQsIoErdChZj8g7DdxWheuiKoN4gbfK4W1APCcuhUA=\r\n-----END RSA PRIVATE KEY-----"
    appId   = "9e9fb94330d97833acfbc041ee1a76793f1bc691"
    stats   = "https://api.fofa.info/v1/search/stats?qbase64=%s&full=%v&fields=%s&ts=%v&sign=%s&app_id=%s"
    URL     = "https://fofa.info/result?qbase64=%s&page=%d&page_size=%d"
)

type fofaRequest struct {
    Query   string `json:"query"`
    Page    int    `json:"page"`
    PageNum int    `json:"page_num"`
}

var rsaPrivateKey *rsa.PrivateKey

func init() {
    rsaPrivateKey, _ = parsePKCS1PemByPrivateKey([]byte(private))
}

type foFaStatsResponse struct {
    Code    int         `json:"code,omitempty"`
    Message string      `json:"message,omitempty"`
    Data    interface{} `json:"data,omitempty"`
}

type data struct {
    Size      int       `json:"size"`
    Page      int       `json:"page"`
    Countries []country `json:"countries"`
}

type country struct {
    Code    string   `json:"code"`
    Name    string   `json:"name"`
    Count   int      `json:"count"`
    Regions []region `json:"regions"`
}
type region struct {
    Code  string `json:"code"`
    Count int    `json:"count"`
    Name  string `json:"name"`
}

func serialize(h map[string]any) string {
    var keys []string
    for k, _ := range h {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    s := ""
    for _, k := range keys {
        switch h[k].(type) {
        case string:
            if h[k] == "" {
                continue
            }
        }
        s += fmt.Sprintf("%s%v", k, h[k])
    }
    return s
}

func hashWithSha256(b []byte) (hashed []byte) {
    myHash := sha256.New()
    myHash.Write(b)
    return myHash.Sum(nil)
}
func parsePKCS1PemByPrivateKey(b []byte) (*rsa.PrivateKey, error) {
    p, _ := pem.Decode(b)
    if p == nil {
        return nil, errors.New("pem格式错误")
    }
    private, err := x509.ParsePKCS1PrivateKey(p.Bytes)
    if err != nil {
        return nil, err
    }
    return private, nil
}

func signQuery(str string) (string, error) {
    hashed := hashWithSha256([]byte(str))
    v15, err := signPKCS1v15(rsaPrivateKey, crypto.SHA256, hashed)
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(v15), nil
}
func signPKCS1v15(private *rsa.PrivateKey, hash crypto.Hash, hashed []byte) (sign []byte, err error) {
    return rsa.SignPKCS1v15(rand.Reader, private, hash, hashed)
}

func main() {
    fofaResponse := &foFaStatsResponse{}
    m := make(map[string]interface{})
    ts := strconv.FormatInt(time.Now().Unix(), 10)
    qbase64 := "baidu.com"
    qbase64 = base64.StdEncoding.EncodeToString([]byte(qbase64))
    m["qbase64"] = qbase64
    m["full"] = false
    m["fields"] = ""
    m["ts"] = ts
    sign, err := signQuery(serialize(m))
    if err != nil {
        panic(err)
    }
    STATS := fmt.Sprintf(stats, qbase64, false, "", ts, sign, appId)
    request, err := retryablehttp.NewRequest(http.MethodGet, STATS, nil)
    if err != nil {
        panic(err.Error())
    }
    request.Header.Set("Referer", "https://fofa.info/")
    resp, err := retryablehttp.DefaultHTTPClient.Do(request)
    if err != nil {
        panic(err.Error())
    }
    if err := json.NewDecoder(resp.Body).Decode(fofaResponse); err != nil {
        panic(err.Error())
    }
    if fofaResponse.Code == -9 {
        panic(fofaResponse)
    }
    switch fofaResponse.Data.(type) {
    case string:
        panic(fofaResponse.Message)
    default:

        if temp, ok := fofaResponse.Data.(map[string]interface{}); !ok {
            fmt.Println(fofaResponse.Data)
            panic(errors.New("错误的对象"))
        } else {
            fmt.Println(temp)
        }
    }

}

image-20240318233451482

最后简单整合到工具里,就可以一键使用了,目前已经实现了,在我的pathScanuncover 里面的fofa-spider

使用也很简单

uncover -fofa-spider domain="baidu.com"
或者
pathScan  -uq 'domain="baidu.com"' -ue fofa-spider -silent

image-20240319002657624

image-20240319003509717

当然具体用法可以参考pathScan中的参考命令

Cookie的实现

由于不加cookie的每个国家只能访问一页的资源所以就很难受,特地加了环境变量FOFA_COOKIE,来获取身份,所以只需要把登陆后的cookie复制出来,利用环境变量 在运行pathScanuncover前,就可以每个城市读5页甚至更多,如下代码

export FOFA_COOKIE="your cookies" uncover -fofa-spider domain="baidu.com" 
或者
export FOFA_COOKIE="your cookies" pathScan  -uq 'domain="baidu.com"' -ue fofa-spider -silent

⚠️但要注意的是,加了cookie后 用的次数过多会存在封号的可能,所以每次运行可以通过临时邮箱建立一个账户,再把cookie加进去,就没事了

提问

如果对这个爬虫的api调用有问题,欢迎邮件提问 [email protected]或者[email protected] 我会第一时间回复的

Zoomeye爬虫的研究

前沿

对fofa爬虫实现后,我特地去看了一下zoomeye的,发现他没有这个api的签名校验,并且它的数据获取完全可以通过调用api来实现。

如下图所示,这里可以通过调用/api/search来获取结果

image-20240319173639944

分析

还是与fofa的思想一样 获取城市信息再逐一爬取,并且我发现,zoomeye还精细了省级和市级行政划分。

image-20240319173751654

这样与fofa相比代码就更加简单了,但是我在爬取的过程中,发现有创宇盾的拦截,会限制请求速率,这没办法了只能控制请求速度了。

image-20240319193547615

Cookie的实现

与fofa的实现类似,需要登陆后,将cookie设置环境变量

⚠️ 这样可能导致封号的处理,请谨慎使用

export ZOOMEYE_COOKIE="your cookies" uncover -zoomeye-spider baidu.com
或者
export ZOOMEYE_COOKIE="your cookies" pathScan -ue zoomeye-spider -uq baidu.com -silent

工具项目地址

results matching ""

    No results matching ""