mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
282 lines
7.2 KiB
Go
282 lines
7.2 KiB
Go
//go:build plugin_activemq || !plugin_selective
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shadow1ng/fscan/common"
|
|
"github.com/shadow1ng/fscan/common/i18n"
|
|
"github.com/shadow1ng/fscan/plugins"
|
|
)
|
|
|
|
// ActiveMQPlugin ActiveMQ扫描插件
|
|
type ActiveMQPlugin struct {
|
|
plugins.BasePlugin
|
|
}
|
|
|
|
func NewActiveMQPlugin() *ActiveMQPlugin {
|
|
return &ActiveMQPlugin{
|
|
BasePlugin: plugins.NewBasePlugin("activemq"),
|
|
}
|
|
}
|
|
|
|
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
|
config := session.Config
|
|
target := info.Target()
|
|
|
|
if config.DisableBrute {
|
|
return p.identifyService(ctx, info, session)
|
|
}
|
|
|
|
// 生成测试凭据
|
|
credentials := GenerateCredentials("activemq", config)
|
|
if len(credentials) == 0 {
|
|
// ActiveMQ默认凭据
|
|
credentials = []Credential{
|
|
{Username: "admin", Password: "admin"},
|
|
{Username: "admin", Password: ""},
|
|
{Username: "admin", Password: "password"},
|
|
{Username: "activemq", Password: "activemq"},
|
|
{Username: "activemq", Password: "admin"},
|
|
{Username: "user", Password: "user"},
|
|
{Username: "guest", Password: "guest"},
|
|
}
|
|
}
|
|
|
|
// 使用公共框架进行并发凭据测试
|
|
authFn := p.createAuthFunc(info, session)
|
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
|
|
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig)
|
|
|
|
if result.Success {
|
|
session.LogVuln(i18n.Tr("activemq_credential", target, result.Username, result.Password))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// createAuthFunc 创建ActiveMQ认证函数
|
|
func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
|
|
return func(ctx context.Context, cred Credential) *AuthResult {
|
|
return p.doActiveMQAuth(ctx, info, cred, session)
|
|
}
|
|
}
|
|
|
|
// doActiveMQAuth 执行ActiveMQ认证
|
|
func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
|
|
target := info.Target()
|
|
config := session.Config
|
|
timeout := config.Timeout
|
|
|
|
resultChan := make(chan *AuthResult, 1)
|
|
|
|
go func() {
|
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
|
if err != nil {
|
|
resultChan <- &AuthResult{
|
|
Success: false,
|
|
ErrorType: classifyActiveMQErrorType(err),
|
|
Error: err,
|
|
}
|
|
return
|
|
}
|
|
|
|
success, err := p.authenticateSTOMP(conn, cred.Username, cred.Password, config)
|
|
if success {
|
|
resultChan <- &AuthResult{
|
|
Success: true,
|
|
Conn: &activeMQConnWrapper{conn},
|
|
ErrorType: ErrorTypeUnknown,
|
|
Error: nil,
|
|
}
|
|
return
|
|
}
|
|
|
|
_ = conn.Close()
|
|
resultChan <- &AuthResult{
|
|
Success: false,
|
|
ErrorType: classifyActiveMQErrorType(err),
|
|
Error: err,
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case result := <-resultChan:
|
|
return result
|
|
case <-ctx.Done():
|
|
go func() {
|
|
result := <-resultChan
|
|
if result != nil && result.Conn != nil {
|
|
_ = result.Conn.Close()
|
|
}
|
|
}()
|
|
return &AuthResult{
|
|
Success: false,
|
|
ErrorType: ErrorTypeNetwork,
|
|
Error: ctx.Err(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// activeMQConnWrapper 包装ActiveMQ连接以实现io.Closer
|
|
type activeMQConnWrapper struct {
|
|
conn net.Conn
|
|
}
|
|
|
|
func (w *activeMQConnWrapper) Close() error {
|
|
return w.conn.Close()
|
|
}
|
|
|
|
// classifyActiveMQErrorType ActiveMQ错误分类
|
|
func classifyActiveMQErrorType(err error) ErrorType {
|
|
if err == nil {
|
|
return ErrorTypeUnknown
|
|
}
|
|
|
|
activeMQAuthErrors := []string{
|
|
"authentication failed",
|
|
"access denied",
|
|
"invalid credentials",
|
|
"login failed",
|
|
"unauthorized",
|
|
"403 forbidden",
|
|
"security exception",
|
|
"invalid user",
|
|
"invalid password",
|
|
"login incorrect",
|
|
}
|
|
|
|
return ClassifyError(err, activeMQAuthErrors, CommonNetworkErrors)
|
|
}
|
|
|
|
// authenticateSTOMP 使用STOMP协议认证ActiveMQ
|
|
func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password string, config *common.Config) (bool, error) {
|
|
timeout := config.Timeout
|
|
if err := rejectLineBreaks(username, password); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00",
|
|
username, password)
|
|
|
|
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
|
if _, err := conn.Write([]byte(stompConnect)); err != nil {
|
|
return false, fmt.Errorf("%s: %w", i18n.GetText("activemq_stomp_send_failed"), err)
|
|
}
|
|
|
|
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
|
response := make([]byte, 1024)
|
|
n, err := conn.Read(response)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s: %w", i18n.GetText("activemq_stomp_read_failed"), err)
|
|
}
|
|
if n == 0 {
|
|
return false, fmt.Errorf("%s", i18n.GetText("activemq_stomp_empty_response"))
|
|
}
|
|
|
|
responseStr := string(response[:n])
|
|
|
|
if strings.Contains(responseStr, "CONNECTED") {
|
|
return true, nil
|
|
} else if strings.Contains(responseStr, "ERROR") {
|
|
errorMsg := i18n.GetText("activemq_stomp_auth_error")
|
|
if strings.Contains(responseStr, "Authentication failed") {
|
|
errorMsg = "Authentication failed"
|
|
} else if strings.Contains(responseStr, "Access denied") {
|
|
errorMsg = "Access denied"
|
|
} else if strings.Contains(responseStr, "Invalid credentials") {
|
|
errorMsg = "Invalid credentials"
|
|
}
|
|
return false, fmt.Errorf("%s", errorMsg)
|
|
}
|
|
|
|
return false, fmt.Errorf("%s", i18n.GetText("activemq_stomp_unknown_response"))
|
|
}
|
|
|
|
// identifyService ActiveMQ服务识别
|
|
func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
|
target := info.Target()
|
|
timeout := session.Config.Timeout
|
|
|
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
|
if err != nil {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "activemq",
|
|
Error: err,
|
|
}
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
stompConnect := "CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\n\n\x00"
|
|
|
|
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
|
if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "activemq",
|
|
Error: fmt.Errorf("STOMP request send failed: %w", writeErr),
|
|
}
|
|
}
|
|
|
|
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
|
response := make([]byte, 512)
|
|
n, err := conn.Read(response)
|
|
if err != nil {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "activemq",
|
|
Error: fmt.Errorf("failed to read response: %w", err),
|
|
}
|
|
}
|
|
if n == 0 {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "activemq",
|
|
Error: fmt.Errorf("%s", i18n.GetText("activemq_stomp_empty_response")),
|
|
}
|
|
}
|
|
|
|
responseStr := string(response[:n])
|
|
|
|
if common.ContainsAny(responseStr, "CONNECTED", "ERROR") {
|
|
banner := "ActiveMQ STOMP"
|
|
if strings.Contains(responseStr, "server:") {
|
|
lines := strings.Split(responseStr, "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "server:") {
|
|
banner = strings.TrimSpace(strings.TrimPrefix(line, "server:"))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
session.LogSuccess(i18n.Tr("activemq_service", target, banner))
|
|
|
|
return &ScanResult{
|
|
Success: true,
|
|
Type: plugins.ResultTypeService,
|
|
Service: "activemq",
|
|
Banner: banner,
|
|
}
|
|
}
|
|
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "activemq",
|
|
Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "ActiveMQ STOMP")),
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
RegisterPluginWithPorts("activemq", func() Plugin {
|
|
return NewActiveMQPlugin()
|
|
}, []int{61613, 61614, 61616, 61617, 61618, 8161})
|
|
}
|