From db1a66556d8cb789f8bc92e1331fcc5fcdbe0b8c Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Tue, 3 Mar 2026 15:47:42 -0600 Subject: [PATCH 1/2] Allow TLS scanners to downgrade to plaintext if handshake fails Especially for banner, when we have less of an expectation for WHAT might be listening, being able to try a TLS handshake, and downgrading to plaintext spares me from having to run a seperate scan or having to do a more complicated "multiple" module setup just to capture a large mix of errors --- integration_tests/amqp091/test.sh | 15 +++++++ integration_tests/mqtt/test.sh | 4 ++ integration_tests/redis/test.sh | 2 + module.go | 39 +++++++++++++++++ modules/amqp091/scanner.go | 27 ++++++++++-- modules/banner/scanner.go | 70 +++++++++++++++++++------------ modules/fox/log.go | 3 +- modules/fox/scanner.go | 25 +++++++++-- modules/mqtt/scanner.go | 24 +++++++++-- modules/redis/scanner.go | 46 ++++++++++++++++---- zgrab2_schemas/zgrab2/amqp091.py | 1 + zgrab2_schemas/zgrab2/banner.py | 6 +++ zgrab2_schemas/zgrab2/fox.py | 1 + zgrab2_schemas/zgrab2/mqtt.py | 1 + zgrab2_schemas/zgrab2/redis.py | 2 + 15 files changed, 219 insertions(+), 47 deletions(-) diff --git a/integration_tests/amqp091/test.sh b/integration_tests/amqp091/test.sh index 83ff92c38..ba9d544cc 100755 --- a/integration_tests/amqp091/test.sh +++ b/integration_tests/amqp091/test.sh @@ -57,4 +57,19 @@ for version in $VERSIONS; do doTest $version "--auth-user guest --auth-pass guest" done +# TLS downgrade test: AMQP containers don't serve TLS on default port, so downgrade should fall back to plaintext +for version in $VERSIONS; do + CONTAINER_NAME="zgrab_amqp091-${version}" + OUTPUT_FILE="$ZGRAB_OUTPUT/amqp091/${version}-tls-downgrade.json" + echo "amqp091/test: Testing TLS downgrade on RabbitMQ ${version}..." + CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh amqp091 --use-tls --allow-tls-downgrade --target-timeout 10s >$OUTPUT_FILE + SERVER_VERSION=$(jp -u data.amqp091.result.server_properties.version <$OUTPUT_FILE) + if [[ "$SERVER_VERSION" == "$version" ]]; then + echo "amqp091/test: TLS downgrade test passed: server version $SERVER_VERSION" + else + echo "amqp091/test: TLS downgrade test failed: Got $SERVER_VERSION, expected $version" + status=1 + fi +done + exit $status diff --git a/integration_tests/mqtt/test.sh b/integration_tests/mqtt/test.sh index 1fa88d35d..02032a914 100755 --- a/integration_tests/mqtt/test.sh +++ b/integration_tests/mqtt/test.sh @@ -23,6 +23,10 @@ CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh mqtt --v5 CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh mqtt --tls -p 8883 > $ZGRAB_OUTPUT/mqtt/mqtt_tls.json CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh mqtt --tls --v5 -p 8883 > $ZGRAB_OUTPUT/mqtt/mqtt_tls_v5.json +# TLS downgrade tests +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh mqtt --tls --allow-tls-downgrade -p 1883 > $ZGRAB_OUTPUT/mqtt/tls_downgrade_plain_port.json +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh mqtt --tls --allow-tls-downgrade -p 8883 > $ZGRAB_OUTPUT/mqtt/tls_downgrade_tls_port.json + # Dump the docker logs echo "mqtt/test: BEGIN docker logs from $CONTAINER_NAME [{(" docker logs --tail all $CONTAINER_NAME diff --git a/integration_tests/redis/test.sh b/integration_tests/redis/test.sh index 18d41b529..ac5bc4cb0 100755 --- a/integration_tests/redis/test.sh +++ b/integration_tests/redis/test.sh @@ -21,6 +21,8 @@ for cfg in $configs; do echo "redis/test: Testing $CONTAINER_NAME" CONTAINER_NAME=$CONTAINER_NAME "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis > "$ZGRAB_OUTPUT/redis/${cfg}-normal.json" CONTAINER_NAME=$CONTAINER_NAME "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis --inline > "$ZGRAB_OUTPUT/redis/${cfg}-inline.json" + # TLS downgrade: Redis containers don't support TLS, so --allow-tls-downgrade should fall back to plaintext + CONTAINER_NAME=$CONTAINER_NAME "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis --use-tls --allow-tls-downgrade > "$ZGRAB_OUTPUT/redis/${cfg}-tls-downgrade.json" # CONTAINER_NAME=$CONTAINER_NAME EXTRA_DOCKER_ARGS=$EXTRA_DOCKER_ARGS "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis --mappings "$MOUNT_CONTAINER/mappings.json" > "$ZGRAB_OUTPUT/redis/${cfg}-normal-mappings.json" # CONTAINER_NAME=$CONTAINER_NAME EXTRA_DOCKER_ARGS=$EXTRA_DOCKER_ARGS "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis --inline --mappings "$MOUNT_CONTAINER/mappings.yaml" > "$ZGRAB_OUTPUT/redis/${cfg}-inline-mappings.json" # CONTAINER_NAME=$CONTAINER_NAME EXTRA_DOCKER_ARGS=$EXTRA_DOCKER_ARGS "$ZGRAB_ROOT/docker-runner/docker-run.sh" redis --custom-commands "$MOUNT_CONTAINER/extra-commands.json" > "$ZGRAB_OUTPUT/redis/${cfg}-normal-extra.json" diff --git a/module.go b/module.go index f409e7b7a..da1934c45 100644 --- a/module.go +++ b/module.go @@ -203,6 +203,45 @@ func (d *DialerGroup) GetTLSDialer(ctx context.Context, t *ScanTarget) func(netw } } +// DialTLSDowngrade dials a TCP connection and attempts a TLS handshake. If the +// handshake fails and allowDowngrade is true, the poisoned connection is closed +// and a fresh plaintext TCP connection is returned instead. Returns the +// connection, whether TLS was successfully negotiated, and any error. +func (d *DialerGroup) DialTLSDowngrade(ctx context.Context, target *ScanTarget, allowDowngrade bool) (net.Conn, bool, error) { + if d.L4Dialer == nil { + return nil, false, errors.New("no L4 dialer set") + } + if d.TLSWrapper == nil { + return nil, false, errors.New("no TLS wrapper set") + } + + addr := net.JoinHostPort(target.Host(), strconv.Itoa(int(target.Port))) + + conn, err := d.L4Dialer(target)(ctx, "tcp", addr) + if err != nil { + return nil, false, fmt.Errorf("failed to connect to %v: %w", target.String(), err) + } + + tlsConn, tlsErr := d.TLSWrapper(ctx, target, conn) + if tlsErr == nil { + return tlsConn, true, nil + } + + // TLS handshake failed + if !allowDowngrade { + CloseConnAndHandleError(conn) + return nil, false, fmt.Errorf("TLS handshake failed for %v: %w", target.String(), tlsErr) + } + + // Handshake poisoned the connection; reconnect plaintext + CloseConnAndHandleError(conn) + conn, err = d.L4Dialer(target)(ctx, "tcp", addr) + if err != nil { + return nil, false, fmt.Errorf("failed to reconnect to %v after TLS downgrade: %w", target.String(), err) + } + return conn, false, nil +} + // ScanResponse is the result of a scan on a single host type ScanResponse struct { // Status is required for all responses. diff --git a/modules/amqp091/scanner.go b/modules/amqp091/scanner.go index d240dda0f..04871839f 100644 --- a/modules/amqp091/scanner.go +++ b/modules/amqp091/scanner.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net" amqpLib "github.com/rabbitmq/amqp091-go" log "github.com/sirupsen/logrus" @@ -20,8 +21,9 @@ type Flags struct { AuthUser string `long:"auth-user" description:"Username to use for authentication. Must be used with --auth-pass. No auth is attempted if not provided."` AuthPass string `long:"auth-pass" description:"Password to use for authentication. Must be used with --auth-user. No auth is attempted if not provided."` - UseTLS bool `long:"use-tls" description:"Use TLS to connect to the server. Note that AMQPS uses a different default port (5671) than AMQP (5672) and you will need to specify that port manually with -p."` - zgrab2.TLSFlags `group:"TLS Options"` + UseTLS bool `long:"use-tls" description:"Use TLS to connect to the server. Note that AMQPS uses a different default port (5671) than AMQP (5672) and you will need to specify that port manually with -p."` + AllowTLSDowngrade bool `long:"allow-tls-downgrade" description:"If --use-tls is enabled and the TLS handshake fails, fall back to plaintext instead of aborting. Requires --use-tls."` + zgrab2.TLSFlags `group:"TLS Options"` } // Module implements the zgrab2.Module interface. @@ -91,7 +93,8 @@ type Result struct { Tune *connectionTune `json:"tune,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSUsed bool `json:"tls_used,omitempty"` } // RegisterModule registers the zgrab2 module. @@ -128,6 +131,9 @@ func (flags *Flags) Validate(_ []string) error { if flags.AuthPass != "" && flags.AuthUser == "" { return errors.New("must provide --auth-user if --auth-pass is set") } + if flags.AllowTLSDowngrade && !flags.UseTLS { + return errors.New("--allow-tls-downgrade requires --use-tls") + } return nil } @@ -149,6 +155,7 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { BaseFlags: &f.BaseFlags, TLSEnabled: f.UseTLS, TLSFlags: &f.TLSFlags, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, } return nil } @@ -183,7 +190,18 @@ func (scanner *Scanner) GetScanMetadata() any { } func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { - conn, err := dialGroup.Dial(ctx, target) + var ( + conn net.Conn + err error + tlsUsed bool + ) + + if scanner.config.AllowTLSDowngrade { + conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + } else { + conn, err = dialGroup.Dial(ctx, target) + tlsUsed = scanner.config.UseTLS + } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("unable to dial target (%v): %w", target.String(), err) } @@ -191,6 +209,7 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, // Setup result and connection cleanup result := &Result{ AuthSuccess: false, + TLSUsed: tlsUsed, } defer func() { if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { diff --git a/modules/banner/scanner.go b/modules/banner/scanner.go index c7fbb749b..ffa592fa0 100644 --- a/modules/banner/scanner.go +++ b/modules/banner/scanner.go @@ -29,19 +29,20 @@ type Flags struct { zgrab2.BaseFlags `group:"Basic Options"` zgrab2.TLSFlags `group:"TLS Options"` - ReadTimeout int `long:"read-timeout" default:"10" description:"Read timeout in milliseconds"` - BufferSize int `long:"buffer-size" default:"8209" description:"Read buffer size in bytes"` - MaxReadSize int `long:"max-read-size" default:"512" description:"Maximum amount of data to read in KiB (1024 bytes)"` - Probe string `long:"probe" default:"\\n" description:"Probe to send to the server. Use triple slashes to escape, for example \\\\\\n is literal \\n. Mutually exclusive with --probe-file."` - ProbeFile string `long:"probe-file" description:"Read probe from file as byte array (hex). Mutually exclusive with --probe."` - Pattern string `long:"pattern" description:"Pattern to match, must be valid regexp."` - UseTLS bool `long:"tls" description:"Sends probe with TLS connection. Loads TLS module command options."` - MaxTries int `long:"max-tries" default:"1" description:"Number of tries for timeouts and connection errors before giving up. Includes making TLS connection if enabled."` - Hex bool `long:"hex" description:"Store banner value in hex. Mutually exclusive with --base64."` - Base64 bool `long:"base64" description:"Store banner value in base64. Mutually exclusive with --hex."` - MD5 bool `long:"md5" description:"Calculate MD5 hash of banner value."` - SHA1 bool `long:"sha1" description:"Calculate SHA1 hash of banner value."` - SHA256 bool `long:"sha256" description:"Calculate SHA256 hash of banner value."` + ReadTimeout int `long:"read-timeout" default:"10" description:"Read timeout in milliseconds"` + BufferSize int `long:"buffer-size" default:"8209" description:"Read buffer size in bytes"` + MaxReadSize int `long:"max-read-size" default:"512" description:"Maximum amount of data to read in KiB (1024 bytes)"` + Probe string `long:"probe" default:"\\n" description:"Probe to send to the server. Use triple slashes to escape, for example \\\\\\n is literal \\n. Mutually exclusive with --probe-file."` + ProbeFile string `long:"probe-file" description:"Read probe from file as byte array (hex). Mutually exclusive with --probe."` + Pattern string `long:"pattern" description:"Pattern to match, must be valid regexp."` + UseTLS bool `long:"tls" description:"Sends probe with TLS connection. Loads TLS module command options."` + AllowTLSDowngrade bool `long:"allow-tls-downgrade" description:"If --tls is enabled and the TLS handshake fails, fall back to plaintext instead of aborting. Requires --tls."` + MaxTries int `long:"max-tries" default:"1" description:"Number of tries for timeouts and connection errors before giving up. Includes making TLS connection if enabled."` + Hex bool `long:"hex" description:"Store banner value in hex. Mutually exclusive with --base64."` + Base64 bool `long:"base64" description:"Store banner value in base64. Mutually exclusive with --hex."` + MD5 bool `long:"md5" description:"Calculate MD5 hash of banner value."` + SHA1 bool `long:"sha1" description:"Calculate SHA1 hash of banner value."` + SHA256 bool `long:"sha256" description:"Calculate SHA256 hash of banner value."` } // Module is the implementation of the zgrab2.Module interface. @@ -58,12 +59,13 @@ type Scanner struct { // ScanResults instances are returned by the module's Scan function. type Results struct { - Banner string `json:"banner,omitempty"` - Length int `json:"length,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - MD5 string `json:"md5,omitempty"` - SHA1 string `json:"sha1,omitempty"` - SHA256 string `json:"sha256,omitempty"` + Banner string `json:"banner,omitempty"` + Length int `json:"length,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSUsed bool `json:"tls_used,omitempty"` + MD5 string `json:"md5,omitempty"` + SHA1 string `json:"sha1,omitempty"` + SHA256 string `json:"sha256,omitempty"` } var ErrNoMatch = errors.New("pattern did not match") @@ -117,6 +119,10 @@ func (f *Flags) Validate(_ []string) error { log.Fatal("Cannot set both --probe and --probe-file") return zgrab2.ErrInvalidArguments } + if f.AllowTLSDowngrade && !f.UseTLS { + log.Fatal("--allow-tls-downgrade requires --tls") + return zgrab2.ErrInvalidArguments + } return nil } @@ -160,6 +166,7 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { TransportAgnosticDialerProtocol: zgrab2.TransportTCP, BaseFlags: &f.BaseFlags, TLSEnabled: f.UseTLS, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, } if f.UseTLS { scanner.dialerGroupConfig.TLSFlags = &f.TLSFlags @@ -175,16 +182,27 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, results Results ) - for try := 0; try < scanner.config.MaxTries; try++ { - conn, err = dialGroup.Dial(ctx, target) + if scanner.config.AllowTLSDowngrade { + var tlsUsed bool + conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) if err != nil { - continue // try again + return zgrab2.TryGetScanStatus(err), nil, err } - break - } - if err != nil { - return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("failed to connect to %v: %w", target.String(), err) + results.TLSUsed = tlsUsed + } else { + for try := 0; try < scanner.config.MaxTries; try++ { + conn, err = dialGroup.Dial(ctx, target) + if err != nil { + continue + } + break + } + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("failed to connect to %v: %w", target.String(), err) + } + results.TLSUsed = scanner.config.UseTLS } + defer func() { // attempt to collect TLS Log if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { diff --git a/modules/fox/log.go b/modules/fox/log.go index d715d0ba8..b5021abdc 100644 --- a/modules/fox/log.go +++ b/modules/fox/log.go @@ -63,5 +63,6 @@ type FoxLog struct { // AuthAgentType corresponds to the "authAgentTypeSpecs" field. AuthAgentType string `json:"auth_agent_type,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSUsed bool `json:"tls_used,omitempty"` } diff --git a/modules/fox/scanner.go b/modules/fox/scanner.go index 15d140fed..6148a290c 100644 --- a/modules/fox/scanner.go +++ b/modules/fox/scanner.go @@ -19,9 +19,10 @@ import ( // Flags holds the command-line configuration for the fox scan module. // Populated by the framework. type Flags struct { - zgrab2.BaseFlags `group:"Basic Options"` - zgrab2.TLSFlags `group:"TLS Options"` - UseTLS bool `long:"use-tls" description:"Sends probe with a TLS connection. Loads TLS module command options."` + zgrab2.BaseFlags `group:"Basic Options"` + zgrab2.TLSFlags `group:"TLS Options"` + UseTLS bool `long:"use-tls" description:"Sends probe with a TLS connection. Loads TLS module command options."` + AllowTLSDowngrade bool `long:"allow-tls-downgrade" description:"If --use-tls is enabled and the TLS handshake fails, fall back to plaintext instead of aborting. Requires --use-tls."` } // Module implements the zgrab2.Module interface. @@ -62,6 +63,9 @@ func (module *Module) Description() string { // On success, returns nil. // On failure, returns an error instance describing the error. func (flags *Flags) Validate(_ []string) error { + if flags.AllowTLSDowngrade && !flags.UseTLS { + return errors.New("--allow-tls-downgrade requires --use-tls") + } return nil } @@ -79,6 +83,7 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { BaseFlags: &f.BaseFlags, TLSEnabled: scanner.config.UseTLS, TLSFlags: &f.TLSFlags, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, } return nil } @@ -119,7 +124,18 @@ func (scanner *Scanner) GetScanMetadata() any { // 4. If the response has the Fox response prefix, mark the scan as having detected the service. // 5. Attempt to read any / all of the data fields from the Log struct func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { - conn, err := dialGroup.Dial(ctx, target) + var ( + conn net.Conn + err error + tlsUsed bool + ) + + if scanner.config.AllowTLSDowngrade { + conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + } else { + conn, err = dialGroup.Dial(ctx, target) + tlsUsed = scanner.config.UseTLS + } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("unable to dial target (%s): %w", target.String(), err) } @@ -128,6 +144,7 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, zgrab2.CloseConnAndHandleError(conn) }(conn) result := new(FoxLog) + result.TLSUsed = tlsUsed // Attempt to read TLS Log from connection. If it's not a TLS connection then the log will just be empty. if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { result.TLSLog = tlsConn.GetLog() diff --git a/modules/mqtt/scanner.go b/modules/mqtt/scanner.go index e76874d76..419ff30fe 100644 --- a/modules/mqtt/scanner.go +++ b/modules/mqtt/scanner.go @@ -19,6 +19,7 @@ type ScanResults struct { ConnectReturnCode byte `json:"connect_return_code,omitempty"` Response string `json:"response,omitempty"` TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + TLSUsed bool `json:"tls_used,omitempty"` } // Flags are the MQTT-specific command-line flags. @@ -26,8 +27,9 @@ type Flags struct { zgrab2.BaseFlags zgrab2.TLSFlags - V5 bool `long:"v5" description:"Scanning MQTT v5.0. Otherwise scanning MQTT v3.1.1"` - UseTLS bool `long:"tls" description:"Use TLS for the MQTT connection"` + V5 bool `long:"v5" description:"Scanning MQTT v5.0. Otherwise scanning MQTT v3.1.1"` + UseTLS bool `long:"tls" description:"Use TLS for the MQTT connection"` + AllowTLSDowngrade bool `long:"allow-tls-downgrade" description:"If --tls is enabled and the TLS handshake fails, fall back to plaintext instead of aborting. Requires --tls."` } // Module implements the zgrab2.Module interface. @@ -75,6 +77,9 @@ func (m *Module) Description() string { // Validate flags func (f *Flags) Validate(_ []string) error { + if f.AllowTLSDowngrade && !f.UseTLS { + return errors.New("--allow-tls-downgrade requires --tls") + } return nil } @@ -106,6 +111,7 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { BaseFlags: &f.BaseFlags, TLSEnabled: f.UseTLS, TLSFlags: &f.TLSFlags, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, } return nil } @@ -299,13 +305,25 @@ func readVariableByteInteger(r io.Reader) ([]byte, error) { // Scan performs the configured scan on the MQTT server. func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { - conn, err := dialGroup.Dial(ctx, target) + var ( + conn net.Conn + err error + tlsUsed bool + ) + + if scanner.config.AllowTLSDowngrade { + conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + } else { + conn, err = dialGroup.Dial(ctx, target) + tlsUsed = scanner.config.UseTLS + } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("error opening connection to target %s: %w", target.String(), err) } defer zgrab2.CloseConnAndHandleError(conn) mqtt := Connection{conn: conn, config: scanner.config} + mqtt.results.TLSUsed = tlsUsed if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { // if the passed in connection is a TLS connection, try to grab the log diff --git a/modules/redis/scanner.go b/modules/redis/scanner.go index 551a6ae41..d6fa31a1e 100644 --- a/modules/redis/scanner.go +++ b/modules/redis/scanner.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "io" + "net" "os" "path/filepath" "strconv" @@ -33,13 +34,14 @@ import ( type Flags struct { zgrab2.BaseFlags `group:"Basic Options"` - CustomCommands string `long:"custom-commands" description:"Pathname for JSON/YAML file that contains extra commands to execute. WARNING: This is sent in the clear."` - Mappings string `long:"mappings" description:"Pathname for JSON/YAML file that contains mappings for command names."` - MaxInputFileSize int64 `long:"max-input-file-size" default:"102400" description:"Maximum size for either input file."` - Password string `long:"password" description:"Set a password to use to authenticate to the server. WARNING: This is sent in the clear."` - DoInline bool `long:"inline" description:"Send commands using the inline syntax"` - UseTLS bool `long:"use-tls" description:"Sends probe with a TLS connection. Loads TLS module command options."` - zgrab2.TLSFlags `group:"TLS Options"` + CustomCommands string `long:"custom-commands" description:"Pathname for JSON/YAML file that contains extra commands to execute. WARNING: This is sent in the clear."` + Mappings string `long:"mappings" description:"Pathname for JSON/YAML file that contains mappings for command names."` + MaxInputFileSize int64 `long:"max-input-file-size" default:"102400" description:"Maximum size for either input file."` + Password string `long:"password" description:"Set a password to use to authenticate to the server. WARNING: This is sent in the clear."` + DoInline bool `long:"inline" description:"Send commands using the inline syntax"` + UseTLS bool `long:"use-tls" description:"Sends probe with a TLS connection. Loads TLS module command options."` + AllowTLSDowngrade bool `long:"allow-tls-downgrade" description:"If --use-tls is enabled and the TLS handshake fails, fall back to plaintext instead of aborting. Requires --use-tls."` + zgrab2.TLSFlags `group:"TLS Options"` } // Module implements the zgrab2.Module interface @@ -161,6 +163,9 @@ type Result struct { // TLSLog is the standard TLS log for the connection if used TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + + // TLSUsed indicates whether TLS was successfully negotiated. + TLSUsed bool `json:"tls_used,omitempty"` } // RegisterModule registers the zgrab2 module @@ -189,6 +194,9 @@ func (module *Module) Description() string { // Validate checks that the flags are valid func (flags *Flags) Validate(_ []string) error { + if flags.AllowTLSDowngrade && !flags.UseTLS { + return errors.New("--allow-tls-downgrade requires --use-tls") + } return nil } @@ -210,6 +218,7 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { BaseFlags: &f.BaseFlags, TLSFlags: &f.TLSFlags, TLSEnabled: f.UseTLS, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, } return nil } @@ -325,15 +334,34 @@ func (scan *scan) SendCommand(cmd string, args ...string) (RedisValue, error) { // StartScan opens a connection to the target and sets up a scan instance for it func (scanner *Scanner) StartScan(ctx context.Context, target *zgrab2.ScanTarget, dialGroup *zgrab2.DialerGroup) (*scan, error) { - conn, err := dialGroup.Dial(ctx, target) + var ( + conn net.Conn + err error + tlsUsed bool + ) + + if scanner.config.AllowTLSDowngrade { + conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + } else { + conn, err = dialGroup.Dial(ctx, target) + tlsUsed = scanner.config.UseTLS + } if err != nil { return nil, fmt.Errorf("could not establish connection to %s: %w", target.String(), err) } + result := &Result{} + result.TLSUsed = tlsUsed + if tlsUsed { + if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { + result.TLSLog = tlsConn.GetLog() + } + } + return &scan{ target: target, scanner: scanner, - result: &Result{}, + result: result, conn: &Connection{ scanner: scanner, conn: conn, diff --git a/zgrab2_schemas/zgrab2/amqp091.py b/zgrab2_schemas/zgrab2/amqp091.py index 842e301ba..4a727ab48 100644 --- a/zgrab2_schemas/zgrab2/amqp091.py +++ b/zgrab2_schemas/zgrab2/amqp091.py @@ -40,6 +40,7 @@ "auth_success": Boolean(), "tune": connection_tune, "tls": zgrab2.tls_log, + "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) }, diff --git a/zgrab2_schemas/zgrab2/banner.py b/zgrab2_schemas/zgrab2/banner.py index 5092a17ec..3a646107c 100644 --- a/zgrab2_schemas/zgrab2/banner.py +++ b/zgrab2_schemas/zgrab2/banner.py @@ -15,6 +15,12 @@ "banner": String(), "length": Unsigned32BitInteger(), "tls": zgrab2.tls_log, + "tls_used": Boolean( + doc="Whether TLS was successfully negotiated. Only set when --tls is used." + ), + "md5": String(), + "sha1": String(), + "sha256": String(), } ) }, diff --git a/zgrab2_schemas/zgrab2/fox.py b/zgrab2_schemas/zgrab2/fox.py index fcf0ee0ee..7143ecc8b 100644 --- a/zgrab2_schemas/zgrab2/fox.py +++ b/zgrab2_schemas/zgrab2/fox.py @@ -31,6 +31,7 @@ "sys_info": String(), "agent_auth_type": String(), "tls": zgrab2.tls_log, + "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) }, diff --git a/zgrab2_schemas/zgrab2/mqtt.py b/zgrab2_schemas/zgrab2/mqtt.py index 100da0972..82d754e4b 100644 --- a/zgrab2_schemas/zgrab2/mqtt.py +++ b/zgrab2_schemas/zgrab2/mqtt.py @@ -13,6 +13,7 @@ "connect_return_code": Unsigned32BitInteger(), "response": String(), "tls": zgrab2.tls_log, + "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) diff --git a/zgrab2_schemas/zgrab2/redis.py b/zgrab2_schemas/zgrab2/redis.py index e53f3d73d..7d09f326c 100644 --- a/zgrab2_schemas/zgrab2/redis.py +++ b/zgrab2_schemas/zgrab2/redis.py @@ -102,6 +102,8 @@ ), doc="The responses from the user-passed custom commands.", ), + "tls": zgrab2.tls_log, + "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) }, From f36acab0405c1f0a0ded6f40e8ef39336eebb59f Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Wed, 4 Mar 2026 22:34:19 -0600 Subject: [PATCH 2/2] Remove tls_used The existence of the tls_log should be sufficient --- modules/amqp091/scanner.go | 12 ++++-------- modules/banner/scanner.go | 18 +++++++----------- modules/fox/log.go | 3 +-- modules/fox/scanner.go | 9 +++------ modules/mqtt/scanner.go | 10 +++------- modules/redis/scanner.go | 18 +++++------------- zgrab2_schemas/zgrab2/amqp091.py | 1 - zgrab2_schemas/zgrab2/banner.py | 3 --- zgrab2_schemas/zgrab2/fox.py | 1 - zgrab2_schemas/zgrab2/mqtt.py | 1 - zgrab2_schemas/zgrab2/redis.py | 1 - 11 files changed, 23 insertions(+), 54 deletions(-) diff --git a/modules/amqp091/scanner.go b/modules/amqp091/scanner.go index 04871839f..4ebfa7087 100644 --- a/modules/amqp091/scanner.go +++ b/modules/amqp091/scanner.go @@ -93,8 +93,7 @@ type Result struct { Tune *connectionTune `json:"tune,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - TLSUsed bool `json:"tls_used,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` } // RegisterModule registers the zgrab2 module. @@ -191,16 +190,14 @@ func (scanner *Scanner) GetScanMetadata() any { func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { var ( - conn net.Conn - err error - tlsUsed bool + conn net.Conn + err error ) if scanner.config.AllowTLSDowngrade { - conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) } else { conn, err = dialGroup.Dial(ctx, target) - tlsUsed = scanner.config.UseTLS } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("unable to dial target (%v): %w", target.String(), err) @@ -209,7 +206,6 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, // Setup result and connection cleanup result := &Result{ AuthSuccess: false, - TLSUsed: tlsUsed, } defer func() { if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { diff --git a/modules/banner/scanner.go b/modules/banner/scanner.go index ffa592fa0..9de3e98a9 100644 --- a/modules/banner/scanner.go +++ b/modules/banner/scanner.go @@ -59,13 +59,12 @@ type Scanner struct { // ScanResults instances are returned by the module's Scan function. type Results struct { - Banner string `json:"banner,omitempty"` - Length int `json:"length,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - TLSUsed bool `json:"tls_used,omitempty"` - MD5 string `json:"md5,omitempty"` - SHA1 string `json:"sha1,omitempty"` - SHA256 string `json:"sha256,omitempty"` + Banner string `json:"banner,omitempty"` + Length int `json:"length,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + MD5 string `json:"md5,omitempty"` + SHA1 string `json:"sha1,omitempty"` + SHA256 string `json:"sha256,omitempty"` } var ErrNoMatch = errors.New("pattern did not match") @@ -183,12 +182,10 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, ) if scanner.config.AllowTLSDowngrade { - var tlsUsed bool - conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) if err != nil { return zgrab2.TryGetScanStatus(err), nil, err } - results.TLSUsed = tlsUsed } else { for try := 0; try < scanner.config.MaxTries; try++ { conn, err = dialGroup.Dial(ctx, target) @@ -200,7 +197,6 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("failed to connect to %v: %w", target.String(), err) } - results.TLSUsed = scanner.config.UseTLS } defer func() { diff --git a/modules/fox/log.go b/modules/fox/log.go index b5021abdc..d715d0ba8 100644 --- a/modules/fox/log.go +++ b/modules/fox/log.go @@ -63,6 +63,5 @@ type FoxLog struct { // AuthAgentType corresponds to the "authAgentTypeSpecs" field. AuthAgentType string `json:"auth_agent_type,omitempty"` - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - TLSUsed bool `json:"tls_used,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` } diff --git a/modules/fox/scanner.go b/modules/fox/scanner.go index 6148a290c..d51675101 100644 --- a/modules/fox/scanner.go +++ b/modules/fox/scanner.go @@ -125,16 +125,14 @@ func (scanner *Scanner) GetScanMetadata() any { // 5. Attempt to read any / all of the data fields from the Log struct func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { var ( - conn net.Conn - err error - tlsUsed bool + conn net.Conn + err error ) if scanner.config.AllowTLSDowngrade { - conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) } else { conn, err = dialGroup.Dial(ctx, target) - tlsUsed = scanner.config.UseTLS } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("unable to dial target (%s): %w", target.String(), err) @@ -144,7 +142,6 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, zgrab2.CloseConnAndHandleError(conn) }(conn) result := new(FoxLog) - result.TLSUsed = tlsUsed // Attempt to read TLS Log from connection. If it's not a TLS connection then the log will just be empty. if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { result.TLSLog = tlsConn.GetLog() diff --git a/modules/mqtt/scanner.go b/modules/mqtt/scanner.go index 419ff30fe..6d9d605fb 100644 --- a/modules/mqtt/scanner.go +++ b/modules/mqtt/scanner.go @@ -19,7 +19,6 @@ type ScanResults struct { ConnectReturnCode byte `json:"connect_return_code,omitempty"` Response string `json:"response,omitempty"` TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - TLSUsed bool `json:"tls_used,omitempty"` } // Flags are the MQTT-specific command-line flags. @@ -306,16 +305,14 @@ func readVariableByteInteger(r io.Reader) ([]byte, error) { // Scan performs the configured scan on the MQTT server. func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { var ( - conn net.Conn - err error - tlsUsed bool + conn net.Conn + err error ) if scanner.config.AllowTLSDowngrade { - conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) } else { conn, err = dialGroup.Dial(ctx, target) - tlsUsed = scanner.config.UseTLS } if err != nil { return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("error opening connection to target %s: %w", target.String(), err) @@ -323,7 +320,6 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, defer zgrab2.CloseConnAndHandleError(conn) mqtt := Connection{conn: conn, config: scanner.config} - mqtt.results.TLSUsed = tlsUsed if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { // if the passed in connection is a TLS connection, try to grab the log diff --git a/modules/redis/scanner.go b/modules/redis/scanner.go index d6fa31a1e..4b18e7474 100644 --- a/modules/redis/scanner.go +++ b/modules/redis/scanner.go @@ -163,9 +163,6 @@ type Result struct { // TLSLog is the standard TLS log for the connection if used TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` - - // TLSUsed indicates whether TLS was successfully negotiated. - TLSUsed bool `json:"tls_used,omitempty"` } // RegisterModule registers the zgrab2 module @@ -335,27 +332,22 @@ func (scan *scan) SendCommand(cmd string, args ...string) (RedisValue, error) { // StartScan opens a connection to the target and sets up a scan instance for it func (scanner *Scanner) StartScan(ctx context.Context, target *zgrab2.ScanTarget, dialGroup *zgrab2.DialerGroup) (*scan, error) { var ( - conn net.Conn - err error - tlsUsed bool + conn net.Conn + err error ) if scanner.config.AllowTLSDowngrade { - conn, tlsUsed, err = dialGroup.DialTLSDowngrade(ctx, target, true) + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) } else { conn, err = dialGroup.Dial(ctx, target) - tlsUsed = scanner.config.UseTLS } if err != nil { return nil, fmt.Errorf("could not establish connection to %s: %w", target.String(), err) } result := &Result{} - result.TLSUsed = tlsUsed - if tlsUsed { - if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { - result.TLSLog = tlsConn.GetLog() - } + if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { + result.TLSLog = tlsConn.GetLog() } return &scan{ diff --git a/zgrab2_schemas/zgrab2/amqp091.py b/zgrab2_schemas/zgrab2/amqp091.py index 4a727ab48..842e301ba 100644 --- a/zgrab2_schemas/zgrab2/amqp091.py +++ b/zgrab2_schemas/zgrab2/amqp091.py @@ -40,7 +40,6 @@ "auth_success": Boolean(), "tune": connection_tune, "tls": zgrab2.tls_log, - "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) }, diff --git a/zgrab2_schemas/zgrab2/banner.py b/zgrab2_schemas/zgrab2/banner.py index 3a646107c..eb3469235 100644 --- a/zgrab2_schemas/zgrab2/banner.py +++ b/zgrab2_schemas/zgrab2/banner.py @@ -15,9 +15,6 @@ "banner": String(), "length": Unsigned32BitInteger(), "tls": zgrab2.tls_log, - "tls_used": Boolean( - doc="Whether TLS was successfully negotiated. Only set when --tls is used." - ), "md5": String(), "sha1": String(), "sha256": String(), diff --git a/zgrab2_schemas/zgrab2/fox.py b/zgrab2_schemas/zgrab2/fox.py index 7143ecc8b..fcf0ee0ee 100644 --- a/zgrab2_schemas/zgrab2/fox.py +++ b/zgrab2_schemas/zgrab2/fox.py @@ -31,7 +31,6 @@ "sys_info": String(), "agent_auth_type": String(), "tls": zgrab2.tls_log, - "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) }, diff --git a/zgrab2_schemas/zgrab2/mqtt.py b/zgrab2_schemas/zgrab2/mqtt.py index 82d754e4b..100da0972 100644 --- a/zgrab2_schemas/zgrab2/mqtt.py +++ b/zgrab2_schemas/zgrab2/mqtt.py @@ -13,7 +13,6 @@ "connect_return_code": Unsigned32BitInteger(), "response": String(), "tls": zgrab2.tls_log, - "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) diff --git a/zgrab2_schemas/zgrab2/redis.py b/zgrab2_schemas/zgrab2/redis.py index 7d09f326c..3e92634e0 100644 --- a/zgrab2_schemas/zgrab2/redis.py +++ b/zgrab2_schemas/zgrab2/redis.py @@ -103,7 +103,6 @@ doc="The responses from the user-passed custom commands.", ), "tls": zgrab2.tls_log, - "tls_used": Boolean(doc="Whether TLS was successfully negotiated."), } ) },