{"id":52832,"date":"2026-10-21T07:00:00","date_gmt":"2026-10-21T00:00:00","guid":{"rendered":"https:\/\/deepquest.code511.com\/blog\/?p=52832"},"modified":"2026-11-11T18:36:50","modified_gmt":"2026-11-11T11:36:50","slug":"__trashed-369","status":"publish","type":"post","link":"https:\/\/deepquest.code511.com\/blog\/2026\/10\/__trashed-369\/","title":{"rendered":"Syncrify 6.2 Path Traversal Enables Root Command Execution"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The flaw involves path traversal through the authenticated PublicLink chunk-upload mechanism in Syncrify&#8217;s embedded Tomcat\/sradef framework. This allows writing files into critical system locations such as\u00a0<code>\/etc\/cron.d<\/code>, resulting in arbitrary command execution with root privileges<\/p>\n\n\n\n<!--more-->\n\n\n\n<p class=\"wp-block-paragraph\">0day Rubbish Research Team is publicly disclosing a vulnerability in Synametrics<br>Syncrify 6.2 build 1178, the closed-source incremental backup and<br>file-replication server from Synametrics Technologies, Inc. (United States). The<br>product is written in Java, distributed for Linux as a tarball archive, and<br>served by an embedded Tomcat 9.0.113 through the vendor&#8217;s in-house sradef<br>framework, whose single front controller routes every request as<br>\/app?operation=&lt;op>&amp;st=&lt;step> to a handler registered in WorkerMappings.xml.<br><br>Type and root cause. Path traversal, CWE-22 primary, with CWE-269 improper<br>privilege management and CWE-78 OS command injection as contributing<br>classifications that describe the deployment shape converting a file write into<br>an execution rather than a defect inside the application. The PublicLink<br>handler, operation=pb, implements the server&#8217;s PublicLink client protocol. Its<br>chunked-upload branch, selected by st=u, reads a length-prefixed binary frame<br>straight from request.getInputStream(), which means the server&#8217;s parameter<br>parsing, form decoding and any framework-level parameter validation are all<br>bypassed by construction: the field that decides the write destination is not a<br>parameter. At PublicLink.java:70-110 the decompiled method reads four big-endian<br>int32 values and one length-prefixed UTF-8 string, then does this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n\n    int protocol    = readInt(in);     \/\/ 4-byte big-endian\n    int chunkNumber = readInt(in);     \/\/ read, then not used to bound\n                                       \/\/ or place the write\n    int totalChunks = readInt(in);     \/\/ read, then not used either\n    int size        = readInt(in);     \/\/ declared length of the body\n    String filename = readString(in);  \/\/ &#91;int32 len]&#91;UTF-8 bytes],\n                                       \/\/ attacker-controlled\n\n    File base   = E();                 \/\/ this user's PublicLinks folder\n    File target = new File(base, filename);\n    if (!target.getParentFile().exists()) {\n        target.getParentFile().mkdirs();\n    }\n    BufferedOutputStream out = new BufferedOutputStream(\n        new FileOutputStream(target, true));   \/\/ append mode\n    byte&#91;] buf = new byte&#91;size];\n    readFully(buf);\n    out.write(buf);\n    out.close();\n    return 0;                          \/\/ 0 means success to the caller<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><br><br>new File(base, filename) is reached with no rejection of &#8220;..&#8221;, no rejection of<br>path separators, no base-name extraction and no canonical containment check.<br>base is \/opt\/Syncrify\/PublicLinks\/&lt;user>\/&lt;generated-folder>, five components<br>below the filesystem root, so a chunk name of ..\/..\/..\/..\/..\/etc\/cron.d\/&lt;name><br>leaves the per-user PublicLinks folder and lands a cron fragment, creating any<br>missing parent directory on the way because of the mkdirs() call. The branch<br>returns 0, f() at PublicLink.java:127 converts that into the client reply<br>code=&#8221;1&#8243; with &lt;payload>0&lt;\/payload>, and the host cron daemon then reads the<br>fragment as a root-authored job definition and runs its command field through a<br>shell on the next minute boundary. The application itself never constructs,<br>quotes or invokes a command line, and every execution site in the product uses<br>an argument vector with no shell, so this could not have been fixed by argument<br>hygiene at the application layer; the execution happens one component later.<br><br>The identity reached was observed rather than inferred, and it is uid=0(root)<br>gid=0(root) groups=0(root). Two independent artifacts establish that the writer<br>process held uid 0: the fragment landed as -rw-r&#8211;r&#8211; 1 root root at mode 0644,<br>and a file&#8217;s owner after creation is the effective uid of the process that<br>created it; and the scheduled job&#8217;s own output reported uid 0, the fragment<br>naming root in its user field, which a scheduler will not honour for a job file<br>it does not already trust.<br><br>What makes this an oversight rather than a design decision is the asymmetry<br>with the rest of the same tree. Three sibling families in the same product,<br>written against the same client protocol, check the same kind of field and<br>reject the same kind of content: the profile-upload, profile-fetch and<br>file-splitter handlers refuse names containing path separators by testing<br>character codes with indexOf(47) and indexOf(92); the multipart upload path<br>collapses the client-supplied name through getName() of a constructed File,<br>which discards every directory component; and the download path runs the name<br>through indexOf(&#8220;..&#8221;) and answers 404 on a match. The chunk-upload branch calls<br>none of them. When one member of a family omits the defense all of its siblings<br>implement, the economical explanation is that the check was not written rather<br>than that it lives somewhere else. There is also no decoding step between the<br>socket and the constructor on this branch, so no encoding trick is involved and<br>none is needed: what the client puts in the frame is what the path is built<br>from.<br><br>How confirmation works, and what was deliberately not reproduced. Confirmation<br>is by host-side artifacts, not by the HTTP reply, and that distinction matters.<br>The reply is a success oracle, so an attacker learns from the network alone that<br>a write was accepted and can iterate on naming from there, but a successful<br>reply is not proof of a successful destination: the code never inspects the<br>resolved path, so a wrong traversal depth still yields code=&#8221;1&#8243; and simply<br>writes somewhere else with intermediate directories created as needed. The<br>read-back of the marker file is the only sufficient signal. Four controls were<br>exercised and the published tool automates one of them. A traversing name and a<br>non-traversing name were driven through identical authentication, identical<br>framing and the identical route in the same session; the benign write was<br>accepted and landed inside the user&#8217;s own PublicLinks folder, and the assertion<br>that neither the cron fragment path nor the marker path existed is what isolates<br>the traversal as the cause of the outcome and of nothing else. A sibling-handler<br>control confirmed at the Java level that the multipart family&#8217;s getName()<br>collapse, applied to a traversing name against the same PublicLinks base,<br>returns the bare final component and keeps the write inside the root. A dispatch<br>control built against st=f instead reached a different void E() overload at<br>PublicLink.java:951 that parses a request parameter named ed as a Long and<br>performed no write anywhere; that control is what fixed the sink to this branch<br>rather than to the handler in general, since an initial static reading had<br>attributed the upload to st=f and the request built on that reading produced no<br>file at all. Authentication controls established that the credential gate is<br>live. The published advisory reproduces the identity string and the frame field<br>lengths derived from the field values, but deliberately not the run&#8217;s own byte<br>capture, which was not preserved; not the generated PublicLinks folder name,<br>which embeds the run&#8217;s timestamp; not the credential pair, which are mandatory<br>tool arguments with no default; and not the per-run fragment name or the marker<br>path. The payload was an identity capture into a marker file. No repository data<br>was read, no product file was modified or deleted, and the fragment and the<br>marker were removed after verification together with stopping the service.<br><br>Scoring. One score is published: 7.5 High,<br>CVSS:3.1\/AV:N\/AC:H\/PR:L\/UI:N\/S:U\/C:H\/I:H\/A:H. The arithmetic is shown so any<br>reader can reproduce it with the CVSS 3.1 base algorithm and its<br>round-up-to-the-tenth rule: ISS = 1 &#8211; (0.44 x 0.44 x 0.44) = 0.914816; Impact<br>for S:U = 6.42 x 0.914816 = 5.873119; Exploitability = 8.22 x 0.85 x 0.44 x<br>0.62 x 0.85 = 1.620146; Base = Roundup(5.873119 + 1.620146) =<br>Roundup(7.493265) = 7.5. AV:N is the honest classification of the component<br>because the vulnerable component is an HTTP handler on a network-bound servlet<br>connector and the credential it accepts exists precisely so that remote backup<br>clients can use it; the deployment-dependent exposure of that listener is priced<br>in attack complexity rather than smuggled into the attack vector. AC:H carries<br>two conditions outside the attacker&#8217;s control: that the listener carrying this<br>route be reachable from the attacker&#8217;s position, and that the host run a<br>scheduler that scans \/etc\/cron.d. PR:L records that the least privileged<br>principal proven sufficient is an authenticated backup user, the product&#8217;s<br>lowest-trust account type. Scope is Unchanged, for the identity reason given<br>below.<br><br>A conditional reading of 8.8 High, CVSS:3.1\/AV:N\/AC:L\/PR:L\/UI:N\/S:U\/C:H\/I:H\/A:H,<br>is published alongside it and is labelled conditional everywhere it appears. It<br>applies only where the listener carrying \/app?operation= is reachable from the<br>attacker&#8217;s network position, which is the normal operating mode for a backup<br>server whose clients push data to it remotely, and it removes the first of the<br>two AC:H conditions by assuming that deployment posture. It computes as<br>Exploitability = 8.22 x 0.85 x 0.77 x 0.62 x 0.85 = 2.835255, Base =<br>Roundup(5.873119 + 2.835255) = Roundup(8.708374) = 8.8. The published score is<br>7.5 and never 8.8; a reader whose deployment keeps the portal on a management<br>network should read the 7.5.<br><br>Two further readings are published as explicitly rejected, both with their<br>arithmetic, so that a reader who disagrees with either call can see exactly what<br>it is worth rather than take our word for it. The changed-scope variant of the<br>same metrics, 8.6 High,<br>CVSS:3.1\/AV:N\/AC:H\/PR:L\/UI:N\/S:C\/C:H\/I:H\/A:H, is rejected because the identity<br>reached is the same root identity the server process already holds on the same<br>host, so no component authority boundary is crossed by the outcome: the server<br>process is root over this host by construction of the deployment, and the<br>compromised scheduler and the compromised server sit inside one authority<br>domain. Its arithmetic: Impact for S:C = 7.52 x (0.914816 &#8211; 0.029) &#8211; 3.25 x<br>(0.914816 x 0.9731 &#8211; 0.02)^13 = 6.661336 &#8211; 0.533310 = 6.128026; Exploitability<br>with PR at the S:C value of 0.68 = 8.22 x 0.85 x 0.44 x 0.68 x 0.85 = 1.776934;<br>Base = Roundup(1.08 x (6.128026 + 1.776934)) = Roundup(8.537357) = 8.6. The<br>privilege gain in this finding is real and substantial, and it is expressed by<br>the metrics meant to carry it, PR:L for the low-trust principal and C:H\/I:H\/A:H<br>for the totality of what root on this host controls; Scope is not the vehicle<br>for expressing privilege distance and using it that way would double-count the<br>same fact.<br><br>The second rejected reading is the one we want readers to notice, because it is<br>the one that could most easily be misused in our favour. Treating the loopback<br>binding we observed in our own testbed as though it were the shipped default<br>yields 7.8 High, CVSS:3.1\/AV:L\/AC:L\/PR:L\/UI:N\/S:U\/C:H\/I:H\/A:H: Exploitability =<br>8.22 x 0.55 x 0.77 x 0.62 x 0.85 = 1.834577, Base = Roundup(5.873119 +<br>1.834577) = Roundup(7.707696) = 7.8. We reject it, and rejecting it lowers the<br>number we publish by 0.3 rather than raising it. That rejected local-vector<br>reading of 7.8 is higher than the 7.5 we publish at AV:N. So the rejection is<br>not a scoring maneuver and cannot be read as one: choosing AV:N with AC:H is the<br>conservative reading on this finding, and a reader who disagrees with us about<br>the factory default of that binding ends up with a larger figure than ours, not<br>a smaller one. The advisory says this itself, in those terms. The reason for the<br>rejection is evidential rather than convenient: that loopback binding was<br>applied by us, in the configuration, before any testing began, so that the host<br>carried no public exposure while research was in progress, and we hold no<br>shipped artifact, no configuration template, no installer script excerpt and no<br>documentation quotation, that states the factory value of that binding in either<br>direction. Not that it binds publicly by default, and not that it binds loopback<br>by default: the premise is unestablished by artifact, which is exactly why the<br>attack complexity is High and exactly why the 8.8 figure above is conditional.<br><br>Authentication. The finding is post-authentication and no unauthenticated<br>reachability is claimed. Nothing in this advisory is rated PR:N, and no<br>unauthenticated reading is offered as a hypothetical. The handler is registered<br>at secLevel=0 in WorkerMappings.xml, which in this framework means no<br>administrator session is required and is correct by design for client-protocol<br>operations, so the framework lets an anonymous request reach it and the only<br>decision left is the handler&#8217;s own authentication call. That call validates a<br>credential from the backup-user store, config\/UserMappings.xml via<br>syncrify.users.DefaultUserManager, and not from the web-administrator store,<br>config\/umap.dat via sradef.config.SecurityManager. The two stores are<br>independent and must not be conflated. On success the method writes the session<br>attribute pbUser, which is the state the upload branch depends on to choose whose<br>base directory to use, and it is written only after the credential check returns<br>non-null, so a caller cannot manufacture it by forging a cookie value: session<br>contents are server-side. The credential lookup contains a branch that returns an<br>authenticated holder with no password comparison at all, and it is not reachable<br>from this HTTP surface. The reason is worth being precise about, because it was<br>confirmed dynamically rather than trusted from the decompilation: the handler<br>tests whether either URL parameter is absent before the lookup is ever called,<br>and the two gates produce distinguishable reply codes. A correct pair was<br>accepted with code=&#8221;1&#8243; and a session cookie; a wrong password was rejected with<br>code=&#8221;-2&#8243;, so the comparison is live; an absent password was rejected with<br>code=&#8221;-1&#8243;, and an absent user with code=&#8221;-1&#8243;, so the earlier gate fired and the<br>lookup was never entered. Had a missing password reached the lookup, that branch<br>would have returned a holder and the reply would have been an acceptance rather<br>than the error code belonging to the earlier gate. The session identifier<br>arrives as an ordinary container cookie and the upload request must present it,<br>but there is no timing constraint between the two requests: the session is the<br>only coupling, no race to win and no window to hit, which is why the chain is<br>reliable rather than probabilistic.<br><br>The unauthenticated surface was exhausted before the post-authentication result<br>was accepted, and the enumeration is the content of that claim. Thirteen<br>operations answer anonymous requests with real content and no credential check;<br>each was probed without credentials and then traced into its handler code for a<br>sink, and all thirteen are sink-free, returning a build number, a login page, a<br>footer, a chart, a captcha image, an about page, click tracking, a newsletter<br>page, a client download and similar read-only or static content. The only<br>user-controlled parameter among them that reaches a function at all is the<br>address supplied to the password-recovery operation, and it is used as a lookup<br>key rather than as a destination, with the recovery message going to the address<br>already registered for that account, so it is an enumeration weakness under<br>CWE-203 and not a write or execution route. The remaining level-0 operations are<br>not anonymous: their handlers authenticate internally against the backup-user<br>store and answer code=&#8221;-2&#8243; without valid credentials, and treating level 0 as<br>unauthenticated would overstate this product&#8217;s exposure by roughly a factor of<br>three. All forty-seven level-0 operations were probed during enumeration on the<br>same listener that carries the vulnerable route, including the client-protocol<br>operations backup clients use. Seventeen Runtime.exec and ProcessBuilder call<br>sites exist in the tree and none is reachable anonymously: eleven belong to boot,<br>client, JRE-update, file-attribute or generic helper code with no network caller,<br>two sit in network handlers that are administrator-level with hardcoded argument<br>vectors and no injectable element, and the one structurally interesting site, a<br>backup job-hook executor, is blocked four separate ways, its script path being an<br>administrator-set value that is empty by default and returns immediately, its<br>executable needing to exist already, its execution being an argument vector with<br>no shell, and its substituted client-address value coming from the container&#8217;s<br>peer address rather than from a client-supplied field so it cannot be forged<br>over TCP. Eight deserialization candidates were examined and none has a network<br>caller; the in-house binary protocol is length-prefixed field parsing with no<br>object stream in it, and the classpath carries no gadget library, no<br>commons-collections, Jackson, XStream, Kryo or Hessian, so there is no chain<br>even if a sink were reached. No template engine and no naming-service lookup<br>exist anywhere in the tree, and the only user-influenced outbound fetch belongs<br>to an administrator-level operation. Two statements belong with that enumeration.<br>It is not a proof that the unauthenticated surface is safe; it is a report that<br>no route to a write, execution or traversal sink could be found within the<br>operations identified, using three independent enumeration directions. And the<br>sweep did surface weaknesses that are real and are deliberately not claimed here<br>and not folded into any metric: a filter-block bypass that exposes debug pages<br>shipped in the web root via a manualOverride parameter, yielding a directory<br>listing and an environment-variable dump; the password-recovery enumeration<br>above; globally disabled CSRF prevention, which is irrelevant to this chain<br>because the chain needs credentials the attacker holds rather than a victim&#8217;s<br>browser; vendor-default credentials for an embedded database that is configured<br>but disabled and was not listening; and a default trial licence serial whose<br>effect on user provisioning was left unresolved and which the chain does not<br>need.<br><br>One fact in this product is a trap for scoring and we did not fall into it. The<br>first-run install wizard creates a web-administrator account with the password<br>letmein, recorded in the code at SimpleLoginHandler.java:271, and the first<br>administrator login is forced through a password change before the<br>administration screens are served. A credential that expires on first use is not<br>a route to unauthenticated access. It is deployment-dependent, relevant only<br>where an operator skipped or defeated the forced change, and in any case it<br>points at the administrator store rather than the store that gates this sink. An<br>attacker holding it would still have to provision a backup user through a<br>licence-gated administrative operation before reaching this chain, which makes it<br>a strictly worse starting position than simply holding the backup-user credential<br>this product exists to hand out to client machines. Accordingly the factory<br>administration password is documented as a contributing fact, is used in no<br>vector, raises no metric, and no conditional reading in the advisory assumes it<br>has not been rotated.<br><br>Impact. Arbitrary file write with an attacker-chosen path and attacker-chosen<br>bytes: the chunk branch writes exactly the size bytes it reads from the request<br>body to whatever absolute path the chunk name resolves to, in append mode,<br>creating missing parent directories on the way. The \/etc\/cron.d destination is<br>the escalation path demonstrated, not the limit of the primitive. On the<br>verified deployment shape, where the server process holds uid 0 and a cron<br>daemon is active, the written fragment is evaluated by the host scheduler as a<br>root-authored job definition and the command runs with the full authority of the<br>identity the server already holds. This is backup infrastructure, and that is<br>what gives the number its weight. Root on this server means read access to the<br>whole repository tree, every backup belonging to every configured client, which<br>for a multi-tenant server is a confidentiality breach across all of them rather<br>than within one account, and it means read access to the server&#8217;s own<br>configuration, which holds administrator and backup-user credential material and<br>transport secrets; the backup-user store compares passwords in plaintext or<br>stores them under a reversible permutation transform, so a read of that file<br>hands over credentials directly. It also means integrity control over the backup<br>chain: repository contents, version and retention metadata and the server&#8217;s own<br>binaries can all be altered in place, and a modified backup set is a failure<br>that stays invisible until the moment the data is needed. Persistence is<br>structural rather than incidental, because a cron fragment runs on every minute<br>boundary until someone deletes it, so one successful chunk write buys recurring<br>execution rather than a single shell, and it survives restarts of both the<br>service and the host. Availability follows the same authority: the service can<br>be stopped, the host rebooted or hung, repositories deleted or truncated, and<br>the scheduler used to keep doing so after remediation of the application alone.<br>The privilege boundary that fails here is inside the product, not on the host:<br>the principal that achieves all of it is a backup user, the lowest-trust account<br>type the product has, created by an administrator for the purpose of moving<br>bytes into one repository, and nothing about that role is supposed to reach a<br>host-scheduler directory. All of that is argued from the identity reached and<br>the deployment layout, and none of it was executed; nothing was done with any<br>stored backup during verification.<br><br>Verification boundary, stated plainly. One instance, one build, 6.2 build 1178,<br>one operating system shape, installed from the distributed Linux tarball archive<br>into \/opt\/Syncrify and started with .\/run.sh, launched as<br>jre\/bin\/java -server -Xmx512m -cp lib\/*.jar com.synametrics.sradef.BootLoader<br>behind Tomcat 9.0.113 announcing itself as Synametrics Web Server v7. Static<br>analysis was a reversal of the decompiled handler tree; the dynamic work was a<br>live run against that instance. Two properties of the instance were ours rather<br>than the product&#8217;s, and both are carried here rather than buried. The portal<br>listener was bound to the loopback address by an explicit configuration edit we<br>made before testing, so the host was not reachable from outside while research<br>was in progress; it is a lab hardening step, not an observed product default, and<br>no shipped artifact establishes the factory value of that binding in either<br>direction. And the backup-user store file did not exist after installation, so<br>the account used for verification was written into config\/UserMappings.xml by<br>hand with plaintext password comparison rather than provisioned through the<br>administrator&#8217;s user-management operation, which is additionally licence-gated<br>and was blocked by a zero licence count. The sink&#8217;s gate does not distinguish<br>between those provisioning paths, and any credential pair the server accepts<br>satisfies it, but only one instantiation of that condition has a live run behind<br>it: the plaintext-comparison branch. The encrypted-entry branch and the<br>directory-authentication branches were examined statically and not exercised.<br>No administrator session and no licence state sits between a backup user and this<br>write, and that should not be rounded down; equally it should not be rounded up<br>into a claim that any account works. The chain was driven from the target host<br>itself over the loopback interface, because that is how the listener was bound in<br>the instance, and the same chain arriving from across a network segment was not<br>verified and is not claimed. Where the advisory treats the route as remotely<br>reachable, that rests on the product&#8217;s own design and binding configuration, on<br>the front controller being the same listener that serves the client-protocol<br>operations backup clients use, and on a structural argument: in any deployment<br>where clients back up across a network, the listener carrying operation=pb must<br>be reachable from those clients or the product cannot perform its function. It is<br>argued, not measured from a separate host, and reproducing it from a remote<br>position would strengthen the finding rather than contradict it. Two listeners<br>were world-bound in the instance, an HTTP status endpoint on 52110 that answered<br>with server version and build, active HTTP sessions and job status, and the<br>accelerated-transfer channel on 27000 over TCP and UDP; neither was observed to<br>serve the \/app front controller, so neither is offered as a demonstrated remote<br>entry point to this sink, and a third listener on 54222, a secondary failure-over<br>HTTP endpoint, was likewise loopback in the instance. Root execution was<br>observed, not inferred; a non-root service deployment was not tested, so no<br>claim is made about what the write primitive achieves under an unprivileged<br>service identity, where \/etc\/cron.d could not be written and the escalation<br>demonstrated here would not follow while the file write itself survived. Exactly<br>one payload shape was executed, an identity capture into a marker file, and only<br>that shape is claimed. The published proof of concept is a rewrite of the<br>verification tool with argument handling and a control mode; the transcripts in<br>the advisory are from the run that established the finding. Only Syncrify 6.2<br>build 1178 was decompiled and exercised: no other version, edition or platform<br>was tested, no cross-version comparison was performed, and no version range is<br>claimed, although the construction is simple enough that adjacent versions very<br>likely share it and that is stated as unevidenced rather than asserted. The same<br>in-house sradef framework also underpins the vendor&#8217;s sibling products, the<br>shipped web.xml of this build still carries the display-name SynaMan, and the<br>shared lib4njre.jar supplies the commons utility package to all of them; that is<br>recorded because it tells a reader where else the same construction might be<br>audited, and this advisory makes no claim about any product other than Syncrify<br>6.2 build 1178, none of which was tested here.<br><br>Detection notes, including one that will otherwise produce false positives by<br>design. Detection cannot come from the application, because the application<br>considers every step of this chain to have succeeded: the upload branch performs<br>no path logging, produces no rejection, records no path-decision failure, and<br>returns exactly the acceptance reply code=&#8221;1&#8243; with &lt;payload>0&lt;\/payload> that a<br>legitimate chunk upload produces, so there is no application-level signal to<br>alert on and nothing distinguishes a benign name from a traversing one. Access<br>logs give weak signal only, and this is the false-positive caution: a<br>chunk-upload request looks like ordinary client traffic, so a rule keyed on the<br>presence of chunk uploads to operation=pb&amp;st=u will fire on every real backup<br>session. The usable anomaly is shape rather than presence, an<br>authenticate-then-upload-one-or-two-small-chunks pattern with no further<br>protocol activity, which is not what a real backup client does, since a genuine<br>session issues a long sequence of such requests. Filesystem integrity monitoring<br>is the reliable control: alert on creation or modification under \/etc\/cron.d,<br>\/etc\/crontab, \/var\/spool\/cron, the init system&#8217;s unit directories and the<br>service&#8217;s own binary and configuration paths, and specifically on any new file in<br>a scheduler directory whose content matches a job line with a user field of root.<br>The PublicLinks tree is worth auditing in its own right, for files whose names or<br>locations do not correspond to shared content and for per-user folders created by<br>accounts that never used the sharing feature. Scheduler and authentication logs<br>correlate the two halves, and a scheduled job whose command references a path<br>under a temporary directory is a reasonable alert on its own. Credential hygiene<br>is a detection control here as well as a mitigation, because each backup-user<br>credential is a potential root write, so unexpected accounts in the backup-user<br>store and accounts created without a corresponding client deployment both<br>deserve review. Two properties of the write are worth knowing when designing<br>those rules. Because the writer appends, a repeated request does not replace the<br>fragment, it adds lines to it, and a malformed second line does not undo a valid<br>first one, so a defender cannot rely on an attacker needing to get the fragment<br>byte-exact in a single shot. And the write is not atomic, there being no<br>write-to-temporary-then-rename step, so an interrupted request can leave a<br>truncated fragment on disk; for a job-definition directory a truncated line is<br>simply skipped by the scheduler, so that does not defeat the chain.<br><br>Mitigation summary: reject or confine the chunk name at the sink, before the<br>file constructor, refusing any value containing a path separator or &#8220;..&#8221; and any<br>absolute value and reducing what remains to a single path component, which is<br>convergence on a pattern the tree already implements three times over rather<br>than new design; then add a canonical containment check after construction and<br>not instead of it, resolving with getCanonicalPath() and requiring that the<br>result start with the canonical path of the authenticated user&#8217;s own PublicLinks<br>folder followed by a separator, since containment is the property that survives<br>every encoding and every absolute-name variation while name-based rejection<br>alone does not; stop creating whatever directory the client names, the mkdirs()<br>call on the resolved target&#8217;s parent letting a client materialise directory<br>structure anywhere the service identity can write, and create the destination<br>once from server-side state instead; give the protocol a real chunk-placement<br>contract using the frame&#8217;s own chunkNumber and totalChunks values, which are read<br>and then ignored, by allocating the destination from session state, verifying the<br>sequence and staging reassembly in a directory no scheduler, service manager,<br>shell profile or startup mechanism consumes before moving the file into place<br>under a server-generated name; run the service under a dedicated unprivileged<br>account with write access confined to the repository and PublicLinks subtrees,<br>which removes the escalation to root command execution because a non-root process<br>cannot place a usable fragment in \/etc\/cron.d, but is defence in depth rather<br>than the fix since the write primitive itself survives; close the container-level<br>gap that makes per-handler secLevel the only gate, the shipped web.xml security<br>constraint covering DELETE, OPTIONS and PUT only and saying so in its own<br>load-time message while every request in this chain arrives over GET or POST and<br>therefore depends entirely on each handler declaring a correct level; audit the<br>same construction across the framework family, because the front controller, the<br>handler registry and the shared helper library are common to the vendor&#8217;s other<br>products and the root cause is framework-shaped rather than handler-local; and<br>ship a regression test that fails the release, posting chunk names consisting of<br>a traversal to \/etc\/cron.d, an absolute name, a URL-encoded name, a name with a<br>trailing separator and a name whose parent-directory segments exceed the<br>repository depth, asserting both a rejection reply and that no file appeared<br>outside the repository subtree. Until a patch exists, operators should treat<br>every backup-user credential as a potential root write and provision them<br>narrowly, keep the portal listener on a management network or behind an<br>authenticating reverse proxy and off the internet, run the service unprivileged<br>where the deployment permits it after verifying repository ownership still works,<br>enable filesystem integrity monitoring on the scheduler and service paths, audit<br>the PublicLinks tree, monitor the backup-user authentication path for the shape<br>described above, and keep a supported restore tested, ideally as an offline or<br>immutable copy of critical repositories outside the server&#8217;s own write reach,<br>since that is the control which survives a full compromise of this host.<br><br>The published proof of concept is Python 3 using only the standard library, with<br>no third-party dependency and no hardcoded target. It takes host, port, scheme,<br>route, operation and upload step; a mandatory backup-user credential pair with no<br>default; the command, marker path, fragment path, fragment user field, fragment<br>schedule, traversal depth, scheduler wait and per-request timeout as arguments,<br>defaulting to the loopback interface, port 5800, a depth of five and an identity<br>capture; an &#8211;insecure flag for self-signed TLS on the https scheme; a &#8211;control<br>mode that performs the identical authenticated upload with a non-traversing chunk<br>name and then asserts that neither the fragment nor the marker exists; a<br>&#8211;proof-command argument that reads proof from a local command&#8217;s standard output<br>for an operator who already holds a shell channel to the host; an &#8211;expect-root<br>flag that exits non-zero unless the proof text reports uid=0; and a &#8211;cleanup<br>mode that removes only the two paths the script created.<br><br>Full technical analysis and the reproducible proof of concept, published<br>unconditionally:<br>  <a href=\"https:\/\/0day-rubbish.com\/blog\/synametrics-syncrify-publiclink-chunk-upload-traversal-cron-root-rce\">https:\/\/0day-rubbish.com\/blog\/synametrics-syncrify-publiclink-chunk-upload-traversal-cron-root-rce<\/a><br><br>Project archive (ongoing disclosure series):<br>  <a href=\"https:\/\/github.com\/Exploit-Garbage\/0day-Rubbish\">https:\/\/github.com\/Exploit-Garbage\/0day-Rubbish<\/a><br><br>Synametrics Technologies, Inc. was notified on the same day through the<br>vendor-wide technical-support mailbox it documents on the product&#8217;s own support<br>page, which is the address we judged appropriate for a security report and is<br>the only channel we used. The vendor publishes no RFC 9116 security.txt, the<br>request returning 404 over https while the http redirect resolves to a 404 as<br>well, and we found no security page, vulnerability-disclosure policy or<br>responsible-disclosure statement anywhere on the vendor&#8217;s live hosts, whose<br>entire web surface is two hosts. A separate documented sales mailbox exists on<br>the same domain and we did not use it. We submitted nothing to any<br>pre-publication intake form, because none exists, and we addressed no individual<br>by name. This disclosure is unconditional: the research is already public, no<br>permission was asked for, no embargo was requested, and nothing is withheld<br>pending a reply. No vulnerability identifier has been assigned to this finding<br>yet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Synametrics Syncrify 6.2 &#8211; command execution<\/p>\n","protected":false},"author":439,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_hide_featured_image":false,"_jetpack_featured_image_duplicate_dismissed":0,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[43,3],"tags":[],"class_list":["post-52832","post","type-post","status-publish","format-standard","hentry","category-exploit","category-security"],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p4bBYZ-dK8","jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/posts\/52832","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/users\/439"}],"replies":[{"embeddable":true,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/comments?post=52832"}],"version-history":[{"count":6,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/posts\/52832\/revisions"}],"predecessor-version":[{"id":53242,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/posts\/52832\/revisions\/53242"}],"wp:attachment":[{"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/media?parent=52832"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/categories?post=52832"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/deepquest.code511.com\/blog\/wp-json\/wp\/v2\/tags?post=52832"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}