Innskot í sniðmát
Vefþjónn skilar oft HTML sem verður til úr sniðmáti. Fyrirsögn, notandanafn og vörulisti koma úr gögnum, en sniðmátið ákveður hvar þau birtast. Fái notandinn hins vegar að véla með sjálft sniðmátið, getur farið svo að texti hans stökkbreytist í segð sem þjónninn situr uppi með að reikna áður en HTTP-svarið er lagt af stað.
Það kallast sniðmátsinnskot (e. server-side template injection, SSTI). Þessi tiltekni veikleiki skýtur rótum sýnum á þjónshliðinni. Vafrinn tekur við tilbúnum afrakstrinum, og ekki er víst að sjálf sniðmátssegðin sem keyrð var komi þar við sögu.
Verkefnið Kortakveðja veitir okkur tækifæri til þess að forskoða kveðjutexta. Við reiðum okkur í upphafi á eitt einfalt reiknipróf, í því skyni að ganga úr skugga um hvort Jinja2 túlki textann raunverulega. Að því búnu grípum við einvörðungu til afmarkaðs lab-hlutar sem verkefnið hefur sjálft skotið vandlega inn í sniðmátssamhengið. Engin þörf er á skel né heldur á almennri Python-hlutakeðju.
SSTI aðeins í einangruðu verkefni
Sniðmátsinnskot getur í sumum vélum leitt til skráarlesturs eða kóðakeyrslu. Einbeitum okkur að því að prófa eingöngu minnstu og sakleysislegustu segðirnar innan ramma þessa einangraða verkefnis. Ekki reyna Python-innri hlutakeðjur, kerfisskipanir, netbeiðnir eða að brjótast út úr sandkassanum. Takmarkinu er vissulega náð þegar afmarkaði lab-hluturinn skilar okkur fánanum tryggilega í hendur.
Hvað gerir sniðmátsvél?
Sniðmát (e. template) er í raun texti samansettur úr föstum hlutum, ásamt sérstökum tilvísunarmerkjum sem ætluð eru fyrir breytileg gögn. Sniðmátsvél (e. template engine) tekur síðan á sig það hlutverk að bræða sniðmátið og gögnin í eina heild.
Einfalt Jinja2-sniðmát gæti verið:
<h1>Halló, {{ username }}</h1>
<p>{{ message }}</p>
Forritið leggur vélinni lið með því að afhenda henni viðeigandi sniðmátssamhengi (e. template context):
{
"username": "anna",
"message": "Pöntunin þín er tilbúin."
}
Útkoman verður HTML:
<h1>Halló, anna</h1>
<p>Pöntunin þín er tilbúin.</p>
Vélin tekst á hendur að túlka {{ username }} einvörðungu sökum þess að merkið stóð þegar í sjálfu sniðmátinu. Textinn sem leynist í message þjónar engu öðru en að vera gildi sem sniðmátið svo aðeins birtir.
Gögn í sniðmáti og sniðmát úr gögnum
Eftirfarandi Flask-kóði velur afdráttarlaust fasta skrá og afhendir notandatextann beint sem hrein gögn:
message = request.form["message"]
return render_template("card.html", message=message)
Ef notandinn sendir:
Halló {{ 7 * 7 }}
birtist sá sami texti að jafnaði algjörlega bókstaflega. Sniðmátsvélin túlkar vissulega {{ message }} inni í card.html, en henni dettur ekki í hug að keyra upp ný Jinja-merki sem kunna að stafa beint úr gildi breytunnar.
Önnur nálgun felur í sér að kóðinn smíði sjálft sniðmátið nákvæmlega upp úr notandainntakinu:
message = request.form["message"]
return render_template_string(message)
Að þessu sinni er message sjálft matreitt og framreitt sem sniðmát. Og sjá, sama inntakið kann nú að snúast í:
Halló 49
Mörkin líta svona út:
fast sniðmát + notandagögn → gögn birt
notandagögn sem sniðmát → gögn túlkuð sem sniðmátskóði
Þetta telst meðal allra gagnlegustu kóðavísbendinganna sem finna má í Flask-gagnaglímum: Fallið render_template_string() er út af fyrir sig enginn veikleiki, en hlýtur óneitanlega athygli hvenær sem strengurinn á uppruna sinn í ótraustri beiðni. Sams konar áhætta gæti hæglega verið til staðar í öðrum forritunarmálum, einkum þegar notandatexta er afhentur API-snidil sem annast það að þýða eða keyra sniðmátin.
Byrjum á endurvarpi
Sendu fyrst venjulegan, auðþekkjanlegan texta:
POST /preview HTTP/1.1
Host: nordurmarkadur.example
Content-Type: application/x-www-form-urlencoded
message=KORT_MERKI_42
Skili KORT_MERKI_42 sér í svarinu göngum við hnarreist út frá því að inntakið nái greiðlega alla leið í svarið. Þetta ber þó einungis merki um endurvarp (e. reflection), og verður enn ekki talið sem sniðmátsinnskot. Ógrynni forrita endurvarpa nefnilega texta hreinlega án þess að túlka hann frekar.
Verkfærinu curl má einkar vel treysta til að varðveita sérstök tákn sé gripið til valkostarins --data-urlencode:
BASE_URL='https://slod-verkefnisins.example'
curl -i -X POST "$BASE_URL/preview" \
--data-urlencode 'message=KORT_MERKI_42'
Gættu þess ávallt að grandskoða hráa HTTP-svarið bæði í Network og Repeater. Hafi JavaScript nefnilega hlaupið til og breytt DOM-trénu eftir á, liggur okkur mikið á að greina hvort útreikningurinn sprotti upphaflega frá þjóninum, eða hvort hann varð til heima í sjálfum vafranum.
Minnsta reikniprófið
Jinja2 og nokkrar aðrar vélar nota tvöfalda slaufusviga fyrir segðir:
{{ 7 * 7 }}
Leyfum okkur að senda það af stað sem stakt eyðublaðsgildi:
curl -i -X POST "$BASE_URL/preview" \
--data-urlencode 'message={{ 7 * 7 }}'
Þessar þrjár mögulegu niðurstöður veita okkur síðan talsvert ólíkar, en dýrmætar upplýsingar:
{{ 7 * 7 }} # birtist bókstaflega: endurvarp, engin staðfest túlkun
49 # segðin var reiknuð: sterk SSTI-vísbending
TemplateSyntaxError ... # vél þekkti líklega sniðmátamerki en hafnaði segð
Þetta prýðilega reiknipróf raskar engu um skrár né heldur ástand forritsins og reynist af þeim sökum fremsta og tryggasta staðfestingin, langtum vænlegri en að þeyta fram langri hlutakeðju sem freistar þess undireins að klófesta kerfisskipun.
Reiknipróf er samhengi, ekki töfraorð
Ekki má gleyma að {{ 7 * 7 }} gæti þess vegna komið fyrir innan JavaScript-ramma, eða verið reiknað af alveg öðru lagi í hugbúnaðinum. Reyndu því að staðfesta svart á hvítu að gildið 49 hvíli raunverulega í hráa HTTP-svarinu frá /preview og vertu ekki feimin(n) við að endurtaka leikinn með öðru smávægilegu reiknidæmi. Hitt er svo annað mál, að eitt villuráfandi 49 framan á síðunni sannar fyrir okkur nákvæmlega ekki neitt.
Létt auðkenning sniðmátsvélar
Margar vélar nota ólíka setningafræði, en sumar skarast. Vinsamlegast stilltu þig um að senda tuttugu próf í beit. Liggur mun nær að hefja leikinn á þeim tækniupplýsingum sem verkefnið sjálft ljóstrar upp um: Flask bendir oft á Jinja2, PHP getur notað Twig og Java-forrit getur notað FreeMarker eða Thymeleaf.
| Merki | Vélar sem gætu þekkt það |
|---|---|
{{ 7 * 7 }} |
Jinja2, Twig og fleiri |
${7 * 7} |
FreeMarker og nokkrar Java-vélar |
<%= 7 * 7 %> |
ERB og skyld snið |
Eitt hið gagnlegasta aðgreiningarpróf sem völ er á til þess að slá föstu um hvort við búum við Jinja2 eða Twig er eftirfarandi:
{{ 7 * '7' }}
Jinja2 endurtekur strenginn oft sjö sinnum, en Twig getur túlkað gildið tölulega. Margvíslegar útgáfur og stillingar eiga það til að hnekkja hegðun kerfisins. Villuboð, hausar, skráarendingar og afhentur frumkóði vega þar af leiðandi alla jafna þyngra á metunum en eitt stakt og óyggjandi auðkenningarpróf.
Taki verkefnið það ótvírætt fram að Flask og Jinja2 séu á dagskrá hlýtur að teljast með öllu ástæðulaust að þreyta ERB, FreeMarker eða Twig í upphafi. Einungis er stefnt að því að auðkenning vélarinnar þrengi sjálft leitarrýmið.
Hvað er í sniðmátssamhenginu?
Yfirleitt á sniðmátssegð aðeins erindi við hrein bókstafleg gildi, þær aðgerðir sem vélin styður heils hugar, auk þeirra hluta sem forritið gætir þess að setja í tiltekið samhengi. Jinja2 gæti fengið:
return render_template_string(
message,
user=current_user,
products=products,
lab=challenge_context,
)
Þá gætu segðir verið:
{{ user.name }}
{{ products[0].name }}
{{ title | upper }}
Punktur mælist hér til lesturs eiginleika, hornklofi kallar eftir staki og lóðrétt strik | beytir síu. Hvað af þessu hlýtur svo lögmæti ræðst nánast alfarið af smíð vélarinnar og eðli hlutanna sjálfra.
Á mörkum venjulegrar gagnaglímu (CTF) kann samhengið vel að spanna stillingar kerfisins, beiðnir sem bárust, öflug hjálparföll eða heilu gagnagrunnshlutina. Í verkefninu Kortakveðja höfum við vísvitandi sniðið umfangið stakk. Verkefnið sjálft bendir rækilega á stakan lab-hlut og góðfús vísbending beinir kastljósinu að einum fágætum eiginleika hans. Því er þér algerlega besefatlaust og ónauðsynlegt að brjótast frá hefðbundnum strengjum (str) og yfir í dimma heima Python-klasa, undirlása (e. dunders) eða innflutning (e. import).
Prófaðu fyrst að birta hlutinn sjálfan:
{{ lab }}
Bæði framsetningin í svarinu, nú eða hrein og klár villa, geta leitt í ljós hvaða afmarkaða eiginleika verkefnið sjálft gerir fyllilega ráð fyrir að þú nýtir þér. Næsta segð hefur lögunina:
{{ lab.<eiginleiki úr vísbendingu verkefnisins> }}
Kaflinn gefur ekki upp heiti eiginleikans eða fánann.
Frá útreikningi að áhrifum
Eitt er víst, SSTI telst seint jafngildi og hreinræktaðrar kerfisskeljar. Getan til illra verka vex aftur á móti i hlutfalli við gerð vélarinnar, núverandi samhengi og þeim takmörkunum sem lagðar eru á:
reikna fasta segð
↓
lesa breytur í samhengi
↓
kalla leyfð hjálparföll
↓
lesa viðkvæm gögn
↓
möguleg kóðakeyrsla í sumum umhverfum
Markmið hverrar gagnaglímu ræður því nánast hvar við drögum mörkin og stoppum. Ef fáninn er í lab-hlutnum er engin ástæða til að halda áfram í almenna kóðakeyrslu. Minni segð er auðveldari að útskýra, öruggari fyrir þjónustuna og sýnir nákvæmlega hvaða traustforsenda brást.
Áhrifin sem slík geta vitaskuld reynst margbreytileg, allt eftir því hvaða samhengi blasir við okkur. Forskoðunarþjónusta með aðeins tilbúnum kortagögnum hefur minni aðgang en sniðmátsvél inni í stjórnendakerfi. Við ættum af fremsta megni að varast stórar fullyrðingar á borð við „við höfum náð fullri stjórn yfir þjóninum“ einungis af þeirri einu sök að okkur auðnaðist að fá {{7*7}} til að skila okkur tölunni 49.
Sandkassi og takmarkað samhengi
Sandkassi (e. sandbox) leitast við eftir fremsta megni að takmarka nákvæmlega til hvaða eiginleika, falla eða kerfisauðlinda túlkaður kóði nær til. Jinja2 býður til dæmis SandboxedEnvironment, og CTF-þjónustan sjálf getur keyrt inni í nsjail.
Til geta því fallið tvö lög í slíkum varnarmúrum:
- Annars vegar tekur sjálf sniðmátsvélin í taumana og afmarkar rækilega hvað hverri segð er heimilað að gera.
- Hins vegar stendur stýrikerfissandkassinn vörð og heftir ferlið sé svo komið að fyrsta laginu hafi verið smeygt hjá.
Virki reikniprófið sem skyldi, en sérhver aðgangur að undirstrikuðum (e. underscored) eiginleikum endar í kaldranalegri SecurityError-villu, þá hefur þér tekist að staðfesta bæði sniðmátstúlkunina, auk þess að hlaupa á öfluga vísbendingu sem talar í gegn gegn algengri Python-hlutakeðju. Varastu þá um leið að túlka þetta sem svo að SSTI-veikleikinn sjálfur hafi horfið eins og dögg fyrir sólu; sannleikurinn er aðeins sá að sjálfri túlkunargetunni voru þröng mörk sett.
Kortakveðja keyrir í nsjail vegna þess að ótraust Jinja2-sniðmát getur haft óvæntar leiðir. Vel að merkja er sandkassanum einungis ætlað að standa vörð um öryggismörk verkefnisins, og ber engan veginn að skilja sem aukalega eða ábætta þraut. Notaðu afmarkaða lab-leiðina og reyndu ekki að lesa hýsiskrár eða umhverfisbreytur.
Sniðmátsinnskot er ekki XSS
Þó bæði SSTI og hefðbundið vefkóðainnskot (XSS) geti fyllilega átt upptök sín í sama textareitnum, liggur munurinn í því að túlkurinn er alls ekki einn og hinn sami.
SSTI: notandatexti → sniðmátsvél á þjóninum → HTML-svar
XSS: notandatexti → HTML/DOM í vafranum → JavaScript-keyrsla
Ef {{7*7}} verður 49 í hráa svarinu gerðist útreikningurinn á þjóninum. Birtist hins vegar <script>-merki og fari fyrst í keyrslu um leið og vafrinn sjálfur leggur dóm á svarið, stöndum við andspænis allt annarri tilgátu sem gerð verða ítarlegri skil í XSS-kaflanum.
Rétt er að halda því til haga að eitt og sama inntakið getur ratað inn í fleiri en eitt samhengi. Jinja2 gæti til dæmis reiknað út segð í upphafi og útkoman henni í kjölfarið skolað ókóðuð upp í HTML. Við setjum okkur þá skýru reglu að reyna á eitt og stakt túlkunarmark í senn, þannig verður niðurstaðan yfirleitt skýrust.
Göngudæmi: Kortakveðja
Opnaðu forskoðunina og sendu fyrst KORT_MERKI_42. Gríptu nú /preview-beiðnina, ýmist í Network eða inni í Burp, og staðfestu sjónrænt endurvarpið (reflection) í hráa svarinu. Sendu síðan {{ 7 * 7 }} með sömu aðferð og kóðun. Komi gildið 49 þar fram hlýtur það að teljast nokkuð traust sönnun þess að textinn okkar hafi farið í gegnum Jinja2-sniðmátstúlkun.
Nú skaltu lesa verkefnisvísbendinguna um tiltækt samhengi. Gefðu okkur þá hið yfirvegaða {{ lab }} og rýndu gaumgæfilega í afmarkaða og snubbótta framsetningu hlutarins. Notaðu síðan eiginleikann sem verkefnið gefur tilefni til:
curl -i -X POST "$BASE_URL/preview" \
--data-urlencode 'message={{ lab.<eiginleiki úr verkefninu> }}'
Ef svarið sýnir fánann er verkefninu lokið. Í lokalausninni er tilvalið að varðveita af fádæma vandvirkni nákvæmlega tvær beiðnir: sjálft reikniprófið til að varpa ljósi á framkomna túlkun og að auki þá stuttu og hnitmiðuðu lágmarkssegð sem færir okkur lokamarkmið CTF-verkefnis.
Standi þetta fádæma {{ lab }} eitt og óvarið, öldungis bókstaflega framan í þig, verðurðu nauðbeygð(ur) til þess að sannreyna hvort leikið hafi verið um réttan reit og hvort slóðin hafi ratað rétta leið. Virki hið annars sakleysislega {{ 7 * 7 }} á hinn bóginn eðlilega en eiginleikinn neitar að skila öðru en skellitómum texta, krefst það óneitanlega dágóðs yfirlesturs á bæði stafsetningu sem og hinni mæltu villu; höfum hugfast að Jinja þykir eiga það til að hylja óskilgreind gildi í stelpufötum tóms strengs. Vertu því ekki ginnkeypt(ur) til að stökkva á vagninn og henda þér í almenna og þunga RCE-keðju út í bláinn.
Vísbendingar gegn veikleikum
Reki {{ 7 * 7 }} óbreytt á fjörur okkar, beint ofan í hráu HTTP-svari án minnstu grettu, má með sanni slá því föstu að okkur hafi með engu móti tekist að staðfesta veikleikann SSTI. Taki JavaScript sig hins vegar til og breyti upplýsingunum í 49 löngu eftir að svarið sjálft sneri heim í hlað, leikur rökstuddur grunur á um að sjálfur túlkurinn búi á biðlarahliðinni. Afhentur kóði, feli hann í sér að stuðst sé við óbifanlega og fasta render_template()-skrá á sama tíma og textanum er deilt inn á borð sem einhverri hversdagslegri breytu, gildir að sjálfsögðu sem óræk vísbending um að einfalt SSTI fái tæpast þrifist á akkúrat þeim slóðum.
Rýrni og sniðmátsvilla sem tilkomin er eftir ófullkomið eða slitrótt {{ telst prýðisvísbending en sannarlega ekki fullkomin og óhrekjandi staðfesting. Láttu fremur duga að steypa saman gildu reikniprófi við hlið bókstaflegs endurvarps til sannprófunar. Uppskeri báðar slíkar beiðnir einu og sömu vögguvísuna í svörum, bendir það óneitanlega til þess að veframminn hafi með góðu eða illu hafnað sérhverjum slaufusviga mörgum mílum áður en honum tókst að komast í tæri við sniðmátsvélina okkar.
Setji samhengið okkur mjög þröng og ákveðin mörk má vel ímynda sér að hvorki meira né minna en aðeins stóreinfaldir og fátæklegir útreikningar séu í raun ætlaðir. Engum er greiði gerður með því að kasta fram fullyrðingum um gífurlegan gagnaleka eða blinda kóðakeyrslu fyrr en hnitmiðuð, afmörkuð og nákvæm segð sýnir svart á hvítu tilætlaða getu.
Samantekt
Sniðmátsvél sameinar fast sniðmát og gögn. Skaðlegt SSTI lætur aðeins á sér kræla þegar og ef notandastýrður texti annaðhvort skreppur í gervi sjálfs sniðmátsins, eða honum tekst hið minnsta að vera túlkaður enn að nýju sem ekta sniðmátskóði. Ferðin okkar hefst með auðsýnilegu endurvarpsmerki, reiðir sig næst á sakleysislegt reiknipróf og leitar loks að því að auðkenna umrædda vél – hvorki meira né minna en nákvæmlega eins langt og þörf krefur.
Hitt er svo annað mál að framsetningin render_template_string() í faðmlögum við beiðnigögn verður alltaf ægileg Flask-vísbending, en í stuttu máli mælir fastmótað og óhagganlegt sniðmát geymandi sínar einu sönnu breytur algjörlega í gegn um sömu tilgátu. Getan þarna að baki, já hún kann stundum að draga línuna allt frá vesælum útreikningum og hækka sig smám saman upp í dýpri lestur samhengisins eða jafnvel galopna og blygðunarlausa kóðakeyrslu. Í leik okkar í CTF mörkum við alltaf lokastöðina nákvæmlega þar við smæstu mögulegu segð sem fleytir okkur af markvissni að lokafánanum. Við leggjum að auki þann stóra ramma okkar í land að virða sandkassa verkefnisins sem strangt en faglegt mörk og nýtum hann í engum tilfellum sem nýjan eða tilfallandi hjalla til að klífa.
Í eftirfarandi kafla hliðrum við okkur lipurlega burt frá fjölbreytilegri túlkun sniðmáta og beinum sjónum okkar dýpra inn í sjálfar skráarslóðirnar. Á þeim slóðum á notandastýrður texti það hreinlega til að slíta forritið af klafanum og ganga þvert á ætlaða möppu eða hreinlega lesa staðbundnar og viðkvæmar skrár án minnsta viðvörunarmerkis.