1. 从标题拆解这个项目的真实意图“PHP小马大马实现分析编写”这个标题乍一看像是某种安全攻防领域的术语但抛开那些容易引发联想的字眼它本质上指向的是一个非常具体的技术命题用PHP语言实现一个轻量级的文件管理与命令执行工具并对比分析“小马”与“大马”两种不同体量实现方案的差异。这里的“小马”通常指代码量极少、功能单一、隐蔽性强的单文件脚本而“大马”则指功能完备、界面丰富、模块化程度高的管理面板。两者之间的取舍恰恰是很多PHP开发者在学习文件系统操作、会话管理、权限控制时绕不开的经典案例。我最早接触这类东西是在做内部运维工具的时候。当时团队需要一个能快速上传到任意PHP环境、立刻就能查看目录结构、编辑配置文件、执行简单系统命令的应急工具。市面上的面板太重部署成本高而手写一个单文件脚本反而最灵活。后来在多个项目中反复迭代逐渐摸清了“小马”和“大马”各自适合的场景、实现难点和避坑要点。这篇文章就把我这些年积累的实现思路、代码细节和踩坑经验完整分享出来适合有一定PHP基础、想深入理解文件系统操作与轻量级工具设计的读者。需要提前说明的是本文所有代码和案例仅用于本地开发环境下的技术学习与内部工具原型验证严禁用于任何未经授权的系统。所有涉及命令执行的部分在实际生产环境中必须做严格的白名单校验和权限隔离这是底线。2. 核心思路与方案选型为什么分“小”和“大”2.1 小马的核心设计哲学极简与生存优先所谓“小马”核心就一个字小。代码量通常控制在几十行以内功能只保留最刚需的几项——比如文件浏览、文件读取、文件写入、以及一个最基础的操作入口。它的设计目标不是好用而是能在各种极端环境下存活并完成最小任务。为什么要把代码压到这么小原因有几个。第一代码越短被静态扫描工具特征匹配的概率越低这在内部渗透测试演练中很有意义。第二短代码意味着依赖少不需要额外的扩展库PHP原生函数就能跑起来。第三传输和部署成本极低一个请求就能完成上传和初始化。但小马的缺点同样明显没有界面操作靠URL参数传递每次都要手动拼参数没有错误处理一出问题就白屏没有权限校验谁拿到地址谁就能用。所以它只适合临时、一次性的场景。我早期写过一个最精简的版本核心逻辑大概是这样?php $action $_GET[a] ?? ls; $path $_GET[p] ?? .; if ($action ls) { foreach (scandir($path) as $f) { echo $f . \n; } } elseif ($action read) { echo file_get_contents($path); } elseif ($action write) { file_put_contents($path, $_POST[c]); echo ok; }这段代码能跑但问题一大堆没有路径过滤可以跳转到任意目录没有输出转义文件名里的特殊字符会破坏页面没有写入校验任意内容都能覆盖。所以它只能存在于实验环境。2.2 大马的功能矩阵从能用走向好用大马则完全是另一个思路。它要解决的是“长期、高频、多人协作”场景下的管理需求。典型的大马会包含以下模块文件管理目录树浏览、文件搜索、批量重命名、权限修改、压缩解压。文件编辑内置代码编辑器支持语法高亮、在线保存、版本备份。命令执行带白名单的命令行接口支持常用系统命令。数据库管理连接配置、表结构查看、SQL执行、数据导出。会话管理登录认证、操作日志、IP白名单。其他工具端口扫描、信息收集、编码转换等。功能一多代码量自然上去通常几千行起步文件数量也从单个变成十几个。这时候就需要考虑目录结构、模块加载、配置分离、前端资源管理等一系列工程问题。我后来重构过一版大马目录结构大致如下/ ├── index.php # 入口与路由 ├── config.php # 配置数据库、白名单、密钥 ├── core/ │ ├── auth.php # 认证逻辑 │ ├── files.php # 文件操作类 │ ├── cmd.php # 命令执行类 │ └── db.php # 数据库操作类 ├── views/ │ ├── layout.php # 页面骨架 │ ├── files.php # 文件管理界面 │ └── editor.php # 编辑器界面 └── assets/ ├── style.css └── app.js这种结构的好处是职责清晰改一个模块不会影响其他部分。但代价是部署时要把整个目录传上去而且入口文件一旦暴露所有功能都暴露了。所以大马必须配认证否则就是灾难。2.3 选型决策什么时候用小什么时候用大我总结了一个简单的判断表场景推荐方案理由临时查看一个文件内容小马单请求完成无需登录批量修改多个目录权限大马需要界面和批量操作内部演练快速验证小马代码短易清理长期运维管理大马功能全有日志网络环境不稳定小马请求小成功率高多人协作大马需要认证和权限区分这个表不是绝对的但能覆盖大部分决策场景。核心原则是任务越简单、越临时就越偏向小马任务越复杂、越长期就越需要大马。3. 小马实现细节从零写一个可用的极简工具3.1 基础功能的最小闭环小马虽然小但至少要完成一个闭环接收指令、执行操作、返回结果。我用一个单文件实现过完整闭环核心参数只有三个action、path、content。?php // 基础配置 $allowActions [list, read, write, delete]; $baseDir __DIR__; // 限制在脚本所在目录 $action $_REQUEST[action] ?? list; $path $_REQUEST[path] ?? .; $content $_REQUEST[content] ?? ; // 路径安全处理 $realPath realpath($baseDir . / . $path); if ($realPath false || strpos($realPath, $baseDir) ! 0) { die(invalid path); } // 动作分发 switch ($action) { case list: $items scandir($realPath); header(Content-Type: application/json); echo json_encode($items); break; case read: if (is_file($realPath)) { echo htmlspecialchars(file_get_contents($realPath)); } break; case write: if (is_file($realPath) is_writable($realPath)) { file_put_contents($realPath, $content); echo written; } break; case delete: if (is_file($realPath)) { unlink($realPath); echo deleted; } break; default: die(unknown action); }这段代码比之前的版本安全了一些加了路径限制和动作白名单。但依然有几个问题没有认证任何人访问都能操作没有CSRF防护没有操作日志。所以它只适合在完全可控的本地环境使用。3.2 路径安全的三个关键检查路径穿越是小马最容易出问题的地方。我见过太多版本直接用$_GET[path]拼路径结果../../etc/passwd就能读走。要防住至少做三层检查第一层realpath()解析真实路径。这个函数会把..和符号链接都展开得到绝对路径。如果解析失败说明路径不存在直接拒绝。第二层前缀匹配。把解析后的路径和允许的根目录做字符串前缀比较。注意要用strpos($realPath, $baseDir) 0不能用strstr因为strstr返回的是子串容易误判。第三层目录分隔符处理。在Windows下路径分隔符是\Linux下是/拼接时要统一。我一般用DIRECTORY_SEPARATOR常量或者直接统一转成/再处理。注意realpath()在文件不存在时返回false所以如果要创建新文件不能直接用这个函数判断。我的做法是先检查父目录是否在允许范围内再拼接文件名。3.3 输出转义与编码问题小马返回的数据经常直接输出到浏览器如果不转义文件名里的script就会执行。我踩过一次坑一个文件名包含img srcx onerroralert(1)结果每次列目录都弹窗。后来所有输出都加htmlspecialchars($str, ENT_QUOTES, UTF-8)。编码问题也常见。中文文件名在不同系统下编码不同Windows可能是GBKLinux是UTF-8。如果直接scandir再json_encode中文会变成\uXXXX前端显示正常但可读性差。我的做法是先用mb_convert_encoding统一转成UTF-8再输出。function fixEncoding($str) { $encoding mb_detect_encoding($str, [UTF-8, GBK, GB2312]); if ($encoding ! UTF-8) { return mb_convert_encoding($str, UTF-8, $encoding); } return $str; }这个函数在处理混合编码目录时特别有用。3.4 小马的隐蔽性优化如果只是内部使用隐蔽性不是重点。但在演练场景下小马需要尽量不引起注意。几个常用技巧文件名伪装成正常文件比如wp-config.php、index.php、favicon.ico。代码中不出现敏感函数名用变量函数或字符串拼接绕过静态扫描。输出内容尽量模拟正常页面比如返回一个404页面或空白页。请求参数用常见名称如id、page、action避免cmd、shell这类词。但我要强调这些技巧仅用于授权的内部演练任何未经授权的使用都是违规的。4. 大马实现细节模块化与工程化4.1 认证模块的设计与实现大马必须要有认证否则功能越多风险越大。我实现过最简单的认证是基于会话的密码登录session_start(); function checkAuth() { if (empty($_SESSION[logged_in])) { if ($_POST[password] your_password_here) { $_SESSION[logged_in] true; } else { include views/login.php; exit; } } }这个逻辑很基础但有几个细节要注意。第一密码不能明文写在代码里至少用password_hash存哈希。第二登录失败要加延迟防止暴力破解。第三会话ID要定期更换防止固定攻击。更完善的做法是加IP白名单$allowIPs [127.0.0.1, 192.168.1.100]; if (!in_array($_SERVER[REMOTE_ADDR], $allowIPs)) { die(access denied); }这样即使密码泄露非白名单IP也进不来。4.2 文件管理模块的核心逻辑文件管理是大马使用频率最高的模块。我把它拆成几个独立功能列目录、读文件、写文件、删文件、重命名、改权限、压缩、解压。列目录要支持排序和过滤function listDir($path, $sort name, $order asc) { $items []; foreach (scandir($path) as $item) { if ($item . || $item ..) continue; $full $path . / . $item; $items[] [ name $item, type is_dir($full) ? dir : file, size is_file($full) ? filesize($full) : 0, mtime filemtime($full), perms substr(sprintf(%o, fileperms($full)), -4), ]; } // 排序逻辑 usort($items, function($a, $b) use ($sort, $order) { $cmp $a[$sort] $b[$sort]; return $order asc ? $cmp : -$cmp; }); return $items; }这个函数返回结构化数据前端渲染成表格支持点击表头排序。比小马的纯文本输出好用得多。文件编辑要支持语法高亮和保存。我用的方案是前端用CodeMirror或Ace编辑器后端只负责读写。保存时先备份原文件function saveFile($path, $content) { if (file_exists($path)) { copy($path, $path . .bak_ . date(YmdHis)); } return file_put_contents($path, $content); }备份文件多了会占空间我一般只保留最近5个版本用glob匹配后按时间排序删除旧的。4.3 命令执行模块的安全边界命令执行是最敏感的功能必须做严格限制。我的做法是白名单加参数过滤$allowCmds [ls, cat, grep, find, ps, netstat]; $cmd $_POST[cmd] ?? ; $parts explode( , $cmd); $base $parts[0]; if (!in_array($base, $allowCmds)) { die(command not allowed); } // 过滤特殊字符 $cmd escapeshellcmd($cmd); $output shell_exec($cmd . 21); echo pre . htmlspecialchars($output) . /pre;escapeshellcmd能转义大部分危险字符但不能完全依赖它。更安全的做法是用proc_open配合参数数组避免shell解析$descriptors [ 0 [pipe, r], 1 [pipe, w], 2 [pipe, w], ]; $process proc_open([ls, -la, $path], $descriptors, $pipes); if (is_resource($process)) { $output stream_get_contents($pipes[1]); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); echo htmlspecialchars($output); }这种方式把命令和参数分开参数不会被shell解释安全性高很多。4.4 数据库管理模块的通用设计数据库管理模块要支持多种数据库我抽象了一个接口interface DbAdapter { public function connect($config); public function query($sql); public function listTables(); public function describeTable($table); }然后针对MySQL、SQLite、PostgreSQL分别实现。MySQL用PDOclass MysqlAdapter implements DbAdapter { private $pdo; public function connect($config) { $dsn mysql:host{$config[host]};dbname{$config[dbname]};charsetutf8mb4; $this-pdo new PDO($dsn, $config[user], $config[pass]); $this-pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } public function query($sql) { $stmt $this-pdo-query($sql); return $stmt-fetchAll(PDO::FETCH_ASSOC); } // ... }查询结果用表格展示支持分页和导出CSV。导出时注意转义逗号和换行否则CSV会错位。5. 实操过程与核心环节实现5.1 环境准备与基础配置先在本地搭一个PHP环境。我用的是PHP 7.4加Apache也可以用Nginx加PHP-FPM。关键配置项; php.ini display_errors On error_reporting E_ALL upload_max_filesize 50M post_max_size 50M max_execution_time 300这些配置在大马中很重要因为要上传大文件、执行长时间操作。小马则不需要改这么多默认配置就能跑。目录权限也要注意。如果要对任意目录操作PHP进程用户需要有相应权限。我一般把工具放在一个独立目录只给这个目录读写权限避免影响系统文件。5.2 小马的完整实现与测试把前面几段代码整合起来加上简单的样式就是一个可用的小马。测试步骤把文件放到Web目录命名为test.php。访问http://localhost/test.php?actionlistpath.应该返回JSON格式的目录列表。访问?actionreadpathtest.php应该返回文件内容。用POST请求actionwritepathnew.txtbody里带contenthello应该创建文件。测试路径穿越?actionreadpath../../etc/passwd应该返回invalid path。如果第5步没有拦住说明路径检查有问题要回去检查realpath和前缀匹配逻辑。5.3 大马的部署与初始化大马部署稍微复杂一点。步骤上传整个目录到Web根目录下的一个子目录比如/tools/。修改config.php设置密码哈希、IP白名单、数据库配置。访问http://localhost/tools/index.php应该跳转到登录页。输入密码登录进入主界面。测试各模块文件管理、命令执行、数据库连接。初始化时要注意第一次登录后立即修改默认密码删除安装脚本检查目录权限确保config.php不可写。5.4 关键参数的计算与选择文件上传大小限制的计算upload_max_filesize和post_max_size要一致且post_max_size要略大于upload_max_filesize因为POST数据还包括其他字段。比如要上传50M文件upload_max_filesize50Mpost_max_size52M。会话超时时间默认session.gc_maxlifetime是1440秒即24分钟。如果操作时间长要调大比如3600秒。但太长也不安全我一般设1800秒。命令执行超时max_execution_time默认30秒对于find、grep这类可能跑很久的命令要调到300秒或更长。也可以用set_time_limit(0)在脚本里动态设置。6. 常见问题与排查技巧实录6.1 小马常见问题速查问题可能原因解决方法访问返回空白PHP未解析或语法错误检查Web服务器配置开启错误显示列目录返回空路径不存在或权限不足用realpath检查路径确认PHP用户有读权限写入失败文件不可写或目录只读检查文件权限确认父目录可写中文文件名乱码编码不一致用mb_convert_encoding统一转UTF-8路径穿越拦截失效前缀匹配逻辑错误用strpos($real, $base) 0注意大小写6.2 大马常见问题速查问题可能原因解决方法登录后立即退出会话保存路径不可写检查session.save_path或改用数据库存会话上传大文件失败PHP配置限制调整upload_max_filesize和post_max_size命令执行无输出命令不存在或被禁用检查白名单用which确认命令路径数据库连接失败扩展未安装或配置错误确认PDO扩展已启用检查连接参数页面样式错乱资源路径错误用绝对路径引用CSS和JS6.3 独家避坑技巧第一个坑scandir返回的数组包含.和..如果不过滤前端会显示这两个特殊目录点击后可能出问题。一定要在循环里跳过。第二个坑file_put_contents在写入大内容时可能失败返回false但不报错。我的做法是检查返回值如果为false用error_get_last()获取错误信息。第三个坑shell_exec默认不返回stderr命令出错时看不到原因。要在命令后加21把错误输出重定向到标准输出。第四个坑大马的文件编辑器如果直接输出文件内容到textarea内容里的/textarea会提前闭合标签。要用htmlspecialchars转义。第五个坑数据库查询结果如果包含二进制数据直接json_encode会失败。要用base64_encode处理二进制字段或者用PDO::PARAM_LOB。6.4 性能优化建议小马本身没什么性能问题但大马在列大目录时会很慢。优化方法用DirectoryIterator代替scandir内存占用更低。分页显示每页50条避免一次渲染上千个文件。缓存目录列表用文件或APCu存设置短过期时间。避免在循环里调filesize和filemtime先收集再批量处理。命令执行模块如果频繁调用可以用持久化进程但实现复杂一般没必要。7. 从工具到工程我的几点个人体会写了这么多版本的小马和大马我最大的体会是工具的价值不在于功能多少而在于是否匹配场景。小马的精髓是“够用就好”大马的精髓是“该有的都有”。很多人一上来就想写大马结果代码臃肿、漏洞百出还不如一个小马实用。另一个体会是安全边界必须清晰。无论是小马还是大马只要涉及文件操作和命令执行就必须假设输入是不可信的。路径要校验、命令要白名单、输出要转义、认证要严格。我见过太多因为图省事而留下后门的案例最后都是自己吃亏。最后分享一个实用技巧不管写什么工具都先写一个最小可用版本跑通核心流程再逐步加功能。这样每加一个功能都能立即验证出问题也容易定位。我写大马时就是先实现文件列表和读取确认没问题后再加写入、删除、命令执行最后才做界面美化。这个顺序让我少走了很多弯路。如果你也在做类似的东西建议从一个小马开始把路径安全和输出转义做扎实然后再考虑扩展。工具是死的场景是活的多想想“这个功能在什么情况下会出问题”比盲目堆代码有用得多。
阅读完成 · 觉得有帮助?