2018-02-13 18:25:47 +00:00
// Package redis provides a zgrab2 Module that probes for redis services.
// The default port for redis is TCP 6379, and it is a cleartext protocol
// defined at https://redis.io/topics/protocol.
// Servers can be configured to require (cleartext) password authentication,
// which is omitted from our probe by default (pass --password <your password>
// to supply one).
// Further, admins can rename commands, so even if authentication is not
// required we may not get the expected output.
// However, we should always get output in the expected format, which is fairly
// distinct. The probe sends a sequence of commands and checks that the response
// is well-formed redis data, which should be possible whatever the
// configuration.
package redis
import (
2019-06-17 19:23:56 +00:00
"encoding/json"
2018-02-13 18:25:47 +00:00
"fmt"
"io"
2019-06-17 19:23:56 +00:00
"io/ioutil"
2019-06-25 15:57:47 +00:00
"os"
2019-06-17 19:23:56 +00:00
"path/filepath"
2019-06-20 16:17:26 +00:00
"strconv"
2018-02-13 18:25:47 +00:00
"strings"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
2019-06-17 19:23:56 +00:00
"gopkg.in/yaml.v2"
2018-02-13 18:25:47 +00:00
)
// Flags contains redis-specific command-line flags.
type Flags struct {
zgrab2 . BaseFlags
2019-06-25 15:57:47 +00:00
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" `
Verbose bool ` long:"verbose" description:"More verbose logging, include debug fields in the scan results" `
2018-02-13 18:25:47 +00:00
}
// Module implements the zgrab2.Module interface
type Module struct {
}
// Scanner implements the zgrab2.Scanner interface
type Scanner struct {
2019-06-17 19:23:56 +00:00
config * Flags
2019-06-19 20:06:43 +00:00
commandMappings map [ string ] string
2019-06-17 19:23:56 +00:00
customCommands [ ] string
2018-02-13 18:25:47 +00:00
}
// scan holds the state for the scan of an individual target
type scan struct {
scanner * Scanner
result * Result
target * zgrab2 . ScanTarget
conn * Connection
2018-06-26 17:51:10 +00:00
close func ( )
2018-02-13 18:25:47 +00:00
}
// Result is the struct that is returned by the scan.
// If authentication is required, most responses can have the value
// "(error: NOAUTH Authentication required.)"
type Result struct {
// Commands is the list of commands actually sent to the server, serialized
// in inline format (e.g. COMMAND arg1 "arg 2" arg3)
Commands [ ] string ` json:"commands,omitempty" zgrab:"debug" `
// RawCommandOutput is the output returned by the server for each command sent;
// the index in RawCommandOutput matches the index in Commands.
RawCommandOutput [ ] [ ] byte ` json:"raw_command_output,omitempty" zgrab:"debug" `
// PingResponse is the response from the server, should be the simple string
// "PONG".
// NOTE: This is invoked *before* calling AUTH, so this may return an auth
// required error even if --password is provided.
PingResponse string ` json:"ping_response,omitempty" `
2019-06-19 13:51:40 +00:00
// AuthResponse is only included if --password is set.
AuthResponse string ` json:"auth_response,omitempty" `
2018-02-13 18:25:47 +00:00
// InfoResponse is the response from the INFO command: "Lines can contain a
// section name (starting with a # character) or a property. All the
// properties are in the form of field:value terminated by \r\n."
InfoResponse string ` json:"info_response,omitempty" `
2019-06-19 13:51:40 +00:00
// Version is read from the InfoResponse (the field "server_version"), if
// present.
Version string ` json:"version,omitempty" `
2019-06-28 19:54:11 +00:00
// Major is the version's major number.
2019-06-28 20:30:12 +00:00
Major * uint32 ` json:"major,omitempty" `
2019-06-28 19:54:11 +00:00
// Minor is the version's minor number.
2019-06-28 20:30:12 +00:00
Minor * uint32 ` json:"minor,omitempty" `
2019-06-28 19:54:11 +00:00
// Patchlevel is the version's patchlevel number.
2019-06-28 20:30:12 +00:00
Patchlevel * uint32 ` json:"patchlevel,omitempty" `
2019-06-28 19:54:11 +00:00
2019-06-19 13:51:40 +00:00
// OS is read from the InfoResponse (the field "os"), if present. It specifies
// the OS the redis server is running.
OS string ` json:"os,omitempty" `
2018-02-13 18:25:47 +00:00
2019-06-20 16:17:26 +00:00
// ArchBits is read from the InfoResponse (the field "arch_bits"), if present.
// It specifies the architecture bits (32 or 64) the redis server used to build.
ArchBits string ` json:"arch_bits,omitempty" `
2019-06-19 20:06:43 +00:00
// Mode is read from the InfoResponse (the field "redis_mode"), if present.
// It specifies the mode the redis server is running, either cluster or standalone.
Mode string ` json:"mode,omitempty" `
2019-06-20 16:17:26 +00:00
// GitSha1 is read from the InfoResponse (the field "redis_git_sha1"), if present.
// It specifies the Git Sha 1 the redis server used.
GitSha1 string ` json:"git_sha1,omitempty" `
// BuildID is read from the InfoResponse (the field "redis_build_id"), if present.
// It specifies the Build ID of the redis server.
BuildID string ` json:"build_id,omitempty" `
// GCCVersion is read from the InfoResponse (the field "gcc_version"), if present.
// It specifies the version of the GCC compiler used to compile the Redis server.
GCCVersion string ` json:"gcc_version,omitempty" `
// MemAllocator is read from the InfoResponse (the field "mem_allocator"), if present.
// It specifies the memory allocator.
MemAllocator string ` json:"mem_allocator,omitempty" `
// Uptime is read from the InfoResponse (the field "uptime_in_seconds"), if present.
// It specifies the number of seconds since Redis server start.
Uptime uint32 ` json:"uptime_in_seconds,omitempty" `
// UsedMemory is read from the InfoResponse (the field "used_memory"), if present.
// It specifies the total number of bytes allocated by Redis using its allocator.
UsedMemory uint32 ` json:"used_memory,omitempty" `
// ConnectionsReceived is read from the InfoResponse (the field "total_connections_received"),
// if present. It specifies the total number of connections accepted by the server.
ConnectionsReceived uint32 ` json:"total_connections_received,omitempty" `
// CommandsProcessed is read from the InfoResponse (the field "total_commands_processed"),
// if present. It specifies the total number of commands processed by the server.
CommandsProcessed uint32 ` json:"total_commands_processed,omitempty" `
2018-02-13 18:25:47 +00:00
// NonexistentResponse is the response to the non-existent command; even if
// auth is required, this may give a different error than existing commands.
NonexistentResponse string ` json:"nonexistent_response,omitempty" `
2019-06-17 21:32:51 +00:00
// CustomResponses is an array that holds the commands, arguments, and
// responses from user-inputted commands.
CustomResponses [ ] CustomResponse ` json:"custom_responses,omitempty" `
2019-06-19 13:51:40 +00:00
// QuitResponse is the response from the QUIT command -- should be the
// simple string "OK" even when authentication is required, unless the
// QUIT command was renamed.
QuitResponse string ` json:"quit_response,omitempty" `
2018-02-13 18:25:47 +00:00
}
// RegisterModule registers the zgrab2 module
func RegisterModule ( ) {
var module Module
_ , err := zgrab2 . AddCommand ( "redis" , "redis" , "Probe for redis" , 6379 , & module )
if err != nil {
log . Fatal ( err )
}
}
// NewFlags provides an empty instance of the flags that will be filled in by the framework
func ( module * Module ) NewFlags ( ) interface { } {
return new ( Flags )
}
// NewScanner provides a new scanner instance
func ( module * Module ) NewScanner ( ) zgrab2 . Scanner {
return new ( Scanner )
}
// Validate checks that the flags are valid
func ( flags * Flags ) Validate ( args [ ] string ) error {
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
2019-06-17 19:23:56 +00:00
err := scanner . initCommands ( )
if err != nil {
log . Fatal ( err )
}
2018-02-13 18:25:47 +00:00
return nil
}
// InitPerSender initializes the scanner for a given sender
func ( scanner * Scanner ) InitPerSender ( senderID int ) error {
return nil
}
// GetName returns the name of the scanner
func ( scanner * Scanner ) GetName ( ) string {
return scanner . config . Name
}
2018-06-26 17:51:10 +00:00
// GetTrigger returns the Trigger defined in the Flags.
func ( scanner * Scanner ) GetTrigger ( ) string {
return scanner . config . Trigger
}
2018-03-21 21:16:58 +00:00
// Close cleans up the scanner.
func ( scan * scan ) Close ( ) {
defer scan . close ( )
}
2019-06-17 19:23:56 +00:00
func getUnmarshaler ( file string ) ( func ( [ ] byte , interface { } ) error , error ) {
var unmarshaler func ( [ ] byte , interface { } ) error
switch ext := filepath . Ext ( file ) ; ext {
case ".json" :
unmarshaler = json . Unmarshal
case ".yaml" , ".yml" :
unmarshaler = yaml . Unmarshal
default :
2019-06-25 15:57:47 +00:00
err := fmt . Errorf ( "file type %s not valid" , ext )
2019-06-17 19:23:56 +00:00
return nil , err
}
return unmarshaler , nil
}
2019-06-25 15:57:47 +00:00
func ( scanner * Scanner ) getFileContents ( file string , output interface { } ) error {
2019-06-17 19:23:56 +00:00
unmarshaler , err := getUnmarshaler ( file )
if err != nil {
return err
}
2019-06-25 15:57:47 +00:00
fileStat , err := os . Stat ( file )
if err != nil {
return err
}
if fileStat . Size ( ) > scanner . config . MaxInputFileSize {
err = fmt . Errorf ( "input file too large" )
return err
}
2019-06-17 19:23:56 +00:00
fileContent , err := ioutil . ReadFile ( file )
if err != nil {
return err
}
err = unmarshaler ( [ ] byte ( fileContent ) , output )
if err != nil {
return err
}
return nil
}
// Initializes the command mappings
func ( scanner * Scanner ) initCommands ( ) error {
2019-06-19 20:06:43 +00:00
scanner . commandMappings = map [ string ] string {
2019-06-17 19:23:56 +00:00
"PING" : "PING" ,
"AUTH" : "AUTH" ,
"INFO" : "INFO" ,
"NONEXISTENT" : "NONEXISTENT" ,
"QUIT" : "QUIT" ,
}
if scanner . config . CustomCommands != "" {
var customCommands [ ] string
2019-06-25 15:57:47 +00:00
err := scanner . getFileContents ( scanner . config . CustomCommands , & customCommands )
2019-06-17 19:23:56 +00:00
if err != nil {
return err
}
scanner . customCommands = customCommands
}
// User supplied a file for updated command mappings
if scanner . config . Mappings != "" {
var mappings map [ string ] string
2019-06-25 15:57:47 +00:00
err := scanner . getFileContents ( scanner . config . Mappings , & mappings )
2019-06-17 19:23:56 +00:00
if err != nil {
return err
}
for origCommand , newCommand := range mappings {
scanner . commandMappings [ strings . ToUpper ( origCommand ) ] = strings . ToUpper ( newCommand )
}
}
return nil
}
2018-02-13 18:25:47 +00:00
// SendCommand sends the given command/args to the server, using the scanner's
// configuration, and drop the command/output into the result.
func ( scan * scan ) SendCommand ( cmd string , args ... string ) ( RedisValue , error ) {
exec := scan . conn . SendCommand
scan . result . Commands = append ( scan . result . Commands , getInlineCommand ( cmd , args ... ) )
if scan . scanner . config . DoInline {
exec = scan . conn . SendInlineCommand
}
ret , err := exec ( cmd , args ... )
if err != nil {
return nil , err
}
scan . result . RawCommandOutput = append ( scan . result . RawCommandOutput , ret . Encode ( ) )
return ret , nil
}
// StartScan opens a connection to the target and sets up a scan instance for it
func ( scanner * Scanner ) StartScan ( target * zgrab2 . ScanTarget ) ( * scan , error ) {
conn , err := target . Open ( & scanner . config . BaseFlags )
if err != nil {
return nil , err
}
return & scan {
target : target ,
scanner : scanner ,
result : & Result { } ,
conn : & Connection {
scanner : scanner ,
conn : conn ,
} ,
2018-03-21 21:16:58 +00:00
close : func ( ) { conn . Close ( ) } ,
2018-02-13 18:25:47 +00:00
} , nil
}
// Force the response into a string. Used when you expect a human-readable
// string.
func forceToString ( val RedisValue ) string {
switch v := val . ( type ) {
case SimpleString :
return string ( v )
case BulkString :
return string ( [ ] byte ( v ) )
case Integer :
return fmt . Sprintf ( "%d" , v )
case ErrorMessage :
return fmt . Sprintf ( "(Error: %s)" , string ( v ) )
case NullType :
return "<null>"
case RedisArray :
return "(Unexpected array)"
default :
panic ( "unreachable" )
}
}
2018-03-12 17:36:11 +00:00
// Protocol returns the protocol identifer for the scanner.
2019-06-25 15:57:47 +00:00
func ( scanner * Scanner ) Protocol ( ) string {
2018-03-12 17:36:11 +00:00
return "redis"
}
2019-06-20 16:17:26 +00:00
// Converts the string to a Uint32 if possible. If not, returns 0 (the zero value of a uin32)
func convToUint32 ( s string ) uint32 {
2019-06-25 15:57:47 +00:00
s64 , err := strconv . ParseUint ( s , 10 , 32 )
2019-06-20 16:17:26 +00:00
if err != nil {
return 0
}
2019-06-25 15:57:47 +00:00
return uint32 ( s64 )
2019-06-20 16:17:26 +00:00
}
2018-02-13 18:25:47 +00:00
// Scan executes the following commands:
// 1. PING
// 2. (only if --password is provided) AUTH <password>
// 3. INFO
// 4. NONEXISTENT
2019-06-19 20:06:43 +00:00
// 5. (only if --custom-commands is provided) CustomCommands <args>
// 6. QUIT
2018-02-13 18:25:47 +00:00
// The responses for each of these is logged, and if INFO succeeds, the version
// is scraped from it.
func ( scanner * Scanner ) Scan ( target zgrab2 . ScanTarget ) ( zgrab2 . ScanStatus , interface { } , error ) {
// ping, info, quit
scan , err := scanner . StartScan ( & target )
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , nil , err
}
2018-03-21 21:16:58 +00:00
defer scan . Close ( )
2018-02-13 18:25:47 +00:00
result := scan . result
2019-06-19 20:06:43 +00:00
pingResponse , err := scan . SendCommand ( scanner . commandMappings [ "PING" ] )
2018-02-13 18:25:47 +00:00
if err != nil {
2019-06-17 19:23:56 +00:00
// If the first command fails (as opposed to succeeding but returning an
2018-02-13 18:25:47 +00:00
// ErrorMessage response), then flag the probe as having failed.
return zgrab2 . TryGetScanStatus ( err ) , nil , err
}
// From this point forward, we always return a non-nil result, implying that
// we have positively identified that a redis service is present.
result . PingResponse = forceToString ( pingResponse )
if scanner . config . Password != "" {
2019-06-19 20:06:43 +00:00
authResponse , err := scan . SendCommand ( scanner . commandMappings [ "AUTH" ] , scanner . config . Password )
2018-02-13 18:25:47 +00:00
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , result , err
}
result . AuthResponse = forceToString ( authResponse )
}
2019-06-19 20:06:43 +00:00
infoResponse , err := scan . SendCommand ( scanner . commandMappings [ "INFO" ] )
2018-02-13 18:25:47 +00:00
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , result , err
}
result . InfoResponse = forceToString ( infoResponse )
2019-06-17 19:23:56 +00:00
if infoResponseBulk , ok := infoResponse . ( BulkString ) ; ok {
2018-02-13 18:25:47 +00:00
for _ , line := range strings . Split ( string ( infoResponseBulk ) , "\r\n" ) {
2019-06-25 15:57:47 +00:00
linePrefixSuffix := strings . SplitN ( line , ":" , 2 )
prefix := linePrefixSuffix [ 0 ]
var suffix string
2019-06-25 16:57:40 +00:00
if len ( linePrefixSuffix ) > 1 {
2019-06-25 15:57:47 +00:00
suffix = linePrefixSuffix [ 1 ]
2019-06-21 20:14:13 +00:00
}
2019-06-25 15:57:47 +00:00
switch prefix {
2019-06-20 16:17:26 +00:00
case "redis_version" :
2019-06-25 15:57:47 +00:00
result . Version = suffix
2019-06-28 19:54:11 +00:00
versionSegments := strings . SplitN ( suffix , "." , 3 )
if len ( versionSegments ) > 0 {
2019-06-28 20:30:12 +00:00
major := convToUint32 ( versionSegments [ 0 ] )
result . Major = & major
2019-06-28 19:54:11 +00:00
}
if len ( versionSegments ) > 1 {
2019-06-28 20:30:12 +00:00
minor := convToUint32 ( versionSegments [ 1 ] )
result . Minor = & minor
2019-06-28 19:54:11 +00:00
}
if len ( versionSegments ) > 2 {
2019-06-28 20:30:12 +00:00
patchlevel := convToUint32 ( versionSegments [ 2 ] )
result . Patchlevel = & patchlevel
2019-06-28 19:54:11 +00:00
}
2019-06-20 16:17:26 +00:00
case "os" :
2019-06-25 15:57:47 +00:00
result . OS = suffix
2019-06-20 16:17:26 +00:00
case "arch_bits" :
2019-06-25 15:57:47 +00:00
result . ArchBits = suffix
2019-06-20 16:17:26 +00:00
case "redis_mode" :
2019-06-25 15:57:47 +00:00
result . Mode = suffix
2019-06-20 16:17:26 +00:00
case "redis_git_sha1" :
2019-06-25 15:57:47 +00:00
result . GitSha1 = suffix
2019-06-20 16:17:26 +00:00
case "redis_build_id" :
2019-06-25 15:57:47 +00:00
result . BuildID = suffix
2019-06-20 16:17:26 +00:00
case "gcc_version" :
2019-06-25 15:57:47 +00:00
result . GCCVersion = suffix
2019-06-20 16:17:26 +00:00
case "mem_allocator" :
2019-06-25 15:57:47 +00:00
result . MemAllocator = suffix
2019-06-20 16:17:26 +00:00
case "uptime_in_seconds" :
2019-06-25 15:57:47 +00:00
result . Uptime = convToUint32 ( suffix )
2019-06-20 16:17:26 +00:00
case "used_memory" :
2019-06-25 15:57:47 +00:00
result . UsedMemory = convToUint32 ( suffix )
2019-06-20 16:17:26 +00:00
case "total_connections_received" :
2019-06-25 15:57:47 +00:00
result . ConnectionsReceived = convToUint32 ( suffix )
2019-06-20 16:17:26 +00:00
case "total_commands_processed" :
2019-06-25 15:57:47 +00:00
result . CommandsProcessed = convToUint32 ( suffix )
2019-06-19 13:51:40 +00:00
}
2018-02-13 18:25:47 +00:00
}
}
2019-06-19 20:06:43 +00:00
bogusResponse , err := scan . SendCommand ( scanner . commandMappings [ "NONEXISTENT" ] )
2018-02-13 18:25:47 +00:00
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , result , err
}
result . NonexistentResponse = forceToString ( bogusResponse )
2019-06-17 21:32:51 +00:00
for i := range scanner . customCommands {
2019-06-25 15:57:47 +00:00
fullCmd := strings . Fields ( scanner . customCommands [ i ] )
resp , err := scan . SendCommand ( fullCmd [ 0 ] , fullCmd [ 1 : ] ... )
2019-06-17 21:32:51 +00:00
if err != nil {
return zgrab2 . TryGetScanStatus ( err ) , result , err
}
customResponse := CustomResponse {
2019-06-25 15:57:47 +00:00
Command : fullCmd [ 0 ] ,
Arguments : strings . Join ( fullCmd [ 1 : ] , " " ) ,
2019-06-17 21:32:51 +00:00
Response : forceToString ( resp ) ,
}
result . CustomResponses = append ( result . CustomResponses , customResponse )
}
2019-06-19 20:06:43 +00:00
quitResponse , err := scan . SendCommand ( scanner . commandMappings [ "QUIT" ] )
if err != nil && err != io . EOF {
2018-02-13 18:25:47 +00:00
return zgrab2 . TryGetScanStatus ( err ) , result , err
2019-06-19 20:06:43 +00:00
} else if quitResponse == nil {
quitResponse = NullValue
2018-02-13 18:25:47 +00:00
}
result . QuitResponse = forceToString ( quitResponse )
return zgrab2 . SCAN_SUCCESS , & result , nil
}