Recursive AI
ResearchCase study

PHP2Shell: switching off a client’s Drupal in two hours

Abstract

On 21 October 2026 Wiz disclosed PHP2Shell, two flaws in core PHP (CVE-2026-109618 and CVE-2026-109619, CVSS up to 9.2). Wiz demonstrated pre-authentication remote code execution on Drupal and says every PHP version back to 5.4 is affected. PHP shipped the patch the same day.

One of our clients, a B2B credit-reporting business built around industry credit associations, ran its member site on Drupal 7 and its credit-application forms on Drupal 8. Patching both safely in time wasn’t realistic, so Kurtis decided to turn Drupal off. The site had already been moving page by page onto Alera behind a proxy. What still ran on Drupal that day was the most-used page, the account report, plus dozens of smaller features and the separate credit-application site.

Agents on Alera did the work, and they replaced nearly all of the client’s Drupal. The account report went first, because the shutdown waited on it. It was rebuilt from scratch that afternoon, and PHP stopped in production 2 hours 4 minutes after Kurtis’s first message, about eight hours after Wiz published. The rest followed overnight: the association pages and the Meeting Book, payment-experience entry, flash alerts, third-party report ordering, and the credit-application site. All 82 of its forms passed their checks by 13:24 UTC on 22 October and are live.

Drupal is off, entirely. Two things were not migrated, by decision: features the client is deliberately deprecating, and Meetings (meeting signup and its editor), which isn’t needed for two months and is due for a full overhaul.

The work took 767 agent runs on Claude Opus 5.5 and Claude Sonnet 5.5. At list prices the tokens would have cost about $1,000. The main findings:

  1. Ship the smallest thing that lets you switch off. Kurtis cut the account report to its core, and the Drupal shutdown waited only for that one page. Everything else could wait behind a 503.
  2. Lock first, then cut over. Within 26 minutes of the go-ahead, the Drupal servers answered only the proxy. That made the next hour of building safe.
  3. A smoke test that loads the page but checks no data is not a smoke test. A change to shared routines took login and search down for 12.5 minutes, and the check after it saw the search page load without checking for results.

1. Timeline

All times are UTC; the team worked in US Central time (UTC−5).

Time (UTC)Event
21 Oct 13:59Wiz publishes PHP2Shell (09:59 EDT). PHP releases patched versions the same day.
19:55A planner for the account-report rebuild starts. The rebuild was already planned as part of the migration and had no code yet.
20:05Kurtis’s call: Drupal goes, as soon as the account report can replace it.
20:05Twelve seconds later, a database agent starts measuring which Drupal features members actually use. Its report is written at 20:11.
20:49First commit of the new account report.
21:04Kurtis approves the cutover plan: “lets get after it”.
21:22The Drupal 8 credit-application service is scaled to zero. Its hostname serves a “back soon” page.
21:23–21:30Every Drupal origin (production, dev, QA, staging) is limited to the four proxy addresses at the load balancer and firewall, and the agents check it from outside.
21:27Account report, first ship, committed, then deployed without a route yet. The cutover kit follows: login, logout and impersonation without Drupal, the back-soon page, and proxy changes.
22:03:56Cutover deploy. The proxy no longer has a Drupal default. Anything not yet rebuilt goes to back-soon.
22:05–22:09Production smoke test as a test user: login, forced password change, dashboard, search, account report, logout, impersonation, sign-in to two connected apps, and zero requests forwarded to Drupal.
22:09:48Drupal 7 PHP scaled to zero in production and dev. 2 h 4 min after Kurtis’s call.
22:19–22:22QA and the last dev PHP services stopped. A guard job starts re-checking every 30 minutes that no pipeline redeploy brings PHP back.
22:24:17–22:36:50Login and search are down after a change to shared routines (§6).
23:59Flash entry and per-line notes live on the account report.
22 Oct 01:33Third-party credit-report ordering live on the account report.
02:17Association pages live.
02:30The deploy now builds before it stops services. Agents measured the production 502 window falling from about 66 s to 3.8 s.
02:40–03:38Meeting Book, payment-experience entry, account-name requests and the permanent list, then the flash-alert pages, all live.
13:24All 82 credit-application forms pass their checks, confirmed by the project manager. All 82 have since gone live.

2. The problem

PHP2Shell is a flaw in the PHP engine, not in Drupal, so no Drupal update helps. An attacker needs no account. Wiz’s write-up says exploitation leaves little in application logs and that the PHP update is the only complete fix. Kurtis’s first message to the planner, at 20:05 UTC, set the terms:

