作者 | 修订时间 |
---|---|
2024-12-31 16:45:11 |
作者 | 修订时间 |
---|---|
2024-03-26 18:14:03 |
FoFa爬虫API进一步研究
前沿
几年前我在博客上发表一个文章,就是 基于fofa爬虫获取URL(感兴趣可以去回看一下) ,但是那个是基于Selenium,由于这个东西爬起来过慢,而且内存开销很大,还是python实现的,我满脸的嫌弃,为了提升爬取的速度,所以就有了这篇文章的诞生。
分析
在上一篇文章中提到,由于匿名用户只能访问前五页(现在只能获取前面一页的内容了,真该死)的内容,所以无法获取到更多内容,然而由于资源会分布在不同城市,那么可以利用这一特点,只要获取到这个城市列表,利用链接依次爬取就可以了,如下图所示。
之前是利用的Selenium来获取的页面内容,然而这种速度太慢了,于是我分析了它的JS发现,于是发现了这个接口https://api.fofa.info/v1/search/stats
,如下图
但是你可以发现它的参数进行了校验的。
当你找寻着个源码,你会发现,这个是rsa的签名认证。而且私钥也是存在的,所以说这里看一下他是如何加密的就可以了。
我们跟进这个 i.$sortFun
函数看一下
他是按照顺序排列的这个e
,然后i += "".concat(e).concat(n[e])
进行拼接,最后得出结果
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)
}
}
}
最后简单整合到工具里,就可以一键使用了,目前已经实现了,在我的pathScan或uncover 里面的fofa-spider
使用也很简单
uncover -fofa-spider domain="baidu.com"
或者
pathScan -uq 'domain="baidu.com"' -ue fofa-spider -silent
当然具体用法可以参考pathScan
中的参考命令
Cookie的实现
由于不加cookie
的每个国家只能访问一页的资源所以就很难受,特地加了环境变量FOFA_COOKIE
,来获取身份,所以只需要把登陆后的cookie复制出来,利用环境变量 在运行pathScan
或uncover
前,就可以每个城市读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调用有问题,欢迎邮件提问 support@wjlin0.com
或者support2@wjlin0.com
我会第一时间回复的
Zoomeye爬虫的研究
前沿
对fofa爬虫实现后,我特地去看了一下zoomeye
的,发现他没有这个api的签名校验,并且它的数据获取完全可以通过调用api来实现。
如下图所示,这里可以通过调用/api/search
来获取结果
分析
还是与fofa的思想一样 获取城市信息再逐一爬取,并且我发现,zoomeye还精细了省级和市级行政划分。
这样与fofa相比代码就更加简单了,但是我在爬取的过程中,发现有创宇盾的拦截,会限制请求速率,这没办法了只能控制请求速度了。
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