一、概述
上一篇讲过XXE注入,与其密切相关的一个漏洞就是SSRF,这个漏洞形成的原因是大都由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。比如从指定URL链接获取图片、下载等,Weblogic等服务器都存在SSRF的经典漏洞,漏洞原理和渗透思路见:SSRF漏洞原理、挖掘技巧及实战案例全汇总。
二、挖掘过程
一般通过关键字或功能点定位发起HTTP请求的相关代码段,如这里使用URL类的openStream方法进行发包:
@RequestMapping("/download")
@ResponseBody
public void downLoadImg(HttpServletRequestrequest, HttpServletResponse response) throws IOException{
try {
String url =request.getParameter("url");
if (StringUtils.isBlank(url)) {
throw newIllegalArgumentException("url异常");
}
downLoadImg(response, url);
}catch (Exception e) {
throw newIllegalArgumentException("异常");
}
}
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();
针对这个从其他服务器下载图片的功能进行审计,url用户可控,实例u调用openStream方法进行网络发包,构造payload进行任意文件读取:
curl -v'http://localhost:8080/download?url=file:///etc/passwd'
三、挖掘技巧
挖掘SSRF漏洞的关键是找到代码中是否存在发起网络请求的类,若存在此类代码则进行定位跟踪,确定访问的url地址是否由用户可控。
功能层面关注:远程图片加载与下载、图片或文章收藏功能、URL分享、通过URL在线翻译、转码、连接或配置远程服务器地址等功能场景。
某些请求可能在后台发起,并不会以功能点的形式在前端呈现,所以从白盒角度可能构造出黑盒层发现不了的SSRF漏洞。
Java中发起网络请求的类和相应函数如下:
HttpURLConnection. getInputStream
URLConnection. getInputStream
Request.Get. execute
Request.Post. execute
URL.openStream
ImageIO.read
OkHttpClient.newCall.execute
HttpClients. execute
HttpClient.execute
在定位SSRF漏洞时可使用的搜索关键词有:
HttpClient.execute|HttpURLConnection|URL.openStream|HttpServletRequest|getParamet|URL|HttpClient|Request|Okhttp|ImageIO.read
四、漏洞防御
SSRF漏洞的防御一般通过白名单方式,一般进行输入校验: 1、限制协议为HTTP、HTTPS协议;
2、禁止URL传入内网IP或者设置URL白名单
如下列代码:
public static BooleansecuritySSRFUrlCheck(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;
}
}
更推荐的方法是对输入进行严格的格式限制:
if(Pattern.matches("[a-zA-Z0-9\\s\\-]{1,50}",userInput)){
}else{
}
在实际审计过程中需跟踪防御代码,看是否过滤不严可被绕过(IPV6、域名解析等)。
五、实战案例
按照正常思路,通过搜索HttpServletRequest等关键字定位到发送请求的代码exchange方法:
@RequestMapping(method = RequestMethod.POST, value ="/exchange")
@LoggerManage(description = "Exchange to metadatarepository")
public Objectexchange(HttpServletRequest req, @RequestBody(required = false) String json)
{
HttpHeaders headers =(HttpHeaders)req.getAttribute(HeaderFilter.REQUEST_ATTRIBUTE_HEADERS);
if (null == headers ||!headers.containsKey(BuildConstants.HEADER_TARGET_URL))
{
throw new Exception(ExceptionCode.PARAM_CHECK_ERROR,"Parameter target-url not exists.");
}
String targetService =headers.getFirst(BuildConstants.HEADER_SERVICE_NAME);
ServiceConfigserviceConfig = BuildConfiguration.getServiceConfig(targetService);
String methodType =StringUtils.defaultIfBlank(StringUtils.upperCase(headers.getFirst(BuildConstants.HEADER_METHOD_TYPE)),
"GET");
String tenantId =headers.getFirst(TENANT_SPACE_ID);
String targetUrl =StringUtils.replace(headers.getFirst(BuildConstants.HEADER_TARGET_URL),"\\", "/");
ResponseEntity response =bdfService.commonExchange(serviceConfig, targetUrl, headers, json);
除去无用代码,简化代码逻辑,exchange主要的操作是对header进行处理,重点是而后执行的getServiceConfig方法,跟进这个方法看进行了什么操作:
public static ServiceConfiggetServiceConfig(String serviceName)
{
if (StringUtils.isBlank(serviceName))
{
return getMetaService();
}
switch (serviceName.toLowerCase())
{
case BuildConstants.METAREPO_SERVICE_NAME:
return getMetaService();
case BuildConstants.SLA_SERVICE_NAME:
return getSlaService();
default:
{
ServiceConfig serviceConfig =new ServiceConfig();
serviceConfig.setAddress("http://" + serviceName);
serviceConfig.setContext("");
serviceConfig.setName(serviceName);
return serviceConfig;
getServiceConfig方法做的事情是:进行为空和case判断(代码里的流程控制结构无非是顺序、判断和循环),若不满足case中值ServiceConfig 则会执行default:将serviceName设置为http请求地址,这里的serviceName 由用户可控。
回到exchange代码,最后一句调用bdfService.commonExchange进行Response回显,跟进:
public ResponseEntity commonExchange(@NotNull Serviceconfig serviceconfig,@NotNull Stringurl, (@NotNull ServiceConfig serviceConfig, @NotNull String url,
@NotNullHttpHeaders headers, @NotNull String json)
{
StringBuilder sb = newStringBuilder(serviceConfig.getAddress()).append(serviceConfig.getContext()).append(url);
String targetUrl =sb.toString();
HttpMethod method =getHttpMethod(headers.getFirst(BuildConstants.HEADER_METHOD_TYPE));
headers.remove(BuildConstants.HEADER_METHOD_TYPE);
headers.remove("Content-Length");
headers.setConnection("keep-alive");
HttpEntity requestEntity = new HttpEntity<>(json,headers);
return commonExchange.exchange(targetUrl,method, requestEntity, Object.class);
}
是对method和header进行一系列操作,最终调用exchange请求,关注这里的参数targetUrl是可控的,最终针对exchange包构造header头service-name:{IP:PORT}/#进行端口探测。
代码审计本身是一门艺术,在Java基础牢固的情况下耐心跟踪、精心构造会无比享受这个审计的过程,按部就班来,只要某段代码有漏洞就总会发现;反之,急于求成或在基本代码都读不懂的情况下去审计代码则会倍显煎熬,所以基础很重要:先懂开发再做审计。