Java代码审计中的SSRF
漏洞原理
SSRF的形成大多是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片等,利用的是服务端的请求伪造。SSRF利用存在缺陷的Web应用作为代理攻击远程和本地的服务器。
漏洞利用
Java的SSRF利用方式比较局限
- 利用file协议任意文件读取。
- 利用http协议端口探测
网络请求支持的协议
Java网络请求支持的协议可通过下面几种方法检测:
- 代码中遍历协议
- 官方文档中查看
import sun.net.www.protocol查看
从import sun.net.www.protocol可以看到,支持以下协议
1
| file ftp mailto http https jar netdoc
|

发起网络请求的类
当然,SSRF是由发起网络请求的方法造成。所以先整理Java能发起网络请求的类。
- HttpClient
- Request(对HttpClient封装后的类)
- HttpURLConnection
- URLConnection
- URL
- okhttp
如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。
比如:
1 2 3 4
| HttpURLConnection HttpClient Request okhttp
|
所以,如果用以下类的方法发起请求,则支持sun.net.www.protocol所有协议
注意,Request类对HttpClient进行了封装。类似Python的requests库。
用法及其简单,一行代码就可以获取网页内容。
1
| Request.Get(url).execute().returnContent().toString();
|
漏洞代码
SSRF中的内网检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.kuang.ssfr.controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL;
@RestController public class SsrfController {
@GetMapping("/ssrf") public String fetchUrl(@RequestParam String url) { StringBuilder html = new StringBuilder(); try { URL u = new URL(url); HttpURLConnection httpUrl = (HttpURLConnection) u.openConnection(); BufferedReader base = new BufferedReader(new InputStreamReader(httpUrl.getInputStream(), "UTF-8")); String line; while ((line = base.readLine()) != null) { html.append(line); } base.close(); return html.toString(); } catch (Exception e) { e.printStackTrace(); return "请求失败"; } } }
|
在代码中HttpURLConnection httpUrl = (HttpURLConnection) urlConnection;,这个地方进行了强制转换,去某度搜索了一下具体用意。得出结论:
1 2
| URLConnection:可以走邮件、文件传输协议。 HttpURLConnection 只能走浏览器的HTTP协议
|
也就是说使用了强转为HttpURLConnection后,利用中只能使用http协议去探测该服务器内网的其他应用。
使用百度做测试

无法读取文件,因为这里只支持http和https的协议。

下面来试试不强制转换成HttpURLConnection
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @GetMapping("/ssrfServlet") public String handleRequest(@RequestParam("url") String url) { String htmlContent; StringBuffer html = new StringBuffer(); try { URL u = new URL(url); URLConnection urlConnection = u.openConnection(); BufferedReader base = new BufferedReader( new InputStreamReader(urlConnection.getInputStream(), "UTF-8") ); while ((htmlContent = base.readLine()) != null) { html.append(htmlContent); } base.close(); return html.toString(); } catch (Exception e) { e.printStackTrace(); return "请求失败"; }
|
是能够成功读取文件的

SSRF中的读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/readfileServlet") public void readFile(@RequestParam("url") String url, HttpServletResponse response) { try (InputStream inputStream = new URL(url).openStream(); OutputStream outputStream = response.getOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); } outputStream.flush(); } catch (Exception e) { e.printStackTrace(); response.setStatus(500); } }
|
和上面的代码对比一下,发现其实都大致相同,唯一不同的地方是一个是用openStream方法获取对象,一个是用openConnection获取对象。两个方法类似

payload
1
| http://127.0.0.1:8080//downloadServlet?url=file:///C:%5c%5c1.txt
|
注意: 这里是三个斜杆,并且反斜杠需要url编码 否则就会报错
SSRF中的文件下载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/download") public void downloadFile(@RequestParam("url") String url, HttpServletResponse response) throws IOException { String filename = "1.txt"; response.setHeader("content-disposition", "attachment;filename=" + filename); try (InputStream inputStream = new URL(url).openStream(); OutputStream outputStream = response.getOutputStream()) { byte[] bytes = new byte[1024]; int len; while ((len = inputStream.read(bytes)) > 0) { outputStream.write(bytes, 0, len); } } }
|
与读取文件不同的是响应头
1
| response.setHeader("content-disposition", "attachment;fileName=" + filename);
|
设置响应头,告诉浏览器这是一个附件,下载时保存为 1.txt
这段代码,设置mime类型为文件类型,访问浏览器的时候就会被下载下来。
同样可以使用file协议,能够成功下载文件

imageIO中的SSRF
imageIO类是jdk中自带的一个类,主要用于操作一些图片文件。比如读写、压缩图片。
ssrf 类:
1 2 3 4 5 6 7 8 9 10 11
| public class ssrf { public static BufferedImage read(URL url) throws IOException { if (url == null) { System.out.println("输入内容为空"); } InputStream istream = url.openStream(); ImageInputStream stream = ImageIO.createImageInputStream(istream); BufferedImage bi = ImageIO.read(stream); return bi; } }
|
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @GetMapping("/httpclient") public void downloadImage(@RequestParam("url") String imageUrl, HttpServletResponse response) throws IOException { response.setContentType("image/png"); try (ByteArrayOutputStream os = new ByteArrayOutputStream(); ServletOutputStream outputStream = response.getOutputStream()) { URL url = new URL(imageUrl); BufferedImage image = ImageIO.read(url); if (image != null) { ImageIO.write(image, "png", os); try (InputStream input = new ByteArrayInputStream(os.toByteArray())) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = input.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } } else { response.sendError(HttpServletResponse.SC_NOT_FOUND, "无法从指定URL获取图片"); } } catch (IOException e) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "图片处理出错"); } }
|
可以通过https协议访问图片

