~ / blog / juice-shop-writeup

OWASP Juice Shop
CTF Write-Up

A practical walkthrough of OWASP Juice Shop, covering challenge discovery, exploitation paths, and the use of Burp Suite against a modern training target.

What This Covers

A challenge-driven walkthrough of Juice Shop findings, from discovery and DOM XSS to coupon leaks and SQL injection login bypasses.

Target / Lab

OWASP Juice Shop in Docker

Tools Used
Burp SuiteBrowser DevToolsDOM payloadsSQLite-focused SQLi
Key Takeaways
  • Treat Juice Shop like a real application and let discovery guide the path.
  • Client-side flaws, business logic issues, and SQLi all appear in one realistic target.
  • Documenting the reasoning behind each solved challenge is as useful as the payload itself.
// challenges solved 7
// total available 173
Score Board DOM XSS Bonus Payload Chatbot Coupon Login Jim Login Bender Admin SQLi
// contents
  1. What Is OWASP Juice Shop?
  2. Challenge 1 — Finding the Score Board
  3. Challenge 2 — DOM XSS
  4. Challenge 3 — Bonus Payload (XSS 2.0)
  5. Challenge 4 — Chatbot Coupon Extraction
  6. Challenge 5 — Login as Jim (SQLi Auth Bypass)
  7. Challenge 6 — Login as Bender (SQLi Auth Bypass)
  8. Challenge 7 — Admin Login via SQLi
  9. Recap

What Is OWASP Juice Shop?

OWASP Juice Shop is an intentionally insecure web application maintained by the Open Web Application Security Project. It simulates a real e-commerce storefront — with products, a shopping cart, user accounts, and a support chatbot — but the entire codebase is riddled with real, exploitable vulnerabilities covering the OWASP Top 10 and beyond.

What makes it unique compared to something like PortSwigger's labs is that you have to find the vulnerabilities yourself. The Score Board (which is itself a hidden challenge) lists all 173 challenges, but the app gives no indication of where attack surfaces are. You explore, poke at endpoints, read JavaScript source, observe error messages — the same reconnaissance process you'd run against a real target.

I'm running Juice Shop locally on 127.0.0.1:3000 with Burp Suite proxying all traffic on port 8080. All requests in the screenshots go through Burp so I can inspect, modify, and replay them freely.

ℹ️ Setup

Juice Shop is easiest to run via Docker: docker run -p 3000:3000 bkimminich/juice-shop. Then point your browser to http://localhost:3000 and configure Burp Suite as a proxy. The app resets on container restart, so your progress clears — keep notes.


01
★☆☆☆☆ — 1 Star

Finding the Hidden Score Board

Category: Security through Obscurity
Goal: Navigate to the Score Board page, which is intentionally hidden from the UI

Approach

The Juice Shop UI has no link to the Score Board — it's deliberately omitted from the navigation. But the app is a single-page Angular application, which means all the route definitions live in the compiled JavaScript bundle that gets sent to the browser on first load. If the route /score-board exists in the app, it has to be declared somewhere in that bundle.

The approach is to view the source of main.js — the primary compiled JS bundle — and search for keywords like "score". Since the JavaScript is minified, it's a wall of text, but the browser's built-in search (Ctrl+F) works fine on the raw source.

1
Navigate to main.js

Open view-source:http://127.0.0.1:3000/main.js directly in the browser to get the raw, searchable source of the compiled Angular bundle.

2
Search for "score"

Use Ctrl+F to search the raw source. Among the matches, you'll find /score-board referenced in a router.navigate() call.

3
Visit the route

Navigate to http://127.0.0.1:3000/#/score-board. The challenge triggers automatically on page load.

Score board URL found in main.js source
Searching 'score' in main.js reveals the /score-board route — 4 of 72 matches shown
Score board page after first solve
The Score Board itself after solving the first challenge — 1/173 solved, showing all available categories
🧠
What this teaches
Client-side routing in SPAs is not a security boundary. Any route registered in an Angular, React, or Vue router is visible in the compiled bundle, regardless of whether the UI exposes a link to it. This is a classic example of security through obscurity failing — hiding a link is not the same as protecting the resource. Always enforce authorization server-side.

02
★☆☆☆☆ — 1 Star

DOM XSS via Search Field

Category: XSS
Goal: Perform a DOM XSS attack using <iframe src="javascript:alert('xss')">

What is DOM XSS?

