GitLab web hooks SSRF(CVE-2018-8801) Patch analysis and How to safely fix SSRF
Auth by Cryin
介绍
GitLab官方发布安全公告,gitlab的WebHooks服务中存在SSRF漏洞,攻击者可以构造请求地址由gitlab发起内部网络请求。从而导致信息泄露,以及潜在的代码执行风险。
web hooks简单来说就是当项目发生提交代码、新建issue、merge等动作时会自动触发webhook url的http请求调用,这个请求接口可以自定义实现对这些动作进行一些处理操作。
受影响版本及详细可参考官方的公告:
GitLab Critical Security Release: 10.5.6, 10.4.6, and 10.3.9
补丁分析
以10.3.9版本的补丁进行分析,补丁commit链接: https://gitlab.com/gitlab-org/gitlab-ce/commit/2655d95d87a7f46029248062514daa3de2efde9b
在新版本中可以看到对于webhooks的请求是否允许向内网发起请求在app/models/application_setting.rb文件中增加了设置项allow_local_requests_from_hooks_and_services,通过这个设置来判断是否允许对内部网络发起请求。默认是false:
allow_local_requests_from_hooks_and_services: false
多处service文件中之前发起http请求使用的方法由原来的HTTParty替换成自定义的Gitlab::HTTP。
以bamboo_service.rb文件为例:
其中Gitlab::HTTP是在新增的两个程序文件实现,分别是lib/gitlab/目录下的http.rb、proxy_http_connection_adapter.rb http方法还是通过HTTParty实现,重点在proxy_http_connection_adapter.rb这个文件中,对HTTParty::ConnectionAdapter进行重写,然后调用UrlBlocker的blocked_url结合是否允许对内部网络进行请求的设置对当前请求的uri是否为内网ip地址进行安全校验。
module Gitlab
class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
def connection
if !allow_local_requests? && blocked_url?
raise URI::InvalidURIError
end
super
end
private
def blocked_url?
Gitlab::UrlBlocker.blocked_url?(uri, allow_private_networks: false)
end
def allow_local_requests?
options.fetch(:allow_local_requests, allow_settings_local_requests?)
end
def allow_settings_local_requests?
Gitlab::CurrentSettings.current_application_settings.allow_local_requests_from_hooks_and_services?
end
end
end
然后看下lib/gitlab/url_blocker.rb文件中blocked_url的实现,blocked_url方法主要是对当前请求uri进行校验,是否为127.0.0.1等本地地址、及是否为IPv4、IPv6格式的本地私有ip地址。如果设置不允许对内部网络进行访问的话。这里请求ip符合拦截的条件,则返回错误不发起当前请求。同时也通过VALID_IMPORT_PORTS限制了请求的端口为22、80、443。
再谈Web Hooks场景SSRF漏洞防御方案
很多应用会使用到Web Hooks这种场景,比如要实时通知第三方、或者给开发者提供消息推动等,诸如此类,url完全外部可控,这个时候由于url地址的不确定性没办法再使用域名白名单校验的方式解决SSRF的问题,此时要做的是限制恶意用户利用这个SSRF对内部网络发起请求和探测。
- 使用独立网络的服务器专门跑所有的回调请求,但本机端口及服务还是存在被探测的风险
- 使用ip黑名单的方式限制对127.*、10.开头的ip及内网私有ip地址发起请求
使用python实现的基于ip黑名单防御SSRF漏洞的代码demo:
import urlparse
import socket
import requests
def check_addr(addr):
if addr.startswith('127.') or addr.startswith('10.') or addr.startswith('192.'):
return False
return True
def safe_web_hooks(url):
parts = urlparse.urlparse(url)
host = parts.hostname
addr = socket.gethostbyname(host)
if not check_addr(addr):
raise ValueError('url policy violation')
resp=requests.get(url,allow_redirects=False)
print resp.status_code
safe_web_hooks('http://127.0.0.1:8080')
上述示例只是演示基本的ip黑名单原理,内网ip地址还需要具体结合企业网络实际情况进行全部覆盖,不然还是会有遗漏。另外注意一点是上述在发起http请求的时候限制了不允许url重定向,这里是要特别注意的,如果忽略了这一点,有可能造成使用重定向绕过黑名单检查继续对内部网络发起探请求。
上述代码如果允许重定向,则可能会绕过这个修复仍然能对内网私有ip地址发起请求。将目标站点是一个符合要求的正常站点域名地址,但是利用重定向跳转到一个内网ip地址。示例代码如下:
@RequestMapping(value="/redirect",method = RequestMethod.GET)
public String redirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String test = request.getParameter("url");
if (!test.isEmpty()) {
response.sendRedirect(test);
}
return test;
}
ssrf利用的请求链接形如: http://www.evil.com/redirect?url=http://127.0.0.1
总结
在代码层面上SSRF的正确防御方案是在对外部输入链接发起请求时对请求对应的ip地址进行黑名单检测判断其是否为内网ip。而且需要考虑url重定向的情况,对重定向后的地址也必须要经过ip黑名单检测,确保不能对内部网络发起探测请求。如笔者给eggjs/egg-security提交的不安全ctx.curl造成ssrf的问题修复,使用新增的ctx.safeCurl即可根据配置的ip黑名单对请求地址进行检测包括重定向后的地址。