java-sec-code-master搭建&&审计
java-sec-code-master搭建
源码地址:
https://github.com/JoyChou93/java-sec-code
搭建环境:
IDEA
apache-maven-3.9.1
apache-tomcat-9.0.105
JDK 1.8
MySQL 5.7.26
数据库:小皮面板自带的MySQL,导入sql文件
在项目结构里面,然后填上你的jdk1.8的bin目录

直接创建数据库
我使用的是phpstudy

然后把
create_db.sql文件的连接数据库的好密码改为自己的(记得到导入数据库配置文件
create_db.sql)然后配置一下tomcat
去官网下载tomcat,解压出来就是这样的

然后在设置里的这个地方添加这个

maven使用IDEA自带就行
(也可以自己下载)
和之前一样,使用 IDEA 打开项目,执行
mvn clean install,然后运行主类即可启动。运行application.java(成功后会显示这个)

运行成功之后,下次直接使用这个就可以启动啦

直接登录就好了

java-sec-code-master审计
使用IDEA搭建了java-sec-code-master
(使用时,要打开phpstudy运行mysql)
命令注入
src\main\java\org\joychou\controller\CommandInject.java
存在三个方法
1
2
3
4@GetMapping("/codeinject") @GetMapping("/codeinject/host") @GetMapping("/codeinject/sec") //codeinject/sec是安全方法(其它两个存在漏洞)/codeInject
1
2
3
4
5
6
7
8
9
10@GetMapping("/codeinject") public String codeInject(String filepath) throws IOException { //String[] cmdList = new String[]{"cmd", "-c", "dir -la " + filepath}; String[] cmdList = new String[]{"cmd.exe", "/c", "dir " + filepath}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); return WebUtils.convertStreamToString(process.getInputStream()); }filepath并未做任何的过滤,直接拼接
在windows中使用&符号拼接cmd命令
由于是web服务应用,使用&符号拼接url命令
payload
localhost:9000/codeinject?filepath=.%26ipconfig
(页面返回主机配置)
/codeinject/host
1
2
3
4
5
6
7
8
9
10
11
12@GetMapping("/codeinject/host") public String codeInjectHost(HttpServletRequest request) throws IOException { String host = request.getHeader("host"); logger.info(host); //String[] cmdList = new String[]{"cmd", "-c", "curl " + host}; String[] cmdList = new String[]{"cmd.exe", "/c", "dir " + host}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); return WebUtils.convertStreamToString(process.getInputStream()); }http的请求头host(无过滤)
payload
抓包之后,在数据包中修改
host:localhost&ipconfig(返回包显示主机信息)
codeinject/sec(不行)
原因:(存在过滤)
1
2
3
4
5
6
7
8
9
10
11
12
13
14@GetMapping("/codeinject/sec") public String codeInjectSec(String filepath) throws IOException { String filterFilePath = SecurityUtil.cmdFilter(filepath); //过滤 if (null == filterFilePath) { return "Bad boy. I got u."; } //String[] cmdList = new String[]{"cmd", "-c", "dir -la " + filterFilePath}; String[] cmdList = new String[]{"cmd.exe", "/c", "dir " + filterFilePath}; ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); return WebUtils.convertStreamToString(process.getInputStream()); } }追踪
cmdFilter1
2
3
4
5
6public static string cmdFilter(string input){ if(!FILTER PATTERN.matcher(input).matches()){ return null; } return input; }查看FILTER PATTERN(常量)
1
private static final Pattern FILTER PATTERN = Pattern.compile("^[a-zA-Z0-9 /\\.-]+$");FILTER PATTERN过滤(大小写,特殊字符)
出现命令注入时,有特殊字符返回null
所以不行
RCE
\src\main\java\org\joychou\controller\Rce.java
(已经展示了命令执行过程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
@RestController
@RequestMapping("/rce")
public class Rce {
@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
Runtime run = Runtime.getRuntime();//直接执行,无过滤
StringBuilder sb = new StringBuilder();
try {
Process p = run.exec(cmd);
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;
while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
if (p.waitFor() != 0) {
if (p.exitValue() == 1)
return "Command exec failed!!";
}
inBr.close();
in.close();
} catch (Exception e) {
return e.toString();
}
return sb.toString();
}Runtime.getRuntime().exec(),ProcessBuilder、通过yaml加载恶意Java对象进行命令执行,通过groovyShell进行命令执行
执行之后执行结果推送回前端显示
SQL注入
\src\main\java\org\joychou\controller\SQLI.java
定义的方法:
1
2
3
4
5
6
7
8
9
10
jdbc_sqli_vul(漏洞方法)
jdbc_sqli_sec(安全方法)
jdbc_ps_vuln
mybatisVuln01(漏洞方法)
mybatisVuln02(漏洞方法)
mybatisVuln03(漏洞方法)
mybatisSec01(安全方法)
mybatisSec02(安全方法)
mybatisSec03(安全方法)
mybatisOrderBySec04(安全方法)jdbc_sqli_vul
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41/** * <p>Sql injection jbdc vuln code.</p><br> * * <a href="http://localhost:8080/sqli/jdbc/vuln?username=joychou">http://localhost:8080/sqli/jdbc/vuln?username=joychou</a> */ @RequestMapping("/jdbc/vuln") public String jdbc_sqli_vul(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); if (!con.isClosed()) System.out.println("Connect to database successfully."); // sqli vuln code Statement statement = con.createStatement(); String sql = "select * from users where username = '" + username + "'"; //直接拼接語句存在sql漏洞 logger.info(sql); ResultSet rs = statement.executeQuery(sql); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close(); } catch (ClassNotFoundException e) { logger.error("Sorry, can't find the Driver!"); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }直接拼接語句存在sql漏洞(对于参数username进行带入数据库查询)
payload
username=1’ or ‘1’=’1
url编码:username=1%27%20or%20%271%27=%271最终的payload:sqli/jdbc/vuln?username=1%27%20or%20%271%27=%271
jdbc_sqli_sec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/** * <p>Sql injection jbdc security code by using {@link PreparedStatement}.</p><br> * * <a href="http://localhost:8080/sqli/jdbc/sec?username=joychou">http://localhost:8080/sqli/jdbc/sec?username=joychou</a> */ @RequestMapping("/jdbc/sec") public String jdbc_sqli_sec(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); if (!con.isClosed()) System.out.println("Connect to database successfully."); // fix code String sql = "select * from users where username = ?"; PreparedStatement st = con.prepareStatement(sql);//使用了PreparedStatement接口来访问数据库 st.setString(1, username); logger.info(st.toString()); // sql after prepare statement ResultSet rs = st.executeQuery(); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close(); } catch (ClassNotFoundException e) { logger.error("Sorry, can't find the Driver!"); e.printStackTrace(); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }1
PreparedStatement st = con.prepareStatement(sql);//使用了PreparedStatement接口来访问数据库PreparedStatement接口是 Java 中用于执行预编译 SQL 语句的关键接口,它继承自Statement接口,主要用于执行参数化 SQL 语句。使用PreparedStatement可以有效防止 SQL 注入攻击,并且在执行多次相同结构的 SQL 语句时能提高性能。防护sql:
使用预编译:使用?(占位符)传递参数,数据库自动处理参数转义
解释:对于php使用?传入的未限制是数字还是字符;但是java中使用?传入限制只能是数字,如果不是数字,那么会加以过滤(转义/加入特殊字符)
eg:
用户输入 字符串拼接结果 预编译处理结果 1WHERE id = '1'WHERE id = 11 OR 1=1WHERE id = '1 OR 1=1'(逻辑被篡改)WHERE id = '1 OR 1=1'(纯字符串)1'; DROP TABLE usersWHERE id = '1'; DROP TABLE users(表被删除)WHERE id = '1\'; DROP TABLE users'(安全)存在对于特殊字符的转义
禁用动态 SQL 拼接:避免在代码中手动拼接 SQL,尤其是包含用户输入的部分。若必须使用动态 SQL,需严格验证输入。
所以java中:
不只order by,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等”,(在有些SQL语句中还是会必然使用到拼接的方式)所以不可注入
jdbc_ps_vuln
也是使用预处理(同上)
mybatisVuln01 02 03
\src\main\java\org\joychou\controller\SQLI.java
1
2
3
4
5
6
7
8
9
10
11
12@GetMapping("/mybatis/sec01") public User mybatisSec01(@RequestParam("username") String username) { return userMapper.findByUserName(username); } @GetMapping("/mybatis/sec02") public User mybatisSec02(@RequestParam("id") Integer id) { return userMapper.findById(id); } @GetMapping("/mybatis/sec03") public User mybatisSec03() { return userMapper.OrderByUsername(); }查看map类
\src\main\resources\mapper\UserMapper.xml
02
1
2
3
4
5<select id="findByUserNameVuln02" parameterType="String" resultMap="User"> select * from users where username like '%${_parameter}%' </select> #使用like进行拼接sql语句${_parameter}是 MyBatis 的字符串替换,会直接将参数内容拼接到 SQL 中解决:
1
2
3
4
5<select id="findByUserNameSafe" parameterType="String" resultMap="User"> select * from users where username like concat('%',#{_parameter},'%') </select> #使用concat('%',#{_parameter},'%')的方式进行查询03
1
2
3
4
5
6
7
8<select id="findByUserNameVuln03" parameterType="String" resultMap="User"> select * from users <if test="order != null"> order by ${order} asc </if> </select> #${}使用的是拼接的方式导致的漏洞产生解决:
如果是order by后的占位,则最好根据情况通过
if-elseif-else来分情况实现。mybatisOrderBySec041
01
\target\classes\org\joychou\mapper\UserMapper.class
1
2
3
4
5
6
7
8public interface UserMapper { @Select({"select * from users where username = #{username}"}) User findByUserName(@Param("username") String var1); @Select({"select * from users where username = '${username}'"}) List<User> findByUserNameVuln01(@Param("username") String var1); #${}使用的是拼接的方式导致的漏洞产生解决:
正确的使用方法应该是使用
#号来进行占位
mybatisOrderBySec04
1
2
3
4@GetMapping("/mybatis/orderby/sec04") public List<User> mybatisOrderBySec04(@RequestParam("sort") String sort) { return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort));//存在过滤函数 }追溯sqlFilter
\src\main\java\org\joychou\security\SecurityUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13/** * 过滤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; }FILTER_PATTERN:
1
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");过滤mybatis中order by不能用#的情况。
严格限制用户输入只能包含a-zA-Z0-9_-.字符@param sql sql
@return 安全sql,否则返回null
SSTI服务器模板注入
\src\main\java\org\joychou\controller\SSTI.java
主要是使用Velocity组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
@RequestMapping("/ssti")
public class SSTI {
/**
* SSTI of Java velocity. The latest Velocity version still has this problem.
* Fix method: Avoid to use Velocity.evaluate method.
* <p>
* http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22)
* Open a calculator in MacOS.
*
* @param template exp
*/
@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组件模板·存在漏洞
参考文章:https://xz.aliyun.com/news/14795
路径遍历
\src\main\java\org\joychou\controller\PathTraversal.java
1
2
3
4
5
6
7/** * http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd */ @GetMapping("/path_traversal/vul") public String getImage(String filepath) throws IOException { return getImgBase64(filepath); }输入路径之后,
filepath参数直接传递给getImgBase64方法,未做任何验证,只存在一个basa64编码(getImgBase64)追溯getImgBase64
1
2
3
4
5
6
7
8
9
10
11
12
13private 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."; } }payload:http://localhost:9000/path_traversal/vul?filepath=d:/test.txt
读取D盘下的测试文件(发现可以成功读取内容)
解决:(过滤)
- 使用白名单过滤、
- 使用
Path和normalize()方法(根目录怕拼接)- 使用
Path.resolve()而非字符串拼接。 - 使用
normalize()方法规范化路径,防止../绕过。
- 使用
文件上传
\src\main\java\org\joychou\controller\FileUpload.java
无过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@PostMapping("/upload") public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { if (file.isEmpty()) { // 赋值给uploadStatus.html里的动态参数message redirectAttributes.addFlashAttribute("message", "Please select a file to upload"); return "redirect:/file/status"; } try { // Get the file and save it somewhere byte[] bytes = file.getBytes(); Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename()); Files.write(path, bytes); redirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'"); } catch (IOException e) { redirectAttributes.addFlashAttribute("message", "upload failed"); logger.error(e.toString()); } return "redirect:/file/status"; }上传文件 未判断文件的类型、扩展名等信息,未对生成文件的文件名进行重置
直接将文件上传到文件保存目录中
payload
直接上传
解决:
- 使用白名单校验扩展名(如
jpg,png,pdf) - 双重验证 MIME 类型(
file.getContentType()) - 对上传图片实际内容的判断,如果图片可以正常读取,就判断其为允许上传文件,否则上传失败。
- 生成安全文件名(设置成随机字符拼接…)(防止路径遍历和注入)
- 如果上传文件名是路径(../是向上跳转,最后会出现路劲遍历的漏洞)
- 攻击者知道服务器上已有重要文件(比如其他用户上传的合法文件
avatar.jpg),可以故意使用相同的文件名上传恶意文件(攻击文件),直接覆盖原文件。后续用户访问执行恶意代码
XSS
\src\main\java\org\joychou\controller\XSS.java
1
2
3
4
5
6
7
8
9
10
11
12/** * Vuln Code. * ReflectXSS * http://localhost:8080/xss/reflect?xss=<script>alert(1)</script> * * @param xss unescape string */ @RequestMapping("/reflect") @ResponseBody public static String reflect(String xss) { return xss; }参数未做过滤,直接显示在前端页面
payload
直接在url中编写xss脚本即可
1
localhost:9000/xss/reflect?xss=<script>alert(1)</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/** * Vul Code. * StoredXSS Step1 * http://localhost:8080/xss/stored/store?xss=<script>alert(1)</script> * * @param xss unescape string */ @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"; } /** * Vul Code. * StoredXSS Step2 * http://localhost:8080/xss/stored/show * * @param xss unescape string */ @RequestMapping("/stored/show") @ResponseBody public String show(@CookieValue("xss") String xss) { return xss; }存储型xss漏洞
store方法将未经过滤的参数直接存储于cookie中
show方法在cookie中将存储的漏洞参数直接显示在页面上
payload
1
写入<script>alert(1)</script>
解决:
- 输入转义(重要)
- 限制长度