Cross-Site Scripting (XSS) occurs when an attacker can inject and execute JavaScript in the context of another user's browser session. DOM XSS specifically refers to cases where the vulnerability exists entirely in client-side JavaScript — the payload never touches the server. The page reads attacker-controlled data from the URL or storage and writes it to the DOM without sanitization.

In Juice Shop, the search field passes the query parameter directly into a DOM element's innerHTML (or similar sink) without escaping. This means any HTML injected into the search query gets interpreted and rendered as actual markup — including <script> tags, event handlers, and <iframe> elements with javascript URIs.

search URL — payload in q= parameter http://127.0.0.1:3000/#/search?q=<iframe src%3D"javascript:alert(`xss`)">

The %3D is URL-encoded =. Some browsers or WAFs will reject a raw = inside a query value, but the encoded form passes through and gets decoded by the browser before the Angular component processes it. The result: the iframe element is created, its src is a javascript URI, and the browser executes alert('xss').

DOM XSS alert executing in Juice Shop
The iframe's javascript: URI fires an alert — Juice Shop confirms the DOM XSS challenge as solved
⚠️ javascript: URIs

The javascript: URI scheme is a legacy feature that evaluates JavaScript when used as a navigation target or iframe src. Modern browsers have increasingly restricted it, but it still works in many contexts. The real-world attack would be crafting a link that a victim clicks — the alert() here is just the proof of concept; a real payload would steal session cookies or perform actions as the victim.

🧠
What this teaches
Never write user-supplied data into the DOM via innerHTML, document.write(), or similar sinks. Use textContent for plain text (which is always safe), and if you must render HTML, sanitize with a library like DOMPurify. Angular has built-in sanitization but it can be bypassed when bypassSecurityTrustHtml() or similar trust-override APIs are used.

03
★☆☆☆☆ — 1 Star

Bonus Payload — SoundCloud iframe XSS

Category: XSS
Goal: Use the specific bonus payload — a SoundCloud embed iframe — in the same DOM XSS vector

Same vector, different payload

This challenge reuses the exact same DOM XSS vulnerability from Challenge 2 but requires you to inject a specific, more complex payload: a fully-formed SoundCloud player embed iframe. The point is to demonstrate that the injection isn't limited to simple alert() calls — you can inject arbitrary, functional HTML and have it render and execute in the victim's browser.

The payload is a real SoundCloud embed iframe that would actually load and play music if the browser allows it. In a real attack, this could be used to deface a page, phish users by injecting fake login forms, or load malicious external resources.

html bonus payload (URL-encoded for q= param)
<iframe width="100%" height="166" scrolling="no" frameborder="no"
  allow="autoplay"
  src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/771984076
  &color=%23ff5500&auto_play=true&hide_related=false&show_comments=true
  &show_user=true&show_reposts=false&show_teaser=true"></iframe>
Bonus payload SoundCloud iframe injected into search
The SoundCloud player renders inside the search results — Juice Shop awards the Bonus Payload challenge
🧠
What this teaches
XSS payloads aren't limited to alert() demos. Any HTML, including external embeds, forms, scripts, and media, can be injected. This challenge drives home that DOM XSS is a real content injection vulnerability — not just a JavaScript execution quirk. A CSP (Content Security Policy) header restricting frame-src and script-src would mitigate this class of injection even if the XSS sink itself remained.

04
★☆☆☆☆ — 1 Star

Extracting a Coupon Code from the Chatbot

Category: Broken Anti Automation / Miscellaneous
Goal: Obtain a valid coupon code by repeatedly prompting the support chatbot

Social engineering a bot

Juice Shop has a support chatbot at /#/chatbot. Like many chatbots, it's built to be helpful — and that helpfulness can be exploited. The bot has hardcoded responses for certain keywords, and with enough prompting, it will reveal a discount coupon code.

The method is simple: persistently ask for a "coupon" in the chat window. The bot resists a couple of times, deflecting with excuses about the CFO and suggesting a Deluxe membership instead. But after asking three times in a row, it capitulates and hands over the code.

Chatbot revealing coupon code after repeated requests
After three "coupon" messages, the bot relents: k#pDmhz3Tq — a 10% discount code
ℹ️ The extracted code

The chatbot reveals the coupon code k#pDmhz3Tq — a 10% discount. In a real system, coupon codes should never be stored in plaintext within chatbot response logic, and chatbots should have rate limiting to prevent repeated probing. "Nagging" attacks like this are a simple form of social engineering that exploits the bot's scripted helpfulness.

🧠
What this teaches
Automated chatbots that handle sensitive data need rate limiting, anomaly detection, and careful response scripting. Any information a bot is willing to provide after N retries can be extracted by a script in seconds. This is relevant for real production chatbots — customer service bots that reveal order details, account info, or promotions are a common attack surface.

