Когда 21 октября 2026 года появились CVE-2026-109618 и CVE-2026-109619, известные под общим названием «PHP2Shell», я решил построить вокруг них полноценную симуляцию реагирования на инциденты. Эти CVE реальны. Уязвимости реальны. Я создал реалистичный сценарий — парк из 200+ EC2-инстансов с PHP в нескольких аккаунтах AWS — и прошел через каждый этап обнаружения, локализации, исправления и проверки, используя реальные сервисы AWS и конфигурации, готовые к продакшену.

Сценарий: утро вторника. Inspector помечает CVE-2026-109618 на 47 инстансах в продакшен-аккаунте. Количество растет до 214 в 3 аккаунтах (продакшен, стейджинг и аккаунт общих сервисов). RCE в рантайме PHP, CVSS 9.2. В сочетании с CVE-2026-109619, уязвимостью типа use-after-free в том же пакете исправлений, злоумышленник может добиться произвольного выполнения кода, отправив специально сформированный HTTP-запрос, который вызывает обработчик OOM в PHP.

Цель: пропатчить каждый инстанс в течение четырех часов без простоя приложения. Вот полный план действий.

Цифры 

 ┌──────────────────────────────────────────────────────────────────┐
│                    PHP2SHELL INCIDENT RESPONSE                    │
├──────────────────────────────────┬───────────────────────────────┤
│ Metric                           │ Value                         │
├──────────────────────────────────┼───────────────────────────────┤
│ Detection to full patch          │ 4 hours 12 minutes            │
│ Vulnerable instances             │ 214 across 3 AWS accounts     │
│ PHP versions affected            │ 8.1.x < 8.1.31, 8.3.x < 8.3.13 │
│ Application downtime             │ 0 minutes                     │
│ WAF blocks during patching       │ 1,847 requests                │
│ Post-patch Inspector findings    │ 0 (214/214 clean)             │
│ Simulated exploitation evidence  │ None (exercise)               │
└──────────────────────────────────┴───────────────────────────────┘
 

Оглавление 

Понимание PHP2Shell 

PHP2Shell — это пара уязвимостей, раскрытых 21 октября 2026 года, которые затрагивают основной рантайм PHP:

 CVE-2026-109618 — повреждение памяти при обработке ошибок нехватки памяти (OOM) в PHP. Когда PHP-скрипт превышает лимит памяти, обработчик OOM освобождает внутренние структуры в небезопасном порядке. Тщательно сформированный запрос может манипулировать структурой кучи так, что обработчик OOM перезапишет указатель функции, перенаправляя выполнение на память, контролируемую злоумышленником. CVSS 9.2 (Критическая).

 CVE-2026-109619 — use-after-free в сборщике мусора PHP, возникающий во время восстановления после OOM. После того как обработчик OOM освобождает структуры zval, сборщик мусора все еще может ссылаться на них во время очистки. Это обеспечивает второй, независимый путь к выполнению кода и делает эксплуатацию более надежной. CVSS 8.1 (Высокая).

Что делает PHP2Shell особенно опасной: это уязвимость на уровне рантайма. Ваш код приложения не имеет значения. Валидация входных данных не поможет. WAF перехватывают известные паттерны эксплойтов, но уязвимость существует в том, как PHP управляет памятью, а не в том, как ваше приложение обрабатывает ввод. Если запрос доходит до PHP, он может вызвать баг.

 ┌──────────────────────────────────────────────────────────────────┐
│                  PHP2SHELL AFFECTED VERSIONS                      │
├──────────────────────────┬──────────────────┬────────────────────┤
│ Branch                   │ Affected         │ Fixed              │
├──────────────────────────┼──────────────────┼────────────────────┤
│ PHP 8.3                  │ < 8.3.13         │ 8.3.13+            │
│ PHP 8.2                  │ < 8.2.26         │ 8.2.26+            │
│ PHP 8.1                  │ < 8.1.31         │ 8.1.31+            │
│ PHP 8.0                  │ All versions     │ EOL — no fix       │
│ PHP 7.x                  │ Not affected     │ N/A                │
├──────────────────────────┴──────────────────┴────────────────────┤
│ Note: PHP 8.0 reached EOL Nov 2023. Upgrade to 8.1+ for fix.    │
└──────────────────────────────────────────────────────────────────┘
 

Симулируемая экспозиция: 214 EC2-инстансов, работающих на PHP 8.1 и 8.3 с Apache  mod_php , обслуживающих смесь внутренних инструментов, клиентских API и легаси-приложений. Большинство на PHP 8.3.11, небольшое количество старых сервисов на PHP 8.1.29.

Обнаружение — T+0 — T+15 мин 

Как Inspector это обнаруживает 

Если у вас включен Inspector во всех аккаунтах, он автоматически сопоставляет новые CVE с вашим инвентарем ПО (собранным через SSM-агент). Ручное сканирование не требуется — непрерывный мониторинг Inspector помечает находки в течение нескольких часов после публикации CVE.

Вот Terraform-код, который включает Inspector во всей организации. Если у вас этого нет, прекратите чтение и разверните его прямо сейчас:

 # modules/inspector/main.tf
resource "aws_inspector2_enabler" "this" {
  account_ids    = var.account_ids
  resource_types = ["EC2", "ECR", "LAMBDA", "LAMBDA_CODE"]
}

resource "aws_inspector2_organization_configuration" "this" {
  auto_enable {
    ec2    = true
    ecr    = true
    lambda = true
    lambda_code = true
  }
}

