Hoppa yfir í efnið

Slóðaflakk og staðbundin skráarinnlimun

Vefur sem býður upp á handbók eða niðurhal þarf að velja skrá. Beiðnin gæti sagt file=intro.txt og forritið bætt því nafni við fasta handbókarmöppu. Eigi notandinn þess kost að lauma .. inn í nafnið er hætt við að skráarslóðin brjótist út úr þeirri möppu sem viðmótið gerir annars ráð fyrir.

Þetta er slóðaflakk (e. path traversal). Þegar skrá er ekki eingöngu lesin heldur beinlínis innlimuð af túlk eða forriti, er oftast rætt um staðbundna skráarinnlimun (e. local file inclusion, LFI). Þessi tvö hugtök eiga það reyndar til að skarast nokkuð í umfjöllun um gagnaglímur, en mikilvægt er að halda til haga að þau lýsa ekki endilega einni og sömu getunni.

Verkefnið Handbókin er afmarkað slóðaflakksverkefni. Það les textaskrár úr manuals/, en tilbúin fixtures/-systurmappa inniheldur prófskrá og fána. Markmiðið snýst einungis um að feta sig eitt skref upp á við og þrýsta sér síðan inn í þá umræddu möppu.

Ekki kanna skráakerfi utan verkefnisins

Verði almennur skráarlestur að raunveruleika getur það leitt til afhjúpunar á lyklum, frumkóða eða jafnvel kerfisskrám. Prófaðu aðeins þær tilbúnu slóðir sem verkefnið nefnir. Ekki lesa /etc/passwd, umhverfisskrár, hýsingarlykla eða skrár annarra þjónusta. Sjálfur sandkassi verkefnisins stendur sem afmörkun, en alls ekki sem sjálfstætt skotmark.

Algildar og afstæðar slóðir

Algild slóð (e. absolute path) byrjar við rót skráakerfisins:

/tmp/challenge/manuals/intro.txt

Afstæð slóð (e. relative path) er túlkuð út frá annarri möppu:

manuals/intro.txt

Í POSIX-líkum kerfum markar / sjálfa rótina. Stakur punktur . vísar þá til núverandi möppu en tveir punktar .. merkja svokallaða móðurmöppu.

Ef við stöndum í:

/tmp/challenge/manuals/

þá vísar:

../fixtures/proof.txt

á:

/tmp/challenge/fixtures/proof.txt

Skráakerfið tekur þarna þann pól í hæðina að ganga fyrst upp úr manuals yfir í challenge og færir sig að því loknu niður í fixtures.

Stýrikerfið Windows styðst tíðum við slóðir í líkingu við C:\Users\Anna\file.txt og á það fyllilega til að samþykkja bakskástrik (e. backslash) sem aðskiljara. Handbókin keyrir á Linux, svo við höldum okkur við /. Tegund stýrikerfis og viðkomandi veframmi þjóna því sem afar hagnýtar vísbendingar um nákvæmlega hvaða slóðarreglur skipti máli hverju sinni.

Þegar vefslóð umbreytist í skráarslóð

Einföld beiðni í handbókinni gæti litið einhvern veginn svona út:

GET /download?file=intro.txt HTTP/1.1
Host: nordurmarkadur.example

Forritið gæti gert:

MANUALS = Path("/tmp/challenge/manuals")
filename = request.args["file"]
path = MANUALS / filename
return path.read_text()

Með intro.txt verður path:

/tmp/challenge/manuals/intro.txt

Með ../fixtures/proof.txt verður textalega slóðin:

/tmp/challenge/manuals/../fixtures/proof.txt

Skráakerfið tekur til við að túlka .. og les umsvifalaust systurskrána. Forritið gekk út frá því sem vísu að filename væri sakleysislegt nafn eitt og sér, en HTTP-biðlarinn lætur sér ekki segjast og slægist í að stjórna heilu slóðarhlutunum.

Þetta er nákvæmlega sá öryggisþröskuldur sem við munum láta reyna á:

notandastýrt `file` → slóðasamsetning → skráakerfi

