前言
在上一篇文章BaseProxy:異步http/https代理中,我介紹了自己的開源項(xiàng)目BaseProxy,這個(gè)項(xiàng)目的初衷其實(shí)是為了滲透測試,抓包改包。在知識(shí)星球中,有很多朋友問我這個(gè)項(xiàng)目的原理及實(shí)現(xiàn)代碼,本篇文章就講解一下和這個(gè)項(xiàng)目相關(guān)的HTTPS的中間人攻擊。
HTTPS隧道代理
HTTPS隧道代理簡單來說是基于TCP協(xié)議數(shù)據(jù)透明轉(zhuǎn)發(fā),在RFC中,為這類代理給出了規(guī)范,Tunneling TCP based protocols through Web proxy servers。瀏覽器客戶端發(fā)送的原始 TCP 流量,代理發(fā)送給遠(yuǎn)端服務(wù)器后,將接收到的 TCP 流量原封不動(dòng)返回給瀏覽器。交互流程如下圖所示:

以連接百度為例,瀏覽器首先發(fā)起 CONNECT 請(qǐng)求:
CONNECT baidu.com:443 HTTP/1.1
代理收到這樣的請(qǐng)求后,根據(jù) host 地址與服務(wù)器建立 TCP 連接,并返回給瀏覽器連接成功的HTTP 報(bào)文(沒有報(bào)文體):
HTTP/1.1 200 Connection Established
瀏覽器一旦收到這個(gè)響應(yīng)報(bào)文,就可認(rèn)為與服務(wù)器的 TCP 連接已打通,后續(xù)可直接透傳。
在BaseProxy項(xiàng)目中,https=False是對(duì)于https實(shí)行透傳。
HTTPS中間人攻擊
HTTPS 代理本質(zhì)上是隧道透傳,僅僅是轉(zhuǎn)發(fā) TCP 流量,無法獲取其中的GET/POST請(qǐng)求的具體內(nèi)容。這就很麻煩,現(xiàn)在 HTTPS 越來越普遍,做安全測試也就拿不到 HTTP 請(qǐng)求。那怎么做呢? 代理需要對(duì) TCP 流量進(jìn)行解密,然后對(duì)明文的HTTP請(qǐng)求進(jìn)行分析,這樣的代理就稱為HTTPS中間人。
正常的HTTPS隧道

在上圖中,隧道代理負(fù)責(zé)瀏覽器和服務(wù)器之間的TCP流量的轉(zhuǎn)發(fā)。
HTTPS中間人
如果需要對(duì)TCP流量進(jìn)行分析和修改,就要將上圖中的代理功能一分為二,即代理既要當(dāng)做TLS服務(wù)端,又要當(dāng)做TLS客戶端,如下圖所示。

