漏洞修复-服务端请求伪造(SSRF)

点击上方名片关注我,为你带来更多踩坑案例

02852f6ad59a48c6275c7a4c6281557b.png

- 什么是SSRF -

    SSRF(Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造请求,由服务端发起请求的一个安全漏洞。

    一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统,因为服务器请求天然可以穿越防火墙。漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做正确的过滤和限制。

- 漏洞防护 -

  1. 若所接收的url是一个固定的域名或者域名范围可控,应该创建白名单来校验域名。

  2. 若不采用白名单,则需要注意以下几点:
    2.1 禁用不需要的协议。仅允许http和https请求。可以防止类似于file:///,gopher://,ftp:// 等引起的问题
    2.2 去除url中的特殊字符,解析目标URL,获取其Host
    2.3 若Host为IP形式,校验Host是否属于内网IP,若Host为域名,解析Host指向的IP,判断是否为内网IP,并在请求时将url中的域名改为ip请求(防止DNS重绑定攻击)
    2.4 不跟随30x跳转,如果有跳转,跳转URL需从2.1开始重新检测

- java中的一个防护样例 -

public class SSRFUtils {
    private ApplicationContext context;


    private final String[] hosts = new String[]{"pan.test.com", "yunpan.luhui.com", ".*\\.pdd.lubenwei.storage"};


    // 中文句号
    private final String HAN_COMMA = "。";


    // 英文句号
    private final String EN_COMMA = ".";


    @Autowired
    public UrlUtils(ApplicationContext context) {
        this.context = context;
    }


    /**
     * 是不是 SSRF 安全 url
     *
     * @return
     */
    public boolean isSafeUrlBySSRF(final String url) {
        if (StringUtils.isEmpty(url)) {
            return true;
        }
        if (isTestProfile()) {
            return true;
        }
        if (isBaiUrl(url)) {
            return true;
        }
        if (isDomainUrl(url)) {
            return false;
        }


        // 包含汉字句号的 IP,也可以请求资源:String uri = "http://10。130。140。203:1122/test1.txt";
        if (url.contains(HAN_COMMA)) {
            String replace = url.replace(HAN_COMMA, EN_COMMA);
            if (isDomainUrl(replace)) {
                return false;
            }
        }


        if (isRedirect(url) == true) {
            return false;
        }
        return true;
    }


    /**
     * 判断url是否是白名单地址
     *
     * @param imageUrl
     * @return
     */
    public boolean isBaiUrl(final String imageUrl) {
        if (StringUtils.isEmpty(imageUrl)) {
            return false;
        }


        final Optional<String> hostOpt = parseHost(imageUrl);
        if (!hostOpt.isPresent()) {
            return false;
        }
        final String h = hostOpt.get();
        for (String host : hosts) {
            if (Pattern.matches(host , h)) {
                return true;
            }
        }


        return false;
    }


    /**
     * 判断url是否是内网地址
     *
     * @param url
     * @return 是/否
     */
    public boolean isDomainUrl(final String url) {
        Optional<String> ipOptional = parseIp(url);
        if (!ipOptional.isPresent()) {
            return false;
        }
        byte[] addr = IPAddressUtil.textToNumericFormatV4(ipOptional.get());
        return internalIp(addr);
    }


    private boolean isTestProfile() {
        Optional<String> profile = getActiveProfile();
        if (!profile.isPresent()) {
            return true;
        }


        if ("dev".equals(profile.get()) || "test".equals(profile.get())) {
            return true;
        }
        return false;
    }




    private Optional<String> parseHost(final String urlStr) {
        try {
            URL url = new URL(urlStr);
            return Optional.of(url.getHost());
        } catch (MalformedURLException e) {
            log.warn("url parse host fail,url:{},ex:{}", urlStr, e.getMessage());
        }
        return Optional.empty();
    }


    /**
     * 内网地址有以下三种,排除这三种ip
     * 10.0.0.0/8:10.0.0.0~10.255.255.255
     *   172.16.0.0/12:172.16.0.0~172.31.255.255
     *   192.168.0.0/16:192.168.0.0~192.168.255.255
     *
     * @param addr
     * @return
     */
    public static boolean internalIp(byte[] addr) {
        // 涉及一些内部ip,各位可自行实现
    }


    /**
     * 解析url的ip地址
     * @param urlStr
     * @return ip地址
     */
    private Optional<String> parseIp(String urlStr) {
        Optional<String> hostOptional = parseHost(urlStr);
        if (!hostOptional.isPresent()) {
            return Optional.empty();
        }
        try {
            InetAddress address = InetAddress.getByName(hostOptional.get());
            String ip = address.getHostAddress();
            return Optional.ofNullable(ip);
        } catch (UnknownHostException e) {
            log.warn("url parse ip fail,url:{},ex:{}", urlStr, e.getMessage());
        }
        return Optional.empty();
    }


    private Optional<String> getActiveProfile() {
        String[] profiles = context.getEnvironment().getActiveProfiles();
        if (!ArrayUtils.isEmpty(profiles)) {
            return Optional.of(profiles[0]);
        }
        return Optional.empty();
    }


    /**
     * 判断 url 是否为 30x跳转
     * @param url
     * @return
     */
    public static boolean isRedirect(String url) {
        boolean redirect = false;
        try {
            URL obj = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) obj.openConnection();
            conn.setReadTimeout(5000);
            conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
            conn.addRequestProperty("User-Agent", "Mozilla");
            conn.addRequestProperty("Referer", "luhui.com");


            //you still need to handle redirect manully.
            conn.setInstanceFollowRedirects(false);
            HttpURLConnection.setFollowRedirects(false);


            // normally, 3xx is redirect
            int status = conn.getResponseCode();
            if (status != HttpURLConnection.HTTP_OK) {
                if (status == HttpURLConnection.HTTP_MOVED_TEMP
                        || status == HttpURLConnection.HTTP_MOVED_PERM
                        || status == HttpURLConnection.HTTP_SEE_OTHER) {
                    redirect = true;
                }
            }
            log.info("msg=isRedirect Request URL ... " + url + " Response Code:" + status);
        } catch (Exception e) {
            log.warn("msg=isRedirect get url error uri:{} error:{} ", url, e);
        }
        return redirect;
    }
}

    以上包含了一些运行环境、白名单以及内网的逻辑,来综合判断SSRF,大家可以当作一个参考

- 结束语 -

    如果你的体系里不光只有一个系统,那么这个问题基本是必须要面对的

猜你喜欢

转载自blog.csdn.net/qq_31363843/article/details/129891766