Slóðaflakk er ekki alltaf LFI

Beiti Flask-kóðinn föllum á borð við read_text() eða send_file() til þess að afhenda bæti, flokkast aðgerðin sannanlega sem skráarlestur. Slík skrá er að sjálfsögðu ekki keyrð sem framkvæmanlegur Python-kóði.

PHP-kóði gæti hins vegar gert:

include($_GET['page']);

Kvaðningin include hvetur beinlínis PHP-túlkinn til að vinna úr skránni. Og það er einmitt þá sem LFI-heitið á afbragðs vel við. Innlimuð PHP-skrá gæti valdið því að kóði færi í keyrslu, en innlimuð textaskrá léti líklegast nægja að birtast á skjánum eða orsaka klaufalega túlkunarvillu.

Önnur föll hafa aðra hegðun:

open(path).read()             # les bæti eða texta
send_file(path)               # sendir skrá
render_template(path)         # túlkar skrá sem sniðmát

Fundur á file=-reit segir okkur því einn og sér lítið um þau raunverulegu áhrif sem raunhæft er að kalla fram. Rýndu því vandlega í svar, villuskilaboð eða tiltækan frumkóða og gættu þess að nefna hæfnina ætíð sínu rétta nafni: hvort sem þar er átt við lestur, niðurhal, sniðmátstúlkun eða beina kóðainnlimun.

Grunnbeiðni og þekkt markskrá

Gott er að hefja leikinn á skrá sem viðmótið vísar nú þegar beint á:

BASE_URL='https://slod-verkefnisins.example'

curl -i -G "$BASE_URL/download" \
  --data-urlencode 'file=intro.txt'

Skráðu stöðukóða, Content-Type, Content-Disposition og innihald. Með þessu hefurðu dregið upp grunnlínu sem sýnir glögglega hvernig fullkomlega venjulegt skráarnafn á að hegða sér.

Því næst beitum við þekktri og gersamlega skaðlausri prófskrá sem verkefnið fullyrðir að sé að finna í systurmöppunni:

curl -i -G "$BASE_URL/download" \
  --data-urlencode 'file=../fixtures/proof.txt'

Ef sérstakt prófmerki birtist höfum við staðfest að .. fór yfir mörkin úr manuals. Eftir þetta situr engin þörf eftir á því að giska í blindni á einhverjar kerfisskrár eða þreyta sig á innantómri upptalningu á möppum.

Skipunin --data-urlencode tryggir að skástrik og punktar séu rækilega kóðaðir ef á þarf að halda, þó þjónninn móttaki að sjálfsögðu afkóðað reitargildi að lokum. Skellirðu þér inni í Repeater muntu hæglega sjá prósentukóðuðu beiðnina og hina raunverulegu afkóðuðu merkingu hlið við hlið.

Afkóðun og samræming slóða

Hvort sem það er vafrinn sjálfur, vefþjónninn, veframminn eða umrætt forrit geta þessir aðilar tekið sig saman um að vinna úr slóðinni í fleiri en einu skrefi. Prósentukóðun getur táknað ../ sem:

%2e%2e%2f

Sé lagið forritað til að afkóða einu sinni verður lokaniðurstaðan einfaldlega ../. Hins vegar gæti tvíkóðuð útgáfa virst sem:

%252e%252e%252f

Sú fyrri afkóðun breytir %25 umvifalaust í stakt %, þannig að næsta lag í röðinni myndi þá síðar sjá %2e%2e%2f. Hér verður þó að undirstrika að slíkt skiptir auðvitað einungis máli í þeim tilvikum þar sem tvö talsins afkóðunarskref eru í alvörunni til staðar. Varastu því algjörlega að stökkva strax í tvíkóðun (e. double encoding) þegar hið venjubundna ../ gegnir hlutverki sínu vel eða þegar inntak bendir til annars en tvöfaldrar vinnslu.

Samræming slóða (e. path normalization) einfaldar hluta eins og . og ..:

/tmp/challenge/manuals/../fixtures/proof.txt
                         ↓
/tmp/challenge/fixtures/proof.txt

