zgrab2/modules/mongodb/scanner.go
dabdine 48f15ef93b
Add mongodb dbnames (#332)
* Allow database names & sizes to be pulled from mongo hosts

* Use cached getListDatabases msg; remove unused code
2022-07-01 15:04:46 -06:00

374 lines
12 KiB
Go

package mongodb
import (
"encoding/binary"
"encoding/hex"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
"gopkg.in/mgo.v2/bson"
)
// Module implements the zgrab2.Module interface
type Module struct {
}
// Flags contains mongodb-specific command-line flags.
type Flags struct {
zgrab2.BaseFlags
}
// Scanner implements the zgrab2.Scanner interface
type Scanner struct {
config *Flags
isMasterMsg []byte
buildInfoCommandMsg []byte
buildInfoOpMsg []byte
listDatabasesMsg []byte
}
// scan holds the state for the scan of an individual target
type scan struct {
scanner *Scanner
result *Result
target *zgrab2.ScanTarget
conn *Connection
close func()
}
// Close cleans up the scanner.
func (scan *scan) Close() {
defer scan.close()
}
// getIsMasterMsg returns a mongodb message containing isMaster command.
// https://docs.mongodb.com/manual/reference/command/isMaster/
func getIsMasterMsg() []byte {
query, err := bson.Marshal(bson.M{"isMaster": 1})
if err != nil {
// programmer error
log.Fatalf("Invalid BSON: %v", err)
}
query_msg := getOpQuery("admin.$cmd", query)
return query_msg
}
// getBuildInfoQuery returns a mongodb message containing a command to retrieve MongoDB build info.
func getBuildInfoQuery() []byte {
query, err := bson.Marshal(bson.M{"buildinfo": 1})
if err != nil {
// programmer error
log.Fatalf("Invalid BSON: %v", err)
}
query_msg := getOpQuery("admin.$cmd", query)
return query_msg
}
// getListDatabasesMsg returns a mongodb message containing a command to retrieve MongoDB database info.
func getListDatabasesMsg() []byte {
query, err := bson.Marshal(bson.M{"listDatabases": 1})
if err != nil {
// programmer error
log.Fatalf("Invalid BSON: %v", err)
}
query_msg := getOpQuery("admin.$cmd", query)
return query_msg
}
// getOpQuery returns a mongodb OP_QUERY message containing the specified BSON-encoded query.
// query expected to be BSON byte array.
func getOpQuery(collname string, query []byte) []byte {
flagslen := 4
collname_len := len(collname) + 1
nskiplen := 4
nretlen := 4
qlen := len(query)
msglen := MSGHEADER_LEN + flagslen + collname_len + nskiplen + nretlen + qlen
out := make([]byte, msglen)
// msg header
binary.LittleEndian.PutUint32(out[0:], uint32(msglen))
binary.LittleEndian.PutUint32(out[12:], OP_QUERY)
// query msg
idx := MSGHEADER_LEN + flagslen
copy(out[idx:idx+collname_len], []byte(collname))
idx += collname_len + nskiplen
binary.LittleEndian.PutUint32(out[idx:idx+nretlen], 1)
idx += nretlen
copy(out[idx:idx+qlen], query)
return out
}
// getOpMsg returns a mongodb OP_MSG message containing the specified BSON-encoded command.
// section expected to be BSON byte array.
func getOpMsg(section []byte) []byte {
flagslen := 4
slen := len(section)
msglen := MSGHEADER_LEN + flagslen + slen
out := make([]byte, msglen)
// msg header
binary.LittleEndian.PutUint32(out[0:], uint32(msglen))
binary.LittleEndian.PutUint32(out[12:], OP_MSG)
// command msg
idx := MSGHEADER_LEN + flagslen
copy(out[idx:idx+slen], []byte(section))
return out
}
// getBuildInfoOpMsg returns a mongodb "OP" message containing query to retrieve MongoDB build info.
func getBuildInfoOpMsg() []byte {
// gleaned from tshark
section_payload, err := bson.Marshal(bson.M{"buildinfo": 1, "$db": "admin"})
if err != nil {
// programmer error
log.Fatalf("Invalid BSON: %v", err)
}
section := make([]byte, len(section_payload)+1)
copy(section[1:], section_payload)
op_msg := getOpMsg(section)
return op_msg
}
// BuildEnvironment_t holds build environment information returned by scan.
type BuildEnvironment_t struct {
Distmod string `bson:"distmod,omitempty" json:"dist_mod,omitempty"`
Distarch string `bson:"distarch,omitempty" json:"dist_arch,omitempty"`
Cc string `bson:"cc,omitempty" json:"cc,omitempty"`
CcFlags string `bson:"ccflags,omitempty" json:"cc_flags,omitempty"`
Cxx string `bson:"cxx,omitempty" json:"cxx,omitempty"`
CxxFlags string `bson:"cxxflags,omitempty" json:"cxx_flags,omitempty"`
LinkFlags string `bson:"linkflags,omitempty" json:"link_flags,omitempty"`
TargetArch string `bson:"target_arch,omitempty" json:"target_arch,omitempty"`
TargetOS string `bson:"target_os,omitempty" json:"target_os,omitempty"`
}
// BuildInfo_t holds the data returned by the the buildInfo query
type BuildInfo_t struct {
Version string `bson:"version,omitempty" json:"version,omitempty"`
GitVersion string `bson:"gitVersion,omitempty" json:"git_version,omitempty"`
SysInfo string `bson:"sysInfo,omitempty" json:"sys_info,omitempty"`
LoaderFlags string `bson:"loaderFlags,omitempty" json:"loader_flags,omitempty"`
CompilerFlags string `bson:"compilerFlags,omitempty" json:"compiler_flags,omitempty"`
Allocator string `bson:"allocator,omitempty" json:"allocator,omitempty"`
Debug bool `bson:"debug,omitempty" json:"debug,omitempty"`
Bits int32 `bson:"bits,omitempty" json:"bits,omitempty"`
MaxBsonObjectSize int32 `bson:"maxBsonObjectSize,omitempty" json:"max_bson_object_size,omitempty"`
JavascriptEngine string `bson:"javascriptEngine,omitempty" json:"javascript_engine,omitempty"`
StorageEngines []string `bson:"storageEngines,omitempty" json:"storage_engines,omitempty"`
BuildEnvironment BuildEnvironment_t `bson:"buildEnvironment,omitempty" json:"build_environment,omitempty"`
}
// IsMaster_t holds the data returned by an isMaster query
type IsMaster_t struct {
IsMaster bool `bson:"ismaster" json:"is_master"`
MaxWireVersion int32 `bson:"maxWireVersion,omitempty" json:"max_wire_version,omitempty"`
MinWireVersion int32 `bson:"minWireVersion,omitempty" json:"min_wire_version,omitempty"`
MaxBsonObjectSize int32 `bson:"maxBsonObjectSize,omitempty" json:"max_bson_object_size,omitempty"`
MaxWriteBatchSize int32 `bson:"maxWriteBatchSize,omitempty" json:"max_write_batch_size,omitempty"`
LogicalSessionTimeoutMinutes int32 `bson:"logicalSessionTimeoutMinutes,omitempty" json:"logical_session_timeout_minutes,omitempty"`
MaxMessageSizeBytes int32 `bson:"maxMessageSizeBytes,omitempty" json:"max_message_size_bytes,omitempty"`
ReadOnly bool `bson:"readOnly" json:"read_only"`
}
type DatabaseInfo_t struct {
Name string `bson:"name" json:"name"`
SizeOnDisk int64 `bson:"sizeOnDisk" json:"size_on_disk"`
Empty bool `bson:"empty" json:"empty"`
}
type ListDatabases_t struct {
Databases []DatabaseInfo_t `bson:"databases,omitempty" json:"databases,omitempty"`
TotalSize int64 `bson:"totalSize,omitempty" json:"total_size,omitempty"`
}
// Result holds the data returned by a scan
type Result struct {
IsMaster *IsMaster_t `json:"is_master,omitempty"`
BuildInfo *BuildInfo_t `json:"build_info,omitempty"`
DatabaseInfo *ListDatabases_t `json:"database_info,omitempty"`
}
// Init initializes the scanner
func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
f, _ := flags.(*Flags)
scanner.config = f
scanner.isMasterMsg = getIsMasterMsg()
scanner.buildInfoCommandMsg = getBuildInfoQuery()
scanner.buildInfoOpMsg = getBuildInfoOpMsg()
scanner.listDatabasesMsg = getListDatabasesMsg()
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
}
// Protocol returns the protocol identifer for the scanner.
func (s *Scanner) Protocol() string {
return "mongodb"
}
// GetTrigger returns the Trigger defined in the Flags.
func (scanner *Scanner) GetTrigger() string {
return scanner.config.Trigger
}
// 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 ""
}
// 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)
}
// Description returns an overview of this module.
func (module *Module) Description() string {
return "Perform a handshake with a MongoDB server"
}
// 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,
},
close: func() { conn.Close() },
}, nil
}
// getIsMaster issues the isMaster command to the MongoDB server and returns the result.
func getIsMaster(conn *Connection) (*IsMaster_t, error) {
document := &IsMaster_t{}
doc_offset := MSGHEADER_LEN + 20
conn.Write(conn.scanner.isMasterMsg)
msg, err := conn.ReadMsg()
if err != nil {
return nil, err
}
if len(msg) < doc_offset+4 {
err = fmt.Errorf("Server truncated message - no query reply (%d bytes: %s)", len(msg), hex.EncodeToString(msg))
return nil, err
}
respFlags := binary.LittleEndian.Uint32(msg[MSGHEADER_LEN : MSGHEADER_LEN+4])
if respFlags&QUERY_RESP_FAILED != 0 {
err = fmt.Errorf("isMaster query failed")
return nil, err
}
doclen := int(binary.LittleEndian.Uint32(msg[doc_offset : doc_offset+4]))
if len(msg[doc_offset:]) < doclen {
err = fmt.Errorf("Server truncated BSON reply doc (%d bytes: %s)",
len(msg[doc_offset:]), hex.EncodeToString(msg))
return nil, err
}
err = bson.Unmarshal(msg[doc_offset:], &document)
if err != nil {
err = fmt.Errorf("Server sent invalid BSON reply doc (%d bytes: %s)",
len(msg[doc_offset:]), hex.EncodeToString(msg))
return nil, err
}
return document, nil
}
func listDatabases(conn *Connection) (*ListDatabases_t, error) {
document := ListDatabases_t{}
conn.Write(conn.scanner.listDatabasesMsg)
msg, err := conn.ReadMsg()
if err != nil {
return nil, err
}
bson.Unmarshal(msg[MSGHEADER_LEN+20:], &document)
return &document, nil
}
// Scan connects to a host and performs a scan.
// https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.rst
func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
scan, err := scanner.StartScan(&target)
if err != nil {
return zgrab2.TryGetScanStatus(err), nil, err
}
defer scan.Close()
result := scan.result
result.IsMaster, err = getIsMaster(scan.conn)
if err != nil {
return zgrab2.SCAN_PROTOCOL_ERROR, nil, err
}
result.DatabaseInfo, err = listDatabases(scan.conn)
if err != nil {
return zgrab2.SCAN_PROTOCOL_ERROR, nil, err
}
var query []byte
var resplen_offset int
var resp_offset int
// See: https://github.com/mongodb/specifications/blob/master/source/message/OP_MSG.rst
// "OP_MSG is only available in MongoDB 3.6 (maxWireVersion >= 6) and later."
if result.IsMaster.MaxWireVersion < 6 {
query = scanner.buildInfoCommandMsg
resplen_offset = 4
resp_offset = 20
} else {
query = scanner.buildInfoOpMsg
resplen_offset = 5
resp_offset = 5
}
scan.conn.Write(query)
msg, err := scan.conn.ReadMsg()
if err != nil {
return zgrab2.TryGetScanStatus(err), &result, err
}
if len(msg) < MSGHEADER_LEN+resplen_offset {
err = fmt.Errorf("Server truncated message - no metadata doc (%d bytes: %s)", len(msg), hex.EncodeToString(msg))
return zgrab2.SCAN_PROTOCOL_ERROR, &result, err
}
bson.Unmarshal(msg[MSGHEADER_LEN+resp_offset:], &result.BuildInfo)
return zgrab2.SCAN_SUCCESS, &result, err
}
// RegisterModule registers the zgrab2 module.
func RegisterModule() {
var module Module
_, err := zgrab2.AddCommand("mongodb", "mongodb", module.Description(), 27017, &module)
if err != nil {
log.Fatal(err)
}
}