Test Date: July 2025
Principal Tester: Noah Tyson
This report contains sensitive information pertaining to the security posture, vulnerabilities, and infrastructure of the assessed environment. It is intended solely for the use of the client and designated stakeholders.
Unauthorized access, reproduction, distribution, or disclosure of this document or any of its contents is strictly prohibited without explicit written consent from the commissioning organization and the testing entity.
All findings, data, and recommendations contained herein must be handled in accordance with applicable information security and data protection policies. Misuse or negligent handling of this report could expose the organization to significant legal, reputational, or operational risk.
Between 10 July 2025 and 24 July 2025, FakeCompany performed an external, black-box web-application penetration test for VulnLawyers. The recommendations provided in this report are structured to facilitate remediation of the identified security risks. This document serves as a formal letter of attestation for the recent VulnLawyers company black-box penetration test.
Evaluation ratings are based on a comparison between findings from the engagement and recognized security industry standards. This document reflects our considered assessment of VulnLawyers' current security posture, specifically as it pertains to the network perimeter. The full evaluation rubric used for this determination can be found in Appendix A.
It is recommended to review the Findings section for a better understanding of risks and security issues discovered.
| Scope | Evaluation | Grade |
|---|---|---|
| Web Application Perimeter | Unsatisfactory | F |
Testing identified ten vulnerabilities, of these there were three key vulnerabilities ranging from Critical to High severity:
Successful exploitation demonstrated the possibility of: unauthorized access to privileged legal data, deletion or tampering of case files, and exposure of client PII—creating significant business, reputational, and potential regulatory risk.
Immediate remediation of the Critical and High findings is strongly advised. A detailed, prioritised roadmap is provided within the report. Re-testing is recommended within 30 days of patching to confirm that controls are effective.
This assessment was conducted as a black-box web application test targeting VulnLawyers' externally-accessible systems. The primary objective was to identify security weaknesses that could be exploited by an unauthenticated attacker, with emphasis on evaluating authentication, access controls, session management, and vulnerabilities outlined in the OWASP Top 10. Testing was performed using a combination of manual techniques and automated tooling, in line with industry best practices. Where applicable, business logic issues were also analyzed to assess the application's resilience to misuse. All findings are accompanied by actionable remediation guidance to assist in mitigating the identified risks.
This assessment was conducted under a defined scope and with explicit constraints provided prior to testing. Infrastructure-level testing, including backend server enumeration, network-layer probing, and denial-of-service attempts, was explicitly out of scope. As a result, findings related to system configuration or patch management are based solely on passively observed indicators and have not been validated through direct testing.
Additionally, the evaluation was performed from the perspective of an unauthenticated external actor unless otherwise noted. No persistent access, source code review, or social engineering techniques were employed. The findings presented herein reflect the application's state only during the assessment window and may not account for subsequent changes or time-based exposure.
| Domains Tested | Subomains Permmited? |
|---|---|
| quartz.ctfio.com | Yes |
The assessment was performed using the following approach:
This section will give a high level overview of the findings, specific technical remediations will be found in that section. The findings in this section are evaluated based on the risk rating matrix found in Appendix B.
Finding ID: C-01
Impact: Full compromise of all user accounts if attacker gains access to API responses or internal storage; enables credential reuse attacks across platforms.
Evidence: Passwords are returned in cleartext within API responses during profile detail queries (e.g., IDOR vulnerability), exposing them to any user able to access that endpoint.
Recommendation: Immediately implement secure password hashing (e.g., bcrypt or Argon2). Ensure no plaintext credentials are stored or returned by the application, and enforce a global password reset post-remediation.
Finding ID: C-02
Impact: Complete web application compromise by accessing all user credentials.
Evidence: Authenticated request to
/lawyers-only-profile-details/2 returned Shayne
Cairns's password (q2V944…).
Recommendation: Enforce server-side authorization: ensure users can only access their own data, and never expose passwords or sensitive data.
Finding ID: H-01
Impact: Full account takeover of lawyer accounts.
Evidence: Valid credentials discovered:
jaskaran.lowe@vulnlawyers.ctf:summer via brute force
Recommendation: Implement rate-limiting, MFA, and enforce stronger password policies.
Finding ID: M-01
Impact: Attackers can map app structure and harvest preliminary data.
Evidence: Subdomain discovered via ffuf, data.quartz.ctfio.com
Recommendation: Serve authentication barriers early (HTTP 401/403), and sanitize public app footprint.
Finding ID: M-02
Impact: Attackers can map app structure and harvest preliminary data.
Evidence: 302 response from /login leaks the
/lawyers-only page.
Recommendation: Remove the body and retire the old route.
Finding ID: L-01
Impact: The use of outdated software introduces latent vulnerabilities that may become exploitable if site functionality expands or if attackers chain weaknesses together. Nginx 1.22.0 is vulnerable to CVE-2022-41741, CVE-2022-41742, among others.
Evidence: Wappalyzer identified Nginx v1.22.0, this information is also available in HTTP response headers.
Recommendation: Upgrade major components to their latest secure versions. Establish a regular dependency review and patch lifecycle process for both frontend and backend components.
Finding ID: L-02
Impact: The use of outdated software introduces latent vulnerabilities that may become exploitable if site functionality expands or if attackers chain weaknesses together. jQuery v1.12.4 contains known cross site scripting (XSS) vulnerabilities.
Evidence: Wappalyzer identified jQuery v1.12.4, this information is also available by using
the browser JavaScript console and running the command jQuery.fn.jquery.
Recommendation: Upgrade major components to their latest secure versions. Establish a regular dependency review and patch lifecycle process for both frontend and backend components.
Finding ID: L-03
Impact: The use of outdated software introduces latent vulnerabilities that may become exploitable if site functionality expands or if attackers chain weaknesses together. Bootstrap v3.3.7 is deprecated with several historic UI-based flaws.
Evidence: Wappalyzer identified Bootstrap v 3.3.7, this information is also available by
using
the browser JavaScript console and running the command $.fn.tooltip.Constructor.VERSION.
Recommendation: Upgrade major components to their latest secure versions. Establish a regular dependency review and patch lifecycle process for both frontend and backend components.
Finding ID: I-01
Impact: Misleading user interface behavior creates the impression that sensitive data is deleted when it is not. This undermines user trust and could lead to inappropriate case handling decisions.
Evidence: When logged in as Shayne Cairns, pressing the “Delete Case” button causes the case to disappear from Shayne's view, upon leaving the page and coming back the case returns.
Recommendation: Ensure that all destructive actions, including case deletions, are handled at the backend level with verifiable state changes. All user-facing features must reflect the true state of the underlying data.
Finding ID: I-02
Impact: Version disclosure may enable targeted exploits against known vulnerabilities in Nginx 1.18.0 (e.g., CVE-2021-23017 - 1-byte memory overwrite).
Evidence: GET request to the /images directory results in a 301 and 403
response leaking Nginx version in the footer.
Recommendation: Our tentative recommendation is to set server_tokens off; in all Nginx blocks to suppress version disclosure in headers and bodies. Please review the technical remediation section for a better understanding.
| Finding | Priority | Estimated Effort | Target Fix Window | Key Compliance Drivers* |
|---|---|---|---|---|
| C-01: Passwords Stored in Plaintext | Critical | Medium | ≤ 14 days | OWASP A07-2021, GDPR (Art. 32), NIST 800-53 IA-5(1), ISO 27001 A.10.1 |
| C-02: IDOR - Profile Credential Disclosure | Critical | Medium | ≤ 14 days | OWASP A01 -2021, GDPR (Art. 32), ISO 27001 A.9.4.1, NIST 800-53 AC-6 |
| H-01: Weak Login / Brute-Force Vulnerability | High | Low - Medium | ≤ 30 days | OWASP A07 -2021, ISO 27001 A.9.4.2, NIST 800-53 AC-7, PCI DSS v4.0 8.3.6, Cyber Essentials |
| M-01, M-02: Information Disclosure | Medium | Low | ≤ 60 days | OWASP A06-2021, ISO 27001 A.13.1.1, CIS Controls 3.4 |
| L-01, L-02, L-03: Use of Outdated Software Components |
Low | Low | ≤ 90 days | OWASP A06-2021, ISO 27001 A.12.6.1, UK GDPR (Art. 32), NCSC Cyber Essentials |
*Compliance references are provided for context; VulnLawyers should map recommendations against their own regulatory obligations. No mappings were made for findings rated Informational. All references are links.
The following section tracks the path taken by the tester to uncover the identified vulnerabilities. A complete list of tools and wordlists used during this assessment is provided in Appendix C.
The tester began with directory and subdomain enumeration using ffuf. While ffuf
automatically performs recursive directory fuzzing, discovering a new subdomain requires initiating a new scan
scoped to that domain. Note: Although the screenshots show use of the -e flag (for file extensions),
it was not required for this particular case.
</> ffuf -H 'Host: FUZZ.quartz.ctfio.com' -u https://quartz.ctfio.com -w <Path to Wordlist> -fs 178
</> ffuf -u https://quartz.ctfio.com/FUZZ -w <Path to Wordlist> -fs 178
</> ffuf -u https://data.quartz.ctfio.com/FUZZ -w <Path to Wordlist> -fs 178
After completing the scans, we navigated to the interesting endpoint data.quartz.ctfio.com/users and
discovered a JSON list of valid usernames.
Armed with a list of usernames, we attempted to access the login page discovered earlier. However, we encountered
a redirect and an “Access Denied” message. Inspecting the raw HTTP response revealed a new endpoint at
/lawyers-only/.
At the new login screen, we captured a request containing a username and password and used Caido's Automate feature to perform a password spraying attack.
We successfully authenticated using the credentials:
jaskaran.lowe@vulnlawyers.ctf:summer
Next, we navigated to the profile update page and intercepted the request when modifying account data. We observed that profile details are retrieved via an endpoint containing the user ID.
We visited this endpoint directly to confirm it was accessible externally.
Since the endpoint did not enforce access controls, we modified the ID value to test access to other users' profiles.
With full visibility into other user profiles, we obtained credentials for all accounts. Notably, Shayne's account was identified as the manager of a sensitive case file.
Finally, we used Shayne's account to delete the referenced case file, discovering an implementation flaw. The file returns after deletion.

