Full Disclosure mailing list archives

[0day-rubbish] Abyss Web Server X1 2.16.22.0 first-run console credential takeover to configuration-write log-as-CGI command execution (7.5)


From: disclosure via Fulldisclosure <fulldisclosure () seclists org>
Date: Tue, 13 Oct 2026 02:20:48 +0000

0day Rubbish Research Team is publicly disclosing a vulnerability chain in
Abyss Web Server X1 2.16.22.0, the free edition of the closed-source Abyss
web server from Aprelium (Tunisia), a product family that has shipped for
more than twenty years. The release artefacts identify 2.16.22.0 as the
vendor's current official release. Between first start and the moment an
administrator sets a console password, any client admitted by the shipped
console access rule can register that administrator with no credential at
all, and three ordinary configuration forms afterwards convert the
configuration plane into an execution plane: the host access log becomes a
shell script inside CGI scope, and any anonymous request to the log path
executes it.

Type and root cause. CWE-306, missing authentication for a critical
function, is primary, and CWE-78, OS command injection, is co-equal rather
than subordinate. CWE-284, CWE-73, CWE-362, CWE-352, CWE-328 and CWE-250
contribute and are reported separately without raising any metric. The two
primary classifications are co-equal because removing either one alone
removes the chain while leaving every other weakness discussed here intact.
It is worth stating what this finding is not, because the technique is the
interesting part: there is no memory corruption, no deserialisation gadget
and no file-upload primitive anywhere in it. The write primitive is the
product's own supported configuration form, and the command enters the
written file through a quote-context escape in a request header field. A fix
that only adds authentication to the console leaves the log-as-interpreter
collision in place; a fix that only filters the header leaves the
unauthenticated credential creation in place. Neither half is sufficient on
its own.

The console authenticates with HTTP Basic on its default port 9999, binds
`bindip` `*` by default, and issues no session cookie anywhere - every
endpoint answers without `Set-Cookie`, including the endpoint that creates
the administrator. On first start the generated `abyss.conf` carries a
`<console>` node with no `<login>` and no `<password>` element, and the
startup terminal prints a message telling the operator to browse
`http://127.0.0.1:9999` to continue with the console access configuration.
While that state holds, `GET /console/` answers a distinctive `302` and the
bootstrap chain leads to `GET /console/credentials` answering `200` with a
creation form plus a `$$xxvxd` page token; a credential-free `POST` to that
endpoint installs the administrator. On an instance that has never been
configured the redirect first passes through `/console/language`; on an
instance whose language is already chosen it goes straight to
`/console/credentials`. Both shapes are unauthenticated throughout. After the
install the same endpoint answers `401` with
`WWW-Authenticate: Basic Realm="Abyss Web Server Console"`, so the
attacker's own POST is what closes the window.

The three forms that follow are a direct serialisation of the configuration
tree - each HTML control's `NAME` is the XML key path it writes - so what
they accept is exactly what the configuration can hold.

  - `POST /hosts/host@0/edit/scripting/paths/path@0/edit` takes
    `vpath=/` with `type=0` (CGI) and writes
    `<paths><path><vpath>/</vpath><type>0</type>...</path></paths>`. A
    `vpath` of `/` places the entire virtual path space of the host inside
    CGI scope, which includes files the server writes into the document root
    itself. There is no minimum-depth requirement and no warning about scope
    width.
  - `POST /hosts/host@0/edit/log` takes an arbitrary absolute filesystem
    path in its `file` control and a `format` of `.combined`, writing
    `<log><file>(absolute path inside the document root)</file><denied/>
    <format>.combined</format></log>`. No whitelist, no restriction to a log
    directory, no rejection of a destination inside the document root, and no
    canonicalisation. The control renders with a `Browse` selector that walks
    the target's directory tree from the filesystem root downwards, so the
    document root does not have to be guessed.
  - `POST /hosts/host@0/edit/scripting/interpreters/interpreter@0/edit`
    takes an arbitrary executable path with `interface=0`, `checkexists=yes`
    and `type=0`, and a second-step extensions sub-form then associates an
    arbitrary extension string. Submitting `/bin/sh` and `ext=log` writes
    `<interpreters><interpreter><file>/bin/sh</file>...<ext>log</ext>
    </interpreter></interpreters>`. `checkexists=yes` is satisfied because
    `/bin/sh` exists; nothing reasons about whether the extension names a
    content type rather than a destination type, whether the interpreter is a
    general-purpose shell, or whether the server itself writes files with
    that extension.

