zgrab2/modules/redis/scanner.go

460 lines
15 KiB
Go
Raw Normal View History

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 (
"encoding/json"
2018-02-13 18:25:47 +00:00
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strconv"
2018-02-13 18:25:47 +00:00
"strings"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
"gopkg.in/yaml.v2"
2018-02-13 18:25:47 +00:00
)
// Flags contains redis-specific command-line flags.
type Flags struct {
zgrab2.BaseFlags
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."`
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 {
config *Flags
commandMappings map[string]string
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"`
// 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"`
// Version is read from the InfoResponse (the field "server_version"), if
// present.
Version string `json:"version,omitempty"`
// 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
// 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"`
// 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"`
// 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"`
// CustomResponses is an array that holds the commands, arguments, and
// responses from user-inputted commands.
CustomResponses []CustomResponse `json:"custom_responses,omitempty"`
// 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
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-02-13 18:25:47 +00:00
// GetPort returns the port being scanned
func (scanner *Scanner) GetPort() uint {
return scanner.config.Port
}
2018-03-21 21:16:58 +00:00
// Close cleans up the scanner.
func (scan *scan) Close() {
defer scan.close()
}
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:
err := fmt.Errorf("File type %s not valid.", ext)
return nil, err
}
return unmarshaler, nil
}
func getFileContents(file string, output interface{}) error {
unmarshaler, err := getUnmarshaler(file)
if err != nil {
return err
}
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 {
scanner.commandMappings = map[string]string{
"PING": "PING",
"AUTH": "AUTH",
"INFO": "INFO",
"NONEXISTENT": "NONEXISTENT",
"QUIT": "QUIT",
}
if scanner.config.CustomCommands != "" {
var customCommands []string
err := getFileContents(scanner.config.CustomCommands, &customCommands)
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
err := getFileContents(scanner.config.Mappings, &mappings)
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")
}
}
// Protocol returns the protocol identifer for the scanner.
func (s *Scanner) Protocol() string {
return "redis"
}
// Converts the string to a Uint32 if possible. If not, returns 0 (the zero value of a uin32)
func convToUint32(s string) uint32 {
s_64, err := strconv.ParseUint(s, 10, 32)
if err != nil {
// TODO: LEARN HOW TO LOG
return 0
}
return uint32(s_64)
}
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
// 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
pingResponse, err := scan.SendCommand(scanner.commandMappings["PING"])
2018-02-13 18:25:47 +00:00
if err != nil {
// 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 != "" {
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)
}
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)
if infoResponseBulk, ok := infoResponse.(BulkString); ok {
fieldsWeAreLookingFor := 12
fieldsFound := 0
2018-02-13 18:25:47 +00:00
for _, line := range strings.Split(string(infoResponseBulk), "\r\n") {
switch line_prefix_suffix := strings.SplitN(line, ":", 2); line_prefix_suffix[0] {
case "redis_version":
fieldsFound += 1
result.Version = line_prefix_suffix[1]
case "os":
fieldsFound += 1
result.OS = line_prefix_suffix[1]
case "arch_bits":
fieldsFound += 1
result.ArchBits = line_prefix_suffix[1]
case "redis_mode":
fieldsFound += 1
result.Mode = line_prefix_suffix[1]
case "redis_git_sha1":
fieldsFound += 1
result.GitSha1 = line_prefix_suffix[1]
case "redis_build_id":
fieldsFound += 1
result.BuildID = line_prefix_suffix[1]
case "gcc_version":
fieldsFound += 1
result.GCCVersion = line_prefix_suffix[1]
case "mem_allocator":
fieldsFound += 1
result.MemAllocator = line_prefix_suffix[1]
case "uptime_in_seconds":
fieldsFound += 1
result.Uptime = convToUint32(line_prefix_suffix[1])
case "used_memory":
2019-06-20 21:14:30 +00:00
fieldsFound += 1
result.UsedMemory = convToUint32(line_prefix_suffix[1])
case "total_connections_received":
fieldsFound += 1
result.ConnectionsReceived = convToUint32(line_prefix_suffix[1])
case "total_commands_processed":
fieldsFound += 1
result.CommandsProcessed = convToUint32(line_prefix_suffix[1])
}
if fieldsWeAreLookingFor == fieldsFound {
2018-02-13 18:25:47 +00:00
break
}
}
}
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)
for i := range scanner.customCommands {
full_cmd := strings.Fields(scanner.customCommands[i])
resp, err := scan.SendCommand(full_cmd[0], full_cmd[1:]...)
if err != nil {
return zgrab2.TryGetScanStatus(err), result, err
}
customResponse := CustomResponse{
Command: full_cmd[0],
Arguments: strings.Join(full_cmd[1:], " "),
Response: forceToString(resp),
}
result.CustomResponses = append(result.CustomResponses, customResponse)
}
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
} else if quitResponse == nil {
quitResponse = NullValue
2018-02-13 18:25:47 +00:00
}
result.QuitResponse = forceToString(quitResponse)
return zgrab2.SCAN_SUCCESS, &result, nil
}