DeepSeek出街老火了,整个AI界都在热火朝天的讨论它。
同时,安全界也没闲着,来自美国的攻击使它不得不通知中国大陆以外的手机号的注册,同时大家也对它的网站和服务安全性进行了审视,这不Wiz Research就发现它们的数据库面向公网暴露并且无需任何身份即可访问。这两个域名oauth2callback.deepseek.com:9000和dev.deepseek.com:9000。
AI的核心技术既需要这些清北的天才去研究,产品也需要专业的人才去打磨。像DeepSeek这么专业的公司都可能出现这样的漏洞,相信互联网上这么数据库无密码暴露的实例也应该不在少数(实际只找到了2个)。
基于上一篇《扫描全国的公网IP要多久》,我们改造一下代码,让它使用 tcp_syn
的方式探测clickhopuse的9000端口。
首先声明,所有的技术都是为了给大家介绍使用Go语言开发底层的网络程序所做的演示,不是为了介绍安全和攻击方面的内容,所以也不会使用已经成熟的端口和IP扫描工具如zmap、rustscan、nmap、masscan、Advanced IP Scanner、Angry IP Scanner、unicornscan等工具。
同时,也不会追求快速,我仅仅在家中的100M的网络中,使用一台10多年前的4核Linux机器进行测试,尽可能让它能出结果。我一般晚上启动它,早上吃过早餐后来查看结果。
我想把这个实验分成两部分:
寻找中国大陆暴露9000端口的公网IP
检查这些IP是否是部署clickhouse并可以无密码的访问
接下来先介绍第一部分。
寻找暴露端口9000的IP
我们需要将上一篇的代码改造,让它使用TCP进行扫描,而不是ICMP扫描,而且我们只扫描9000端口。
为了更有效的扫描,我做了以下的优化:
使用ICMP扫描出来的可用IP, 一共五千多万
使用tcp sync模拟TCP建联是的握手,这样目的服务器会回一个sync+ack的包
同时探测机自动回复一个RST, 我们也别老挂着目的服务器,怪不好意思的,及时告诉人家别等着咱了
同样的,我们也定义一个TCPScanner
结构体,用来使用TCP握手来进行探测。如果你已经阅读了前一篇文章,应该对我们实现的套路有所了解。
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
package fishfinding
import (
"net"
"os"
"time"
"github.com/kataras/golog"
"golang.org/x/net/bpf"
"golang.org/x/net/ipv4"
)
type TCPScanner struct {
src net.IP
srcPort int
dstPort int
input chan string
output chan string
}
func NewTCPScanner(srcPort, dstPort int , input chan string , output chan string ) *TCPScanner {
localIP := GetLocalIP()
s := &TCPScanner{
input: input,
output: output,
src: net.ParseIP(localIP).To4(),
srcPort: srcPort,
dstPort: dstPort,
}
return s
}
func (s *TCPScanner) Scan() {
go s.recv()
go s.send(s.input)
}
这里定义了一个TCPScanner
结构体,它有一个Scan
方法,用来启动接收和发送两个goroutine。接收goroutine用来接收目标服务器的回复(sync+ack 包),发送goroutine用来发送TCP sync包。
发送逻辑
发送goroutine首先通过net.ListenPacket
创建一个原始套接字,这里使用的是ip4:tcp
,然后发送TCP的包就可以了。
我并没有使用gopacket这个库来构造TCP包,而是自己构造了TCP包,因为我觉得gopacket这个库太重了,而且我只需要构造TCP包,所以自己构造一个TCP包也不是很难。
seq数我们使用了当前进程的PID,这样在接收到回包的时候,还可以使用这个seq数来判断是不是我们发送的回包。
注意这里我们要计算tcp包的checksum, 并没有利用网卡的TCP/IP Checksum Offload功能,而是自己计算checksum,原因在于我的机的网卡很古老了,没有这个功能。
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
func (s *TCPScanner) send(input chan string ) error {
defer func () {
time.Sleep(5 * time.Second)
close (s.output)
golog.Infof("send goroutine exit" )
}()
conn, err := net.ListenPacket("ip4:tcp" , s.src.To4().String())
if err != nil {
golog.Fatal(err)
}
defer conn.Close()
pconn := ipv4.NewPacketConn(conn)
filter := createEmptyFilter()
if assembled, err := bpf.Assemble(filter); err == nil {
pconn.SetBPF(assembled)
}
seq := uint32 (os.Getpid())
for ip := range input {
dstIP := net.ParseIP(ip)
if dstIP == nil {
golog.Errorf("failed to resolve IP address %s" , ip)
continue
}
tcpHeader := &TCPHeader{
Source: uint16 (s.srcPort),
Destination: uint16 (s.dstPort),
SeqNum: seq,
AckNum: 0 ,
Flags: 0 x002,
Window: 65535 ,
Checksum: 0 ,
Urgent: 0 ,
}
tcpHeader.Checksum = tcpChecksum(tcpHeader, s.src, dstIP)
packet := tcpHeader.Marshal()
_, err = conn.WriteTo(packet, &net.IPAddr{IP: dstIP})
if err != nil {
golog.Errorf("failed to send TCP packet: %v" , err)
}
}
return nil
}
接收逻辑
接收goroutine首先创建一个原始套接字,使用net.ListenIP
,然后使用ipv4.NewPacketConn
来创建一个ipv4.PacketConn
,并设置ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface
,这样可以获取到源IP、目标IP和接口信息。 这里必须设置ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface
, 否则不能获取到目标服务器的IP。pv4.FlagDst
到是不需要的。
接收到数据后,我们解析TCP头,然后判断是否是我们发送的包,如果是我们发送的包,我们就将目标IP发送到output
通道。
如果是我们发送的回包,我们就判断是否是SYN+ACK包,同时判断ACK是否和我们发送的seq对应,如果是,我们就将目标IP发送到output
通道。
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
func (s *TCPScanner) recv() error {
defer recover ()
conn, err := net.ListenIP("ip4:tcp" , &net.IPAddr{IP: s.src})
if err != nil {
golog.Fatal(err)
}
defer conn.Close()
pconn := ipv4.NewPacketConn(conn)
if err := pconn.SetControlMessage(ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true ); err != nil {
golog.Fatalf("set control message error: %v\n" , err)
}
seq := uint32 (os.Getpid()) + 1
buf := make ([]byte , 1024 )
for {
n, peer, err := conn.ReadFrom(buf)
if err != nil {
golog.Errorf("failed to read: %v" , err)
continue
}
if n < tcpHeaderLength {
continue
}
tcpHeader := ParseTCPHeader(buf[:n])
if tcpHeader.Destination != uint16 (s.srcPort) || tcpHeader.Source != uint16 (s.dstPort) {
continue
}
if tcpHeader.Flags == 0 x012 && tcpHeader.AckNum == seq {
s.output <- peer.String()
}
}
}
完整的代码在这里 。
最终我把可以连接端口9000的IP保存到了一个文件中,一共有970+个IP。
检查没有身份验证clickhouse
接下来我们要检查这些IP是否是clickhouse的服务,而且没有身份验证。
使用类似的方法,我们定义一个ClickHouseChecker
结构体,用来检查这些IP是否是clickhouse的服务。
它会尝试使用这些IP和9000建立和clickhouse的连接,如果连接成功,并且调用Ping()
方法成功,我们就认为这个IP是clickhouse的服务。
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
package fishfinding
import (
"context"
"fmt"
"runtime"
"sync"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
_ "github.com/ClickHouse/clickhouse-go/v2"
"github.com/kataras/golog"
)
type ClickHouseChecker struct {
wg *sync.WaitGroup
port int
input chan string
output chan string
}
func NewClickHouseChecker(port int , input chan string , output chan string , wg *sync.WaitGroup) *ClickHouseChecker {
s := &ClickHouseChecker{
port: port,
input: input,
output: output,
wg: wg,
}
return s
}
func (s *ClickHouseChecker) Check() {
parallel := runtime.NumCPU()
for i := 0 ; i < parallel; i++ {
s.wg.Add(1 )
go s.check()
}
}
func (s *ClickHouseChecker) check() {
defer s.wg.Done()
for ip := range s.input {
if ip == "splitting" || ip == "failed" {
continue
}
if isClickHouse(ip, s.port) {
s.output <- ip
}
}
}
func isClickHouse(ip string , port int ) bool {
conn, err := clickhouse.Open(&clickhouse.Options{
Addr: []string {fmt.Sprintf("%s:%d" , ip, port)},
Settings: clickhouse.Settings{
"max_execution_time" : 1 ,
},
DialTimeout: time.Second,
MaxOpenConns: 1 ,
MaxIdleConns: 1 ,
ConnMaxLifetime: time.Duration(1 ) * time.Minute,
ConnOpenStrategy: clickhouse.ConnOpenInOrder,
BlockBufferSize: 10 ,
MaxCompressionBuffer: 1024 ,
})
if err != nil {
golog.Errorf("open %s:%d failed: %v" , ip, port, err)
return false
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err = conn.Ping(ctx)
if err != nil {
golog.Warnf("ping %s:%d failed: %v" , ip, port, err)
return false
}
return true
}
实际扫描下来,几乎所有的IP的9000端口都连接超时或者不是clickhouse服务,只有4个IP是clickhouse服务,但是需要身份验证。,报错default: Authentication failed: password is incorrect, or there is no user with such name.
挺好的一件事情,至少公网暴露的clickhouse服务都是需要身份验证的。
当然也有可能是clickhouse的服务端配置了IP白名单,只允许内网访问,这样的话我们就无法访问了。也可能是clickhouse的端口改成了其他端口,我们无法访问。
有必要扫描一下全网的IP和它们的9000端口了
使用既有的程序即可。我们先拉取全网的网段信息。
1
wget -c -O- http://ftp.apnic.net/stats/apnic/delegated-apnic-latest | awk -F '|' '/ipv4/ {print $4 "/" 32-log($5)/log(2)}' | cat > ipv4.txt
先用icmp_scan
扫描一下公网课访问的IP地址:
1
2
3
4
5
6
7
8
9
10
11
12
......
[INFO] 2025 /01 /31 03 :56 223.255 .250.221 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.1 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .240.91 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.10 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.15 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.11 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.115 is alive
[INFO] 2025 /01 /31 03 :56 223.255 .233.100 is alive
[INFO] 2025 /01 /31 03 :56 send goroutine exit
[INFO] 2025 /01 /31 03 :56 total: 884686592 , alive: 15500888 , time: 2 h35m28.930123788 s
一共8亿多个IP,可以ping的通的有1500多万个,耗时2小时扫描完。
根据网友在上一篇的留言反馈,光美国就有8亿多个IP。 我问deepseek,全球有37亿个IP,美国有9亿个,这个数量才合理,我自己扫描的8亿要远远少于这个数量。而且活跃的IP我感觉应该远远大于1500多万。 但是这些不重要了,我要做的就是能扫描到可以免密登录的clickhouse服务,看看这些IP里面有没有。
接下来我们使用tcp_scan
扫描这些IP的9000端口:
1
2
3
4
5
6
7
8
9
10
11
......
[INFO] 2025 /01 /31 08 :47 223.197 .222.126 is alive
[INFO] 2025 /01 /31 08 :47 223.197 .219.60 is alive
[INFO] 2025 /01 /31 08 :47 223.220 .171.218 is alive
[INFO] 2025 /01 /31 08 :47 223.221 .238.176 is alive
[INFO] 2025 /01 /31 08 :47 223.197 .235.26 is alive
[INFO] 2025 /01 /31 08 :47 223.197 .225.240 is alive
[INFO] 2025 /01 /31 08 :47 223.197 .225.208 is alive
[INFO] 2025 /01 /31 08 :47 223.197 .219.139 is alive
[INFO] 2025 /01 /31 08 :47 send goroutine exit
[INFO] 2025 /01 /31 08 :47 total: 15500890 , alive: 3953 , time: 2 m41.23585658 s
在这1500多万个IP中,有3953个IP的9000端口是可以访问的,但是都需要验证能不能进行clickhouse的操作,我们需要进一步检查。
接下来我们使用clickhouse_check
检查这些IP是否是clickhouse服务:
1
2
3
4
5
6
7
8
9
10
11
12
......
[WARN] 2025 /01 /31 11 :47 ping 223.197 .222.126 :9000 failed: read : read tcp 192.168 .1.5 :53494 ->223.197 .222.126 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping 223.197 .219.60 :9000 failed: read : read tcp 192.168 .1.5 :49718 ->223.197 .219.60 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping 223.221 .238.176 :9000 failed: read : read tcp 192.168 .1.5 :56662 ->223.221 .238.176 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping 223.197 .235.26 :9000 failed: read : read tcp 192.168 .1.5 :47676 ->223.197 .235.26 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping send:9000 failed: dial tcp: lookup send on 127.0 .0.53 :53 : server misbehaving
[WARN] 2025 /01 /31 11 :47 ping total::9000 failed: dial tcp: address total::9000 : too many colons in address
[WARN] 2025 /01 /31 11 :47 ping 223.197 .225.240 :9000 failed: read : read tcp 192.168 .1.5 :55342 ->223.197 .225.240 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping 223.197 .225.208 :9000 failed: read : read tcp 192.168 .1.5 :43300 ->223.197 .225.208 :9000 : i/o timeout
[WARN] 2025 /01 /31 11 :47 ping 223.197 .219.139 :9000 failed: read : read tcp 192.168 .1.5 :57552 ->223.197 .219.139 :9000 : i/o timeout
[INFO] 2025 /01 /31 11 :47 total: 2 , time: 4 m20.744235925 s
4分钟完成。最终还是真的发现有两个IP的9000端口是clickhouse服务,而且不需要密码验证。
类似的我们还可以验证Redis、Mysql等服务的安全性。