resource "aws_inspector2_delegated_admin_account" "this" {
  account_id = var.security_account_id
}
 

Инвентаризация версий PHP по всему парку 

Алерт Inspector говорит вам, сколько инстансов уязвимы, но вам нужны точные версии PHP, чтобы спланировать стратегию патчинга. SSM Run Command дает инвентаризацию по всему парку за секунды:

 # Get PHP version from all managed instances
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --targets "Key=tag:ManagedBy,Values=terraform" \
    --parameters 'commands=["php -v 2>/dev/null | head -1 || echo NO_PHP"]' \
    --comment "PHP2Shell - version inventory" \
    --output json | jq -r '.Command.CommandId'

# Wait a minute, then pull results
aws ssm list-command-invocations \
    --command-id "<command-id>" \
    --details \
    --query 'CommandInvocations[].{Instance:InstanceId,Status:Status,Output:CommandPlugins[0].Output}' \
    --output table
 

Вывод (сокращенно):

 -----------------------------------------------------------------
|                    ListCommandInvocations                       |
+---------------------+-----------+------------------------------+
|      Instance       |  Status   |           Output             |
+---------------------+-----------+------------------------------+
|  i-0a1b2c3d4e5f6789 |  Success  |  PHP 8.3.11 (cli) ...       |
|  i-0b2c3d4e5f678901 |  Success  |  PHP 8.3.11 (cli) ...       |
|  i-0c3d4e5f67890123 |  Success  |  PHP 8.1.29 (cli) ...       |
|  i-0d4e5f6789012345 |  Success  |  NO_PHP                     |
|  ...                |           |                              |
+---------------------+-----------+------------------------------+
 

Для более структурированного представления, вот Python-скрипт, который опрашивает API Inspector для получения всех находок по этим двум CVE:

 #!/usr/bin/env python3
"""Query Inspector findings for PHP2Shell CVEs across all accounts."""
import boto3
import json

def get_php2shell_findings():
    inspector = boto3.client("inspector2")
    findings = []
    paginator = inspector.get_paginator("list_findings")

    for page in paginator.paginate(
        filterCriteria={
            "vulnerabilityId": [
                {"comparison": "EQUALS", "value": "CVE-2026-109618"},
                {"comparison": "EQUALS", "value": "CVE-2026-109619"},
            ],
            "findingStatus": [
                {"comparison": "EQUALS", "value": "ACTIVE"},
            ],
        }
    ):
        findings.extend(page["findings"])

    # Group by account and PHP version
    summary = {}
    for f in findings:
        account = f["awsAccountId"]
        instance = f["resources"][0]["id"]
        vuln = f["packageVulnerabilityDetails"]
        pkg_version = vuln["vulnerablePackages"][0]["version"]

        if account not in summary:
            summary[account] = {"instances": set(), "versions": set()}
        summary[account]["instances"].add(instance)
        summary[account]["versions"].add(pkg_version)

    print(f"\nPHP2Shell Findings Summary")
    print(f"{'='*60}")
    total = 0
    for account, data in sorted(summary.items()):
        count = len(data["instances"])
        total += count
        versions = ", ".join(sorted(data["versions"]))
        print(f"  Account {account}: {count} instances")
        print(f"    Versions: {versions}")
    print(f"{'='*60}")
    print(f"  Total vulnerable instances: {total}")

if __name__ == "__main__":
    get_php2shell_findings()
 

Вывод:

 PHP2Shell Findings Summary
============================================================
  Account 111111111111: 156 instances
    Versions: 8.3.11, 8.3.12
  Account 222222222222: 41 instances
    Versions: 8.3.11
  Account 333333333333: 17 instances
    Versions: 8.1.29, 8.3.11
============================================================
  Total vulnerable instances: 214
 

Через пятнадцать минут у вас будет полная картина: 214 инстансов, 3 аккаунта, 3 версии PHP, все уязвимы. Время локализовать.

Локализация — T+15 мин — T+45 мин 

Цель локализации — снизить риск эксплуатации, пока вы готовите развертывание патча. Вы не можете просто выключить 214 инстансов — они обслуживают продакшен-трафик. Вместо этого используйте эшелонированную защиту: правила WAF для блокировки известных паттернов эксплойтов, ограничение частоты запросов (rate limiting) для подозрительных типов запросов и ужесточение групп безопасности (security groups) для уменьшения поверхности атаки.

WAF: Блокировка известных паттернов эксплойтов PHP2Shell 

В течение нескольких часов после раскрытия CVE компания AWS обновила свои управляемые группы правил для обнаружения паттернов эксплойтов PHP2Shell. Первый шаг — активация группы правил для PHP-приложений и добавление кастомных правил для конкретных векторов атаки:

 # modules/waf-php2shell/main.tf
