来自公众号:新世界杂货铺

写在最后面

“年年岁岁花类似,岁岁年年人不同”,没有什么是永恒的,很多货色都将成为过来式。比方,我以前在文章中自称“笔者”,细细想来这个称说还是有肯定的距离感,通过一番三思而行后,我打算将文章中的自称改为“老许”。

对于自称,老许就不扯太远了,上面还是回到本篇的宗旨。

什么是SSRF

SSRF英文全拼为Server Side Request Forgery,翻译为服务端申请伪造。攻击者在未能获得服务器权限时,利用服务器破绽以服务器的身份发送一条结构好的申请给服务器所在内网。对于内网资源的访问控制,想必大家心里都无数。

下面这个说法如果不好懂,那老许就间接举一个理论例子。当初很多写作平台都反对通过URL的形式上传图片,如果服务器对URL校验不严格,此时就为歹意攻击者提供了拜访内网资源的可能。

“千里之堤,溃于蚁穴”,任何可能造成危险的破绽咱们程序员都不应漠视,而且这类破绽很有可能会成为他人绩效的垫脚石。为了不成为垫脚石,上面老许就和各位读者一起看一下SSRF的攻防回合。

回合一:变幻无穷的内网地址

为什么用“变幻无穷”这个词?老许先不答复,请各位读者急躁往下看。上面,老许用182.61.200.7(www.baidu.com的一个IP地址)这个IP和各位读者一起温习一下IPv4的不同示意形式。

留神⚠️:点分混合制中,以点宰割地每一部分均能够写作不同的进制(仅限于十、八和十六进制)。

下面仅是IPv4的不同体现形式,IPv6的地址也有三种不同示意形式。而这三种体现形式又能够有不同的写法。上面以IPv6中的回环地址0:0:0:0:0:0:0:1为例。

留神⚠️:冒分十六进制表示法中每个X的前导0是能够省略的,那么我能够局部省略,局部不省略,从而将一个IPv6地址写出不同的表现形式。0位压缩表示法和内嵌IPv4地址表示法同理也能够将一个IPv6地址写出不同的表现形式。

讲了这么多,老许曾经无奈统计一个IP能够有多少种不同的写法,麻烦数学好的算一下。

内网IP你认为到这儿就完了嘛?当然不!不晓得各位读者有没有听过xip.io这个域名。xip能够帮你做自定义的DNS解析,并且能够解析到任意IP地址(包含内网)。

咱们通过xip提供的域名解析,还能够将内网IP通过域名的形式进行拜访。

对于内网IP的拜访到这儿仍将持续!搞过Basic验证的应该都晓得,能够通过http://user:passwd@hostname/进行资源拜访。如果攻击者换一种写法或者能够绕过局部不够谨严的逻辑,如下所示。

对于内网地址,老许掏空了所有的常识储备总结出上述内容,因而老许说一句变幻无穷的内网地址不过分吧!

此时此刻,老许只想问一句,当歹意攻击者用这些不同表现形式的内网地址进行图片上传时,你怎么将其辨认进去并回绝拜访。不会真的有大佬用正则表达式实现上述过滤吧,如果有请留言通知我让小弟学习一下。

花样百出的内网地址咱们曾经根本理解,那么当初的问题是怎么将其转为一个咱们能够进行判断的IP。总结下面的内网地址可分为三类:一、自身就是IP地址,仅表现形式不对立;二、一个指向内网IP的域名;三、一个蕴含Basic验证信息和内网IP的地址。依据这三类特色,在发动申请之前依照如下步骤能够辨认内网地址并回绝拜访。

  1. 解析出地址中的HostName。
  2. 发动DNS解析,取得IP。
  3. 判断IP是否是内网地址。

上述步骤中对于内网地址的判断,请不要疏忽IPv6的回环地址和IPv6的惟一本地地址。上面是老许判断IP是否为内网IP的逻辑。

// IsLocalIP 判断是否是内网ipfunc IsLocalIP(ip net.IP) bool {    if ip == nil {        return false    }    // 判断是否是回环地址, ipv4时是127.0.0.1;ipv6时是::1    if ip.IsLoopback() {        return true    }    // 判断ipv4是否是内网    if ip4 := ip.To4(); ip4 != nil {        return ip4[0] == 10 || // 10.0.0.0/8            (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12            (ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16    }    // 判断ipv6是否是内网    if ip16 := ip.To16(); ip16 != nil {        // 参考 https://tools.ietf.org/html/rfc4193#section-3        // 参考 https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses        // 判断ipv6惟一本地地址        return 0xfd == ip16[0]    }    // 不是ip间接返回false    return false}