`POST /` carrying `/restart=Restart` - the product's own restart button, with
the main page showing "The configuration has changed. Press [Restart] to
apply the modifications." and host status `Running(Updated)` - applies all
three. The request-serving slave process bounces, and only the new
generation writes into the newly configured log destination. The access log
is now a shell script inside CGI scope. A request whose `User-Agent` is
`zz"; (command); #` appends a `.combined` line in which that value lands
inside the format's own double-quoted field, where the quote closes the
field, the semicolon separates a statement and the trailing comment swallows
the remainder. An anonymous `GET` on the log path then makes the product
start the CGI child `/bin/sh (log file)`, and the shell reads the whole file
line by line, executing the semicolon segment. Commands run as the identity
of the server process.

How confirmation works. The final trigger answers `500`, not `200`, and that
is the appearance of success rather than failure: `/bin/sh` writes no CGI
headers, so the product cannot construct a valid CGI response and answers
with its standard 348-byte error page while the CGI log records `Broken pipe`
for that invocation. Proof is server-side and three-channel rather than
client-observed. The product's own CGI error log names the child it spawned
directly, `CGI: [/bin/sh x.log ]  URI: /x.log`, interleaved with the shell's
own diagnostics `x.log: line N: 127.0.0.1: command not found` - those are the
log's ordinary leading fields being treated as command words, and they do not
stop execution of later lines, which is why a noisy log prefix is harmless.
The diagnostics incremented from two lines to four as the log grew between
fires, which is causality visible in the log: each fire re-reads the whole
file and re-executes every line. A marker file written by the injected
command contained `uid=uid=0(root) gid=0(root) groups=0(root)`, 43 bytes and
root-owned; the doubled prefix is the injected command's own literal, not a
product string, and the identity the server process ran under is
`uid=0(root) gid=0(root) groups=0(root)`. Neither HTTP status in the last two
steps indicates failure - the smuggling request only has to be logged, and it
was answered `404` with a 336-byte body on a nonexistent path precisely so
it would have no other effect on the target.

What was deliberately not reproduced. The published advisory reproduces the
identity string and the product's own log lines; it does not publish the
per-run marker token, the credential pair used in verification or any
hostname from the run, because all three are artefacts of one execution
rather than facts about the product. No credential was leaked, seeded or
reused: the pair was chosen by the test operator at that moment, it is not a
product default, and this post reproduces no credential. No reverse shell, no
callback listener, no persistence mechanism and no destructive payload was
exercised.

Scoring. One score is published: 7.5 High,
`CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H`. That vector computes to
7.493264 raw - ISS 0.914816, Impact(S:U) 5.873119, Exploitability
1.620146 from weights AV 0.62, AC 0.44, PR 0.85, UI 0.85 - and is taken to
7.5 by the CVSS ceiling-to-tenth Roundup. No pre-existing numeric score was
inherited: the sources behind this advisory carried no computed CVSS figure,
so there was nothing to correct, and every number published here was computed
from its vector.

The Attack Vector is Adjacent, not Network, and the reason is an artefact
rather than a judgement call. The console binds every interface by default
and the shipped documentation presents that as the recommended setting, but
its `ipcontrol` block permits loopback, all three RFC1918 IPv4 ranges
(`10.0.0.1-10.255.255.254`, `172.16.0.1-172.31.255.254`,
`192.168.0.1-192.168.255.254`) and IPv6 unique-local plus link-local, with no
deny entries, and refuses public-internet sources. The check is evaluated on
the connected socket peer and was measured not to be influenced by
`X-Forwarded-For` or `X-Real-IP`. Those same address-range literals appear as
byte literals inside the stripped binaries. A default installation is
therefore not internet-reachable, and no published score assumes that it is.
The adjacent position is genuinely wide in practice - the whole office LAN,
the whole home LAN, every tenant on a shared management VLAN, every container
on a bridge network, every instance on a cloud VPC subnet - but it is
adjacent, and the headline vector says so.

