Scratching the Web

In my last post, I wrote about solving a CTF that required reading C code and learning about Linux file descriptors (FDs). In this one, I’m writing about when I tried OWASP Juice Shop, an intentionally vulnerable web app for training and learning.

These early challenges are mostly meant to make you familiar with the basics: HTTP requests, what the browser blocks versus what the server enforces, how client-side and server-side validation differ, and where simple mistakes show up in real apps.

I also expected these to be relatively easy for me since I already have a pretty decent foundation from doing development in the past.

Setting Up

Setting up OWASP Juice Shop was very simple. I just had to clone the repo:

git clone https://github.com/juice-shop/juice-shop.git --depth 1

Then run npm install and npm start. Yep, that’s it. Quite simple.

Poking Around

After Juice Shop was up and running, the homepage looked like a simple ecommerce product page. There was a search box for products, which caught my attention because I’ve heard those are a very common place to find XSS.

I tried the classic <script> alert(document.cookie) </script> and obviously it didn’t work. Couldn’t be that easy.

But I used to watch random videos about cybersecurity even before starting to learn it seriously, so I knew a few XSS payloads like <img src="unreachable" onerror="alert(document.cookie)"/>. I tried it and it worked. The popup appeared.

Random XSS Attempt

Still, I didn’t get any feedback like “Challenge Solved” or anything. As far as I remembered, the app is supposed to give you something like a flag when you trigger or find vulnerabilities.

That’s when I googled how I was supposed to play OWASP Juice Shop and found out it has a Score Board with a list of challenges. Finding the scoreboard itself is a challenge.

Let’s Find the Score Board

During my googling, I got a hint that I could find the URL to the scoreboard by analyzing the JS bundles. So I opened Developer Tools, went to the Debugger tab, and searched for score in the main.js bundle. After a few “next, next, next”, I found the route: /score-board.

When I visited it, I finally saw the scoreboard and the list of all challenges in the app. Score Board

Exploring the App

At this point, I had a list of challenges, but I wasn’t familiar with the app itself or its features. So I just browsed around: creating accounts, checking out, ordering, visiting settings, and all that.

After a few minutes of doing the basic things, I was pretty familiar with the app and its expected workflows.

Setting Up Goals

Now I had to set some clear goals for my first day of shopping, so I set a target: solve all easy-level (1-star) challenges.

There were 14 one-star challenges, and one of them was already solved (finding the scoreboard).

Clearing Up the Super Trivial Ones

Some of them looked super trivial, so I quickly cleared them up.

  1. Privacy Policy: Simply visiting the privacy policy page solves this one. Not technical at all, it’s just there to encourage the habit of reading privacy policies.
  2. DOM XSS: I already triggered a DOM XSS at the beginning using the img payload. This challenge expects you to trigger XSS using the iframe payload provided in the challenge description. Pasting the given iframe payload into the search box solves it.
  3. Bonus Payload: Basically the same as DOM XSS, just a different payload. This one plays a song about OWASP Juice Shop from SoundCloud. It’s fun.
  4. Mass Dispel: This required closing multiple “Challenge Solved” notifications at once. The application manual already mentioned that Shift + click closes multiple notifications, so this one was also very trivial.

Let’s Start for Real

Now I was ready to solve each one, one by one.

Bully Chatbot

This was labeled as: challenges which are not considered serious and/or realistic but exist more for entertainment.

There was a support chatbot, and I was expected to get a coupon code from it.

I was just randomly chatting without much thought, and it randomly gave me a coupon. Challenge solved. After all, like they said themselves, it wasn’t meant to be realistic, just for fun.

Confidential Document

This challenge expected me to get access to some confidential documents. I tried things for a while and didn’t find anything. When I used the first hint, it said: Analyze and tamper with links in the application that deliver a file directly.

That made me think about places in the app that deliver a file directly through a URL. After a bit of wandering, I found it: the order receipt. When an order is placed, you get an invoice and the URL looks like /ftp/<invoice-id>.pdf. Order Invoice

That immediately made me curious if I could reach /ftp/. When I visited that route, it landed me in a web directory with a bunch of files. I tried opening some of them and apparently I could only open PDF and MD files. Trying to open any other files resulted in some kind of error. Web Directory