“We need to ditch drupal - it’s an emergency because of a major php CVE that we’re not going to be able to patch.”

Fourteen minutes later, answering the planner’s questions:

“Yes, the CVE can be exploited by someone who isn’t logged in. Deadline is hours. We need to turn off drupal as soon as the [account report] is done. It will give us reduced functionality for a bit, but we can add back in all the lower-importance stuff over the coming days.”

Three things made a same-day shutdown possible:

  • The migration was already under way. Production traffic already ran through an Alera proxy that sent some paths to Alera and the rest to Drupal. The dashboard, search and login were already on Alera. Switching off Drupal meant changing the proxy’s default, not moving DNS.
  • The data didn’t live in Drupal. The databases stayed where they were. Only the PHP application servers had to stop.
  • Usage data existed. Drupal’s own access log was off, but an older per-feature hit log in the business database covered years. Over 90 days, the account report had 42,014 hits from 735 users, the dashboard and search were already rebuilt, and 96 of the 157 custom Drupal routes had no recorded use at all. That turned “replace Drupal” into “replace the one page the shutdown waits on, then the rest in order of use”.

3. How the work was organized

The work ran as four projects in the client’s Alera swarm. Each had a short decisions document. Each was run by an orchestrator agent that delegated to developer agents, and one project manager agent sat above them.

ProjectGoalState at the end of this report
Account reportRebuild the member account report at its old URL, with confidentiality rules intactLive from 22:03 UTC on 21 Oct. Notes, flash entry and third-party report ordering were added overnight.
Drupal shutdownLock down every PHP origin, cut Drupal out of the proxy, stop PHP, then restore features by trafficDrupal off everywhere; nearly every feature replaced. Guard job running; proxy retirement planned for the next week.
Association pagesAssociation home pages, document archive, roster, uploads, Meeting Book, experience entry, requests, permanent listLive by 03:02 UTC on 22 Oct
Credit applicationReplace the Drupal 8 credit-application site and its 82 supplier formsBuilt, checked and live: 82 of 82 forms

The account-report rebuild had started ten minutes before the emergency, as a normal migration project with a planner. The CVE turned its go-live into the trigger for the shutdown. A second planner took Kurtis’s CVE call and wrote the shutdown plan. That plan had five phases: lock the origins now, build a cutover kit while the account report was built, cut over, restore features over the following days, and retire the proxy the next week. The project manager ran account report and shutdown in parallel and kept them from racing each other on the one route they shared.

The cutover kit was most of the hidden work. Login, logout, “who is logged in” and staff impersonation all quietly depended on Drupal sessions. The kit moved them onto Alera’s own tokens. It also added the back-soon page (an HTTP 503, so crawlers don’t index it) and redirects for old public URLs. Dev couldn’t build the current code for an unrelated reason, so the kit was proven on staging, which shares production’s databases and proxy path.

The credit-application project followed Kurtis’s design, settled that evening and the next morning. Each supplier’s form became a free-form markdown spec in versioned storage. A builder agent turns a spec and its diff into a web component made from shared sections: resale certificate, owners, references, address, signature and uploads. Staff preview, then publish. Agents transformed the 82 legacy form definitions into specs. They compared every block of agreement terms against the old forms (707 of 707 equal), built every form, and ran dry-run submissions through the real back end.

4. What the human did

Kurtis typed about 34 messages into the five top-level agent conversations, between 20:05 and 03:16 UTC (method in the verification note). None of them were code. They were decisions, and each one moved the work.

  • Set the emergency and the scope. “it’s only the account view itself. but that’s ok - we just need to get it out asap.” The page’s PDF, notes page and paid-report ordering were cut from the first ship.
  • Approved the cutover plan in one message. He answered seven numbered items, including that header links to unbuilt pages should just land on back-soon.
  • Reviewed the live page and called the regressions. About ten minutes after cutover he sent six defects, among them: “‘All Members’ vs ‘My Experience’ widgets are a CLEAR downgrade.” They were fixed and on production within the hour. They also became a standing rule: the old page is the visual reference.
  • Chose what came back next. He skipped the next feature by traffic and took the association pages instead: “The site feels broken without those.”
  • Designed the credit-application replacement. He rejected a short generic form and a git-based spec store. On the spec format: “the spec is markdown. If you need something exact, put it in a code fence or `backticks`. This isn’t 2022.” On a “several days” estimate: “That’s an extra couple of hours of work, not ‘several days’.”

