Spring til indhold
DAsikkerhedagent-securityai-agenter

Jeg skrev advarslen, mens shellen kørte

I februar advarede jeg andre om at auditere deres vibecodede projekter. Shellen havde da kørt på min egen boks i 56 dage. Den levede 8,5 måneder — og i oktober satte nogen en model til at lede efter huller i selve PHP.

Scoret for

Den 7. februar skrev jeg et opslag om, at alle burde gå tilbage og se på de projekter, de havde vibecodet i 2025.

"Hvis du vibecoded projekter i 2025 med React2Shell, er du næsten sikkert eksponeret. Bots have nok allerede grabbed dine environment variabler. Du skal nok roll nogle keys."

Det var ikke et dårligt råd. Det var bare ikke rettet det rigtige sted.

Da jeg skrev det, havde der kørt en webshell på min egen server i 56 dage. Den gik ind den 13. december 2025. Tre uger før jeg skrev advarslen, havde operatøren tjekket ind på den igen. Jeg fandt den først den 26. august 2026, fordi Hetzner sendte mig en abuse-mail.

Jeg skriver det her, fordi den uge, vi lige har været igennem, gør det relevant igen — og fordi min egen sag siger noget præcist om, hvad der er ved at ændre sig.

Ugen hvor nogen satte en model til at lede

Pressen kalder den Black October.

Den 21. oktober kom to huller i PHP: CVE-2026-109618 og CVE-2026-109619. Begge med CVSS 9.2. Uautentificeret fjernkørsel af kode i selve request-håndteringen, før app-koden kommer til: den ene gennem hukommelseskorruption, når processen løber tør for hukommelse, den anden gennem tilstand fra én request, der bliver genbrugt af den næste på samme worker. Alle 8.2- til 8.5-udgivelser før 8.2.35, 8.3.36, 8.4.27 og 8.5.12 er sårbare. Hollandske NCSC og CERT-EU gik begge ud med advarsler.

Jeg patchede samme dag: begge mine Coolify-bokse fra 4.4.6 til 4.4.7, som bragte PHP 8.4.27 med sig. En WordPress-testinstans rullet ud igen. En gammel URL-forkorter stoppet, fordi dens image ikke var patchet.

Det interessante er, hvordan fejlene blev fundet.

Det var Calif, et red-team-firma, der fandt dem — ved at køre OpenAIs GPT-5.6 Sol mod PHP under et initiativ, OpenAI kalder Patch the Planet. Wiz fandt uafhængigt begge fejl. Derefter blev der koordineret i omkring to måneder, før noget blev offentliggjort.

Den detalje er hele pointen. Modellen sad ikke i et laboratorium. Den sad hos et sikkerhedsfirma, der gjorde sit arbejde.

PHP kører på omkring 70 % af alle websites. Det tal er eksponering, ikke skade — Forrester kalder det en koncentrationsrisiko. Men det er omfanget af den flade, der skulle patches, fordi nogen satte en model til at lede i selve sproget.

Det nye er ikke, at angribere er blevet dygtigere. Det nye er, at et helt almindeligt firma nu kan sætte en maskine til at lede efter huller — og at det bliver billigere for hver måned, der går.

26 sekunder

Min egen sag er den anden ende af den historie, og den er ældre.

React2Shell blev offentliggjort den 3. december 2025 — fjernkørsel af kode gennem Next.js' server-komponenter. Ti dage senere, den 13. december, var tre apps på min boks kompromitteret.

Den tredje er den, der er værd at se på. Fra logfilen:

  • 21:51:09 — en POST til roden. Den kører i 20.009 millisekunder og afbrydes.
  • 21:51:35 — GET /_node/v6/patch?0=id svarer 200 med 70 bytes.

26 sekunder fra exploit til fungerende kommandolinje. Så et kald til /ip-93a1…, der svarer 200 med 14 bytes: en beacon, der returnerer boksens IP, så operatøren ved, hvad de har fået.

Og så blev der stille. Men ikke tomt.

Den 24. december kørte de id igen. Den 16. januar igen. Den 8. marts. Den 6. juli. Fire besøg over et halvt år, alle fra Tor-exits, alle 200. Det er ikke efterladenskaber fra en scanner, men en adgang, nogen vedligeholdt.

Min advarsel på LinkedIn ligger mellem besøg nummer to og tre.

Den app, ingen rører ved, bliver ved med at være hacket

Shellen blev smidt ud af de to andre apps. Ikke fordi jeg gjorde noget.

