文件上传
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语句中一般带有select,Driver,可以通过全局查找来快速定位
如果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);
在mybatis的order 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, "&", "&");
origin = StringUtils.replace(origin, "<", "<");
origin = StringUtils.replace(origin, ">", ">");
origin = StringUtils.replace(origin, "\"", """);
origin = StringUtils.replace(origin, "'", "'");
origin = StringUtils.replace(origin, "/", "/");
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-Credentials
为 true
。这是一个跨域策略中的误配置问题,可能影响部分应用。
@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.org
和 test.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()