HTB CWES study materials
HTB CWES study resources

Time-Based Oracle Behind a Caching Layer and Three IIS Nodes

HTB CWES · Put your knowledge to work in context.

All case studies

Time-Based Oracle Behind a Caching Layer and Three IIS Nodes

During an external assessment of a retail storefront running ASP.NET on IIS 10 with SQL Server 2019, you find that the sort parameter on /catalog/search is concatenated into an ORDER BY clause. A single quote returns HTTP 500; two quotes return 200. An error-based payload using CONVERT(int, @@version) is blocked by the WAF with a 403, but CASE WHEN payloads return 200 consistently. Time-based tests with WAITFOR DELAY '0:0:5' are unreliable: identical requests sometimes take 0.2 s, sometimes 5.2 s, sometimes 10.1 s. Response headers show X-Cache: HIT on roughly 60% of responses and Via: varnish. The application sits behind three IIS nodes, and the load balancer offers no session affinity; the only cookie is ARRAffinity, which the LB ignores. Your goal is to extract the admin password hash from dbo.Users where RoleID = 1. The WAF blocks UNION, xp_, and OPENROWSET. The client authorized read-only proof within a 2-hour window.

1. Why are the time-based results inconsistent, and what measurement strategy would give reliable boolean inference?

Read the worked answer

Three factors interact. X-Cache: HIT means Varnish answered without touching IIS, so a 5 s WAITFOR never ran; that explains the 0.2 s samples. On a MISS the request lands on one of three IIS nodes with no affinity, so the same payload can execute once or, if the LB retries after a timeout, more than once - hence 5.2 s and 10.1 s. Timing is a noisy, bimodal channel, not a clean delay oracle.

The better oracle is already available: the injection point is ORDER BY, so a CASE expression changes the result order deterministically. For example, sort=CASE WHEN (<condition>) THEN price ELSE name END returns a different first page for true vs false. Add a cache-buster to force a MISS and always compare the full ordered ID list, not just the first row. Validate with known-true (1=1) and known-false (1=2) controls, then run each test twice with different cache-busters. If timing must be used, send five samples per condition, use a 2 s delay, and compare medians while discarding HITs.

2. Given the WAF blocks UNION and stacked queries are not confirmed, how do you extract the hash? Compare boolean inference, OOB, and error-based, and state the next request.

Read the worked answer

UNION and stacked queries are blocked, and OOB DNS is unavailable because xp_dirtree is on the blocked list. Error-based extraction through CONVERT is also blocked. Boolean inference over the ORDER BY oracle is the reliable path.

The hash is 64 hex characters, so 32 bytes. For each character position, binary-search the ASCII value with CASE WHEN (SELECT ASCII(SUBSTRING(password,<pos>,1)) FROM dbo.Users WHERE RoleID=1) > <mid> THEN price ELSE name END. That is roughly six requests per character, about 384 requests total - feasible in the window. Use a fresh cache-buster per request and compare ordered result IDs; do not rely on HTTP status.

The next request should be the known-true control with a unique cache-buster: sort=CASE WHEN (SELECT COUNT(*) FROM dbo.Users WHERE RoleID=1)=1 THEN price ELSE name END&cb=<random>, followed by the 1=2 control. Only after both behave as expected should character extraction begin.

3. How do you confirm the hash belongs to an admin and demonstrate impact without cracking it?

Read the worked answer

Extract username, RoleID, and the hash for the admin row, and show that the same oracle can read @@version and DB_NAME(). That proves read access to the user table, not just to a version string. Confirm the hash format (SQL Server 0x0100... or a 64-hex SHA-256) and that it is associated with RoleID=1. You do not need the plaintext to demonstrate account-takeover risk when password hashes are exposed; offline cracking is a separate, often out-of-scope step.

Report as high or critical depending on whether the hash is crackable and how privileged the admin account is. The key evidence is the reproducible request/response pair and the exact row. Remediation: parameterize the ORDER BY with a server-side allowlist of column names and directions; a prepared statement cannot parameterize a column name, so an allowlist is the correct fix, not escaping. Add WAF coverage for header surfaces as defense in depth, but do not treat the WAF as the fix.