The Attack Complexity is High because success requires arriving before the
administrator arms the console. The endpoint answers `401` from the moment a
credential exists, and that was measured in every verification round. The
window's field duration is not established and is not estimated anywhere in
this research; the advisory invents no figure for it.

Two conditional readings are published alongside the primary, each labelled
as conditional and neither quoted as the headline.

  - Conditional A, the window left open indefinitely:
    `CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` = 8.8 High
    (Exploitability 2.835255, base 8.708373). Applies where automation
    installs and never arms the console, where appliances are staged before
    deployment, or where containers and host images regenerate a pristine
    configuration on every restart, so the attacker polls an unauthenticated
    state rather than racing a person.
  - Conditional B, the console also exposed to a network:
    `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` = 9.8 Critical
    (Exploitability 3.887043, base 9.760161), and with the window contested
    `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H` = 8.1 High
    (Exploitability 2.221167, base 8.094286). Applies where an operator
    publishes port 9999 through NAT or a port forward, binds it on a publicly
    addressed host, or relays through a same-host reverse proxy whose
    forwarded traffic presents a permitted socket peer. That proxy shape
    passes `ipcontrol` without any header logic being consulted at all. Both
    are conditional on a deployment change we did not test.

One reading was rejected. Scope Changed was considered seriously and refused:
`CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H` computes to 8.4 (ISS
0.914816, Impact(S:C) 6.128026, Exploitability 1.620146, base 1.08 times
7.748172 = 8.368026) and is not published as the finding's score. Three
reasons. Both listeners belong to one product and one process family - a CLI
master and a `--slave --interactive` service process - so the console and the
host plane are two doors into one authority, not two authorities, and
applying configuration to the serving plane is the product's documented
intended behaviour. The CGI child runs as the server process's own identity
on the same host, in the same filesystem view, so no different security
authority is crossed. And the operating-system consequences depend on how the
deployment runs the server, which is established below as
deployment-dependent; scoring Changed on the strength of a root deployment we
could not prove to be the shipped default would be exactly the inflation the
surrounding discipline exists to prevent.

Authentication. The chain head requires no principal at all - only that no
credential exists yet and that the source address passes `ipcontrol`. The
configuration-write and restart forms require a console administrator, and
that administrator is the credential the unauthenticated request just minted,
so no credential owned by anybody else is consumed at any point. That is why
`PR:N` is the correct metric. `UI:N` because no administrator action, no
browser session and no induced click is required at any step. The gate was
tested by behaviour rather than trusted from configuration: after the
credential was installed an unauthenticated `GET /console/credentials`
returned `401` with the Basic realm, observed in every round and reproduced
by an independent manual request sequence.

Two negative controls isolate causality in a chain with two independently
necessary links. The execution control deleted the marker, re-sent the
identical `User-Agent` payload to a different nonexistent path - the fourth
of five log lines in the quoted run, byte-identical in its payload portion -
and deliberately did not request the log path; after a wait long enough to
cover the observed flush latency the marker did not exist. A request carrying
shell metacharacters in a header therefore executes nothing by itself, no
matter how many times it is logged. The follow-up step requested the log path
again and the marker reappeared with byte-identical content, which is the
determinism half: the file write is necessary but not sufficient, the CGI
fire is necessary but not sufficient, and the pair is sufficient and
repeatable. A third control excluded spoofing by construction: the
configuration contents and the marker file were each read server-side, and
the CGI error log independently corroborated both the child's command line
and the shell's line-by-line execution, so no claim rests on a client-observed
value alone.

Adjacent claims that are explicitly not made. No authentication bypass exists
outside the window: once a credential is present no unauthenticated route to
a configuration form was found, the console answers `401` across its routes,
and repeated attempts against the creation endpoint after arming also return
`401`. No session attack is claimed, because with no cookie mechanism there
is no session fixation, no token theft and no cookie replay; `$$xxvxd` is a
per-server-run page token that every client can read from any form page, so
it is not a session secret and predicting it buys nothing - it was parsed,
not attacked. No password-reset surface exists to bypass: the product has no
forgot-password, recovery-email or secret-question flow, and the shipped
documentation's recovery for a lost console password is a filesystem
operation. Password guessing was neither needed nor attempted, and the
absence of observed rate limiting on the console's Basic authentication is
reported as a weakness in its own right rather than folded into the chain.

