成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

談一談如何在Python開發中拒絕SSRF漏洞

安全 漏洞
特別是這兩年,大量利用SSRF攻擊內網服務的案例被爆出來,導致SSRF漏洞慢慢受到重視。這就給Web應用開發者提出了一個難題:如何在保證業務正常的情況下防御SSRF漏洞?

一 、SSRF漏洞常見防御手法及繞過方法

SSRF是一種常見的Web漏洞,通常存在于需要請求外部內容的邏輯中,比如本地化網絡圖片、XML解析時的外部實體注入、軟件的離線下載等。當攻擊者傳入一個未經驗證的URL,后端代碼直接請求這個URL,將會造成SSRF漏洞。

具體危害體現在以下幾點上:

URL為內網IP或域名,攻擊者將可以通過SSRF漏洞掃描目標內網,查找內網內的漏洞,并想辦法反彈權限

URL中包含端口,攻擊者將可以掃描并發現內網中機器的其他服務,再進一步進行利用

當請求方法允許其他協議的時候,將可能利用gopher、file等協議進行第三方服務利用,如利用內網的redis獲取權限、利用fastcgi進行getshell等

特別是這兩年,大量利用SSRF攻擊內網服務的案例被爆出來,導致SSRF漏洞慢慢受到重視。這就給Web應用開發者提出了一個難題:如何在保證業務正常的情況下防御SSRF漏洞?

很多開發者認為,只要檢查一下請求url的host不為內網IP,即可防御SSRF。這個觀點其實提出了兩個技術要點:

1.如何檢查IP是否為內網IP

2.如何獲取真正請求的host

于是,攻擊者通過這兩個技術要點,針對性地想出了很多繞過方法。

二、 如何檢查IP是否為內網IP

這實際上是很多開發者面臨的第一個問題,很多新手甚至連內網IP常用的段是多少也不清楚。

何謂內網IP,實際上并沒有一個硬性的規定,多少到多少段必須設置為內網。有的管理員可能會將內網的IP設置為233.233.233.0/24段,當然這是一個比較極端的例子。

通常我們會將以下三個段設置為內網IP段,所有內網內的機器分配到的IP是在這些段中:

  1. 192.168.0.0/16 => 192.168.0.0 ~ 192.168.255.255 
  2. 10.0.0.0/8 => 10.0.0.0 ~ 10.255.255.255 
  3. 172.16.0.0/12 => 172.16.0.0 ~ 172.31.255.255 

所以通常,我們只需要判斷目標IP不在這三個段,另外還包括一個 127.0.0.0/8 段即可。

很多人會忘記 127.0.0.0/8 ,認為本地地址就是 127.0.0.1 ,實際上本地回環包括了整個127段。你可以訪問http://127.233.233.233/,會發現和請求127.0.0.1是一個結果:

所以我們需要防御的實際上是4個段,只要IP不落在這4個段中,就認為是“安全”的。

網上一些開發者會選擇使用“正則”的方式判斷目標IP是否在這四個段中,這種判斷方法通常是會遺漏或誤判的,比如如下代碼:

這是Sec-News最老版本判斷內網IP的方法,里面使用正則判斷IP是否在內網的幾個段中。這個正則也是我當時臨時在網上搜的,很明顯這里存在多個繞過的問題:

1. 利用八進制IP地址繞過

2. 利用十六進制IP地址繞過

3. 利用十進制的IP地址繞過

4. 利用IP地址的省略寫法繞過

這四種方式我們可以依次試試:

四種寫法(5個例子):012.0.0.1 、 0xa.0.0.1 、 167772161 、 10.1 、 0xA000001 實際上都請求的是10.0.0.1,但他們一個都匹配不上上述正則表達式。

更聰明一點的人是不會用正則表達式來檢測IP的(也許這類人并不知道內網IP的正則該怎么寫)。Wordpress的做法是,先將IP地址規范化,然后用“.”將其分割成數組parts,然后根據parts[0]和parts[1]的取值來判斷:

其實也略顯麻煩,而且曾經也出現過用進制方法繞過的案例( WordPress <4.5 SSRF 分析 ),不推薦使用。

我后來選擇了一種更為簡單的方法。眾所周知,IP地址是可以轉換成一個整數的,在PHP中調用ip2long函數即可轉換,在Python使用inet_aton去轉換。