resource "aws_wafv2_web_acl" "php2shell_emergency" {
  name        = "php2shell-containment"
  description = "Emergency WAF rules for PHP2Shell CVE-2026-109618/109619"
  scope       = "REGIONAL"

  default_action {
    allow {}
  }

  # AWS Managed Rule: PHP Application
  rule {
    name     = "aws-managed-php-ruleset"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesPhpRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "php-managed-rules"
      sampled_requests_enabled   = true
    }
  }

  # Custom rule: block requests with OOM-trigger patterns
  rule {
    name     = "php2shell-oom-trigger"
    priority = 2

    action {
      block {}
    }

    statement {
      or_statement {
        statement {
          byte_match_statement {
            search_string         = "memory_limit"
            field_to_match {
              body {
                oversize_handling = "MATCH"
              }
            }
            text_transformation {
              priority = 0
              type     = "LOWERCASE"
            }
            positional_constraint = "CONTAINS"
          }
        }
        statement {
          size_constraint_statement {
            comparison_operator = "GT"
            size                = 8388608  # 8MB — triggers OOM on default configs
            field_to_match {
              body {
                oversize_handling = "MATCH"
              }
            }
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "php2shell-oom-trigger"
      sampled_requests_enabled   = true
    }
  }

  # Rate limiting: throttle suspicious request patterns
  rule {
    name     = "php2shell-rate-limit"
    priority = 3

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 100
        aggregate_key_type = "IP"

        scope_down_statement {
          byte_match_statement {
            search_string         = ".php"
            field_to_match {
              uri_path {}
            }
            text_transformation {
              priority = 0
              type     = "LOWERCASE"
            }
            positional_constraint = "ENDS_WITH"
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "php2shell-rate-limit"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "php2shell-containment"
    sampled_requests_enabled   = true
  }

  tags = {
    Purpose   = "incident-response"
    Incident  = "PHP2Shell-2026-10"
    Temporary = "true"
  }
}

# Associate with all ALBs serving PHP applications
resource "aws_wafv2_web_acl_association" "php_albs" {
  for_each = toset(var.php_alb_arns)

  resource_arn = each.value
  web_acl_arn  = aws_wafv2_web_acl.php2shell_emergency.arn
}
 

Примените это через  terraform apply , нацелившись только на модуль WAF. Общее время: около 3 минут от  terraform plan  до активации правил WAF.

Ужесточение групп безопасности 

В этом сценарии у нескольких PHP-инстансов были группы безопасности, разрешающие входящий трафик с  0.0.0.0/0  на порты 80/443 — легаси-конфиги с тех времен, когда перед всем не стояли ALB. Их нужно ограничить только до ALB:

 # Find instances with overly permissive security groups
aws ec2 describe-instances \
    --filters "Name=tag:Runtime,Values=php" \
    --query 'Reservations[].Instances[].{ID:InstanceId,SGs:SecurityGroups[].GroupId}' \
    --output json | \
jq -r '.[].SGs[]' | sort -u | while read sg; do
    aws ec2 describe-security-groups \
        --group-ids "$sg" \
        --query 'SecurityGroups[].IpPermissions[?contains(IpRanges[].CidrIp, `0.0.0.0/0`)]' \
        --output json | jq -e 'length > 0' > /dev/null 2>&1 && echo "OPEN: $sg"
done
 

В симуляции это выявило 12 групп безопасности с публичными входящими правилами. Отзовите публичный доступ и добавьте ссылки на группы безопасности ALB. Во время инцидента вы не рефакторите Terraform-модули — вы используете CLI и приводите состояние в соответствие позже.

Проверка компрометации через GuardDuty 

Прежде чем тратить три часа на патчинг, вы захотите узнать, не скомпрометированы ли вы уже. GuardDuty непрерывно отслеживает индикаторы компрометации — необычные API-вызовы, майнинг криптовалюты, трафик к командным серверам (C2). Проверьте наличие находок, связанных с вашими PHP-инстансами:

 # Check GuardDuty for any findings related to our PHP instances
aws guardduty list-findings \
    --detector-id "$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)" \
    --finding-criteria '{
        "Criterion": {
            "resource.instanceDetails.tags.value": {
                "Eq": ["php"]
            },
            "severity": {
                "Gte": 4
            },
            "updatedAt": {
                "GreaterThanOrEqual": 1729468800000
            }
        }
    }' \
    --query 'FindingIds' \
    --output json
 

В этом сценарии результат — пустой массив, находок нет. GuardDuty не пометил никакой подозрительной активности. Это обнадеживает, но не означает, что вы в безопасности — CVE всего 24 часа, и активная эксплуатация может только набирать обороты.

Дашборд CloudWatch в реальном времени 

Далее, настройте дашборд CloudWatch для мониторинга ситуации во время патчинга — количество блокировок WAF, здоровье инстансов и частоту ошибок Apache:

 # Quick dashboard for incident monitoring
aws cloudwatch put-dashboard \
    --dashboard-name "PHP2Shell-Incident" \
    --dashboard-body '{
        "widgets": [
            {
                "type": "metric",
                "properties": {
                    "title": "WAF Blocks (PHP2Shell Rules)",
                    "metrics": [
                        ["AWS/WAFV2", "BlockedRequests", "WebACL", "php2shell-containment",
                         "Rule", "php2shell-oom-trigger", {"stat": "Sum", "period": 60}],
                        ["AWS/WAFV2", "BlockedRequests", "WebACL", "php2shell-containment",
                         "Rule", "php2shell-rate-limit", {"stat": "Sum", "period": 60}]
                    ],
                    "view": "timeSeries",
                    "period": 60
                }
            },
            {
                "type": "metric",
                "properties": {
                    "title": "ALB Healthy Host Count",
                    "metrics": [
                        ["AWS/ApplicationELB", "HealthyHostCount",
                         "TargetGroup", "php-app-tg",
                         "LoadBalancer", "app/php-alb/abc123",
                         {"stat": "Minimum", "period": 60}]
                    ],
                    "view": "timeSeries",
                    "period": 60
                }
            }
        ]
    }'
 

Через 30 минут после первоначального алерта у вас будет: правила WAF, блокирующие известные паттерны эксплойтов, группы безопасности, ограниченные только ALB, подтверждение от GuardDuty об отсутствии компрометации и дашборд для наблюдения во время патчинга. Теперь начинается настоящая работа.

Исправление — T+45 мин — T+3 ч 

Стратегия исправления 

Имея 214 инстансов в продакшене, стейджинге и общих сервисах, вы не можете пропатчить все сразу. Стратегия:

  1.   Сначала стейджинг — пропатчить все 41 инстанс стейджинга, провести smoke-тесты
  2.   Общие сервисы — пропатчить 17 инстансов в общем аккаунте
  3.   Продакшен — rolling-патчинг с параллельностью 10% и проверками здоровья (health gates)

SSM Run Command обрабатывает все три фазы. SSM-документ управляет самим патчингом: обновление PHP, грациозный перезапуск Apache, проверка новой версии.

SSM-документ для патчинга PHP 

 # Create an SSM document for repeatable patching
aws ssm create-document \
    --name "PHP2Shell-Patch" \
    --document-type "Command" \
    --document-format "YAML" \
    --content '{
        "schemaVersion": "2.2",
        "description": "Patch PHP for CVE-2026-109618 and CVE-2026-109619",
        "parameters": {
            "ExpectedVersion": {
                "type": "String",
                "description": "Expected PHP version after patching (e.g., 8.3.13)"
            }
        },
        "mainSteps": [
            {
                "action": "aws:runShellScript",
                "name": "PatchPHP",
                "inputs": {
                    "runCommand": [
                        "#!/bin/bash",
                        "set -euo pipefail",
                        "",
                        "echo \"=== PHP2Shell Patch — $(date -u) ===\"",
                        "echo \"Instance: $(curl -s http://169.254.169.254/latest/meta-data/instance-id)\"",
                        "",
                        "# Record current version",
                        "CURRENT=$(php -v | head -1 | awk \"{print \\$2}\")",
                        "echo \"Current PHP version: $CURRENT\"",
                        "",
                        "# Update PHP packages",
                        "if command -v dnf &>/dev/null; then",
                        "    dnf update -y php* 2>&1",
                        "elif command -v apt-get &>/dev/null; then",
                        "    apt-get update -qq && apt-get install -y --only-upgrade php* 2>&1",
                        "elif command -v yum &>/dev/null; then",
                        "    yum update -y php* 2>&1",
                        "fi",
                        "",
                        "# Graceful Apache restart (finish existing requests)",
                        "if systemctl is-active --quiet httpd; then",
                        "    systemctl reload httpd",
                        "    echo \"Apache (httpd) reloaded\"",
                        "elif systemctl is-active --quiet apache2; then",
                        "    systemctl reload apache2",
                        "    echo \"Apache (apache2) reloaded\"",
                        "fi",
                        "",
                        "# Verify new version",
                        "NEW=$(php -v | head -1 | awk \"{print \\$2}\")",
                        "echo \"New PHP version: $NEW\"",
                        "",
                        "if [[ \"$NEW\" == \"{{ ExpectedVersion }}\" ]]; then",
                        "    echo \"PATCH SUCCESS: $CURRENT -> $NEW\"",
                        "else",
                        "    echo \"WARNING: Expected {{ ExpectedVersion }}, got $NEW\"",
                        "    exit 1",
                        "fi"
                    ]
                }
            }
        ]
    }'
 

Фаза 1: Стейджинг (T+45 мин — T+1 ч 15 мин) 

Сначала стейджинг. Все 41 инстанс, параллельность 50% — быстро, но с достаточным запасом, чтобы заметить проблемы:

 # Patch staging — PHP 8.3 instances
aws ssm send-command \
    --document-name "PHP2Shell-Patch" \
    --targets "Key=tag:Environment,Values=staging" \
    --parameters '{"ExpectedVersion":["8.3.13"]}' \
    --max-concurrency "50%" \
    --max-errors "5" \
    --timeout-seconds 300 \
    --comment "PHP2Shell patch - staging - PHP 8.3" \
    --output json | jq -r '.Command.CommandId'
 

Пока SSM патчит стейджинг, запустите набор интеграционных тестов против стейджинг-окружения. Логи Apache не должны показывать ошибок после перезагрузки. PHP  opcache_reset()  автоматически очищает кэш байт-кода при перезагрузке — никакого устаревшего кода.

Фаза 2: Общие сервисы (T+1 ч 15 мин — T+1 ч 30 мин) 

В аккаунте общих сервисов смесь PHP 8.3 и PHP 8.1. Запустите две команды — по одной на версию:

 # PHP 8.3 instances in shared services
aws ssm send-command \
    --document-name "PHP2Shell-Patch" \
    --targets '[
        {"Key":"tag:Environment","Values":["shared"]},
        {"Key":"tag:PHPVersion","Values":["8.3"]}
    ]' \
    --parameters '{"ExpectedVersion":["8.3.13"]}' \
    --max-concurrency "50%" \
    --max-errors "2" \
    --timeout-seconds 300 \
    --comment "PHP2Shell patch - shared - PHP 8.3"

