Overview
Eloquia is a Windows-based Hack The Box machine built around two Django applications, a vulnerable OAuth account-linking flow, and a custom Windows service with an unsafe executable ACL.
The route from the web applications to SYSTEM was:
- discover the Eloquia and Qooqle virtual hosts;
- abuse an OAuth callback that omitted
state and allowed identity replacement;
- force an authenticated reviewer through that callback using stored HTML navigation;
- use Django admin and SQL Explorer to load a native SQLite extension;
- recover a saved credential from Edge using DPAPI and AES-GCM;
- obtain a primary process under the recovered user;
- race a periodically restarted service whose executable was writable by that user.
Reconnaissance
The target exposed IIS over HTTP and Windows Remote Management:
80/tcp Microsoft-IIS/10.0
5985/tcp Microsoft HTTPAPI/2.0
The initial redirect identified eloquia.htb. Following the site's OAuth flow exposed a second virtual host, qooqle.htb.
Eloquia was a Django article platform. Qooqle was a separate Django application acting as its OAuth provider. That separation immediately made the trust boundary between the two applications the most interesting part of the attack surface.
OAuth account linking to administrator takeover
I registered an account on each application and created a Qooqle OAuth application using Eloquia's callback URL:
http://eloquia.htb/accounts/oauth2/qooqle/callback/
Two details made the flow exploitable.
First, authorization requests did not use a state value. That removed the normal binding between the browser session that initiated authorization and the callback that completed it.
Second, visiting the callback while already authenticated did not reject the operation or require confirmation. Instead, it replaced the Qooqle identity linked to the current Eloquia account. I confirmed this behavior against my own test account before involving the reviewer.
Delivering a fresh authorization code
Articles accepted a stored <meta http-equiv="refresh">, and reported articles were reviewed by an authenticated privileged user. A static authorization code was not reliable because Qooqle codes expired after roughly 15 seconds.
I therefore used an attacker-controlled redirect endpoint. When requested, it generated a fresh authorization code for my Qooqle identity and immediately redirected the reviewer to:
http://eloquia.htb/accounts/oauth2/qooqle/callback/?code=<fresh-code>
The reported article contained the equivalent of:
<meta http-equiv="refresh" content="0;url=http://<redacted-lab-host>/oauth">
The reviewer loaded the article, followed the redirect, and completed the callback inside its existing Eloquia session. Its privileged Eloquia account was consequently relinked to my Qooqle identity.
Authenticating to Eloquia through that identity then returned the admin profile and exposed Django admin. This was not a conventional OAuth login bypass; it was an account-linking CSRF that replaced the external identity attached to an already authenticated privileged account.
SQL Explorer to native code execution
Django admin exposed SQL Explorer at:
/dev/sql-explorer/play/
A version query identified SQLite 3.45.1:
SELECT sqlite_version() AS version;
The database path was visible through SQLite's pragma interface:
SELECT * FROM pragma_database_list;
-- C:\Web\Eloquia\db.sqlite3
The critical finding was that SQLite extension loading was enabled. Querying a nonexistent DLL reached the loader instead of returning not authorized:
SELECT load_extension('C:\nonexistent\test.dll');
That error distinction proved that SQL Explorer could reach SQLite's native extension mechanism.
Crossing the upload boundary
The public article form validated banner uploads as images. The Django admin form for the same model, however, accepted an arbitrary file. This created a useful mismatch: the application exposed a writable path through admin while SQL Explorer provided a native DLL loader.
I compiled a small x64 SQLite extension exporting sqlite3_extension_init, gave it an image-style filename, and uploaded it through the administrative article form. The server stored the bytes unchanged beneath:
C:\Web\Eloquia\static\assets\images\blog\<uploaded-file>
Loading that local file through SQLite executed the extension:
SELECT load_extension(
'C:\Web\Eloquia\static\assets\images\blog\<uploaded-file>'
);
The resulting callback executed as the local eloquia\web account.
The important chain was not merely "upload a DLL." It required three independent weaknesses to line up:
- privileged access to an alternate upload form;
- unchanged storage of attacker-controlled bytes at a predictable local path;
- SQLite extension loading exposed through an administrative query interface.
Recovering a saved credential from Edge
The next useful secret was stored in the Edge profile belonging to the same Windows user.
Chromium-based browsers on Windows protect saved credentials in two stages:
- the browser's
Local State file stores an AES key wrapped with DPAPI;
- the
Login Data SQLite database stores individual password values encrypted with AES-GCM.
Because my code was already executing under the live web token, CryptUnprotectData could unwrap the profile's AES key. I then used that key to decrypt the relevant AES-GCM entry from Default\Login Data.
The recovered account was:
User: Olivia.KAT
Pass: <redacted>
Olivia.KAT belonged to Remote Management Users. WinRM accepted the NTLM credential but refused shell creation in this instance, so I treated the credential as a local privilege boundary rather than relying on remote management.
Privilege escalation through Failure2Ban
The host ran a custom Failure2Ban Windows service from a Visual Studio debug directory:
C:\Program Files\Qooqle IPS Software\Failure2Ban - Prototype\Failure2Ban\bin\Debug\Failure2Ban.exe
Source code and service logs showed that a cleanup job restarted the service approximately every five minutes. The executable's ACL granted Olivia.KAT write access even though the service ran as SYSTEM:
ELOQUIA\Olivia.KAT:(I)(RX,W)
NT AUTHORITY\SYSTEM:(I)(F)
This is the classic writable service-binary condition with one complication: the executable remained locked while the service was running.
I prepared a minimal replacement executable that performed a harmless proof of SYSTEM-level file access. The replacement contained no general-purpose shell or persistence behavior.
Obtaining an Olivia primary process
WinRM would authenticate Olivia but would not create a shell. To obtain a normal primary process under that identity, I loaded a second SQLite extension as web. It used CreateProcessWithLogonW with the recovered credential to start the installed, allow-listed Python interpreter:
"C:\Program Files\Python311\python.exe" C:\Web\Eloquia\static\assets\images\blog\<redacted-script>
The Python process repeatedly attempted to open the service executable for writing and replace it. While the service was live, Windows returned sharing violations or PermissionError. During the next scheduled cleanup window, the service stopped briefly and the replacement succeeded after tens of thousands of attempts.
The cleanup job then restarted the replaced executable as SYSTEM, completing the privilege escalation.
Defensive takeaways
The most important fixes are architectural:
- require and validate OAuth
state, bind callbacks to a server-side initiation record, and expire that record after one use;
- require explicit reauthentication and confirmation before replacing a linked identity;
- sanitize stored HTML and prohibit browser-navigation primitives in user content;
- apply identical upload validation and storage rules across public and administrative forms;
- disable SQLite extension loading unless it is explicitly required;
- remove general query consoles from production administrative interfaces;
- treat browser profiles as credential stores and minimize opportunities for untrusted code to run under interactive user tokens;
- install service binaries in protected release directories, audit their ACLs, and never run production services from writable debug trees.
Eloquia was a strong example of how several individually understandable weaknesses can combine into a complete Windows compromise. The decisive step at each stage was identifying which security boundary the current primitive could cross next.