Module: Msf::Exploit::Remote::Mongodb
- Includes:
- Tcp
- Defined in:
- lib/msf/core/exploit/remote/mongodb.rb
Overview
This mixin provides the MongoDB functionality shared by the auxiliary scanner modules: the low-level wire protocol helpers (OP_QUERY and OP_MSG packet construction, reply framing, BSON reply parsing, and cursor draining via getMore) with per-connection protocol negotiation, the shared #get_version buildInfo probe and #mongodb_auth_state auth probe, and the single shared authentication entry point, #authenticate, which tries SCRAM-SHA-1 (MongoDB 3.0+) and falls back to legacy MONGODB-CR (MongoDB < 3.0). Modules build their flows on these primitives and must not reimplement the version probe or auth handshakes; only the module-specific parts (credential reporting, brute-force loops) stay in the modules themselves.
Constant Summary collapse
- AUTH_ERROR_CODES =
MongoDB error codes that explicitly indicate an authentication or authorization failure. 13 = Unauthorized, 18 = AuthenticationFailed. https://www.mongodb.com/docs/manual/reference/error-codes/
[13, 18].freeze
- AUTH_ERROR_CODENAMES =
The codeName counterparts of AUTH_ERROR_CODES.
%w[Unauthorized AuthenticationFailed].freeze
- AUTH_ERROR_MESSAGES =
Substrings of an errmsg (or of a raw reply body that will not parse as BSON) that indicate a command failed for authentication reasons rather than some unrelated error.
[ 'unauthorized', 'not authorized', 'authentication failed', 'auth failed', 'requires authentication', 'need to login' ].freeze
- MAX_MESSAGE_LENGTH =
Upper bound on the length of a single wire protocol message accepted from the server. mongod caps maxMessageSizeBytes far below this, so a declared length over it is corrupt and must not be trusted for framing.
64 * 1024 * 1024
- OP_MSG =
MongoDB wire protocol opcode for OP_MSG, the modern command transport. Introduced in 3.6; from 6.0 on it is the only way to run anything beyond the handshake/buildInfo/SASL allowlist that OP_QUERY answers.
2013- MAX_GETMORE_BATCHES =
Safety cap on getMore rounds so a server that keeps reporting an open cursor cannot loop forever; far past any realistic system.users or listCollections result set.
1000
Instance Attribute Summary
Attributes included from Tcp
Instance Method Summary collapse
-
#authenticate(user: , pass: , db: ) ⇒ String?
Authenticates against the server: SCRAM-SHA-1 first (MongoDB 3.0+), falling back to legacy MONGODB-CR (MongoDB < 3.0) when SCRAM is not accepted.
- #disconnect ⇒ Object
-
#get_version ⇒ String?
Retrieves the server version with the buildInfo command.
- #initialize(info = {}) ⇒ Object
-
#mongodb_auth_state ⇒ Symbol
Determines whether the server enforces authentication, as far as an unauthenticated probe allows:.
-
#mongodb_build_msg_packet(db, bson_payload) ⇒ String
Builds a MongoDB wire protocol OP_MSG message (opcode 2013), the modern command transport supported from 3.6 on.
-
#mongodb_build_packet(coll_name, bson_payload, number_to_return: 1) ⇒ String
Builds a MongoDB wire protocol OP_QUERY message.
-
#mongodb_doc_auth_error?(doc) ⇒ Boolean
True only when the document explicitly indicates an authentication or authorization failure: error codes 13/18, their codeNames, or an errmsg matching a known auth phrase.
-
#mongodb_encode_scram_username(user) ⇒ String?
Encodes a username for use in a SCRAM client-first message per RFC 5802 section 5.1: '=' becomes '=3D' and ',' becomes '=2C' so they cannot be mistaken for payload delimiters.
-
#mongodb_getmore_collection(cursor_doc, fallback) ⇒ String?
Derives the collection argument for a getMore command from a cursor's namespace ("db.system.users" -> "system.users", "db.$cmd.listCollections" -> "$cmd.listCollections"), falling back to the collection the original query ran against when the cursor carries no namespace.
-
#mongodb_have_auth_error?(response) ⇒ Boolean
True only when the response explicitly indicates an authentication or authorization failure; see #mongodb_doc_auth_error? for what counts.
-
#mongodb_msg_query_all(sock, db, coll, bson_payload, timeout: 5, number_to_return: 0) ⇒ Array<BSON::Document>
OP_MSG counterpart of #mongodb_query_all: runs a find against a collection (or an arbitrary command when coll is '$cmd') and drains the reply cursor with getMore commands.
-
#mongodb_parse_cursor_id(response) ⇒ Integer
Extracts the cursor id from the OP_REPLY header of a raw response.
-
#mongodb_parse_doc(response) ⇒ BSON::Document?
Parses the single BSON reply document from a raw OP_REPLY response.
-
#mongodb_parse_docs(response) ⇒ Array<BSON::Document>
Parses every BSON document present in a raw OP_REPLY response body.
-
#mongodb_parse_msg(response) ⇒ Array<BSON::Document, Hash>
Parses an OP_MSG reply into its body document and any document sequences.
-
#mongodb_parse_scram_payload(payload) ⇒ Hash<String, String>
Parses a SCRAM
key=value,key=valuewire payload into a Hash. -
#mongodb_query_all(sock, namespace, bson_payload, timeout: 5, number_to_return: 0) ⇒ Array<BSON::Document>
Sends an OP_QUERY and drains the reply cursor with getMore commands until it is exhausted, so a namespace holding more documents than the first OP_REPLY batch (101 documents by default) is fully retrieved.
-
#mongodb_query_error(docs) ⇒ String?
Detects a legacy OP_QUERY error reply hiding among the result documents: failed collection queries are answered with an ordinary document carrying
$err(orerrmsg) plus ok: 0 instead of data, so a dump that only checks for empty results mistakes them for records. -
#mongodb_read_exact(sock, length, timeout) ⇒ String?
Reads exactly
lengthbytes from the socket. -
#mongodb_read_message(sock, timeout = 5) ⇒ String?
Reads one complete MongoDB wire protocol message: the 16 byte standard header first, then messageLength minus 16 body bytes, so a large reply split across TCP segments is fully reassembled before it is parsed.
-
#mongodb_run_command(db, cmd, timeout: 5) ⇒ Array<BSON::Document, Boolean>
Runs a single command document over whichever wire protocol the server negotiated, normalizing the reply.
-
#mongodb_use_op_msg? ⇒ Boolean
Negotiates the wire protocol for this connection, memoized: MongoDB 3.6+ speak OP_MSG, which from 6.0 on is the only transport able to run anything beyond the handshake/buildInfo/SASL allowlist that OP_QUERY still answers, while older servers only understand OP_QUERY.
-
#require_auth? ⇒ Boolean
Backward-compatible boolean view of #mongodb_auth_state: true only when the server verifiably enforces authentication; :unknown does not count as required.
Methods included from Tcp
#chost, #cleanup, #connect, #connect_timeout, #cport, #handler, #lhost, #lport, #peer, #print_prefix, #proxies, #replicant, #rhost, #rport, #set_tcp_evasions, #shutdown, #ssl, #ssl_cipher, #ssl_verify_mode, #ssl_version, #sslkeylogfile
Instance Method Details
#authenticate(user: , pass: , db: ) ⇒ String?
Authenticates against the server: SCRAM-SHA-1 first (MongoDB 3.0+), falling back to legacy MONGODB-CR (MongoDB < 3.0) when SCRAM is not accepted. This is the single authentication entry point shared by all MongoDB modules; do not reimplement the handshakes per module.
667 668 669 670 671 672 673 674 675 676 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 667 def authenticate(user: datastore['USERNAME'], pass: datastore['PASSWORD'], db: datastore['AUTH_DB']) mechanism = mongodb_auth_scram_sha1(user, pass, db) return mechanism if mechanism vprint_status('SCRAM-SHA-1 not accepted or failed; trying MONGODB-CR fallback...') nonce = mongodb_get_nonce(db) return nil if nonce.blank? mongodb_auth_cr(user, pass, db, nonce) end |
#disconnect ⇒ Object
100 101 102 103 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 100 def disconnect @mongodb_use_op_msg = nil super end |
#get_version ⇒ String?
Retrieves the server version with the buildInfo command. buildInfo answers without authentication through MongoDB 8.0; 8.1+ gates it behind authentication. With a username configured the mixin authenticates on the connection and asks again for the real version string (the password may legitimately be empty); without credentials the auth error itself dates the server, which is reported as '8.1+'. Note that a successful retry leaves the connection authenticated, so anything that probes for enforced authentication (like #require_auth?) must run before calling this. When a version is identified the service is reported to the database (when the host module supports reporting).
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 619 def get_version buildinfo_cmd = BSON::Document.new({ 'buildInfo' => BSON::Int32.new(1) }) doc, auth_error = mongodb_run_command('admin', buildinfo_cmd) return nil unless doc version_str = doc['version'] if version_str.nil? && auth_error if datastore['USERNAME'].to_s.empty? version_str = '8.1+' else vprint_status('buildInfo requires authentication; retrying with configured credentials') if authenticate doc, = mongodb_run_command('admin', buildinfo_cmd) version_str = doc['version'] if doc end version_str ||= '8.1+' end end return nil if version_str.nil? if respond_to?(:report_service) report_service( host: rhost, port: rport, name: 'mongodb', proto: 'tcp', info: "MongoDB #{version_str}" ) end version_str rescue StandardError => e vprint_error("Failed to parse version from buildInfo: #{e.}") nil end |
#initialize(info = {}) ⇒ Object
53 54 55 56 57 58 59 60 61 62 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 53 def initialize(info = {}) super ([ Opt::RPORT(27017), OptString.new('AUTH_DB', [ false, 'Database to authenticate against', 'admin' ]), OptString.new('USERNAME', [ false, 'Username for authentication', '' ]), OptString.new('PASSWORD', [ false, 'Password for authentication', '' ]), ]) end |
#mongodb_auth_state ⇒ Symbol
Determines whether the server enforces authentication, as far as an unauthenticated probe allows:
- Over OP_MSG (3.6+) listDatabases answers with an authentication error on servers that enforce auth and succeeds otherwise, which separates the two states on every version.
- Over legacy OP_QUERY (pre-3.6) listDatabases separates the states the same way. Should it fail for another reason, the state falls back to buildInfo inference: 8.1+ gates it behind authentication (auth error = required, success = disabled), while 6.0-8.0 servers reject most OP_QUERY commands identically with auth on or off and report :unknown rather than a guess.
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 575 def mongodb_auth_state doc, auth_error = mongodb_run_command('admin', BSON::Document.new({ 'listDatabases' => BSON::Int32.new(1) })) return :required if auth_error return :absent if doc && doc['ok'].to_i == 1 return :unknown if mongodb_use_op_msg? doc, auth_error = mongodb_run_command('admin', BSON::Document.new({ 'buildInfo' => BSON::Int32.new(1) })) return :required if auth_error version = doc.is_a?(Hash) ? doc['version'] : nil return :unknown if version.to_s.empty? begin Rex::Version.new(version) >= Rex::Version.new('8.1') ? :absent : :unknown rescue StandardError :unknown end end |
#mongodb_build_msg_packet(db, bson_payload) ⇒ String
Builds a MongoDB wire protocol OP_MSG message (opcode 2013), the modern command transport supported from 3.6 on. Unlike OP_QUERY the database is not part of a namespace: the $db document field selects it, so it is injected into the (re-serialized) command document when not already present.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 157 def mongodb_build_msg_packet(db, bson_payload) doc = BSON::Document.from_bson(BSON::ByteBuffer.new(bson_payload)) doc['$db'] = db unless doc.key?('$db') payload = doc.to_bson.to_s msg_len = 16 + 4 + 1 + payload.length packet = [msg_len].pack('V') packet << Rex::Text.rand_text(4) # requestID packet << "\x00\x00\x00\x00" # responseTo: 0 packet << [OP_MSG].pack('V') # opCode: 2013 packet << "\x00\x00\x00\x00" # flagBits packet << "\x00" # section kind: body packet << payload packet end |
#mongodb_build_packet(coll_name, bson_payload, number_to_return: 1) ⇒ String
Builds a MongoDB wire protocol OP_QUERY message.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 131 def mongodb_build_packet(coll_name, bson_payload, number_to_return: 1) coll_str = "#{coll_name}\x00" req_id = Rex::Text.rand_text(4) msg_len = 16 + 4 + coll_str.length + 4 + 4 + bson_payload.length packet = [msg_len].pack('V') packet << req_id # requestID packet << "\x00\x00\x00\x00" # responseTo: 0 packet << "\xd4\x07\x00\x00" # opCode: 2004 (OP_QUERY) packet << "\x00\x00\x00\x00" # flags packet << coll_str # fullCollectionName packet << "\x00\x00\x00\x00" # numberToSkip: 0 packet << [number_to_return].pack('V') # numberToReturn packet << bson_payload packet end |
#mongodb_doc_auth_error?(doc) ⇒ Boolean
Returns true only when the document explicitly indicates an authentication or authorization failure: error codes 13/18, their codeNames, or an errmsg matching a known auth phrase. A generic ok:0 failure (unknown command, bad syntax, ...) is not treated as an auth error, since that would misclassify servers and trigger pointless brute forcing.
535 536 537 538 539 540 541 542 543 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 535 def mongodb_doc_auth_error?(doc) return false unless doc.is_a?(Hash) # BSON::Document subclasses Hash return true if AUTH_ERROR_CODES.include?(doc['code'].to_i) return true if doc['codeName'] && AUTH_ERROR_CODENAMES.include?(doc['codeName']) errmsg = doc['errmsg'].to_s.downcase AUTH_ERROR_MESSAGES.any? { |msg| errmsg.include?(msg) } end |
#mongodb_encode_scram_username(user) ⇒ String?
Encodes a username for use in a SCRAM client-first message per RFC 5802 section 5.1: '=' becomes '=3D' and ',' becomes '=2C' so they cannot be mistaken for payload delimiters. Usernames containing a nul byte cannot be represented at all and are rejected.
287 288 289 290 291 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 287 def mongodb_encode_scram_username(user) return nil if user.nil? || user.include?("\x00") user.gsub('=', '=3D').gsub(',', '=2C') end |
#mongodb_getmore_collection(cursor_doc, fallback) ⇒ String?
Derives the collection argument for a getMore command from a cursor's namespace ("db.system.users" -> "system.users", "db.$cmd.listCollections" -> "$cmd.listCollections"), falling back to the collection the original query ran against when the cursor carries no namespace.
504 505 506 507 508 509 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 504 def mongodb_getmore_collection(cursor_doc, fallback) ns = cursor_doc ? cursor_doc['ns'].to_s : '' coll = ns.empty? ? nil : ns.split('.', 2)[1] coll.nil? || coll.empty? ? fallback : coll end |
#mongodb_have_auth_error?(response) ⇒ Boolean
Returns true only when the response explicitly indicates an authentication or authorization failure; see #mongodb_doc_auth_error? for what counts.
549 550 551 552 553 554 555 556 557 558 559 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 549 def mongodb_have_auth_error?(response) return true if response.nil? || response.length <= 36 doc = mongodb_parse_doc(response) return mongodb_doc_auth_error?(doc) if doc # Unparseable body: search the raw bytes for the same auth phrases # rather than treating any error text as an auth failure. raw = response[36..].to_s.downcase AUTH_ERROR_MESSAGES.any? { |msg| raw.include?(msg) } end |
#mongodb_msg_query_all(sock, db, coll, bson_payload, timeout: 5, number_to_return: 0) ⇒ Array<BSON::Document>
OP_MSG counterpart of #mongodb_query_all: runs a find against a collection (or an arbitrary command when coll is '$cmd') and drains the reply cursor with getMore commands. OP_MSG has no plain-query opcode, so collection reads become find commands, and batches may arrive either inside the cursor document or as a kind 1 'documents' sequence.
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 445 def mongodb_msg_query_all(sock, db, coll, bson_payload, timeout: 5, number_to_return: 0) payload_doc = begin BSON::Document.from_bson(BSON::ByteBuffer.new(bson_payload.to_s)) rescue StandardError BSON::Document.new end if coll == '$cmd' cmd = payload_doc else cmd = BSON::Document.new({ 'find' => coll, 'filter' => payload_doc }) cmd['batchSize'] = BSON::Int32.new(number_to_return) if number_to_return > 0 end body, sequences = mongodb_msg_send(sock, db, cmd, timeout) return [] if body.nil? return [body] if body['ok'] && body['ok'].to_i != 1 cursor_doc = body['cursor'] return [body] unless cursor_doc # e.g. listDatabases: not a cursor command docs = mongodb_msg_batch(sequences, cursor_doc, 'firstBatch') cursor_id = cursor_doc['id'].is_a?(Integer) ? cursor_doc['id'] : 0 getmore_collection = mongodb_getmore_collection(cursor_doc, coll == '$cmd' ? nil : coll) batches = 0 while cursor_id != 0 break if getmore_collection.nil? || getmore_collection.empty? break if (batches += 1) > MAX_GETMORE_BATCHES getmore_cmd = BSON::Document.new({ 'getMore' => BSON::Int64.new(cursor_id), 'collection' => getmore_collection }) getmore_cmd['batchSize'] = BSON::Int32.new(number_to_return) if number_to_return > 0 vprint_status("Following cursor #{cursor_id} on #{db}.#{coll} (#{docs.length} documents so far)") body, sequences = mongodb_msg_send(sock, db, getmore_cmd, timeout) break if body.nil? || body['ok'].to_i != 1 cursor_doc = body['cursor'] break unless cursor_doc docs.concat(mongodb_msg_batch(sequences, cursor_doc, 'nextBatch')) cursor_id = cursor_doc['id'].is_a?(Integer) ? cursor_doc['id'] : 0 getmore_collection = mongodb_getmore_collection(cursor_doc, getmore_collection) end docs end |
#mongodb_parse_cursor_id(response) ⇒ Integer
Extracts the cursor id from the OP_REPLY header of a raw response.
342 343 344 345 346 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 342 def mongodb_parse_cursor_id(response) return 0 if response.nil? || response.length < 28 response[20, 8].unpack1('Q<') end |
#mongodb_parse_doc(response) ⇒ BSON::Document?
Parses the single BSON reply document from a raw OP_REPLY response.
178 179 180 181 182 183 184 185 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 178 def mongodb_parse_doc(response) return nil if response.nil? || response.length <= 36 buffer = BSON::ByteBuffer.new(response[36..]) BSON::Document.from_bson(buffer) rescue StandardError nil end |
#mongodb_parse_docs(response) ⇒ Array<BSON::Document>
Parses every BSON document present in a raw OP_REPLY response body.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 191 def mongodb_parse_docs(response) return [] if response.nil? || response.length <= 36 data = response[36..] return [] if data.nil? || data.empty? docs = [] offset = 0 while offset < data.length break if offset + 4 > data.length doc_len = data[offset, 4].unpack1('V') break if doc_len.nil? || doc_len <= 0 || (offset + doc_len) > data.length buffer = BSON::ByteBuffer.new(data[offset, doc_len]) docs << BSON::Document.from_bson(buffer) offset += doc_len end docs rescue StandardError => e vprint_error("BSON parse error: #{e.}") [] end |
#mongodb_parse_msg(response) ⇒ Array<BSON::Document, Hash>
Parses an OP_MSG reply into its body document and any document sequences. A reply carries the 16 byte standard header, 4 bytes of flag bits, then one or more sections: a kind 0 section holds the single body document, and kind 1 sections hold a cstring identifier plus a stream of documents (find/getMore replies may deliver their batch as a 'documents' sequence instead of inside the cursor document).
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 228 def mongodb_parse_msg(response) return [nil, {}] if response.nil? || response.length <= 21 flags = response[16, 4].unpack1('V') data = response[20..] data = data[0...-4] if (flags & 1) == 1 # checksumPresent strips the CRC body = nil sequences = {} offset = 0 while offset < data.length kind = data.getbyte(offset) if kind.zero? doc_len = data[offset + 1, 4].unpack1('V') break if doc_len.nil? || doc_len < 5 || (offset + 1 + doc_len) > data.length body = BSON::Document.from_bson(BSON::ByteBuffer.new(data[offset + 1, doc_len])) offset += 1 + doc_len elsif kind == 1 # The section size counts from the size field itself (the kind # byte before it is not included), so the section spans # offset + 1 through offset + 1 + size. size = data[offset + 1, 4].unpack1('V') break if size.nil? || size < 5 || (offset + 1 + size) > data.length section = data[(offset + 1)...(offset + 1 + size)] terminator = section.index("\x00", 4) sequences[section[4...terminator]] = mongodb_parse_seq_docs(section[(terminator + 1)..]) if terminator offset += 1 + size else break end end [body, sequences] rescue StandardError [nil, {}] end |
#mongodb_parse_scram_payload(payload) ⇒ Hash<String, String>
Parses a SCRAM key=value,key=value wire payload into a Hash.
272 273 274 275 276 277 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 272 def mongodb_parse_scram_payload(payload) payload.split(',').each_with_object({}) do |var, hash| k, v = var.split('=', 2) hash[k] = v if k && v end end |
#mongodb_query_all(sock, namespace, bson_payload, timeout: 5, number_to_return: 0) ⇒ Array<BSON::Document>
Sends an OP_QUERY and drains the reply cursor with getMore commands until it is exhausted, so a namespace holding more documents than the first OP_REPLY batch (101 documents by default) is fully retrieved. Both cursor styles are followed: plain collection queries expose the cursor id in the OP_REPLY header, while command cursors (listCollections and friends) embed a cursor document carrying firstBatch/nextBatch in the reply document.
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 363 def mongodb_query_all(sock, namespace, bson_payload, timeout: 5, number_to_return: 0) db, coll = namespace.split('.', 2) if mongodb_use_op_msg? return mongodb_msg_query_all(sock, db, coll, bson_payload, timeout: timeout, number_to_return: number_to_return) end # Commands answer with a single reply document and mongod enforces that # on the wire: an OP_QUERY against a $cmd namespace must set # numberToReturn to exactly 1 or -1 (0 draws "Bad numberToReturn"). # Cursor batching for commands happens inside that document, so 1 is # always the right envelope no matter what the caller asked for. number_to_return = 1 if namespace.end_with?('$cmd') sock.put(mongodb_build_packet(namespace, bson_payload, number_to_return: number_to_return)) response = (sock, timeout) return [] if response.nil? docs = mongodb_parse_docs(response) reply = docs.first cursor_doc = reply.is_a?(BSON::Document) ? reply['cursor'] : nil if cursor_doc # Command cursor: the batches live inside the reply's cursor # document, so the wrapper itself is not a result document. first_batch = cursor_doc['firstBatch'] docs = first_batch.is_a?(Array) ? first_batch.dup : [] cursor_id = cursor_doc['id'].is_a?(Integer) ? cursor_doc['id'] : 0 getmore_collection = mongodb_getmore_collection(cursor_doc, coll) else cursor_id = mongodb_parse_cursor_id(response) getmore_collection = coll end batches = 0 while cursor_id != 0 break if getmore_collection.nil? || getmore_collection.empty? break if (batches += 1) > MAX_GETMORE_BATCHES getmore_cmd = BSON::Document.new({ 'getMore' => BSON::Int64.new(cursor_id), 'collection' => getmore_collection }) vprint_status("Following cursor #{cursor_id} on #{namespace} (#{docs.length} documents so far)") sock.put(mongodb_build_packet("#{db}.$cmd", getmore_cmd.to_bson.to_s)) response = (sock, timeout) break if response.nil? reply = mongodb_parse_doc(response) break if reply.nil? || reply['ok'].to_i != 1 cursor_doc = reply['cursor'] batch = cursor_doc ? cursor_doc['nextBatch'] : mongodb_parse_docs(response) break if batch.nil? || batch.empty? docs.concat(batch) if cursor_doc cursor_id = cursor_doc['id'].is_a?(Integer) ? cursor_doc['id'] : 0 getmore_collection = mongodb_getmore_collection(cursor_doc, getmore_collection) else cursor_id = mongodb_parse_cursor_id(response) end end docs end |
#mongodb_query_error(docs) ⇒ String?
Detects a legacy OP_QUERY error reply hiding among the result
documents: failed collection queries are answered with an ordinary
document carrying $err (or errmsg) plus ok: 0 instead of data, so a
dump that only checks for empty results mistakes them for records.
518 519 520 521 522 523 524 525 526 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 518 def mongodb_query_error(docs) doc = docs.first return nil unless doc.is_a?(Hash) # BSON::Document subclasses Hash return doc['$err'] || doc['errmsg'] if doc['$err'] || doc['errmsg'] return "command failed (code #{doc['code']})" if doc.key?('ok') && doc['ok'].to_i == 0 nil end |
#mongodb_read_exact(sock, length, timeout) ⇒ String?
Reads exactly length bytes from the socket. Rex get_once returns as
soon as any data is available, so a reply split across TCP segments
arrives in pieces that must be accumulated.
301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 301 def mongodb_read_exact(sock, length, timeout) data = '' while data.length < length chunk = sock.get_once(length - data.length, timeout) return nil if chunk.nil? || chunk.empty? data << chunk end data rescue IOError # covers EOFError, its subclass nil end |
#mongodb_read_message(sock, timeout = 5) ⇒ String?
Reads one complete MongoDB wire protocol message: the 16 byte standard header first, then messageLength minus 16 body bytes, so a large reply split across TCP segments is fully reassembled before it is parsed.
322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 322 def (sock, timeout = 5) header = mongodb_read_exact(sock, 16, timeout) return nil if header.nil? msg_len = header.unpack1('V') return nil if msg_len.nil? || msg_len < 16 || msg_len > MAX_MESSAGE_LENGTH return header if msg_len == 16 body = mongodb_read_exact(sock, msg_len - 16, timeout) return nil if body.nil? header + body end |
#mongodb_run_command(db, cmd, timeout: 5) ⇒ Array<BSON::Document, Boolean>
Runs a single command document over whichever wire protocol the server negotiated, normalizing the reply.
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 114 def mongodb_run_command(db, cmd, timeout: 5) if mongodb_use_op_msg? doc, = mongodb_msg_send(sock, db, cmd, timeout) [doc, mongodb_doc_auth_error?(doc)] else sock.put(mongodb_build_packet("#{db}.$cmd", cmd.to_bson.to_s)) resp = (sock, timeout) [mongodb_parse_doc(resp), mongodb_have_auth_error?(resp)] end end |
#mongodb_use_op_msg? ⇒ Boolean
Negotiates the wire protocol for this connection, memoized: MongoDB 3.6+ speak OP_MSG, which from 6.0 on is the only transport able to run anything beyond the handshake/buildInfo/SASL allowlist that OP_QUERY still answers, while older servers only understand OP_QUERY. The probe itself rides an OP_QUERY buildInfo because every version answers it: a parsed version dates the server, and the auth error reply (no version) marks 8.1+ where buildInfo is gated behind authentication.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 74 def mongodb_use_op_msg? return @mongodb_use_op_msg unless @mongodb_use_op_msg.nil? begin doc, _resp = mongodb_opquery_command('admin', BSON::Document.new({ 'buildInfo' => BSON::Int32.new(1) })) rescue StandardError # A probe that cannot even be sent (no connection yet) defaults to # the legacy protocol so callers fail the way they always have. doc = nil end version = doc.is_a?(Hash) ? doc['version'] : nil @mongodb_use_op_msg = if version begin Rex::Version.new(version) >= Rex::Version.new('3.6') rescue StandardError false end else # 8.1+ gates buildInfo behind authentication mongodb_doc_auth_error?(doc) end vprint_status("Negotiated wire protocol: #{@mongodb_use_op_msg ? 'OP_MSG' : 'OP_QUERY'}") @mongodb_use_op_msg end |
#require_auth? ⇒ Boolean
Backward-compatible boolean view of #mongodb_auth_state: true only when the server verifiably enforces authentication; :unknown does not count as required.
600 601 602 |
# File 'lib/msf/core/exploit/remote/mongodb.rb', line 600 def require_auth? mongodb_auth_state == :required end |