下图为依照上述步骤检测申请是否是内网申请的后果。

小结:URL形式多样,能够应用DNS解析获取标准的IP,从而判断是否是内网资源。

回合二:URL跳转

如果歹意攻击者仅通过IP的不同写法进行攻打,那咱们天然能够居安思危,然而这场矛与盾的较量才刚刚开局。

咱们回顾一下回合一的进攻策略,检测申请是否是内网资源是在正式发动申请之前,如果攻击者在申请过程中通过URL跳转进行内网资源拜访则齐全能够绕过回合一中的进攻策略。具体攻打流程如下。

如图所示,通过URL跳转攻击者可取得内网资源。在介绍如何进攻URL跳转攻打之前,老许和各位读者先一起温习一下HTTP重定向状态码——3xx。

依据维基百科的材料,3xx重定向码范畴从300到308共9个。老许特意瞧了一眼go的源码,发现官网的http.Client收回的申请仅反对如下几个重定向码。

301:申请的资源已永恒挪动到新地位;该响应可缓存;重定向申请肯定是GET申请。

302:要求客户端执行长期重定向;只有在Cache-Control或Expires中进行指定的状况下,这个响应才是可缓存的;重定向申请肯定是GET申请。

303:当POST(或PUT / DELETE)申请的响应在另一个URI能被找到时可用此code,这个code存在次要是为了容许由脚本激活的POST申请输入重定向到一个新的资源;303响应禁止被缓存;重定向申请肯定是GET申请。

307:长期重定向;不可更改申请办法,如果原申请是POST,则重定向申请也是POST。

308:永恒重定向;不可更改申请办法,如果原申请是POST,则重定向申请也是POST。

3xx状态码温习就到这里,咱们持续SSRF的攻防回合探讨。既然服务端的URL跳转可能带来危险,那咱们只有禁用URL跳转就齐全能够躲避此类危险。然而咱们并不能这么做,这个做法在躲避危险的同时也极有可能误伤失常的申请。那到底该如何防备此类攻打伎俩呢?

看过老许“Go中的HTTP申请之——HTTP1.1申请流程剖析”这篇文章的读者应该晓得,对于重定向有业务需要时,能够自定义http.Client的CheckRedirect。上面咱们先看一下CheckRedirect的定义。

CheckRedirect func(req *Request, via []*Request) error

这里特地阐明一下,req是行将收回的申请且申请中蕴含前一次申请的响应,via是曾经收回的申请。在通晓这些条件后,进攻URL跳转攻打就变得非常容易了。

  1. 依据前一次申请的响应间接回绝307308的跳转(此类跳转能够是POST申请,危险极高)。
  2. 解析出申请的IP,并判断是否是内网IP。

根据上述步骤,可如下定义http.Client

