在上一篇文章BaseProxy:異步http/https代理中,我介紹了本身的開源項目BaseProxy,這個項目的初衷實際上是爲了滲透測試,抓包改包。在知識星球中,有不少朋友問我這個項目的原理及實現代碼,本篇文章就講解一下和這個項目相關的HTTPS的中間人攻擊。html
HTTPS隧道代理簡單來講是基於TCP協議數據透明轉發,在RFC中,爲這類代理給出了規範,Tunneling TCP based protocols through Web proxy servers。瀏覽器客戶端發送的原始 TCP 流量,代理髮送給遠端服務器後,將接收到的 TCP 流量原封不動返回給瀏覽器。交互流程以下圖所示:python
以鏈接百度爲例,瀏覽器首先發起 CONNECT 請求:web
CONNECT baidu.com:443 HTTP/1.1
代理收到這樣的請求後,根據 host 地址與服務器創建 TCP 鏈接,並返回給瀏覽器鏈接成功的HTTP 報文(沒有報文體):瀏覽器
HTTP/1.1 200 Connection Established
瀏覽器一旦收到這個響應報文,就可認爲與服務器的 TCP 鏈接已打通,後續可直接透傳。安全
在BaseProxy項目中,https=False
是對於https實行透傳。服務器
HTTPS 代理本質上是隧道透傳,僅僅是轉發 TCP 流量,沒法獲取其中的GET/POST請求的具體內容。這就很麻煩,如今 HTTPS 愈來愈廣泛,作安全測試也就拿不到 HTTP 請求。那怎麼作呢? 代理須要對 TCP 流量進行解密,而後對明文的HTTP請求進行分析,這樣的代理就稱爲HTTPS中間人。app
在上圖中,隧道代理負責瀏覽器和服務器之間的TCP流量的轉發。機器學習
若是須要對TCP流量進行分析和修改,就要將上圖中的代理功能一分爲二,即代理既要當作TLS服務端,又要當作TLS客戶端,以下圖所示。異步
在上圖中,用一個 TLS 服務器假裝成遠端的真正的服務器,接收瀏覽器的 TLS 流量,解析成明文。這個時候能夠對明文進行分析修改,而後用明文做爲原始數據,模擬 TLS 客戶端將原始數據向遠端服務器轉發。源碼分析
CA證書是我當時遇到的坑,以前沒接觸過。HTTPS傳輸是須要證書的,用來對HTTP明文請求進行加解密。通常正常網站的證書都是由合法的 CA 簽發,則稱爲合法證書。在上圖中,瀏覽器會驗證隧道代理中 TLS 服務器 的證書:
www.baidu.com
,則返回的證書 CN 屬性必須是 www.baidu.com
。對於第一點,合法的 CA 機構不會給咱們簽發證書的,不然HTTPS安全性形同虛設,所以咱們須要自制CA證書,並導入到瀏覽器的信任區中。
對於第二點,咱們因爲須要對各個網站進行HTTPS攔截,所以咱們須要實時生成相應域名的服務器證書,並使用自制的CA證書進行簽名。
經過以上的講解,HTTPS中間人的原理已經基本清楚,下面簡要地說明一下BaseProxy源碼。
代理其實就是一個HTTPS服務器,使用了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 = []##請求攔截插件列表 self.rsp_plugs = []##響應攔截插件列表 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
對HTTP請求的解析與響應,關鍵在於ProxyHandle類,實現其中的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鏈接請求 :return: ''' self.is_connected = True#用來標識是否以前經歷過CONNECT if self.server.https: self.connect_intercept() else: self.connect_relay() def do_GET(self): ''' 處理GET請求 :return: ''' ...... do_HEAD = do_GET do_POST = do_GET do_PUT = do_GET do_DELETE = do_GET do_OPTIONS = do_GET
與CA證書相關的內容都放在了CAAuth類中。生成CA證書代碼以下:
def _gen_ca(self,again=False): # Generate key #若是證書存在並且不是強制生成,直接返回證書信息 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))
根據域名實時生成服務器證書,並對服務器證書進行自簽名。代碼以下:
def _sign_ca(self,cn,cnp): #使用合法的CA證書爲代理程序生成服務器證書 # 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)))
關注公衆號:七夜安全博客
知識星球已經50多人了,隨着人數的增多,價格以後會上漲,越早關注越多優惠。星球的福利有不少: