mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
fix: Oracle TNS Resend 重试 + ANO 格式修正,扩展集成测试至 22 协议
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 镜像
This commit is contained in:
@@ -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 {
|
func (p *OraclePlugin) doOracleAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||||
target := info.Target()
|
serviceNames := []string{"XE", "ORCL", "XEPDB1"}
|
||||||
serviceNames := []string{"ORCL", "XE", "XEPDB1", target}
|
|
||||||
|
|
||||||
for _, serviceName := range serviceNames {
|
for _, serviceName := range serviceNames {
|
||||||
connectCtx, cancel := context.WithTimeout(ctx, config.ModuleTimeout())
|
connectCtx, cancel := context.WithTimeout(ctx, config.ModuleTimeout())
|
||||||
err := oracleRawAuth(connectCtx, info.Host, info.Port, serviceName, cred.Username, cred.Password, config.ModuleTimeout())
|
err := oracleRawAuth(connectCtx, info.Host, info.Port, serviceName, cred.Username, cred.Password, config.ModuleTimeout())
|
||||||
if err != nil {
|
|
||||||
cancel()
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
state.IncrementTCPSuccessPacketCount()
|
||||||
|
return &AuthResult{Success: true}
|
||||||
|
}
|
||||||
|
|
||||||
errorType := classifyOracleErrorType(err)
|
errorType := classifyOracleErrorType(err)
|
||||||
if errorType == ErrorTypeAuth {
|
if errorType == ErrorTypeAuth {
|
||||||
return &AuthResult{
|
return &AuthResult{Success: false, ErrorType: errorType, Error: err}
|
||||||
Success: false,
|
|
||||||
ErrorType: errorType,
|
|
||||||
Error: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
return &AuthResult{
|
|
||||||
Success: true,
|
|
||||||
ErrorType: ErrorTypeUnknown,
|
|
||||||
Error: nil,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +93,7 @@ func (p *OraclePlugin) doOracleAuth(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// classifyOracleErrorType Oracle错误分类
|
// classifyOracleErrorType Oracle错误分类
|
||||||
func classifyOracleErrorType(err error) ErrorType {
|
func classifyOracleErrorType(err error) ErrorType {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -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 s.acfl0&1 != 0 && s.acfl0&4 == 0 && s.acfl1&8 == 0 {
|
||||||
if err := s.advancedNegotiation(); err != nil {
|
if err := s.advancedNegotiation(); err != nil {
|
||||||
return err
|
return fmt.Errorf("ANO: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nego, err := s.protocolNegotiation()
|
nego, err := s.protocolNegotiation()
|
||||||
@@ -156,20 +156,36 @@ func (s *oracleSession) connect(ctx context.Context, host string, port int, serv
|
|||||||
if len(connectData) <= 230 {
|
if len(connectData) <= 230 {
|
||||||
copy(buf[70:], connectData)
|
copy(buf[70:], connectData)
|
||||||
}
|
}
|
||||||
|
sendConnect := func() error {
|
||||||
if err := s.writeRaw(ctx, buf); err != nil {
|
if err := s.writeRaw(ctx, buf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(connectData) > 230 {
|
if len(connectData) > 230 {
|
||||||
s.reset()
|
s.reset()
|
||||||
s.putBytes([]byte(connectData)...)
|
s.putBytes([]byte(connectData)...)
|
||||||
if err := s.writeData(); err != nil {
|
return s.writeData()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := sendConnect(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
p, err := s.readPacket()
|
var p *oraclePacket
|
||||||
|
for resends := 0; resends < 3; resends++ {
|
||||||
|
var err error
|
||||||
|
p, err = s.readPacket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if p.typ != oraclePacketResend {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := sendConnect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch p.typ {
|
switch p.typ {
|
||||||
case oraclePacketAccept:
|
case oraclePacketAccept:
|
||||||
if len(p.raw) < 40 {
|
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.acfl0 = p.raw[22]
|
||||||
s.acfl1 = p.raw[23]
|
s.acfl1 = p.raw[23]
|
||||||
|
if s.version >= 315 {
|
||||||
s.handshakeComplete = true
|
s.handshakeComplete = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
case oraclePacketRefuse:
|
case oraclePacketRefuse:
|
||||||
return oracleRefuseError(p.raw)
|
return oracleRefuseError(p.raw)
|
||||||
@@ -658,22 +676,45 @@ func toUint64(v interface{}) uint64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *oracleSession) advancedNegotiation() error {
|
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.reset()
|
||||||
s.writeANOHeader(101, 4, 0)
|
s.writeANOHeader(13+len(body), 4, 0)
|
||||||
s.writeANOServiceHeader(4, 3)
|
s.putBytes(body...)
|
||||||
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})
|
|
||||||
if err := s.writeData(); err != nil {
|
if err := s.writeData(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,3 +201,64 @@ openldap:
|
|||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8025/api/v2/messages || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8025/api/v2/messages || exit 1"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 10
|
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
|
||||||
|
|||||||
@@ -427,6 +427,131 @@ func TestSMTPServiceDetect(t *testing.T) {
|
|||||||
t.Logf("smtp: type=%s banner=%s", result.Type, result.Banner)
|
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) {
|
func TestRedisConnectionRefused(t *testing.T) {
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user