# PHP 8.1 instances in shared services
aws ssm send-command \
    --document-name "PHP2Shell-Patch" \
    --targets '[
        {"Key":"tag:Environment","Values":["shared"]},
        {"Key":"tag:PHPVersion","Values":["8.1"]}
    ]' \
    --parameters '{"ExpectedVersion":["8.1.31"]}' \
    --max-concurrency "50%" \
    --max-errors "1" \
    --timeout-seconds 300 \
    --comment "PHP2Shell patch - shared - PHP 8.1"
 

Фаза 3: Продакшен (T+1 ч 30 мин — T+3 ч) 

Продакшен требует больше осторожности. 156 инстансов за ALB, обслуживающих клиентский трафик. Патчите с параллельностью 10% и максимальным порогом ошибок 5 — если более 5 инстансов не проходят патчинг, команда останавливается, и вы проводите расследование.

Критически важный момент: health-чеки ALB. Каждый инстанс должен пройти health-чеки после перезагрузки Apache, прежде чем SSM перейдет к следующей пачке. Настройте свои ALB с health-чеком, который действительно нагружает PHP:

 Health check path:    /health.php
Healthy threshold:    2
Unhealthy threshold:  3
Interval:             10 seconds
Timeout:              5 seconds
 

Эндпоинт  /health.php  не должен просто возвращать 200 OK — он должен проверять, что PHP обрабатывает запросы и может подключиться к базе данных:

 <?php
