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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

说到 Web 安全我们前端可能接触较多的是 XSS 和 CSRF。工作原因在所负责的内部服务中遭遇了SSRF 的困扰在此记录一下学习过程及解决方案。SSRFServer-Side Request Forgery即服务端请求伪造是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下SSRF 攻击的目标是从外网无法访问的内部系统。

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

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

SSRF 带来的危害有

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

通用的解决方案有

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/8169.254.0.0/16127.0.0.0/8 和 240.0.0.0/4 等保留网段* 若 IP 为 10.0.0.0/8172.16.0.0/12192.168.0.0/16 私有网段请求该 IP 地址并判断响应 contents-type 是否为 application/json
3.解决 URL 获取器和 URL 解析器不一致的方法为解析 URL 后去除 RFC3986 中 user、pass 并重新组合 URL然后是按照以上思路实现的 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/8169.254.0.0/16127.0.0.0/8240.0.0.0/4 保留网段
 * 若访问 10.0.0.0/8172.16.0.0/12192,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 漏洞的原则结合上述几个函数就能大致完成。

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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6