545 lines
12 KiB
Ruby
545 lines
12 KiB
Ruby
# -*- coding: binary -*-
|
|
#
|
|
# Author:: Michael Neumann
|
|
# Copyright:: (c) 2005 by Michael Neumann
|
|
# License:: Same as Ruby's or BSD
|
|
#
|
|
|
|
require 'postgres_msf'
|
|
require 'postgres/buffer'
|
|
require 'rex/io/stream'
|
|
|
|
# Monkeypatch to preserve original code intent
|
|
# (postgres-pr originally defined read_exactly_n_bytes on IO
|
|
# as a while loop)
|
|
module Rex::IO::Stream
|
|
def read_exactly_n_bytes(n)
|
|
timed_read(n)
|
|
end
|
|
end
|
|
|
|
# Namespace for Metasploit branch.
|
|
module Msf
|
|
module Db
|
|
|
|
module PostgresPR
|
|
|
|
class ParseError < RuntimeError; end
|
|
class DumpError < RuntimeError; end
|
|
|
|
|
|
# Base class representing a PostgreSQL protocol message
|
|
class Message
|
|
# One character message-typecode to class map
|
|
MsgTypeMap = Hash.new { UnknownMessageType }
|
|
|
|
def self.register_message_type(type)
|
|
raise "duplicate message type registration" if MsgTypeMap.has_key?(type)
|
|
|
|
MsgTypeMap[type] = self
|
|
|
|
self.const_set(:MsgType, type)
|
|
class_eval "def message_type; MsgType end"
|
|
end
|
|
|
|
def self.read(stream, startup=false)
|
|
type = stream.read_exactly_n_bytes(1) unless startup
|
|
length = stream.read_exactly_n_bytes(4).unpack('N').first # FIXME: length should be signed, not unsigned
|
|
|
|
raise ParseError unless length >= 4
|
|
|
|
# initialize buffer
|
|
buffer = Buffer.of_size(startup ? length : 1+length)
|
|
buffer.write(type) unless startup
|
|
buffer.write_int32_network(length)
|
|
buffer.copy_from_stream(stream, length-4)
|
|
|
|
(startup ? StartupMessage : MsgTypeMap[type]).create(buffer)
|
|
end
|
|
|
|
def self.create(buffer)
|
|
obj = allocate
|
|
obj.parse(buffer)
|
|
obj
|
|
end
|
|
|
|
def self.dump(*args)
|
|
new(*args).dump
|
|
end
|
|
|
|
def dump(body_size=0)
|
|
buffer = Buffer.of_size(5 + body_size)
|
|
buffer.write(self.message_type)
|
|
buffer.write_int32_network(4 + body_size)
|
|
yield buffer if block_given?
|
|
raise DumpError unless buffer.at_end?
|
|
return buffer.content
|
|
end
|
|
|
|
def parse(buffer)
|
|
buffer.position = 5
|
|
yield buffer if block_given?
|
|
raise ParseError, buffer.inspect unless buffer.at_end?
|
|
end
|
|
|
|
def self.fields(*attribs)
|
|
names = attribs.map {|name, type| name.to_s}
|
|
arg_list = names.join(", ")
|
|
ivar_list = names.map {|name| "@" + name }.join(", ")
|
|
sym_list = names.map {|name| ":" + name }.join(", ")
|
|
class_eval %[
|
|
attr_accessor #{ sym_list }
|
|
def initialize(#{ arg_list })
|
|
#{ ivar_list } = #{ arg_list }
|
|
end
|
|
]
|
|
end
|
|
end
|
|
|
|
class UnknownMessageType < Message
|
|
def dump
|
|
raise
|
|
end
|
|
end
|
|
|
|
class Authentification < Message
|
|
register_message_type 'R'
|
|
|
|
AuthTypeMap = Hash.new { UnknownAuthType }
|
|
|
|
def self.create(buffer)
|
|
buffer.position = 5
|
|
authtype = buffer.read_int32_network
|
|
klass = AuthTypeMap[authtype]
|
|
obj = klass.allocate
|
|
obj.parse(buffer)
|
|
obj
|
|
end
|
|
|
|
def self.register_auth_type(type)
|
|
raise "duplicate auth type registration" if AuthTypeMap.has_key?(type)
|
|
AuthTypeMap[type] = self
|
|
self.const_set(:AuthType, type)
|
|
class_eval "def auth_type() AuthType end"
|
|
end
|
|
|
|
# the dump method of class Message
|
|
alias message__dump dump
|
|
|
|
def dump
|
|
super(4) do |buffer|
|
|
buffer.write_int32_network(self.auth_type)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
auth_t = buffer.read_int32_network
|
|
raise ParseError unless auth_t == self.auth_type
|
|
yield if block_given?
|
|
end
|
|
end
|
|
end
|
|
|
|
class UnknownAuthType < Authentification
|
|
end
|
|
|
|
class AuthentificationOk < Authentification
|
|
register_auth_type 0
|
|
end
|
|
|
|
class AuthentificationKerberosV4 < Authentification
|
|
register_auth_type 1
|
|
end
|
|
|
|
class AuthentificationKerberosV5 < Authentification
|
|
register_auth_type 2
|
|
end
|
|
|
|
class AuthentificationClearTextPassword < Authentification
|
|
register_auth_type 3
|
|
end
|
|
|
|
module SaltedAuthentificationMixin
|
|
attr_accessor :salt
|
|
|
|
def initialize(salt)
|
|
@salt = salt
|
|
end
|
|
|
|
def dump
|
|
raise DumpError unless @salt.size == self.salt_size
|
|
|
|
message__dump(4 + self.salt_size) do |buffer|
|
|
buffer.write_int32_network(self.auth_type)
|
|
buffer.write(@salt)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@salt = buffer.read(self.salt_size)
|
|
end
|
|
end
|
|
end
|
|
|
|
class AuthentificationCryptPassword < Authentification
|
|
register_auth_type 4
|
|
include SaltedAuthentificationMixin
|
|
def salt_size; 2 end
|
|
end
|
|
|
|
|
|
class AuthentificationMD5Password < Authentification
|
|
register_auth_type 5
|
|
include SaltedAuthentificationMixin
|
|
def salt_size; 4 end
|
|
end
|
|
|
|
class AuthentificationSCMCredential < Authentification
|
|
register_auth_type 6
|
|
end
|
|
|
|
class PasswordMessage < Message
|
|
register_message_type 'p'
|
|
fields :password
|
|
|
|
def dump
|
|
super(@password.size + 1) do |buffer|
|
|
buffer.write_cstring(@password)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@password = buffer.read_cstring
|
|
end
|
|
end
|
|
end
|
|
|
|
class ParameterStatus < Message
|
|
register_message_type 'S'
|
|
fields :key, :value
|
|
|
|
def dump
|
|
super(@key.size + 1 + @value.size + 1) do |buffer|
|
|
buffer.write_cstring(@key)
|
|
buffer.write_cstring(@value)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@key = buffer.read_cstring
|
|
@value = buffer.read_cstring
|
|
end
|
|
end
|
|
end
|
|
|
|
class BackendKeyData < Message
|
|
register_message_type 'K'
|
|
fields :process_id, :secret_key
|
|
|
|
def dump
|
|
super(4 + 4) do |buffer|
|
|
buffer.write_int32_network(@process_id)
|
|
buffer.write_int32_network(@secret_key)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@process_id = buffer.read_int32_network
|
|
@secret_key = buffer.read_int32_network
|
|
end
|
|
end
|
|
end
|
|
|
|
class ReadyForQuery < Message
|
|
register_message_type 'Z'
|
|
fields :backend_transaction_status_indicator
|
|
|
|
def dump
|
|
super(1) do |buffer|
|
|
buffer.write_byte(@backend_transaction_status_indicator)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@backend_transaction_status_indicator = buffer.read_byte
|
|
end
|
|
end
|
|
end
|
|
|
|
class DataRow < Message
|
|
register_message_type 'D'
|
|
fields :columns
|
|
|
|
def dump
|
|
sz = @columns.inject(2) {|sum, col| sum + 4 + (col ? col.size : 0)}
|
|
super(sz) do |buffer|
|
|
buffer.write_int16_network(@columns.size)
|
|
@columns.each {|col|
|
|
buffer.write_int32_network(col ? col.size : -1)
|
|
buffer.write(col) if col
|
|
}
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
n_cols = buffer.read_int16_network
|
|
@columns = (1..n_cols).collect {
|
|
len = buffer.read_int32_network
|
|
if len == -1
|
|
nil
|
|
else
|
|
buffer.read(len)
|
|
end
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
class CommandComplete < Message
|
|
register_message_type 'C'
|
|
fields :cmd_tag
|
|
|
|
def dump
|
|
super(@cmd_tag.size + 1) do |buffer|
|
|
buffer.write_cstring(@cmd_tag)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@cmd_tag = buffer.read_cstring
|
|
end
|
|
end
|
|
end
|
|
|
|
class EmptyQueryResponse < Message
|
|
register_message_type 'I'
|
|
end
|
|
|
|
module NoticeErrorMixin
|
|
attr_accessor :field_type, :field_values
|
|
|
|
def initialize(field_type=0, field_values=[])
|
|
raise ArgumentError if field_type == 0 and not field_values.empty?
|
|
@field_type, @field_values = field_type, field_values
|
|
end
|
|
|
|
def dump
|
|
raise ArgumentError if @field_type == 0 and not @field_values.empty?
|
|
|
|
sz = 1
|
|
sz += @field_values.inject(1) {|sum, fld| sum + fld.size + 1} unless @field_type == 0
|
|
|
|
super(sz) do |buffer|
|
|
buffer.write_byte(@field_type)
|
|
break if @field_type == 0
|
|
@field_values.each {|fld| buffer.write_cstring(fld) }
|
|
buffer.write_byte(0)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@field_type = buffer.read_byte
|
|
break if @field_type == 0
|
|
@field_values = []
|
|
while buffer.position < buffer.size-1
|
|
@field_values << buffer.read_cstring
|
|
end
|
|
terminator = buffer.read_byte
|
|
raise ParseError unless terminator == 0
|
|
end
|
|
end
|
|
end
|
|
|
|
class NoticeResponse < Message
|
|
register_message_type 'N'
|
|
include NoticeErrorMixin
|
|
end
|
|
|
|
class ErrorResponse < Message
|
|
register_message_type 'E'
|
|
include NoticeErrorMixin
|
|
end
|
|
|
|
# TODO
|
|
class CopyInResponse < Message
|
|
register_message_type 'G'
|
|
end
|
|
|
|
# TODO
|
|
class CopyOutResponse < Message
|
|
register_message_type 'H'
|
|
end
|
|
|
|
class Parse < Message
|
|
register_message_type 'P'
|
|
fields :query, :stmt_name, :parameter_oids
|
|
|
|
def initialize(query, stmt_name="", parameter_oids=[])
|
|
@query, @stmt_name, @parameter_oids = query, stmt_name, parameter_oids
|
|
end
|
|
|
|
def dump
|
|
sz = @stmt_name.size + 1 + @query.size + 1 + 2 + (4 * @parameter_oids.size)
|
|
super(sz) do |buffer|
|
|
buffer.write_cstring(@stmt_name)
|
|
buffer.write_cstring(@query)
|
|
buffer.write_int16_network(@parameter_oids.size)
|
|
@parameter_oids.each {|oid| buffer.write_int32_network(oid) }
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@stmt_name = buffer.read_cstring
|
|
@query = buffer.read_cstring
|
|
n_oids = buffer.read_int16_network
|
|
@parameter_oids = (1..n_oids).collect {
|
|
# TODO: zero means unspecified. map to nil?
|
|
buffer.read_int32_network
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
class ParseComplete < Message
|
|
register_message_type '1'
|
|
end
|
|
|
|
class Query < Message
|
|
register_message_type 'Q'
|
|
fields :query
|
|
|
|
def dump
|
|
super(@query.size + 1) do |buffer|
|
|
buffer.write_cstring(@query)
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
@query = buffer.read_cstring
|
|
end
|
|
end
|
|
end
|
|
|
|
class RowDescription < Message
|
|
register_message_type 'T'
|
|
fields :fields
|
|
|
|
class FieldInfo < Struct.new(:name, :oid, :attr_nr, :type_oid, :typlen, :atttypmod, :formatcode); end
|
|
|
|
def dump
|
|
sz = @fields.inject(2) {|sum, fld| sum + 18 + fld.name.size + 1 }
|
|
super(sz) do |buffer|
|
|
buffer.write_int16_network(@fields.size)
|
|
@fields.each { |f|
|
|
buffer.write_cstring(f.name)
|
|
buffer.write_int32_network(f.oid)
|
|
buffer.write_int16_network(f.attr_nr)
|
|
buffer.write_int32_network(f.type_oid)
|
|
buffer.write_int16_network(f.typlen)
|
|
buffer.write_int32_network(f.atttypmod)
|
|
buffer.write_int16_network(f.formatcode)
|
|
}
|
|
end
|
|
end
|
|
|
|
def parse(buffer)
|
|
super do
|
|
n_fields = buffer.read_int16_network
|
|
@fields = (1..n_fields).collect {
|
|
f = FieldInfo.new
|
|
f.name = buffer.read_cstring
|
|
f.oid = buffer.read_int32_network
|
|
f.attr_nr = buffer.read_int16_network
|
|
f.type_oid = buffer.read_int32_network
|
|
f.typlen = buffer.read_int16_network
|
|
f.atttypmod = buffer.read_int32_network
|
|
f.formatcode = buffer.read_int16_network
|
|
f
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
class StartupMessage < Message
|
|
fields :proto_version, :params
|
|
|
|
def dump
|
|
sz = @params.inject(4 + 4) {|sum, kv| sum + kv[0].size + 1 + kv[1].size + 1} + 1
|
|
|
|
buffer = Buffer.of_size(sz)
|
|
buffer.write_int32_network(sz)
|
|
buffer.write_int32_network(@proto_version)
|
|
@params.each_pair {|key, value|
|
|
buffer.write_cstring(key)
|
|
buffer.write_cstring(value)
|
|
}
|
|
buffer.write_byte(0)
|
|
|
|
raise DumpError unless buffer.at_end?
|
|
return buffer.content
|
|
end
|
|
|
|
def parse(buffer)
|
|
buffer.position = 4
|
|
|
|
@proto_version = buffer.read_int32_network
|
|
@params = {}
|
|
|
|
while buffer.position < buffer.size-1
|
|
key = buffer.read_cstring
|
|
val = buffer.read_cstring
|
|
@params[key] = val
|
|
end
|
|
|
|
nul = buffer.read_byte
|
|
raise ParseError unless nul == 0
|
|
raise ParseError unless buffer.at_end?
|
|
end
|
|
end
|
|
|
|
class SSLRequest < Message
|
|
fields :ssl_request_code
|
|
|
|
def dump
|
|
sz = 4 + 4
|
|
buffer = Buffer.of_size(sz)
|
|
buffer.write_int32_network(sz)
|
|
buffer.write_int32_network(@ssl_request_code)
|
|
raise DumpError unless buffer.at_end?
|
|
return buffer.content
|
|
end
|
|
|
|
def parse(buffer)
|
|
buffer.position = 4
|
|
@ssl_request_code = buffer.read_int32_network
|
|
raise ParseError unless buffer.at_end?
|
|
end
|
|
end
|
|
|
|
=begin
|
|
# TODO: duplicate message-type, split into client/server messages
|
|
class Sync < Message
|
|
register_message_type 'S'
|
|
end
|
|
=end
|
|
|
|
class Terminate < Message
|
|
register_message_type 'X'
|
|
end
|
|
|
|
end # module PostgresPR
|
|
|
|
end
|
|
end
|