client := &http.Client{    CheckRedirect: func(req *http.Request, via []*http.Request) error {        // 跳转超过10次,也回绝持续跳转        if len(via) >= 10 {            return fmt.Errorf("redirect too much")        }        statusCode := req.Response.StatusCode        if statusCode == 307 || statusCode == 308 {            // 回绝跳转拜访            return fmt.Errorf("unsupport redirect method")        }        // 判断ip        ips, err := net.LookupIP(req.URL.Host)        if err != nil {            return err        }        for _, ip := range ips {            if IsLocalIP(ip) {                return fmt.Errorf("have local ip")            }            fmt.Printf("%s -> %s is localip?: %v\n", req.URL, ip.String(), IsLocalIP(ip))        }        return nil    },}

如上自定义CheckRedirect能够防备URL跳转攻打,但此形式会进行屡次DNS解析,效率不佳。后文会联合其余攻击方式介绍更加有效率的进攻措施。

小结:通过自定义http.ClientCheckRedirect能够防备URL跳转攻打。

回合三:DNS Rebinding

家喻户晓,发动一次HTTP申请须要先申请DNS服务获取域名对应的IP地址。如果攻击者有可控的DNS服务,就能够通过DNS重绑定绕过后面的进攻策略进行攻打。

具体流程如下图所示。

验证资源是是否非法时,服务器进行了第一次DNS解析,取得了一个非内网的IP且TTL为0。对解析的IP进行判断,发现非内网IP能够后续申请。因为攻击者的DNS Server将TTL设置为0,所以正式发动申请时须要再次进行DNS解析。此时DNS Server返回内网地址,因为曾经进入申请资源阶段再无进攻措施,所以攻击者可取得内网资源。

额定提一嘴,老许特意看了Go中DNS解析的局部源码,发现Go并没有对DNS的后果作缓存,所以即便TTL不为0也存在DNS重绑定的危险。

在发动申请的过程中有DNS解析才让攻击者无隙可乘。如果咱们能对该过程进行管制,就能够防止DNS重绑定的危险。对HTTP申请管制能够通过自定义http.Transport来实现,而自定义http.Transport也有两个计划。

计划一

dialer := &net.Dialer{}transport := http.DefaultTransport.(*http.Transport).Clone()transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {    host, port, err := net.SplitHostPort(addr)    // 解析host和 端口    if err != nil {        return nil, err    }    // dns解析域名    ips, err := net.LookupIP(host)    if err != nil {        return nil, err    }    // 对所有的ip串行发动申请    for _, ip := range ips {        fmt.Printf("%v -> %v is localip?: %v\n", addr, ip.String(), IsLocalIP(ip))        if IsLocalIP(ip) {            continue        }        // 非内网IP可持续拜访        // 拼接地址        addr := net.JoinHostPort(ip.String(), port)        // 此时的addr仅蕴含IP和端口信息        con, err := dialer.DialContext(ctx, network, addr)        if err == nil {            return con, nil        }        fmt.Println(err)    }    return nil, fmt.Errorf("connect failed")}// 应用此client申请,可防止DNS重绑定危险client := &http.Client{    Transport: transport,}

transport.DialContext的作用是创立未加密的TCP连贯,咱们通过自定义此函数可躲避DNS重绑定危险。另外特地阐明一下,如果传递给dialer.DialContext办法的地址是惯例IP格局则可应用net包中的parseIPZone函数间接解析胜利,否则会持续发动DNS解析申请。

计划二

dialer := &net.Dialer{}dialer.Control = func(network, address string, c syscall.RawConn) error {    // address 曾经是ip:port的格局    host, _, err := net.SplitHostPort(address)    if err != nil {        return err    }    fmt.Printf("%v is localip?: %v\n", address, IsLocalIP(net.ParseIP(host)))    return nil}transport := http.DefaultTransport.(*http.Transport).Clone()// 应用官网库的实现创立TCP连贯transport.DialContext = dialer.DialContext// 应用此client申请,可防止DNS重绑定危险client := &http.Client{    Transport: transport,}

dialer.Control在创立网络连接之后理论拨号之前调用,且仅在go版本大于等于1.11时可用,其具体调用地位在sock_posix.go中的(*netFD).dial办法里。

上述两个进攻计划不仅仅能够防备DNS重绑定攻打,也同样能够防备其余攻击方式。事实上,老许更加举荐计划二,几乎一劳永逸!

小结

  1. 攻击者能够通过本人的DNS服务进行DNS重绑定攻打。
  2. 通过自定义http.Transport能够防备DNS重绑定攻打。

集体教训

1、不要下发具体的错误信息!不要下发具体的错误信息!不要下发具体的错误信息!

如果是为了开发调试,请将错误信息打进日志文件里。强调这一点不仅仅是为了防备SSRF攻打,更是为了防止敏感信息透露。例如,DB操作失败后间接将error信息下发,而这个error信息很有可能蕴含SQL语句。

再额定多说一嘴,老许的公司对打进日志文件的某些信息还要求脱敏,堪称是非常严格了。

2、限度申请端口。

在完结之前特地阐明一下,SSRF破绽并不只针对HTTP协定。本篇只探讨HTTP协定是因为go中通过http.Client发动申请时会检测协定类型,某P*P语言这方面检测就会弱很多。尽管http.Client会检测协定类型,然而攻击者依然能够通过破绽一直更换端口进行内网端口探测。

最初,衷心希望本文可能对各位读者有肯定的帮忙。

  1. 写本文时, 笔者所用go版本为: go1.15.2
  2. 文章中所用残缺例子:https://github.com/Isites/go-...