使用Python搭建反向代理分析设备流量

0分享至

用扫码二维码

分享至好友和朋友圈

文章目录

故事的开始DNS欺骗开始动手写一个简易DNS服务器写一个反向代理服务器实战验证配置DNS配置反向代理开始抓包

本文是接续《利用Frida手动绕过Android APP证书校验 》和《泄露的网站证书和私钥?来做些有趣的实验吧! 》这两篇文章的后续,将这两篇文章中提到的技术进行了简化和实现和拓展,提供一种新的测试思路。

故事的开始

书接上文。在使用Apache配置了反向代理,利用Proxifier转发流量并使用Burpsuite抓包后,发现这一流程非常麻烦,且难以在其他领域复现。比如测试目标是IoT设备,无法接入设备修改Hosts文件,则较难引导流量。

虽然说ARP攻击也是可以完成的,但是很容易在局域网内引起网络拥堵、丢包等情况:总是会觉得设备响应变慢,用Wireshark抓包的时候看到一大片重传包(不知道是不是哪里设置的不对,如果有大佬知道还烦请指点一二)。

因此想到使用DNS欺骗的方法来引导流量,并搭建反向代理,在出口上指定Proxy到Burpsuite,这样就可以在一个程序里完成全部操作,省时省力。

DNS欺骗

这里的DNS欺骗其实是有歧义的。一般意义上说DNS欺骗,大体就是两种:篡改Hosts和本地DNS劫持。

篡改Hosts:这种方法并不通用,很多设备都无法修改Hosts文件,如IoT设备、未Root的安卓/iOS设备等。

本地DNS劫持:这种方式使用ARP引导流量,过滤出其中的DNS请求,将要篡改的域名进行响应。一般用Ettercap工具去做(如果一定要劫持推荐试试Bettercap),但是仍然没跳出ARP的范畴。

因此这里选用了一个门槛略高但方式更加温和的思路:伪造DNS服务器。

门槛高是因为这种方式需要接触设备/路由器,将其中的首选DNS服务器地址设置成本机的IP,而温和是因为这种方式并不会对局域网流量造成任何影响。使用ARP攻击,局域网内会有大量的假ARP请求,且目标机器的流量全部会经过本机,而我们感兴趣的可能只是其中很小很小很小的一部分。

开始动手

既然确定了思路,下面就是动手环节了,先搭建一个DNS服务器。

写一个简易DNS服务器

为什么要自己写一个?不用现成的?

这两个疑问可能是很多人(包括我自己)都会提出的,毕竟重复造轮子是一件很没有意义和效率的事情。但是基于我找到的DNS服务器搭建都是很复杂的方案,比如Bind9,Windows Server等等,尝试过后发现复杂且难用(压根就没配置成功过),于是一气之下就打算自己写一个。

原理很简单,在本地监听53端口,当流量到达的时候按照DNS的协议解析,将要查询的域名提取出来,根据本地的规则匹配后返回IP,并封装成DNS应答发送出去。

在查询DNS解析协议的时候,发现网上竟然有类似的Python代码,虽然说性能肯定不行,但是毕竟承载的设备数量不多,自己测试用肯定没问题。

于是拿来主义,Copy过来源码,简单调试一下,成功!

在此声明一下,这段源码在很多不同的博客或社区都有转载,但是无一例外都没注明作者。在进一步搜索之后,某一个博客页面上转载的代码里有一句注释,表明这个代码的作者是@author: RobinTang,不知道我这样使用是否侵权~原始代码如下:

Created on 2012-10-15@author: RobinTangimport socketserverimport struct# DNS Queryclass SinDNSQuery:def __init__(self, data):i = 1self.name = while True:d = data[i]if d == 0:break;if d < 32:self.name = self.name + .else:self.name = self.name + chr(d)i = i + 1self.querybytes = data[0:i + 1](self.type, self.classify) = struct.unpack(>HH, data[i + 1:i + 5])self.len = i + 5def getbytes(self):return self.querybytes + struct.pack(>HH, self.type, self.classify)# DNS Answer RRS# this class is also can be use as Authority RRS or Additional RRSclass SinDNSAnswer:def __init__(self, ip):self.name = 49164self.type = 1self.classify = 1self.timetolive = 190self.datalength = 4self.ip = ipdef getbytes(self):res = struct.pack(>HHHLH, self.name, self.type, self.classify, self.timetolive, self.datalength)s = self.ip.split(.)res = res + struct.pack(BBBB, int(s[0]), int(s[1]), int(s[2]), int(s[3]))return res# DNS frame# must initialized by a DNS query frameclass SinDNSFrame:def __init__(self, data):(self.id, self.flags, self.quests, self.answers, self.author, self.addition) = struct.unpack(>HHHHHH, data[0:12])self.query = SinDNSQuery(data[12:])def getname(self):return self.query.namedef setip(self, ip):self.answer = SinDNSAnswer(ip)self.answers = 1self.flags = 33152def getbytes(self):res = struct.pack(>HHHHHH, self.id, self.flags, self.quests, self.answers, self.author, self.addition)res = res + self.query.getbytes()if self.answers != 0:res = res + self.answer.getbytes()return res# A UDPHandler to handle DNS queryclass SinDNSUDPHandler(socketserver.BaseRequestHandler):def handle(self):data = self.request[0].strip()dns = SinDNSFrame(data)socket = self.request[1]namemap = SinDNSServer.namemapif(dns.query.type==1):# If this is query a A record, then response itname = dns.getname();if namemap.__contains__(name):# If have record, response itdns.setip(namemap[name])socket.sendto(dns.getbytes(), self.client_address)elif namemap.__contains__(*):# Response default addressdns.setip(namemap[*])socket.sendto(dns.getbytes(), self.client_address)else:# ignore itsocket.sendto(data, self.client_address)else:# If this is not query a A record, ignore itsocket.sendto(data, self.client_address)# DNS Server# It only support A record query# user it, U can create a simple DNS serverclass SinDNSServer:def __init__(self, port=53):SinDNSServer.namemap = {}self.port = portdef addname(self, name, ip):SinDNSServer.namemap[name] = ipdef start(self):HOST, PORT = "0.0.0.0", self.portserver = socketserver.UDPServer((HOST, PORT), SinDNSUDPHandler)server.serve_forever()# Now, test itif __name__ == "__main__":sev = SinDNSServer()sev.addname(www.aa.com, 192.168.0.1)# add a A recordsev.addname(www.bb.com, 192.168.0.2)# add a A recordsev.addname(*, 0.0.0.0) # default addresssev.start() # start DNS server# Now, U can use "nslookup" command to test it# Such as "nslookup www.aa.com"

这段代码在Win Python 3.7环境下是没问题,可以正常响应,但是有两个问题:

只能对域名进行精准匹配,无法使用*通配符;

对于没有指定的域名,若设置了default address,则会一律返回该地址,否则就不回复

在使用过程中,我需要让这个服务器对于我没指定的地址回复真实地址,这样可以保证被欺骗设备的其他业务是正常的(业务间可能存在关联性,某一业务无法访问可能导致其他业务停止),同时通配符可以减少统计域名的麻烦,也不会漏掉流量。

针对以上两点,对源代码进行了部分的修改(修改详情可参考文末的连接)。

写一个反向代理服务器

虽然使用Apache + Proxifier可以转流量,但是用到的工具多不易排错,且当需要修改配置时,Apache配置文件的查询、新增都相对麻烦。

反向代理转发的思路也很简单:因为DNS欺骗,客户端会把流量发到本机,因此

本机监听端口,将收到的HTTP(S)流量解析;

取出Host字段中的目标域名、端口,重组新的请求发出;

获取服务端响应

发送给客户端

既然要求便捷且不考虑性能,直接用Python的http.server模块构建一个HTTP服务器接收请求即可。

重写http.server.BaseHTTPRequestHandler模块,对其中的HTTP方法处理进行重写即可:

class MyHandler(http.server.BaseHTTPRequestHandler):def req(self):try:if isinstance(self.request, ssl.SSLSocket):scheme = "https://"else:scheme = "http://"# 根据Host信息重组URLself.url = scheme + self.headers["host"].strip("\n") + self.path# 判断是否有HTTP Bodyif self.headers.__contains__(Content-Length):data = self.rfile.read(int(self.headers[Content-Length]))else:data = ""req = requests.Request(method=self.command, url=self.url, headers=self.headers, data=data)s = requests.Session()prepped = req.prepare()# 将请求通过代理发送出去r = s.send(prepped, verify=False,proxies=proxies, allow_redirects=False, stream=True)# 设置对客户端的响应头self.send_response(r.status_code)for key in r.headers:self.send_header(key, r.headers[key])self.end_headers()# 写入Response Body,写完后会自动发出这个请求self.wfile.write(r.content)except IOError as e:print(e)self.send_error(404, file not found: %s % self.path)except Exception as e:print(e)def do_GET(self):self.req()def do_POST(self):self.req()def do_HEAD(self):self.req()def do_OPTIONS(self):self.req()def do_PUT(self):self.req()def do_DELETE(self):self.req()def do_MOVE(self):self.req()def do_TRACE(self):self.req()

http.server.BaseHTTPRequestHandler模块会读取当前请求的HTTP Method,并调用do_xxx函数来进行处理。在这里我们并不需要对不同Method进行差异化处理,我们只想安安静静把他们转发出去,因此下面的do_xxx全部都调用req()统一进行处理。

完整代码已上传Github:https://github.com/mactavishmeng/mitmserver

Github上的版本是将DNS服务器和反代服务器集成在一起,通过配置文件mitmserver.json来进行配置:

{"proxies" : {"http": ":8080","https":":8080"},"dns_list" : [{"host": "www.baidu.cn", "address": "192.168.1.3"},{"host": "*.baidu.cn", "address": "192.168.1.3"},{"host": "www.google.com", "address": "192.168.1.3"}],"dns_query_enable" : true,"http_list" : [{"address":"0.0.0.0", "port":80, "ishttps":false},{"address":"0.0.0.0", "port":443, "ishttps":true, "certfile":"./certificate.crt", "keyfile":"./private_key.key"}]}

在这个配置文件中可以轻易的对各个部分进行方便的调整,如配置的代理,本地监听的HTTP(S)端口,证书,DNS列表等。

实战验证

实际使用中,整个流程的核心有两点:DNS如何欺骗,HTTPS证书是否是真的。

配置DNS

虽然上面说了那么多,还写了一个DNS服务器,但是如果你的路由器支持插件,可以自定义hosts,实际上是不需要这么麻烦的。比如我借到的这台极路由:

直接用插件强行更改设备的DNS查询结果,不过有些路由器不支持修改Hosts,并不通用。

手机、PC等可直接修改网络配置的设备

对于手机APP来说,可以直接在“无线局域网”中将IP设置修改为“静态”,在DNS服务器部分配置IP为本机即可。以安卓为例:

IoT设备等无法直接修改配置的设备

而IoT设备因为无法在设备上操作,需要在路由器上设置,如果能获得控制权的话; – )

将路由器的DNS设置为本机IP即可(不同路由器界面可能不同,但原理是一样的):

至此,设备端DNS欺骗的前置步骤完成。

配置反向代理

打开mitmserver.json,修改其中的监听端口的配置。

如这里监听的APP通信,它与三个域名进行HTTPS通信,其中两个域名访问443,一个域名访问9988。此时配置列表里需打开两个监听端口(443、9988),并将"ishttps"的值设置为true,表示这个端口监听HTTPS。后面的certfile和keyfile部分填入证书和私钥文件的路径。

如果你没有这两个文件,可以从Burpsuite等抓包工具中导出,或使用openssl工具生成自签名证书。

开始抓包

在本机上配置好要监听的端口、要劫持的域名、填入Burpsuite的地址后,即可开始抓包。

因为手头上没有合适的能公开的IoT设备,因此以安卓APP为例,其原理是一致的。

APP已经做了证书校验的绕过,所以这里的HTTPS证书是随意找了一个来配置的(毕竟已经绕过校验)。配置文件如下:

配置DNS。这里偷懒没有去路由器上配置,直接在手机上配了,因此在运行日志里看到的请求源是手机的IP。如果在路由器上配置,则日志中的请求IP会是网关的IP。

运行起来:

对应在Burpsuite中拦截到的请求:

这样,仅需一个py文件就完成了全部的流程,简单方便,且增减配置只需要修改JSON的配置即可,比起在Apache的conf文件中改来改去方便的多。

# 反向代理

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

/阅读下一篇/

返回网易首页 下载网易新闻客户端