Forrit búa yfir þeim valkosti að sía streng hvort heldur sem er áður en til samræmingar kemur, eftir samræminguna eða hreinlega á báðum stigum. Þessi röð hefur vitaskuld úrslitaáhrif. Slái bannlisti til dæmis eitt ../-mynstur út af borðinu áður en samliggjandi texti sætir einföldun, getur komið fyrir að önnur klók framsetning smíði mynstrið bara aftur að nýju. Innan veggja CTF-verkefna leggjum við einungis í slíkar prófanir ef hvort heldur svar eða frumkóði staðfesta slíka röðun; við forðumst nefnilega sem heitan eldinn að varpa fram langri upptalningu af slóðaframsetningum eins og vélmenni.

Þegar slóðarhlutinn sjálfur breytist

Vafrar og líka verkfærið curl eiga það fyllilega til að einfalda ../ niður í snyrtilegan slóðarhluta löngu áður en nokkur beiðni er send úr hlaði:

/static/../admin

Viljir þú stöðva slíkt gildir valkosturinn --path-as-is um að varðveita slíkan slóðarhluta í heilu lagi:

curl --path-as-is 'https://nordurmarkadur.example/static/../admin'

Þar sem um verkefnið Handbókin er að ræða dúkkur skráarslóðin hins vegar upp inni í file-fyrirspurnarreit, og þar af leiðandi leikur ekki vafi á um að --data-urlencode sé rétta og réttláta verkfærið í verkið. Ekkert liggur því á að nota --path-as-is nema því aðeins að .. sé sannanlega formlegur hluti af HTTP-slóðinni sjálfri.

Algild slóð fær því umflúið grunnmöppuna

Í ákveðnum forritaskilum á sér stað sú hegðun að algildur seinni slóðarhluti hreinlega hunsar með öllu hina fyrri grunnmöppu. Tökum Python sem dæmi:

Path("/tmp/challenge/manuals") / "/tmp/challenge/fixtures/proof.txt"

verður:

/tmp/challenge/fixtures/proof.txt

Sú algilda slóð hefur því sölsað undir sig öll völdin. Skapi forritið sér reglu um að sía ../ í burtu, en veiti samtímis upphafsskástriki (e. leading slash) fararleyfi, rís augljóslega tilgáta um að algild slóð gæti verið raunhæf. Rétt er að nefna að slík tilgáta er reyndar engin nauðsyn í okkar verkefni, enda hæfum við markvissast tilbúna markslóð sem verkefnið veitir skýlaust samþykki fyrir.

Lítum við til Windows-stýrikerfisins er eðlilegt að drifstafir (e. drive letters) og jafnvel UNC-slóðir kalli fram sambærileg áhrif. Varastu því umfram allt að henda til Windows-framsetningu gegn verkefni byggðu á Linux og Flask án þeirra vísvitandi vísbendinga að eitthvert ytra lag í arkitektúrnum taki sig til og túlki slíkt.

Tákntenglar og endanleg staðsetning

Tákntengill er skrá sem vísar á aðra slóð. Textalega séð gæti slóðin vel hljómað eins og hún lúri örugg inni í manuals, en reynist hún geyma tákntengil inni í sér er jafn víst að vísað verði umvifalaust út fyrir þá möppu.

Fallið Path.resolve() vinnur það stórvirki að grafa upp kanóníska slóð (e. canonical path) meðal annars með því að eiga við .. auk flestra gerða af tákntenglum. Verði það niðurstaðan að afhentur kóði reyni að upplýsa hvort tveggja, grunnmöppu sem og markslóð, og gangi svo frá því að bera þær endanlegu staðsetningar saman, liggur nánast í augum uppi að einföld notkun á ../ muni skila afar döprum árangri.

Nemi kóðinn staðar við það eitt að mæla eingöngu gildi textaforskeyta stefnir þó vissulega í alvarlegan rugling:

/srv/manuals
/srv/manuals-old/secret.txt

