Hoppa yfir í efnið

Viðskiptarök og kapphlaup

Margir vefveikleikar byrja ekki á sérstöku tákni. Beiðnin getur verið fullkomlega gilt JSON, SQL er stikað og notandinn rétt innskráður, en röð aðgerða eða gildi brýtur samt reglu forritsins.

Viðskiptarök (e. business logic) eru reglurnar sem segja hvað aðgerð merkir í tilteknu kerfi: verð má ekki verða neikvætt, afsláttarkóði gildir einu sinni og pöntun má ekki teljast greidd áður en greiðsluskrefi lýkur. Í CTF leitum við að muninum á því sem forritið gerir ráð fyrir og því sem HTTP-biðlari getur í raun sent.

Verkefnið Tvisvar velkomin hefur afsláttarkóða sem á að virka einu sinni. Ein beiðni lækkar ekki verðið nógu mikið til að fá fána. Tvær beiðnir sem ná athuguninni næstum samtímis geta báðar fengið afsláttinn áður en kerfið merkir kóðann notaðan.

Aðeins tvær beiðnir í gervikörfuna

Kapphlaupspróf geta valdið miklu álagi ef þau eru keyrð í lykkju. Verkefnið er hannað fyrir nákvæmlega tvær samhliða beiðnir í eigin gervilotu. Ekki fjölga þráðum, prófa raunveruleg kaup eða senda beiðnir á önnur kerfi.

Reglan sem á alltaf að halda

Óbreytanleiki (e. invariant) er regla sem á að halda fyrir og eftir aðgerð. Í gerviverslun gætu reglurnar verið:

magn ≥ 1
heildarverð ≥ 0
afsláttur ≤ heildarverð
WELCOME-notkun ≤ 1
greidd pöntun hefur staðfesta gervigreiðslu

Þetta eru ekki HTTP-reglur. Vafrinn getur sent quantity=-1 þótt fellilistinn bjóði aðeins 1 til 10. Þjónninn getur fengið tvær beiðnir á sama augnabliki þótt hnappurinn gráni eftir fyrsta smell.

Gott CTF-próf byrjar því á að skrifa regluna í einni setningu. „WELCOME má lækka sömu körfu einu sinni“ er prófanleg fullyrðing. „Kannski er eitthvað skrýtið með afslátt“ er of óljóst.

Viðmótið sýnir eina leið, HTTP margar

Greiðsluferli gæti litið svona út:

karfa → heimilisfang → sending → gervigreiðsla → staðfesting

Þetta er stöðuvél: hvert skref breytir stöðu og aðeins ákveðin næstu skref eiga að vera leyfð.

JavaScript getur falið Staðfesta-hnappinn þar til fyrri skrefum lýkur. En slóðirnar á bak við hnappana eru sjálfstæðar HTTP-beiðnir:

POST /checkout/address
POST /checkout/shipping
POST /checkout/pay
POST /checkout/confirm

Í lokuðu verkefni má taka lögmæta grunnbeiðni og spyrja:

  • Hvað gerist ef seinna skref er kallað beint?
  • Má endurtaka einnota skref?
  • Er hægt að fara aftur og breyta gildi eftir staðfestingu?
  • Tengir þjónninn skrefin við sömu gervikörfu?

Við prófum aðeins endurstillanleg gervigögn. Að bein slóð skili 200 er ekki nóg; lokastaðan þarf að brjóta skilgreindan óbreytanleika.

Verð og magn

Vefviðmót gæti sent:

POST /cart/update HTTP/1.1
Content-Type: application/json

{"product_id":17,"quantity":2}

product_id og quantity eru notandastýrð. Í CTF eru hagnýt jaðargildi:

0
-1
hæsta gildi sem verkefnið nefnir
næsta gildi fyrir ofan þau mörk

Ekki byrja á gríðarstórri heiltölu sem gæti valdið auðlindavanda. Við viljum greina rökreglu, ekki láta þjóninn búa til milljarð hluta.

Ef quantity=-1 hækkar inneign eða dregur neikvætt verð frá heild hefur sama reitur verið notaður í tveimur merkingum. Ef þjónninn skilar aðeins skýrri höfnun og karfan helst óbreytt er neikvæða magntilgátan lokuð.

Hver stjórnar einingarverðinu?

Sum viðmót senda bæði magn og verð:

{"product_id":17,"quantity":2,"unit_price":12900}

Skoðaðu hvort svar eða síðari karfa breytist þegar aðeins unit_price breytist. Ef þjónninn sækir verðið sjálfur er reiturinn kannski aðeins fyrir birtingu eða hunsaður. Ef notandastýrða verðið verður að heild er það brotinn óbreytanleiki.

Við fullyrðum ekki veikleika aðeins vegna þess að reitur sé í JSON. Sýndu áhrif í þjónsvarinu eða lokastöðunni.

Gjaldmiðlar og námundun

Peningar geta verið táknaðir sem heilar minnstu einingar eða tugabrot. Flotkommutölur hafa ekki nákvæma framsetningu á öllum tugabrotum:

0.1 + 0.2 != 0.3

Í CTF getur munur komið upp þegar afsláttur er reiknaður í einum gjaldmiðli og námundaður í öðrum, eða þegar hver lína er námunduð áður en heild er reiknuð. Leitaðu að verkefnissértækri vísbendingu um sent, aura, gengi eða mörk; ekki prófa þúsundir tugabrota.

Tvö svör sem sýna 49.99 og 50.00 geta skipt máli ef flaggmörkin eru nákvæmlega 50. Skráðu bæði hráa JSON-gildið og textann sem viðmótið birtir. Vafrinn getur sniðið tölu án þess að breyta gildinu hjá þjóninum.

Afslættir og endurspilun

Einnota afsláttur hefur að minnsta kosti þrjú gögn:

kóði + karfa/notandi + hvort hann hafi verið notaður

Endurspilun (e. request replay) er að senda sömu beiðni aftur. Fyrsta prófið er einfalt:

POST /coupon/apply code=WELCOME
POST /coupon/apply code=WELCOME

Ef seinna svarið segir „þegar notað“ og heildin breytist aðeins einu sinni virkar röðbundin endurspilun ekki. Það útilokar þó ekki kapphlaup, því seinni beiðnin kom eftir að fyrri hafði lokið.

Önnur viðskiptarök geta falist í að stafla afsláttum, nota sama kóða á nýja körfu eða beita afslætti eftir að heild var þegar staðfest. Veldu aðeins þá tilgátu sem lýsing og beiðnir verkefnisins styðja.

Kapphlaup

Kapphlaup (e. race condition) verður þegar niðurstaða ræðst af tímasetningu eða röð samhliða aðgerða. Algengt mynstur er „athuga og framkvæma“:

if not coupon_used(cart_id):
    apply_discount(cart_id)
    mark_coupon_used(cart_id)

Með einni beiðni lítur þetta rétt út. Tvær beiðnir geta hins vegar fléttast:

tími  beiðni A                         beiðni B
────  ──────────────────────────────   ──────────────────────────────
 t1   les: coupon_used = false
 t2                                      les: coupon_used = false
 t3   lækkar verð
 t4                                      lækkar verð
 t5   merkir notað
 t6                                      merkir notað

Báðar tóku ákvörðun út frá gamalli stöðu. Þetta er einnig kallað athugun-notkun kapphlaup (e. time-of-check to time-of-use, TOCTOU).

Sýnilegur biðtími í kóða getur stækkað gluggann í kennsluverkefni, en raunveruleg kapphlaup þurfa ekki sleep(). Gagnagrunnsaðgerðir, netkall eða fleiri vinnsluferli geta skapað náttúrulegt bil.

Samhliða er annað en hratt í röð

Þessar beiðnir eru ekki samhliða:

session.post(url, data=data)
session.post(url, data=data)

Önnur byrjar eftir að sú fyrri skilar. Til að prófa kapphlaup þurfa beiðnirnar að bíða við sama ræsipunkt og fara af stað nær samtímis.

Python hefur threading.Barrier fyrir slíka samstillingu:

from concurrent.futures import ThreadPoolExecutor
from threading import Barrier

barrier = Barrier(2)


def apply_once():
    barrier.wait()
    return send_coupon_request()


with ThreadPoolExecutor(max_workers=2) as pool:
    responses = list(pool.map(lambda _: apply_once(), range(2)))

Kóðinn sendir nákvæmlega tvær beiðnir. Barrier(2) lætur báða þræði bíða þar til þeir eru tilbúnir. send_coupon_request() stendur hér fyrir eina beiðni með fyrirfram stofnaðri gervilotu; full útgáfa kemur í göngudæminu hér á eftir.

Burp Repeater getur einnig sent hóp beiðna samhliða í útgáfum sem styðja Send group in parallel. Staðfestu að báðar beiðnir hafi sömu lotuköku og sama körfuauðkenni.

Göngudæmi: Tvisvar velkomin

Opnaðu verkefnið og skoðaðu upphafsverð, afsláttarupphæð og flaggmörk. Notaðu /lab/reset áður en þú prófar svo gervikarfan sé í þekktri stöðu.

Sendu fyrst eina WELCOME-beiðni handvirkt. Sæktu körfuna aftur og staðfestu að ein notkun lækki verðið en nái ekki flaggmörkum. Önnur beiðni í röð á að fá höfnun. Þetta staðfestir ætlaða reglu og sýnir að venjuleg endurspilun nægir ekki.

Endurstilltu í nýja lotu. Eftirfarandi beinagrind heldur utan um kökuna og sendir nákvæmlega tvær beiðnir samhliða:

import requests
from concurrent.futures import ThreadPoolExecutor
from threading import Barrier

base_url = "https://slod-verkefnisins.example"
session = requests.Session()
session.post(f"{base_url}/lab/reset", timeout=5)
cookies = session.cookies.get_dict()

barrier = Barrier(2)


def worker():
    barrier.wait()
    return requests.post(
        f"{base_url}/coupon/apply",
        data={"code": "WELCOME"},
        cookies=cookies,
        timeout=5,
    )


with ThreadPoolExecutor(max_workers=2) as pool:
    futures = [pool.submit(worker) for _ in range(2)]
    responses = [future.result() for future in futures]

for response in responses:
    print(response.status_code, response.text)

print(session.get(f"{base_url}/cart", timeout=5).text)

Skiptu fráteknu dæmaslóðinni út fyrir úthlutaða Örvangursslóð. Ekki hækka max_workers eða setja kallið í lykkju.

Ef báðar beiðnir fá samþykki og lokaverðið lækkar tvisvar hefur kapphlaupið brotið einnota-regluna. Fáninn birtist aðeins þegar tilgreind mörk nást.

Ef aðeins ein beiðni fær afslátt skaltu nota /lab/reset og prófa aftur í mesta lagi fáein skipti. Stöðug höfnun getur þýtt að glugginn sé of lítill eða verkefnið rangt stillt. Tilkynntu vandann í stað þess að fjölga þráðum.

Atómískar aðgerðir mæla gegn veikleika

Atómísk aðgerð (e. atomic operation) birtist öðrum samhliða aðgerðum sem eitt óskipt skref. Gagnagrunnskóði gæti til dæmis uppfært aðeins ef kóðinn er enn ónotaður og athugað hvort ein röð breyttist.

Ef frumkóði sýnir eina slíka skilyrta uppfærslu, eða gagnagrunnsfærslu (e. database transaction) sem læsir viðkomandi körfu meðan bæði athugun og breyting fara fram, er einfalt tvíbeiðnakapphlaup mótsagt. Orðið „transaction“ eitt er þó ekki nóg. Tvær aðskildar færslur með bili á milli geta enn kapphlaupið.

Í HTTP-svörum er 409 Conflict fyrir aðra beiðnina ásamt aðeins einni verðbreytingu sterk vísbending um að þjónninn hafi greint áreksturinn. Tvö 200-svör eru ekki ein og sér sönnun; sæktu lokastöðuna og teldu raunveruleg áhrif.

Að sleppa eða endurtaka skref

Kapphlaup er aðeins ein tegund viðskiptarökvillu. Sama vinnuferli má prófa með fáum afmörkuðum breytingum:

  • kalla staðfestingu áður en gervigreiðsla er skráð,
  • endursenda einnota boð eftir að það var notað,
  • breyta magni eftir að verð var reiknað,
  • nota afslátt á aðra körfu en hann var gefinn fyrir,
  • eða senda neikvætt gildi þar sem viðmótið býður aðeins jákvætt.

Veldu eina reglu og eina mælingu. Ef þú sleppir greiðsluskrefi en pöntunin helst pending hefur óbreytanleikinn ekki brotnað. Ef hún verður paid án tilbúinnar greiðslu er áhrif sýnilegt.

CTF-lausn á að lýsa röð beiðna, ekki aðeins lokagildinu. Viðskiptarök eru ferli og endurtakanleg röð er hluti sönnunarinnar.

Vísbendingar gegn veikleikum

Jaðargildi sem þjónninn hafnar og skilur stöðuna óbreytta styður ekki veikleika. Tvöföld beiðni sem fær tvö 200-svör en aðeins einn afslátt getur verið idempotent aðgerð. Sýnilegur hnappur sem má smella tvisvar segir lítið nema þjónsstaðan breytist tvisvar.

Föst verð sótt hjá þjóninum, heiltölumörk á magni og skýr stöðuvél geta lokað einföldum tilgátum. Atómísk skilyrt uppfærsla og aðeins ein breytt röð mæla gegn check-then-use kapphlaupi.

Tímasveifla er heldur ekki sönnun. Sýndu brotinn óbreytanleika: tvo afslætti, óleyfilega stöðu eða endanlegt gildi sem venjulegt flæði nær ekki.

Samantekt

Viðskiptarök segja hvaða gildi, röð og endurtekning aðgerða eru leyfð. Veikleiki getur falist í neikvæðu magni, notandastýrðu verði, námundun, skrefi sem má sleppa eða einnota aðgerð sem má endurtaka.

Kapphlaup verður þegar tvær beiðnir taka ákvörðun út frá sömu gömlu stöðu. Röðbundin endurspilun prófar ekki það sama og samhliða sending. Í lokuðu verkefni notum við Barrier, nákvæmlega tvo þræði og eina gervilotu, og staðfestum áhrifin í lokastöðunni.

Næsti kafli sameinar mörg atriði vefleiðarinnar í eitt Norðurmarkaðsverkefni. Þar þarf að lesa beiðnir, halda utan um lotu, greina IDOR og XSS-samhengi og staðfesta eina þjónshliðarsprautun með afmörkuðu prófi.