很久沒(méi)更新了,其實(shí)https明文攔截已經(jīng)實(shí)現(xiàn)很久了只是沒(méi)發(fā)博客(畢竟我太懶),步入正題吧,上一篇用netty實(shí)現(xiàn)的http代理服務(wù)器還無(wú)法對(duì)https報(bào)文進(jìn)行解密,原因也說(shuō)了,就是服務(wù)器的私鑰不在我們這,根據(jù)RSA公鑰加密私鑰解密的特性,如果我們沒(méi)有私鑰的話是不可能獲取到https的真實(shí)內(nèi)容的,那有沒(méi)有什么辦法解密https的報(bào)文呢,當(dāng)然有通過(guò)代理服務(wù)器偽造ssl證書就可以達(dá)到目的,那么具體是什么原理的,下面一步一步來(lái)分析。
https協(xié)議
首先來(lái)回顧下https協(xié)議的ssl握手

簡(jiǎn)單敘述下ssl握手中,只說(shuō)說(shuō)ssl單向驗(yàn)證過(guò)程
- 客戶端向服務(wù)器發(fā)出ssl握手,發(fā)送client-random隨機(jī)數(shù)。
- 服務(wù)器返回ssl證書和server-randon隨機(jī)數(shù)。
- 客戶端校驗(yàn)ssl證書,校驗(yàn)通過(guò),再生成一個(gè)premaster-secret隨機(jī)數(shù)用服務(wù)器證書里的公鑰加密發(fā)送,這個(gè)時(shí)候客戶端已經(jīng)可以通過(guò)三個(gè)隨機(jī)數(shù)算出對(duì)稱加密的密鑰了。
- 服務(wù)器用私鑰解密premaster-secret,也拿到了三個(gè)隨機(jī)數(shù)算出對(duì)稱加密的密鑰。
- 兩邊都用算出來(lái)的對(duì)稱密鑰進(jìn)行報(bào)文加密和解密
注意第三步非常關(guān)鍵,ssl證書是采用信任鏈的方式來(lái)驗(yàn)證ssl證書是否有效,在瀏覽器中都會(huì)內(nèi)置好許多受信任的CA證書,而CA證書下簽發(fā)的ssl證書來(lái)訪問(wèn)瀏覽器才會(huì)驗(yàn)證通過(guò),不然就會(huì)提示證書不安全(12306典型的例子,因?yàn)?2306是自己簽發(fā)的CA根證書,不存在于瀏覽器受信任的證書列表中,所以瀏覽器會(huì)提示不安全)。所以就是要制作一個(gè)服務(wù)器證書,然后還要讓瀏覽器安全校驗(yàn)通過(guò)才行。
動(dòng)態(tài)替換ssl證書

在代理服務(wù)器拿到目標(biāo)服務(wù)器ssl證書的時(shí)候,不返回給瀏覽器,而是使用我們自己制作的ssl證書,這個(gè)時(shí)候有個(gè)問(wèn)題,就是自己制作的證書并不是瀏覽器中CA根證書簽發(fā)的,所以我們需要把自己制作的CA根證書加入瀏覽器受信任的根證書頒發(fā)機(jī)構(gòu)。

代理服務(wù)器返回的ssl證書中是由剛剛的CA證書簽發(fā)的,這樣就形成了一個(gè)受信任鏈,其實(shí)代理服務(wù)器就相當(dāng)于一個(gè)瀏覽器訪問(wèn)目標(biāo)目標(biāo)服務(wù)器,獲取到明文之后在通過(guò)客戶端和代理服務(wù)器建立的ssl連接再轉(zhuǎn)發(fā)給瀏覽器,就可以捕獲并攔截到明文了。
openssl制作CA根證書
生成java支持的私鑰
openssl genrsa -des3 -out ca.key 2048
openssl pkcs8 -topk8 -nocrypt -inform PEM -outform DER -in ca.key -out ca_private.pem
再通過(guò)CA私鑰生成CA證書
openssl req -sha256 -new -x509 -days 365 -key ca.key -out ca.crt \
-subj "/C=CN/ST=GD/L=SZ/O=lee/OU=study/CN=ProxyeeRoot"
上面我們就制作了一個(gè)自己的CA證書,打開(kāi)ca.crt來(lái)看一看
這里我們就擁有了CA根證書的私鑰,只要在程序中通過(guò)這個(gè)私鑰就能向下簽發(fā)服務(wù)器證書了,如圖代理服務(wù)器簽發(fā)了一個(gè)www.baidu.com的ssl證書

