JAVA代码审计

文件上传

java中,文件上传函数为:
MultipartFile
代码审计思路,全局查找MultiparFile函数,然后查看上下文有没有对上传文件进行限制,有没有办法绕过限制,从而做到任意文件上传(暂不考虑代码特性)
containns能防止text/html;charset=UTF-8绕过

for (String blackMimeType : mimeTypeBlackList) {
            if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {
                logger.error("[-] Mime type error: " + mimeType);
                deleteFile(filePath);
                return "Upload failed. Illeagl picture.";
            }
        }

文件上传无非就是5个拦截点:

1、上传成功是否重命名文件

2、MIME是否检测

3、是否读取文件内容判断文件是否正常

4、是否限制上传后缀

5、语言版本是否存在特殊截断字符

SQL注入

java中,SQL语句中一般带有selectDriver,可以通过全局查找来快速定位
如果SQL语句中,参数由用户自行决定,则存在SQL注入,例如:

String sql = "select * from users where username = '" + username + "'"; 
logger.info(sql);           //在日志中记录语句
ResultSet rs = statement.executeQuery(sql);         //执行语句

但是可以使用?作为占位符,prepareStatement函数防止SQL注入,并使用setString插入传参,例如:

String sql = "select * from users where username = ?";      //使用?作为占位符
PreparedStatement st = con.prepareStatement(sql);  //prepareStatement防止SQL注入
st.setString(1, username);   //setString安全的插入参数
logger.info(st.toString());  // sql after prepare statement
ResultSet rs = st.executeQuery();

但是prepareStatement函数也存在一个错误用法,不使用?作为占位符,而是直接将参数传入SQL语句中,还是会存在SQL注入:

String sql = "select * from users where username = '" + username + "'";
PreparedStatement st = con.prepareStatement(sql);
logger.info(st.toString());
ResultSet rs = st.executeQuery();

还有一种SQL写法是MyBatis(动态构建SQL语句)

@Resource
    private UserMapper userMapper;          //注入实例,用于执行MyBatis的数据库操作。

@Mapper
public interface UserMapper {

    /**
     * If using simple sql, we can use annotation. Such as @Select @Update.
     * If using ${username}, application will send a error.
     */
    @Select("select * from users where username = #{username}")
    User findByUserName(@Param("username") String username);

    @Select("select * from users where username = '${username}'")
    List<User> findByUserNameVuln01(@Param("username") String username);

    List<User> findByUserNameVuln02(String username);
    List<User> findByUserNameVuln03(@Param("order") String order);

    User findById(Integer id);

    User OrderByUsername();

}
XML内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.joychou.mapper.UserMapper">

    <resultMap type="org.joychou.dao.User" id="User">
        <id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
        <id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
        <id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
    </resultMap>

    <!--<select id="findByUserName" resultMap="User">-->
	    <!--select * from users where username = #{username}-->
    <!--</select>-->

    <select id="findByUserNameVuln02" parameterType="String" resultMap="User">
        select * from users where username like '%${_parameter}%'
    </select>

    <select id="findByUserNameVuln03" parameterType="String" resultMap="User">
        select * from users
        <if test="order != null">
            order by ${order} asc
        </if>
    </select>

    <select id="findById" resultMap="User">
        select * from users where id = #{id}
    </select>


    <select id="OrderByUsername" resultMap="User">
        select * from users order by id asc limit 1
    </select>
</mapper>

使用${username}插入参数,是会存在SQL注入的

调用函数部分:
@GetMapping("/mybatis/vuln01")
    public List<User> mybatisVuln01(@RequestParam("username") String username) {
        return userMapper.findByUserNameVuln01(username);
    }

UserMapper的一部分:
@Select("select * from users where username = '${username}'")
    List<User> findByUserNameVuln01(@Param("username") String username);

使用%${_parameter}%插入参数也会存在SQL注入

XML文件内容:
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
        select * from users where username like '%${_parameter}%'
    </select>


调用函数部分:
@GetMapping("/mybatis/vuln02")
    public List<User> mybatisVuln02(@RequestParam("username") String username) {
        return userMapper.findByUserNameVuln02(username);
    }

Mapper部分:
List<User> findByUserNameVuln02(String username);

mybatisorder by语句中不能使用#,所以${order}会导致SQL注入,需要对输入的内容进行一次过滤

函数调用部分:
@GetMapping("/mybatis/sec03")
    public User mybatisSec03() {
        return userMapper.OrderByUsername();
    }