而且IP地址是和2^32內的整數一一對應的,也就是說0.0.0.0 == 0,255.255.255.255 == 2^32 - 1。所以,我們判斷一個IP是否在某個IP段內,只需將IP段的起始值、目標IP值全部轉換為整數,然后比較大小即可。

于是,我們可以將之前的正則匹配的方法修改為如下方法:

這就是一個最簡單的方法,也最容易理解。

假如你懂一點掩碼的知識,你應該知道IP地址的掩碼實際上就是(32 - IP地址所代表的數字的末尾bit數)。所以,我們只需要保證目標IP和內網邊界IP的前“掩碼”位bit相等即可。借助位運算,將以上判斷修改地更加簡單:

  1. from socket import inet_aton 
  2. from struct import unpack 
  3.   
  4. def ip2long(ip_addr): 
  5.     return unpack("!L", inet_aton(ip_addr))[0] 
  6.       
  7. def is_inner_ipaddress(ip): 
  8.     ip = ip2long(ip)     
  9.     return ip2long('127.0.0.0') >> 24 == ip >> 24 or \ 
  10.            ip2long('10.0.0.0') >> 24 == ip >> 24 or \         
  11.            ip2long('172.16.0.0') >> 20 == ip >> 20 or \        
  12.            ip2long('192.168.0.0') >> 16 == ip >> 16 

以上代碼也就是Python中判斷一個IP是否是內網IP的最終方法,使用時調用is_inner_ipaddress(...)即可(注意自己編寫捕捉異常的代碼)。

三、 host獲取與繞過

如何獲取"真正請求"的Host,這里需要考慮三個問題:

1. 如何正確的獲取用戶輸入的URL的Host?

2. 只要Host只要不是內網IP即可嗎?

3. 只要Host指向的IP不是內網IP即可嗎?

如何正確的獲取用戶輸入的URL的Host?

第一個問題,看起來很簡單,但實際上有很多網站在獲取Host上犯過一些錯誤。最常見的就是,使用http://233.233.233.233@10.0.0.1:8080/、http://10.0.0.1#233.233.233.233這樣的URL,讓后端認為其Host是233.233.233.233,實際上請求的卻是10.0.0.1。這種方法利用的是程序員對URL解析的錯誤,有很多程序員甚至會用正則去解析URL。

在Python 3下,正確獲取一個URL的Host的方法:

  1. from urllib.parse import urlparse 
  2.   
  3. url = 'https://10.0.0.1/index.php' 
  4. urlparse(url).hostname 

這一步一定不能犯錯,否則后面的工作就白做了。

只要Host只要不是內網IP即可嗎?

第二個問題,只要檢查一下我們獲取到的Host是否是內網IP,即可防御SSRF漏洞么?

答案是否定的,原因是,Host可能是IP形式,也可能是域名形式。如果Host是域名形式,我們是沒法直接比對的。只要其解析到內網IP上,就可以繞過我們的is_inner_ipaddress了。

網上有個服務 http://xip.io ,這是一個“神奇”的域名,它會自動將包含某個IP地址的子域名解析到該IP。比如 127.0.0.1.xip.io ,將會自動解析到127.0.0.1,www.10.0.0.1.xip.io將會解析到10.0.0.1:

這個域名極大的方便了我們進行SSRF漏洞的測試,當我們請求http://127.0.0.1.xip.io/info.php的時候,表面上請求的Host是127.0.0.1.xip.io,此時執行is_inner_ipaddress('127.0.0.1.xip.io')是不會返回True的。但實際上請求的卻是127.0.0.1,這是一個標準的內網IP。