The window, and a caveat that is easy to get wrong. The window is re-enterable
and is not once per machine. It re-opens whenever the credential nodes are
removed, which is also the product's own documented recovery for a forgotten
console password, and it re-opens on any redeployment that regenerates the
configuration - a CLI redeployment from a pristine tree, an operator
following that documentation, a host image rebuilt from a clean tree, or a
container started from a fresh configuration image. Describing this as a
one-shot race that ends forever after first setup is incorrect. What is
one-shot is the transition per configuration state: the endpoint answers
`401` from the moment a credential exists.

Separately, the unarmed-versus-armed state is discoverable without any
authentication at all. An unauthenticated `GET /console/` answers a
distinctive `302` while the instance is unarmed and `401` once it is armed, so
an attacker can survey a private network continuously for unarmed instances
and race any administrator to them. That converts the race from luck into
monitoring, and the advisory reports that discovery primitive to the vendor
separately rather than treating it as a detail of the main chain. What is not
established, and is not estimated, is how long the window stays open in a real
deployment: the sources contain no field measurement of the interval between
installation and arming. The shapes differ by orders of magnitude - an
administrator who follows the startup message immediately closes it in
seconds, while automation that installs and never arms, an appliance staged
before deployment, a development instance left running or a container
rebuilt from a pristine image on every restart may leave it open
indefinitely, and in that last shape the attacker keeps arriving rather than
racing anybody.

Impact. Full administrative takeover by an unauthenticated client on the same
private network, with a minted console credential that is structurally
indistinguishable from the legitimate one - the same
`root/console/login` node and the same MD5 digest storage - so the intended
administrator who arrives second is locked out with a `401` and no alert
distinguishes what happened from a typo. From there:

  - arbitrary operating-system command execution as the server process
    identity through the product's own CGI mechanism, with no payload upload
    required, because the access log is the payload carrier and it
    re-executes on every request to the log path until the log is rotated or
    truncated;
  - read access to everything that identity can read - all served content,
    every TLS private key reachable through the console's certificate
    management, and the host filesystem to that identity's limits, which at
    root means the entire host;
  - complete control of the configuration tree of every virtual host:
    scripting and interpreter bindings, logging, TLS material, access
    control, proxying, and the console's own listener settings, including the
    ability to widen `ipcontrol` or rebind the console port, which removes the
    defender's ability to re-narrow the surface remotely;
  - persistence in at least four independent ways: the minted credential
    survives restarts; the interpreter binding and CGI scope survive restarts
    and are invisible to a casual audit; the smuggled command stays in the
    log file and fires on every request to that path; and the executed
    command can install an operating-system startup mechanism with no further
    product interaction;
  - availability impact at will: hosts can be stopped, the service restarted
    repeatedly, and the arbitrary-path log write alone permits filling a
    filesystem as a standalone primitive independent of the execution chain.

Two primitives are reported to the vendor separately and are given no score
of their own, because scoring them here would double-count what the chain
already carries: the arbitrary-path continuous append as the server user, for
which log poisoning and disk filling need nothing else in the chain, and the
console file browser's read-only whole-filesystem enumeration. Also reported
separately, and folded into no metric, are the amplifiers: the default
`ipcontrol` breadth combined with an all-interface bind and the documentation's
"recommended" framing of that bind (CWE-284); configuration writes carrying
no CSRF token with no session cookie (CWE-352); MD5 password retention
(CWE-328); console Basic authentication with no observed rate limiting while
`antihack`, the request-filtering subsystem that could have evaluated the
payload, defaults to `enabled no`; and the absence of enforced privilege
separation (CWE-250).

Verification boundary, stated plainly. All dynamic work ran against a live
instance of the product built from the official Linux release tree
`abwsx1.tgz`, answering real HTTP traffic - not an emulation, not a protocol
replica and not a reimplementation. Every request was answered by the
vendor's own stripped binary, and every server-side artefact quoted was read
off that instance's disk. Three rounds produced the `uid=0(root)` marker
green: one manual request sequence driven end to end with command-line HTTP
tooling, and two fully automated rounds of a standard-library driver, each
exiting zero, the second with cleanup disabled so the scene could be
inspected. The two automated rounds were line-for-line identical on the
client side apart from the omitted cleanup segment and the per-server-run
page token, which differs between runs by design. One further observation is
reported because it is evidence rather than a footnote: a round in which the
smuggling and fire steps ran before the new generation had taken over was
served by the pre-restart process, wrote the payload into the previous log
destination and produced no marker, which is what proves the log redirection
is generation-scoped and why the shipped tool waits on a positive test of the
redirect - a request with a distinguishing `User-Agent` appearing in the new
file - rather than on the port coming back.

