Nginx 修正洩漏內部 IP 漏洞

收到了一個漏洞報告,內容是外部可以查看到反向代理前的內部 IP。

在筆者建立的測試環境中,外部會向 192.168.0.210 的 Nginx 發送請求,Nginx 會將請求轉送給 172.23.0.6 主機。如果這個請求沒有身份驗證訊息,就會要求要登入,會在回應的 Header 中夾帶 Location 資訊,而 Location 的內容則是指向登入頁面,瀏覽器看到有 Location 資訊就會自動重定向網頁到指定的頁面。

實際情況:發送請求給 http://192.168.0.210:8000 ,沒有登入就會在 Header 中夾帶 Location ,內容是 http://192.168.0.210:8000/login ,瀏覽器就會把使用者的畫面跳轉到登入頁面。

筆者一開始想到的是在正常的請求中用什麼 host 進來,就會是當前的 host 並且再加上目標頁面,例如 192.168.0.1 進來,就會導向 http://192.168.0.1/login。
在 curl 中可以使用 -H 指定 host 假裝是 192.168.0.1 :
    
curl -v -H "Host: 192.168.0.1" http://192.168.0.210
    

在回應中 Location 就會變成 http://192.168.0.1/login

但是這並不能解釋為什麼外部可以知道內部的 IP ,因為你必須要先知道內部 IP ,才能使用上面的方式得到這樣的結果。

重現漏洞情況

經過一番研究,使用 Linux 中的 netcat 工具(簡稱 nc),用來發送 HTTP 請求,就可以重現這個漏洞,可以查看到反向代理內部的 IP:
    
echo -ne "GET / HTTP/1.0\r\n\r\n" | nc 192.168.0.210 8000
    

範例輸出:
    
echo -ne "GET / HTTP/1.0\r\n\r\n" | nc 192.168.0.210 8000
HTTP/1.1 302 Found
Server: nginx
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: private, must-revalidate
Date: Tue, 26 Nov 2024 05:46:33 GMT
Location: http://172.23.0.6:8000/login
pragma: no-cache
expires: -1

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://172.23.0.6:8000/login'" />

        <title>Redirecting to http://172.23.0.6:8000/login</title>
    </head>
    <body>
        Redirecting to <a href="http://172.23.0.6:8000/login">http://172.23.0.6:8000/login</a>.
    </body>
</html>
    

難道只能使用 netcat 才能重現嗎?又經過一番研究,筆者也找到使用 curl 重現的方式:
    
curl -v --http1.0 -H "Host:" http://192.168.0.210:8000
    

重點在於要使用 HTTP 1.0 ,並且不要帶入 Host ,因為 curl 指令會自動帶入 Host 所以無法重現,這裡需要手動塞入空的 Host 才可以。

範例輸出:
    
curl -v --http1.0 -H "Host:" http://192.168.0.210:8000
*   Trying 192.168.0.210:8000...
* Connected to 192.168.0.210 (192.168.0.210) port 8000 (#0)
> GET / HTTP/1.0
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Connection: close
< Cache-Control: private, must-revalidate
< Date: Tue, 26 Nov 2024 05:45:53 GMT
< Location: http://172.23.0.6:8000/login
< pragma: no-cache
< expires: -1
<
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://172.23.0.6:8000/login'" />

        <title>Redirecting to http://172.23.0.6:8000/login</title>
    </head>
    <body>
        Redirecting to <a href="http://172.23.0.6:8000/login">http://172.23.0.6:8000/login</a>.
    </body>
* Closing connection 0
</html>
    

解決方式

修改 Nginx 設定檔:
    
vi /etc/nginx/nginx.conf
    

在 server 區塊中增加以下內容:
    
    server {
        listen 8000;
        listen [::]:8000;

        server_name 192.168.0.210
        server_name_in_redirect on;
	}
    

這裡修改的設定是手動指定 server_name ,並且在重定向時自動使用剛剛設定的 server_name ,這樣就不會暴露其他 host 了。

測試設定檔是否修改正確:
    
nginx -t
    

重新啟動 nginx:
    
systemctl reload nginx
    

如果出現以下錯誤:
    
systemctl reload nginx
bash: systemctl: command not found
    

可以先使用以下指令找到 nginx 的位置:
    
which nginx
    

範例輸出:
    
which nginx
/usr/sbin/nginx
    

然後執行以下指令重新讀取設定檔:
(剛剛查詢出來的結果為 /usr/sbin/nginx ,如果結果不同請自行替換:)
    
/usr/sbin/nginx -s reload
    

修改完成後我們可以再執行一次,檢查 Location 是否有洩漏
範例輸出:
    
curl -v --http1.0 -H "Host:" http://192.168.0.210:8000
*   Trying 192.168.0.210:8000...
* Connected to 192.168.0.210 (192.168.0.210) port 8000 (#0)
> GET / HTTP/1.0
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Connection: close
< Cache-Control: private, must-revalidate
< Date: Tue, 26 Nov 2024 05:43:40 GMT
< Location: http://192.168.0.210:8000/login
< pragma: no-cache
< expires: -1
<
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://192.168.0.210:8000/login'" />

        <title>Redirecting to http://192.168.0.210:8000/login</title>
    </head>
    <body>
        Redirecting to <a href="http://192.168.0.210:8000/login">http://192.168.0.210:8000/login</a>.
    </body>
* Closing connection 0
</html>
    

Location 已經自動被替換為 server_name 了,修改成功!

參考資料:
Web Server HTTP Header Internal IP Disclosure
thorntech.com - HTTP Private IP Disclosure

留言