JavaScript og öryggislíkan vafrans
Í fyrri köflum sáum við vafrann senda HTTP-beiðnir, túlka HTML og taka við svörum. Vafrinn birtir þó ekki einungis kyrrstæð skjöl. JavaScript getur brugðist við smellum, breytt síðunni, geymt gögn og sent nýjar beiðnir án þess að öll síðan endurhlaðist.
Þetta eykur sannarlega á þægindi vefsins, en skapar jafnframt þann algenga misskilning í CTF-verkefnum: ef JavaScript neitar að senda beiðni er auðvelt að halda að beiðnin sé bönnuð. JavaScript keyrir hins vegar í vafra keppandans. Keppandinn getur lesið kóðann, breytt lifandi síðunni og smíðað HTTP-beiðnina sjálfur.
Verkefnið Faldi hnappurinn verður rauði þráður kaflans. Síðan sýnir hnapp sem á ekki að virkjast fyrr en stigið nær 100. Markmiðið er alls ekki að spila sig upp í 100 stig, heldur öllu fremur að komast að því hvað hnappurinn myndi senda og athuga hvað þjónninn sannreynir í raun.
Vafrinn þinn er hluti af æfingaumhverfinu
Keyrðu aðeins JavaScript og breyttar beiðnir í verkefninu sem kaflinn vísar á eða í eigin staðbundnu umhverfi. Ekki líma óþekktan kóða í Console og ekki flytja kökur eða önnur lotugögn úr raunverulegum síðum yfir í æfingavafrann.
JavaScript glæðir HTML lífi
Í HTML-kaflanum sáum við að HTML lýsir innihaldi og formum en falin eða óvirk eining breytir ekki reglum þjónsins. JavaScript bætir keyrsluhegðun við skjalið á meðan síðan er opin.
Skoðum hnapp og skriftuslóð í HTML:
<p id="score">Stig: 0/100</p>
<button id="reward" disabled>Sækja fána</button>
<script src="/static/app.js"></script>
JavaScript-skráin gæti stjórnað hnappinum:
let score = 0;
const button = document.querySelector("#reward");
button.disabled = score < 100;
Kóðinn finnur hnappinn í DOM og stillir disabled eftir staðbundnu gildi. Þjónninn sér hins vegar ekki hnappinn; hann sér aðeins HTTP-beiðnirnar sem vafrinn sendir.
Upprunakóði og hið lifandi DOM-tré
Þegar vafri fær HTML-svar myndar hann DOM, lifandi hlutalíkan skjalsins. JavaScript getur bætt við einindum, eytt þeim eða breytt eiginleikum þeirra. Þess vegna geta View Source og Elements sýnt ólíka hluti.
View Source sýnir yfirleitt HTML-svarið sem kom frá þjóninum. Elements sýnir núverandi DOM eftir að vafrinn hefur túlkað HTML og JavaScript hefur mögulega breytt því. Sources eða Debugger sýnir JavaScript- og aðrar skrár sem vafrinn sótti.
Í Faldi hnappurinn skaltu byrja í Elements. Þú sérð að hnappurinn er disabled. Ef þú eyðir eiginleikanum geturðu smellt á hnappinn, en breytingin lifir aðeins í þínum vafra og hverfur við endurhleðslu. Stóra spurningin er því ekki hvort hægt sé að breyta lit hnappsins, heldur hvaða HTTP-beiðni smellurinn kallar beinlínis fram.
Farðu næst í Sources eða finndu script src í HTML. JavaScript sem vafrinn þarf að keyra er ekki leyndarmál. Þótt skrá sé löng eða þjöppuð getur leit að orðum eins og fetch, /api/, flag, reward eða auðkenni hnappsins fundið áhugaverðan hluta. DevTools býður oft upp á Pretty print með {}-tákni til að gera þjappaðan kóða læsilegri.
Leitaðu fyrst að tengingunni við þjóninn
Þegar um stór JavaScript-forrit er að ræða er fátítt að nokkuð gagnist að lesa hverja einustu línu allt frá byrjun. Finndu slóðir, fetch()-köll, eyðublöð og atburðahlustara. Rektu því næst einungis þann kóða sem myndar þá beiðni sem varðar þig.
Stundum vísar JavaScript-skrá í frumkort (e. source map) sem DevTools notar til að sýna upprunalegri skráarnöfn og kóða. Ef frumkort er þegar tengt síðunni er eðlilegt að skoða það í CTF. Það er hins vegar ekki ástæða til að byrja að giska á eða skanna fjölda .map-slóða sem verkefnið nefnir ekki.
Atburðir tengja aðgerðir við kóða
Atburður (e. event) er eitthvað sem gerist í vafranum: notandi smellir, slær inn texta, sendir eyðublað eða síðan lýkur hleðslu. JavaScript getur skráð fall sem á að keyra þegar ákveðinn atburður verður:
button.addEventListener("click", saekjaVerdlaun);
Þetta þýðir að smellur á hnappinn kallar á fallið saekjaVerdlaun. Fallið gæti breytt DOM án nokkurra netsamskipta, eða það gæti sent HTTP-beiðni. Network-flipinn segir okkur hvort beiðni fór í raun af stað.
Búi óvirkur hnappur ekki til neina færslu í Network er það auðvitað ofur eðlilegt: vafrinn framkvæmdi ekki smellinn. Ef þú fjarlægir disabled og smellur aftur geturðu séð beiðnina sem kóðinn var tilbúinn að senda. En ef líkaminn inniheldur enn score: 0 hefur breytingin á HTML ekki breytt JavaScript-breytunni.
Hér reynist eftirfarandi aðgreining afar gagnleg:
HTML-eiginleiki hnapps ≠ JavaScript-breyta ≠ gögn sem þjónn geymir
Þessi gildi geta vissulega tengst, en eru aldrei ósjálfrátt sami hluturinn.
fetch() sendir HTTP-beiðni
fetch() er vafraforritaskil sem JavaScript notar til að senda HTTP-beiðni. Einföld GET-beiðni getur verið:
const response = await fetch("/api/status");
const data = await response.json();
console.log(data);
fetch() skilar ekki svarlíkamanum samstundis. Netsamskipti taka tíma, svo await bíður inni í fallinu þar til svarið kemur. Þetta kallast ósamstillt keyrsla (e. asynchronous execution): vafrinn getur áfram sinnt öðrum atburðum á meðan beðið er.
Til að nota await er fallið yfirleitt merkt async:
async function saekjaStodu() {
const response = await fetch("/api/status");
const data = await response.json();
return data;
}
Við þurfum ekki að sökkva okkur ofan í loforð (e. promises) né atburðalykkju (e. event loop) JavaScript-málsins hér. Fyrir CTF-rannsókn nægir fyrst að finna slóðina og valkostina sem fetch() fær.
Beiðni með aðferð, hausum og JSON-líkama gæti litið svona út:
const response = await fetch("/api/reward", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ score: score })
});
const result = await response.json();
Þetta samsvarar í grófum dráttum:
POST /api/reward HTTP/1.1
Host: nordurmarkadur.example
Content-Type: application/json
{"score":0}
JSON.stringify() breytir JavaScript-gildi í JSON-texta. response.json() gerir hið gagnstæða fyrir svarlíkamann og skilar JavaScript-gildi.
fetch() kastar ekki villu fyrir alla HTTP-villukóða
fetch() getur skilað venjulegu Response-gildi fyrir 404, 403 eða 500. Kóðinn þarf að skoða response.status eða response.ok til að greina slíkt. Ef síðan sýnir aðeins „villa“ skaltu skoða raunverulegan stöðukóða og svarlíkama í Network.
Viðmótið sendir einungis eina mögulega beiðni
JavaScriptið á síðu sýnir hvað höfundur bjóst við að venjulegur vafri myndi senda. Það skilgreinir ekki allar beiðnir sem hægt er að senda. Sama API-beiðni má smíða með curl:
curl -i -X POST https://nordurmarkadur.example/api/reward \
-H 'Content-Type: application/json' \
--data '{"score":0}'
Eða í Burp Repeater:
POST /api/reward HTTP/1.1
Host: nordurmarkadur.example
Content-Type: application/json
{"score":0}
Þegar þú breytir 0 í annað gildi ertu ekki að „hakka JavaScript“. Þú ert að senda aðra HTTP-beiðni til þjónsins. Aðeins svar þjónsins sker úr um hvort hann beri traust til gildisins.
Sama gildir um falda eyðublaðsreiti, maxlength-skilyrði, fellilista og óvirka hnappa. Þeir stýra því hvað venjulegt viðmót sendir. Í CTF gefa þeir oft tilgátu um áhugavert inntak, en við staðfestum hana með afmarkaðri beiðni og samanburði á svörum.
Console gagnast sem lítið tilraunaborð
Console leyfir okkur að skoða DOM og keyra stutt JavaScript í uppruna síðunnar. Til dæmis:
document.querySelector("#reward").disabled
skilar stöðu hnappsins. Þetta:
document.querySelector("#reward").disabled = false
virkjar hann staðbundið. Og þetta sýnir allar skrár sem síðan hleður sem script:
[...document.scripts].map(script => script.src)
Einnig má prófa afmarkað fetch()-kall í Console:
await fetch("/api/status").then(response => response.text())
Sliíkt kall notfærir sér kökur og uppruna síðunnar nákvæmlega samkvæmt reglum vafrans. Það getur því hegðað sér öðruvísi en curl sem hefur enga köku nema þú bætir henni við.
Stjórnborðið (Console) er vel liðtækt fyrir stakar tilraunir, en hyggilegra er að varðveita endurtakanlegar lausnir í curl, Repeater eða litlu forriti. Skipun sem aðeins er til í Console-sögu er erfiðari að útskýra og keyra aftur síðar.
Hvar geymir vafrinn ástand sín\?
Vefsíða getur haldið utan um ástand á fleiri en einum stað, meðal annars í vafrageymslu (e. web storage). Hér gefur að líta algengustu geymslustaðina sem sjást í DevTools:
| Geymsla | Líftími í grófum dráttum | Hvernig hún fer til þjónsins |
|---|---|---|
| JavaScript-breyta | Þar til síða endurhleðst eða kóði breytir henni | Aðeins ef JavaScript setur hana í beiðni |
| DOM | Þar til síða eða JavaScript breytir því | Aðeins ef JavaScript eða eyðublað les gildið |
sessionStorage |
Yfirleitt meðan viðkomandi vafraflipi lifir | Aðeins ef JavaScript les og sendir gildið |
localStorage |
Getur lifað endurhleðslu og endurræsingu vafra | Aðeins ef JavaScript les og sendir gildið |
| Vefkaka | Samkvæmt gildistíma og reglum kökunnar | Vafrinn getur sent hana sjálfkrafa með HTTP-beiðni |
localStorage og sessionStorage geyma strengi undir heitum. Í Console má skoða þau:
localStorage.getItem("score")
sessionStorage.getItem("stage")
og í DevTools birtast þau undir Application eða Storage. Keppandi getur breytt eigin vafrageymslu, svo gildi þar er ekki sjálfkrafa sönnun fyrir þjóninum. En ef JavaScript les gildið og sendir það óbreytt getur breytingin orðið hluti af áhugaverðri beiðni.
Kökur eru öðruvísi vegna þess að vafrinn getur sent þær sjálfkrafa. Sumar kökur eru girtar af fyrir JavaScript jafnvel þótt þær birtist í DevTools. Við skoðum kökureglur, lotur og lotuauðkenni nánar síðar. Í bili skaltu muna að localStorage, sessionStorage og kökur eru þrír ólíkir hlutir.
Uppruni \(*origin*\): skema, hýsir og tengi
Vafrinn þarf reglur um hvernig síður frá ólíkum stöðum mega eiga samskipti. Uppruni (e. origin) er samsetning þriggja atriða:
skema + hýsir + tengi
Fyrir https://nordurmarkadur.example/account er uppruninn í grófum dráttum:
https + nordurmarkadur.example + 443
Slóðarhlutinn /account tilheyrir ekki upprunanum. Þess vegna eru þessar slóðir af sama uppruna:
https://nordurmarkadur.example/
https://nordurmarkadur.example/search?q=ull
https://nordurmarkadur.example/api/reward
En hver þessara breytinga býr til annan uppruna:
http://nordurmarkadur.example/ # annað skema
https://admin.nordurmarkadur.example/ # annar hýsir
https://nordurmarkadur.example:8443/ # önnur tengi
Þetta er nákvæmur samanburður. Hvorki sameiginlegt aðallén né sami netþjónn á bak við tjöldin fá sjálfkrafa tvo uppruna til að teljast eins.
Sama-upprunaregla
Sama-upprunareglan (e. same-origin policy) takmarkar hvernig JavaScript frá einum uppruna getur lesið gögn frá öðrum. Án hennar gæti handahófskennd síða reynt að lesa innskráðan tölvupóst eða bankaupplýsingar úr öðrum vafraflipa.
Sama-upprunareglan felur þó ekki í sér algert bann við öllum beiðnum á milli uppruna. Vafri má til dæmis birta mynd frá öðrum uppruna og HTML-eyðublað getur undir ákveðnum skilyrðum sent beiðni þangað. Meginspurningin er oft hvort JavaScript fær að lesa svarið.
Ef þetta JavaScript keyrir á https://nordurmarkadur.example:
const response = await fetch("https://api.annad.example/profile");
const data = await response.json();
getur vafrinn stöðvað JavaScript frá því að lesa svarið vegna sama-upprunareglunnar. Network getur samt sýnt að beiðni var send. Af þeim sökum verður villa um uppruna í Console ekki metin sem sönnun þess að þjónninn hafi farið varhluta af beiðninni.
Sama-upprunareglan er vafraregla. curl og Burp Repeater eru ekki vafrar og framfylgja henni ekki. Þessum verkfærum er frjálst að lesa hvaða HTTP-svar sem þjónninn mætti senda þeim. Þau öðlast þó ekki sjálfkrafa kökur, auðkenningu eða leyfi sem vafrinn hafði.
CORS heimilar vafrakóða lestur
CORS (e. Cross-Origin Resource Sharing) er leið fyrir þjón að segja vafra að tiltekinn annar uppruni megi lesa svar. Svarhaus getur til dæmis verið:
Access-Control-Allow-Origin: https://nordurmarkadur.example
Komi til ákveðinna beiðna sendir vafrinn fyrst svokallaða OPTIONS-forkönnun (e. preflight) til þess að ganga úr skugga um að aðferðin og hausarnir séu leyfðir. Í Network getur því birst OPTIONS rétt á undan PUT eða POST.
CORS er ekki aðgangsstýring þjónsins. Það stýrir því hvort vafri afhendir JavaScript svar frá öðrum uppruna. Ef leynileg API-leið skilar gögnum til hvers sem sendir beina beiðni verður hún ekki örugg aðeins af því að Access-Control-Allow-Origin vantar. curl þarf ekki samþykki CORS.
Í gagnaglímum er þetta harla gagnleg vísbending: ef eina „hindrunin“ er CORS-villa í Console skaltu skoða beiðnina og svarið í Network eða endurtaka heimilaða verkefnisbeiðni í Repeater. Ef þjónninn sjálfur svarar 401 eða 403 er hins vegar önnur athugun í gangi; CORS-hjáveita ein leysir hana ekki.
CSP ræður því hverju vafrinn fær hlaðið og keyrt
Öryggisstefna efnis (e. Content Security Policy, CSP) kemur oft í svarhaus og takmarkar hvaðan síða má hlaða eða keyra efni:
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'
script-src 'self' leyfir í þessu dæmi skriftur frá sama uppruna en ekki handahófskenndar ytri skriftur. connect-src 'self' takmarkar meðal annars hvert fetch() og WebSocket-tengingar mega fara.
Þegar CSP stöðvar aðgerð birtir Console yfirleitt skýra villu. Mun hyggilegra er að kynna sér stefnuna sjálfa í stað þess að þreifa sig áfram með langan lista af JavaScript-brotum. Stefnan ein og sér getur þannig gefið til kynna hvort tiltekin lausnarleið sé afar ólíkleg eða hvort nauðsynlegt reynist að nýta sér þær JavaScript-skrár sem þegar eru heimilaðar.
Eins og CORS er CSP framfylgt af vafranum. Hún kemur ekki í veg fyrir að þú sendir beina HTTP-beiðni með curl eða Repeater á heimilaðan CTF-endapunkt. Hún er varnarlag gegn tilteknum vafraaðgerðum, ekki lýsing á aðgangsstýringu API-leiðarinnar.
Göngudæmi: fram hjá falda hnappinum
Opnaðu Faldi hnappurinn með Network og Sources tilbúin. Taktu fyrst eftir því hvað gerist ekki: venjulegur smellur á óvirka hnappinn sendir enga beiðni. Network-listinn helst óbreyttur. Þetta segir aðeins að viðmótið stöðvaði smellinn.
Skoðaðu hnappinn í Elements og finndu JavaScript-skrána sem tengist honum. Leitaðu að auðkenni hnappsins eða fetch. Skráðu slóðina, aðferðina, Content-Type og lögun JSON-líkamans. Finndu líka hvaðan score-gildið kemur. Er það reiknað af þjóninum, lesið úr köku eða aðeins staðbundin JavaScript-breyta?
Þú hefur fulla heimild til þess að fjarlægja disabled í Elements og smella, allt í því skyni að sjá hvernig grunnbeiðnin lítur út. Svarið ætti ekki að gefa fána ef líkaminn sendir enn núll stig. Sendu beiðnina síðan í Repeater eða afritaðu hana sem curl og breyttu aðeins score-reitnum í gildið sem viðmótið segist krefjast:
BASE_URL='https://slod-verkefnisins.example'
curl -i -X POST "$BASE_URL/api/reward" \
-H 'Content-Type: application/json' \
--data '{"score":<gildið sem viðmótið krefst>}'
Vertu þess þó minnug(ur) að dæmaslóðin er frátekin; notaðu ætíð þína eigin Örvangursslóð.
Berðu svarið saman við grunnbeiðnina. Ef breytt notandastýrt gildi nægir hefur þjónninn treyst fullyrðingu biðlarans um stig. Ef þjónninn reiknar stigin sjálfur og svarar áfram með höfnun var disabled aðeins fyrsta vísbending, ekki veikleikinn. Þá ættirðu ekki að fullyrða að aðgangsstýring hafi verið brotin.
Þú getur einnig sent fetch() úr Console, en Repeater eða curl sýnir skýrar að lausnin byggist á HTTP-beiðninni en ekki töfrabreytingu á hnappnum. Gættu þess að varðveita minnstu mögulegu beiðni sem skilar endurtekinni niðurstöðu og skilaðu fánanum í keppniskerfið án þess að birta hann í opinberum glósum.
Hagnýtt vinnuferli í JavaScript-verkefni
Þegar viðmót virðist stöðva aðgerð skaltu ekki byrja á því að endurskrifa allt JavaScriptið. Athugaðu fyrst hvort smellurinn sendir beiðni. Ef svo er skaltu skoða hana í Network og endurtaka hana. Ef ekki skaltu finna atburðahlustarann og rekja hvaða skilyrði þarf til að beiðnin verði búin til.
Beindu sjónum þínum að þeim landamærum þar sem staðbundin gildi færast yfir í HTTP: fetch(), XMLHttpRequest, eyðublaðssending eða WebSocket. Spyrðu þig í kjölfarið hvort þjónninn reikni gildið eða staðfesti það á eigin spýtur. Svar þjónsins er sönnunin. HTML-eiginleiki, JavaScript-skilyrði og texti í localStorage eru aðeins vísbendingar þar til beiðni sýnir áhrif þeirra.
Ef hegðun verður óskýr skaltu endurhlaða síðuna, hreinsa Network og breyta einu atriði. Breyting í Elements, Console-breyta og breytt Repeater-beiðni eru þrjár ólíkar tilraunir. Skráðu hver þeirra breytti svarinu.
Hvað tökum við með okkur?
HTML lýsir uppbyggingu, CSS útliti og JavaScript hegðun í vafranum. DOM er lifandi útgáfa síðunnar og getur verið ólíkt upphaflega HTML-svarinu. Atburður getur kallað á fetch() sem myndar HTTP-beiðni, en keppandi getur smíðað sömu beiðni án viðmótsins.
localStorage, sessionStorage, JavaScript-breytur og kökur geyma ástand á ólíkan hátt. Sama-upprunareglan, CORS og CSP stjórna ákveðinni hegðun vafrans, en koma ekki í stað athugana sem vefþjónn þarf að framkvæma. Í CTF lesum við þessar reglur sem hluta af umhverfinu: þær segja hvaða leiðir vafrinn leyfir og hvenær beint HTTP-tól gefur skýrari mynd.
Næst færum við okkur yfir í auðkenning og endurheimt aðgangs. Þar mun aðgreiningin milli viðmóts og þjóns sýna sig verða enn mikilvægari: skilaboð á innskráningarsíðu geta lekið upplýsingum, endurheimtartákn geta verið fyrirsjáanleg og röð skrefa getur skipt meira máli en það sem JavaScript birtir.