top of page

Hunt Nextcloud CVE-2026-110447: The App That Ran a Shell

Writer: Inception Security
Inception Security
18 hours ago
14 min read

A small office runs Nextcloud on one server, and staff edit spreadsheets in the browser without asking how. It works because of one optional app, added from the app store long ago, that downloads a copy of a document server and runs it on the same box. Most days nobody thinks about it. When the machine restarts, there is a short stretch before that bundled server is back, and during it the app's plain PHP file tries to start the server again with each request that comes in.


Collabora published an advisory on 14 October 2026 for CVE-2026-110447, an unauthenticated operating system command injection in that app, called richdocumentscode. Three admins reported compromised servers in a GitHub issue, and The Hacker News dates the first logged attack to 10 September. No source we read names an actor or gives an overall count of compromised servers.


Who runs this app


Collabora says only a minority of Nextcloud users run the app this bug lives in. The built-in CODE app is the shortcut for people who wanted Nextcloud Office without a second server. The Hacker News says its store listing calls it "for local testing and non-production use" and "for personal use or for small teams." Collabora calls the app focused on home users, and says the scope of the exploit is narrow. It is also the setup a small office, a one-person IT shop or an MSP's single-client file server ends up with.


The window is your own maintenance


Per the advisory, a request to proxy.php can run an operating system command as the web server user, but only while the built-in CODE server is not running: after a container restart, an upgrade or a crash. The Hacker News adds that proxy.php is a plain PHP file the web server runs straight from the app folder and that it loads none of Nextcloud's code, which fits the advisory's description of the flaw as unauthenticated. Once the bundled server is back up, the window closes. Our reading is that a box that restarts often has more windows.


The fix came before the warning


According to The Hacker News, the fixing commit was written on 20 August and committed on 9 September. 26.4.303 shipped on 11 September, and when we checked the release page, its notes named only the bundled CODE build. The first logged attack, in one admin's access logs, is 10 September. The CVE and advisory arrived on 14 October, 33 days after 26.4.303 shipped. Collabora's explanation is a backlog in GitHub's CVE process, and it says it is applying to become a CNA. Collabora also says Nextcloud urged users to update at the time; the Nextcloud post it links is about a new sandbox feature and does not mention a vulnerability.


Anyone watching a CVE feed heard nothing for a month. The real safeguard was knowing the optional app was installed and either keeping it current or removing it.


Disabling the app won't fix it, and an update won't clean it up


The advisory is explicit that disabling the app does not close the hole. It must be upgraded to 26.4.303 or later, or removed. One admin in the GitHub report had disabled the app while waiting for a fix.


An update also does not remove a shell planted earlier. Collabora's reply in the issue tells users to upgrade and to remove any lingering shx*.php files. A shell can reach whatever the web server user can reach. Per the admin reports and The Hacker News, on one server the attacker used a shell to search for password databases, password manager files and crypto wallets. The first reporter wrote that the requests they reviewed show no command that archived or sent files out, and advises treating anything the web server account can read as possibly exposed. Italy's national CSIRT describes a case where the attacker did take a configuration file, covered below.


Who is exposed


Only installs with the Collabora Online - Built-in CODE Server app (package richdocumentscode) below 26.4.303 are exposed. Collabora's blog says Docker images, RPM and DEB packages and Nextcloud All-in-One images are unaffected, and the advisory says standalone Collabora Online servers are not affected. Those statements cover this bug only. The advisory scopes the bug by app version, not by how Nextcloud itself is installed, and it names a container restart as one of the windows, so treat any Nextcloud container that has the built-in app below 26.4.303 as in scope.


The fixed version needs Nextcloud 30 or later. Nextcloud 29 and older are end of life, and per The Hacker News the app store still serves them an affected version, so the advisory tells those users to upgrade Nextcloud or remove the app. Version 26.4.402 came out on 24 September, but The Hacker News says some admins report it fails to start or open documents on some setups and went back to 26.4.303, the version the advisory names.


What the vendor, NVD and the responders published


Collabora's advisory rates the bug Critical, CWE-78, CVSS v4.0 9.2. NVD's API lists the record as Deferred with no NVD-assigned score. The only CVSS score is the same 9.2 from the CVE record, marked Secondary, and the description says the flaw was "exploited in the wild in September and October 2026." CISA's own SSVC entry on the NVD record, timestamped 14 October, marks exploitation as none, which does not match the CVE text or the admin reports. The CISA KEV catalog, release 2026.10.16, did not list it when we checked on the morning of 18 October, and we found no CISA alert.


Nextcloud's own advisory list has no entry for this issue. BleepingComputer says Nextcloud told it that it had not observed widespread exploitation. Nobody gives an overall count of compromised servers, and we found no scanning data from Shadowserver or GreyNoise as of the morning of 18 October. Censys says on its 16 October advisory page that it detects 19,454 hosts with the app installed, which counts hosts running the app, not victims.