The whole run, server and client together, sat inside a dedicated network
namespace whose only interface was loopback, brought up before the server
started; every request in every round was sent to `127.0.0.1`. A listener
survey performed in the host's root namespace while the run was in progress
returned zero hits on either product port, which is the isolation evidence -
a `bindip` of `*` inside a namespace with no other interface cannot be
reached from outside it. Consequently no step of the chain was driven from a
separate network host, and the claim that an attacker sharing a private
network can do this is argued from shipped artefacts - the all-interface bind
and the `ipcontrol` allow ranges, whose literals also appear inside the
binary images - rather than demonstrated between two machines. A two-host
detonation would be the direct demonstration and was not performed, because
running the product with its default all-interface bind on a reachable
network was not acceptable for this work. Reproducing it from a remote
position would strengthen the finding, not contradict it.

This is a pure binary-audit case with no source code of any kind in the
release tree. Two stripped native images were analysed: `abyssws-x64`, ELF
64-bit stripped, 5700872 bytes, BuildID
`d7545bad2f47fb7b9f269ae98cb7baea1594fa12`, and `abyssws-x86`, ELF32 i386
stripped, 6174164 bytes, BuildID
`81b83c50c27df109282654730c52539a7db37cc2`; OpenSSL 3.x, nghttp2, PCRE2 and
zlib are statically embedded in a hand-written C HTTP stack. The
administration console's HTML is generated from templates compiled into those
binaries and the shipped `console.js` is a zero-byte placeholder, so the
forms and their accepted values had to be recovered rather than read. The
recovery method is behavioural: fetch each console page and read the emitted
`FORM` control names, which are configuration key paths; index the shipped
`lang/*.lng` UI semantic tables together with a 47324-line string dump of the
x64 image; then submit each form and read back the resulting configuration
file. That yields the interface contract and its effects, and it is the limit
of the method - no disassembly-level symbol evidence is cited anywhere in the
advisory, and none is needed for the claims made, because every claim is
anchored to a form field, a configuration fragment or an observed process
behaviour instead of a symbol. It also means the sink is composite and has no
single function address to cite, which is normal for a configuration-plane
defect in a stripped binary. The sink was found from the configuration side
rather than from a crash or a disassembly: because control names are key
paths, reading the generated default configuration maps everything the
console can change, and the chain came from comparing two innocuous answers -
a log form that accepts any absolute destination, and an interpreter form
that accepts any executable and any extension without reasoning about whether
the two could ever coincide.

Not exercised, and not claimed. The x86 image in the same tree was not
executed; it shares the release artefacts, the configuration model and the
console templates and is characterised from them rather than from a
detonation. The Windows build of the same version was not obtained for
testing, the paid X2 edition was not tested, and earlier 2.x versions were not
tested. Only this version was tested. The effect of enabling the `antihack`
protection system on this payload was not measured, so it is listed as a
hardening measure and never claimed as a tested mitigation. The chain's
behaviour against a target with active log rotation, a log destination on a
different filesystem, or a document root the server user cannot write was not
measured. No privilege escalation beyond the server process identity was
attempted or claimed - the product is a single-process C server model with no
setuid helper chain observed - and no cross-host movement, container escape or
sandbox escape is claimed. The parser memory-safety family (chunked transfer
state machine, multipart ranges, `Expect` handling, HTTP/2 glue) was not
fuzzed; it is named for the vendor as unexhausted surface and as a hardening
target, not as a finding.