// /var/www/html/health.php
http_response_code(200);
header('Content-Type: application/json');

$checks = [
    'php' => phpversion(),
    'opcache' => opcache_get_status(false) ? 'active' : 'disabled',
    'db' => 'unknown',
];

try {
    $pdo = new PDO(
        "mysql:host=" . getenv('DB_HOST') . ";port=3306",
        getenv('DB_USER'),
        getenv('DB_PASS'),
        [PDO::ATTR_TIMEOUT => 2]
    );
    $checks['db'] = 'connected';
} catch (PDOException $e) {
    $checks['db'] = 'error';
    http_response_code(503);
}

echo json_encode($checks);
 

Теперь rolling-патчинг:

 # Production PHP 8.3 — 10% concurrency, strict error threshold
aws ssm send-command \
    --document-name "PHP2Shell-Patch" \
    --targets '[
        {"Key":"tag:Environment","Values":["production"]},
        {"Key":"tag:PHPVersion","Values":["8.3"]}
    ]' \
    --parameters '{"ExpectedVersion":["8.3.13"]}' \
    --max-concurrency "10%" \
    --max-errors "5" \
    --timeout-seconds 600 \
    --comment "PHP2Shell patch - production - PHP 8.3"
 

Во время раскатки следите за дашбордом CloudWatch. Количество здоровых хостов ALB должно падать ровно на размер пачки (10%) во время каждой волны, а затем восстанавливаться в течение 20-30 секунд по мере перезагрузки Apache и прохождения health-чеков. В реальном инциденте ожидайте, что ваш дашборд WAF покажет заблокированные запросы от автоматизированных сканеров, ищущих эту CVE — в этой симуляции я оценил примерно 1847 блокировок, основываясь на наблюдаемых скоростях сканирования для похожих CVE в PHP.

Обработка инстансов с PHP 8.1 без апстрим-фиксов 

Вот распространенная сложность: некоторые инстансы могут работать на PHP 8.1.29 в Amazon Linux 2, где в репозитории пакетов еще нет 8.1.31. Для них нужно применить другой подход:

 # For instances where the patched version isn't in repos yet,
# install from the Remi repository (Amazon Linux 2)
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --targets "Key=InstanceIds,Values=i-0c3d4e5f67890123,i-0e5f678901234567,i-0f6789012345678a" \
    --parameters 'commands=[
        "yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm",
        "yum-config-manager --enable remi-php81",
        "yum update -y php*",
        "php -v | head -1",
        "systemctl reload httpd"
    ]' \
    --comment "PHP2Shell - PHP 8.1 from Remi repo"
 

Это обновит такие инстансы до 8.1.31. Задокументируйте этот воркэраунд и добавьте его в свой ранбук по патчингу для будущего использования.

Проверка и расследование — T+3 ч — T+4 ч 

Патчинг завершен. Теперь нужно доказать это — себе, руководству и для отчета об инциденте.

Повторная проверка версий PHP 

Та же команда SSM, что и при обнаружении, но теперь все инстансы должны сообщать об исправленных версиях:

 # Verify all instances are on patched versions
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --targets "Key=tag:ManagedBy,Values=terraform" \
    --parameters 'commands=["php -v 2>/dev/null | head -1 || echo NO_PHP"]' \
    --comment "PHP2Shell - post-patch verification" \
    --output json | jq -r '.Command.CommandId'
 

Разберите результаты и подтвердите: 0 инстансов работают на уязвимых версиях. Каждый PHP-инстанс должен быть на 8.3.13 или 8.1.31.

Повторное сканирование Inspector 

Inspector сканирует непрерывно, но вам нужны свежие данные, чтобы закрыть вопрос. Запустите повторное сканирование:

 # Trigger Inspector rescan for affected instances
for account in 111111111111 222222222222 333333333333; do
    aws inspector2 create-findings-report \
        --report-format CSV \
        --s3-destination "bucketName=security-reports-${account},keyPrefix=php2shell/post-patch" \
        --filter-criteria '{
            "vulnerabilityId": [
                {"comparison": "EQUALS", "value": "CVE-2026-109618"},
                {"comparison": "EQUALS", "value": "CVE-2026-109619"}
            ]
        }'
done
 

