-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"### The failure branch returns the user's role\n\nThe pre-patch Perl in `Index.pm`, quoted by watchTowr Labs from the vulnerable firmware, reads:\n\n```perl\nif ($password_change_status == $Login::status_codes{ERROR_PASSWD_CHANGE}->{val}) {\n $params->{json_authenticate} = {\n success => 'true',\n message => $Login::status_codes{ERROR_PASSWD_CHANGE}->{msg}.\": \". $message,\n status => $Login::status_codes{ERROR_PASSWD_CHANGE}->{val},\n };\n $self->audit(\"failed_password_change\", $params->{user});\n return $userrole;\n}\n```\n\nThree things are on the record in this block. The branch runs on password-change failure; that is what the condition compares to. The response body the branch constructs names its own failure twice, once in `message` and once in `status`, and then declares `success => 'true'`. The branch returns `$userrole`.\n\n`$userrole` is the return type the success branch returns. The dispatcher upstream reads a return value and routes on whether it names a role authorized to run module handlers. The failure branch was wired to narrate the failure in JSON and to return the authority of the success branch. Two outcomes, one sentinel. This is the [sentinel-conflates-failure-modes](https://nefariousplan.com/patterns/sentinel-conflates-failure-modes) shape in Perl: a return value too narrow to carry its two distinct meanings, read by a caller that treats \"success\" and \"failure-with-error-body\" as the same instruction. The nuclei template `projectdiscovery/nuclei-templates/http/cves/2026/CVE-2026-116606.yaml` matches `'\"success\":\"true\"'` in the response body for exactly this reason: the vulnerable appliance narrates its own bypass in every response it returns, and the detection template is reading the narration.\n\n### The patch comment is the admission\n\nThe 9.4.0.030 fix replaces the return statement:\n\n```perl\n$self->audit(\"failed_password_change\", $params->{user});\n# Send the error and stop here. Returning a userrole\n# would let response() run module handlers.\nreturn $self->_bailout_with_json_login_response(\n $env, $params, {\n success => 'true',\n message => $Login::status_codes{ERROR_PASSWD_CHANGE}->{msg}\n . \": \" . $message,\n status => $Login::status_codes{ERROR_PASSWD_CHANGE}->{val},\n });\n```\n\nThe two lines of Perl comment above the return are the vendor's own plain-English statement of CVE-2026-116606. \"Returning a userrole would let `response()` run module handlers.\" That sentence, in the fix-commit author's own words, is the complete mechanism of the bypass. The pre-patch code was a `return $userrole` that let `response()` run module handlers. That is what it did. That is why it was a bug. The patch replaces the statement whose behaviour the comment narrates with a function named `_bailout_with_json_login_response`. The function name is a rephrasing of the comment.\n\nThe JSON response body still carries `success => 'true'` on failure after the patch. The post-patch response is identical to the pre-patch response below that `success` field. The only thing that changed between vulnerable and fixed is which return statement runs. Everything else on the line is decoration.\n\nThis is also the [fail-open intercept](https://nefariousplan.com/patterns/fail-open-intercept) shape, in Perl. The pattern (prior exhibits include [Tomcat's `EncryptInterceptor`](https://nefariousplan.com/posts/tomcat-encryptinterceptor-fails-open) and [Admidio's SAML signature gate](https://nefariousplan.com/posts/admidio-smc-require-auth-signed-was-a-setting)) is the one where a gate detects a failure, logs it, builds an error-shaped response for it, and forwards the request anyway, because the handler's return type was never widened to distinguish \"I succeeded\" from \"I failed and here is the failure object.\" `$userrole` on both branches is the Perl dialect of that collapse: the handler's return value and the dispatcher's authorization read are the same channel.\n\n### The chain lives in one POST\n\nThe watchTowr detection artifact `watchTowr-vs-Barracuda.py` sends one HTTP request. The form fields are:\n\n```python\ndata = {\n \"user\": \"admin\",\n \"new_password\": \"one\",\n \"retype_password\": \"two\",\n \"UPDATE_activation_code\": \"1\",\n \"pu_bayesian_upload\": \"1\",\n}\nfiles = {\n \"bayesian\": (\"poc.bak\", backup, \"application/octet-stream\")\n}\n```\n\n`new_password=one` and `retype_password=two` are the bypass trigger. The two values do not match, the password-change code fires `ERROR_PASSWD_CHANGE`, the failure branch returns `$userrole`, and `response()` proceeds to run the module handlers the fix-author's comment warned about.\n\n`UPDATE_activation_code=1` and `pu_bayesian_upload=1` are the module-handler selectors. The dispatcher, now holding an authorizing return value, routes to the Bayesian-upload handler and the activation-code handler in the same request. The Bayesian file is uploaded. It is a tar archive. Inside the archive is `mysql-poc.gz`, a gzipped SQL dump that executes against the appliance's configuration database:\n\n```sql\nDELETE FROM `config`\n WHERE `variable`='system_timezone'\n AND `scope`='global'\n AND `scope_data`='';\nINSERT INTO `config`\n (`variable`,`value`,`scope`,`scope_data`,`comment`,`deleted`)\nVALUES\n ('system_timezone', 0x, 'global', '', 'System Timezone', NULL);\n```\n\nThe payload the hex decodes to is a timezone string. The payload is `UTC#';;#`. Alongside the SQL dump, the PoC builds a `backup.xml` manifest that advertises the archive to the restore handler:\n\n```xml\n\n \" path=\"mysql-poc.gz\" />\n\n\" type=\"manual_backup\">\n```\n\nThe `firmware_version=\"9.4.0.020\"` field is the version one release below the fix. The PoC author set it to match the version range the chain targets, because the restore handler compares the manifest's declared version to the appliance's own and refuses a mismatch. The declared version is a field the attacker fills in to pass the appliance's own version check. The restore handler verifies the archive's integrity, writes the SQL dump into MySQL, and advances. The `system_timezone` row is now whatever the attacker put in it.\n\n### The sink is `sed` with `#` as the delimiter\n\nCVE-2026-111560 is the command injection the poisoned `system_timezone` row reaches. The sink is in `Backup2.pm`:\n\n```perl\nif( -f \"/home/product/code/config/tz\" ) {\n my $system_timezone = Barracuda::Config::get('system_timezone');\n `sed -i 's#TZ=.*#TZ=$system_timezone#' /home/product/code/config/tz`;\n}\n```\n\nThe Perl backticks execute a shell command. The shell command is a `sed -i` whose `s` operator uses `#` as the delimiter, which is a reasonable choice when the pattern or replacement contains `/`. It is not a reasonable choice when the replacement is a variable the attacker controls through the database.\n\nWith `$system_timezone` set to `UTC#';touch /tmp/pwned;#`, the shell receives:\n\n```bash\nsed -i 's#TZ=.*#TZ=UTC#';touch /tmp/pwned;#' /home/product/code/config/tz\n```\n\nThat is three commands. The first `#` after `UTC` closes the sed substitution early. The single quote closes the shell's single-quoted string literal. The semicolon ends the first command. `touch /tmp/pwned` runs as the user Perl's backticks run as, which on an ESG appliance is root. The trailing `#` comments out the rest of the line, including the file argument that would otherwise have left `sed` running on a half-closed expression.\n\nThe same character that is the sed delimiter is the shell comment introducer. The author of the Perl line made a single choice, `s#...#...#`, that created two parsers reading the same string in incompatible ways. The payload `UTC#';;#` is calibrated to both. The `sed` command and the injected `touch` are separated by a `;`, which means the attacker's command runs whether or not the preceding `sed` succeeds. The order of operations is guaranteed by the shell's statement separator, not by the health of the first command.\n\n### The checksum salt is in the firmware\n\nThe backup archive the restore handler unpacks is integrity-checked. The manifest in `backup.xml` carries a checksum field, and the restore handler refuses archives whose checksum does not match. The watchTowr PoC computes the checksum this way:\n\n```python\nSECRET = b\"Barracuda Networks is your new best friend\"\n...\nchecksum = hashlib.md5(mysql_dump + SECRET + b\".\").hexdigest()\n```\n\nThe constant string is a salt the integrity check applies to the gzipped SQL dump. It is 42 bytes of ASCII, followed by a literal `.`, hashed with MD5. It is also a string literal in the firmware image every Barracuda ESG customer runs. The property the check was designed to enforce was that the attacker did not know the salt. The property the check actually enforces is that the attacker did not extract strings from the firmware. Barracuda sells the firmware to customers.\n\nThe appliance is doing exactly what the integrity check was designed to do, with a secret that was never one. The product name of the attack, written by Sina Kheirkhah of watchTowr as the name of their blog post, is the salt verbatim: \"Barracuda Networks is your new best friend.\" A researcher who titles their disclosure with the vendor's own shared secret has already told you what the secret was.\n\n### Barracuda rated the two halves separately\n\nBarracuda's advisory, published 2 November 2026, assigns CVE-2026-116606 a CVSS of 9.3 with the note *administrative UI access required*, and CVE-2026-111560 a CVSS of 8.6 with the note *administrative credentials required*. The advisory decomposes the chain into two independent bugs: one that admits an unauthenticated caller through the admin UI's front door, one that runs shell as root given an admin's credentials.\n\nThe detection artifact watchTowr published treats the two as one bug. The POST that triggers the bypass carries the file that triggers the injection. There is no second request. The administrative UI access the 9.3 rating cites as a precondition is reachable on an unauthenticated appliance; the administrative credentials the 8.6 rating cites as a precondition are supplied by the same HTTP request through the bypass. The chain is pre-auth RCE as root in one POST to `/cgi-mod/index.cgi`.\n\nThe advisory credits no external researcher. The watchTowr blog, which names Sina Kheirkhah of watchTowr as the discoverer, is the public analysis of the chain. Barracuda's own disclosure timeline, as summarized in third-party coverage, says the command injection was internally reported on 15 October 2026 and the bypass on 22 October 2026, seven days apart, as if they were two findings. The watchTowr PoC found them together, which is how they are deployed.\n\n### What the record shows\n\nFour technical facts are on the record. The password-change failure branch returned `$userrole`, and `response()` ran module handlers on that return. The backup integrity check used a salt shipped in every firmware image. The command-injection sink was a `sed` substitution whose delimiter was the shell's comment introducer. The JSON body the failure branch emits declares `success => 'true'` before and after the patch.\n\nOne advisory fact is on the record. Barracuda rated the two halves of the chain as if neither half were a precondition of the other, and credited the bug to no one.\n\nPoC: [watchtowrlabs/watchTowr-vs-Barracuda-RCE-CVE-2026-116606-CVE-2026-111560](https://github.com/watchtowrlabs/watchTowr-vs-Barracuda-RCE-CVE-2026-116606-CVE-2026-111560)","closing_line":"The patch is one return statement. The advisory is two.","hook_md":"Barracuda's Email Security Gateway has a password-change handler at `/cgi-mod/index.cgi`. When a caller submits `new_password` and `retype_password` that do not match, the handler logs `ERROR_PASSWD_CHANGE`, builds an error-shaped JSON body whose `success` field is set to `'true'`, and returns `$userrole`. The dispatcher reads that return value as authorization to run module handlers. One of the module handlers it now runs is the backup-restore handler, and the backup it restores contains a MySQL dump whose rows become shell commands by the time `Backup2.pm` runs `sed`. The 9.4.0.030 patch is one return statement, and the two lines of Perl comment the fix-author left above it say the quiet part in the vendor's own words.","post_id":851,"slug":"cve-2026-116606-barracuda-failed-password-returns-userrole","title":"CVE-2026-116606: Barracuda's Failed Password Change Returns $userrole. The Patch Comment Names the Bug.","type":"initial","unreadable_sentence":"The administrative UI access the 9.3 rating cites as a precondition is reachable on an unauthenticated appliance; the administrative credentials the 8.6 rating cites as a precondition are supplied by the same HTTP request through the bypass."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCavDSoQAKCRDeZjl4jgkQ JscyAQD9pAQ8Jg4meM80zGIwvnoCVtFKbXnsXvWfC97Q6KZmHQEAw3smL4tUGj3X pKq1vX8kBegvJIKE4sN3MIzhsjJXkww= =yORe -----END PGP SIGNATURE-----