Anyway, when I went back to the scoreboard, the Confidential Document challenge was solved. As a side effect of the errors from opening random files in that web directory, the Error Handling challenge also got solved at the same time. Solved

Takeaways

  • Direct file-serving endpoints and predictable paths can expose more than intended.
  • “Hidden” directories like /ftp/ are worth testing when URLs suggest them.
  • Error pages can leak behavior and sometimes solve an “Error Handling” style challenge by accident (😹).

Exposed Metrics

This was quite simple. The description itself pointed to the Prometheus GitHub repo, and after reading a bit about Prometheus, I learned this is the rough idea of how it works:

  1. The application exposes an HTTP route like /metrics.
  2. Prometheus polls this route at certain intervals to collect metrics.
  3. Prometheus stores the metrics in a time-series database.

If /metrics is exposed publicly, it can leak system information and metrics.

So I visited /metrics, saw the metrics, and the challenge was solved. Metrics

Takeaways

  • Observability endpoints like /metrics are great internally but risky when public.
  • A simple URL guess can be enough when defaults are left exposed.

Missing Encoding

This one expected me to Retrieve the photo of Bjoern’s cat in “melee combat-mode”.

Right after it mentioned a photo, I went to the Photo Wall page and the first image wasn’t rendered properly. When I inspected the image element in DevTools, the filename contained some weird characters:

ᓚᘏᗢ-#zatschi-#whoneedsfourlegs-1572600969477.jpg

Where is the cat?

Since the title already hinted it was about encoding, I URL-encoded the name and replaced the image URL with the encoded one:

%E1%93%9A%E1%98%8F%E1%97%A2-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg

Here is the cat.

Boom, challenge solved.

Outdated Allowlist

This required me to redirect users to one of their cryptocurrency addresses that is not promoted any longer.

Naturally, “cryptocurrency address” implies payment, so I went to the payment page after checking out a product. There was a list of alternative payment options. While inspecting their URLs, I noticed a route like /redirect?to=<URL>.

Since the challenge mentioned an address that isn’t promoted anymore, I guessed it might still exist in the JS bundle. I searched the main.js bundle for /redirect?to= and got a match:

redirect?to=https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm

Redirect It looked like a Bitcoin address that is no longer promoted. Visiting that route solved another challenge.

Takeaways

  • Frontend bundles often contain old constants and URLs that the UI no longer shows.
  • Searching JS bundles for patterns like redirect?to= can reveal a lot.

Web3 Sandbox

We were supposed to find a Web3 sandbox that was left accidentally. This also wasn’t reachable from the UI, so I went to the same main.js bundle and searched for sandbox. As expected, I found the route: web3-sandbox.

Sandbox When I visited /#/web3-sandbox, I could access the sandbox and the challenge was solved.

Takeaways

  • If a feature is “not in the UI”, it might still be deployed.

Zero Stars

The challenge was to give a zero-star rating, but the feedback form at /#/contact only allowed a minimum rating of 1 star.

But that restriction was only in the UI. What if I sent the request myself to the backend with the rating field set to 0?

Sending the request manually would mean handling auth headers and other things, so I used Burp Suite to intercept the request and changed the rating field to 0.

Zero Rating Burp

And boom, another challenge solved. Now only one one-star challenge left.

Zero Rating Solved

Takeaways

  • Client-side validation is just a suggestion unless the server enforces it.
  • Intercepting and editing requests is often enough to test trust boundaries.
  • Tools like Burp Suite make it easy to change one field and see what breaks.

Repetitive Registration

This challenge said to apply the DRY (Don’t Repeat Yourself) principle when registering. But the user registration form has both Password and Repeat Password fields.

If you enter different values, the UI obviously doesn’t let you submit the request. It disables the button. But again, what if I intercept the traffic in Burp Suite and change the password fields there?

Don’t Repeat Yourself

After changing the Repeat Password field to something else in Burp Suite, the user was still created successfully and the challenge was solved.

Don’t Repeat Yourself

Takeaways

  • UI checks do not matter if the backend does not validate the same rules.
  • Registration flows are a great place to test server-side validation.
  • “DRY” here is really about trust: the server should not trust duplicated client fields.