В течение 30 минут все 214 находок должны перейти из статуса ACTIVE в CLOSED. Inspector подтверждает: в парке больше нет уязвимых версий PHP.

Анализ логов Apache 

Даже если GuardDuty не показывает индикаторов компрометации, вам следует проанализировать логи доступа Apache за период экспозиции вокруг раскрытия CVE, чтобы поискать попытки эксплуатации:

 # Search for PHP2Shell exploit patterns in Apache logs
# Run via SSM across all PHP instances
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --targets "Key=tag:Runtime,Values=php" \
    --parameters 'commands=[
        "echo \"=== Instance: $(curl -s http://169.254.169.254/latest/meta-data/instance-id) ===\"",
        "zgrep -c \"memory_limit\\|ini_set.*memory\\|str_repeat.*9999\" /var/log/httpd/access_log* /var/log/apache2/access.log* 2>/dev/null || echo \"No matches\"",
        "echo \"Large POST requests (>5MB):\"",
        "awk \"\\$9 > 5242880 && /POST/\" /var/log/httpd/access_log /var/log/apache2/access.log 2>/dev/null | wc -l"
    ]' \
    --comment "PHP2Shell - log analysis for exploitation"
 

В реальном сценарии вы, скорее всего, увидели бы запросы, содержащие  memory_limit  в строках запроса — автоматизированные сканеры уязвимостей (Nuclei, Nikto) обычно начинают зондирование в течение 12 часов после появления CVE. Ищите большие POST-запросы, соответствующие паттерну heap spray. Их отсутствие говорит о том, что эксплуатация не удалась.

Автоматизированный отчет о проверке после патчинга 

Наконец, создайте отчет о проверке для записи об инциденте:

 #!/usr/bin/env python3
"""Generate post-patch verification report for PHP2Shell incident."""
import boto3
import json
from datetime import datetime

def generate_report():
    inspector = boto3.client("inspector2")
    ssm = boto3.client("ssm")

    # Get current Inspector findings for PHP2Shell CVEs
    findings = []
    paginator = inspector.get_paginator("list_findings")
    for page in paginator.paginate(
        filterCriteria={
            "vulnerabilityId": [
                {"comparison": "EQUALS", "value": "CVE-2026-109618"},
                {"comparison": "EQUALS", "value": "CVE-2026-109619"},
            ],
        }
    ):
        findings.extend(page["findings"])

    active = [f for f in findings if f["status"] == "ACTIVE"]
    closed = [f for f in findings if f["status"] == "CLOSED"]

    report = {
        "incident": "PHP2Shell (CVE-2026-109618, CVE-2026-109619)",
        "report_generated": datetime.utcnow().isoformat() + "Z",
        "inspector_findings": {
            "active": len(active),
            "closed": len(closed),
            "total": len(findings),
        },
        "status": "RESOLVED" if len(active) == 0 else "IN_PROGRESS",
    }

    # Get SSM managed instance count for verification
    inventory = ssm.get_inventory(
        Filters=[
            {
                "Key": "AWS:Application.Name",
                "Values": ["php*"],
                "Type": "Equal",
            }
        ]
    )

    report["ssm_managed_php_instances"] = len(
        inventory.get("Entities", [])
    )

    print(json.dumps(report, indent=2))

    if report["status"] == "RESOLVED":
        print(f"\nPHP2Shell remediation verified.")
        print(f"  Active findings: {len(active)}")
        print(f"  Closed findings: {len(closed)}")
        print(f"  All {len(closed)} previously vulnerable instances patched.")
    else:
        print(f"\nWARNING: {len(active)} active findings remain.")
        for f in active:
            print(f"  {f['resources'][0]['id']}: {f['title']}")

if __name__ == "__main__":
    generate_report()
 

Вывод:

 {
  "incident": "PHP2Shell (CVE-2026-109618, CVE-2026-109619)",
  "report_generated": "2026-10-22T16:12:00Z",
  "inspector_findings": {
    "active": 0,
    "closed": 428,
    "total": 428
  },
  "status": "RESOLVED",
  "ssm_managed_php_instances": 214
}
 

428 закрытых находок (214 инстансов × 2 CVE). Ноль активных. Инцидент разрешен. Это целевое конечное состояние.

Предотвращение будущих инцидентов 

Уметь реагировать за четыре часа — это хорошо. Необходимость никогда не суетиться — еще лучше. Вот три вещи, которые стоит создать проактивно, до того как появится следующая критическая CVE.

Terraform-модуль: Готовность к реагированию на инциденты безопасности 

Упакуйте свою инфраструктуру реагирования на инциденты в Terraform-модуль, который может развернуть любая команда. Он подготавливает Inspector, сбор инвентаря SSM, ссылки на группы правил WAF, GuardDuty и алармы CloudWatch — все, что нужно для обнаружения и реагирования на следующий PHP2Shell:

 # modules/security-response-readiness/main.tf

# --- Inspector ---
resource "aws_inspector2_enabler" "ec2" {
  account_ids    = [data.aws_caller_identity.current.account_id]
  resource_types = ["EC2"]
}

# Inspector event bridge rule for critical findings
resource "aws_cloudwatch_event_rule" "inspector_critical" {
  name        = "${var.environment}-inspector-critical-findings"
  description = "Alert on Inspector critical/high findings"

  event_pattern = jsonencode({
    source      = ["aws.inspector2"]
    detail-type = ["Inspector2 Finding"]
    detail = {
      severity = ["CRITICAL", "HIGH"]
      status   = ["ACTIVE"]
    }
  })
}

