Apache OpenMeetings 9.1.0 leftovers
OpenMeetings 9.1.0 closed the whiteboard fuid read. LOGIN calendar save still overwrites another meeting and closes the public room. A stale /install wizard still plants a second ADMIN. Official KMS docker still publishes unauth JSON-RPC on 8888. ADMIN_BACKUP import still skips Path.of.
- Name
- Apache OpenMeetings 9.1.0 leftovers
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- Apache OpenMeetings (ASF)
- Affected
- Apache OpenMeetings 9.1.0 (9773c59e1af327bf988018e661f456ae9997c29f). Java 21. Kurento 7.3.0+.
- Published
- 16 Oct 2026
- Updated
- 16 Oct 2026
- Tags
- 0-day, openmeetings, apache, idor, privilege-leftover, remote, kms
The conference room was already closed. The calendar said I owned it.
I am @abraxas_null. This is a map of leftovers on Apache OpenMeetings 9.1.0 (9773c59e1af327bf988018e661f456ae9997c29f, 2026-07-14, latest GA). Java 21. Kurento 7.3.0+. Twenty source hunts. Loopback witness labs. No shells.
The 9.1.0 announcement is a security release. It closed CVE-2026-49488: a moderator whiteboard download that would follow fuid off the files tree. I pinned that tag on purpose. A security release is where leftovers hide.
Packs are AGPLv3 under github.com/abraxas. Vendor is security@openmeetings.apache.org. Unpaid ASF VDP. No public issue on apache/openmeetings.
Why this product
OpenMeetings is a video-conference stack with a Wicket UI, CXF SOAP+REST under /openmeetings/services, OpenJPA, optional Hazelcast, and Kurento for WebRTC. LMS plugins (Moodle, Sakai, Joomla) log in as a dedicated SOAP user and mint room hashes. Default self-register is on. That combination is a lot of object ids and a lot of "the SOAP account is supposed to stay below ADMIN."
I wanted the unpublished remainder after the 2026 wave, not a replay of 49488.
The pin and the skip list
Tag 9.1.0. Peel 9773c59e. The git tag object 273dda0 is not a commit; HEAD is the "9.1.0 Release" commit. Official image apache/openmeetings:min-9.1.0 is linux/amd64 and does not ship a JRE. Labs wrap Temurin 21.
Already public on this pin, so I did not re-file them:
- CVE-2026-49488 whiteboard
fuidOS read (OmFileHelper.validateLocation) - CVE-2026-34020 REST login GET password (now POST form)
- CVE-2026-33266 hardcoded remember-me key (per-instance AES-GCM)
- CVE-2026-33005 personal/group folder-by-id for non-owners
- CVE-2025-7962 SMTP CR/LF (angus-mail 2.0.5)
- CVE-2024-54676 OpenJPA docs whitelist
- CVE-2023-29246 UI
Path.of/ backup XML NUL - CVE-2023-28936 invitation LIKE then
equals - CVE-2023-29032 Wicket ADMIN copy from invite
- CVE-2023-28326 invitation SOAPLogin.moderator
- CVE-2021-27576 NetTest 3s delay + 50-client cap on
type=upload
Also closed: ProcessHelper list-form argv, CXF 4.2.2 StAX XXE, Wicket CSRF listener, SameSite Lax, DEPLOYMENT, Tomcat manager excluded, H2 console off, AJP commented, install wizard GET after isInstalled() for new requests.
Privilege model, because SOAP is not ADMIN
There is no servlet auth filter on CXF. Every mutating/list call goes through BaseWebService.performCall, which loads Sessiondata by SID (UUID v4) and tests User.Right. POST /user/login mints a SID for any LOGIN user. SOAP methods still fail later unless that user also has Right.SOAP.
Default self-register grants LOGIN + DASHBOARD + ROOM. SOAP is the LMS integration account. Installer admin also gets SOAP. SOAP listing every user and minting room hashes is the documented CMS SSO surface. SOAP planting ADMIN via UserDTO.rights is leftover. Calendar save as LOGIN vs owner is leftover. Those are different bugs.
SID compare still uses JPQL LIKE then equals (same leftover pattern as CVE-2023-28936). Equality is the control.
Surfaces that returned nothing useful
Invitation wildcard redeem stays closed by equals. Generate-URL cannot target a registered USER. FileWebService personal folder-by-id for non-owners stays closed (room parentId < 0 metadata leftover is Medium). Conversion uses UUID + canonical path. Clustering OpenJPA lists are off in default startup.sh. Chat .html(cm.message) still stores markup; CSP 'strict-dynamic' is the remaining control, so I left XSS at Medium.
What I labbed
Critical on stock 9.1.0: none. High leftovers that proved:
| Leftover | Class | Reach | Pack |
|---|---|---|---|
| Calendar save | IDOR | Remote LOGIN | openmeetings-calendar-save-idor |
| Private chat export | IDOR | Remote LOGIN | openmeetings-chat-user-idor |
SOAP UserDTO.rights |
Privilege leftover | Remote SOAP | openmeetings-soap-user-rights |
| NetTest ping POST | DoS leftover | Remote unauth | openmeetings-nettest-ping-dos |
Whiteboard fileId |
IDOR | Remote presenter | openmeetings-wb-fileid-idor |
| Type.USER invite recordings | IDOR | Remote hash | openmeetings-invite-recording-acl |
| SOAP/REST OTP skip | 2FA skip | Remote password | openmeetings-soap-otp-skip |
| Remember-me cookie | ATO | Remote cookie | openmeetings-remember-me-password |
Stale /install |
Privilege leftover | Remote first-boot | openmeetings-stale-install-admin |
| Backup PATH import | File write leftover | Remote ADMIN_BACKUP | openmeetings-backup-config-path |
| Clustering TCP 5636 | CWE-502 | Remote docs-on | openmeetings-cluster-openjpa |
| Published KMS 8888 | Unauth KMS | Remote unauth | openmeetings-kms-unauth |
Labs talk to loopback. Replay is ./run.sh. Witness last lines are in each pack README.
Calendar save IDOR
CalendarWebService.save allows a LOGIN caller when the request DTO owner is null or self. Delete on the same class checks the persisted owner. Save does not.
CalendarMapper.get loads appointmentDao.get(dto.getId()) then copies title, members, and rMapper.get(dto.getRoom()). RoomMapper.get writes closed, ispublic, name, capacity on the managed room entity. dao.update persists both.
I created two LOGIN users. Victim saved appointment 1 on public conference room 2, title ORIGINAL-CALENDAR-TITLE, closed=0. Attacker POSTed the same id, omitted owner, set title OPENMEETINGS-CALENDAR-SAVE-WITNESS, room.closed=true. Save returned 200. Owner 2→3. SOAP GET /room/2 showed closed=true, isPublic=false. Victim title/range went empty. Attacker range returned the witness row.
What an attacker can do
| Method | Sink | Privilege |
|---|---|---|
POST /services/calendar/?sid= owner omitted |
CalendarWebService.save |
Integrity (lab-proven): overwrite title, steal owner. |
Same save room.closed=true |
RoomMapper.get |
Integrity + availability (lab-proven): close and hide the public conference room. |
| Attacker title/range GET | post-write DTO | Recon (lab-proven): witness title. |
Closed: DELETE /calendar/{id} still owner or SOAP/ADMIN. Range reads stay owner-scoped. Recording MP4 is a different ACL. Unauthenticated save still needs a SID.
CWE-639. 7.6 High CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L. Pack: abraxas/openmeetings-calendar-save-idor (loopback client). Last line: SUCCESS OPENMEETINGS-CALENDAR-SAVE-WITNESS.
Suggested fix: load the persisted appointment. Require a.isOwner(caller) unless SOAP/ADMIN. Treat omitted owner as keep-persisted, not caller-becomes-owner. Do not dirty a room the caller cannot moderate.
Stale install wizard
InstallWizardPage constructor redirects new requests when Application.isInstalled(). That lock looks complete until you keep the Wicket page from before the flip.
ImportInitvalues.checkInstalled logs "no need to install" and does not return. Finish still calls loadAll(cfg, true). loadInitUserAndGroup inserts another ADMIN+SOAP user.
I started an uninstalled 9.1.0 tarball. Session 3E8EE0F6 walked Welcome, DB, user step, filled staleadm, jumped to Finish, and stayed live. A second web wizard completed first install as admin1. Fresh GET /install redirected to /signin. The stale session still POSTed ?3-4.0-wizard-form-buttons-finish. staleadm then SOAP-logged in. User list count=2. Rights: ADMIN, SOAP, LOGIN, DASHBOARD, ROOM.
admin.sh -i is a second JVM and does not load Tomcat CryptProvider, so REST login returns error.unknown after CLI install. The leftover is the stale web page, so the lab used a second web wizard as the operator.
What an attacker can do
| Method | Sink | Privilege |
|---|---|---|
| Stale Finish POST | checkInstalled continues; loadInitUserAndGroup |
Integrity (lab-proven): second ADMIN+SOAP account. |
Same loadAll |
persistence / SMTP from stale form | Integrity (inferred): rewrite persistence.xml. |
Closed: new /install after install redirects. Tomcat manager excluded. H2 console off.
CWE-367. 8.1 High CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H. Pack: abraxas/openmeetings-stale-install-admin. Last line: SUCCESS OPENMEETINGS-STALE-INSTALL-WITNESS.
Suggested fix: checkInstalled must return. One-time install token. Drop the Wicket page when isInstalled() flips. Do not insert a second ADMIN if one exists.
Unauth Kurento on the docs port
OpenMeetings Java defaults kurento.ws.url to loopback. Official InstallMediaServer.md publishes Kurento with docker -p 8888:8888. That JSON-RPC has no auth.
The full apache/openmeetings:9.1.0 image starts kurento-media-server on 0.0.0.0:8888, then can fail catalina as nobody without JAVA_HOME. I labbed the leftover the docs teach: kurento/kurento-media-server:7.3.0 with 127.0.0.1:18164:8888.
HTTP POST is WebSocket-only (426). ws://127.0.0.1:18164/kurento upgraded 101. Unauth ping returned pong. Unauth connect minted a sessionId. Unauth invoke manager_ServerManager.getInfo returned version 7.3.0 and the module list.
What an attacker can do
| Method | Sink | Privilege |
|---|---|---|
Unauth ping / connect / getInfo |
Kurento JSON-RPC | Recon + session mint (lab-proven). |
Unauth PlayerEndpoint (same socket) |
KMS media elements | Recon / integrity (inferred): HTTP/file fetch as the KMS UID. Not exercised. |
Closed: OM Java default loopback. OM /openmeetings still wants a SID for rooms.
CWE-306. 8.6 High CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L. Pack: abraxas/openmeetings-kms-unauth. Last line: SUCCESS OPENMEETINGS-KMS-UNAUTH-WITNESS.
Suggested fix: do not publish 8888. Bind KMS to a private network. Put a token on the JSON-RPC. Change the install doc example.
Backup import skips Path.of
CVE-2023-29246 closed NUL injection on Admin Config Type.PATH via Path.of. BackupImport.importConfigs still writes those rows with no Path.of. Java 21 ProcessBuilder also rejects NUL, so this is the leftover of the 29246 sink, not a new argv RCE.
I restored a 3822-byte backup zip via admin.sh -r (same BackupImport.performImport as Wicket ADMIN_BACKUP). configuration.path.imagemagick and path.ffmpeg stored as PATH /tmp/OPENMEETINGS-BACKUP-CONFIG-PATH-WITNESS. Wicket /admin/backup/upload with a REST SID returned access denied; it wants the websocket Client SID.
Control: Path.of accepts this Unix witness path. Path.of with a NUL suffix throws InvalidPathException. No converter was executed.
What an attacker can do
| Method | Sink | Privilege |
|---|---|---|
| ADMIN_BACKUP restore | importConfigs cfgDao.update |
Integrity (lab-proven): raw Type.PATH converter path. |
| Later conversion jobs | ffmpeg/imagemagick argv from those rows | Integrity / availability (inferred). NUL argv RCE closed on this JDK. |
Closed: Admin Config UI still Path.of. ROOM cannot restore.
CWE-20. 6.5 High CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H. Pack: abraxas/openmeetings-backup-config-path. Last line: SUCCESS OPENMEETINGS-BACKUP-CONFIG-PATH-WITNESS.
Suggested fix: run the ConfigForm Path.of validator inside importConfigs before cfgDao.update.
Private chat export IDOR
ChatForm.process takes activeTab. Prefix chatTab-u loads any user id. ChatToolbar user branches are u -> true. Save/delete buttons are admin-visible only. AjaxDownloadBehavior is on the toolbar itself.
Attacker omuserB signed in through Wicket, set activeTab=chatTab-u2, downloaded the victim DM CSV: "Vic User","10/16/26, 10:39 PM","OPENMEETINGS-CHAT-IDOR-WITNESS".
Integrity (inferred): same activeTab on delete wipes that inbox. Export was the oracle.
Closed: global/room export still needs admin or room moderator.
CWE-639. 7.1 High. Pack: abraxas/openmeetings-chat-user-idor. Last line: SUCCESS OPENMEETINGS-CHAT-IDOR-WITNESS.
SOAP add copies ADMIN
SOAP is the LMS account. UserMapper loads by id and setRights(dto.getRights()). UserWebService.add adds ROOM/LOGIN/DASHBOARD and never strips ADMIN.
soaplab is SOAP, not ADMIN. Its DELETE /user/3 returned error.notallowed. As soaplab, POST /user/ created plantedadm with ADMIN and firstname OPENMEETINGS-SOAP-RIGHTS-WITNESS. plantedadm then DELETE /user/3 returned 200 Deleted. Extra IOC: id-merge overwrite of user 4.
Closed: ROOM SID cannot add. Frontend register has no rights array.
CWE-269. 8.8 High. Pack: abraxas/openmeetings-soap-user-rights. Last line: SUCCESS OPENMEETINGS-SOAP-RIGHTS-WITNESS.
NetTest ping leftover of CVE-2021-27576
RateLimitRequestFilter returns early for type=ping. POST still reads the entity to EOF. GET /signin mints a JSESSIONID. No login.
Two parallel 8 MiB ping POSTs both returned 200, bytes=8388608. Control type=upload: first 200, immediate second 429. Immediate ping after that 200. No cookie: ping 403.
CWE-770. 7.5 High. Pack: abraxas/openmeetings-nettest-ping-dos. Last line: SUCCESS OPENMEETINGS-NETTEST-PING-WITNESS.
Whiteboard fileId plant
CREATE_OBJ stores client JSON including any fileId. RoomResourceReference serves those bytes if isAtWb sees that id. CVE-2026-49488 closed OS path read. This leftover is a numeric id.
First joiner of public conference room 2 is presenter. Attacker planted victim fileId 7, then GET /room/file/7 returned HTTP 200 image/png 57 bytes with the witness.
Closed: validateLocation OS path. DOWNLOAD still moderator-only.
CWE-639. 6.5 High. Pack: abraxas/openmeetings-wb-fileid-idor. Last line: SUCCESS OPENMEETINGS-WB-FILEID-WITNESS.
Type.USER invitation is the recording owner
CVE-2023-29032 stopped copying Wicket ADMIN. checkHashes still setUser(invitation.getInvitee(), hrights). Recording MP4 ACL keys off ownerId == getUserId().
Unauth GET /recordings/mp4/2 404. CONTACT hash 404. Unauth GET /hash?invitation= then the same MP4: 200 video/mp4, witness in body.
Closed: LIKE wildcard redeem. CONTACT guests.
CWE-639. 7.5 High. Pack: abraxas/openmeetings-invite-recording-acl. Last line: SUCCESS OPENMEETINGS-INVITE-RECORDING-WITNESS.
SOAP/REST skip TOTP
When otp.enabled is true, TOTP runs only in Wicket SignInDialog. UserWebService.login is password then SID.
Wicket password-only showed the OTP challenge and did not grant a dashboard. POST /user/login with user/pass only returned Type.SUCCESS and SID b58de0be-.... That SID saved a calendar row whose description is the witness. Default OTP is off; the lab turns it on.
CWE-304. 8.1 High. Pack: abraxas/openmeetings-soap-otp-skip. Last line: SUCCESS OPENMEETINGS-SOAP-OTP-SKIP-WITNESS.
Remember-me still is the password
CVE-2026-33266 closed the hardcoded key. Cookie LoggedIn still holds username+password. Opt-in checkbox credentials:rememberMe.
AJAX login with remember-me set minted Set-Cookie: LoggedIn (HttpOnly, SameSite=Lax). A new client with only that cookie hit the authenticated main page. Control without the cookie stayed on sign-in. Manual AES-GCM decrypt hit InvalidTag in this run; replay still worked.
CWE-522. 8.1 High. Pack: abraxas/openmeetings-remember-me-password. Last line: SUCCESS OPENMEETINGS-REMEMBER-ME-WITNESS.
Clustering TCP 5636
CVE-2024-54676 docs-whitelisted OpenJPA lists. Default startup.sh does not set them. Clustering.xml still documents RemoteCommitProvider=tcp.
Stock: port 5636 closed. Docs-on: 5636 listened. A serialized HashMap containing the witness produced ClassCastException HashMap vs RemoteCommitEvent. That is readObject on the socket. No gadget chain.
CWE-502. 8.1 High because an admin must uncomment clustering. Pack: abraxas/openmeetings-cluster-openjpa. Last line: SUCCESS OPENMEETINGS-CLUSTER-OPENJPA-WITNESS.
Medium/Low stay off this post. Chat stored XSS is still behind CSP 'strict-dynamic'. File room-metadata leftover of CVE-2026-33005 is metadata only.
Contact
Abraxas Labs. abraxaslabs.tech. github.com/abraxas. @abraxas_null. abraxas.null@proton.me.
Please request the CVE for these reports and credit Abraxas Labs.