SSRF在服务端代码中属于常见的漏洞,关于SSRF漏洞的介绍及攻击方式在网络上都有详细的资料,这两篇就写的非常详细了:
SSRF安全指北 - 博客 - 腾讯安全应急响应中心
SSRF Tips | xl7dev
目前互联网企业中的项目,主要以Java项目为主,我们知道在Java语言下,SSRF漏洞的利用场景一般比较有限,很少能通过该漏洞进行文件读取等操作,一般来说能进行漏洞利用的场景如下:
因此本文只针对内网探测类的SSRF攻击类型来进行防御修复讨论,由于在不同的业务场景下,制定一套相对通用的SSRF修复方案时是存在一定困难的,一般会产生两种不同的场景:
我们根据不同的业务场景,设定了3种修复方案,其中前两种分别对应以上两个业务场景,第三种作为最终的解决方案,但实施起来比较复杂,目前无法实现,仅作为探索。
修复方案代码仅基于Java编写
/*
1. ssrf可增加白名单的修复方案,检查域名非法字符,白名单限制二级域名
*/
public boolean ssrfFilterSecondaryDomain(String externalUrl) throws MalformedURLException {
URL url = new URL(externalUrl);
String domain = url.getHost();
if (!domainFilter(domain)){
return false;
}
String secondaryDomain = getSecondaryDomain(domain);
List domainWhiteList = new ArrayList<>();
domainWhiteList.add("yuanfudao.com");
domainWhiteList.add("fbcontent.cn");
for (Iterator it = domainWhiteList.iterator(); it.hasNext();) {
String value = it.next();
if(secondaryDomain.equals(value)){
return true;
}
}
return false;
}
private static String getSecondaryDomain(String domain) {
String[] domains = domain.split("\\.");
return domains[domains.length - 2] + "." + domains[domains.length - 1];
}
private static boolean domainFilter(String domain) {
String regex = "[^a-zA-Z0-9\\.-]";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(domain);
return !matcher.find();
}
这种场景修复起来较为简单,把服务器需要访问的资源限制在一定范围之内即可。
对url中host部分先进行域名有效字符的校验,防止使用反斜线等特殊字符进行绕过;再基于白名单对二级域名进行校验。
例如上述代码提供的ssrfFilter方法就可以对url进行一次过滤,符合规则返回true,可以使服务器该url进行访问,反之则不能访问。
其中的domainWhiteList部分可以由开发人员根据业务需求指定。
/*
2. 无法添加白名单的修复方案
检测非法字符CRLF
->通过域名解析ip
->判断ip是否为内网地址
->固定url中的host部分为该ip,同时设置headers中的host为域名,防止某些站点检测host不为域名时不能访问
->请求不跟随302跳转
可解决大部分ssrf问题
*/
public URLConnection httpSecureAccess(String externalUrl) throws IOException {
URL exurl = new URL(externalUrl);
String host = exurl.getHost();
String protocol = exurl.getProtocol();
// 判断是否存在非法特殊字符
String decode = URLDecoder.decode(externalUrl, "utf-8");
if (decode.contains("\r\n")){
//System.out.println("Illegal characters CRLF error");
throw new IllegalArgumentException();
}
// 判断ip是否为内网,目前只判断ipv4
InetAddress[] addresses = InetAddress.getAllByName(host);
List ips = new ArrayList<>();
for (int i = 0; i < addresses.length; i++) {
if(addresses[i] instanceof Inet4Address) {
if (ipIsInner(addresses[i].getHostAddress())) {
//System.out.println("Illegal address error");
throw new IllegalArgumentException();
} else {
ips.add(addresses[i].getHostAddress());
}
}
}
// 若该域名对应多个ip则随机取值,同时重新拼接URL,设置host为域名,同时不跟随302跳转
Collections.shuffle(ips);
String ip = ips.get(0);
String newurl = externalUrl.replaceFirst(host, ip);
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
URL url = new URL(newurl);
// http和https请求分别处理,其他协议直接拒绝
if (protocol.equals("https")) {
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
connection.setRequestProperty("Host", host);
connection.setHostnameVerifier(new TrustAnyHostnameVerifier());
return connection;
}
else if (protocol.equals("http")) {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
connection.setRequestProperty("Host", host);
return connection;
}
else {
//System.out.println("Illegal protocol error");
throw new IllegalArgumentException();
}
}
static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session){
return true;
}
}
static List ipFilterRegexList = new ArrayList<>();
static {
Set ipFilter = new HashSet();
ipFilter.add("^10\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])"
+ "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])" + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("^172\\.(1[6789]|2[0-9]|3[01])\\" + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
+ ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("^192\\.168\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
+ ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
ipFilter.add("127.0.0.1");
ipFilter.add("0.0.0.0");
ipFilter.add("localhost");
for (String reg : ipFilter) {
ipFilterRegexList.add(Pattern.compile(reg));
}
}
public static boolean ipIsInner(String ip) {
for (Pattern reg : ipFilterRegexList) {
Matcher matcher = reg.matcher(ip);
if (matcher.find()){
return true;
}
}
return false;
}
由于业务需求导致无法添加域名的白名单时,可从多个角度对url进行限制,尽量防止攻击的发生。
观察公司业务发现http请求的类大都使用的是HttpURLConnection,因此上述代码也以该类作为演示,其他类具体使用到的方法可能略有不同,但修复思路是相同的。
这种情况下的修复思路如下:
使用ip替换域名这一操作会导致访问资源出现一些问题,比如某些站点会对headers中的host进行校验,如果host不是域名则访问会被拒绝;以及https证书校验的过程也会受阻。
以上问题的解决方案:
上述代码提供的httpSecureAccess方法,输入为url,如果该url符合规范则返回一个URLConnection类的connection连接,开发人员可以直接使用这个connection进行资源读取等操作;如果url不符合规范则直接抛出异常。
这部分修复方案可以保证绝大多数情况下对SSRF进行防御,但由于对http请求进行了较多的修改,可能会产生其他问题,因此还要经过实践验证。
/*
3. 更安全的修复方案,通过代理服务器访问外网资源,且该服务器对内网隔离。可更彻底的解决ssrf问题,但修复成本较高。
*/
public HttpURLConnection httpProxyAccess(String externalUrl) throws IOException {
String proxyHost = "10.x.x.x";
int proxyPort = 8080;
String proxyUser = "";
String proxyPass = "";
InetSocketAddress isa = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, isa);
Authenticator.setDefault(new CustomAuthenticator(proxyUser, proxyPass));
URL url = new URL(externalUrl);
HttpURLConnection connection = (HttpURLConnection)url.openConnection(proxy);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
return connection;
}
static class CustomAuthenticator extends Authenticator {
private String user = "";
private String password = "";
public CustomAuthenticator(String user, String password) {
this.user = user;
this.password = password;
}
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, password.toCharArray());
}
}
最安全的修复方案是增加服务器集群,专门用来对需要访问资源的服务进行代理。
可分为内网集群和外网集群, 例如需要访问外网资源的服务走外网的代理集群,且该集群对办公内网隔离。
这样一来所有访问资源的请求都由专用的服务器集群发出,避免了内网探测的风险;一旦有利用SSRF进行任意文件读取的场景,也不会对生产环境的服务器本身造成危害。
但是该方案操作难度较大,我们现在的情况还不具备设置服务器集群作为代理的条件,因此仅作为一种思路进行讨论。