He was also the one who noticed a production-wide 502 at about 01:44 UTC, while looking at a page that should have shown back-soon. The project manager traced it to a short window when every path failed, probably a server restart during a deploy. Later that night the deploy was changed to build before stopping services, which shortened that window (§1).

5. Verification

  • Lockdown was checked from outside. Direct requests to every Drupal origin returned 403, and the old pages still worked through the proxy until cutover. After cutover the proxy log showed zero requests forwarded to Drupal.
  • Production smoke tests used a real login. A dedicated test account had its password temporarily swapped, then restored and checked byte for byte. With it the agents tested login, the forced password change, dashboard and export, search, the account report, logout, impersonation by form, and sign-in to two connected apps. They also checked that old paths landed on back-soon. After the outage in §6 the smoke test was required to check search results, not just page loads.
  • The account report matched the old numbers. A developer agent compared the trend tiles with the live legacy API: 191 comparisons across 35 accounts and 5 test viewers with different sharing rules, 0 mismatches (agents’ own account). The confidentiality rules for European accounts and per-member sharing were kept from the first ship.
  • The credit forms were dry-run against the real back end. The final sweep made 161 submissions across all 82 forms through the real submit path in dry-run mode, and none was rejected (agents’ own account). It found two bugs that would have blocked real applicants; both were fixed before the forms were reported ready.
  • Test data was cleaned and read back. Rows written by test users, files and mail went to a designated test customer and test mailbox, and were deleted and checked afterwards.

Not verified: the forgot-password email (no real inbox was used).

6. What went wrong

  • A 12.5-minute outage of login and search. A change deployed at 22:24 UTC marked some shared routines internal-only. About 40 callers used them over plain HTTP, and they broke. The restriction was moved to the proxy instead, live at 22:36:50 UTC. The smoke test after the deploy saw the search page load and never checked for results. Rule: a smoke test after a shared change must exercise the data path: a real login, real search results, a real sign-in to connected apps.
  • Three forms sent as ready too early. A stricter check added at 11:31 UTC failed three of the 35 forms sent to Kurtis as ready at 10:20. They were withdrawn at 12:20, before any approval, and fixed by 13:24. Lesson: re-check “ready” after the checker changes.

7. Results

MeasureValue
Kurtis’s call to PHP stopped in production2 h 4 min (20:05 → 22:09:48 UTC)
Wiz disclosure to PHP stoppedabout 8 h 10 min
Go-ahead to every Drupal origin locked to the proxyabout 26 min (21:04 → 21:30 UTC)
PHP reachable from the internet after 22:09 UTCnone found (checked across production, dev, QA and staging; DNS walk found no other PHP host)
Drupal features replaced on Aleranearly all. By 03:38 UTC on 22 Oct: the account report with notes, flash entry and third-party report ordering; association home, archive, roster, uploads, Meeting Book, experience entry and edit, account-name requests, permanent list, flash-alert pages
Credit-application forms built, checked and live82 of 82
Not migrated, by decisionfeatures being deprecated; Meetings (signup and editor), not needed for two months and due for a full overhaul
Deploy 502 window in productionabout 66 s → 3.8 s (agents’ measurement)
Agent runs767: 257 on Claude Opus 5.5 (planning, management, orchestration, reviews, documentation, feedback), 510 on Claude Sonnet 5.5 (development and form building)
Model calls14,726
Tokens79.6M input, 15.2M output, 1.73B cache reads, 105.6M cache writes
Cost at list pricesabout $1,000 metered-equivalent: about $590 for the project agents (planning, orchestration, development and reviews), $186 for the credit-form builder runs, $178 for the documentation agents and $46 for the feedback agents. Priced per model call at Anthropic’s list prices with 5-minute cache-write rates; at 1-hour rates it would be about $1,220. Recorded spend was $0 (subscription).
Deploy-tagged commits to the production branch53 (21 Oct 20:00 UTC to 22 Oct 14:00 UTC)

8. Recommendations

  1. Keep a strangler proxy in front of anything you can’t patch quickly. Because the proxy already existed, the shutdown was a change to its default, made in minutes, with rollback one command away.
  2. Measure usage before you decide what to rebuild. A per-feature hit log turned an open-ended rewrite into one gating page plus an ordered plan for the rest.
  3. Lock the origins before you build. Limiting the Drupal servers to the proxy took minutes and removed the deadline pressure from the build.
  4. Re-verify “ready” after the checker changes. The forms that were pulled back were ready under the old check. Only a re-check before reporting “ready” catches that.

Links