前言
前段时间面试某大厂安全实习生岗位时,被着重问到了 Java审计 SSRF漏洞时,需要重点关注哪些类 ?
我:我审计 SSRF时基本只关注一些关键字函数,没怎么关注过敏感类 😭😭😭
常出现的业务点
SSRF形成的原因大都是由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。比如从指定URL链接获取图片、下载等。
出现SSRF漏洞的主要业务有:
(1) 通过URL地址分享网页内容
(2) 在线服务,外链文章翻译 (有道)
(3) 通过URL地址加载或下载图片,PDF导出
(4) 加载远端配置
常见利用点
http协议利用
url = http://127.0.0.1/secret.txt

file协议利用
url = file:///etc/passwd

总结不全,后续深入探究 ssrf打内网的利用。
重点审计类/函数
java.net.URL
和 java.net.URLConnection
service层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static String URLConnection(String url) { try { URL u = new URL(url); URLConnection urlConnection = u.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); String inputLine; StringBuilder html = new StringBuilder();
while ((inputLine = in.readLine()) != null) { html.append(inputLine); } in.close(); return html.toString(); } catch (Exception e) { return e.getMessage(); } }
|
Controller层:
1 2 3 4
| @RequestMapping(value = "/ssrfTest", method = {RequestMethod.POST, RequestMethod.GET}) public String URLConnectionVuln(@RequestParam String url) { return getUrl.URLConnection(url); }
|
java.net.HttpURLConnection
和Javax.net.ssl.HttpsURLConnection
这两个类都是 java.net.URLConnection
的子类,分别用于处理 http和 https请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL;
public class HttpURLConnectionExample { public static String URLConnection(String url) { try { URL url = new URL(url); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } reader.close(); } catch (IOException e) { e.printStackTrace(); } } }
|
org.apache.http.client.methods.HttpGet
和org.apache.http.client.methods.HttpPost
Apache HttpClient 是一个功能强大的 HTTP 客户端库,用于发送 HTTP 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils;
import java.io.IOException;
public class ApacheHttpClientExample { public static String URLConnection(String url) { HttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(url); try { HttpResponse response = httpClient.execute(httpGet); String responseBody = EntityUtils.toString(response.getEntity()); System.out.println(responseBody); } catch (IOException e) { e.printStackTrace(); } } }
|
okhttp3.Request
和okhttp3.OkHttpClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response;
import java.io.IOException;
public class OkHttpExample { public static String URLConnection(String url) { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { if (response.isSuccessful()) { System.out.println(response.body().string()); } } catch (IOException e) { e.printStackTrace(); } } }
|
除了建立 HTTP(s)协议连接,还可以直接通过 Socket建立连接,所以也要关注 Socket相关类。
java.net.Socket
socket.getInputStream().read()
和 socket.getInputStream().write()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.io.IOException; import java.io.InputStream; import java.net.Socket;
public class SocketExample { public static void main(String[] args) { try { String host = "127.0.0.1"; int port = 8080; Socket socket = new Socket(host, port); InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, bytesRead)); } socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
|
java.nio.channels.SocketChannel
socketChannel.connect()
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
| import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel;
public class SocketChannelExample { public static void main(String[] args) { try { String host = "127.0.0.1"; int port = 8080; SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(host, port)); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead; while ((bytesRead = socketChannel.read(buffer)) != -1) { buffer.flip(); byte[] data = new byte[bytesRead]; buffer.get(data); System.out.println(new String(data)); buffer.clear(); } socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } }
|
总结:
1 2 3 4 5 6 7 8 9
| Http协议建立连接 (1) java.net.URL 和 java.net.URLConnection (2) java.net.HttpURLConnection 和 javax.net.ssl.HttpsURLConnection (3) org.apache.http.client.methods.HttpGet、org.apache.http.client.methods.HttpPost (4) okhttp3.Request 和 okhttp3.OkHttpClient
socket协议建立连接 (1) java.net.Socket (2) java.nio.channels.SocketChannel
|
修复方案
处理正确302跳转(在业务角度看,不能直接禁止302,而是对跳转的地址重新进行检查)
1 2 3 4 5 6 7
| int statusCode = httpURLConnection.getResponseCode(); if (statusCode = 302) { String redirectedUrl = httpURLConnection.getHeaderField("Location"); if(checkURL(redirectURL)){ finalUrl = redirectedUrl; } }
|
限制协议只能为http/https,阻止跨协议 (如file协议,防止任意文件读取)
1 2 3 4
| public static boolean isAllowedProtocol(URL url) { String protocol = url.getProtocol().toLowerCase(); return protocol.equals("http") || protocol.equals("https"); }
|
访问黑名单(禁止访问内网),访问白名单(只允许访问白名单中的地址)
1 2 3 4 5 6 7 8 9
| if (!isInSafeDomain(parsedUrl)) { return false; }
if (isInBlacklist(parsedUrl)) { return false; }
|
设置常见web端口白名单(防止端口扫描,可能业务设定比较大)
防御 SSRF中的 dns重绑定攻击
DNS重绑定攻击与防御
绕过逻辑
先对用户输入的 url进行一次 dns解析,判断其是否合法,若合法则继续进入到下一步逻辑中,伪代码如下所示:
1 2 3 4
| InetAddress addresse = InetAddress.getAllByName(url); if(isValid(address)) { ...... }
|
通过 isValid()校验后,立即将 dns解析结果设置为服务器内网地址 (如 http://127.0.0.1),在服务器请求 url,第二次解析域名。此时已经过了ttl的时间,解析记录缓存IP被删除,所以重新进行 dns解析,解析结果为服务器内网地址,服务器请求内网数据并返回给攻击者:
1 2 3 4 5
| URL u = new URL(url); URLConnection urlConnection = u.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
|
解析记录缓存维持时间:
1 2 3 4 5 6 7
| 1. java中DNS请求成功的话默认缓存30s(字段为networkaddress.cache.ttl,默认情况下没有设置),失败的默认缓存10s。(缓存时间在 /Library/Java/JavaVirtualMachines/jdk /Contents/Home/jre/lib/security/java.security 中配置)
2. 在php中则默认没有缓存。
3. Linux默认不会进行DNS缓存,mac和windows会缓存(所以复现的时候不要在mac、windows上尝试)
4. 有些公共DNS服务器,比如114.114.114.114还是会把记录进行缓存,但是8.8.8.8是严格按照DNS协议去管理缓存的,如果设置TTL为0,则不会进行缓存。
|
在传统的ssrf修复方案中,由于java会存在默认的dns缓存,所以一般认为java不存在DNS rebinding问题。但是试想这么一个场景,如果刚刚好到了DNS缓存时间,此时更新DNS缓存,那些已经过了SSRF Check而又没有正式发起业务请求的request,是否使用的是新的DNS解析结果。其实理论上只要在发起第一次请求后等到30秒之前的时候再请求即可,但为了保证效果,可以在28s左右,开始以一个较短的时间间隔去发送请求,以达到时间竞争的效果。 — 《SSRF安全指北》
修复方案 (final)
由于 DNS重绑定攻击的存在,不得不设置一个更加全面的修复方案:
- 去除url中的特殊字符,限制协议为 http/https
- 判断是否属于内网ip (黑名单)
- 如果是域名的话,将url中的域名改为ip - 防止dns rebinding,直接访问 ip不用二次进行dns解析
- 请求的url为3中返回的url
- 不跟随30x跳转(跟随跳转需要从1开始重新检测)