我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

8.3版建站源码代码审计:SQL注入与文件上传链实战

8.3版建站源码代码审计:SQL注入与文件上传链实战 简介ZZCMS 8.3 源码包面向希望从真实项目中练习代码审计、理解PHP架构与建站逻辑的开发者也适合安全入门者作为漏洞分析素材。压缩包以zip格式封装大小约2.8MB上游文件清单显示文件总数为0主要文件类型明细暂未提供。已有786人浏览或学习说明其在同类学习资料中具有一定关注度。围绕源码可重点观察SQL注入、XSS、CSRF等风险的输入验证与权限控制实现梳理MVC分层、路由、模板引擎及插件钩子机制并结合内容发布、用户管理、响应式布局等模块理解CMS的建站流程。它更适合作为对照阅读与审计练习的样本帮助读者把安全问题定位、代码结构分析和PHP项目设计串联起来形成可复用的审查思路。1. 从一套 8.3 版建站源码说起代码审计练手为什么选它拿到这套 8.3 版建站源码时我先做了件很枯燥的事——数根目录下的 .php 文件。一百多个入口散在根目录后台、会员中心、安装目录各成一套这种“多入口、少框架”的网站建站老架构对做代码审计的人来说反而是好事它把建站程序里最典型的几种坏习惯全摊开了拼接 SQL、文件名直取、路径靠字符串相加一个不落。我前后用它练过三轮第一轮只跑扫描器报告里几十条全是误报第二轮开始手工跟 include 链才挖到真东西第三轮把审计过程写成可复现的步骤才敢说摸清了它的脾气。下面写的就是第三轮的做法、判断依据和踩过的坑新手能照着走一遍做建站交付的熟手也能拿来判断手上的老系统还值不值得继续维护。2. 上手准备把 8.3 源码跑起来并画出攻击面地图2.1 目录结构和入口文件先认路再动刀解压之后别急着打开编辑器乱翻。我一般先看三层第一层是目录名admin、user、inc、install、uploadfile、template这套命名基本能推断出后台、会员、公共库、安装向导、上传落地目录和模板第二层是根目录散落的 .php 文件它们往往是最早写的、最没人管、也最容易被忘记鉴权的入口第三层是inc里的公共文件后面所有过滤逻辑都得从这里找。多入口架构意味着鉴权是分散的每个入口自己 include 一遍登录校验只要有一个文件忘了写就是一条不需要登录的通路。所以第一步是量化攻击面我习惯用两条命令起手# 按一级目录统计 php 文件数量快速看出代码重心在哪 find . -name *.php | awk -F/ {print $2} | sort | uniq -c | sort -rn # 找出所有直接读取外部参数的文件这些就是候选入口 grep -rl \$_\(GET\|POST\|REQUEST\|COOKIE\) --include*.php . | wc -l grep -rl \$_\(GET\|POST\|REQUEST\|COOKIE\) --include*.php . | head -40第一条命令的awk -F/ {print $2}是取路径第二段也就是一级目录名如果你在源码根目录执行输出会直接告诉你 admin 目录有多少文件、user 目录有多少文件。第二条统计的是“直接碰外部输入”的文件数量这个数字除以总文件数大概就是你需要重点看的比例。我手上这份大概三成的文件直接读参数不算多但足够排一个优先级队列。排完队列先看install目录。安装向导类文件是审计里性价比最高的目标它们通常要求“未安装”状态才可访问但如果源码被直接部署到生产环境而安装目录没删判断逻辑往往只看一个配置文件是否存在绕过成本极低。这类问题不属于某个具体漏洞而是部署习惯问题建站交付时最容易被忽略。2.2 公共包含与过滤函数把守门的那个人找出来老 CMS 的过滤通常不是中间件而是一个或几个自定义函数。找它们的办法很土但有效先列出所有函数定义再按名字里的关键词筛。# 列出全部自定义函数名重点看名字里带 check/filter/clean/safe/escape 的 grep -rhoP function\s\K\w --include*.php . | sort -u | grep -iE check|filter|clean|safe|escape|add # 找到定义位置看它到底做了什么 grep -rn function checkstr\|function clean\|function safe --include*.php .找到之后别只看函数体要看谁 include 了它。这是新手最容易漏的一环公共过滤函数写在inc/global.php里A 文件 include 了就安全B 文件自己造了个连接、直接查库就完全绕过了。我一般的做法是把 include 关系列成一张表标出哪些入口引了公共库、哪些没引没引的那批就是重点怀疑对象。读过滤函数本身要看三件事它是黑名单还是白名单它对数组类型怎么处理它在什么时机被调用。见过一种写法是对$_POST整体跑一遍过滤但过滤逻辑里用is_array()判断后直接continue于是提交id[]1这种数组参数就整段跳过检查。这类“看起来全局过滤了”的写法比完全不过滤更危险因为它给了人虚假的安全感。2.3 工具和手工的配比什么时候别信扫描器扫描器在这类老代码上的定位是“线索生成器”不是结论机器。它的价值在于快速覆盖一百多个文件的表层模式匹配缺点是误报率高得离谱只要看到$_GET出现在 SQL 语句附近就报注入不管那个变量是不是已经过了intval也不管那个文件是不是根本没被任何页面 include。我的配比大概是工具跑一遍拿清单然后人工用“可达性”三要素过滤判断维度要看什么不合格就划掉入口可达这个文件能否被匿名访问有没有前置的登录校验必须登录且无绕过路径参数可控参数是从 GET/POST/COOKIE 直取还是从数据库、配置读出来的完全不可控过滤缺失变量到危险函数中间有没有类型转换、白名单、预处理有intval或参数化三样同时满足才值得跟进。我见过太多人卡在第一步扫描器报了一个admin/xxx.php的注入兴奋半天结果那个文件开头就require了登录校验且没有任何前置文件能绕过白忙一场。反过来有些真正的洞扫描器根本不报因为参数经过了两层函数传递模式匹配断链了这种只能靠手工跟变量流。提示环境版本一定要和源码年代对齐。这套 8.3 源码是给 PHP 5.x 时代写的直接扔到 PHP 8 上会因为一堆废弃函数报错很多注入点根本走不到 SQL 执行那一步你会误以为是代码没问题。3. SQL 注入这条主线四种写法四种判断依据3.1 拼接查询的识别信号引号、intval 和 order by判断一个点是不是注入我不看它有没有“过滤”我看拼接的形态。下面这种是这套源码里最常见的模式?php // 典型的“看着过滤了”的写法只处理了单引号没管类型 $id $_GET[id]; $id str_replace(, , $id); // 去掉单引号 $sql select * from {$pre}news where id$id; // 变量没加引号包裹 $rs mysql_query($sql); // 老式 mysql_ 扩展无预处理 ?判断依据在于SQL 里变量没有引号包裹说明它被当成数字或标识符处理这时候去单引号毫无意义——数字型注入根本不需要引号。真正能挡住它的是intval($id)或(int)$id把变量强转成整数后面再怎么拼都翻不了车。第二类信号是order by、limit、group by后面的变量。这类位置数据库不允许用预处理占位符老代码只能拼接于是成了重灾区。识别方法是搜这几个关键字后面跟变量的写法# 找出排序、分页字段被变量拼接的位置 grep -rnE order by[^;]*\\\$|limit[^;]*\\\$ --include*.php .这类点即便做了黑名单过滤也常见绕过——黑名单里写了select|union|and|or但排序字段的注入根本不需要这些词用,(select 1 from ...)或者直接desc后面的表达式就能探。所以我的原则是排序字段一律按高危处理除非看到明确的白名单数组映射。3.2 后台参数、cookie 和搜索条件最容易被漏掉的三类注入点后台文件的审计优先级常被低估。很多人的思路是“后台要登录不算洞”但实际交付场景里后台弱口令、越权、以及后台低权限账号恰恰是最常见的入口。后台代码的过滤往往比前台更松因为开发者默认“能进后台的都是自己人”。我在admin目录里见过的写法包括列表页的搜索关键词直接拼进like %...%翻页参数直接拼进limit批量删除的 ID 数组用implode拼进in (...)这三处都不需要绕过什么复杂逻辑只要后台能进就能打。Cookie 里的注入更隐蔽。老 CMS 习惯把用户名、用户 ID 写进 cookie 做“记住登录”然后在每个页面用 cookie 里的值去查库?php // cookie 里的值被当作可信数据直接进查询 $username $_COOKIE[username]; $sql select * from {$pre}user where username$username; $rs mysql_query($sql); ?这里变量有引号包裹看起来安全但如果程序没有用addslashes或者数据库连接没设字符集宽字节构造依然能打穿。审计时我会专门检查数据库连接文件里有没有set names或mysql_set_charset没有的话所有带引号包裹的注入点都要重新评估一遍。第三类是搜索条件。搜索框的输入经常被允许带特殊字符因为产品经理要求“支持搜标题里的引号”结果就是过滤被产品需求逼着放宽。这类点的验证方式很直接搜一个带单引号的关键词看页面是报 SQL 错误还是正常返回空结果。3.3 从怀疑到复现本地打开 SQL 回显再验证发现可疑点之后不要在代码里猜改成“让它自己说话”。本地环境里最快的做法是把执行语句打印出来看变量最终长什么样?php // 仅用于本地调试把最终 SQL 打到页面或日志里 $sql select * from {$pre}news where id$id; error_log([AUDIT] . $sql); // 写入 php 错误日志不影响页面 $rs mysql_query($sql); if (!$rs) { error_log([AUDIT-ERR] . mysql_error()); // 顺带记录数据库报错 } ?把这几行加进去然后正常访问页面、正常构造参数日志里就能看到变量有没有被吃掉、引号有没有被转义、语句结构变成什么样。这比盯着源码推演快得多也是我判断误报最可靠的一招。验证阶段还有个细节先确认“语句结构可控”再谈利用。构造and 11和and 12看返回差异是最小成本的确认方式如果页面没有回显就在本地数据库上开慢查询日志用时间差确认。我一般在本地虚拟机上验证全程断网只对着自己导入的测试数据。有一点要强调任何验证都只在自己搭建的环境里做拿别人的站点练手是另一回事性质和后果完全不同。4. 文件这条主线上传、包含、读取怎么串成链4.1 上传点看三重校验的执行顺序上传点的审计关键不是“有没有校验”而是“校验的是什么”和“最终保存的是什么”是不是同一个东西。老代码常见的三重校验是扩展名黑名单、MIME 类型、getimagesize图片头检测。听起来挺全但翻车的点往往在执行顺序和变量覆盖上。?php // 常见结构先校验再改名最后移动 $allow array(jpg,gif,png); $ext strtolower(substr($_FILES[up][name], strrpos($_FILES[up][name],.)1)); if (!in_array($ext, $allow)) { exit(type error); } // 第一重扩展名白名单 if (!getimagesize($_FILES[up][tmp_name])) { exit(not image); } // 第二重图片头 $newname $_POST[filename] . . . $ext; // 关键文件名来自用户提交 move_uploaded_file($_FILES[up][tmp_name], uploadfile/ . $newname); ?这段的重点在倒数第二行扩展名来自上传文件的原始名已过白名单但文件主名来自另一个 POST 参数两者不同源。如果程序在别处对filename没有二次清洗攻击者就能构造出“白名单扩展名 可控主名”的组合落地路径不再受控。审计时我会把move_uploaded_file的第二个参数拆开看目录从哪来、文件名从哪来、有没有被basename()截断。只要文件名链路里有一个环节来自用户输入这个上传点就要标红。还有一个高频问题是上传目录的可执行性。很多建站程序把uploadfile放在 Web 根目录下并且目录里没有.htaccess或 Nginx 的location限制只要文件名能被控制扩展名通过某种方式变了就是一个直接可访问的可执行文件。判断这一点不看代码看部署配置。4.2 路径拼接里的 ../读取和删除共用一套毛病文件读取和删除的漏洞形态几乎一模一样都是“目录前缀 用户输入文件名”然后直接调file_get_contents、readfile、unlink。区分只在最后调的函数不同。?php // 读取前缀拼接没有 basename没有白名单 $f $_GET[f]; readfile(../uploadfile/ . $f); // f 传 ../../etc/passwd 之类的相对路径 // 删除同一套拼接危害更大 $f $_GET[f]; unlink(../uploadfile/ . $f); ?审计这类点时判断依据是三个“有没有”有没有basename()截掉路径分隔符有没有用realpath()之后做前缀比对有没有白名单目录限制。三个都没有就是任意读或任意删。basename()只是削弱不是根治因为有些系统在basename之后还会拼一次用户输入。文件删除在实战里的价值常被低估。删配置文件、删安装锁文件、删.htaccess任何一个都能把系统推到“可重装”或“防护失效”的状态危害不比读取低。我审计时会把所有unlink、rename、rmdir出现的位置单独列一张表逐个看参数来源。4.3 单点低危串成链条一个能落地的例子单个文件读取是低危单个上传白名单是低危单独看都不值得写报告。但把几条串起来结论就不一样了。这套源码里一个典型链路是文件读取点能读到数据库配置文件配置里有库名、用户名、口令和表前缀拿到前缀之后SQL 注入点的构造就从“猜表名”变成“精确打击”注入能读数据就有机会拿到后台账号后台里有上传点文件名可控上传目录可执行链路闭环。这个链条里每一环单独出现的概率都不低串起来才是完整的风险叙事。审计报告里我一般会把链条画成文字步骤每一步标注“入口文件 参数 所需前置条件”让读的人一眼看出哪一环最容易被打断——加固往往只需要在那一环上加个basename或者把上传目录挪出 Web 根目录成本极低。5. 避坑这五个地方我翻过车5.1 只跑工具不跟 include 链漏报一片现象扫描器报告几十条逐条看过去大半是误报翻完之后以为这系统挺干净结果手工翻inc目录十分钟就找到一个真注入。原因工具按文件独立分析看不到跨文件的数据流。参数在 A 文件被读取经过 B 文件的函数处理最后在 C 文件进 SQL这条链在单文件视角下是断的。反向也一样工具看到$_GET和 SQL 在同一文件就报完全不管中间有没有intval。解决把 include 关系先摸清按“入口文件 → 公共库 → 业务逻辑”的顺序跟变量而不是按文件名顺序翻。我现在的习惯是先在纸上画出包含树再决定从哪个叶子开始看效率完全不一样。5.2 环境版本不对注入点根本走不到现象代码里明明有拼接本地构造参数却始终返回正常页面日志里也看不到 SQL 报错。折腾两个小时以为是过滤生效了。原因源码是 PHP 5.x 时代写的跑在 PHP 7/8 上mysql_query这类函数已经不可用或者被替换成了兼容层执行到那一步直接 fatal页面被错误处理器吞掉看起来像“没反应”。另外 PHP 5.4 之后magic_quotes_gpc被移除老代码里依赖它做自动转义的逻辑全失效反而会多一些注入点。解决按源码年代选 PHP 版本数据库也尽量用同时代的版本。这一步看着麻烦但省下的调试时间远超搭建成本。搭好之后先跑一遍首页和后台确认功能正常再开始审计。5.3 以为全局过滤是万能的没测数组提交现象公共文件里明明有对整个$_POST的过滤循环按常理不该有注入实际却能打。原因过滤循环里对数组类型做了continue或者只处理了字符串分支。提交id[]1这种形式$_POST[id]是数组过滤逻辑直接跳过后续拼进 SQL 时数组被转成字符串Array或者在某些写法下被implode展开绕过就成立了。解决所有涉及外部参数的地方都要单独测一次数组提交尤其是那种“拿到参数直接进 SQL”的写法。审计时在过滤函数里专门找is_array分支看它是不是走了捷径。5.4 字符集没设带引号包裹的注入点被误判为安全现象变量有引号包裹代码里也调了addslashes看着密不透风结果实际能打。原因数据库连接建立后没有执行set names utf8或者mysql_set_charset连接字符集是默认的 GBKaddslashes加的反斜杠被当成宽字节的一部分吃掉引号逃逸。解决先看连接文件有没有设置字符集没有就把所有带引号包裹的点重新评估一遍。这类问题在老建站程序里非常普遍属于典型的“看着安全其实不安全”。5.5 只测前台不测后台和接口覆盖面缺一半现象前台翻得很仔细结论是“过滤还行”交付之后被人从后台打穿。原因后台代码的过滤强度普遍低于前台因为开发者假设能进后台的都是可信用户接口文件尤其是给移动端或小程序用的往往是后加的绕过了公共库的 include。这两块在“先测前台”的惯性下最容易漏。解决审计范围一开始就按目录权重分配前台、后台、接口、安装目录四个区都要过一遍。后台即使需要登录也要看有没有弱口令、默认账号、以及登录校验本身是否可以绕过。6. 收口把审计结论写成别人能复现的东西审计做到最后产出不是“我发现了几个洞”而是一份别人拿着就能复现的记录。我踩过的坑是早期报告只写结论不写路径过两周自己都复现不出来只能重来一遍。后来我固定用一张表收口每条风险四行写完字段内容要求反例入口文件路径 参数名 请求方法只写“搜索框”前置条件是否需要登录、需要什么权限、依赖哪个配置只写“无需登录”关键代码从参数读取到危险函数的最小片段贴整个文件验证步骤在本地环境的具体操作和数据变化只写“存在注入”这张表的用处不只是给别人看更是给自己做交叉验证。填“关键代码”那一栏时如果发现从参数到危险函数之间还有三层函数调用没跟说明还没审计完填“前置条件”时如果说不清需要什么权限说明鉴权逻辑没看懂。我现在的习惯是表填不满就不写结论宁可标成“待确认”。还有一个具体技巧值得单独说——把所有风险点按“修复成本”排序而不是按“危害等级”排序。危害高但修复成本也高的比如要重构整个查询层在交付场景里往往推不动危害中等但一行intval就能解决的反而应该排在报告最前面。这套 8.3 源码里我最后给出的清单前三条全是“加一行类型转换”或“把上传目录挪出 Web 根目录”这种改动对方当天就能改完上线剩下的大改项列在后面作为长期计划。这样报告才有人真正去执行。从那以后我每次做代码审计不管源码多小都强制走一遍“包含树 → 危险函数清单 → 可达性确认 → 报告四栏表”这个流程一步不跳。跳过哪一步后面就要用两倍时间补回来这是血泪经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表