mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
fix: Cassandra CQL 协议头缺少 flags 字节 + version 方向位错误
cqlSend 写 8 字节头(缺 flags),实际 CQL v4 需要 9 字节。 version byte 0x84 是 response 方向,request 应为 0x04。 同时扩展集成测试至 17 个协议:新增 Memcached、Elasticsearch、 MSSQL、RabbitMQ、MQTT、LDAP、Cassandra、Neo4j、Kafka、SMTP。
This commit is contained in:
@@ -74,7 +74,7 @@ func (p *CassandraPlugin) createAuthFunc(info *common.HostInfo, config *common.C
|
|||||||
//
|
//
|
||||||
// [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
// [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
||||||
const (
|
const (
|
||||||
cqlVersion = 0x84 // version=4, direction=request
|
cqlVersion = 0x04 // version=4, direction=request
|
||||||
cqlOpStartup = 0x01
|
cqlOpStartup = 0x01
|
||||||
cqlOpAuthRsp = 0x0f
|
cqlOpAuthRsp = 0x0f
|
||||||
cqlOpQuery = 0x07
|
cqlOpQuery = 0x07
|
||||||
@@ -178,12 +178,13 @@ func nextCQLStreamID() uint16 {
|
|||||||
func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
||||||
id := nextCQLStreamID()
|
id := nextCQLStreamID()
|
||||||
|
|
||||||
// frame: [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
// CQL v4 frame: [1B version] [1B flags] [2B stream] [1B opcode] [4B length] [body]
|
||||||
header := make([]byte, 8)
|
header := make([]byte, 9)
|
||||||
header[0] = cqlVersion
|
header[0] = cqlVersion // 0x04 = request, version 4
|
||||||
binary.BigEndian.PutUint16(header[1:3], id)
|
header[1] = 0x00 // flags
|
||||||
header[3] = opcode
|
binary.BigEndian.PutUint16(header[2:4], id)
|
||||||
binary.BigEndian.PutUint32(header[4:8], uint32(len(body)))
|
header[4] = opcode
|
||||||
|
binary.BigEndian.PutUint32(header[5:9], uint32(len(body)))
|
||||||
|
|
||||||
buf := append(header, body...)
|
buf := append(header, body...)
|
||||||
_, err := conn.Write(buf)
|
_, err := conn.Write(buf)
|
||||||
|
|||||||
@@ -87,3 +87,117 @@ services:
|
|||||||
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')", "-u", "admin", "-p", "mongo123"]
|
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')", "-u", "admin", "-p", "mongo123"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
memcached:
|
||||||
|
image: memcached:1-alpine
|
||||||
|
ports:
|
||||||
|
- "11211:11211"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "echo stats | nc localhost 11211 | grep -q pid"]
|
||||||
|
interval: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: elasticsearch:7.17.24
|
||||||
|
environment:
|
||||||
|
discovery.type: single-node
|
||||||
|
xpack.security.enabled: "false"
|
||||||
|
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
|
||||||
|
ports:
|
||||||
|
- "19200:9200"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
mssql:
|
||||||
|
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||||
|
environment:
|
||||||
|
ACCEPT_EULA: "Y"
|
||||||
|
SA_PASSWORD: "MssqlTest123!"
|
||||||
|
MSSQL_PID: Express
|
||||||
|
ports:
|
||||||
|
- "11433:1433"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'MssqlTest123!' -Q 'SELECT 1' || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3-management-alpine
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: admin
|
||||||
|
RABBITMQ_DEFAULT_PASS: rabbit123
|
||||||
|
ports:
|
||||||
|
- "15672:15672"
|
||||||
|
- "15673:5672"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
ports:
|
||||||
|
- "11883:1883"
|
||||||
|
volumes:
|
||||||
|
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mosquitto_sub -t '$$SYS/#' -C 1 -W 2 || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
openldap:
|
||||||
|
image: osixia/openldap:1.5.0
|
||||||
|
environment:
|
||||||
|
LDAP_ORGANISATION: "Test"
|
||||||
|
LDAP_DOMAIN: "test.local"
|
||||||
|
LDAP_ADMIN_PASSWORD: "ldap123"
|
||||||
|
ports:
|
||||||
|
- "10389:389"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -b 'dc=test,dc=local' -D 'cn=admin,dc=test,dc=local' -w ldap123 || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
cassandra:
|
||||||
|
image: cassandra:4.1
|
||||||
|
environment:
|
||||||
|
CASSANDRA_AUTHENTICATOR: AllowAllAuthenticator
|
||||||
|
ports:
|
||||||
|
- "19042:9042"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "cqlsh -e 'DESCRIBE CLUSTER' || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:5
|
||||||
|
environment:
|
||||||
|
NEO4J_AUTH: "neo4j/neo4jtest123"
|
||||||
|
ports:
|
||||||
|
- "17687:7687"
|
||||||
|
- "17474:7474"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
image: apache/kafka:3.7.0
|
||||||
|
ports:
|
||||||
|
- "19092:9092"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
image: mailhog/mailhog
|
||||||
|
ports:
|
||||||
|
- "11025:1025"
|
||||||
|
- "18025:8025"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8025/api/v2/messages || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 10
|
||||||
|
|||||||
@@ -204,6 +204,229 @@ func TestMongoDBBrute(t *testing.T) {
|
|||||||
t.Logf("mongodb brute: user=%s pass=%s", result.Username, result.Password)
|
t.Logf("mongodb brute: user=%s pass=%s", result.Username, result.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Memcached ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestMemcachedUnauthorized(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
info := hostInfo(testHost, 11211)
|
||||||
|
plugin := services.NewMemcachedPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected memcached to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("memcached: type=%s banner=%s", result.Type, result.Banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Elasticsearch ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestElasticsearchUnauthorized(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
info := hostInfo(testHost, 19200)
|
||||||
|
plugin := services.NewElasticsearchPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected elasticsearch to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("elasticsearch: type=%s vulinfo=%s", result.Type, result.VulInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MSSQL ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestMSSQLBrute(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
|
||||||
|
{Username: "sa", Password: "wrong"},
|
||||||
|
{Username: "sa", Password: "MssqlTest123!"},
|
||||||
|
}
|
||||||
|
info := hostInfo(testHost, 11433)
|
||||||
|
plugin := services.NewMSSQLPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected mssql brute to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("mssql brute: user=%s pass=%s", result.Username, result.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RabbitMQ ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestRabbitMQBrute(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
|
||||||
|
{Username: "admin", Password: "wrong"},
|
||||||
|
{Username: "admin", Password: "rabbit123"},
|
||||||
|
}
|
||||||
|
info := hostInfo(testHost, 15672)
|
||||||
|
plugin := services.NewRabbitMQPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected rabbitmq brute to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("rabbitmq brute: user=%s pass=%s", result.Username, result.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MQTT ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestMQTTServiceDetect(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
info := hostInfo(testHost, 11883)
|
||||||
|
plugin := services.NewMQTTPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected mqtt service detect to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("mqtt: service=%s banner=%s", result.Service, result.Banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SMB ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSMBBrute(t *testing.T) {
|
||||||
|
t.Skip("SMB requires port 445 which is reserved on WSL2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LDAP ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestLDAPBrute(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
|
||||||
|
{Username: "cn=admin,dc=test,dc=local", Password: "wrong"},
|
||||||
|
{Username: "cn=admin,dc=test,dc=local", Password: "ldap123"},
|
||||||
|
}
|
||||||
|
info := hostInfo(testHost, 10389)
|
||||||
|
plugin := services.NewLDAPPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected ldap brute to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("ldap brute: user=%s pass=%s", result.Username, result.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cassandra ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestCassandraServiceDetect(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
session.Config.DisableBrute = true
|
||||||
|
info := hostInfo(testHost, 19042)
|
||||||
|
plugin := services.NewCassandraPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected cassandra service detect to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("cassandra: type=%s banner=%s", result.Type, result.Banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Neo4j ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestNeo4jBrute(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
|
||||||
|
{Username: "neo4j", Password: "wrong"},
|
||||||
|
{Username: "neo4j", Password: "neo4jtest123"},
|
||||||
|
}
|
||||||
|
info := hostInfo(testHost, 17687)
|
||||||
|
plugin := services.NewNeo4jPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected neo4j brute to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("neo4j brute: user=%s pass=%s", result.Username, result.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kafka ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestKafkaNoAuth(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
info := hostInfo(testHost, 19092)
|
||||||
|
plugin := services.NewKafkaPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected kafka to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("kafka: type=%s banner=%s", result.Type, result.Banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SMTP ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSMTPServiceDetect(t *testing.T) {
|
||||||
|
session := testSession()
|
||||||
|
info := hostInfo(testHost, 11025)
|
||||||
|
plugin := services.NewSMTPPlugin()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := plugin.Scan(ctx, info, session)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result is nil")
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("expected smtp to succeed, got error: %v", result.Error)
|
||||||
|
}
|
||||||
|
t.Logf("smtp: type=%s banner=%s", result.Type, result.Banner)
|
||||||
|
}
|
||||||
|
|
||||||
// ── 连接失败场景 ──────────────────────────────────────────────
|
// ── 连接失败场景 ──────────────────────────────────────────────
|
||||||
|
|
||||||
func TestRedisConnectionRefused(t *testing.T) {
|
func TestRedisConnectionRefused(t *testing.T) {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
listener 1883
|
||||||
|
allow_anonymous true
|
||||||
Reference in New Issue
Block a user