你的IP白名單靠譜嗎

前言

最近組內(nèi)同事在開發(fā)需求時,需要獲取一個第三方線上的所有車型,但是他們的線上服務(wù)對我們的線上服務(wù)器做了白名單。

同事的做法是,單獨拉一個分支,在預(yù)發(fā)暴露一個http接口,用于觸發(fā)這個掉接口拖庫的行為,然后保存到我們的數(shù)據(jù)庫。

我覺得這樣的做法一點也不工程師,會在應(yīng)用內(nèi)冗余很多一次性代碼,而且也沒必要上到預(yù)發(fā)去做這事。

我的第一個思路,在預(yù)發(fā)服務(wù)器通過nginx開代理服務(wù)器,本地電腦連這個代理調(diào)用不就得了,后來因為跟運維部門溝通不順利作罷。

因為我也做過網(wǎng)關(guān)的白名單插件,因此我嘗試性的給本地的請求加了幾個頭。

@Headers({
    "X-Real-ip:xxxx",
    "x-forwarded-for:xxxx",
    "x-remote-IP:xxxx",
})

嗯,果不其然,成功了。

本文會分享獲取ip的一些小知識,以及為何我加的頭能破解ip白名單和如何防范ip白名單被破解。

常用部署架構(gòu)

image.png
image.png

一般的部署結(jié)構(gòu)就是,一個nginx后面反向代理多個服務(wù)器。

IP獲取原理

  1. 從應(yīng)用層獲取(L7)

對于http來講,就是從header中獲取。

  1. 從tcp層獲取(L4)

tcp層的話就是從tcp報文中獲取了,其實就是通過socket api獲取。

tomcat

經(jīng)過研究tomcat,支持從L4和L7獲取ip。

具體代碼見org.apache.catalina.connector.Request#getRemoteAddr

public String getRemoteAddr() {
    if (remoteAddr == null) {
        coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest);
        remoteAddr = coyoteRequest.remoteAddr().toString();
    }
    return remoteAddr;
}

coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest); 用來做緩存優(yōu)化,只有g(shù)et具體某個值的時候,才去做對應(yīng)操作獲取。

對于ActionCode.REQ_HOST_ADDR_ATTRIBUTE的處理邏輯如下

見org.apache.coyote.AbstractProcessor#action

case REQ_HOST_ADDR_ATTRIBUTE: {
    if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) {
        request.remoteAddr().setString(socketWrapper.getRemoteAddr());
    }
    break;
}

很明顯能感知到調(diào)用的是socket api了吧。

最終調(diào)用一下方法

org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#populateRemoteAddr

protected void populateRemoteAddr() {
    SocketChannel sc = getSocket().getIOChannel();
    if (sc != null) {
        InetAddress inetAddr = sc.socket().getInetAddress();
        if (inetAddr != null) {
            remoteAddr = inetAddr.getHostAddress();
        }
    }
}

至于L7,代碼邏輯是找到了,在org.apache.catalina.valves.RemoteIpValve,主要通過x-forwarded-for來兜底,但是SpringBoot下默認沒走這套邏輯,不深入研究了。

需要注意的是,對于以上的部署結(jié)構(gòu),我們從remoteAddr獲取到的是nginx的ip,所以肯定是無效的。

所以tomcat這邊的ip肯定在上游通過header傳下來的。

nginx

第一個知識點,在nginx中通過$remote_addr內(nèi)置變量獲取tcp層的客戶端ip,這是我們需要的ip。

就像這樣

proxy_set_header X-real-ip $remote_addr;

第二個知識點,針對多層代理的情況,可以在每一層的nginx設(shè)置$proxy_add_x_forwarded_for

就像這樣

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

它會吧每一層的ip疊加起來。

比如1.0.0.0訪問2.0.0.0,然后2.0.0.0訪問3.0.0.0。最終我在3.0.0.0看到的X-Forwarded-For
1.0.0.0,``2.0.0.0。

如果上一層的http請求沒有X-Forwarded-For頭,默認取$remote_addr的值。

測試代碼

image.png
image.png

java

默認端口8080

@GetMapping("/hello")
public String hello(HttpServletRequest request){
    System.out.println(request.getRemoteAddr());
    System.out.println(request.getHeader("X-Forwarded-For"));
    System.out.println(request.getHeader("X-real-ip"));
    System.out.println(request.getHeader("Host"));
    return "hello";
}

nginx

關(guān)于nginx使用,參考http://openresty.org/en/

nginx分為2種情況,單層和多層。

下面的是多層的配置,因為我使用最近的一層,就能模擬單層的場景了。