The privilege ceiling, stated exactly. The achieved identity is arbitrary
command execution as the identity of the server process, and that identity is
deployment-dependent. `uid=0(root) gid=0(root) groups=0(root)` was observed on
the verified instance, where the marker file was 43 bytes and root-owned. Root
was reached because that instance was started with the product's own
`--allow-root` CLI flag from a working copy of the release tree on a host
where the process was launched by a privileged user. Root execution is not
established as the shipped default on Linux, and no such claim is made.
Separating the three levels of evidence: it is artefact-backed that
`--allow-root` exists in the product's CLI family, that the process accepted
it, and that the resulting CGI child ran with that identity; it is
characterised but not artefact-proved that a dedicated low-privilege `abyss`
account is intended for production deployments, because no shipped artefact -
install script, init unit file, or a user-setting node in the generated
default configuration - proves that is the default runtime identity; and it is
a structural argument rather than a measurement that the default port-80 bind
implies the process starts with root privileges or holds
`CAP_NET_BIND_SERVICE`, since on Linux binding below 1024 requires one of the
two, and whether privileges are then dropped, and to which identity, depends
on how the deployment starts the server - a systemd unit with `User=`, an init
script, a manual start, the vendor's autostart setup routine, or an
`--allow-root` start. A stock deployment that simply runs the binary with
enough privilege to bind port 80 and never drops is a realistic shape, not an
exotic one. No CVSS metric was raised or lowered on account of any of this:
either identity yields full compromise of the product, its configuration, its
keys and its served content, and root additionally yields the host. Impact
C/I/A are all High on that basis and the rating does not depend on resolving
the question.

Detection notes, including the reasons detection is weak by construction.
Every request in the chain is legitimate documented product usage: a setup
form, ordinary authenticated configuration submissions with the same content
type and shape as any administrator session, the product's own restart button,
a `404` with an unusual `User-Agent` among many `404`s, and an anonymous `500`
on a document-root file. There is also no meaningful host-based anomaly,
because the executed command is a child of the server process, its parent is
the product's own CGI machinery, and no new binary, new port or new account is
required. The signals that do exist, in order of value: `cgi.log` entries
naming a non-script file, which is the server telling you a shell was invoked
on a log file, together with the shell's own `line N: ... command not found`
diagnostics, the highest-fidelity indicator available; `500` responses for
paths ending in a data extension such as `.log`, `.txt`, `.tmp` or `.bak`,
since the product answers `500` whenever a bound interpreter fails to produce
CGI headers; configuration drift, namely a `<paths><path><vpath>/</vpath>`
entry, an interpreter whose `<file>` is a shell, or a `<log><file>` pointing
inside the document root, each of which should fail an audit on its own
merits; an administrator credential nobody recognises, or a legitimate
administrator receiving `401` on first visit to a freshly deployed instance;
access-log lines whose `User-Agent` contains shell metacharacters - `";`, `#`,
`$(`, redirections - which are rare in benign traffic and are the payload's
signature; and repeated unauthenticated `GET /console/` probes from many
hosts, which indicate window surveying. Status code alone is not a usable
detector for the takeover itself, because a minted credential produces no
failed login, no session creation and no distinguishable log entry.

Mitigation summary, ordered so that the first item alone closes this chain.
Refuse the colliding pair at configuration-commit time: cross-check every
`<interpreters><interpreter><ext>` value against every host `<log><file>`
extension and against the product's own known output extensions, and refuse
the write with a specific error rather than committing it and letting the
intersection execute - this single check removes the mechanism that turns the
log into an executable path. Refuse to bind a general-purpose shell as an
interpreter for a data extension, requiring an explicit separately confirmed
override for `/bin/sh`, `/bin/bash`, `/bin/dash`, `cmd.exe`, `powershell` and
equivalents, since a data extension bound to a shell has no legitimate purpose
in a web server's own configuration. Do not let one console form write an
arbitrary vpath-to-interpreter pair: validate the scripting subtree as a unit
and require explicit acknowledgement for a CGI script-path scope of `/`, which
is scope-wide by construction and should not be a value an ordinary form
silently accepts. Constrain the log destination and the file browser:
canonicalise console-supplied paths, resolve them against the server's own
root, reject destinations inside the document root, inside any CGI script path
or outside an allowed log directory, and restrict the console browser to the
same allowed subtrees instead of enumerating from the filesystem root. Make
console credential creation unreachable from the network by generating the
initial credential locally at first start - a random password written into the
configuration with root-only permissions and printed to the startup terminal,
or a forced local or installer-bound setup - so the credential exists before
the console listener can serve a creation form; if a network-visible creation
step is kept, bind it to loopback or require a one-time setup token read from
the local filesystem. Narrow the console's default exposure by binding
`127.0.0.1`, treating non-loopback binding as an explicit opt-in with a
warning, replacing the hardcoded RFC1918-wide and IPv6 ULA/link-local allow
ranges with loopback-only plus per-range operator opt-in, and withdrawing the
documentation's "recommended" framing of an all-interface console bind. Add a
same-site CSRF token to every mutating console form. Enforce privilege
separation by running the request-serving worker under a dedicated
unprivileged account regardless of how the process was started, reserving root
for the low-port bind or using `CAP_NET_BIND_SERVICE`, documenting the runtime
identity so operators can audit it, and making `--allow-root` warn on use.
Modernise credential handling as defence in depth with a memory-hard or
bcrypt-class digest and with rate limiting and lockout on console Basic
authentication. And add a regression test: commit a configuration with a
`log`-extension log destination inside the document root, an interpreter
binding `ext=log` to `/bin/sh` and a `<vpath>` of `/`, assert that the commit
is rejected naming the collision, assert separately that requesting such a log
path returns its content as `200` `text/plain` rather than `500` with a
`CGI: [/bin/sh ...]` entry in `cgi.log`, and assert from a first-run
configuration that the credential already exists before the console listener
accepts its first connection. Note that fixing in the reverse order, starting
with the log writer or with header filtering, would not break this chain.

