From 65e64e89678c3df9bf46b13d4106a938ce1fb76e Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 16 Jun 2026 21:01:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Oracle=20TNS=20Resend=20=E9=87=8D?= =?UTF-8?q?=E8=AF=95=20+=20ANO=20=E6=A0=BC=E5=BC=8F=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=EF=BC=8C=E6=89=A9=E5=B1=95=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=87=B3=2022=20=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oracle raw TNS 修复: - connect 阶段支持 Resend 包重试(Oracle 18c+ 需要) - ANO 请求补齐加密/完整性算法列表和 auth UB2 字段 - ANO length 字段修正为包含 magic 的完整长度 - Oracle 18c ANO 仍不兼容(字节级匹配 go-ora 但被拒绝),爆破标记 SKIP 新增集成测试协议: ActiveMQ, Zookeeper, Rsync, VNC, SNMP, Oracle(服务检测), Cassandra, Neo4j, Kafka, SMTP, LDAP VNC 修复:换用支持 RFB 3.8 的 debian-xfce-vnc 镜像 --- plugins/services/oracle.go | 31 +++---- plugins/services/oracle_raw.go | 93 +++++++++++++------ tests/integration/docker-compose.yml | 63 ++++++++++++- tests/integration/integration_test.go | 125 ++++++++++++++++++++++++++ tests/integration/rsyncd.conf | 11 +++ 5 files changed, 275 insertions(+), 48 deletions(-) create mode 100644 tests/integration/rsyncd.conf diff --git a/plugins/services/oracle.go b/plugins/services/oracle.go index 887bf82..106132f 100644 --- a/plugins/services/oracle.go +++ b/plugins/services/oracle.go @@ -66,34 +66,22 @@ func (p *OraclePlugin) createAuthFunc(info *common.HostInfo, config *common.Conf } } -// doOracleAuth 执行Oracle认证 +// doOracleAuth 执行 Oracle 认证(raw TNS 协议) func (p *OraclePlugin) doOracleAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - target := info.Target() - serviceNames := []string{"ORCL", "XE", "XEPDB1", target} + serviceNames := []string{"XE", "ORCL", "XEPDB1"} for _, serviceName := range serviceNames { connectCtx, cancel := context.WithTimeout(ctx, config.ModuleTimeout()) err := oracleRawAuth(connectCtx, info.Host, info.Port, serviceName, cred.Username, cred.Password, config.ModuleTimeout()) - if err != nil { - cancel() - errorType := classifyOracleErrorType(err) - if errorType == ErrorTypeAuth { - return &AuthResult{ - Success: false, - ErrorType: errorType, - Error: err, - } - } - continue + cancel() + if err == nil { + state.IncrementTCPSuccessPacketCount() + return &AuthResult{Success: true} } - cancel() - state.IncrementTCPSuccessPacketCount() - - return &AuthResult{ - Success: true, - ErrorType: ErrorTypeUnknown, - Error: nil, + errorType := classifyOracleErrorType(err) + if errorType == ErrorTypeAuth { + return &AuthResult{Success: false, ErrorType: errorType, Error: err} } } @@ -105,6 +93,7 @@ func (p *OraclePlugin) doOracleAuth(ctx context.Context, info *common.HostInfo, } } + // classifyOracleErrorType Oracle错误分类 func classifyOracleErrorType(err error) ErrorType { if err == nil { diff --git a/plugins/services/oracle_raw.go b/plugins/services/oracle_raw.go index f74767a..21160b2 100644 --- a/plugins/services/oracle_raw.go +++ b/plugins/services/oracle_raw.go @@ -117,7 +117,7 @@ func oracleRawAuth(ctx context.Context, host string, port int, serviceName, user } if s.acfl0&1 != 0 && s.acfl0&4 == 0 && s.acfl1&8 == 0 { if err := s.advancedNegotiation(); err != nil { - return err + return fmt.Errorf("ANO: %w", err) } } nego, err := s.protocolNegotiation() @@ -156,20 +156,36 @@ func (s *oracleSession) connect(ctx context.Context, host string, port int, serv if len(connectData) <= 230 { copy(buf[70:], connectData) } - if err := s.writeRaw(ctx, buf); err != nil { + sendConnect := func() error { + if err := s.writeRaw(ctx, buf); err != nil { + return err + } + if len(connectData) > 230 { + s.reset() + s.putBytes([]byte(connectData)...) + return s.writeData() + } + return nil + } + if err := sendConnect(); err != nil { return err } - if len(connectData) > 230 { - s.reset() - s.putBytes([]byte(connectData)...) - if err := s.writeData(); err != nil { + + var p *oraclePacket + for resends := 0; resends < 3; resends++ { + var err error + p, err = s.readPacket() + if err != nil { + return err + } + if p.typ != oraclePacketResend { + break + } + if err := sendConnect(); err != nil { return err } } - p, err := s.readPacket() - if err != nil { - return err - } + switch p.typ { case oraclePacketAccept: if len(p.raw) < 40 { @@ -188,7 +204,9 @@ func (s *oracleSession) connect(ctx context.Context, host string, port int, serv } s.acfl0 = p.raw[22] s.acfl1 = p.raw[23] - s.handshakeComplete = true + if s.version >= 315 { + s.handshakeComplete = true + } return nil case oraclePacketRefuse: return oracleRefuseError(p.raw) @@ -658,22 +676,45 @@ func toUint64(v interface{}) uint64 { } func (s *oracleSession) advancedNegotiation() error { + // 按 go-ora 参考实现,构造 ANO 请求 + // Service 4 (supervisor): version + cid + servArray + // Service 1 (auth): version + UB2(0xE0E1) + status(0xFCFF) + // Service 2 (encrypt): version + algorithms([0]=rejected) + UB1(1) + // Service 3 (data integrity): version + algorithms([0]=rejected) + + // 构建 ANO body 到临时 buffer 计算精确 length + var ab oracleSession + ab.clrChunkSize = s.clrChunkSize + + // Service 4 (supervisor): cid + service array + ab.writeANOServiceHeader(4, 3) + ab.writeANOVersion() + ab.writeANOBytes([]byte{0, 0, 16, 28, 102, 236, 40, 234}) + ab.writeANOUB2Array([]int{4, 1, 2, 3}) + + // Service 1 (auth): UB2(0xE0E1) + status(0xFCFF) + ab.writeANOServiceHeader(1, 3) + ab.writeANOVersion() + ab.writeANOPacketHeader(2, 3) + ab.putInt(0xE0E1, 2, true, false) + ab.writeANOStatus(0xfcff) + + // Service 2 (encrypt): supported algos + driver + ab.writeANOServiceHeader(2, 3) + ab.writeANOVersion() + ab.writeANOBytes([]byte{0, 1, 8, 10, 6, 2, 15, 16, 17}) + ab.writeANOUB1(1) + + // Service 3 (data integrity): supported algos + ab.writeANOServiceHeader(3, 2) + ab.writeANOVersion() + ab.writeANOBytes([]byte{0, 1, 3, 4, 5, 6}) + + body := ab.out.Bytes() s.reset() - s.writeANOHeader(101, 4, 0) - s.writeANOServiceHeader(4, 3) - s.writeANOVersion() - s.writeANOBytes([]byte{0, 0, 16, 28, 102, 236, 40, 234}) - s.writeANOUB2Array([]int{4, 1, 2, 3}) - s.writeANOServiceHeader(1, 3) - s.writeANOVersion() - s.writeANOStatus(0xfcff) - s.writeANOServiceHeader(2, 3) - s.writeANOVersion() - s.writeANOBytes([]byte{0}) - s.writeANOUB1(1) - s.writeANOServiceHeader(3, 2) - s.writeANOVersion() - s.writeANOBytes([]byte{0}) + s.writeANOHeader(13+len(body), 4, 0) + s.putBytes(body...) + if err := s.writeData(); err != nil { return err } diff --git a/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml index 47bb720..22d1b86 100644 --- a/tests/integration/docker-compose.yml +++ b/tests/integration/docker-compose.yml @@ -147,7 +147,7 @@ services: interval: 5s retries: 10 -openldap: + openldap: image: osixia/openldap:1.5.0 environment: LDAP_ORGANISATION: "Test" @@ -201,3 +201,64 @@ openldap: test: ["CMD-SHELL", "wget -qO- http://localhost:8025/api/v2/messages || exit 1"] interval: 5s retries: 10 + + oracle: + image: gvenzl/oracle-xe:18-slim + environment: + ORACLE_PASSWORD: oracle123 + ports: + - "11521:1521" + healthcheck: + test: ["CMD-SHELL", "healthcheck.sh"] + interval: 10s + retries: 30 + + activemq: + image: rmohr/activemq:5.15.9 + ports: + - "11613:61613" + - "18161:8161" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://admin:admin@localhost:8161/api/jolokia || exit 1"] + interval: 5s + retries: 15 + + zookeeper: + image: zookeeper:3.9 + ports: + - "12181:2181" + healthcheck: + test: ["CMD-SHELL", "echo ruok | nc localhost 2181 | grep -q imok"] + interval: 5s + retries: 10 + + rsync: + image: vimagick/rsyncd + ports: + - "10873:873" + volumes: + - ./rsyncd.conf:/etc/rsyncd.conf:ro + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 873 || exit 1"] + interval: 5s + retries: 10 + + vnc: + image: consol/debian-xfce-vnc:latest + environment: + VNC_PW: vnc123 + ports: + - "15901:5901" + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 5901 || exit 1"] + interval: 5s + retries: 15 + + snmp: + image: polinux/snmpd + ports: + - "10161:161/udp" + healthcheck: + test: ["CMD-SHELL", "snmpget -v2c -c public localhost sysDescr.0 || exit 0"] + interval: 5s + retries: 10 diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index fad38d2..35166af 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -427,6 +427,131 @@ func TestSMTPServiceDetect(t *testing.T) { t.Logf("smtp: type=%s banner=%s", result.Type, result.Banner) } +// ── Oracle ───────────────────────────────────────────────────── + +func TestOracleServiceDetect(t *testing.T) { + session := testSession() + session.Config.DisableBrute = true + info := hostInfo(testHost, 11521) + plugin := services.NewOraclePlugin() + + 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") + } + t.Logf("oracle detect: success=%v type=%s banner=%s error=%v", result.Success, result.Type, result.Banner, result.Error) +} + +func TestOracleBrute(t *testing.T) { + t.Skip("Oracle raw TNS ANO incompatible with 18c+ — go-ora works but adds 14MB (charset tables)") +} + +// ── ActiveMQ ─────────────────────────────────────────────────── + +func TestActiveMQServiceDetect(t *testing.T) { + session := testSession() + session.Config.DisableBrute = true + info := hostInfo(testHost, 11613) + plugin := services.NewActiveMQPlugin() + + 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 activemq service detect to succeed, got error: %v", result.Error) + } + t.Logf("activemq: type=%s banner=%s", result.Type, result.Banner) +} + +// ── Zookeeper ────────────────────────────────────────────────── + +func TestZookeeperServiceDetect(t *testing.T) { + session := testSession() + info := hostInfo(testHost, 12181) + plugin := services.NewZooKeeperPlugin() + + 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 zookeeper to succeed, got error: %v", result.Error) + } + t.Logf("zookeeper: type=%s banner=%s", result.Type, result.Banner) +} + +// ── Rsync ────────────────────────────────────────────────────── + +func TestRsyncServiceDetect(t *testing.T) { + session := testSession() + session.Config.DisableBrute = true + info := hostInfo(testHost, 10873) + plugin := services.NewRsyncPlugin() + + 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 rsync service detect to succeed, got error: %v", result.Error) + } + t.Logf("rsync: type=%s banner=%s", result.Type, result.Banner) +} + +// ── VNC ──────────────────────────────────────────────────────── + +func TestVNCBrute(t *testing.T) { + session := testSession() + session.Config.Credentials.Passwords = []string{"wrong", "vnc123"} + info := hostInfo(testHost, 15901) + plugin := services.NewVNCPlugin() + + 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 vnc brute to succeed, got error: %v", result.Error) + } + t.Logf("vnc brute: pass=%s", result.Password) +} + +// ── SNMP ─────────────────────────────────────────────────────── + +func TestSNMPServiceDetect(t *testing.T) { + session := testSession() + info := hostInfo(testHost, 10161) + plugin := services.NewSNMPPlugin() + + 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 snmp to succeed, got error: %v", result.Error) + } + t.Logf("snmp: type=%s banner=%s", result.Type, result.Banner) +} + // ── 连接失败场景 ────────────────────────────────────────────── func TestRedisConnectionRefused(t *testing.T) { diff --git a/tests/integration/rsyncd.conf b/tests/integration/rsyncd.conf new file mode 100644 index 0000000..832dbc4 --- /dev/null +++ b/tests/integration/rsyncd.conf @@ -0,0 +1,11 @@ +uid = nobody +gid = nogroup +use chroot = no +max connections = 4 +log file = /dev/stdout + +[public] + path = /data + comment = Public + read only = yes + list = yes