Skoðum við dæmið sjáum við að báðar slóðir geyma sömu upphafsstafi, en til allrar ólukku hefur sú seinni ekkert inn í hinu fyrra möppuhólfi að gera. Birtir þetta vel stóra og ríka ástæðu til þess að rýna vel í hvort allur samanburður nái í raun yfir slóðarhluta eða eigi sér aðeins stað á yfirborði hrárra strengja. Lánið okkar felst hins vegar í því að Handbókin fyrirfinnur sér ekki slíka athugun; þar af leiðir að stakt mæðra-skref nægir okkur.

Algengar síur og vísbendingar gegn veikleikum

Auðsóttara verður varla fyrir forritið en að grípa aðeins yfirborðskennt í síðasta nafn slóðarinnar með hjálp basename() eða jafnvel Path.name. Fari fram sem horfir verður hlutskiptið:

../fixtures/proof.txt → proof.txt

og hið tilætlaða slóðaflakk rennur algjörlega út í sandinn. Sé þar að auki til staðar strangur leyfilisti, er varpar auðkenni á borð við intro stíft yfir á fasta skrá, liggur ekkert valkvætt eða frjálst skráarnafn eftir handa okkur að breyta. Allt eru þetta mjög svo yfirþyrmandi vísbendingar sem draga úr líkum á slóðaflakki við tiltekinn reit.

Miklum mun gisnara og veikara mynstur væri aftur á móti ef forritið lætur sér nægja að rífa niður bókstaflegt ../ mynstur eitt og stakt skipti. Þegar þannig háttar til skiptir sköpum í hvaða röð afkóðun, endurtekin hreinsun eða slóðarsamræming hreinlega mætir til leiks; hvort það verði fyrr eða seinna. Taktu eftir að annaðhvort svarið sem kemur eða kóðinn sjálfur ættu afdráttarlaust að styrkja eina hnitmiðaða og vel ígrundaða tilraun af okkar hálfu; bregðist okkur bogalistin við að finna slíkan stuðning væri viturlegra að draga andann og beina sjónum okkar heldur að allt öðrum skráarreit.

Sé leitað í smiðju eldri CTF-lausna kennir margra grasa en sérílagi stinga stundum í stúf hugmyndir um að nýta núllbæti (e. null byte), dæmigert sett fram sem %00, beinlínis til að skera miskunnarlaust aftan af skráarnafni rétt áður en vélrænni fastri endingu tekst að smella aftan á. Verum samt raunsæ því framsækin og nútímaleg forritaskil hjá jafnt Python, PHP sem öðrum kerfum horfa ætíð tortryggnum augum á núllbæti innvafin slóðum og hafna þeim yfirleitt umsvifalaust. Láttu þér því síst af öllu detta í hug að byggja væntingar þínar á svona úreltri og grárri hjáleið — nema auðvitað að þú sitjir yfir verkefni sem beinlínis prýðir sig af því að vera reist á grunni lúaslegs og gamaldags hugbúnaðarumhverfis.

Ætli forritið sér, við allar aðstæður, að græða hina dæmigerðu .txt-endingu við slóðina sína, mætti fljótlega álykta sem svo að markskrá sem í ofanálag endar einnig nákvæmlega á .txt feli í sér býsna eðlilega og klóka lausn. Það telst mun nærtækari og sáraeinföld nálgun, langt á undan því að vaða blindandi af stað og freista þess að hrista af sér endinguna með öllum tiltækum brögðum.

PHP-síur sem CTF-hliðarspor

Sem hugbúnaðarkerfi skartar PHP svokölluðum straumhjúpum (e. stream wrappers), sem fá föll til að starfa í kringum allt önnur fyrirbæri en almennar textaskrár gera. Algengt CTF-dæmi er:

php://filter/convert.base64-encode/resource=index.php

Fáist viðkvæm og varnarlaus include-skipun, eða hrein og klár lesleið, til þess að sloka þetta í sig, gæti PHP vitanlega stigið skrefið og umsvifalaust Base64-kóðað sjálfan frumkóðann, rétt áður en honum væri í raun varpað á skjáinn. Hér getur gríðarlega góð hjálp verið fólgin, allra helst ef bein og hvatvís innlimun á til að kveikja undir PHP-skránni beint í stað þess sáraeinfalda hjáhjóls að gera hana aðeins sýnilega og auðlesna.

