目录
1、背景
原来做的项目上线了,但是将地址作为参数传递到后台请求的时候被我们公司安全部门拦截了,问题是可能将本服务器作为跳板,获取公司内部的数据,可能会出现以下问题:
1.攻击者操控服务器发出自定义http(或其他支持的协议)请求,探测生产网中的服务。
2.将攻击者直接代理进内网中,绕过网络访问控制,直接访问内网进行漫游、甚至获取服务器敏感信息和凭证。为攻击者进一步渗透测试打开了大门。
2、解决问题的思路
1、首先验证请求的路径是否为http或https这种形式,如果是file:///, dict://, ftp://, gopher:// 等请求禁止。
2、满足条件1后,采用黑白名单的方式进行判断,将需要的IP地址放到数据库中或者配置文件中作为白名单,每次请求访问方法前都拦截,判断请求参数IP是否属于白名单,如果不属于白名单,判断是否为公司的内网或是黑名单若是则终止本次请求,否则认为是普通网站请求
3、解决问题方法:
1、采用AOP对请求进行拦截。
首先创建注解类,代码如下所示:
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Diffintercept {
}
2、在请求方法上加入该注解
3、将拦截作为一个单独的模块封装成类
这里涉及了想表达的地方:首先本次采用的是将白名单作为配置文件进行管理的(也可以采用数据库),白名单应该是一个list列表而不是key-value,我在ResourceBundle类中找到了getStringArray()方法,以为能够将下图1中存储方式转为String[] 数组或者List集合,在下面代码注释掉的那一句,但是抛出了转型失败错误(String不能转为String数组)我以为不支持“,”做识别符,又换成“;”、“-”但是不行,真正的解释如下:
PropertyResourceBundle在读文件时使用Properties.load(stream),它存储的是String。所以它永远返回的都是String而不是String数组。简言之,PropertyResourceBundle不支持getStringArray这个方法。
遇到上述错误我就使用了ResourceBundle的containsKey(ip)方法,这招有效果,配置文件需要改成图二的形式,但是有一点,实在是太别扭了,所以我还是想变成比较友好的形式,这时候我想到了--虽然获取不了字符串,但是我可以获取value后自己利用split函数解析啊,这个我熟啊哈哈,于是最后的代码就是下面这一段,同时最终的配置文件也就是图一。
import java.net.InetAddress;
import java.util.ArrayList;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.ResourceBundle;
public class SSRFChecker {
ResourceBundle resourceBundle = ResourceBundle.getBundle("diff");
public boolean checkURL(String urlString) {
try {
//String[] whiteLists = resourceBundle.getStringArray("whiteList");
String whiteList = resourceBundle.getString("whiteList");
String[] whiteLists = whiteList.split(",");
URL url = new URL(urlString);
String protocol = url.getProtocol();
if (!whitelistProtocolArray.contains(protocol)) {
System.out.println("Unsupported protocol~ Error:" + url);
return false;
}
String host = url.getHost();
InetAddress address = InetAddress.getByName(host);
String ip = address.getHostAddress();
boolean contains = Arrays.asList(whiteLists).contains(ip);
//resourceBundle.containsKey(ip)
if(contains) {
System.out.println("Good Ip~ Success:" + ip + ", " + host + "," + url);
return true;
}
for (String blacklistIp : blacklistIpArray) {
if (equalIpSubnet(ip, blacklistIp)) {
System.out.println("Bad IP~ Error:" + ip + ", " + host + "," + url);
return false;
}
}
System.out.println("Good Ip~ Success:" + ip + ", " + host + "," + url);
return true;
} catch (Exception e) {
//e.printStackTrace();
System.out.println("Bad URL~ Error:" + urlString);
return false;
}
}
//支持以下内网IP形式:
//10.0.0.0/8
//172.16.0.0/12
//192.168.0.0/16
private boolean equalIpSubnet(String ip, String blacklistIp) {
long ipAddr = IpUtil.ip2long(ip);
int type = Integer.parseInt(blacklistIp.replaceAll(".*/", ""));
int mask = 0xFFFFFFFF << (32 - type);
String cidrIp = blacklistIp.replaceAll("/.*", "");
long cidrIpAddr = IpUtil.ip2long(cidrIp);
return (ipAddr & mask) == (cidrIpAddr & mask);
}
}
在上述代码中,有一个识别是否为内网的代码通过计算是否在ip范围来实现检测,还是比较实用的。
4、使用SpringAOP做拦截器拦截非法请求
这部分就比较常用了。
@Component
public class Interceptor implements HandlerInterceptor {
SSRFChecker ssrfChecker = new SSRFChecker();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// // 获取方法上的注解
Diffintercept requiredStatistics = handlerMethod.getMethod().getAnnotation(Diffintercept.class);
//如果不为空,说明有方法请求
if(requiredStatistics!=null){
//获取参数
String requestUrl = request.getParameter("requestUrl");
String requestUrl2 = request.getParameter("requestUrl2");
//解码
requestUrl = URLDecoder.decode(requestUrl, "UTF-8");
requestUrl2 = URLDecoder.decode(requestUrl2, "UTF-8");
//判断是否为白名单账号,防止请求非法数据。
boolean b1 = ssrfChecker.checkURL(requestUrl);
boolean b2 = ssrfChecker.checkURL(requestUrl2);
//如果不相等,说明存在非法请求,返回false
if(!b1||!b2) {
//重置response
response.reset();
//设置编码格式
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw = response.getWriter();
pw.write("非法访问地址,请先注册后在使用diff工具");
pw.flush();
pw.close();
return false;
}
//在这里可以做一个一个参数是否规范的验证,也可以在前端做校验。明后天完成这部分内容
return true;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//System.out.println("afterCompletion");
}
}