diff --git a/errors.go b/errors.go index df1dc37..32caa78 100644 --- a/errors.go +++ b/errors.go @@ -2,4 +2,15 @@ package zgrab2 import "errors" +// ErrMismatchedFlags is thrown if the flags for one module type are +// passed to an incompatible module type. var ErrMismatchedFlags = errors.New("mismatched flag/module") + +// ErrInvalidArguments is thrown if the command-line arguments invalid. +var ErrInvalidArguments = errors.New("invalid arguments") + +// ErrInvalidResponse is returned when the server returns a syntactically-invalid response. +var ErrInvalidResponse = errors.New("invalid response") + +// ErrUnexpectedResponse is returned when the server returns a syntactically-valid but unexpected response. +var ErrUnexpectedResponse = errors.New("unexpected response") diff --git a/integration_tests/pop3/cleanup.sh b/integration_tests/pop3/cleanup.sh new file mode 100755 index 0000000..5cd4267 --- /dev/null +++ b/integration_tests/pop3/cleanup.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set +e + +echo "pop3/cleanup: Tests cleanup for pop3" + +CONTAINER_NAME=zgrab_pop3 + +docker stop $CONTAINER_NAME diff --git a/integration_tests/pop3/container/Dockerfile b/integration_tests/pop3/container/Dockerfile new file mode 100644 index 0000000..4ddecc8 --- /dev/null +++ b/integration_tests/pop3/container/Dockerfile @@ -0,0 +1,9 @@ +FROM zgrab2_service_base:latest + +RUN apt-get install -y popa3d + +WORKDIR / +COPY entrypoint.sh . +RUN chmod a+x ./entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/integration_tests/pop3/container/entrypoint.sh b/integration_tests/pop3/container/entrypoint.sh new file mode 100755 index 0000000..28d0b63 --- /dev/null +++ b/integration_tests/pop3/container/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -x + +while true; do + watch /usr/sbin/popa3d -D + echo "popa3d exited unexpectedly. Restarting..." + sleep 1 +done diff --git a/integration_tests/pop3/setup.sh b/integration_tests/pop3/setup.sh new file mode 100755 index 0000000..76cc54c --- /dev/null +++ b/integration_tests/pop3/setup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +############################# +## FIXME: REMOVE -p 110:110 # +############################# + +echo "pop3/setup: Tests setup for pop3" + +CONTAINER_TAG="zgrab_pop3" +CONTAINER_NAME="zgrab_pop3" + +# If the container is already running, use it. +if docker ps --filter "name=$CONTAINER_NAME" | grep -q $CONTAINER_NAME; then + echo "pop3/setup: Container $CONTAINER_NAME already running -- nothing to setup" + exit 0 +fi + +# If it is not running, try launching it -- on success, use that. +echo "pop3/setup: Trying to launch $CONTAINER_NAME..." +if ! docker run -p 110:110 --rm --name $CONTAINER_NAME -td $CONTAINER_TAG; then + echo "pop3/setup: Building docker image $CONTAINER_TAG..." + # If it fails, build it from ./container/Dockerfile + docker build -t $CONTAINER_TAG ./container + # Try again + echo "pop3/setup: Launching $CONTAINER_NAME..." + docker run --rm --name $CONTAINER_NAME -p 110:110 -td $CONTAINER_TAG +fi diff --git a/integration_tests/pop3/test.sh b/integration_tests/pop3/test.sh new file mode 100755 index 0000000..08ca00d --- /dev/null +++ b/integration_tests/pop3/test.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e +MODULE_DIR=$(dirname $0) +ZGRAB_ROOT=$MODULE_DIR/../.. +ZGRAB_OUTPUT=$ZGRAB_ROOT/zgrab-output + +mkdir -p $ZGRAB_OUTPUT/pop3 + +CONTAINER_NAME=zgrab_pop3 + +OUTPUT_ROOT=$ZGRAB_OUTPUT/pop3 + +echo "pop3/test: Tests runner for pop3" +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh pop3 > $OUTPUT_ROOT/banner.json +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh pop3 --quit > $OUTPUT_ROOT/banner.quit.json +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh pop3 --help --quit > $OUTPUT_ROOT/help.banner.quit.json +CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh pop3 -noop --help --quit > $OUTPUT_ROOT/noop.help.banner.quit.json + +# TODO: the pop3 container does not support STARTTLS; they suggest +# wrapping it in stunnel (which would handle the --pop3s case). + +FIELDS="help quit banner noop" +status=0 +for field in $FIELDS; do + for file in $(find $OUTPUT_ROOT -iname "*$field*.json"); do + echo "check $file for $field" + RESULT=$($ZGRAB_ROOT/jp data.pop3.result.$field < $file) + if [ "$RESULT" = "null" ]; then + echo "Did not find $field in $file [[" + cat $file + echo "]]" + status=1 + fi + done +done + +# Dump the docker logs +echo "pop3/test: BEGIN docker logs from $CONTAINER_NAME [{(" +docker logs --tail all $CONTAINER_NAME +echo ")}] END docker logs from $CONTAINER_NAME" diff --git a/modules/pop3.go b/modules/pop3.go new file mode 100644 index 0000000..e36dbf8 --- /dev/null +++ b/modules/pop3.go @@ -0,0 +1,7 @@ +package modules + +import "github.com/zmap/zgrab2/modules/pop3" + +func init() { + pop3.RegisterModule() +} diff --git a/modules/pop3/pop3.go b/modules/pop3/pop3.go new file mode 100644 index 0000000..d3e36fc --- /dev/null +++ b/modules/pop3/pop3.go @@ -0,0 +1,37 @@ +package pop3 + +import ( + "net" + "regexp" + + "github.com/zmap/zgrab2" +) + +// This is the regex used in zgrab. +var pop3EndRegex = regexp.MustCompile(`(?:\r\n\.\r\n$)|(?:\r\n$)`) + +const readBufferSize int = 0x10000 + +// Connection wraps the state and access to the SMTP connection. +type Connection struct { + Conn net.Conn +} + +// ReadResponse reads from the connection until it matches the pop3EndRegex. Copied from the original zgrab. +// TODO: Catch corner cases, parse out success/error character. +func (conn *Connection) ReadResponse() (string, error) { + ret := make([]byte, readBufferSize) + n, err := zgrab2.ReadUntilRegex(conn.Conn, ret, pop3EndRegex) + if err != nil { + return "", nil + } + return string(ret[0:n]), nil +} + +// SendCommand sends a command, followed by a CRLF, then wait for / read the server's response. +func (conn *Connection) SendCommand(cmd string) (string, error) { + if _, err := conn.Conn.Write([]byte(cmd + "\r\n")); err != nil { + return "", err + } + return conn.ReadResponse() +} \ No newline at end of file diff --git a/modules/pop3/scanner.go b/modules/pop3/scanner.go new file mode 100644 index 0000000..e285050 --- /dev/null +++ b/modules/pop3/scanner.go @@ -0,0 +1,239 @@ +// Package pop3 provides a zgrab2 module that scans for POP3 mail +// servers. +// Default Port: 110 (TCP) +// +// The --send-help and --send-noop flags tell the scanner to send a +// HELP or NOOP command and read the response. +// +// The --pop3s flag tells the scanner to perform a TLS handshake +// immediately after connecting, before even attempting to read +// the banner. +// The --starttls flag tells the scanner to send the STLS command, +// and then negotiate a TLS connection. +// The scanner uses the standard TLS flags for the handshake. +// --pop3s and --starttls are mutually exclusive. +// --pop3s does not change the default port number from 110, so +// it should usually be coupled with e.g. --port 995. +// +// The --send-quit flag tells the scanner to send a QUIT command +// before disconnecting. +// +// So, if no flags are specified, the scanner simply reads the banner +// returned by the server and disconnects. +// +// The output contains the banner and the responses to any commands that +// were sent, and if or --pop3s --starttls were set, the standard TLS logs. +package pop3 + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/zmap/zgrab2" + "strings" +) + + +// ScanResults instances are returned by the module's Scan function. +type ScanResults struct { + // Banner is the string sent by the server immediately after connecting. + Banner string `json:"banner,omitempty"` + + // NOOP is the server's response to the NOOP command, if one is sent. + NOOP string `json:"noop,omitempty"` + + // HELP is the server's response to the HELP command, if it is sent. + HELP string `json:"help,omitempty"` + + // StartTLS is the server's response to the STARTTLS command, if it is sent. + StartTLS string `json:"starttls,omitempty"` + + // QUIT is the server's response to the QUIT command, if it is sent. + QUIT string `json:"quit,omitempty"` + + // TLSLog is the standard TLS log, if --starttls or --pop3s is enabled. + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` +} + +// Flags holds the command-line configuration for the POP3 scan module. +// Populated by the framework. +type Flags struct { + zgrab2.BaseFlags + zgrab2.TLSFlags + + // SendHELP indicates that the client should send the HELP command. + SendHELP bool `long:"send-help" description:"Send the HELP command"` + + // SendNOOP indicates that the NOOP command should be sent. + SendNOOP bool `long:"send-noop" description:"Send the NOOP command before closing."` + + // SendQUIT indicates that the QUIT command should be sent. + SendQUIT bool `long:"send-quit" description:"Send the QUIT command before closing."` + + // POP3Secure indicates that the client should do a TLS handshake immediately after connecting. + POP3Secure bool `long:"pop3s" description:"Immediately negotiate a TLS connection"` + + // StartTLS indicates that the client should attempt to update the connection to TLS. + StartTLS bool `long:"starttls" description:"Send STLS before negotiating"` + + // Verbose indicates that there should be more verbose logging. + Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"` +} + +// Module implements the zgrab2.Module interface. +type Module struct { +} + +// Scanner implements the zgrab2.Scanner interface. +type Scanner struct { + config *Flags +} + +// RegisterModule registers the zgrab2 module. +func RegisterModule() { + var module Module + _, err := zgrab2.AddCommand("pop3", "pop3", "Probe for pop3", 110, &module) + if err != nil { + log.Fatal(err) + } +} + +// NewFlags returns a default Flags object. +func (module *Module) NewFlags() interface{} { + return new(Flags) +} + +// NewScanner returns a new Scanner instance. +func (module *Module) NewScanner() zgrab2.Scanner { + return new(Scanner) +} + +// Validate checks that the flags are valid. +// On success, returns nil. +// On failure, returns an error instance describing the error. +func (flags *Flags) Validate(args []string) error { + if flags.StartTLS && flags.POP3Secure { + log.Error("Cannot send both --starttls and --pop3s") + return zgrab2.ErrInvalidArguments + } + return nil +} + +// Help returns the module's help string. +func (flags *Flags) Help() string { + return "" +} + +// Init initializes the Scanner. +func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { + f, _ := flags.(*Flags) + scanner.config = f + return nil +} + +// InitPerSender initializes the scanner for a given sender. +func (scanner *Scanner) InitPerSender(senderID int) error { + return nil +} + +// GetName returns the Scanner name defined in the Flags. +func (scanner *Scanner) GetName() string { + return scanner.config.Name +} + +// Protocol returns the protocol identifier of the scan. +func (scanner *Scanner) Protocol() string { + return "pop3" +} + +// GetPort returns the port being scanned. +func (scanner *Scanner) GetPort() uint { + return scanner.config.Port +} + +func getPOP3Error(response string) error { + if !strings.HasPrefix(response, "-") { + return nil + } + return fmt.Errorf("POP3 error: %s", response[1:]) +} + +// Scan performs the POP3 scan. +// 1. Open a TCP connection to the target port (default 110). +// 2. If --pop3s is set, perform a TLS handshake using the command-line +// flags. +// 3. Read the banner. +// 4. If --send-help is sent, send HELP, read the result. +// 5. If --send-noop is sent, send NOOP, read the result. +// 6. If --starttls is sent, send STLS, read the result, negotiate a +// TLS connection using the command-line flags. +// 7. If --send-quit is sent, send QUIT and read the result. +// 8. Close the connection. +func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) { + c, err := target.Open(&scanner.config.BaseFlags) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + defer c.Close() + result := &ScanResults{} + if scanner.config.POP3Secure { + tlsConn, err := scanner.config.TLSFlags.GetTLSConnection(c) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + result.TLSLog = tlsConn.GetLog() + if err := tlsConn.Handshake(); err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + c = tlsConn + } + conn := Connection{Conn: c} + banner, err := conn.ReadResponse() + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + result.Banner = banner + if scanner.config.SendHELP { + ret, err := conn.SendCommand("HELP") + if err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + result.HELP = ret + } + if scanner.config.SendNOOP { + ret, err := conn.SendCommand("NOOP") + if err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + result.NOOP = ret + } + if scanner.config.StartTLS { + ret, err := conn.SendCommand("STLS") + if err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + result.StartTLS = ret + if err := getPOP3Error(ret); err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + tlsConn, err := scanner.config.TLSFlags.GetTLSConnection(conn.Conn) + if err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + result.TLSLog = tlsConn.GetLog() + if err := tlsConn.Handshake(); err != nil { + return zgrab2.TryGetScanStatus(err), result, err + } + conn.Conn = tlsConn + } + if scanner.config.SendQUIT { + ret, err := conn.SendCommand("QUIT") + if err != nil { + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + } + result.QUIT = ret + } + return zgrab2.SCAN_SUCCESS, result, nil +} \ No newline at end of file diff --git a/schemas/__init__.py b/schemas/__init__.py index b49d932..1c74e8c 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -8,3 +8,4 @@ import schemas.ntp import schemas.mssql import schemas.redis import schemas.telnet +import schemas.pop3 diff --git a/schemas/pop3.py b/schemas/pop3.py new file mode 100644 index 0000000..f3d0a34 --- /dev/null +++ b/schemas/pop3.py @@ -0,0 +1,23 @@ +# zschema sub-schema for zgrab2's pop3 module +# Registers zgrab2-pop3 globally, and pop3 with the main zgrab2 schema. +from zschema.leaves import * +from zschema.compounds import * +import zschema.registry + +import schemas.zcrypto as zcrypto +import schemas.zgrab2 as zgrab2 + +pop3_scan_response = SubRecord({ + "result": SubRecord({ + "banner": String(), + "noop": String(), + "help": String(), + "starttls": String(), + "quit": String(), + "tls": zgrab2.tls_log, + }) +}, extends=zgrab2.base_scan_response) + +zschema.registry.register_schema("zgrab2-pop3", pop3_scan_response) + +zgrab2.register_scan_response_type("pop3", pop3_scan_response)