After confirming we can compromise the web application, we doubled back to poke at the web application from different angles noticing inconsistent Nginx versioning when met with a 403 error when accessing the /images and /js directories.
The following technical remediations are recommended to address the vulnerabilities identified during the assessment. Each recommendation is designed to not only resolve the immediate issue but also to promote long-term security hygiene through defensive development practices, access control enforcement, and architectural improvements.
To ensure the confidentiality and integrity of user credentials, passwords must never be stored or transmitted in plaintext. The current practice represents a severe violation of industry best practices and creates significant risk in the event of database compromise, insider access, or accidental data exposure.
Passwords must be hashed using a strong, adaptive one-way hashing function such as bcrypt, scrypt, or Argon2id. These algorithms are specifically designed to resist brute-force and dictionary attacks by increasing computational effort through configurable cost parameters.
</> Example: Secure Hashing
from flask import Flask, request, jsonify
import bcrypt
@app.route('/register', methods=['POST'])
def register():
data = request.json
username = data.get("username")
password = data.get("password")
if not username or not password:
return jsonify({"error": "Missing username or password"}), 400
salt = bcrypt.gensalt(rounds=12) ### <-- This is the configurable cost parameter.
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
users[username] = hashed
return jsonify({"message": f"User {username} registered successfully."}), 201
@app.route('/lawyers-only-login', methods=['POST'])
def login():
data = request.json
username = data.get("username")
password = data.get("password")
stored_hash = users.get(username)
if not stored_hash:
return jsonify({"error": "Invalid credentials"}), 401
if bcrypt.checkpw(password.encode('utf-8'), stored_hash):
return jsonify({"message": "Login successful"}), 200
else:
return jsonify({"error": "Invalid credentials"}), 401
Plaintext credentials currently in the database must be invalidated. Users should be prompted to reset their passwords through a secure password reset flow. Do not attempt to retroactively hash existing plaintext passwords, as this assumes compromise has not already occurred.
</> Example: Password Reset Flow
from itsdangerous import URLSafeTimedSerializer
from flask_mail import Message
def send_password_reset_email(user_email):
user = User.query.filter_by(email=user_email).first()
if not user:
return
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
token = serializer.dumps(user.email, salt='password-reset')
reset_url = url_for('reset_password', token=token, _external=True)
msg = Message("Password Reset", recipients=[user.email])
msg.body = f"Click to reset your password: {reset_url}"
mail.send(msg)
@app.route('/reset/<token>', methods=['GET', 'POST'])
def reset_password(token):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
try:
email = serializer.loads(token, salt='password-reset', max_age=3600)
except SignatureExpired:
return "Token expired", 403
except BadSignature:
return "Invalid token", 403
if request.method == 'POST':
new_password = request.form['password']
user = User.query.filter_by(email=email).first()
user.set_password(new_password)
db.session.commit()
return redirect(url_for('login'))
return render_template('reset_password_form.html', token=token)
Sensitive data such as passwords must also be excluded from all application responses and log files. APIs returning user account details must be audited to ensure no sensitive fields are included, even for authenticated users.
Access to password hashes (even if secure) should be restricted to the authentication subsystem only. Ensure logs, backups, and support tooling do not inadvertently leak these values.
Security mechanisms should be in place to ensure that any future code handling credentials enforces secure practices, such as:
These changes are foundational to any secure authentication system and are necessary for compliance with GDPR (Art. 32), ISO 27001 Annex A.9, and OWASP recommendations.
</> Example: Validation Check
curl -s https://quartz.ctfio.com/profile-details/6 | jq '.password'
To prevent unauthorized access to user credentials or other sensitive data through predictable URL parameters,
the application should enforce object-level access control on all endpoints. User identifiers exposed in URLs
(e.g., /profile-details/2) should be replaced with opaque references or UUIDs to prevent enumeration.
Additionally, sensitive information such as passwords must never be included in API responses, even when the
requesting user is authenticated.
Access control logic should be implemented server-side using authenticated session context, ensuring that users may only retrieve, modify, or delete resources they explicitly own. This validation must occur for every request and should not rely on UI constraints alone.
</> Example: Flask route with object-level access control using session context
from flask import session, abort
@app.route('/lawyers-only-profile-details/<int:user_id>')
def get_profile(user_id):
current_user_id = session.get('user_id')
if user_id != current_user_id:
abort(403)
user = db.get_user_by_id(user_id)
return jsonify(user.to_safe_dict())
To mitigate brute-force and password spraying risks, the login functionality must incorporate robust anti-automation controls. Rate-limiting should be applied on a per-IP and per-username basis to throttle repeated login attempts. This can be achieved at the web server level (e.g., Nginx limit_req) or within the application itself.
Additionally, the application should enforce stronger password policies (e.g., minimum 12 characters, alphanumeric and special characters), support multi-factor authentication (MFA), and implement account lockout or delay mechanisms after repeated failed logins. All failed authentication attempts should be logged and reviewed regularly.
</> Example: Rate-limiting in Nginx config
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;
server {
location /login {
limit_req zone=auth_limit burst=10 nodelay;
proxy_pass http://backend;
}
}
</> Example: Validation Check
for i in {1..10}; do curl -s -X POST https://quartz.ctfio.com/lawyers-only-login -d "user=test&pass=wrong" > /dev/null; done
curl -s -X POST https://quartz.ctfio.com/lawyers-only-login -d "user=test&pass=wrong" | grep -i 'too many attempts'
To prevent attackers from discovering hidden subdomains or application instances through fuzzing, the server configuration should be hardened to reject unknown Host headers. All virtual hosts should be explicitly declared in the web server configuration, and a default catch-all server block should return an error (e.g., HTTP 444) or redirect to a generic error page.
</> Example: Nginx config
server {
listen 80 default_server;
server_name _;
return 444;
}
server {
listen 80;
server_name quartz.ctfio.com;
root /var/www/quartz;
index index.html;
if ($host !~ ^(quartz\.ctfio\.com)$) {
return 444;
}
}
In addition, internal or staging environments should be protected via network-level access control, such as IP allowlisting or VPN-only access, and should not be accessible from the public internet. DNS records should be reviewed to ensure that only legitimate and necessary subdomains are publicly resolvable.
Some web servers may include the original response body in HTTP 30x redirects, which can inadvertently leak internal information such as error messages, stack traces, or unauthenticated views. This behavior is especially problematic when the body content contains sensitive information and is delivered to unauthenticated or unintended clients before redirection completes.
To prevent such disclosures, the application and underlying web server should be configured to avoid rendering bodies on redirection responses, particularly those involving 302 (Found) and 307 (Temporary Redirect) status codes. In Nginx, this can be achieved by issuing return directives instead of using rewrite ... redirect, or by stripping response bodies manually using the empty_gif module or custom headers where applicable.
</> Example: Nginx config
location /login {
if ($unauthorized) {
return 302 https://quartz.ctfio.com/denied;
}
proxy_pass http://quartz.ctfio.com/lawyers-only;
}
</> Example: Application level
@app.route('/login', methods=['POST'])
def login():
if not authorized():
return redirect("/denied", code=302)
return render_template("dashboard.html")
Additionally, review all authentication-related routes to ensure that redirects are performed early in the request pipeline, before any content is generated. If application logic must generate content conditionally, guard those blocks behind strict authentication checks.
This mitigation ensures attackers cannot extract sensitive clues during redirect behavior and aligns with secure-by-default design principles.
</> Example: Validation Check
curl -i -k -X GET https://quartz.ctfio.com/login --max-redirs 0
The use of outdated and vulnerable software components introduces unnecessary risk. While the current deployment may be minimal in functionality, it remains susceptible to exploitation via known vulnerabilities in its core dependencies.
Nginx 1.22.0: This version is vulnerable to several known issues (e.g., CVE-2025-23419, CVE-2022-41741, CVE-2022-41742). Upgrade to Nginx 1.26.3 or higher to address these issues. Testing should be conducted to ensure compatibility with custom modules and configurations.
jQuery 1.12.4: This version contains multiple XSS and DOM-related vulnerabilities. Upgrade to jQuery 3.7.1 or higher, and refactor deprecated API usage to ensure compatibility.
Bootstrap 3.3.7: This version relies on jQuery and is no longer maintained. Migrate to Bootstrap 5.x, which provides native JavaScript implementations and improved XSS protections. If full migration is not feasible, implement strict CSP headers and sanitize all user-provided content with a trusted library such as DOMPurify.
Maintaining an up-to-date dependency chain is essential for defense-in-depth and long-term maintainability of the application.
The application should accurately represent the outcome of user actions. In the current implementation, the “Delete Case” button appears to remove a legal case, but the data is not actually deleted from the backend or hidden from other users. This misrepresentation can mislead users into believing data has been irreversibly removed.
If deletion functionality is intended, implement the deletion logic on the server side. This should include a request handler that validates the user's permissions and securely updates the shared datastore to reflect the change (e.g., soft-delete the case).
If true deletion is not desired, the frontend should be updated to reflect that the action is only a local or session-level hiding mechanism, not a persistent removal. Ensure the UI communicates this clearly to avoid confusion.
Regardless of the approach, actions related to case management should be logged with:
</> Example: Python Implementation
from flask import request, session, abort
from datetime import datetime
@app.route('/case/<int:case_id>/delete', methods=['POST'])
def soft_delete_case(case_id):
case = db.get_case(case_id)
current_user_id = session.get('user_id')
if not case or case.owner_id != current_user_id:
abort(403)
reason = request.form.get("reason", "N/A")
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
case.status = "deleted"
db.commit()
db.insert_audit_log({
"event": "case_delete",
"user_id": current_user_id,
"timestamp": datetime.utcnow().isoformat(),
"ip_address": client_ip,
"record_id": case_id,
"reason": reason
})
return jsonify({"status": "marked as deleted"})
</> Example: Modal Delete Confirmation
<Modal>
<h3>Confirm Case Deletion
<p>Type DELETE to confirm.
<input onChange={...} />
</Modal>
</> Example: Validation Check
curl -X POST -b shayne_cookie.txt https://quartz.ctfio.com/lawyers-only \
-H "X-Reason: Test" -H "X-User-ID: 2" -d "action=delete" #inspect audit log after
curl -X POST -b not_shayne_cookie.txt https://quartz.ctfio.com/lawyers-only \
-H "X-Reason: Test" -H "X-User-ID: 420" -d"action=delete" \
| grep -q '403' && echo "RBAC Working"
These controls ensure that the application's behavior aligns with legal and business expectations for traceability, accountability, and risk mitigation even when actions are taken by authorized users.
While no direct vulnerability was confirmed due to the scope limitation prohibiting infrastructure testing, the exposure of inconsistent Nginx version numbers across HTTP responses may indicate proxy misconfiguration or layered reverse proxy setups that inadvertently leak upstream details. In this case, the presence of both nginx/1.22.0 and nginx/1.18.0 in same response increases uncertainty around patch levels and complicates forensic response in the event of an incident.
To remediate this configure Nginx to avoid disclosing the version string by setting
server_tokens off; in both the main nginx.conf and any relevant site-specific configuration files.
If a proxy is returning a different Nginx version, ensure it is also configured to suppress headers, or consider
normalizing the response headers with proxy_hide_header Server; and explicitly setting a generic
add_header Server directive.
</> Example: Nginx config
http {
server_tokens off;
proxy_hide_header Server;
add_header Server "Secure" always;
}
Review all Nginx or proxy configurations in the request chain to ensure version alignment and confirm that no outdated nodes are present behind the frontend server.
Although this finding was rated Informational, addressing it is a low-effort, high-clarity measure that supports operational hygiene and minimizes unnecessary information disclosure to external parties.
</> Example: Verification Check
curl -s -D - https://quartz.ctfio.com -o /dev/null | grep -i ^Server | grep -Eq 'nginx|Ubuntu' && echo "Not Remediated" || echo "Remediated"
| Grade | Rating | Criteria Description |
|---|---|---|
| A | Excellent | Security posture exceeds industry standards across key domains. Only a few low-risk items were observed, indicating strong governance and proactive oversight. |
| B | Good | Security posture meets most industry standards. A limited number of moderate-risk findings were noted, but core controls remain sound and effective. |
| C | Adequate | Security posture satisfies minimum expectations, though gaps in consistency across controls were observed. Improvements are recommended to reduce exposure. |
| D | Marginal | Security posture falls below acceptable thresholds in several areas. High-risk issues or control deficiencies require prompt remediation to avoid operational risk. |
| F | Unsatisfactory | Security posture does not meet baseline standards. Critical vulnerabilities or systemic failures pose substantial risk to confidentiality, integrity, and availability. |
| Risk Level | Description | Action Required |
|---|---|---|
| Critical | Immediate system compromise likely | Immediate remediation |
| High | Major impact, easily exploitable | Remediate as soon as possible |
| Medium | Moderate impact or requires user interaction | Fix in standard patch cycle |
| Low | Minor impact or low likelihood | Fix when feasible |
| Informational | No immediate risk | Optional review |
| Tool | Description |
|---|---|
Caido |
Intercepting and manipulating HTTP requests |
Wappalyzer |
Identification of web technologies |
Ffuf |
Directory and subdomain discovery |
| Command line request manipulation | |
| 📝 Wordlists | https://app.hackinghub.io/wordlists.zip |
| Name | Position | Company | Phone Number | |
|---|---|---|---|---|
| Noah Tyson | Principal Penetration Tester | FakeCompany | noahtyson@fake.com | 123-456-7890 |
| Shayne Cairns | Manager | VulnLawyers | shayne.cairns@vulnlawyers.ctf | 987-654-3210 |