05
★★★☆☆ — 3 Stars

Login as Jim via SQL Injection

Category: Injection
Goal: Log in as the user Jim without knowing his password

The vulnerability

The login endpoint builds its authentication query by directly concatenating the user-supplied email into a SQL string — the same class of vulnerability as the PortSwigger login bypass labs. The key difference here is you need to know (or guess) a valid username/email first.

The Juice Shop product listing page reveals that one of the products has a review by "jim@juice-sh.op" — enough to know Jim is a registered user. From there, the SQLi auth bypass is identical to Lab 2 in the PortSwigger series: inject into the email field to comment out the password check entirely.

original login query SELECT * FROM Users WHERE email = 'jim@juice-sh.op' AND password = '...'

after injection — email field: jim@juice-sh.op' -- SELECT * FROM Users WHERE email = 'jim@juice-sh.op' --' AND password = '123'

The single quote closes the email string value. The double dash -- comments out the remainder of the query, including the AND password = '...' check. The database finds the row for Jim and returns it — authentication succeeds without ever validating the password.

Login form with jim@juice-sh.op' -- injected
Email field contains jim@juice-sh.op' -- — the password value is irrelevant
🧠
What this teaches
Username enumeration from publicly visible user-generated content (reviews, comments, forum posts) is a real recon technique. Once you have a valid email, SQLi auth bypass is trivial if the backend isn't using parameterized queries. The fix is always the same: use prepared statements / parameterized queries — never concatenate user input into SQL strings.

06
★★★☆☆ — 3 Stars

Login as Bender via SQL Injection

Category: Injection
Goal: Log in as the user Bender without knowing his password

Identical technique, different target

This challenge is structurally identical to Challenge 5 — SQLi auth bypass via comment injection into the email field. The only difference is the target user: Bender, whose email "bender@juice-sh.op" can also be found in product reviews within the application.

Both Jim and Bender are pop culture references (Star Trek and Futurama respectively) that Juice Shop uses to add personality to its fake user database. Their emails are intentionally discoverable through normal app browsing — teaching you to treat any disclosed user identifier as a potential entry point.

Login form with bender@juice-sh.op' -- injected
Same technique: bender@juice-sh.op' -- in the email field, password doesn't matter
🧠
What this teaches
When a single class of vulnerability (SQLi auth bypass) is applicable to every user account in the system, the impact is total account compromise — not just one user. This is why fixing the root cause (parameterized queries) matters more than trying to protect individual accounts. Any user whose email is visible anywhere in the app becomes a target.

07
★★★☆☆ — 3 Stars

Admin Login & Search SQLi via Burp Suite

Category: Injection
Goal: Log in as admin using 'or 1=1--, and explore the search endpoint's SQLi vulnerability using Burp Suite

Part 1 — Provoking errors in Burp

Before attempting the login bypass, I explored other injection surfaces using Burp Suite's Repeater. The product search endpoint at /rest/products/search?q= is a good candidate — it takes user input and almost certainly queries the database for matching products.

Appending a single quote to the search term (apple') immediately reveals a verbose error response — the server returns a 500 with a full SQLite error including the raw SQL query. This is a critical information disclosure: you can see exactly how the query is constructed, which tells you the injection syntax you need.

Burp Suite showing /rest/admin 500 error
POST to /rest/admin with the avsl@gmail credentials — an "Unexpected path" error leaks the full Express.js stack trace
Burp Suite showing SQLite error from search endpoint
Adding a single quote to the search query triggers an SQLITE_ERROR — the full query is exposed in the error response

The error message reveals the underlying query:

leaked query from error response SELECT * FROM Products WHERE ((name LIKE '%apple'%' OR description LIKE '%apple'%') AND deletedAt IS NULL) ORDER BY name

You can now see there are two nested parentheses wrapping the WHERE condition. To escape the string and comment out the rest, you need to close both — the correct payload is apple')) followed by --.

Part 2 — Confirming and exploiting the search SQLi

Using the information from the error, the payload apple'))-- closes the name LIKE clause, closes both parentheses, then comments out the remainder. The server returns a 200 with "data":[] — no products found (expected, since "apple))" doesn't match anything), but more importantly, no error. The injection is working.

Burp Suite showing 200 OK after fixing the injection syntax
apple'))-- returns 200 OK with empty data — injection is confirmed, query executes cleanly

