2018-02-12 16:43:41 +00:00
// Package ftp contains the zgrab2 Module implementation for FTP(S).
//
// Setting the --authtls flag will cause the scanner to attempt a upgrade the
// connection to TLS. Settings for the TLS handshake / probe can be set with
// the standard TLSFlags.
//
// The scan performs a banner grab and (optionally) a TLS handshake.
//
// The output is the banner, any responses to the AUTH TLS/AUTH SSL commands,
// and any TLS logs.
package ftp
import (
"net"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
)
// ScanResults is the output of the scan.
// Identical to the original from zgrab, with the addition of TLSLog.
type ScanResults struct {
// Banner is the initial data banner sent by the server.
Banner string ` json:"banner,omitempty" `
// AuthTLSResp is the response to the AUTH TLS command.
// Only present if the FTPAuthTLS flag is set.
AuthTLSResp string ` json:"auth_tls,omitempty" `
// AuthSSLResp is the response to the AUTH SSL command.
// Only present if the FTPAuthTLS flag is set and AUTH TLS failed.
AuthSSLResp string ` json:"auth_ssl,omitempty" `
// TLSLog is the standard shared TLS handshake log.
// Only present if the FTPAuthTLS flag is set.
TLSLog * zgrab2 . TLSLog ` json:"tls,omitempty" `
}
// Flags are the FTP-specific command-line flags. Taken from the original zgrab.
// (TODO: should FTPAuthTLS be on by default?).
type Flags struct {
zgrab2 . BaseFlags
zgrab2 . TLSFlags
Verbose bool ` long:"verbose" description:"More verbose logging, include debug fields in the scan results" `
FTPAuthTLS bool ` long:"authtls" description:"Collect FTPS certificates in addition to FTP banners" `
}
// Module implements the zgrab2.Module interface.
type Module struct {
}
// Scanner implements the zgrab2.Scanner interface, and holds the state
// for a single scan.
type Scanner struct {
config * Flags
}
// Connection holds the state for a single connection to the FTP server.
type Connection struct {
// buffer is a temporary buffer for sending commands -- so, never interleave
// sendCommand calls on a given connection
buffer [ 1024 ] byte
config * Flags
results ScanResults
conn net . Conn
}
// RegisterModule registers the ftp zgrab2 module.
func RegisterModule ( ) {
var module Module
_ , err := zgrab2 . AddCommand ( "ftp" , "FTP" , "Grab an FTP banner" , 21 , & module )
if err != nil {
log . Fatal ( err )
}
}
// NewFlags returns the default flags object to be filled in with the
// command-line arguments.
func ( m * Module ) NewFlags ( ) interface { } {
return new ( Flags )
}
// NewScanner returns a new Scanner instance.
func ( m * Module ) NewScanner ( ) zgrab2 . Scanner {
return new ( Scanner )
}
// Validate does nothing in this module.
func ( f * Flags ) Validate ( args [ ] string ) error {
return nil
}
// Help returns this module's help string.
func ( f * Flags ) Help ( ) string {
return ""
}
2018-03-12 17:36:11 +00:00
// Protocol returns the protocol identifer for the scanner.
func ( s * Scanner ) Protocol ( ) string {
return "ftp"
}
2018-02-12 16:43:41 +00:00
// Init initializes the Scanner instance with the flags from the command
// line.
func ( s * Scanner ) Init ( flags zgrab2 . ScanFlags ) error {
f , _ := flags . ( * Flags )
s . config = f
return nil
}
// InitPerSender does nothing in this module.
func ( s * Scanner ) InitPerSender ( senderID int ) error {
return nil
}
// GetName returns the configured name for the Scanner.
func ( s * Scanner ) GetName ( ) string {
return s . config . Name
}
// GetPort returns the configured port for the Scanner.
func ( s * Scanner ) GetPort ( ) uint {
return s . config . Port
}
// ftpEndRegex matches zero or more lines followed by a numeric FTP status code and linebreak, e.g. "200 OK\r\n"
var ftpEndRegex = regexp . MustCompile ( ` ^(?:.*\r?\n)*([0-9] { 3})( [^\r\n]*)?\r?\n$ ` )
// isOKResponse returns true iff and only if the given response code indicates
// success (e.g. 2XX)
func ( ftp * Connection ) isOKResponse ( retCode string ) bool {
// TODO: This is the current behavior; should it check that it isn't garbage that happens to start with 2 (e.g. it's only ASCII chars, the prefix is 2[0-9]+, etc)?
return strings . HasPrefix ( retCode , "2" )
}
// readResponse reads an FTP response chunk from the server.
// It returns the full response, as well as the status code alone.
func ( ftp * Connection ) readResponse ( ) ( string , string , error ) {
respLen , err := zgrab2 . ReadUntilRegex ( ftp . conn , ftp . buffer [ : ] , ftpEndRegex )
if err != nil {
return "" , "" , err
}
ret := string ( ftp . buffer [ 0 : respLen ] )
retCode := ftpEndRegex . FindStringSubmatch ( ret ) [ 1 ]
return ret , retCode , nil
}
// GetFTPBanner reads the data sent by the server immediately after connecting.
// Returns true if and only if the server returns a success status code.
// Taken over from the original zgrab.
func ( ftp * Connection ) GetFTPBanner ( ) ( bool , error ) {
banner , retCode , err := ftp . readResponse ( )
if err != nil {
return false , err
}
ftp . results . Banner = banner
return ftp . isOKResponse ( retCode ) , nil
}
// sendCommand sends a command and waits for / reads / returns the response.
func ( ftp * Connection ) sendCommand ( cmd string ) ( string , string , error ) {
ftp . conn . Write ( [ ] byte ( cmd + "\r\n" ) )
return ftp . readResponse ( )
}
// SetupFTPS returns true if and only if the server reported support for FTPS.
// First attempt AUTH TLS; if that fails, try AUTH SSL.
// Taken over from the original zgrab.
func ( ftp * Connection ) SetupFTPS ( ) ( bool , error ) {
ret , retCode , err := ftp . sendCommand ( "AUTH TLS" )
if err != nil {
return false , err
}
ftp . results . AuthTLSResp = ret
if ftp . isOKResponse ( retCode ) {
return true , nil
}
ret , retCode , err = ftp . sendCommand ( "AUTH SSL" )
if err != nil {
return false , err
}
ftp . results . AuthSSLResp = ret
if ftp . isOKResponse ( retCode ) {
return true , nil
}
return false , nil
}
// GetFTPSCertificates attempts to perform a TLS handshake with the server so
// that the TLS certificates will end up in the TLSLog.
// First sends the AUTH TLS/AUTH SSL command to tell the server we want to
// do a TLS handshake. If that fails, break. Otherwise, perform the handshake.
// Taken over from the original zgrab.
func ( ftp * Connection ) GetFTPSCertificates ( ) error {
ftpsReady , err := ftp . SetupFTPS ( )
if err != nil {
return err
}
if ! ftpsReady {
return nil
}
var conn * zgrab2 . TLSConnection
if conn , err = ftp . config . TLSFlags . GetTLSConnection ( ftp . conn ) ; err != nil {
return err
}
ftp . results . TLSLog = conn . GetLog ( )
if err = conn . Handshake ( ) ; err != nil {
// NOTE: With the default config of vsftp (without ssl_ciphers=HIGH), AUTH TLS succeeds, but the handshake fails, dumping "error:1408A0C1:SSL routines:ssl3_get_client_hello:no shared cipher" to the socket.
return err
}
ftp . conn = conn
return nil
}
// Scan performs the configured scan on the FTP server, as follows:
// * Read the banner into results.Banner (if it is not a 2XX response, bail)
// * If the FTPAuthTLS flag is not set, finish.
// * Send the AUTH TLS command to the server. If the response is not 2XX, then
// send the AUTH SSL command. If the response is not 2XX, then finish.
// * Perform ths TLS handshake / any configured TLS scans, populating
// results.TLSLog.
// * Return SCAN_SUCCESS, &results, nil
func ( s * Scanner ) Scan ( t zgrab2 . ScanTarget ) ( status zgrab2 . ScanStatus , result interface { } , thrown error ) {
var err error
conn , err := t . Open ( & s . config . BaseFlags )
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , nil , err
}
2018-03-21 21:16:58 +00:00
defer conn . Close ( )
2018-02-12 16:43:41 +00:00
ftp := Connection { conn : conn , config : s . config , results : ScanResults { } }
is200Banner , err := ftp . GetFTPBanner ( )
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , & ftp . results , err
}
if s . config . FTPAuthTLS && is200Banner {
if err := ftp . GetFTPSCertificates ( ) ; err != nil {
return zgrab2 . SCAN_APPLICATION_ERROR , & ftp . results , err
}
}
return zgrab2 . SCAN_SUCCESS , & ftp . results , nil
}