Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions integration_tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
33 changes: 25 additions & 8 deletions integration_tests/telnet/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions integration_tests/telnet/tls-container/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
14 changes: 14 additions & 0 deletions integration_tests/telnet/tls-container/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions integration_tests/telnet/tls-container/inetd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/telnetd
7 changes: 7 additions & 0 deletions integration_tests/telnet/tls-container/stunnel.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pid = /var/run/stunnel.pid
foreground = no

[telnets]
accept = 992
connect = 23
cert = /etc/stunnel/stunnel.pem
4 changes: 4 additions & 0 deletions modules/telnet/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
56 changes: 49 additions & 7 deletions modules/telnet/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ package telnet
import (
"context"
"fmt"
"log"
"net"

"github.com/zmap/zgrab2"
)
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions zgrab2_schemas/zgrab2/telnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"do": ListOf(telnet_option),
"wont": ListOf(telnet_option),
"dont": ListOf(telnet_option),
"tls": zgrab2.tls_log,
}
)
},
Expand Down