De blev bygget om på patchet Next.js senere — i februar og i maj — som en sidegevinst ved almindelige udrulninger. Shellen lå inde i containeren, og en ny container er en ny container. Ingen af dem var en reaktion på noget — jeg vidste ikke, der var noget at reagere på. Og en genbygning fjerner shellen, ikke eksponeringen: deres hemmeligheder havde ligget åbne i henholdsvis to og fem måneder.

Den tredje app blev aldrig rullet ud igen. Den var færdig. Den virkede. Der var ingen grund til at røre den.

Det er derfor, den levede 8,5 måneder.

Det er den mest ubehagelige lektie i hele sagen, fordi den er omvendt af, hvordan man normalt tænker på risiko. Den app, der er under aktiv udvikling, bliver patchet gratis, hver gang nogen retter en knap. Den app, der er færdig, står stille — og "færdig" betyder i praksis "fastholdt på den version, der var aktuel, da jeg mistede interessen".

Jeg har en håndfuld af dem. Det har du sandsynligvis også.

Socket'en var med vilje

Grunden til at netop den container var alvorlig og ikke bare en hacket landingsside:

id
uid=1001(nextjs) gid=65533(nogroup) groups=988(docker),65533(nogroup)

App-brugeren var i docker-gruppen, og /var/run/docker.sock var bind-mountet med skriveadgang. Der skulle ikke eskaleres noget. docker run -v /:/host var direkte tilgængelig i hele vinduet.

Og socket'en var ikke en fejlkonfiguration. Appen er et kontrolpanel til Docker — den taler med socket'en, fordi det er hele dens formål. Man kan ikke fjerne den uden at fjerne appen.

Så den rigtige formulering af lektien er skarpere end "mount ikke docker.sock": et Docker-kontrolplan på et offentligt værtsnavn er host-root med ekstra trin. Enhver fjernkørsel af kode dér er ikke begrænset til containeren. Det, der hjælper, er en auth-gate foran eller en filtrerende socket-proxy — ikke at lade som om socket'en kan undværes.

Runde to: patchet på en dag, blind på spørgsmålet

Så kom oktober, og jeg havde lært noget. Jeg havde en fortegnelse. Jeg vidste præcis, hvilke flader der kørte PHP: Coolify selv på begge bokse, en WordPress-testinstans, en URL-forkorter. Ingen PHP på værterne.

Og jeg kunne alligevel ikke svare på det eneste spørgsmål, der betød noget.

Blev vi ramt?

Traefiks access-log på den ene boks holdt op med at skrive den 18. september. Prod-Traefik har ingen access-log. Coolifys egen nginx logger ikke requests. Der var ingen data at søge i.

Første gang blev jeg kompromitteret uden at vide det. Anden gang patchede jeg hurtigt og var stadig blind. Det er den samme mangel set fra to sider: jeg havde bygget systemer, jeg ikke kunne stille spørgsmål til bagefter.

Hvad der ikke skete

Her er den del, jeg synes de fleste af disse historier springer over.

Jeg fandt ingenting på hosten. Ingen fremmede kommandoer i .bash_history — kun mit eget arbejde. Uændrede systemd-units. Tre forventede nøgler i authorized_keys. Ingen spor af en angriber på disken i december-vinduet. Det eneste, der beviseligt gik tilbage over HTTP, var svar fra id og en 14 bytes beacon med boksens IP.

Men socket'en betyder, at fravær af spor ikke er bevis på fravær. Proxy-loggen kan ikke udelukke exfiltrering, og en shell med host-root i 8,5 måneder er en shell med host-root i 8,5 måneder. Jeg behandlede alle hemmeligheder på den boks som lækkede, fordi det er den eneste forsvarlige antagelse.

Og den vigtigste afgrænsning: intet i loggen tyder på, at angrebet på mig var AI-drevet. Selve indbruddet var en helt almindelig scanner mod en kendt, upatchet sårbarhed — 26 sekunder fra exploit-requesten til en fungerende shell. De fire besøg bagefter var derimod nogen, der vedligeholdt adgangen.

AI-delen i Black October ligger på opdagelsessiden, ikke på udnyttelsessiden. Forsvaret kom først. Angriberne, der ramte mig, behøvede ikke en model for at udnytte et hul, der allerede var fundet.

Det er derfor, opdagelsessiden er den, der er værd at holde øje med.

Ormene er stadig i laboratoriet

Der bliver talt meget om AI-orme lige nu, og det er værd at være præcis om, hvor vi er.

Anthropic udgav den 12. oktober et studie af, om modeller kan replikere sig selv uden et datacenter — orme, der kører modellen direkte på den hardware, de kompromitterer, så vægtene bliver en del af payloaden. De testede elleve open-weights-modeller. Én, GLM-5.2, klarede det hele vejen og spredte sig til 8 af 8 noder i 4 af 5 forsøg. Forfatterne kalder selv opsætningen forenklet, med trivielt adgangsniveau — altså en øvre grænse, ikke en forventning. Deres egen opsummering er den sætning, der skal citeres:

"We don't yet have a full rogue replicator, but agents are becoming increasingly capable."

OpenAI offentliggjorde den 25. september, at de havde bygget selvreplikerende prompt-injektioner i simulering: en injektion, der både når sit mål og bliver kopieret videre til næste agent. Fundet i juni, i en opstilling hvor en angribende agent stod over for en forsvarende. Kun i simulering.

Der er ikke offentliggjort nogen udbrud. Det er forskning, og det er den rigtige retning at kigge — men det er ikke det, der ramte mig, og det er ikke det, der skete i oktober.

Det tredje spring

I juli skrev jeg, at vi var midt i et spring, hvor modellerne blev udskiftelige. I går gennemgik jeg Anthropics S-1 og fandt tallene bag samme påstand: en bruttomargin på 85 %, og en pris per opgave, der falder hurtigere end prisen per token. En margin af den størrelse på noget, der er ved at blive en råvare, er netop grunden til at tro, at prisen falder videre — også hos dem, der ikke har en dyr forskningsafdeling at betale for.

Kæden ser sådan ud:

  1. Agenterne begyndte at virke. I min egen oplevelse, et sted omkring slutningen af 2025, holdt de op med at foreslå kode og begyndte at færdiggøre opgaver.
  2. Intelligensen blev en råvare. Flere modeller blev gode nok til samme opgave, open weights tog volumen, og prisen begyndte at falde.
  3. Og nu bliver den billige intelligens rettet mod al kode, der findes.

Det tredje er det, der skete i oktober. Ikke at angreb blev automatiserede — det har de været i tyve år. Men at det at finde et nyt hul faldt i pris, på samme kurve som alt andet.

Man kan læse det som en god nyhed, og den læsning har noget for sig. Fejlene i PHP blev fundet af et sikkerhedsfirma, ikke af en angriber. Koalitionen fik to måneder. Patchen var klar, da verden fik at vide, hvad der var galt. Forsvaret løb først.

Men asymmetrien er ikke til vores fordel i det lange løb, og grunden er banal: forsvaret skal patche alt. Angriberen skal finde én ting. Når prisen på "at finde én ting" falder, falder den for begge sider — og den ene side skal holde styr på alt, hvad den har, mens den anden kun skal bruge en liste over mål.

Min egen sag er beviset i lille format. Jeg vidste, hvad React2Shell var. Jeg skrev endda advarslen. Men 18 containere på min boks kørte stadig sårbar Next.js den dag, jeg opdagede det — næsten ni måneder efter, at patchen fandtes. Problemet var aldrig viden. Det var at holde styr på, hvad jeg havde.

Tre ting, jeg tager med

Før en liste, ikke en god hensigt. Forskellen mellem februar og oktober var ikke, at jeg blev dygtigere til sikkerhed. Det var, at jeg i oktober kunne svare på "hvor kører PHP hos mig" på ti minutter. Advarsler virker ikke på egne systemer. Lister gør.

Log, så du kan svare bagefter. Patchning er den nemme del — den kan gøres på en dag. Spørgsmålet "blev vi ramt" kan ikke besvares bagefter, hvis dataene ikke blev opsamlet dengang. Jeg har slået access-logning til på begge bokse — det ene sted igen, det andet for første gang, og det er den eneste ændring fra oktober, der vil betyde noget næste gang.

Byg dine færdige projekter om — med opdaterede afhængigheder. Det er den præcise formulering, og præcisionen er hele pointen. Shellen forsvandt fra mine to andre apps, fordi genbygningen løftede Next.js med sig — deres versionsinterval tillod patchen at rykke, så en ny build fik en version, hvor fejlen var rettet. Havde versionen været pinnet eksakt, havde den samme genbygning produceret nøjagtig den samme sårbare container. En ny udrulning alene er ikke en sikkerhedshandling. En udrulning, der må opdatere afhængighederne, er — og det samler også rettelser op for huller, du ikke har hørt om endnu.

Og en fjerde, teknisk, som jeg betalte dyrt for: grep skjuler fund i logfiler, der indeholder NUL-bytes. Access-loggen var 5,98 GB og indeholdt netop dem, så GNU grep kaldte den binær, skrev "binary file matches" til stderr og ingenting til stdout. Min første søgning gav nul resultater, og jeg troede et øjeblik, at angriberen aldrig havde fået kontakt. Det var forkert. Brug grep -a på access-logs.

Advarslen var rigtig nok i februar. Jeg læste den bare ikke som noget, der handlede om mig.