worker_processes  1;
error_log logs/error.log;
events {
        worker_connections 1024;
}
http {
        server {
                listen 8081;
                location / {
                        proxy_set_header            Host $host;
                        proxy_set_header            X-real-ip $remote_addr;
                        proxy_set_header            X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_pass http://localhost:8080;
                }
        }

        server {
                listen 8082;
                location / {
                        proxy_set_header            Host $host;
                        proxy_set_header            X-real-ip $remote_addr;
                        proxy_set_header            X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_pass http://localhost:8081;
                }
        }

        server {
                listen 8083;
                location / {
                        proxy_set_header            Host $host;
                        proxy_set_header            X-real-ip $remote_addr;
                        proxy_set_header            X-Forwarded-For $remote_addr;
                        proxy_pass http://localhost:8082;
                }
        }
}

保證你的手機和電腦在一個網(wǎng)絡(luò),然后通過ifconfig en0 獲取你的電腦網(wǎng)卡地址,比如我電腦地址為192.168.3.2,我的手機地址為192.168.3.23

通過手機訪問以下地址

http://192.168.3.2:8080/hello
http://192.168.3.2:8081/hello
http://192.168.3.2:8082/hello
http://192.168.3.2:8083/hello

通過電腦調(diào)用以下命令

curl http://localhost:8083/hello -H "Host:192.178.1.1"
curl http://localhost:8083/hello -H "X-Forwarded-For:192.178.1.1"
curl http://localhost:8082/hello -H "X-Forwarded-For:192.178.1.1"

對應(yīng)輸出分別為

#http://192.168.3.2:8080/hello
192.168.3.23
null
null
192.168.3.2:8080

#http://192.168.3.2:8081/hello
127.0.0.1
192.168.3.23
192.168.3.23
192.168.3.2

#http://192.168.3.2:8082/hello
0:0:0:0:0:0:0:1
192.168.3.23, 127.0.0.1
127.0.0.1
192.168.3.2

#http://192.168.3.2:8083/hello
0:0:0:0:0:0:0:1
192.168.3.23, 127.0.0.1
127.0.0.1
192.168.3.2

#curl http://localhost:8083/hello -H "Host:192.178.1.1"
0:0:0:0:0:0:0:1
127.0.0.1, 127.0.0.1, 127.0.0.1
127.0.0.1
192.178.1.1

#curl http://localhost:8083/hello -H "X-Forwarded-For:192.178.1.1"
127.0.0.1
127.0.0.1, 127.0.0.1, 127.0.0.1
127.0.0.1
localhost

#curl http://localhost:8082/hello -H "X-Forwarded-For:192.178.1.1"
127.0.0.1
192.178.1.1, 127.0.0.1, 127.0.0.1
127.0.0.1
localhost

可以發(fā)現(xiàn)

  1. X-Forwarded-For記錄的是整條鏈路請求經(jīng)過節(jié)點的ip地址,并且上一條的header不存在X-Forwarded-For頭的情況下,它是取客戶端的ip,可以被篡改
  2. X-real-ip返回的是上一跳的ip地址,無效
  3. Host默認從訪問地址中拿,一層層往下傳遞,如果客戶端有取客戶端的,可以被篡改

最佳實踐

一般情況下,代理服務(wù)器都是一層,所以我們直接用proxy_set_header X-real-ip $remote_addr 即可,或者proxy_set_header X-Forwarded-For $remote_addr;也是一個道理

但是在多代理服務(wù)存在的可能性下,首先我們必須使用X-Forwarded-For,其次最外層的nginx服務(wù)器需要配置為proxy_set_header X-Forwarded-For $remote_addr;

為何我能繞過白名單

天下代碼一大抄。

首先我猜測,對方的對外nginx服務(wù)器的配置肯定是

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

其次,針對java代碼中獲取ip的邏輯

取我自己網(wǎng)關(guān)項目ip白名單的邏輯

public static String getRemoteAddr(HttpServletRequest request) {
  List<String> headers = Lists.newArrayList("remoteip", "X-Real-IP","X-Forwarded-For");
  for (String header : headers) {
    String ip = request.getHeader(header);
    if (isValid(ip)) {
      if (header.equals("X-Forwarded-For")) {
        ip = ip.split(",")[0];
      }
      return ip;
    }
  }
  log.info("未獲取到客戶端IP");
  return Constants.UNKNOWN_IP_ADDRESS;
}

存在取多個header的邏輯,勢必可以通過模擬header的方式進行繞過。

參考

https://www.nginx.cn/doc/index.html
https://www.cnblogs.com/lvcisco/p/10309834.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容