java動(dòng)態(tài)簽發(fā)ssl證書
JAVA自帶的SSL以及X509庫(kù)只能使用SSL證書,不能生成SSL證書。因此我們使用“Bouncy Castle”這個(gè)算法庫(kù)來(lái)實(shí)現(xiàn)SSL證書的生成。
//maven
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
//注冊(cè)bouncycastle
Security.addProvider(new BouncyCastleProvider());
//生成ssl證書公鑰和私鑰
KeyPairGenerator caKeyPairGen = KeyPairGenerator.getInstance("RSA", "BC");
caKeyPairGen.initialize(2048, new SecureRandom());
PrivateKey serverPriKey = keyPair.getPrivate();
PublicKey serverPubKey = keyPair.getPublic();
//通過(guò)CA私鑰動(dòng)態(tài)簽發(fā)ssl證書
public static X509Certificate genCert(String issuer, PublicKey serverPubKey, PrivateKey caPriKey, String host) throws Exception {
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
String issuer = "C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=ProxyeeRoot";
String subject = "C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=" + host;
v3CertGen.reset();
v3CertGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
v3CertGen.setIssuerDN(new X509Principal(issuer));
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 10 * ONE_DAY));
v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + 3650 * ONE_DAY));
v3CertGen.setSubjectDN(new X509Principal(subject));
v3CertGen.setPublicKey(serverPubKey);
//SHA256 Chrome需要此哈希算法否則會(huì)出現(xiàn)不安全提示
v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
//SAN擴(kuò)展 Chrome需要此擴(kuò)展否則會(huì)出現(xiàn)不安全提示
GeneralNames subjectAltName = new GeneralNames(new GeneralName(GeneralName.dNSName, host));
v3CertGen.addExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName);
X509Certificate cert = v3CertGen.generateX509Certificate(caPriKey);
return cert;
}
至此我們最重要的功能已經(jīng)實(shí)現(xiàn)了,接著就是拿著ssl證書返回給客戶端即可捕獲明文了。
效果
這里我攔截了訪問(wèn)https://www.baidu.com的請(qǐng)求,并在加上了一對(duì)響應(yīng)頭。

這樣就以動(dòng)態(tài)簽發(fā)ssl證書方法,解決了https明文捕獲的問(wèn)題。此外還添加了對(duì)websocket的支持,并且提供攔截器對(duì)外使用,實(shí)現(xiàn)上面效果的代碼如下:
new NettyHttpProxyServer().initProxyInterceptFactory(() -> new HttpProxyIntercept() {
@Override
public boolean afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse) {
//攔截響應(yīng),添加一個(gè)響應(yīng)頭
httpResponse.headers().add("intercept","test");
return true;
}
}).start(9999);
后記
希望通過(guò)一個(gè)sdk的方式把http代理服務(wù)器開(kāi)源出去供大家使用,源碼托管在github,歡迎start。
筆者現(xiàn)在用這個(gè)sdk嗅探http下載請(qǐng)求,然后對(duì)于支持http斷點(diǎn)下載的文件,采用多連接分段下載,想體驗(yàn)可以在這里看看。
當(dāng)然也可以做到類似fiddler的自動(dòng)替換請(qǐng)求體、響應(yīng)體功能,具體就看大家的業(yè)務(wù)需求了!