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黑名单对请求地址进行检测包括重定向后的地址。

参考