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

2019-01-2213:57:06WEB安全防护Comments3,325 views字数 3539阅读模式

Web 安全,我们前端可能接触较多的是 XSS 和 CSRF。工作原因,在所负责的内部服务中遭遇了SSRF 的困扰,在此记录一下学习过程及解决方案。SSRF(Server-Side Request Forgery),即服务端请求伪造,是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定 URL 地址获取网页文本内容,加载指定地址的图片,下载等等。攻击者可根据程序流程,使用应用所在服务器发出攻击者想发出的 http 请求,利用该漏洞来探测生产网中的服务,可以将攻击者直接代理进内网中,可以让攻击者绕过网络访问控制,可以下载未授权的文件,可以直接访问内网,甚至能够获取服务器凭证。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

笔者负责的内部 web 应用中有一个下载文件的接口 /download,其接受一个 url 参数,指向需要下载的文件地址,应用向该地址发起请求,下载文件至应用所在服务器,然后作后续处理。问题便来了,应用所在服务器在这里成了跳板机,攻击者利用这个接口相当于取得了内网权限,能够进行不少具有危害的操作。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

SSRF 带来的危害有:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

  • 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息;
  • 攻击运行在内网或本地的应用程序(比如溢出);
  • 对内网 web 应用进行指纹识别,通过访问默认文件实现;
  • 攻击内外网的 web 应用,主要是使用 get 参数就可以实现的攻击(比如 struts2,sqli 等);
  • 利用 file 协议读取本地文件等。

通用的解决方案有:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

  1. 过滤返回信息。验证远程服务器对请求的响应是比较容易的方法。如果 web 应用是去获取某一种类型的文件,那么在把返回结果展示给用户之前先验证返回的信息是否符合标准;
  2. 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态;
  3. 限制请求的端口为 http 常用的端口,比如 80, 443, 8080, 8090;
  4. 白名单内网 ip。避免应用被用来获取获取内网数据,攻击内网;
  5. 禁用不需要的协议。仅仅允许 http 和 https 请求。可以防止类似于file:///,gopher://,ftp:// 等引起的问题。

由于笔者的应用 /download 接口请求的文件地址比较固定,因此采用了白名单 IP 的方式。当然,笔者也学习了一下更加全面的解决方案,下面给出安全部门同事的思路:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

  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 地址并判断响应 contents-type 是否为 application/json
  3. 解决 URL 获取器和 URL 解析器不一致的方法为:解析 URL 后去除 RFC3986 中 user、pass 并重新组合 URL

然后是按照以上思路实现的 Node.js 版本的处理 SSRF 漏洞的主要函数的代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

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://www.cainiaoxueyuan.com/anquan/9370.html

最后,一句话总结:永远不要相信用户的输入!文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

作者:逆葵
链接:https://juejin.im/post/5c466988f265da615f778eda
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/anquan/9370.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/anquan/9370.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定