GoでTCPプロキシを作るシリーズ
| Part1 net.Listener | Part2 透過プロキシ | Part3 HTTPS MITM | Part4 ロードバランサー |
|---|---|---|---|
| ✅ Done | ✅ Done | 👈 Now | - |
はじめに
今回はHTTPS通信の中身を覗くプロキシを作る。
「え、HTTPSって暗号化されてるから見れないんじゃ...」
そう、普通は見れない。でもMITM(Man-in-the-Middle)攻撃の手法を使えば可能なんだよね。セキュリティの学習にもなるから、やってみよう。
⚠️ 注意
このテクニックは、自分が管理するネットワーク・端末でのみ使用してください。他人の通信を傍受することは犯罪です。
MITMプロキシの仕組み
通常のHTTPS通信:
┌──────────┐ ┌──────────┐
│ Client │ ══════ TLS Tunnel ══════► │ Server │
└──────────┘ └──────────┘
暗号化されてて中身見えない
MITMプロキシ:
┌──────────┐ TLS① ┌──────────┐ TLS② ┌──────────┐
│ Client │ ═════════► │ Proxy │ ═════════► │ Server │
└──────────┘ └──────────┘ └──────────┘
偽の証明書を信頼 2つのTLS接続を中継 本物のサーバー
ポイントは「偽の証明書をクライアントに信頼させる」こと。
CA証明書の作成
まず、自己署名のCA(認証局)証明書を作成する。
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"time"
)
func generateCA() (*x509.Certificate, *ecdsa.PrivateKey, error) {
// 秘密鍵生成(ECDSA P-256)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
// シリアル番号(ランダム)
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
// CA証明書テンプレート
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"My Proxy CA"},
CommonName: "My Proxy Root CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0), // 10年有効
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
// 自己署名証明書を作成
certDER, err := x509.CreateCertificate(
rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, err
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, err
}
return cert, privateKey, nil
}
func saveCA(cert *x509.Certificate, key *ecdsa.PrivateKey) error {
// 証明書をPEM形式で保存
certFile, err := os.Create("ca.crt")
if err != nil {
return err
}
defer certFile.Close()
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
// 秘密鍵をPEM形式で保存
keyFile, err := os.Create("ca.key")
if err != nil {
return err
}
defer keyFile.Close()
keyBytes, _ := x509.MarshalECPrivateKey(key)
pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
return nil
}
サーバー証明書の動的生成
各ドメインごとに、CA証明書で署名したサーバー証明書を動的に生成する。
type CertificateCache struct {
mu sync.RWMutex
certs map[string]*tls.Certificate
caCert *x509.Certificate
caKey *ecdsa.PrivateKey
}
func NewCertificateCache(caCert *x509.Certificate, caKey *ecdsa.PrivateKey) *CertificateCache {
return &CertificateCache{
certs: make(map[string]*tls.Certificate),
caCert: caCert,
caKey: caKey,
}
}
func (c *CertificateCache) GetCertificate(host string) (*tls.Certificate, error) {
// ポート番号を除去
hostname := strings.Split(host, ":")[0]
// キャッシュを確認
c.mu.RLock()
if cert, ok := c.certs[hostname]; ok {
c.mu.RUnlock()
return cert, nil
}
c.mu.RUnlock()
// なければ生成
cert, err := c.generateCert(hostname)
if err != nil {
return nil, err
}
// キャッシュに保存
c.mu.Lock()
c.certs[hostname] = cert
c.mu.Unlock()
return cert, nil
}
func (c *CertificateCache) generateCert(hostname string) (*tls.Certificate, error) {
// 秘密鍵生成
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"My Proxy"},
CommonName: hostname,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0), // 1年有効
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{hostname},
}
// CA証明書で署名
certDER, err := x509.CreateCertificate(
rand.Reader, template, c.caCert, &privateKey.PublicKey, c.caKey)
if err != nil {
return nil, err
}
cert := &tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: privateKey,
}
return cert, nil
}
MITMプロキシの実装
package main
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
)
type MITMProxy struct {
listener net.Listener
certCache *CertificateCache
}
func NewMITMProxy(addr string, caCert *x509.Certificate, caKey *ecdsa.PrivateKey) (*MITMProxy, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return &MITMProxy{
listener: listener,
certCache: NewCertificateCache(caCert, caKey),
}, nil
}
func (p *MITMProxy) Start() {
fmt.Println("MITM Proxy started")
for {
conn, err := p.listener.Accept()
if err != nil {
continue
}
go p.handleConnection(conn)
}
}
func (p *MITMProxy) handleConnection(clientConn net.Conn) {
defer clientConn.Close()
reader := bufio.NewReader(clientConn)
req, err := parseHTTPRequest(reader)
if err != nil {
return
}
if req.Method == "CONNECT" {
// HTTPS: MITMモード
p.handleMITM(clientConn, req)
} else {
// HTTP: 通常転送
p.handleHTTP(clientConn, reader, req)
}
}
func (p *MITMProxy) handleMITM(clientConn net.Conn, req *HTTPRequest) {
hostname := strings.Split(req.Host, ":")[0]
fmt.Printf("[MITM] Starting for %s\n", hostname)
// 証明書を取得(キャッシュから or 新規生成)
cert, err := p.certCache.GetCertificate(hostname)
if err != nil {
fmt.Println("Failed to get certificate:", err)
return
}
// クライアントに接続成功を通知
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
// クライアントとTLSハンドシェイク(偽の証明書で)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*cert},
}
clientTLS := tls.Server(clientConn, tlsConfig)
if err := clientTLS.Handshake(); err != nil {
fmt.Println("Client TLS handshake failed:", err)
return
}
defer clientTLS.Close()
// リクエストを読み込む
reader := bufio.NewReader(clientTLS)
innerReq, err := parseHTTPRequest(reader)
if err != nil {
fmt.Println("Failed to parse inner request:", err)
return
}
fmt.Printf("[MITM] %s %s%s\n", innerReq.Method, hostname, innerReq.Path)
// ターゲットサーバーに接続
serverConn, err := tls.Dial("tcp", req.Host, &tls.Config{
InsecureSkipVerify: true, // 開発用。本番では適切に検証
})
if err != nil {
fmt.Println("Failed to connect to server:", err)
return
}
defer serverConn.Close()
// リクエストを転送
serverConn.Write(innerReq.RawData)
// レスポンスを読み込んで転送
io.Copy(clientTLS, serverConn)
}
リクエスト・レスポンスの検査
MITMできたら、通信内容を検査できる。
func (p *MITMProxy) handleMITMWithInspection(clientConn net.Conn, req *HTTPRequest) {
hostname := strings.Split(req.Host, ":")[0]
cert, _ := p.certCache.GetCertificate(hostname)
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
tlsConfig := &tls.Config{Certificates: []tls.Certificate{*cert}}
clientTLS := tls.Server(clientConn, tlsConfig)
clientTLS.Handshake()
defer clientTLS.Close()
reader := bufio.NewReader(clientTLS)
innerReq, _ := parseHTTPRequest(reader)
// 🔍 リクエストの内容を検査
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf("📤 Request to %s\n", hostname)
fmt.Printf(" Method: %s\n", innerReq.Method)
fmt.Printf(" Path: %s\n", innerReq.Path)
for k, v := range innerReq.Headers {
fmt.Printf(" %s: %s\n", k, v)
}
// サーバーに接続・転送
serverConn, _ := tls.Dial("tcp", req.Host, &tls.Config{InsecureSkipVerify: true})
defer serverConn.Close()
serverConn.Write(innerReq.RawData)
// レスポンスを読み込み
serverReader := bufio.NewReader(serverConn)
resp, respBody := readFullResponse(serverReader)
// 🔍 レスポンスの内容を検査
fmt.Printf("📥 Response from %s\n", hostname)
fmt.Printf(" Status: %d %s\n", resp.StatusCode, resp.StatusText)
for k, v := range resp.Headers {
fmt.Printf(" %s: %s\n", k, v)
}
if len(respBody) < 500 {
fmt.Printf(" Body: %s\n", string(respBody))
}
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
// クライアントに転送
clientTLS.Write(resp.RawHeader)
clientTLS.Write(respBody)
}
リクエストの改変
せっかくMITMできてるので、リクエストを改変することもできる。
func modifyRequest(req *HTTPRequest) *HTTPRequest {
// User-Agentを変更
req.Headers["User-Agent"] = "Modified-Agent/1.0"
// 特定のCookieを削除
delete(req.Headers, "Cookie")
// ヘッダを追加
req.Headers["X-Proxy-By"] = "My-MITM-Proxy"
// RawDataを再構築
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s %s %s\r\n", req.Method, req.Path, req.Version)
for k, v := range req.Headers {
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
}
buf.WriteString("\r\n")
req.RawData = buf.Bytes()
return req
}
レスポンスの改変
レスポンスも改変できる。広告ブロッカーとかに応用可能。
func modifyResponse(resp *HTTPResponse, body []byte) []byte {
// HTMLの場合、スクリプトを注入
contentType := resp.Headers["Content-Type"]
if strings.Contains(contentType, "text/html") {
injection := `<script>console.log("Injected by proxy!");</script>`
bodyStr := string(body)
bodyStr = strings.Replace(bodyStr, "</body>", injection+"</body>", 1)
return []byte(bodyStr)
}
return body
}
証明書をブラウザに登録
生成したCA証明書をブラウザに登録すると、警告なしでMITMできる。
Windows
# 管理者権限で実行
certutil -addstore "Root" ca.crt
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt
Linux (Firefox)
# certutil(NSS Tools)が必要
certutil -A -n "My Proxy CA" -t "C,," -i ca.crt -d ~/.mozilla/firefox/*.default/
セキュリティ上の考慮事項
MITMプロキシを使う際の注意点:
| 項目 | 説明 |
|---|---|
| 法的リスク | 他人の通信を傍受するのは違法 |
| HSTS | 一度HTTPSで接続したサイトはHTTPにダウングレードできない |
| 証明書ピンニング | 一部のアプリは特定の証明書のみを信頼 |
| CT(Certificate Transparency) | 証明書の発行がログに記録される仕組み |
HSTSとの戦い
HSTSが設定されているサイトは、ブラウザが強制的にHTTPSを使う。
Strict-Transport-Security: max-age=31536000; includeSubDomains
この場合、ユーザーが http:// でアクセスしても、ブラウザが自動で https:// にリダイレクトする。
まとめ
今回学んだこと:
| 技術 | 説明 |
|---|---|
| CA証明書 | 自己署名の認証局を作成 |
| 動的証明書生成 | ドメインごとに証明書を生成 |
| TLSハンドシェイク | クライアント・サーバー両方と |
| 通信の検査 | リクエスト・レスポンスの内容確認 |
| 通信の改変 | ヘッダやボディの書き換え |
次回はロードバランサーを実装するよ。複数のバックエンドサーバーに負荷分散させる。
改めて注意
この技術は、自分が管理するネットワーク・端末でのデバッグ目的でのみ使用してください。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!