Module: Msf::Post::Linux::F5Mcp
- Defined in:
- lib/msf/core/post/linux/f5_mcp.rb
Overview
This mixin lets you programmatically interact with F5’s “mcp” service, which is a database service on a variety of F5’s devices, including BIG-IP and BIG-IQ.
mcp uses a UNIX domain socket @ /var/run/mcp for all communications. As of writing this module, it’s world-accessible, so anybody can query or write to it. We implemented a few interesting things as modules, and your best bet for learning how to work this is to look at those modules, but this will document it briefly.
Data is read and written by serializing a TLV-style structure and writing it to that socket, then parsing the response.
If you’re just reading data, you can use ‘mcp_simple_query()` to build a query that fetches everything under a given name, and get a Hash of data back. That’s by far the easiest way to handle things.
To create a more complex query, you’ll need to use mcp_build(), which serializes a message. You can generate a single message, or an array of them. Then use mcp_send_recv() to write it/them to the socket. Additionally, mcp_send_recv() automatically parses them and returns a whole big nested array of data.
To actually use that data without going crazy, I suggest using either mcp_get_single(tagname) to fetch a single tag, or mcp_get_multiple(tagname) if multiple of the same tag can be returned. Finally, the response from that can be passed to mcp_to_h() to convert the response to a hash (note that if there are multiple of the same tag, map_to_h() will only keep one of them).
Obviously, this is all way more complex than mcp_simple_query(). You can see this in action in the module ‘linux/local/f5_create_user`.
Instance Method Summary collapse
-
#initialize(info = {}) ⇒ Object
rubocop:disable Metrics/ModuleLength.
-
#mcp_build(tag, type, data) ⇒ Object
Build an mcp message.
-
#mcp_get_multiple(hash, name) ⇒ Object
Pull an array of tags with the same name out of a tag/value structure.
-
#mcp_get_single(hash, name) ⇒ Object
Pull a single value out of a tag/value structure (ie, the thing returned by mcp_parse()).
-
#mcp_parse(stream) ⇒ Object
Recursively parse an mcp message from a binary stream into an object.
-
#mcp_parse_responses(incoming_data) ⇒ Object
Parse one or more packets (including headers) into an array of packets.
- #mcp_send_recv(messages) ⇒ Object
-
#mcp_simple_query(querytype) ⇒ Object
Do a query_all request for something that will reply with a single query result.
-
#mcp_to_h(array) ⇒ Object
Take an array of results from an mcp query, and change them from an array of tag=>value into a hash.
Instance Method Details
#initialize(info = {}) ⇒ Object
rubocop:disable Metrics/ModuleLength
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 39 def initialize(info = {}) file = ::File.join(Msf::Config.data_directory, 'f5-mcp-objects.txt') objects = ::File.read(file) raise("Could not load #{file}!") unless objects @tags_by_id = objects .split(/\n/) .reject { |o| o.start_with?('#') } .map(&:strip) .map do |o| value, tag = o.split(/ /, 2) raise("Invalid line in #{file}: #{o}") if tag.nil? [value.to_i(16), tag] end .to_h .freeze @tags_by_name = @tags_by_id.invert.freeze super(info) end |
#mcp_build(tag, type, data) ⇒ Object
Build an mcp message
Adapted from github.com/rbowes-r7/refreshing-mcp-tool/blob/main/mcp-builder.rb
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 292 def mcp_build(tag, type, data) if @tags_by_name[tag].nil? raise "Invalid mcp tag: #{tag}" end if @tags_by_name[type].nil? raise "Invalid mcp type: #{type}" end out = '' if type == 'structure' out = [data.join.length, data.join].pack('Na*') elsif type == 'string' out = [data.length + 2, data.length, data].pack('Nna*') elsif type == 'uquad' out = [data].pack('Q>') elsif type == 'ulong' out = [data].pack('N') elsif type == 'uword' out = [data].pack('n') elsif type == 'long' out = [data].pack('N') elsif type == 'tag' out = [@tags_by_name[data]].pack('n') elsif type == 'byte' out = [data].pack('C') elsif type == 'mac' out = [data].pack('a6') else raise "Unknown type: #{type}" end out = [@tags_by_name[tag], @tags_by_name[type], out].pack('nna*') return out end |
#mcp_get_multiple(hash, name) ⇒ Object
Pull an array of tags with the same name out of a tag/value structure. For example, when you perform a query for ‘userdb_entry`, it returns multiple tags with the same name.
The result is:
-
If there are no values, return an empty array
-
If there are one or more values, return them as an array
274 275 276 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 274 def mcp_get_multiple(hash, name) hash.select { |entry| entry[:tag] == name }.map { |entry| entry[:value] } end |
#mcp_get_single(hash, name) ⇒ Object
Pull a single value out of a tag/value structure (ie, the thing returned by mcp_parse()). The result is:
-
If there are no values with that tag name, return nil
-
If there’s a single value with that tag name, return it
-
If there are multiple values with that tag name, print an error and return nil
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 250 def mcp_get_single(hash, name) # Get all the entries entries = mcp_get_multiple(hash, name) if entries.empty? # If there are none, return nil return nil elsif entries.length == 1 # If there's one, return it return entries.pop else # If there are multiple entries, print a warning and return nil print_error("Query for mcp type #{name} was supposed to have one response but had #{entries.length}") return nil end end |
#mcp_parse(stream) ⇒ Object
Recursively parse an mcp message from a binary stream into an object
Adapted from github.com/rbowes-r7/refreshing-mcp-tool/blob/main/mcp-parser.rb
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 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 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 142 def mcp_parse(stream) # Reminder: this has to be an array, not a hash, because there are # often duplicate entries (like multiple userdb_entry results when a # query is performed). result = [] # Make a Hash of parsers. Some of them are recursive, which is fun! # # They all take the stream as an input argument, and return # [value, stream] parsers = { # The easy stuff - simple values 'ulong' => proc { |s| s.unpack('Na*') }, 'long' => proc { |s| s.unpack('Na*') }, 'uquad' => proc { |s| s.unpack('Q>a*') }, 'uword' => proc { |s| s.unpack('na*') }, 'byte' => proc { |s| s.unpack('Ca*') }, 'service' => proc { |s| s.unpack('na*') }, # Parse 'time' as a time 'time' => proc do |s| value, s = s.unpack('Na*') [Time.at(value), s] end, # Look up 'tag' values 'tag' => proc do |s| value, s = s.unpack('na*') [@tags_by_id[value], s] end, # Parse MAC addresses 'mac' => proc do |s| value, s = s.unpack('a6a*') [value.bytes.map { |b| '%02x'.format(b) }.join(':'), s] end, # 'string' is prefixed by two length values 'string' => proc do |s| length, otherlength, s = s.unpack('Nna*') # I'm sure the two length values have a semantic difference, but just check for sanity if otherlength + 2 != length raise "Inconsistent string lengths: #{length} + #{otherlength}" end s.unpack("a#{otherlength}a*") end, # 'structure' is recursive 'structure' => proc do |s| length, s = s.unpack('Na*') struct, s = s.unpack("a#{length}a*") [mcp_parse(struct), s] end, # 'array' is a bunch of consecutive values of the same type, which # means we need to index back into this same parser array 'array' => proc do |s| length, s = s.unpack('Na*') array, s = s.unpack("a#{length}a*") type, elements, array = array.unpack('nNa*') type = @tags_by_id[type] || '<unknown type 0x%04x>'.format(type) array_results = [] elements.times do array_result, array = parsers[type].call(array) array_results << array_result end [array_results, s] end } begin while stream.length > 2 tag, type, stream = stream.unpack('nna*') tag = @tags_by_id[tag] || '<unknown tag 0x%04x>'.format(tag) type = @tags_by_id[type] || '<unknown type 0x%04x>'.format(type) if parsers[type] value, stream = parsers[type].call(stream) result << { tag: tag, value: value } else raise "Tried to parse unknown mcp type (skipping): type = #{type}, tag = #{tag}" end end rescue StandardError => e # If we fail somewhere, print a warning but return what we have print_warning("Parsing mcp data failed: #{e.}") end result end |
#mcp_parse_responses(incoming_data) ⇒ Object
Parse one or more packets (including headers) into an array of packets.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 67 def mcp_parse_responses(incoming_data) replies = [] while incoming_data.length > 16 # Grab the length and remove the header from the incoming data expected_length, _, incoming_data = incoming_data.unpack('Na12a*') # Read the packet packet, incoming_data = incoming_data.unpack("a#{expected_length}a*") # Sanity check if packet.length != expected_length print_warning('mcp message is truncated!') return replies end # Parse it replies << mcp_parse(packet) end return replies end |
#mcp_send_recv(messages) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 90 def mcp_send_recv() # Attach headers to each message and combine them = .map do |m| [m.length, 0, 0, 0, m].pack('NNNNa*') end.join('') # Encode as base64 so we can pass it on the commandline = Rex::Text.encode_base64() # Sometimes, the service doesn't respond with a complete packet, but # instead truncates it. This only seems to happen on very long replies, # and seems to happen ~50% of the time, so running this loop 5 times # gives a pretty high chance of it working # # This isn't a problem with Metasploit, it even happens when I use # socat directly.. I think it's just because we don't have AF_UNIX. # In this example, 559604 is right and 548160 is truncated: # # # echo 'AAAAEAAAAAAAAAAAAAAAAAtlAA0AAAAICEoADQAAAAA=' | base64 -d | socat -t100 - UNIX-CONNECT:/var/run/mcp | wc -c # 559604 # # echo 'AAAAEAAAAAAAAAAAAAAAAAtlAA0AAAAICEoADQAAAAA=' | base64 -d | socat -t100 - UNIX-CONNECT:/var/run/mcp | wc -c # 548160 # # This loop is the best we can do without having access to an AF_UNIX # socket (or doing something much, much more complex) 0.upto(4) do # Send the request messages(s) to the socket incoming_data = cmd_exec("echo '#{}' | base64 -d | socat -t100 - UNIX-CONNECT:/var/run/mcp") # Fail if we got no response or no header if !incoming_data || incoming_data.length < 16 print_error('Request to /var/run/mcp socket failed') return nil end # Get the expected length and make sure the full response is at least # that long expected_length = incoming_data.unpack('N').pop if incoming_data.length < expected_length vprint_warning("mcp responded with #{incoming_data.length} bytes instead of the promised #{expected_length} bytes! Trying again...") else return mcp_parse_responses(incoming_data) end end print_error("mcp isn't responding with a full message, giving up") nil end |
#mcp_simple_query(querytype) ⇒ Object
Do a query_all request for something that will reply with a single query result.
Attempts to abstract away all the messiness in the protocol, instead we just query for a type and get all the responses as an array of hashes
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 334 def mcp_simple_query(querytype) # Get the raw result result = mcp_send_recv([ mcp_build('query_all', 'structure', [ mcp_build(querytype, 'structure', []) ]) ]) # Error handling unless result print_error('mcp_send_recv failed') return nil end # Sanity check - we only expect one result if result.length != 1 print_error("mcp_send_recv query was supposed to return one result, but returned #{result.length} results instead") return nil end # Get that result result = result.pop # Get the reply result = mcp_get_single(result, 'query_reply') if result.nil? print_error("mcp didn't return a query_reply to our query") return nil end # Get all the fields for the querytype result = mcp_get_multiple(result, querytype) # Convert each result to a hash result = result.map do |single_result| mcp_to_h(single_result) end result end |
#mcp_to_h(array) ⇒ Object
Take an array of results from an mcp query, and change them from an array of tag=>value into a hash.
Note! If there are multiple fields with the same tag, this will only return one of them!
283 284 285 286 287 |
# File 'lib/msf/core/post/linux/f5_mcp.rb', line 283 def mcp_to_h(array) array.map do |r| [r[:tag], r[:value]] end.to_h end |