From here, a UNION SELECT can be used to dump database schema information. The query below pulls from sqlite_master — SQLite's internal table that stores the CREATE TABLE statements for every table in the database. With 9 columns in the original query, we UNION a matching 9-column select:

sql schema dump via UNION
apple'))+UNION+SELECT+sql,2,3,4,5,6,7,8,9+FROM+sqlite_master--
Burp Suite UNION SELECT dumping sqlite_master
UNION SELECT from sqlite_master returns the full CREATE TABLE statements — including Addresses, Users, and other tables

Part 3 — Admin login bypass

With the database structure confirmed, the final step is the admin login. Since we know the app uses SQLi-vulnerable login (demonstrated in Challenges 5 and 6), the classic ' or 1=1-- payload in the email field logs in as the first user in the database — which in Juice Shop is always the admin account.

Login form with 'or 1=1-- payload
'or 1=1-- in the email field — the always-true condition returns the first user row, which is admin
⚠️ Verbose error disclosure

The most dangerous thing in this challenge isn't the SQLi itself — it's the verbose error response that hands you the full query structure. Without that, you'd be blind-guessing the parenthesis depth. In production, database errors should never be returned to the client. A generic "Something went wrong" response forces an attacker to probe blind, which is significantly slower and noisier.

ℹ️ sqlite_master

SQLite stores its schema in a special table called sqlite_master (or sqlite_schema in newer versions). The sql column contains the full CREATE TABLE statement for each object. This is the SQLite equivalent of querying information_schema.tables in MySQL/PostgreSQL, or all_tables in Oracle — it gives you a complete blueprint of the database structure in a single query.

Script breakdown — key payload decisions

payload elementwhy it's needed
apple')) The original query wraps the search term in double parentheses: ((name LIKE '%q%' OR ...). A single closing paren leaves the outer one unclosed — syntax error. Two closing parens )) closes both, leaving a valid query stub before our comment.
UNION SELECT sql,2,3,...,9 The original SELECT returns 9 columns (id, name, description, price, deluxePrice, image, createdAt, updatedAt, deletedAt). The UNION must match exactly. Filling positions 2–9 with integer literals satisfies the column count without needing to know the actual column types of sqlite_master.
FROM sqlite_master SQLite-specific metadata table. Returns one row per database object (tables, indexes, views). The sql column in position 1 contains the CREATE statement — which we see in the name field of the JSON response.
'or 1=1-- For the login: the leading ' closes the email string, or 1=1 makes the WHERE clause always true, -- comments out the rest including the password check. The first matching row (admin) is returned.
🧠
What this teaches
This challenge bundles three separate lessons: (1) verbose error messages are a critical info disclosure — they reveal query structure that an attacker can use to craft working payloads; (2) SQLite's sqlite_master is as exploitable as information_schema in MySQL/PostgreSQL — always consider the database type when enumerating; (3) the ' or 1=1-- login bypass is so fundamental that it should be tested on every login form during a pentest, every time.

Recap — What I've Learned So Far

Seven challenges in and the pattern is clear: Juice Shop rewards methodical reconnaissance and the same fundamental techniques seen in controlled lab environments — but applied in a more realistic, self-directed context.

Source code recon

SPA bundles expose routes, endpoints, and internal logic. view-source: + search is often the fastest way to map an Angular/React app's attack surface.

DOM XSS

Any URL parameter written to innerHTML without sanitization is a DOM XSS sink. CSP and safe DOM APIs (textContent) are the fix.

Social engineering bots

Chatbots with hardcoded responses can be probed to reveal sensitive data. Rate limiting and response auditing are essential for any bot handling business logic.

SQLi auth bypass

' -- and ' or 1=1-- remain viable against any login endpoint not using parameterized queries. Username enumeration from public content makes targeted bypasses easy.

Verbose error exploitation

Raw SQL errors in HTTP responses hand you the query structure. Turn off verbose errors in production — always. A generic error message makes blind injection significantly harder.

SQLite schema enumeration

sqlite_master is the SQLite equivalent of information_schema. Knowing your target's database type determines which metadata tables are available for schema enumeration.

✅ Up next

The next set of challenges will go deeper into broken access control, IDOR vulnerabilities, JWT manipulation, and the more complex injection scenarios in Juice Shop's Practitioner and Expert tiers. The Score Board is now visible — the map is open, everything is fair game.

Move through the archive

Browse all posts
Older post PortSwigger SQLi Labs Write-Up 2026-04-02 . ~45 min read

Related posts

Posts that overlap with the same tools, techniques, or target areas.

← Back to blog