Italy's national CSIRT, in a bulletin dated 16 October and republished by CSIRT Toscana, describes one compromised Nextcloud server in Italy. The attacker installed three web shells, took a configuration file that held database and mail credentials and application secrets, and ran one check command through one of the shells. The CSIRT says the evidence fits an automated campaign that is still active and run by more than one actor, and says no evidence had emerged yet of access to user documents, administrator rights, persistence through other mechanisms or lateral movement. It names no actor and gives no campaign-wide count.


The Hacker News lists indicators from the admin reports: shell files with names beginning shx or shrf, such as shxa35d0.php and shrf4645.php, found in apps/richdocumentscode. The source addresses were 139.28[.]18[.]122 (10 and 11 September) and 89.38[.]98[.]50 (14 September, against a patched server, where no new shell appeared). The payload tried to call back to a public webhook service, and one shell file has SHA-256 4b9fbd0663fbfd6028fc216ed18e0d6ef3ae498a7fdc97083e4b0ce3cec40562. These come from the admins' reports, and an attacker can change any of them. Check for them once, then lean on the behavior hunts below.


What to change this week


  1. Find out whether you run it. In Nextcloud, open Apps and search for Collabora Online - Built-in CODE Server, and look for the richdocumentscode folder on disk, under apps or, on the official Docker image, under custom_apps. It is not the Nextcloud Office app (richdocuments). Check every Nextcloud you host for clients, not only your own.

  2. Update to 26.4.303 or later, or remove the app. On Nextcloud 29 or older, upgrade Nextcloud or remove it. Disabling the app does not close the hole.

  3. Look for shells even after updating. List PHP files in the richdocumentscode folder (apps or custom_apps) that are not part of the app, and look first at any whose names begin with shx or shrf, keep a copy for evidence and delete them. Nextcloud's integrity check flagged the new file for one admin in the issue, so test the occ integrity check on your version as a way to find extra files.

  4. Pull the logs while you have them: web access logs back to 1 September for proxy.php, and failed sudo emails or auth logs for the web server user, which is how one admin found out.

  5. If you find a shell, treat what the web server user can read as exposed. That includes the Nextcloud config file. Italy's CSIRT reports an attacker took a configuration file holding database and mail credentials, so rotate the database password, the instance secret and the mail account password, revoke app passwords, then reset whatever those files unlocked. If password manager databases or wallet files sat where that account could read them, treat them as compromised, and rebuild from known-good. If you find evidence, compare it with the admin reports in the GitHub issue in Sources, and keep your own logs and files out of public threads.

  6. Keep a list of optional apps on every Nextcloud you run and subscribe to each app's release notes, since this fix shipped with no security wording.


Four hunts to run (test first)


These are our own threat hunting queries, built from the artifacts above, and we have not run them on live customer data. Replace every YOUR_* placeholder. A hit means you should look closer, not that you're compromised. Every search is written to start on 1 September 2026, which is before the first logged attack on 10 September, and to run through today, though your retention may not reach that far (Hunt 3 explains the Defender limit). Start earlier if your logs reach back, because every version below 26.4.303 is affected. The table under the hunts, "Interpret before you escalate", says what is benign and what is not.


Expect one normal result: proxy.php legitimately starts the bundled server through bash, so a bash process with coolwsd in its command line is expected. Collabora's reply in the issue says the odd process output after patching looks scary but should be harmless. The Hacker News says a patched server that took an attempt still started a CODE server carrying the attack text in its startup options, so read the full command line before you clear a row.


The attack request lands in the web access logs. The shell runs as the web server user, so process and file telemetry on the Nextcloud box is where a planted shell shows up. A shell planted before patching can stay reachable without another attack request to proxy.php.


Hunt 1 in Splunk: proxy.php requests with encoded traversal or shell characters


Search 1: proxy.php requests carrying request markers


index=YOUR_WEB_INDEX earliest=1788220800 "richdocumentscode"
| eval path=lower(coalesce(uri_path, url, uri, ""))
| eval qs=lower(coalesce(uri_query, query, ""))
| where like(path, "%apps/richdocumentscode/proxy.php%")
| eval raw_uri=path.if(len(qs)>0, "?".qs, "")
| eval client=coalesce(src_ip, clientip, src)
| eval code=coalesce(status, http_status)
| eval method=coalesce(http_method, method)
| eval markers=mvappend(
    if(match(raw_uri, "%2e%2e|\\.\\./"), "encoded_dot_dot", null()),
    if(match(raw_uri, "\\$\\(|%24%28"), "command_substitution", null()),
    if(match(raw_uri, "\\$\\{ifs\\}|%24%7bifs%7d"), "ifs_variable", null()),
    if(match(raw_uri, "base64"), "base64_word", null()),
    if(match(raw_uri, ";|\\||\\x60|%3b|%7c|%60"), "shell_metacharacter", null()),
    if(in(client, "139.28.18.122", "89.38.98.50"), "published_ip_low_fidelity", null()))