Með sanni má segja að hér sitji afdráttarlaus PHP-sértæk tækni í hásæti, enda vitanlega engin almenn né gjaldgeng lausn gegn rótgrónu slóðaflakki. Henni á líka afskaplega illa við Python-kallið Path.read_text() inni í verkefninu okkar Handbókin. Við nefnum þetta þó svona í framhjáhlaupi hér til þess eins að auðga orðaforða þinn í tengslum við hefðbundin CTF-skrif; kaflinn sem slíkur verður alls ekki hlaðinn af ótölulegum lista af hjúpum.

Göngudæmi: Handbókin

Veltu þér nú yfir í rammann og opnaðu fyrir handbókalistann, taktu síðan skemmtilegt skref og sæktu eina heila skrá með viðkomu hefðbundins og saklauss tengils. Vörpum við svo beiðninni umsvifalaust inn í Repeater og eltumst þar dagsdaglega uppi nákvæmlega hvaða staka file-gildi flaut þar með í för. Djarft gæti svo verið að skipta því snyrtilega út fyrir dæmið ../fixtures/proof.txt og á endanum njóta ávaxtanna og staðfesta að prófmerkið lifi af aðgerðina.

Eftir að heppnin ljómar við okkur með fyrrgreindum hætti má álíta sem svo að stóra kortlagning slóðarinnar liggi greinileg fyrir augum okkar:

manuals/ + ../fixtures/proof.txt
        = fixtures/proof.txt

Væntanlega gætirðu líka hugleitt að spila fram nákvæmlega sama og staðfesta slóðarmynstrinu en yfirfært á sjálfa þá tilteknu markskrá (e. target file) sem verkefnið eitt ber ofarlega í munni:

curl -i -G "$BASE_URL/download" \
  --data-urlencode 'file=../fixtures/<markskrá verkefnisins>'

Gættu samt að því að þessi fyrrgreindi kafli leggur engin lokasvör eða fullmótuð og hnökralaus skráarheiti upp í hendurnar á þér fyrir lokabeiðnina. Lítum á það sem fullnaðarsigur þegar CTF-markmiðinu reynist endanlega komið í höfn, og höldumst því fjarri þeirri fásinnu að hlaupa í blindni áfram til þess eins að ráðast að saklausum skrám uppi á skjánum.

Komi slíkt upp úr kafinu að proof.txt launi velvilja þinn með harðneskjulegu 404-svari, líttu á það óvænta svar í samjöfnuði við viðbrögðin við venjulegri skrá. Hefðirðu kannski gætt betur að hvort viðkomandi forrit væri eitt af þessum sem þjösnast til og troða dæminu manuals/ heilum tvisvar í fangið á þér? Nú, eða bæti við fastri endingu, já eða hver veit, hvort file kunni ef til vill leynt að hafa birst harðkóðað sem lokaður partur af stærra JSON-gildi. Munum samt eitt: ein villa dugar okkur stutt til að afsaka að rjúka til handa og fóta og þeyta inn blöndu af tvíkóðun, algildum slóðum ásamt núllbætum öllum í sömu andránni.

Hvenær tilgátan stenst ekki

Ekki má útiloka að framsetningin file=intro.txt sé vitaskuld einungis klæðskerasniðið auðkenni sem forritið sér alfarið sjálft um að fletta upp af nærgætni inni í sínu eigin harðvíraða, fasta og lokaða korti; um sanna skráarslóð var þar aldrei að ræða. Rekist ../fixtures/proof.txt á sama svellkalda vegg almennrar höfnunar og viðgengst að henda óséðum auðkennum um allar trissur, er víst að slóðaflakkið reynist í senn ósannað ef aukreitis vantar algjörlega eitt einasta gagnlegt dæmi um sjáanlegt skráar-API inni í upprunakóðanum sjálfum.

