sqlmap源码分析下
Author: recar
E-mail: 1766241489@qq.com
补充联合查询一个点
以及sqlmap –os-shell -d api参数等分析
补充联合查询一个点
上次因为提取了一个正则 但是那个网站把url返回到页面上了 所以匹配上误报了
于是要确定中间匹配的内容的正则 验证注入这里是随机的字母字符串 但是只是验证
获取数据还是根据返回的数据
匹配数据的 qxxxq(?P<result>.*?)qxxxq
中间是这样写的正则
下面是联合查询的一些详细的地方
先获取有几个字段 1' ORDER BY 10-- yPij
二分判断
然后会去判断 每个字段最大输出的长度是40 还是10
UNION_MIN_RESPONSE_CHARS 10
1 | for charCount in (UNION_MIN_RESPONSE_CHARS << 2, UNION_MIN_RESPONSE_CHARS): |
验证成功后获取数据
上次分析了注入点的验证确认 这里我们继续讲下 如何获取想要的数据
我们这里测试获取数据库名 使用参数 --dbs
断点会跟入这个与数据库相关的函数 action()
进入函数里面可以看到如下根据参数判断是获取指定数据
如 当前用户 当前数据库 是否dba等
1 | # Enumeration options |
因为我们指定了参数 --dbs
所以进入getDbs
这个函数
路径如下:plugins/generic/databases.py:81
1 | if conf.getDbs: |
然后根据数据库选择判断选择封装对象rootQuery = queries[Backend.getIdentifiedDbms()].dbs
可以看到是 获取之前判断的数据库类型然后再选定对应的数据库
针对获取不同的数据选择不同的sql
这些都是 sqlmap/xml/queries.xml
下的
因为是获取数据库
如下是选择使用哪个sql语句
1 | if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema: |
上面即是
如果有 information_schema 那么就是SELECT DISTINCT(db) FROM mysql.db LIMIT %d,1
否则SELECT DISTINCT(schema_name) FROM INFORMATION_SCHEMA.SCHEMATA LIMIT %d,1
Mysql 5 以上有内置库 information_schema
所以可以看到版本根据版本来判断
是这样判断是否是否版本5以上的
1 | if Backend.isVersionGreaterOrEqualThan("5"): |
先使用的联合查询 统计下字段名 但是没有返回信息
可以看下下面的log日志和手动url的页面返回内容
1 | [18:42:28] [PAYLOAD] -8751' UNION ALL SELECT NULL,NULL,CONCAT(0x717a717a71,IFNULL(CAST(COUNT(schema_name) AS CHAR),0x20),0x71786a6a71) FROM INFORMATION_SCHEMA.SCHEMATA-- gKIC |
union联合查询没有成功那么这里就走到了报错获取
1 | value = errorUse(forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) |
如果union可以 那么就是调用下面的方法
1 | value = _goUnion(forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) |
在利用报错读取数据之前,sqlmap需要searching for error chunk length,这length是干嘛用的呢?
简单的说,就是报错返回的内容不可能是很长很长的,肯定有长度限制
这里可以看到二分的判断长度
然后获取有多少个数据库
1' AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT(0x7178627871,(SELECT IFNULL(CAST(COUNT(schema_name) AS CHAR),0x20) FROM INFORMATION_SCHEMA.SCHEMATA),0x7162786a71,0x78))s), 8446744073709551610, 8446744073709551610))) AND 'Pgzy'='Pgzy
COUNT 函数统计
然后 获取数据库名
1' AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT(0x7178627871,(SELECT MID((IFNULL(CAST(schema_name AS CHAR),0x20)),1,451) FROM INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1),0x7162786a71,0x78))s), 8446744073709551610, 8446744073709551610))) AND 'FhJw'='FhJw
最后获取数据库
剩下的获取详细字段等都是类似的
简单流程图如下:
数据的获取就是如上
–os-shell
我们这里看下 获取shell的这块
主要是这两个命令 –os-cmd,–os-shell
执行命令输入如下
先选择web语言
选择是否尝试获取完整目录
具体函数 (plugins/generic/takeover.py:67
)
1 | def osShell(self): |
initENV
lib/takeover/abstraction.py:178
1 | def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False): |
主要是 webInit
webInit
1 | def webInit(self): |
_webFileInject 上传小马文件
调用方法上传文件暂存器即小马
上传完后请求页面
1 | def _webFileInject(self, fileContent, fileName, directory): |
加载大小马 写入 最后获取 获取shell
他开始加载了两个文件 小马和大马
会先上传小马
最开始先尝试 limit的方式语句写入文件
LINES TERMINATED BY和LINES STARTING BY原理为在输出每条记录的结尾或开始处插入webshell内容,所以即使只查询一个字段也可以写入webshell内容
然后再上传 大马
最后获取交互shell
对于mysql导出文件
这里因为需要搞这个环境有如下要求
- 高版本的MYSQL添加了一个新的特性secure_file_priv,该选项限制了mysql导出文件的权限
需要设置为空字符串 不能是Null 不能是指定路径 - 还有需要对 /etc/apparmor.d 修改配置文件 对usr.sbin.mysqld 增加导出指定目录文件的权限
- 文件不能覆盖写入,所以文件必须为不存在 mysql 执行 into outfile 存在的话会报错
- 还要给mysql能写到指定目录的权限
- 这里如果是docker的话 需要 增加参数 –privileged 给予最高的root权限并且修改 apparmor对应的配置才可以
linux安全防御需要修改文件 usr.sbin.mysqld
修改后重启
1 | test |
-d 直连数据库提权
如果直接连接数据库的话是使用的udf提权:
mysql udf提权
UDF(user defined function)用户自定义函数,是mysql的一个拓展接口。用户可以通过自定义函数实现在mysql中无法方便实现的功能,其添加的新函数都可以在sql语句中调用,就像调用本机函数一样。
sqlmap的这些文件是 异或编码的 而且是有解码脚本的
sqlmap/extra/cloak目录下的cloak.py
知道了数据库的用户名和密码
python sqlmap.py -d "mysql://root:root@192.168.19.128:3306/mysql" --os-shell
会先测试mysql 并且会再次确认
确认后并判断出mysql的版本后 这里根据命令行的不同参数选择不同的函数
我们这里选择的是 交互shell 所以会 进入 conf.dbmsHandler.osShell()
函数
os与dbs判断
选择64位还是32位
这块代码的路径 sqlmap-1.3/plugins/generic/takeover.py(81)osShell()
上传 udf的代码
sqlmap-1.3/lib/takeover/udf.py(180)udfInjectSys()
1 |
|
检测 sys_eval
sys_exec
是否存在选择是否添加
写文件
1 | if len(self.udfToCreate) > 0: # 如果是有需要添加的 |
可单独异或解码
mysql udf 提权文件
解码后到本地临时目录 然后再写到mysql插件目录
下图是测试上传的文件
需要的条件
需要有这个目录的 /usr/lib/mysql/plugin/写入权限
而且如果需要 udf提权后能执行成功 是需要先执行下面的操作要关闭对mysql的安全限制
否则怎么执行都会返回 NULL
1 | sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/ |
这个真的花了好久才研究出来(网上的文章几乎都是直接就能执行 可能是 mysql版本低的原因 我的是 5.7)
最后交互shell执行命令
sqlmap api
sqlapi 运行截图
sqlmapapi.py 是有服务端和客户端的 分别是-s参数和-c参数
server
1 | def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None): |
run
会控制不允许开启多个服务端
使用 bottle
来创建web服务 只要一个简单的文件就可以直接实现web服务
比如说 获取当前的任务的信息
@get("/option/<taskid>/list")
从 DataStore.tasks
获取并输出返回json
他的这些数据存储到 内存中 日志存储到sqlite3中
api形式如何执行测试
每个任务最终的调用当时 是开个进程去执行 ["python", "sqlmap.py", "--api", "-c", configFile]
接口请求控制去开启这个任务
使用这个接口来操作
使用sqlmapapi -c 客户端模式进行测试
这里使用 pythonapi的客户端模块来创建任务
- 创建一个任务
new -u "http://192.168.19.128:8081/Less-1/?id=1"
- 查看扫描状态
- 获取注入结果
- 就是说类似 AWVS的那种有web api的形式可以直接调用任务这样使用
sqlmap api的利用
这里本来打算做个扫描平台那种 sqlmap只作为一种检测引擎
这里可以 通过对url去重 过滤等操作后 下发到节点进行操作
可以每个机器上都有个sqlmap 然后每次下发任务celery 直接请求本机端口 再次判断这个任务这样就可以了