diff --git a/integration_tests/docker-compose.yml b/integration_tests/docker-compose.yml index e0ca793ba..ce65e0428 100644 --- a/integration_tests/docker-compose.yml +++ b/integration_tests/docker-compose.yml @@ -531,6 +531,16 @@ services: depends_on: - service_base + telnet-tls: + container_name: "zgrab_telnet_tls" + build: + context: ./telnet/tls-container + networks: + - telnet-tls-network + hostname: "target" + depends_on: + - service_base + networks: amqp091-3.12.14-network: driver: bridge @@ -747,6 +757,11 @@ networks: ipam: config: - subnet: 100.64.0.176/30 + telnet-tls-network: + driver: bridge + ipam: + config: + - subnet: 100.64.0.196/30 managesieve-network: driver: bridge ipam: diff --git a/integration_tests/telnet/test.sh b/integration_tests/telnet/test.sh index 75c032674..521c6b225 100755 --- a/integration_tests/telnet/test.sh +++ b/integration_tests/telnet/test.sh @@ -7,16 +7,33 @@ ZGRAB_OUTPUT=$ZGRAB_ROOT/zgrab-output mkdir -p $ZGRAB_OUTPUT/telnet -CONTAINER_NAME=zgrab_telnet +OUTPUT_ROOT="$ZGRAB_OUTPUT/telnet" -OUTPUT_FILE=$ZGRAB_OUTPUT/telnet/telnet.json +function dumpDockerLogs() { + echo "telnet/test: BEGIN docker logs from $CONTAINER_NAME [{(" + docker logs --tail all $CONTAINER_NAME + echo ")}] END docker logs from $CONTAINER_NAME" +} echo "telnet/test: Tests runner for telnet" -CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh telnet > $OUTPUT_FILE -# Dump the docker logs -echo "telnet/test: BEGIN docker logs from $CONTAINER_NAME [{(" -docker logs --tail all $CONTAINER_NAME -echo ")}] END docker logs from $CONTAINER_NAME" +# Plain telnet test +CONTAINER_NAME=zgrab_telnet +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh telnet > $OUTPUT_ROOT/telnet.json +dumpDockerLogs + +# TLS telnet test (port 992 is the standard telnets port) +CONTAINER_NAME=zgrab_telnet_tls +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh telnet --tls -p 992 > $OUTPUT_ROOT/telnet_tls.json +dumpDockerLogs + +# TLS downgrade tests +# Connecting with --tls --allow-tls-downgrade to the TLS port should succeed with TLS +CONTAINER_NAME=zgrab_telnet_tls +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh telnet --tls --allow-tls-downgrade -p 992 > $OUTPUT_ROOT/tls_downgrade_tls_port.json +dumpDockerLogs -# TODO: If there are any other relevant log files, dump those to stdout here. +# Connecting with --tls --allow-tls-downgrade to the plain port should fall back to plaintext +CONTAINER_NAME=zgrab_telnet_tls +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh telnet --tls --allow-tls-downgrade -p 23 > $OUTPUT_ROOT/tls_downgrade_plain_port.json +dumpDockerLogs diff --git a/integration_tests/telnet/tls-container/Dockerfile b/integration_tests/telnet/tls-container/Dockerfile new file mode 100644 index 000000000..cf27f28f8 --- /dev/null +++ b/integration_tests/telnet/tls-container/Dockerfile @@ -0,0 +1,21 @@ +FROM zgrab2_service_base:latest + +RUN apt-get install -y inetutils-telnetd stunnel4 openssl + +# Generate self-signed certificate for TLS +RUN mkdir -p /etc/stunnel && \ + openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -keyout /etc/stunnel/stunnel.key \ + -out /etc/stunnel/stunnel.crt \ + -subj "/C=US/ST=State/L=City/O=Test/CN=telnet-tls.example.com" && \ + cat /etc/stunnel/stunnel.key /etc/stunnel/stunnel.crt > /etc/stunnel/stunnel.pem && \ + chmod 600 /etc/stunnel/stunnel.pem + +COPY inetd.conf /etc/inetd.conf +COPY stunnel.conf /etc/stunnel/stunnel.conf + +WORKDIR / +COPY entrypoint.sh . +RUN chmod a+x ./entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/integration_tests/telnet/tls-container/entrypoint.sh b/integration_tests/telnet/tls-container/entrypoint.sh new file mode 100644 index 000000000..73c36d7e7 --- /dev/null +++ b/integration_tests/telnet/tls-container/entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -x + +# Start stunnel for TLS-wrapped telnet on port 992 +stunnel /etc/stunnel/stunnel.conf + +# Run plain telnet via inetd in the foreground +while true; do + if ! inetutils-inetd -d; then + echo "telnetd exited unexpectedly. Restarting..." + sleep 1 + fi +done diff --git a/integration_tests/telnet/tls-container/inetd.conf b/integration_tests/telnet/tls-container/inetd.conf new file mode 100644 index 000000000..48906ee59 --- /dev/null +++ b/integration_tests/telnet/tls-container/inetd.conf @@ -0,0 +1 @@ +telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/telnetd diff --git a/integration_tests/telnet/tls-container/stunnel.conf b/integration_tests/telnet/tls-container/stunnel.conf new file mode 100644 index 000000000..3a9ed4985 --- /dev/null +++ b/integration_tests/telnet/tls-container/stunnel.conf @@ -0,0 +1,7 @@ +pid = /var/run/stunnel.pid +foreground = no + +[telnets] +accept = 992 +connect = 23 +cert = /etc/stunnel/stunnel.pem diff --git a/modules/telnet/log.go b/modules/telnet/log.go index ec75baede..17282856a 100644 --- a/modules/telnet/log.go +++ b/modules/telnet/log.go @@ -14,6 +14,8 @@ package telnet +import "github.com/zmap/zgrab2" + // TelnetLog is the output of the telnet grab. type TelnetLog struct { // Banner is the telnet banner returned by the server. @@ -30,6 +32,8 @@ type TelnetLog struct { // Dont is the list of options that the server requests the client *not* use. Dont []TelnetOption `json:"dont,omitempty"` + + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` } // isTelnet checks if this struct represents having actually detected a Telnet service. diff --git a/modules/telnet/scanner.go b/modules/telnet/scanner.go index ddc0b76ee..9add2841d 100644 --- a/modules/telnet/scanner.go +++ b/modules/telnet/scanner.go @@ -14,6 +14,8 @@ package telnet import ( "context" "fmt" + "log" + "net" "github.com/zmap/zgrab2" ) @@ -22,14 +24,26 @@ import ( // Populated by the framework. type Flags struct { zgrab2.BaseFlags `group:"Basic Options"` - MaxReadSize int `long:"max-read-size" description:"Set the maximum number of bytes to read when grabbing the banner" default:"65536"` - Banner bool `long:"force-banner" description:"Always return banner if it has non-zero bytes"` + zgrab2.TLSFlags `group:"TLS Options"` + + MaxReadSize int `long:"max-read-size" description:"Set the maximum number of bytes to read when grabbing the banner" default:"65536"` + Banner bool `long:"force-banner" description:"Always return banner if it has non-zero bytes"` + 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."` } func NewModule() *zgrab2.TypedModule[Flags, Scanner, *Scanner] { return zgrab2.NewTypedModule[Flags, Scanner, *Scanner]("telnet", "Telnet Remote Terminal Communication (Telnet)", "Fetch a telnet banner", 23) } +func (f Flags) Validate(_ []string) error { + if f.AllowTLSDowngrade && !f.UseTLS { + log.Fatal("--allow-tls-downgrade requires --tls") + return zgrab2.ErrInvalidArguments + } + return nil +} + // Scanner implements the zgrab2.Scanner interface. type Scanner struct { zgrab2.BaseScanner @@ -44,18 +58,46 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { scanner.DialerGroupConfig = &zgrab2.DialerGroupConfig{ TransportAgnosticDialerProtocol: zgrab2.TransportTCP, BaseFlags: &f.BaseFlags, + TLSEnabled: f.UseTLS, + NeedSeparateL4Dialer: f.AllowTLSDowngrade, + } + if f.UseTLS { + scanner.DialerGroupConfig.TLSFlags = &f.TLSFlags } return nil } // Scan connects to the target (default port TCP 23) and attempts to grab the Telnet banner. func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { - conn, err := dialGroup.Dial(ctx, target) - if err != nil { - return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("could not establish connection to telnet server %s: %w", target.String(), err) - } - defer zgrab2.CloseConnAndHandleError(conn) + + var ( + conn net.Conn + err error + ) + result := new(TelnetLog) + + if scanner.config.AllowTLSDowngrade { + conn, _, err = dialGroup.DialTLSDowngrade(ctx, target, true) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + } else { + conn, err = dialGroup.Dial(ctx, target) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, fmt.Errorf("could not establish connection to telnet server %s: %w", target.String(), err) + } + } + + defer func() { + // attempt to collect TLS Log + if tlsConn, ok := conn.(*zgrab2.TLSConnection); ok { + result.TLSLog = tlsConn.GetLog() + } + // cleanup our connection + zgrab2.CloseConnAndHandleError(conn) + }() + if err := GetTelnetBanner(result, conn, scanner.config.MaxReadSize); err != nil { if scanner.config.Banner && len(result.Banner) > 0 { return zgrab2.TryGetScanStatus(err), result, err diff --git a/zgrab2_schemas/zgrab2/telnet.py b/zgrab2_schemas/zgrab2/telnet.py index 235ebe43b..ce262b325 100644 --- a/zgrab2_schemas/zgrab2/telnet.py +++ b/zgrab2_schemas/zgrab2/telnet.py @@ -23,6 +23,7 @@ "do": ListOf(telnet_option), "wont": ListOf(telnet_option), "dont": ListOf(telnet_option), + "tls": zgrab2.tls_log, } ) },