Sé kerfinu stillt þannig upp að viðkomandi og afgreidd skrá þurfi óhjákvæmilega að koma ætíð upp úr fyrirfram fastmótuðu eða yfirlýstu og skilgreindu afmörkuðu stóru mengi, bregðist Path.name við því að plokka slysalaust út allan snefil af möppuhlutum, nú eða komi beinlínis sú skýra játning úr endanlegri resolve()-mælingu í kærleiksríkan og strangan samjöfnuð beint á móti sjálfri grunnmöppunni, liggur við fótinn að mælir slíkt mjög sterkt gegn því að slóðaflakk sé yfirleitt mögulegt. Hittu vel að, móttekinn 200-kóði með almennum villutexta telst vitaskuld ekki til skráarlestrar; galdurinn er sá að innihald textans þarf fyrst og síðast að eiga nána samsvörun við okkar eina hjartkæru markskrá.

Gætum hins vegar einnig vandlega að réttum orðanotkunum á hlutunum. Að lesa PHP-frumkóða sem Base64-kóðaðan texta í gegnum síu er dæmi um skráarlestur í gegnum hjúp. Kjósum við svo fremur að gína yfir the og láta á okkar snærum eitt stakt include sjá leikandi um dæmið að sparka og keyra í loft upp nýupphlaðna og væna PHP-skrá flokkum við það óneitanlega sem dæmigert tilfelli staðbundinnar skráarinnlimunar (LFI) (Local File Inclusion) og í sama vetfangi ber hann stóra von í sér með möguleika á verulegri kóðakeyrslu (e. Code Execution). Að sækja PDF-skrá úr systurmöppu telst ótvírætt til slóðaflakks. Af nákvæmri orðanotkun má margt læra um hvaða getu forritið hefur í raun sýnt.

Samantekt

Gott og vel, skráð slóðaflakk gengur í skrokk þegar eitthvert utanaðkomandi og afgerandi notandastýrt inntak á slóðarhluta teygir anga sína hressilega alla leið framúr hinni fráteknu grunnmöppu og rennir yfir sér meðal annars .., reynist jafnvel vel búin algildri slóð, tekur mið af leynilegum tákntenglum og öðru ókunnugu og fráleitu sem vel mætti túlka á nákvæman og snjallan veg. Tökum fram LFI sem gengur fyllilega miklu nærri kjarna málsins ef staðbundin skrá reynist beinlínis sköpuð og tekin formlega til fanga inn í vafningsleysi túlks eða jafnvel hjá frambærilegu forriti; gætum þess og höldum hugföstum að stakt og fáheyrt niðurhal í raunveruleika veitir litla vissu eða örugga sýnd að þar gæti fyllilega staðið innlimun til boða.

Förum fyrst hægt í sakirnar og veljum einhverja þekkta skrá með tilbúinni prófskrá á handlegg okkar, til að láta eina og stöðuga slembibreytingu nægja yfir örlitla og fasmikla sveigju á áreitisvöldum slóðarhluta í viðkomandi prófi. Ekki er laust við að hefðbundin kóðun, hliðstæð afkóðun sem og vönduð samræming muni birtast í hrærigraut eða afar fágætum en ólíkum röðunum. Tvíkóðun sem jafnvel er viðloðandi og hlaðin gamaldags og lúnum núllbætum skýtur auðvitað stundum upp kollinum í vörnum fáséðra og fornra hjáleiða; skilyrt algjörlega eðlilegum hvötum inni í forritshögun þegar viðkomandi dularfulla umhverfi býr rækilega upp tilefni eitt slíkra ævintýra. Fallið resolve(), notkun afmarkaðra skráarkorta og það að takmarka inntak við síðasta nafn skrár eru allt mjög öflugar vísbendingar sem draga úr líkum á því að slóðaflakk eigi sér stað.

Í næsta kafla snúum við flæðinu við. Í stað þess að biðja þjóninn að lesa skrá sendir notandinn skrá til hans, og við skoðum hvaða gögn í upphleðslunni stjórna heiti, gerð, innihaldi og birtingu.