resource "aws_cloudwatch_event_target" "inspector_to_sns" {
  rule      = aws_cloudwatch_event_rule.inspector_critical.name
  target_id = "send-to-sns"
  arn       = var.alert_sns_topic_arn
}

# --- SSM Inventory ---
resource "aws_ssm_association" "inventory" {
  name             = "AWS-GatherSoftwareInventory"
  association_name = "${var.environment}-software-inventory"

  schedule_expression = "rate(12 hours)"

  targets {
    key    = "tag:ManagedBy"
    values = ["terraform"]
  }

  parameters = {
    applications    = "Enabled"
    awsComponents   = "Enabled"
    customInventory = "Enabled"
  }
}

# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
    runtime_monitoring {
      autoEnable = "ALL"
    }
  }
}

# --- CloudWatch Alarm: Inspector finding count ---
resource "aws_cloudwatch_metric_alarm" "inspector_critical_count" {
  alarm_name          = "${var.environment}-inspector-critical-findings"
  alarm_description   = "Critical Inspector findings detected"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "CRITICAL"
  namespace           = "AWS/Inspector2"
  period              = 300
  statistic           = "Sum"
  threshold           = 0
  treat_missing_data  = "notBreaching"

  alarm_actions = [var.alert_sns_topic_arn]

  tags = {
    Environment = var.environment
    Purpose     = "security-response"
  }
}

# --- WAF: Pre-staged rule group reference ---
resource "aws_wafv2_web_acl" "security_baseline" {
  name  = "${var.environment}-security-baseline"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  rule {
    name     = "aws-managed-common"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "common-rules"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "aws-managed-known-bad-inputs"
    priority = 2

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "known-bad-inputs"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "aws-managed-php"
    priority = 3

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesPhpRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "php-rules"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "security-baseline"
    sampled_requests_enabled   = true
  }

  tags = {
    Environment = var.environment
    Purpose     = "security-baseline"
  }
}

data "aws_caller_identity" "current" {}

variable "environment" {
  type = string
}

variable "alert_sns_topic_arn" {
  type        = string
  description = "SNS topic for security alerts"
}
 

Окно обслуживания SSM для автоматизированного патчинга 

Настройте автоматизированный еженедельный патчинг для пакетов PHP, чтобы вы никогда не отставали от обновлений безопасности более чем на 7 дней:

 # modules/ssm-patching/main.tf
resource "aws_ssm_maintenance_window" "php_patching" {
  name              = "${var.environment}-php-weekly-patch"
  schedule          = "cron(0 6 ? * SUN *)"  # Sunday 6 AM UTC
  duration          = 2
  cutoff            = 1
  allow_unassociated_targets = false

  tags = {
    Environment = var.environment
    Purpose     = "automated-patching"
  }
}

resource "aws_ssm_maintenance_window_target" "php_instances" {
  window_id     = aws_ssm_maintenance_window.php_patching.id
  name          = "php-instances"
  resource_type = "INSTANCE"

  targets {
    key    = "tag:Runtime"
    values = ["php"]
  }
}

resource "aws_ssm_maintenance_window_task" "patch_php" {
  window_id        = aws_ssm_maintenance_window.php_patching.id
  task_type        = "RUN_COMMAND"
  task_arn         = "AWS-RunPatchBaseline"
  priority         = 1
  max_concurrency  = "10%"
  max_errors       = "5"

  targets {
    key    = "WindowTargetIds"
    values = [aws_ssm_maintenance_window_target.php_instances.id]
  }

  task_invocation_parameters {
    run_command_parameters {
      parameter {
        name   = "Operation"
        values = ["Install"]
      }
      parameter {
        name   = "RebootOption"
        values = ["NoReboot"]
      }
    }
  }
}

# Custom patch baseline that includes PHP packages
resource "aws_ssm_patch_baseline" "php" {
  name             = "${var.environment}-php-patch-baseline"
  operating_system = "AMAZON_LINUX_2023"

  approval_rule {
    approve_after_days = 0  # Approve immediately for critical
    compliance_level   = "CRITICAL"

    patch_filter {
      key    = "SEVERITY"
      values = ["Critical", "Important"]
    }
    patch_filter {
      key    = "CLASSIFICATION"
      values = ["Security"]
    }
  }

  approval_rule {
    approve_after_days = 3  # 3-day delay for non-critical
    compliance_level   = "MEDIUM"

    patch_filter {
      key    = "SEVERITY"
      values = ["Medium", "Low"]
    }
  }

  tags = {
    Environment = var.environment
    Purpose     = "php-patching"
  }
}
 

Пайплайн Golden AMI 

Настоящее решение — встраивание пропатченного PHP в AMI, чтобы новые инстансы запускались с последней версией. Добавьте шаг в ваш пайплайн сборки AMI, который завершается с ошибкой, если для установленной версии PHP существуют критические CVE:

 #!/bin/bash
# golden-ami-php-gate.sh — runs during AMI build
# Fails the build if PHP has known critical CVEs
set -euo pipefail

PHP_VERSION=$(php -v | head -1 | awk '{print $2}')
echo "Checking PHP $PHP_VERSION for known critical CVEs..."