在上圖中,用一個(gè) TLS 服務(wù)器偽裝成遠(yuǎn)端的真正的服務(wù)器,接收瀏覽器的 TLS 流量,解析成明文。這個(gè)時(shí)候可以對(duì)明文進(jìn)行分析修改,然后用明文作為原始數(shù)據(jù),模擬 TLS 客戶端將原始數(shù)據(jù)向遠(yuǎn)端服務(wù)器轉(zhuǎn)發(fā)。
CA證書問題
CA證書是我當(dāng)時(shí)遇到的坑,之前沒接觸過。HTTPS傳輸是需要證書的,用來對(duì)HTTP明文請(qǐng)求進(jìn)行加解密。一般正常網(wǎng)站的證書都是由合法的 CA 簽發(fā),則稱為合法證書。在上圖中,瀏覽器會(huì)驗(yàn)證隧道代理中 TLS 服務(wù)器 的證書:
- 驗(yàn)證是否是合法 CA 簽發(fā)。
- 驗(yàn)證該證書 CN 屬性是否是所請(qǐng)求的域名。即若瀏覽器打開
www.baidu.com,則返回的證書 CN 屬性必須是www.baidu.com。
對(duì)于第一點(diǎn),合法的 CA 機(jī)構(gòu)不會(huì)給我們簽發(fā)證書的,否則HTTPS安全性形同虛設(shè),因此我們需要自制CA證書,并導(dǎo)入到瀏覽器的信任區(qū)中。
對(duì)于第二點(diǎn),我們由于需要對(duì)各個(gè)網(wǎng)站進(jìn)行HTTPS攔截,因此我們需要實(shí)時(shí)生成相應(yīng)域名的服務(wù)器證書,并使用自制的CA證書進(jìn)行簽名。
BaseProxy源碼分析
通過以上的講解,HTTPS中間人的原理已經(jīng)基本清楚,下面簡要地說明一下BaseProxy源碼。
HTTP服務(wù)器
代理其實(shí)就是一個(gè)HTTPS服務(wù)器,使用了Python中的HTTPServer類,為了增加異步特性,將其放到線程池中。
class MitmProxy(HTTPServer):
def __init__(self,server_addr=('', 8788),RequestHandlerClass=ProxyHandle, bind_and_activate=True,https=True):
HTTPServer.__init__(self,server_addr,RequestHandlerClass,bind_and_activate)
logging.info('HTTPServer is running at address( %s , %d )......'%(server_addr[0],server_addr[1]))
self.req_plugs = []##請(qǐng)求攔截插件列表
self.rsp_plugs = []##響應(yīng)攔截插件列表
self.ca = CAAuth(ca_file = "ca.pem", cert_file = 'ca.crt')
self.https = https
def register(self,intercept_plug):
if not issubclass(intercept_plug, InterceptPlug):
raise Exception('Expected type InterceptPlug got %s instead' % type(intercept_plug))
if issubclass(intercept_plug,ReqIntercept):
self.req_plugs.append(intercept_plug)
if issubclass(intercept_plug,RspIntercept):
self.rsp_plugs.append(intercept_plug)
class AsyncMitmProxy(ThreadingMixIn,MitmProxy):
pass
HTTPS請(qǐng)求與響應(yīng)
對(duì)HTTP請(qǐng)求的解析與響應(yīng),關(guān)鍵在于ProxyHandle類,實(shí)現(xiàn)其中的do_CONNECT和do_GET方法,并在do_CONNECT方法中判斷是使用透傳模式還是中間人模式。
class ProxyHandle(BaseHTTPRequestHandler):
def __init__(self,request,client_addr,server):
self.is_connected = False
BaseHTTPRequestHandler.__init__(self,request,client_addr,server)
def do_CONNECT(self):
'''
處理https連接請(qǐng)求
:return:
'''
self.is_connected = True#用來標(biāo)識(shí)是否之前經(jīng)歷過CONNECT
if self.server.https:
self.connect_intercept()
else:
self.connect_relay()
def do_GET(self):
'''
處理GET請(qǐng)求
:return:
'''
......
do_HEAD = do_GET
do_POST = do_GET
do_PUT = do_GET
do_DELETE = do_GET
do_OPTIONS = do_GET
CA證書生成以及代理證書的自簽名
與CA證書相關(guān)的內(nèi)容都放在了CAAuth類中。生成CA證書代碼如下:
def _gen_ca(self,again=False):
# Generate key
#如果證書存在而且不是強(qiáng)制生成,直接返回證書信息
if os.path.exists(self.ca_file_path) and os.path.exists(self.cert_file_path) and not again:
self._read_ca(self.ca_file_path) #讀取證書信息
return
self.key = PKey()
self.key.generate_key(TYPE_RSA, 2048)
# Generate certificate
self.cert = X509()
self.cert.set_version(2)
self.cert.set_serial_number(1)
self.cert.get_subject().CN = 'baseproxy'
self.cert.gmtime_adj_notBefore(0)
self.cert.gmtime_adj_notAfter(315360000)
self.cert.set_issuer(self.cert.get_subject())
self.cert.set_pubkey(self.key)
self.cert.add_extensions([
X509Extension(b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
X509Extension(b"keyUsage", True, b"keyCertSign, cRLSign"),
X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=self.cert),
])
self.cert.sign(self.key, "sha256")
with open(self.ca_file_path, 'wb+') as f:
f.write(dump_privatekey(FILETYPE_PEM, self.key))
f.write(dump_certificate(FILETYPE_PEM, self.cert))
with open(self.cert_file_path, 'wb+') as f:
f.write(dump_certificate(FILETYPE_PEM, self.cert))
根據(jù)域名實(shí)時(shí)生成服務(wù)器證書,并對(duì)服務(wù)器證書進(jìn)行自簽名。代碼如下:
def _sign_ca(self,cn,cnp):
#使用合法的CA證書為代理程序生成服務(wù)器證書
# create certificate
try:
key = PKey()
key.generate_key(TYPE_RSA, 2048)
# Generate CSR
req = X509Req()
req.get_subject().CN = cn
req.set_pubkey(key)
req.sign(key, 'sha256')
# Sign CSR
cert = X509()
cert.set_version(2)
cert.set_subject(req.get_subject())
cert.set_serial_number(self.serial)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(31536000)
cert.set_issuer(self.cert.get_subject())
ss = ("DNS:%s" % cn).encode(encoding="utf-8")
cert.add_extensions(
[X509Extension(b"subjectAltName", False, ss)])
cert.set_pubkey(req.get_pubkey())
cert.sign(self.key, 'sha256')
with open(cnp, 'wb+') as f:
f.write(dump_privatekey(FILETYPE_PEM, key))
f.write(dump_certificate(FILETYPE_PEM, cert))
except Exception as e:
raise Exception("generate CA fail:{}".format(str(e)))
最后
關(guān)注公眾號(hào):七夜安全博客

- 回復(fù)【1】:領(lǐng)取 Python數(shù)據(jù)分析 教程大禮包
- 回復(fù)【2】:領(lǐng)取 Python Flask 全套教程
- 回復(fù)【3】:領(lǐng)取 某學(xué)院 機(jī)器學(xué)習(xí) 教程
- 回復(fù)【4】:領(lǐng)取 爬蟲 教程
知識(shí)星球已經(jīng)50多人了,隨著人數(shù)的增多,價(jià)格之后會(huì)上漲,越早關(guān)注越多優(yōu)惠。星球的福利有很多:
- 比如上面的教程,已經(jīng)提前在知識(shí)星球中分享
- 可以發(fā)表一些問題,大家一塊解決
- 我之后寫的電子書,錄制的教學(xué)視頻,對(duì)于知識(shí)星球的朋友都是優(yōu)惠的(基本上免費(fèi))
- 一些節(jié)假日會(huì)給大家發(fā)個(gè)紅包或者贈(zèng)書