| where isnotnull(markers)
| stats count AS requests, min(_time) AS first_seen, max(_time) AS last_seen, values(markers) AS markers, values(code) AS statuses, values(method) AS methods BY client
| sort 0 first_seen
| convert ctime(first_seen) ctime(last_seen)

Search 2: all proxy.php requests by source


index=YOUR_WEB_INDEX earliest=1788220800 "richdocumentscode"
| eval path=lower(coalesce(uri_path, url, uri, ""))
| where like(path, "%apps/richdocumentscode/proxy.php%")
| eval client=coalesce(src_ip, clientip, src)
| stats count AS requests, dc(path) AS distinct_paths, min(_time) AS first_seen, max(_time) AS last_seen BY client
| sort 0 - requests
| convert ctime(first_seen) ctime(last_seen)

Field mapping: the names above follow common Splunk web data (uri_path, uri_query, src_ip, status, http_method), and CIM add-ons vary, so run Search 1 on one day without either where line and confirm the fields exist. Check that your logs keep the full path: some stacks log only the decoded or normalized path, so the encoded markers would not appear and traversal may be collapsed. Behind a reverse proxy the client is often the proxy's address, so read X-Forwarded-For. Do not log or print request bodies.


Normal use hits proxy.php with plain paths, so an outside address with a handful of markers is the signal, and Search 2 shows which sources are unusual. A request that carries markers is a lead whether or not it succeeded, so check the response status and the app version at that time. A match on the two published addresses is weak evidence on its own, because an attacker can change addresses. An empty result means nothing until you know the web logs are exported, proxy.php is logged and the logs reach back to 1 September.


Hunt 2 in Kibana: new PHP files under apps/richdocumentscode


Our earlier piece on a PHP file that appears where only images should be, Hunt Bricksforge: the GIF that turns into PHP, is a different product and a different flaw, but the same file check applies here.


Search 1: PHP files created or changed under the app folder


Run it in Discover against logs-endpoint.events.file-* (Elastic Defend), or your file integrity data stream:


event.category:file and event.type:(creation or change) and file.path:*apps/richdocumentscode/* and file.extension:php and @timestamp >= "2026-09-01T00:00:00Z"

Both searches start at 1 September 2026 UTC. The time picker uses your browser's time zone, so set it wide enough to include that date. Add the columns @timestamp, host.name, file.path, file.name, process.name and user.name.


Search 2: the published file names


event.category:file and file.name:(shx* or shrf*) and @timestamp >= "2026-09-01T00:00:00Z"

Field mapping: both searches use ECS file fields, and the file name fields are case-sensitive keywords. Test first by creating a harmless test.php in that folder on a non-production host and confirming the event arrives, because the endpoint integration may exclude app folders or not run on that host.


An update legitimately rewrites PHP files here, so line up Search 1 by time and by process.name (a Lens table split by minute and process.name works). An update writes many app files in the same minute, through the web server's PHP process (php-fpm, apache2 or httpd) when an admin updates from the Apps page, or through php when occ runs. A single PHP file written by sh, bash, dash or a utility such as base64, or any lone file outside an update burst, is the lead. Look at the writing process and its parents, because the attack command writes the shell itself and the writer is often a shell or a decoding utility, not the PHP process that runs updates. Search 2 is a one-time name check from the admin reports. Without Elastic Defend, Nextcloud's integrity check lists extra files (test the occ command on your version), and a find on the app folder sorted by modification time works. An empty result only means something if the sensor is on the Nextcloud box.


Hunt 3 in Microsoft Defender for Endpoint KQL: the web server user spawning shells


let windowStart = datetime(2026-09-01);
let webUsers = dynamic(["www-data","apache","nginx","www","http"]);
DeviceProcessEvents
| where Timestamp >= windowStart
| where AccountName in~ (webUsers)
| where FileName in~ ("sh","bash","dash","curl","wget")
| extend ExpectedLaunch = ProcessCommandLine contains "coolwsd" or InitiatingProcessCommandLine contains "coolwsd"
| extend Cmds = strcat(ProcessCommandLine, " ", InitiatingProcessCommandLine)
| extend HasMarkers = Cmds contains "$(" or Cmds contains "${IFS}" or Cmds contains "%2e%2e" or Cmds contains "base64"
| summarize FirstSeen=min(Timestamp), LastSeen=max(Timestamp), Events=count(), SampleCommands=make_set(ProcessCommandLine, 5), Initiators=make_set(InitiatingProcessFileName, 5) by DeviceName, AccountName, FileName, ExpectedLaunch, HasMarkers
| order by FirstSeen asc

Field mapping: DeviceProcessEvents needs Defender for Endpoint on Linux onboarded to the Nextcloud box. Advanced hunting keeps 30 days of data, so on 18 October it reaches back only to about 18 September unless you stream the table to Sentinel or another store. The HasMarkers test reads both the process and the initiating command line. The user list holds common default web server account names, so replace it with the account your web server actually runs as and run one day for that user without the FileName filter to confirm the account name and that the sensor is reporting.


Rows with ExpectedLaunch true and no markers are most likely the app starting its bundled server, but read the full command lines before you clear them. Rows with ExpectedLaunch false and curl, wget or a bash running a decoded string are the leads, and so is any row with HasMarkers true. A health check or scheduled job you can identify and explain is benign. Hunt 4 applies the same idea to ProcessRollup2, without the coolwsd rows. If this hunt comes back empty, confirm the Defender for Endpoint sensor on Linux is reporting from that box before you trust the result.


Hunt 4 in Falcon CQL: web server user command lines that name password stores, plus shell spawns


event_platform=Lin event_simpleName=ProcessRollup2
| UserName=/^(www-data|apache|nginx|www|http)$/i
| case {
    CommandLine=/(\.kdbx|wallet\.dat|keystore\.json|bitwarden|1password|\.bitcoin|\.electrum|\.monero|\.ethereum)/i | signal := "web_user_names_password_store_or_wallet" ;
    ImageFileName=/\/(sh|bash|dash|curl|wget)$/i CommandLine!=/coolwsd/i | signal := "web_user_shell_or_fetch" ;
    * | signal := "" ;
  }
| signal != ""
| groupBy([aid, ComputerName, signal], function=[count(as=Events), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen), collect([CommandLine, ParentBaseFileName], limit=10)], limit=20000)
| sort(FirstSeen, order=asc, limit=max)

Field mapping: this is Falcon Linux sensor data in Next-Gen SIEM. UserName can be empty on Linux, so switch that line to the UID of your web user if it is. Start the time range on 1 September 2026 if your Falcon retention reaches that far; FirstSeen and LastSeen come back as epoch milliseconds. The file types and password manager and wallet names in this search come from the admin reports summarized by The Hacker News.


The Falcon sensor does not log every file read, so this finds the search for a password store, the way the admin reports describe it, not the read. To see reads you need auditd watches on the files that matter. The shell branch drops rows with coolwsd in the command line, so review those rows in Hunt 3, which keeps them and flags request markers. Do not run a bare kdbx search across all users, since admins trip it constantly. Test first by running find for a harmless file name as the web user in a lab and confirming the command line shows up. If nothing comes back, first confirm the Falcon sensor is reporting process events from the Nextcloud box.


Interpret before you escalate


Finding

Benign read

Escalate when

proxy.php request from an outside address with encoded traversal or shell characters in the path

None we know of

Always a lead: check the response status and the app version at that time, then look for a new PHP file and a shell spawned by the web server user in the same minutes

A patched server shows an attempt and no new file

No new file appeared, which fits the patch holding for that request

Check for earlier success, since a shell planted before patching can stay reachable without another attack request

New PHP file in apps/richdocumentscode

Part of an app update, written alongside many other app files in the same minute

It is not from an update: treat the web server user as compromised, preserve the file and logs, cut the box off the network if you can, rotate what the account could read, then rebuild from known-good

Web server user spawning bash with coolwsd in the command line

The app starting its bundled server

The same command line also carries request markers

Web server user spawning curl, wget or bash with a long decoded string

A health check or change you can name

Nothing you can name explains it

Web server user command line naming a password store or wallet

None we know of

Any hit: treat everything that account can read as exposed

Empty across all four hunts

Nothing matched in the data you have

Doesn't mean you're clean: confirm web logs, process and file telemetry exist for the Nextcloud box back to 1 September, and update or remove the app regardless


Sources



What This Means for Your Team


Find the app, update or remove it, then look for shells and check logs back to 1 September. Treat an empty hunt as a gap in coverage until you have confirmed the logs exist.


Inception Protection


Inception Protection is our managed detection and response service, and it works with the Microsoft licenses you already have. We watch Defender and sign-in data in your Microsoft environment, dig into anything odd and work through containment with your team. A self-hosted file server sits beside that environment, so keep its patching and logs on your own list.


Inception Foresight


To find out whether your Microsoft environment would catch suspicious activity, try the free Inception Foresight M365 Assessment: https://www.inceptionsecurity.com/m365assessment. You can also follow Inception Security on LinkedIn and @inceptionsec on X.


bg-map-white.png

INCEPTION SECURITY™

A cybersecurity company with in depth knowledge of the threat landscape and security controls.

NAVIGATION

GET IN TOUCH

© 2025 All Rights Reserved by INCEPTION SECURITY™ .

bottom of page