Code前端首页关于Code前端联系我们

Web安全漏洞SSRF介绍及解决方案

terry 2年前 (2023-09-28) 阅读数 98 #Web安全

Web安全,我们的接口可能接触的比较多的是XSS和CSRF。由于工作原因,我在负责的内部服务中遇到了SSRF问题。我在这里记录一下学习过程和解决方案。 SSRF(Server-Side Request Forgery),即服务器端请求伪造,是攻击者构造的用于伪造服务器发起的请求的安全漏洞。通常,SSRF 攻击的目标是外部网络无法访问的内部系统。

SSRF 大多是由于服务器提供了从其他服务器应用程序检索数据的功能,并且没有对目标地址进行过滤或限制而造成的。例如,从指定URL获取网页上的文本内容、从指定地址加载图片、下载等。根据应用程序流程,攻击者可以利用应用程序所在的服务器发出攻击者请求的HTTP请求。想要做。该漏洞可用于发现生产网络中的服务。攻击者可以直接代理到内网,从而使攻击者绕过网络访问。检查一下,您可以下载未经授权的文件、直接访问内网甚至获取服务器凭据。

作者负责的内部Web应用程序有一个下载文件的接口/download。它接受一个 url 参数,该参数指向需要下载的文件的地址。应用程序向该地址发起请求,并将文件下载到应用程序位置。服务器,然后进行后续处理。出现问题。应用所在的服务器就成为了跳板。攻击者利用该接口获取内网权限,可以执行多种恶意操作。

SSRF造成的损害有:

  • 可以对外网、服务器所在内网、本地进行端口扫描,获取部分服务的banner信息;
  • 攻击内网或本地运行的应用程序(如溢出;struts2、sqli等);
  • 使用文件协议读取本地文件等。

常见的解决方案有:

  1. 过滤返回信息。验证远程服务器对请求的响应是更简单的方法。如果Web应用程序要获取特定类型的文件,则在向用户显示返回结果之前,必须检查返回的信息是否符合标准;
  2. 发送错误信息,防止用户根据错误信息判断远程服务器。端口状态;
  3. 将请求的端口限制为常用的http端口,例如80、443、8080、8090;
  4. 内网IP白名单。防止应用程序被利用获取内网数据并对内网进行攻击;
  5. 禁用不必要的协议。仅允许 http 和 https 请求。可以防止file:///、gopher://、ftp://等引起的问题。

由于作者应用/download接口请求的文件地址比较固定,所以白名单采用IP方式。当然,笔者也了解了更全面的解决方案。下面是安全部门同事的想法:

  1. 协议限制(默认允许的协议是HTTP和HTTPS)、30x跳数(标准不允许30x跳数)、统一错误信息(默认不统一、统一错误信息)避免恶意攻击并根据错误信息进行判断)
  2. IP地址评估:
    • 无法访问0.0.0.0/8、169.254.0.0/16、127.0.0.0/8和240.0。 0.0/4等保留网段
    • 如果IP为10.0.0.0/8、172.16.0.0/12、192.168.0.0/16私网段,请求IP地址并确定响应 内容
      是不是application/json
  3. 解决URL getter和URL parser不一致的方法是:解析完URL后,去掉user并提交RFC3986并组装根据实现的URLDen Node.js以上想法。处理SSRF漏洞的主函数版本号:
    const dns = require('dns')
    const parse = require('url-parse')
    const ip = require('ip')
    const isReservedIp = require('martian-cidr').default
    
    const protocolAndDomainRE = /^(?:https?:)?\/\/(\S+)$/
    
    const localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/
    const nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/
    
    /**
     * 检查链接是否合法
     * 仅支持 http/https 协议
     * @param {string} string
     * @returns {boolean}
     */
    function isValidLink (string) {
        if (typeof string !== 'string') {
            return false
        }
    
        var match = string.match(protocolAndDomainRE)
        if (!match) {
            return false
        }
    
        var everythingAfterProtocol = match[1]
        if (!everythingAfterProtocol) {
            return false
        }
    
        if (localhostDomainRE.test(everythingAfterProtocol) ||
            nonLocalhostDomainRE.test(everythingAfterProtocol)) {
            return true
        }
    
        return false
    }
    
    /**
     * @param {string} uri
     * @return string
     * host 解析为 ip 地址
     * 处理 SSRF 绕过:URL 解析器和 URL 获取器之间的不一致性
     *
     */
    async function filterIp(uri) {
        try {
            if (isValidLink(uri)) {
                const renwerurl = renewUrl(uri)
                const parseurl = parse(renwerurl)
                const host = await getHostByName(parseurl.host)
                const validataResult = isValidataIp(host)
    
                if(!validataResult) {
                    return false
                } else {
                    return renwerurl
                }
            } else {
                return false
            }
        } catch (e) {
            console.log(e)
        }
    }
    
    /**
     * 根据域名获取 IP 地址
     * @param {string} domain
     */
    function getHostByName (domain) {
        return new Promise((resolve, reject) => {
            dns.lookup(domain, (err, address, family) => {
                if(err) {
                    reject(err)
                }
                resolve(address)
            })
        })
    }
    
    /**
     * @param {string} host
     * @return {array} 包含 host、状态码
     *
     * 验证 host ip 是否合法
     * 返回值 array(host, value)
     * 禁止访问 0.0.0.0/8,169.254.0.0/16,127.0.0.0/8,240.0.0.0/4 保留网段
     * 若访问 10.0.0.0/8,172.16.0.0/12,192,168.0.0/16 私有网段,标记为 PrivIp 并返回
     */
    
    function isValidataIp (host) {
        if ((ip.isV4Format(host) || ip.isV6Format(host)) && !isReservedIp(host)) {
            if (ip.isPrivate(host)) {
                return [host, 'PrivIp']
            } else {
                return [host, 'WebIp']
            }
        } else {
            return false
        }
    }
    
    /**
     * @param {string} uri
     * @return {string} validateuri
     * 解析并重新组合 url,其中禁止'user' 'pass'组合
     */
    
    function renewUrl(uri) {
        const uriObj = parse(uri)
        let validateuri = `${uriObj.protocol}//${uriObj.host}`
        if (uriObj.port) {
            validateuri += `:${uriObj.port}`
        }
        if (uriObj.pathname) {
            validateuri += `${uriObj.pathname}`
        }
        if (uriObj.query) {
            validateuri += `?${uriObj.query}`
        }
        if (uriObj.hash) {
            validateuri += `#${uriObj.hash}`
        }
        return validateuri
    }
    复制代码

    对于可能导致漏洞的主界面管理函数,由于逻辑不同,这里不再给出具体实现。但只要遵循上面建议的避免SSRF漏洞的原则,结合上面的特性,就可以大致完成。

    最后一句话总结:永远不要相信用户输入!

    作者:倪奎
    链接:https://juejin.im/post/5c466988f265da615f778eda
    来源:掘金
    版权归作者所有。商业转载请联系作者获取授权。非商业转载请注明来源。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门