XML部分:
<select id="findByUserNameVuln03" parameterType="String" resultMap="User">
        select * from users
        <if test="order != null">
            order by ${order} asc
        </if>
    </select>

Mapper部分:
List<User> findByUserNameVuln03(@Param("order") String order);

只要使用${}作为插入参数,并且参数值由用户自己决定,该部分就会存在SQL注入,解决办法是替换为#{}

还需要看是否存在相关过滤,比如参数输入的黑名单匹配,预处理等操作。

例如使用SecurityUtil.sqlFilter(sort)函数(该函数是自己写的)过滤

/**
     * 过滤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;
    }
需要确保过滤逻辑是足够严格的,才能防止绕过

XSS跨站脚本攻击

XSS注入的本质其实就是后端输出的内容没有进行过滤,导致前端读取到这种内容从而加载成html或JavaScript,导致执行XSS恶意指令(虽然,XSS利用真的不多,加上目前的前端框架升级,新框架基本上不存在XSS了)

在前后端一体化的架构中,如果后端代码直接返回用户的输入数据而未进行适当的处理,XSS是很容易触发的。

因为在前后端一体化的架构中,后端负责生成和返回页面内容(HTML、JSON、文本等)直接给前端。如果后端直接通过return返回数据,通常意味着它会直接传递给前端,而不会再经过前端的额外处理

Spring MVC框架中,使用@ResponseBody注解的控制器方法会直接将方法返回值作为HTTP响应的内容。这代表返回的数据会直接发给前端,而不是渲染模板或经过进一步处理。

这就导致这一段代码,通过reflect接收参数xss,然后直接返回给前端,照成反射型XSS注入

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

也就是在spring MVC框架中,快速查找

request.getParameter
request.getQueryString
request.getHeader
request.getCookies
HttpServletResponse.getWriter()
response.setHeader()
return(与@RestController或@ResponseBody一起使用)

查看上下文是否存在过滤可以判断是否存在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;
    }

而使用@ResponseBody控制器+return还想要解决XSS的问题,只需要进行严格过滤或转义即可

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

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

其中safe和encode以及reflect方法(函数)使用static(静态)的原因,是因为代码没有调用复杂内容,涉及任何实例变量或实例方法的调用,这样写可以节省内存,优化性能。
而store和show方法(函数)不使用static(静态)的原因是因为调用了实例:HttpServletResponse@CookieValue,从而不适用static

@Controller会渲染视图,使用视图解析器渲染 HTML 或其他页面。(加上@ResponseBody控制器后返回的就是json或XML了,也就是@RestController=@Controller+@ResponseBody)而@RestController默认返回对象或数据,并且将数据直接写入 HTTP 响应体(通常返回 JSON 或 XML 格式的响应)。

CORS配置错误

简单讲解一下CORS配置错误,如果一个网站存在CORS配置错误漏洞,那么攻击者就可以进行CSRF攻击

受害者登录a.com,并保留了登录凭证(Cookie)。
攻击者引诱受害者访问了b.com。
b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
a.com以受害者的名义执行了act=xx。
攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作(例如转钱,购买商品等操作)。

看代码:

这个是一个典型的CORS配置错误漏洞,通过获取请求包中的origin字段内容作为Access-Control-Allow-Origin响应头,这会导致攻击者可以自行编辑origin内容,从而绕过CORS限制,且response.setHeader(“Access-Control-Allow-Credentials”, “true”);允许Cookie传递,这就导致会出现CSRF漏洞

@RequestMapping("/vuln/origin")
    public static String vuls1(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("origin");
        response.setHeader("Access-Control-Allow-Origin", origin); // 设置Origin值为Header中获取到的
        response.setHeader("Access-Control-Allow-Credentials", "true");  // cookie
        return info;
    }

第二个代码:

虽然这种方式放宽了跨域限制,但如果前端同时设置 withCredentials(表示发送请求时携带Cookie等凭据),会导致请求失败。因为 Access-Control-Allow-Origin* 时,不允许同时设置 Access-Control-Allow-Credentialstrue。这是一个跨域策略中的误配置问题,可能影响部分应用。

@RequestMapping("/vuln/setHeader")
    public static String vuls2(HttpServletResponse response) {
        // 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常
        response.setHeader("Access-Control-Allow-Origin", "*");
        return info;
    }

第三个代码:

这也是一种放宽CORS策略的方式,允许任何域发起跨域请求,可能带来安全风险(例如 CSRF 攻击)。虽然这种方式配置简单,但过于宽松的跨域策略可能允许恶意网站读取敏感数据。

@CrossOrigin("*")
    @RequestMapping("/vuln/crossOrigin")
    public static String vuls3() {
        return info;
    }

对于第一个代码的修复:

通过 @CrossOrigin 注解限制允许的跨域请求域名为 joychou.orgtest.joychou.me,限制了可以访问这个接口的域。

@CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"})
    @RequestMapping("/sec/crossOrigin")
    public static String secCrossOrigin() {
        return info;
    }

也可以直接使用spring提供的防止跨站攻击的对象

这个方法对应 Spring 的 Web MVC 配置器(webMvcConfigurer),这里处理了 CSRF token 的获取,并且可能与 MVC 层的配置相关。

@RequestMapping("/sec/webMvcConfigurer")
    public CsrfToken getCsrfToken_01(CsrfToken token) {
        return token;
    }

这个方法说明了 Spring Security 处理跨域资源共享 (CORS) 设置的场景,并指出在 Spring Security 处理 CORS 的场景下,不支持自定义 checkOrigin

@RequestMapping("/sec/httpCors")
    public CsrfToken getCsrfToken_02(CsrfToken token) {
        return token;
    }

这个方法表示使用自定义的 Filter 来处理 CORS,在这种情况下,允许自定义 checkOrigin

@RequestMapping("/sec/originFilter")
    public CsrfToken getCsrfToken_03(CsrfToken token) {
        return token;
    }

这个方法通过 CorsFilter 处理 CORS 请求,但不支持自定义 checkOrigin

@RequestMapping("/sec/corsFilter")
    public CsrfToken getCsrfToken_04(CsrfToken token) {
        return token;
    }

这个方法是用来检查请求的来源是否安全。如果请求的 Origin 不在白名单中,就认定为不安全。这个方法会根据请求头中的 Origin 字段,检查其是否在安全白名单中,并相应设置 Access-Control-Allow-Origin 等 CORS 响应头。

@RequestMapping("/sec/checkOrigin")
    public String seccode(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("Origin");

        // 如果origin不为空并且origin不在白名单内,认定为不安全。
        // 如果origin为空,表示是同域过来的请求或者浏览器直接发起的请求。
        if (origin != null && SecurityUtil.checkURL(origin) == null) {
            return "Origin is not safe.";
        }
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        return LoginUtils.getUserInfo2JsonStr(request);
    }

命令注入(Command Injection)

命令注入一般出现在功能需要调用系统命令的地方,例如,查看当前目录下有哪些文件,又或者请求某个IP或者域名查看是否能够访问。

如下代码,就是查询命令,由用户输入的参数filepath拼接查询,但是没有进行任何过滤,这会导致攻击者可以通过;等方式拼接命令,从而实现任意命令执行,在可通网的情况下,还能够通过nc反弹shell至VPS中

 @GetMapping("/codeinject")
    public String codeInject(String filepath) throws IOException {

        String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath};
        ProcessBuilder builder = new ProcessBuilder(cmdList);   //执行命令
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

又比如这个代码:

从用户发起的数据包中拼接了host,而这个host是可以被用户改动的,而代码并没有进行任何相关过滤,从而出现了命令注入

@GetMapping("/codeinject/host")
    public String codeInjectHost(HttpServletRequest request) throws IOException {

        String host = request.getHeader("host");        //从请求头中gethost
        logger.info(host);
        String[] cmdList = new String[]{"sh", "-c", "curl " + host};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

修复这个问题的最好办法就是添加过滤:

下面代码通过判断用户输入的内容中是否属于正则匹配的[a-zA-Z0-9_/\\.-]+$],如果不是,则设置为空,然后判断filterFilePath是否为空,为空则不执行命令,这会使得命令执行仅仅限制在这个命令之中,而无法注入其他的命令。(实际的代码审计中应该考虑的是绕过部分是否足够严格,常规来说很难出现无过滤情况)

private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");

public static String cmdFilter(String input) {
        if (!FILTER_PATTERN.matcher(input).matches()) {
            return null;
        }

        return input;
    }


@GetMapping("/codeinject/sec")
    public String codeInjectSec(String filepath) throws IOException {
        String filterFilePath = SecurityUtil.cmdFilter(filepath);   //过滤SecurityUtil.cmdFilter
        if (null == filterFilePath) {
            return "Bad boy. I got u.";
        }
        String[] cmdList = new String[]{"sh", "-c", "ls -la " + filterFilePath};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

快速定位:

Runtime.exec()、ProcessBuilde、getParameter()、getHeader()