zgrab2/modules/mysql/scanner.go

239 lines
7.8 KiB
Go

// Package mysql provides the mysql implementation of the zgrab2.Module.
// Grabs the HandshakePacket (or ERRPacket) that the server sends
// immediately upon connecting, and then if applicable negotiate an SSL
// connection.
package mysql
import (
"reflect"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
"github.com/zmap/zgrab2/lib/mysql"
)
// ScanResults contains detailed information about the scan.
type ScanResults struct {
// ProtocolVersion is the 8-bit unsigned integer representing the
// server's protocol version sent in the initial HandshakePacket from
// the server.
// This has been 10 for all MySQL versionssince 3.2.2 (from 1998).
ProtocolVersion byte `json:"protocol_version"`
// ServerVersion is a null-terminated string giving the specific
// server version in the initial HandshakePacket. Often of the format
// x.y.z, but not always.
ServerVersion string `json:"server_version"`
// ConnectionID is the server's internal identifier for this client's
// connection, sent in the initial HandshakePacket.
ConnectionID uint32 `json:"connection_id" zgrab:"debug"`
// AuthPluginData is optional plugin-specific data, whose meaning
// depends on the value of AuthPluginName. Returned in the initial
// HandshakePacket.
AuthPluginData []byte `json:"auth_plugin_data" zgrab:"debug"`
// CharacterSet is the identifier for the character set the server is
// using. Returned in the initial HandshakePacket.
CharacterSet byte `json:"character_set" zgrab:"debug"`
// StatusFlags is the set of status flags the server returned in the
// initial HandshakePacket. Each true entry in the map corresponds to
// a bit set to 1 in the flags, where the keys correspond to the
// #defines in the MySQL docs.
StatusFlags map[string]bool `json:"status_flags"`
// CapabilityFlags is the set of capability flags the server returned
// initial HandshakePacket. Each true entry in the map corresponds to
// a bit set to 1 in the flags, where the keys correspond to the
// #defines in the MySQL docs.
CapabilityFlags map[string]bool `json:"capability_flags"`
// AuthPluginName is the name of the authentication plugin, returned
// in the initial HandshakePacket.
AuthPluginName string `json:"auth_plugin_name,omitempty" zgrab:"debug"`
// ErrorCode is only set if there is an error returned by the server,
// for example if the scanner is not on the allowed hosts list.
ErrorCode *int `json:"error_code,omitempty"`
// ErrorMessage is an optional string describing the error. Only set
// if there is an error.
ErrorMessage string `json:"error_message,omitempty"`
// RawPackets contains the base64 encoding of all packets sent and
// received during the scan.
RawPackets []string `json:"raw_packets,omitempty"`
// TLSLog contains the usual shared TLS logs.
TLSLog *zgrab2.TLSLog `json:"tls,omitempty"`
}
// Convert the ConnectionLog into the output format.
func readResultsFromConnectionLog(connectionLog *mysql.ConnectionLog) *ScanResults {
ret := ScanResults{}
if connectionLog == nil {
return nil
}
// If we received neither a Handshake nor an Error message, then no
// MySQL service is detected.
if connectionLog.Handshake == nil && connectionLog.Error == nil {
return nil
}
if connectionLog.Handshake != nil {
ret.RawPackets = append(ret.RawPackets, connectionLog.Handshake.Raw)
switch handshake := connectionLog.Handshake.Parsed.(type) {
case *mysql.HandshakePacket:
ret.ProtocolVersion = handshake.ProtocolVersion
ret.ServerVersion = handshake.ServerVersion
ret.ConnectionID = handshake.ConnectionID
len1 := len(handshake.AuthPluginData1)
ret.AuthPluginData = make([]byte, len1+len(handshake.AuthPluginData2))
copy(ret.AuthPluginData[0:len1], handshake.AuthPluginData1)
copy(ret.AuthPluginData[len1:], handshake.AuthPluginData2)
ret.CharacterSet = handshake.CharacterSet
ret.StatusFlags = mysql.GetServerStatusFlags(handshake.StatusFlags)
ret.CapabilityFlags = mysql.GetClientCapabilityFlags(handshake.CapabilityFlags)
ret.AuthPluginName = handshake.AuthPluginName
default:
log.Fatalf("Unreachable code -- ConnectionLog.Handshake was set to a non-handshake packet: %v / %v", connectionLog.Handshake.Parsed, reflect.TypeOf(connectionLog.Handshake.Parsed))
}
}
if connectionLog.Error != nil {
ret.RawPackets = append(ret.RawPackets, connectionLog.Error.Raw)
switch err := connectionLog.Error.Parsed.(type) {
case *mysql.ERRPacket:
temp := int(err.ErrorCode)
ret.ErrorCode = &temp
ret.ErrorMessage = err.ErrorMessage
default:
temp := -1
ret.ErrorCode = &temp
ret.ErrorMessage = "Unexpected packet type"
}
}
if connectionLog.SSLRequest != nil {
ret.RawPackets = append(ret.RawPackets, connectionLog.SSLRequest.Raw)
}
return &ret
}
// Flags give the command-line flags for the MySQL module.
type Flags struct {
zgrab2.BaseFlags
zgrab2.TLSFlags
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
}
// Module is the implementation of the zgrab2.Module interface.
type Module struct {
}
// Scanner is the implementation of the zgrab2.Scanner interface.
type Scanner struct {
config *Flags
}
// RegisterModule is called by modules/mysql.go to register the scanner.
func RegisterModule() {
var module Module
_, err := zgrab2.AddCommand("mysql", "MySQL", "Grab a MySQL handshake", 3306, &module)
if err != nil {
log.Fatal(err)
}
}
// NewFlags returns a new default flags object.
func (m *Module) NewFlags() interface{} {
return new(Flags)
}
// NewScanner returns a new Scanner object.
func (m *Module) NewScanner() zgrab2.Scanner {
return new(Scanner)
}
// Validate validates the flags and returns nil on success.
func (f *Flags) Validate(args []string) error {
return nil
}
// Help returns the module's help string.
func (f *Flags) Help() string {
return ""
}
// Init initializes the Scanner with the command-line flags.
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
}
// Protocol returns the protocol identifer for the scanner.
func (s *Scanner) Protocol() string {
return "mysql"
}
// GetName returns the name from the command line flags.
func (s *Scanner) GetName() string {
return s.config.Name
}
// GetPort returns the port that is being scanned.
func (s *Scanner) GetPort() uint {
return s.config.Port
}
// Scan probles the target for a MySQL server.
// 1. Connects and waits to receive the handshake packet.
// 2. If the server supports SSL, send an SSLRequest packet, then
// perform the standard TLS actions.
// 3. Process and return the results.
func (s *Scanner) Scan(t zgrab2.ScanTarget) (status zgrab2.ScanStatus, result interface{}, thrown error) {
var tlsConn *zgrab2.TLSConnection
sql := mysql.NewConnection(&mysql.Config{})
defer func() {
recovered := recover()
if recovered != nil {
thrown = recovered.(error)
status = zgrab2.TryGetScanStatus(thrown)
// TODO FIXME: do more to distinguish errors
}
result = readResultsFromConnectionLog(&sql.ConnectionLog)
if tlsConn != nil {
result.(*ScanResults).TLSLog = tlsConn.GetLog()
}
}()
defer sql.Disconnect()
var err error
conn, err := t.Open(&s.config.BaseFlags)
if err != nil {
panic(err)
}
if err = sql.Connect(conn); err != nil {
panic(err)
}
if sql.SupportsTLS() {
if err = sql.NegotiateTLS(); err != nil {
panic(err)
}
if tlsConn, err = s.config.TLSFlags.GetTLSConnection(sql.Connection); err != nil {
panic(err)
}
if err = tlsConn.Handshake(); err != nil {
panic(err)
}
// Replace sql.Connection to allow hypothetical future calls to go over the secure connection
sql.Connection = tlsConn
}
// If we made it this far, the scan was a success. The result will be grabbed in the defer block above.
return zgrab2.SCAN_SUCCESS, nil, nil
}