Until a patch exists operators can close it themselves: arm the console in the
same automation step that installs the server and never redeploy from a
pristine configuration without arming it; bind or firewall the console port to
loopback or named management addresses and never publish it through NAT or a
same-host reverse proxy without authentication in front; audit for the three
collision signals named above; watch `cgi.log` for shells invoked on data
files; run the server unprivileged without `--allow-root`, using a bind
capability, a port redirect or a socket handed in by the init system on port
80; and re-arm from the local filesystem any deployment whose arming history
is unknown, since a minted credential cannot be told apart from a legitimate
one - which is also the recovery the product documents.

The proof of concept is published in the repository at
product/aprelium/abyss-web-server-x1-2.16.22.0/firstrun-console-takeover-conf-to-cgi-rce/exploit/aprelium_abyss_web_server_firstrun_console_takeover_conf_to_cgi_root_rce.py
and uses the Python standard library only. Target, both ports, the credential
to mint, CGI script-path scope, log format and destination, log URI,
smuggling and control paths, interpreter, extension, command, marker path,
every timeout and poll count are all command-line arguments with loopback or
neutral placeholder defaults, and the tool writes a credential and changes the
target's configuration rather than merely probing it. Beyond the default
mode, which runs window probe, credential creation, arming-gate assertion,
configuration write, restart, `User-Agent` smuggling, interpreter trigger,
marker report, negative control and a determinism re-trigger, it offers
`--probe` to check only whether the first-run window is open, changing nothing
and exiting non-zero when the console is already armed; `--gate` to assert
only that the credential endpoint refuses with `401` and a Basic realm once a
credential exists; and `--control` to run only the negative control against an
already configured instance. Safety rails retained from the verification work
are a loopback-only target unless `--allow-remote` is given explicitly, a
non-destructive marker command by default, no callback listener, no reverse
shell, no persistence mechanism, and cleanup that removes only files the run
itself created.

Full technical analysis and a reproducible proof-of-concept:
  https://0day-rubbish.com/blog/aprelium-abyss-web-server-firstrun-console-takeover-conf-to-cgi-rce

Project archive (ongoing disclosure series):
  https://github.com/Exploit-Garbage/0day-Rubbish

Aprelium has been notified through the Standard Technical Support mailbox it
publishes and documents for all users of both the free and the commercial
edition of this product, with the same technical content and the same
verification boundary as this post. The vendor publishes no RFC 9116
security.txt and no product security page, and its legal, privacy and support
policy pages contain no vulnerability-reporting language at all, so there was
no security-specific channel for us to use and no pre-publication form to
submit to. The notification requests nothing in return and asks for no
permission: this disclosure is unconditional, the research is already public,
no embargo was offered or accepted, and nothing is being withheld pending a
reply. No vulnerability identifier has been assigned to this finding yet.

--
0day Rubbish Research Team
disclosure () 0day-rubbish com
https://0day-rubbish.com
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/


Current thread: