15 KiB
Het wosuid-lab — root-RCE zonder setuid-bit
foowosd een bewust kwetsbare daemon die root is omdat hij als *GESTART* als
root (poort 2344, standaard alleen loopback)
foowosc het exploit: verandert een stack-overloop in een **root**-shell door
shellcode uit te voeren — dezelfde 23 bytes die de user-level-
`food`-daemon in het hoofdlab kraakten
Dit is het derde lab in de serie. Dezelfde exploit-werktuigketen, dezelfde stijl, één fundamenteel verschil:
| Lab | hoe het doelproces root wordt | uid=0(root)-shell? |
|---|---|---|
| food/fooc | nooit — het is een gewone user-daemon | nee |
| foosd/foosc | de SUID-bit (chmod u+s) — euid 0, ruid 1000 |
ja (vereist setreuid in de shellcode, want bash reset euid→ruid) |
| foowosd/foowosc | geen — root start de daemon (sudo / systemd User=root) |
ja (gewone execve-shellcode) |
De setuid-bit is een transportmiddel voor privileges — niet de privileges
zelf. Een daemon die door root is gestart, heeft reële, effectieve en
opgeslagen uid allemaal 0. Voor de kernel is dat root, punt uit; hij kan en wil
niet weten of het proces daar via +s op een bestand of via sudo ./foowosd
is gekomen. De overloop in een root-gestarte daemon is dus een root-exploit — "ik
heb geen SUID-binaries" is niet hetzelfde als "ik ben niet exploiteerbaar".
Dat is de hele les van dit lab. Al het andere hieronder is het mechanisme.
Snelle start (wat de gebruiker vroeg)
cd wosuid
make # bouwt de daemon, het exploit en de test-harness
Het echte werk — draai de daemon als root
sudo make run-root # start foowosd als uid 0 (proces, geen bestandstoestand)
make test-root # elke techniek moet nu uid=0(root) geven
Geen sudo? Het identieke kernelpad via een user-namespace
make run-root-ns # uid 0 in een user-namespace — geen wachtwoord nodig
make test-root # dezelfde oordelen; gebruikt door CI en iedereen zonder sudo
Baseline — daemon als je normale gebruiker (nergens root)
make run # foowosd draait met jouw uids
make test # exploits landen shells, maar `root` wordt verwacht als MISSING
Opruimritueel (altijd: dit is een root-shell-lab)
make stop
foowosd is niet setuid, en niets in deze map doet ooit chmod +s — dat is
het punt. De gevaarlijke toestand is het proces, niet het bestand.
Wanneer je wordt gezegd de SUID-bit te zetten
Dat zul je niet worden. Dit lab heeft bewust geen SUID-bit:
foowosdwordt gebouwd, is eigendom van jou en heeft gewone toestanden als elk ander programma.- Hij wordt root zoals echte daemons dat doen — door gestart te worden door root.
make run-rootgebruiktsudovoor precies dat, enmake run-root-nsregelt een echte uid-0-proces zonder ook maar iets daarvan.
De Suid-bit hoort bij het zuster-lab (foosd). Het contrast tussen de twee
is de leerstof:
- SUID-lab: de bit geeft euid 0, maar ruid 1000 →
execve("/bin/sh")wordt gedegradeerd door de wacht van bash (euid != ruid→ reset) → de shellcode moet eerstsetreuid(0,0)aanroepen (32-byte-payload). - Dit lab: root start het proces → ruid == euid == 0 → de wacht heeft
niets om te resetten → de gewone 23-byte
execve-shellcode houdt root.
Dezelfde overloop. Dezelfde techniek. Andere oorsprong van privileges, andere payload-vorm. Dat is de les in het klein.
Het protocol
Welke client er ook verbindt, foowosd begroet hem met:
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banner + leaks)
BUF=0x7ffd… (het bufferadres)
ids=euid/ruid is het "ben ik root?"-kanaal. foowosc print een luide
waarschuwing wanneer euid niet 0 is (dus je startte de daemon als gewone
gebruiker): de payload landt nog steeds, maar de shell wordt een
gebruiker-shell, en het exploit "kapot" noemen zou fout zijn — hij escaleert
alleen niet.
Spellingsnoot:
ids=, nieteuid=/ruid=. De test-harness bewijst een levendeiddoor de letterlijke vormuid=NNN(te matchen, dus het banner mag nooit een deelstring bevatten die zelf aan de controle voldoet. (In het SUID-lab produceerde precies die val een spectaculaire fout-positief.)
De exploit-technieken (foowosc -t …)
Alle vier de exploit-paden hieronder werken tegen foowosd. Wanneer de daemon root is, geeft alles dat iets spawnt root — in tegenstelling tot het SUID-lab, waar ret2win/ret2libc stilletjes door de wacht van bash naar uid 1000 werden gedegradeerd. Hier is er geen mismatch om te bewaken.
-t |
wat er gebeurt | wanneer de daemon root is |
|---|---|---|
shellcode |
23-byte execve("/bin/sh", NULL, NULL) draait op de stack. |
root-shell (standaard) |
ret2win |
sprong naar win() → execl("/bin/sh") |
root-shell |
ret2libc |
ROP: pop rdi; ret → "/bin/sh" → system() |
root-shell |
demo |
alleen rommel-overloop — verwacht een SIGSEGV in de daemonlog | crash, volgens ontwerp |
leak |
print alleen leaks, stuurt geen payload | n.v.t. |
./foowosc -t shellcode # interactief; standaardtarget 127.0.0.1:2344
./foowosc -t shellcode -n # sturen en rapporteren, geen interactieve sessie
Een succesvolle interactieve sessie schakelt je terminal door naar de shell op
het slachtoffer — er is precies één shell in het beeld, en dat is /bin/sh
die als root binnenin foowosd draait. Typ id om uid=0(root) te zien.
Waarom er hier geen ret2win-root-techniek is
foosc had er één — die sprong naar een win_root() die setreuid(0,0) aanriep
vóór exec, omdat een setuid-proces draaide met een reële uid die nog 1000 zei.
Een root-gestart proces heeft de reële uid al 0; er valt niets op te ruimen,
dus de extra functie en techniek zouden niets leren. Verwijderd.
Wat foowosc doet, stap voor stap
- Statische analyse —
objdump -dvan./foowosd. Vindtvulnerable_handler,win(), delea -0x50(%rbp)diebufadresseert, en de eerste kaleret. Uit de verplaatsing berekent hetrip_off = 80 + 8 = 88. Niets is hardcoded; het overleeft een herbouw. - Zelf-introspectie — leest zijn eigen
/proc/self/mapsendlsym()tsystem/readom de libc-offsets te leren. De libc-base van het doelwit isleaked_read − off_read, daarnasystem = base + off_system, enz. Die delta-rekenkunde is de reden dat exploits libc-versies overleven. - Verbind — leest het banner/leaks (
ids=,stack=,libc=,BUF=). - Bouw de payload — voor
shellcode: 23 bytes machinecode, padding totrip_off, daarna de opgeslagen RIP =buf(zodat deretop de code springt). Voorret2win/ret2libc: adressen berekend uit de analyse — geen stackuitvoering nodig. - De uitlijnfix — een gekaapte kale
retgeeft de calleersp ≡ 8 (mod 16), en glibc's SSE2-code faalt metmovapsop een niet-uitgelijnde stack (de crash-reporter logtsi_addr=(nil)— de hint). foowosc plaatst één extraret-gadget vóór het echte doelwit en herstelt de invariant. Een zijdenhandschoentechniek in een shellcode-lab, maar het is het verschil tussen een payload die "soms werkt" en een die altijd werkt. - Stuur, daarna schakel door — het doelproces is de shell; dit proces
splist alleen bytes. Geen lokale shell, geen andere lezer — de
single-read-cursor-bug (één opgegeten byte per chunk) is gedocumenteerd in
become_shell().
De bewuste bugs van de daemon (allemaal in foowosd.c, allemaal echte CWE-klassen)
| # | bug | CWE | notitie |
|---|---|---|---|
| 1 | read(fd, buf, 512) in een 64-byte stack-buffer |
CWE-120 | de overloop: 448 bytes voorbij buf, opgeslagen RIP op +88 |
| 2 | alleen snprintf(line, …, "%.*s", …); maar aanvaller-% in het echo-pad |
CWE-134 | het lek is hier de echte payload; een %n in een root-proces zou write-what-where als root zijn |
| 3 | kinderen behouden root terwijl ze onbetrouwbare input afhandelen | CWE-271 | de correcte drop_privs() (setgroups→setgid→setuid, in die volgorde, met verificatie) staat in het bestand, gecommentarieerd, bewust nooit aangeroepen |
| 4 | ids=, stack=, libc=, BUF= onthuld aan elke client |
CWE-200 | zonder deze leaks konden de shellcode- en ret2libc-technieken geen adressen berekenen (ASLR zou ze verslaan) |
De handler heeft precies dezelfde buf[64]/read(512)-vorm als de twee andere
labs, dus de gedeelde objdump-gebaseerde herkenningspipeline werkt ongewijzigd.
Zo inspecteer je de daemon (leerpad)
make status # draait hij? als welke uid? bestandstoestand wordt getoond
make run-root # of run / run-root-ns
./foowosc -t leak # zie het banner en leaks, stuur niets
./foowosc -t demo # rommel-overloop -> SIGSEGV, gelogd met RIP/rsp
./foowosc -t shellcode # de interactieve root-shell
make test-root # volledige matrix, alle technieken, --must-root
Crash-reporter: Bij SIGSEGV logt de daemon het foutadres, RIP en RSP. Een ret
in een niet-canonieke 0x4141… faalt bij de ret zelf (RIP als 0x4028xx,
si_addr=(nil)) — goed om te weten vóór je een logregel als NULL-dereferentie
verkeerd leest.
Waarom de shellcode 23 bytes is, niet 32
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
Het SUID-lab heeft setreuid(0,0) vóór dit nodig. Dit lab niet, om de reden
die overal herhaald wordt: ruid is al 0, omdat root het proces startte.
make verify bewijst dat de bytes in foowosc.c byte-voor-byte zijn wat
shellcode.S assembleren tot.
Tegenmaatregelen — wat make hardened verandert
Geharde build (-fstack-protector-strong -fPIE -pie -z noexecstack):
| techniek | kwetsbare foowosd |
geharde foowosd_hardened |
|---|---|---|
shellcode |
root-shell (uitvoerbare stack) | SIGSEGV bij de canary-controle / NX |
ret2win / ret2libc |
root-shell | canary aborted de ret — maar merk: een PIE-build maakt deze adressen ook willekeurig |
demo |
SIGSEGV, gelogd | SIGSEGV, gelogd |
make test-hardened demonstreert het live. De belangrijke observatie is niet
alleen dat de tegenmaatregelen de technieken doodden — het is dat ze de daemon
niet "niet-root" maakten. Een geharde build die nog steeds als root
gestart wordt, is nog steeds een root-daemon; de tegenmaatregel verhoogt
alleen de lat voor de aanvaller. Minste privilege (drop_privs()) en
geheugenveiligheid zijn twee verschillende bugs, en een daemon die geen root
nodig heeft, zou het niet moeten hebben.
Veiligheidsleuningen (zelfde beleid als het SUID-lab)
- Alleen loopback. foowosd weigert iets anders dan
127.0.0.1/localhost/::1te binden, tenzij je-Lgeeft. Een root-daemon op een echte interface is een externe root-dienst.-Lbestaat alleen om de wacht te tonen; gebruik het niet op iets dat ertoe doet. - De toestand wordt luid gelogd. Bij de start print hij
ruid/euiden of dit een root-proces is, zodat je altijd weet welk exploit-resultaat je moet verwachten. - Oordelen komen uit exit-status in
make test*(de retourcode van de pty-harness), nooit uit het greppen van zijn stdout — grepbare output liegt. - De pty moet cooked + ECHO off draaien, anders echoët de harness zijn eigen commandoregel en vervalst de markering. De harness zet ECHO uit en houdt ECHONL aan.
- Opruimritueel:
make stopna elke sessie. Als de daemon root-bezeten is, zegtstopjesudo pkill -x foowosdte draaien. - Draai dit nooit op een host waar je om geeft. Het bestaat om
uid=0-shells over de loopback-interface uit te delen.
Oefeningen
- Draai
make run(user-daemon), daarna./foowosc -t shellcode. Waarom is de shell niet root? (Checkids=in het banner — foowosc zegt het je vóór je überhaupt verbindt.) make stop && sudo make run-root && make test-root. Verklaar aan de hand van de bannerregel waarom alle vier de technieken nuuid=0(root)geven.- Vind in
foowosd.cdrop_privs()en lees waarom de volgorde vansetgroups → setgid → setuidertoe doet. Bepaal waar inmain()hij thuis zou horen, en wat het aanvalsoppervlak van het lab wordt wanneer hij daadwerkelijk wordt aangeroepen. - Bereken
rip_offmet de hand uitobjdump -d foowosd: vind delea -0xNN(%rbp)vanbufbinneninvulnerable_handler, daarnaNN + 8. foowosc doet precies dat; controleer zijn rekensom tegen de jouwe. make hardened && make test-hardened. Welke techniek valt voor de canary, en welke voor NX? Waarom verandert harden niet watmake statusover het proces rapporteert?- Vergelijk de shellcodes van de twee labs: 23 bytes hier, 32 voor foosd. Wat doen de extra 9 bytes, en waarom zijn ze alleen in het SUID-geval nodig?
- Lees de commentaar van
become_shell()over de enkele lees-cursor. Maak de faaltoestand mentaal na: twee lezers op één socket betekent dat de login-shell één byte per chunk eet — "uid=1000…" arriveert als "id=1000…". Waarom kan een relay-proces deze bug nooit hebben?
Bestanden
foowosd.c de kwetsbare root-daemon (elke regel gecommentarieerd)
foowosc.c het exploit (elke regel gecommentarieerd)
shellcode.S referentie-assembly voor de 23-byte-payload
tests/pty_wosuid_test.c de pty-harness (markering + strikte id-vorm-controle)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
Zuster-labs: ../food.c/../fooc.c (user-level-baseline, poort 2342) en
../suid/ (SUID-root-daemon foosd/foosc, poort 2343). De poorten zijn
bewust verschillend — je kunt alle drie tegelijk draaien en hun ids=-regels
in het banner kruislings controleren.