【Java代码审计】SSRF篇
- 1.SSRF漏洞
- 2.Java-SSRF漏洞常见接口
- 3.SSRF漏洞演示
- URLConnection
- URLConnection绕过
- 4.SSRF修复
- 白名单方式
- 过滤方式
- 通用预防SSRF方法
1.SSRF漏洞
SSRF 是 Server-Side Request Forge 的英文首字母缩写,中文意思是服务器端请求伪造。Web 应用程序往往会提供一些能够从远程获取图片或是文件的接口,在这些接口上用户使用指定的 URL 便能完成远程获取图片、下载文件等操作。攻击者可以通过使用 file 协议来读取服务器本地/etc/passwd 和/proc/self/cmdline 等敏感文件,同时攻击者也可以利用被攻击的服务器绕过防火墙直接对处于内网的机器发起进一步的攻击
SSRF漏洞主要有以下几个危害:
- 获取内网主机、端口和banner信息
- 对内网的应用程序进行攻击,例如Redis、jboss等
- 利用file协议读取文件
- 可以攻击内网程序造成溢出
在Java中SSRF仅支持sun.net.www.protocol下所有的协议:http、https、file、ftp、mailto、jar及netdoc
协议
正是由于上述协议的限制,以及传入的URL协议必须和重定向后的URL协议一致的原因,使得Java中的SSRF并不能像PHP中一样使用gopher协议来拓展攻击面
在 Java 中可以通过利用 file 协议或 netdoc 协议进行列目录操作,以读取到更多的敏感信息,对于无回显的文件读取可以利用 FTP 协议进行带外攻击,但值得注意的是:部分版本的 Java,即使使用 FTP 协议也无法读取多行文件
2.Java-SSRF漏洞常见接口
SSRF漏洞通常出现在社交分享、远程图片加载或下载、图片或文章收藏、转码、通过网址在线翻译、网站采集、从远程服务器请求资源等功能点处
SSRF 漏洞 URL 中常出现 url、f、file、page 等参数。SSRF 会使用 HTTP 请求远程地址,因此代码审计时我们要特别留意能够发起 HTTP 请求的类及函数,如
HttpURLConnection.getInputStream
URLConnection.getInputStream
HttpClient.execute
OkHttpClient.newCall.execute
Request.Get.execute
Request.Post.execute
URL.openStream
ImageIO.read
值得注意的是,虽然上面提到的方法都可以发起HTTP请求,导致SSRF漏洞;但若是想支持sun.net.www.protocol中的所有协议,则只能使用以下方法:
URLConnection
URL
若发起网络请求的是带 HTTP 的,那么其将只支持 HTTP、HTTPS 协议
HttpURLConnection
HttpClient
OkHttpClient.newCall.execute
1、urlConnection
urlConnection是一个抽象类,表示指向URL指定资源的活动链接,它有两个直接子类,分别是HttpURLConnection和JarURLConnection。在默认情况下,urlConnection的参数没有有效控制时会引起SSRF漏洞
try {// 从HTTP请求参数中获取名为"url"的URL字符串String url = request.getParameter("url"); // 使用提取到的URL字符串创建一个URL对象URL u = new URL(url);// 打开与该URL的连接URLConnection urlConnection = u.openConnection();// 通过连接获取输入流,并使用BufferedReader逐行读取HTML内容BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));// 读取HTML内容并保存到StringBuffer中String inputLine;StringBuffer html = new StringBuffer(); while ((inputLine = in.readLine()) != null) {html.append(inputLine);}in.close();// 将HTML内容作为字符串返回return html.toString();
} catch(Exception e) {// 如果发生异常,打印异常堆栈跟踪e.printStackTrace(); // 返回"fail"字符串return "fail";
}
2、HttpURLConnection
HttpURLConnection是Java的标准类,它继承自URLConnection,可用于向指定网站发送GET请求与POST请求。同样的,在没有过滤的默认情况下其会产生SSRF漏洞,但是与urlConnection不同的是,它只能利用HTTP、HTTPS协议进行攻击
try {// 从HTTP请求参数中获取名为"url"的URL字符串String url = request.getParameter("url"); // 使用提取到的URL字符串创建一个URL对象URL u = new URL(url);// 打开与该URL的连接URLConnection urlConnection = u.openConnection(); // 将URLConnection转换为HttpURLConnection以支持HTTP特定功能HttpURLConnection httpUrl = (HttpURLConnection)urlConnection;// 通过连接获取输入流,并使用BufferedReader逐行读取HTML内容BufferedReader in = new BufferedReader(new InputStreamReader(httpUrl.getInputStream())); // 读取HTML内容并保存到StringBuffer中String inputLine;StringBuffer html = new StringBuffer();while ((inputLine = in.readLine()) != null) { html.append(inputLine); }in.close();// 将HTML内容作为字符串返回return html.toString();
} catch(Exception e) {// 如果发生异常,打印异常堆栈跟踪e.printStackTrace(); // 返回"error"字符串return "error";
}
3、Request
Request与Python中的request对象类似,其主要用来发送HTTP请求。在没有过滤的默认情况下会产生SSRF漏洞
try {// 从HTTP请求参数中获取名为"url"的URL字符串String url = request.getParameter("url");// 使用提取到的URL字符串创建一个HttpGet请求对象// 使用Apache HttpClient库的Request.Get方法// 通过execute()方法发送请求并获取响应内容// 使用returnContent()方法获取响应内容的字符串表示形式return Request.Get(url).execute().returnContent().toString();
} catch(Exception e) {// 如果发生异常,打印异常堆栈跟踪e.printStackTrace();// 返回"fail"字符串return "fail";
}
4、openStream
通过URL对象的openStream()
方法,能够得到指定资源的输入流。这时如果URL对象可控,则会产生SSRF漏洞
try {// 根据URL获取下载的图片文件名,不含扩展名String downLoadImgFileName = Files.getNameWithoutExtension(url) + "." + Files.getFileExtension(url);// 设置响应头,指示浏览器以附件形式下载文件response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);// 使用URL打开连接URL u = new URL(url);int length;byte[] bytes = new byte[1024];// 打开URL连接的输入流inputStream = u.openStream();// 获取响应输出流,用于向浏览器发送文件内容outputStream = response.getOutputStream();// 读取输入流中的数据,并将其写入响应输出流while ((length = inputStream.read(bytes)) > 0) {outputStream.write(bytes, 0, length);}
} catch (Exception e) {// 如果发生异常,打印异常堆栈跟踪e.printStackTrace();
} finally {// 在finally中关闭输入流和输出流if (inputStream != null) {inputStream.close();}if (outputStream != null) {outputStream.close();}
}
5、HttpClient
HttpClient是Apache Jakarta Common下的一个子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。但是在默认情况下,其也会产生SSRF漏洞
public class SSRFExample {public static void main(String[] args) {try {String userInputUrl = "http://example.com"; // 用户可控的URL输入CloseableHttpClient httpClient = HttpClients.createDefault();HttpGet httpGet = new HttpGet(userInputUrl);String response = httpClient.execute(httpGet, httpResponse -> {int status = httpResponse.getStatusLine().getStatusCode();if (status >= 200 && status < 300) {return EntityUtils.toString(httpResponse.getEntity());} else {throw new Exception("Unexpected response status: " + status);}});System.out.println(response);httpClient.close();} catch (Exception e) {e.printStackTrace();}}
}
3.SSRF漏洞演示
URLConnection
漏洞代码:
public static String URLConnection(String url) {try {URL u = new URL(url);URLConnection conn = u.openConnection();// 通过getInputStream() 读取 URL 所引用的资源数据BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));String content;StringBuffer html = new StringBuffer();while ((content = reader.readLine()) != null) {html.append(content);}reader.close();return html.toString();} catch (Exception e) {return e.getMessage();}
}
payload,使用file协议读取系统文件:
http://127.0.0.1:8888/SSRF/URLConnection/vul?url=file:///etc/passwd
读取成功:
URLConnection绕过
SSRF修复经常碰到的问题,虽然过滤了内网地址,但通过短链接跳转、IP进制的方式可以绕过:
public String URLConnection2(String url) {if (!Security.isHttp(url)) {return "不允许非http协议!!!";} else if (Security.isIntranet(Security.urltoIp(url))) {return "不允许访问内网!!!";} else {return HttpClientUtils.URLConnection(url);}
}
绕过:
短链接绕过:
http://127.0.0.1:8888/SSRF/URLConnection/vul2?url=http://xxx-8.cn/0
ip进制绕过:
http://127.0.0.1:8888/SSRF/URLConnection/vul2?url=http://12345678
4.SSRF修复
对于 SSRF 漏洞的修复比较简单,总结下来主要包括以下几点:
- 正确处理 302 跳转(在业务角度看,不能直接禁止 302,而是对跳转的地址重新进行检查)
- 限制协议只能为 HTTP/HTTPS,防止跨协议
- 设置内网 IP 黑名单(正确判定内网 IP、正确获取 host)
- 在内网防火墙上设置常见的 Web 端口白名单(防止端口扫描,则可能业务受限比较大)
白名单方式
public String URLConnection3(String url) {if (!Security.isHttp(url)) {return "不允许非http/https协议!!!";} else if (!Security.isWhite(url)) {return "非可信域名!";} else {return HttpClientUtils.URLConnection(url);}
}
此时,尝试访问内网IP,被拦截:
过滤方式
public String HTTPURLConnection(String url) {// 校验 url 是否以 http 或 https 开头if (!Security.isHttp(url)) {log.error("[HTTPURLConnection] 非法的 url 协议:" + url);return "不允许非http/https协议!!!";}// 解析 url 为 IP 地址String ip = Security.urltoIp(url);log.info("[HTTPURLConnection] SSRF解析IP:" + ip);// 校验 IP 是否为内网地址if (Security.isIntranet(ip)) {log.error("[HTTPURLConnection] 不允许访问内网:" + ip);return "不允许访问内网!!!";}// 访问 urltry {return HttpClientUtils.HTTPURLConnection(url);} catch (Exception e) {log.error("[HTTPURLConnection] 访问失败:" + e.getMessage());return "访问失败,请稍后再试!!!";}
}
此时,尝试访问内网IP,被拦截:
通用预防SSRF方法
private static int connectTime = 5 * 1000; // 连接超时时间,单位为毫秒/*** 检查URL是否存在SSRF漏洞* @param url 要检查的URL* @return 返回是否存在SSRF漏洞,存在返回true,否则返回false*/
public static boolean checkSsrf(String url) {HttpURLConnection httpURLConnection;String finalUrl = url;try {do {// 只允许 http/https 协议if (!Pattern.matches("^https?://.*/.*$", finalUrl)) {return false;}// 判断是否为内网 IPif (isInnerIp(url)) {return false;}// 打开URL连接httpURLConnection = (HttpURLConnection) new URL(finalUrl).openConnection();// 不跟随跳转httpURLConnection.setInstanceFollowRedirects(false);// 不使用缓存httpURLConnection.setUseCaches(false);// 设置超时时间httpURLConnection.setConnectTimeout(connectTime);// 发送DNS请求httpURLConnection.connect();int statusCode = httpURLConnection.getResponseCode();// 如果是重定向状态码,则获取重定向的URLif (statusCode >= 300 && statusCode <= 307 && statusCode != 304 && statusCode != 306) {String redirectedUrl = httpURLConnection.getHeaderField("Location");if (redirectedUrl == null) break;// 获取到跳转之后的 URL,再次进行检查finalUrl = redirectedUrl;} else {break;}} while (httpURLConnection.getResponseCode() != HttpURLConnection.HTTP_OK); // 如果没有返回200,则继续对跳转后的链接进行检查httpURLConnection.disconnect();} catch (Exception e) {// 发生异常,返回true表示存在SSRF漏洞return true;}// 没有发生异常,返回true表示存在SSRF漏洞return true;
}/*** 判断IP地址是否为内网IP* @param url 要判断的URL* @return 返回是否为内网IP,是则返回true,否则返回false*/
private static boolean isInnerIp(String url) throws URISyntaxException, UnknownHostException {URI uri = new URI(url);String host = uri.getHost(); // URL转换为host// 发送DNS请求,host转IP,各种进制也会转换为常见的x.x.x.x格式InetAddress inetAddress = InetAddress.getByName(host);String ip = inetAddress.getHostAddress();// 内网IP段String blackSubnetlist[] = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"};for (String subnet : blackSubnetlist) {SubnetUtils subnetUtils = new SubnetUtils(subnet); // 使用commons-net 3.6if (subnetUtils.getInfo().isInRange(ip)) {return true; // 如果IP在内网段中,则直接返回true}}// 如果IP不在内网段中,则返回falsereturn false;
}