# Query Inspector's vulnerability database (via NIST NVD API)
RESPONSE=$(curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?\
cpeName=cpe:2.3:a:php:php:${PHP_VERSION}:*:*:*:*:*:*:*\
&cvssV3Severity=CRITICAL" 2>/dev/null)

VULN_COUNT=$(echo "$RESPONSE" | jq -r '.totalResults // 0')

if [[ "$VULN_COUNT" -gt 0 ]]; then
    echo "BLOCKING: PHP $PHP_VERSION has $VULN_COUNT critical CVE(s):"
    echo "$RESPONSE" | jq -r '.vulnerabilities[].cve | "\(.id): \(.descriptions[0].value)"'
    echo ""
    echo "Update PHP before building this AMI."
    exit 1
fi

echo "PHP $PHP_VERSION — no critical CVEs found. AMI build may proceed."
 

Этот гейт работает в пайплайне сборки Packer. Если у PHP есть критические CVE, сборка AMI падает и оповещает команду. Больше никакого деплоя уязвимых рантаймов в продакшен.

Извлеченные уроки 

 1. Непрерывный мониторинг Inspector не подлежит обсуждению. Inspector находит уязвимости для вас автоматически, в течение нескольких часов после публикации CVE. Если вы полагаетесь на ежеквартальные сканирования уязвимостей, ваши инстансы могут быть открыты месяцами. Включите Inspector в каждом аккаунте, для каждого типа ресурсов. Это стоит копейки по сравнению с альтернативой.

 2. SSM Run Command превращает задачу «пропатчить 214 серверов» из проекта на выходные в задачу на обеденный перерыв. Без SSM вам пришлось бы подключаться по SSH к инстансам по одному или собирать новый AMI и перезапускать весь парк. SSM позволяет патчить волнами с контролем параллельности и пороговыми значениями ошибок. Весь патчинг продакшена в этом упражнении занял 90 минут без простоя.

 3. WAF дает вам время, а не безопасность. Правила WAF блокируют известные паттерны эксплойтов, и это ценно — это снижает вашу экспозицию, пока вы патчите. Но WAF ловит только известные паттерны. Локализация — это мост к патчингу, а не его замена.

 4. Тегируйте свои инстансы последовательно, иначе таргетинг SSM не сработает. Теги вроде  Runtime=php  и  Environment=production  позволяют точно нацеливать команды SSM. Инстансы с чрезмерно разрешительными группами безопасности в этом сценарии? Они также были теми, у кого тегирование было непоследовательным. Тегирование ресурсов — это не только для биллинга, это ваш механизм таргетинга во время инцидентов.

 5. Реагирование на инциденты, которое вы практикуете, — это то, которое работает. Именно поэтому я построил эту симуляцию. Наличие инструментов (Inspector, SSM, WAF) — это только половина дела, вам также нужен отрепетированный процесс. Прохождение реалистичного сценария от начала до конца, с реальными командами и реальными конфигами, выявляет пробелы, которые никогда не покажет обсуждение за столом. Проведите это упражнение со своей командой до того, как появится следующая критическая CVE.

Чек-лист действий 

До инцидента (разверните сейчас) 

  • [ ] Включите Inspector во всех аккаунтах (EC2, ECR, Lambda)
  • [ ] Разверните SSM-агент на каждом EC2-инстансе
  • [ ] Настройте сбор инвентаря ПО через SSM (расписание 12 часов)
  • [ ] Включите GuardDuty со сканированием вредоносного ПО на EBS и мониторингом рантайма
  • [ ] Разверните WAF с управляемыми группами правил AWS (Common, Known Bad Inputs, PHP)
  • [ ] Настройте правило EventBridge: критические находки Inspector → SNS → Slack/PagerDuty
  • [ ] Тегируйте все инстансы последовательно ( Runtime ,  Environment ,  ManagedBy )
  • [ ] Создайте эндпоинты health-чеков, которые проверяют стек приложения (а не просто HTTP 200)

Во время инцидента 

  • [ ] Получите находки Inspector — определите радиус поражения (аккаунты, инстансы, версии)
  • [ ] Запустите команду инвентаризации SSM — получите точные версии по всему парку
  • [ ] Активируйте правила локализации WAF — заблокируйте известные паттерны эксплойтов
  • [ ] Проверьте GuardDuty — есть ли индикаторы компрометации?
  • [ ] Ужесточите группы безопасности — только ALB, никакого прямого публичного доступа
  • [ ] Настройте дашборд мониторинга инцидента (блокировки WAF, здоровые хосты, частота ошибок)
  • [ ] Сначала пропатчите стейджинг, проведите smoke-тесты
  • [ ] Пропатчите продакшен с rolling-параллельностью (10%) и проверками здоровья
  • [ ] Проверьте: проверка версии SSM, повторное сканирование Inspector, анализ логов

После инцидента 

  • [ ] Сформировать отчет о проверке (результаты Inspector закрыты, версии подтверждены)
  • [ ] Проанализировать логи на предмет попыток эксплуатации в период уязвимости
  • [ ] Обновить AMI pipeline, добавив проверку CVE для затронутой среды выполнения
  • [ ] Настроить автоматическое еженедельное обновление через SSM Maintenance Window
  • [ ] Задокументировать runbook с точными использованными командами
  • [ ] Провести командно-штабные учения в течение 30 дней
  • [ ] Проверить и удалить временные правила WAF (базовые правила оставить постоянными)
  • [ ] Обновить аудит групп безопасности для выявления будущих отклонений

Это отдельная публикация, не входящая в серию статей по оптимизации затрат в AWS. Материалы по оптимизации затрат можно найти в полной серии на Dev.to.