所以,在檢查Host的時候,我們需要將Host解析為具體IP,再進行判斷,代碼如下:

  1. import socket 
  2.  import re 
  3.  from urllib.parse import urlparse 
  4.  from socket import inet_aton 
  5.  from struct import unpack 
  6.    
  7.  def check_ssrf(url): 
  8.      hostname = urlparse(url).hostname 
  9.        
  10.      def ip2long(ip_addr):     
  11.          return unpack("!L", inet_aton(ip_addr))[0]     
  12.            
  13.      def is_inner_ipaddress(ip): 
  14.          ip = ip2long(ip)     
  15.          return ip2long('127.0.0.0') >> 24 == ip >> 24 or \       
  16.                 ip2long('10.0.0.0') >> 24 == ip >> 24 or \         
  17.                 ip2long('172.16.0.0') >> 20 == ip >> 20 or \    
  18.                 ip2long('192.168.0.0') >> 16 == ip >> 16 
  19.                   
  20.       try: 
  21.           if not re.match(r"^https?://.*/.*$", url):      
  22.                  raise BaseException("url format error")  
  23.           ip_address = socket.getaddrinfo(hostname, 'http')[0][4][0] 
  24.           if is_inner_ipaddress(ip_address):        
  25.                raise BaseException("inner ip address attack")    
  26.           return True, "success" 
  27.        except BaseException as e:    
  28.           return False, str(e)     
  29.        except:     
  30.           return False, "unknow error" 

首先判斷url是否是一個HTTP協議的URL(如果不檢查,攻擊者可能會利用file、gopher等協議進行攻擊),然后獲取url的host,并解析該host,最終將解析完成的IP放入is_inner_ipaddress函數中檢查是否是內網IP。

只要Host指向的IP不是內網IP即可嗎?

第三個問題,是不是做了以上工作,解析并判斷了Host指向的IP不是內網IP,即防御了SSRF漏洞?

答案繼續是否定的,上述函數并不能正確防御SSRF漏洞。為什么?

當我們請求的目標返回30X狀態的時候,如果沒有禁止跳轉的設置,大部分HTTP庫會自動跟進跳轉。此時如果跳轉的地址是內網地址,將會造成SSRF漏洞。

這個原因也很好理解,我以Python的requests庫為例。requests的API中有個設置,叫allow_redirects,當將其設置為True的時候requests會自動進行30X跳轉。而默認情況下(開發者未傳入這個參數的情況下),requests會默認將其設置為True:

所以,我們可以試試請求一個302跳轉的網址:

默認情況下,將會跟蹤location指向的地址,所以返回的status code是最終訪問的頁面的狀態碼。而設置了allow_redirects的情況下,將會直接返回302狀態碼。

所以,即使我們獲取了http://t.cn/R2iwH6d的Host,通過了is_inner_ipaddress檢查,也會因為302跳轉,跳到一個內網IP,導致SSRF。

這種情況下,我們有兩種解決方法:

1. 設置allow_redirects=False,不允許目標進行跳轉

2. 每跳轉一次,就檢查一次新的Host是否是內網IP,直到抵達最后的網址

第一種情況明顯是會影響業務的,只是規避問題而未解決問題。當業務上需要目標URL能夠跳轉的情況下,只能使用第二種方法了。

所以,歸納一下,完美解決SSRF漏洞的過程如下:

1. 解析目標URL,獲取其Host

2. 解析Host,獲取Host指向的IP地址

3. 檢查IP地址是否為內網IP

4. 請求URL

5. 如果有跳轉,拿出跳轉URL,執行1

0x04 使用requests庫的hooks屬性來檢查SSRF

那么,上一章說的5個過程,具體用Python怎么實現?