可以通过file协议访问图片,无法访问其他文件。在程序中设置了抛出异常,访问文件时会有error提示

若未设置抛出异常,能够看到文件是否存在
如:


如果文件不存在的话会显示找不到指定文件,而存在但文件不为图片文件的话则显示image==null。
HttpClient下的SSRF
1 2 3 4
| CloseableHttpClient httpClient = HttpClients.createDefault(); HttpClient client = new HttpClient(); HttpGet getRequest = new HttpGet(url); HttpResponse response = httpClient.execute(getRequest);
|
OkHttp 下的SSRF
1 2 3 4 5
| Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build();
Response response = client.newCall(request).execute();
|
白盒规则
上面的漏洞代码可总结为4种情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Request.Get(url).execute()
URL u; int length; byte[] bytes = new byte[1024]; u = new URL(url); inputStream = u.openStream();
String url = "http://127.0.0.1"; CloseableHttpClient client = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse; try { httpResponse = client.execute(httpGet);
URLConnection urlConnection = url.openConnection(); HttpURLConnection urlConnection = url.openConnection();
|
漏洞修复
那么,根据利用的方式,修复方法就比较简单。
- 限制协议为HTTP、HTTPS协议。
- 禁止URL传入内网IP或者设置URL白名单。
- 不用限制302重定向。
漏洞修复代码如下:
需要添加guava库(目的是获取一级域名),在pom.xml中添加
1 2 3 4 5
| <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency>
|
代码的验证逻辑:
- 验证协议是否为http或者https
- 验证url是否在白名单内
函数调用:
1 2 3 4
| String[] urlwhitelist = {"joychou.org", "joychou.me"}; if (!securitySSRFUrlCheck(url, urlwhitelist)) { return; }
|
函数验证代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public static Boolean securitySSRFUrlCheck(String url, String[] urlwhitelist) { try { URL u = new URL(url); if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) { return false; } String host = u.getHost().toLowerCase(); String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString();
for (String whiteurl: urlwhitelist){ if (rootDomain.equals(whiteurl)) { return true; } } return false;
} catch (Exception e) { return false; } }
|
DNS Rebinding Bypass SSRF
详细请看:https://xz.aliyun.com/news/8300
由于我们是用它来绕过SSRF漏洞,所以简单理解就是:当某一个SSRF检测是通过DNS解析后的ip地址来判断是否为安全地址的话,我们可以通过DNS rebinding来进行绕过。
传统SSRF过滤流程
- 获取到输入的URL,从该URL中提取host
- 对该host进行DNS解析,获取到解析的IP
- 检测该IP是否是合法的,比如是否是私有IP等
- 如果IP检测为合法的,则进入curl的阶段发包
从DNS解析的角度来看,这个过程一共有两次解析,第一次是对该host进行DNS解析,第二次是进入curl的阶段发包,这两次请求之间存在一个时间差,如果我们能够修改DNS地址在第一次请求的时候为合法地址,第二次请求时为恶意地址,就可以绕过这个检测了。
DNS Rebinding如何利用?
攻击者注册一个域名(如attacker.com),并在攻击者控制下将其代理给DNS服务器。 服务器配置为很短响应时间的TTL记录,防止响应被缓存。 当受害者浏览到恶意域时,攻击者的DNS服务器首先用托管恶意客户端代码的服务器的IP地址作出响应。 例如,他们可以将受害者的浏览器指向包含旨在在受害者计算机上执行的恶意JavaScript或Flash脚本的网站。
恶意客户端代码会对原始域名(例如attacker.com)进行额外访问。 这些都是由同源政策所允许的。 但是,当受害者的浏览器运行该脚本时,它会为该域创建一个新的DNS请求,并且攻击者会使用新的IP地址进行回复。 例如,他们可以使用内部IP地址或互联网上某个目标的IP地址进行回复。
TTL是一条域名解析记录在DNS服务器中的存留时间。把这个值设置的非常小可以防止DNS解析结果被缓存,进而使得每次获取DNS解析结果是不同的。
简单理一下这个过程:
- 攻击者配置了一台DNS服务器用于解析某域名
- 每次请求后返回的解析结果不一样,分别是一个合法地址,一个是恶意地址
- 当服务器在第一次请求的时候返回合法地址,第二次请求时返回的是恶意地址。就可以绕过限制进行利用
参考
SSRF in Java-先知社区 (aliyun.com)
Java 审计之SSRF篇(续) - nice_0e3 - 博客园 (cnblogs.com)
Java 审计之SSRF篇 - nice_0e3 - 博客园 (cnblogs.com)
DNS Rebinding Bypass SSRF-先知社区 (aliyun.com)