我們可以寫一個循環,循環條件就是“該次請求的狀態碼是否是30X”,如果是就繼續執行循環,繼續跟進location,如果不是,則退出循環。代碼如下:

  1. r = requests.get(url, allow_redirects=False
  2. while r.is_redirect: 
  3.     url = r.headers['location']  
  4.     succ, errstr = check_ssrf(url)  
  5.     if not succ: 
  6.         raise Exception('SSRF Attack.')  
  7.     r = requests.get(url, allow_redirects=False

這個代碼思路大概沒有問題,但非常簡陋,而且效率不高。

只要你翻翻requests的源代碼,你會發現,它在處理30X跳轉的時候考慮了很多地方:

  • 所有請求放在一個requests.Session()中
  • 跳轉有個緩存,當下次跳轉地址在緩存中的時候,就不用多次請求了
  • 跳轉數量有最大限制,不可能無窮無盡跳下去
  • 解決307跳轉出現的一些BUG等

如果說就按照之前簡陋的代碼編寫程序,固然可以防御SSRF漏洞,但上述提高效率的方法均沒用到。

那么,有更好的解決方法么?當然有,我們翻一下requests的源代碼,可以看到一行特殊的代碼:

hook的意思就是“劫持”,意思就是在hook的位置我可以插入我自己的代碼。我們看看dispatch_hook函數做了什么:

  1. def dispatch_hook(key, hooks, hook_data, **kwargs): 
  2.      """Dispatches a hook dictionary on a given piece of data.""" 
  3.      hookshooks = hooks or dict() 
  4.      hookshooks = hooks.get(key)     
  5.      if hooks:     
  6.          if hasattr(hooks, '__call__'): 
  7.              hooks = [hooks]     
  8.          for hook in hooks:       
  9.              _hook_data = hook(hook_data, **kwargs)       
  10.              if _hook_data is not None:        
  11.                  hook_data = _hook_data 
  12.      return hook_data 

hooks是一個函數,或者一系列函數。這里做的工作就是遍歷這些函數,并調用:

  1. _hook_data = hook(hook_data,**kwargs) 

我們翻翻文檔,可以找到hooks event的說明 http://docs.python-requests.org/en/master/user/advanced/?highlight=hook#event-hooks :

文檔中定義了一個print_url函數,將其作為一個hook函數。在請求的過程中,響應對象被傳入了print_url函數,請求的域名被打印了下來。

我們可以考慮一下,我們將檢查SSRF的過程也寫為一個hook函數,然后傳給requests.get,在之后的請求中一旦獲取response就會調用我們的hook函數。這樣,即使我設置allow_redirects=True,requests在每次請求后都會調用一次hook函數,在hook函數里我只需檢查一下response.headers['location']即可。

說干就干,先寫一個hook函數:

當r.is_redirect為True的時候,也就是說這次請求包含一個跳轉。獲取此時的r.headers['location'],并進行一些處理,最后傳入check_ssrf。當檢查不通過時,拋出一個異常。

然后編寫一個請求函數safe_request_url,意思是“安全地請求一個URL”。使用這個函數請求的域名,將不會出現SSRF漏洞:

我們可以看到,在第一次請求url前,還是需要check_ssrf一次的。因為hook函數_request_check_location只是檢查30X跳轉時是否存在SSRF漏洞,而沒有檢查最初請求是否存在SSRF漏洞。

不過上面的代碼還不算完善,因為_request_check_location覆蓋了原有(用戶可能定義的其他hooks)的hooks屬性,所以需要簡單調整一下。

最終,給出完整代碼:

  1. import socket 
  2.  import re 
  3.  import requests 
  4.  from urllib.parse import urlparse 
  5.  from socket import inet_aton 
  6.  from struct import unpack 
  7.  from requests.utils import requote_uri 
  8.    
  9.  def check_ssrf(url): 
  10.      hostname = urlparse(url).hostname 
  11.        
  12.      def ip2long(ip_addr): 
  13.          return unpack("!L", inet_aton(ip_addr))[0] 
  14.        
  15.      def is_inner_ipaddress(ip):     
  16.          ip = ip2long(ip)     
  17.          return ip2long('127.0.0.0') >> 24 == ip >> 24 or \                 
  18.                 ip2long('10.0.0.0') >> 24 == ip >> 24 or \                 
  19.                 ip2long('172.16.0.0') >> 20 == ip >> 20 or \                 
  20.                 ip2long('192.168.0.0') >> 16 == ip >> 16     
  21.                   
  22.       try:         
  23.           if not re.match(r"^https?://.*/.*$", url):             
  24.               raise BaseException("url format error")         
  25.           ip_address = socket.getaddrinfo(hostname, 'http')[0][4][0] 
  26.           if is_inner_ipaddress(ip_address):             
  27.               raise BaseException("inner ip address attack")         
  28.           return True, "success"     
  29.             
  30.        except BaseException as e:         
  31.            return False, str(e)     
  32.        except:         
  33.            return False, "unknow error" 
  34.              
  35. def safe_request_url(url, **kwargs): 
  36.     def _request_check_location(r, *args, **kwargs):         
  37.         if not r.is_redirect:             
  38.             return         
  39.               
  40.         url = r.headers['location']         
  41.           
  42.         # The scheme should be lower case...         
  43.         parsed = urlparse(url)         
  44.         url = parsed.geturl()         
  45.           
  46.         # Facilitate relative 'location' headers, as allowed by RFC 7231.         
  47.         # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')         
  48.         # Compliant with RFC3986, we percent encode the url.         
  49.         if not parsed.netloc:             
  50.             url = urljoin(r.url, requote_uri(url))         
  51.         else:             
  52.             url = requote_uri(url)         
  53.               
  54.         succ, errstr = check_ssrf(url)         
  55.         if not succ:             
  56.             raise requests.exceptions.InvalidURL("SSRF Attack: %s" % (errstr, ))     
  57.           
  58.     success, errstr = check_ssrf(url)     
  59.     if not success:         
  60.         raise requests.exceptions.InvalidURL("SSRF Attack: %s" % (errstr,))     
  61.           
  62.     all_hooks = kwargs.get('hooks', dict())     
  63.     if 'response' in all_hooks:         
  64.         if hasattr(all_hooks['response'], '__call__'):             
  65.             r_hooks = [all_hooks['response']]         
  66.         else:             
  67.             r_hooks = all_hooks['response']         
  68.               
  69.         r_hooks.append(_request_check_location)     
  70.           
  71.     else: 
  72.         r_hooks = [_request_check_location]     
  73.           
  74.     all_hooks['response'] = r_hooks    
  75.     kwargs['hooks'] = all_hooks     
  76.     return requests.get(url, **kwargs) 

外部程序只要調用safe_request_url(url)即可安全地請求某個URL,該函數的參數與requests.get函數參數相同。

完美在Python Web開發中解決SSRF漏洞。其他語言的解決方案類似,大家可以自己去探索。

參考內容:

http://www.luteam.com/?p=211

http://docs.python-requests.org/

責任編輯:趙寧寧 來源: 安全客
相關推薦

2021-07-28 20:12:17

WindowsHeap內存

2021-02-19 09:19:11

消息隊列場景

2022-07-04 10:51:27

數據中臺數據倉庫

2022-02-14 22:22:30

單元測試Junit5

2018-08-21 14:42:29

閃存存在問題

2021-03-15 22:42:25

NameNodeDataNode分布式

2020-02-19 10:45:04

開發技能代碼

2014-07-17 10:11:53

Android LAPI谷歌

2020-10-29 08:38:07

Volodya漏洞惡意軟件

2018-01-11 09:51:34

2021-05-11 08:48:23

React Hooks前端

2021-11-23 09:45:26

架構系統技術

2011-07-28 09:22:56

Oracle WDPOracle數據庫

2015-03-27 15:07:55

云計算IaaS平臺Docker

2016-07-08 13:33:12

云計算

2017-11-21 14:32:05

容器持久存儲

2019-01-30 10:59:48

IPv6Happy EyebaIPv4

2009-12-29 14:25:14

phpXSS漏洞

2020-11-20 10:22:34

代碼規范設計

2018-09-05 15:15:58

來電顯示來電顯示欺詐身份
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 在线免费观看黄a | 国产日韩欧美一区二区 | 在线91| 华人黄网站大全 | 丝袜久久| 欧美专区在线视频 | 国产成人在线看 | 久久久久久av | 亚洲国产日韩一区 | 国产成人在线观看免费 | 亚州精品天堂中文字幕 | 夜夜骚| 久久精品亚洲精品国产欧美 | 国产精品乱码一二三区的特点 | 欧美白人做受xxxx视频 | 中文天堂在线一区 | 99精品久久久 | 国产精品mv在线观看 | 日韩成人免费av | 国产羞羞视频在线观看 | 午夜在线视频 | 99re热精品视频 | 欧美日韩亚洲国产综合 | 亚洲日韩中文字幕一区 | 色一情一乱一伦一区二区三区 | 日韩欧美国产精品 | 国产成人免费视频网站高清观看视频 | 免费黄篇 | 91精品国产一区二区三区蜜臀 | 欧美日韩视频在线 | 精品久久国产 | 免费久久久久久 | 免费午夜视频 | 中文字幕在线视频网站 | 日本免费一区二区三区 | 91精品综合久久久久久五月天 | 亚洲精品一 | 亚洲成av | www.国产日本 | 91 在线| 国产一级视频在线播放 |