借力 SqlMap,实现 API 层 SQL 注入测试自动化
Hello, comrades!
因每次安全测试时,使用SqlMap都要基于命令行执行命令运行,感觉非常的不便利。在查看官方文档时,看到官方有提供简单的API。由此,就基于官方的API封装方法调用,再基于Fastapi框架实现接口调用进行安全测试中Sql注入测试。
SqlMap API (REST-JSON)
- 首先是把服务运行起来。
$ python sqlmapapi.py -s -H "0.0.0.0"
[12:47:51] [INFO] Running REST-JSON API server at '0.0.0.0:8775'..
[12:47:51] [INFO] Admin ID: 89fd118997840a9bd7fc329ab535b881
[12:47:51] [DEBUG] IPC database: /tmp/sqlmapipc-SzBQnd
[12:47:51] [DEBUG] REST-JSON API server connected to IPC database
[12:47:51] [DEBUG] Using adapter 'wsgiref' to run bottle
[12:48:10] [DEBUG] Created new task: 'a42ddaef02e976f0'
[12:48:10] [DEBUG] [a42ddaef02e976f0] Started scan
[12:48:16] [DEBUG] [a42ddaef02e976f0] Retrieved scan status
[12:48:50] [DEBUG] [a42ddaef02e976f0] Retrieved scan status
[12:48:55] [DEBUG] [a42ddaef02e976f0] Retrieved scan log messages
[12:48:59] [DEBUG] [a42ddaef02e976f0] Retrieved scan data and error messages
- 最后是调用示例。
$ python sqlmapapi.py -c -H "192.168.110.1"
[12:47:53] [DEBUG] Example client access from command line:
$ taskid=$(curl http://192.168.110.1:8775/task/new 2>1 | grep -o -I '[a-f0-9
]\{16\}') && echo $taskid
$ curl -H "Content-Type: application/json" -X POST -d '{"url": "http://testp
hp.vulnweb.com/artists.php?artist=1"}' http://192.168.110.1:8775/scan/$taskid/st
art
$ curl http://192.168.110.1:8775/scan/$taskid/data
$ curl http://192.168.110.1:8775/scan/$taskid/log
[12:47:53] [INFO] Starting REST-JSON API client to 'http://192.168.110.1:8775'..
.
[12:47:53] [DEBUG] Calling http://192.168.110.1:8775
[12:47:53] [INFO] Type 'help' or '?' for list of available commands
api> ?
help Show this help message
new ARGS Start a new scan task with provided arguments (e.g. 'new -u "http://
testphp.vulnweb.com/artists.php?artist=1"')
use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4
')
data Retrieve and show data for current task
log Retrieve and show log for current task
status Retrieve and show status for current task
stop Stop current task
kill Kill current task
list Display all tasks
flush Flush tasks (delete all tasks)
exit Exit this client
api> new -u "http://testphp.vulnweb.com/artists.php?artist=1" --banner --flush-s
ession
[12:48:10] [DEBUG] Calling http://192.168.110.1:8775/task/new
[12:48:10] [INFO] New task ID is 'a42ddaef02e976f0'
[12:48:10] [DEBUG] Calling http://192.168.110.1:8775/scan/a42ddaef02e976f0/start
[12:48:10] [INFO] Scanning started
api (a42ddaef02e976f0)> status
[12:48:16] [DEBUG] Calling http://192.168.110.1:8775/scan/a42ddaef02e976f0/statu
s
{
"status": "running",
"returncode": null,
"success": true
}
api (a42ddaef02e976f0)> status
[12:48:50] [DEBUG] Calling http://192.168.110.1:8775/scan/a42ddaef02e976f0/statu
s
{
"status": "terminated",
"returncode": 0,
"success": true
}
Servers
基于上述官方文档的示例,我们在服务器上首先启动一个SqlMap服务,用于SqlMap API运行。
dfws@dfws:~/sqlmapproject-sqlmap$ ls
data doc Dockerfile extra lib LICENSE plugins README.md sqlmapapi.py sqlmapapi.yaml sqlmap.conf sqlmap.py tamper thirdparty
dfws@dfws:~/sqlmapproject-sqlmap$ cat Dockerfile
# 使用官方 Python 镜像作为基础镜像
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 复制 sqlmap 文件到容器中
COPY . /app
# 安装依赖
RUN apt-get update && apt-get install -y git && \
pip install --no-cache-dir requests
# 暴露端口
EXPOSE 8775
# 启动 sqlmapapi 服务
CMD ["python", "sqlmapapi.py", "-s", "-H", "0.0.0.0", "-p", "8775"]
这里是把SqlMap源码pull到服务器,z自己手动加了一个Dockerfile文件,先打成镜像后,再基于镜像启动一个容器运行SqlMap服务。
84ffd2a0c8cd sqlmap-service "python sqlmapapi.py…" 3 months ago Up 3 months 0.0.0.0:8775->8775/tcp, :::8775->8775/tcp
Development
服务启动好以后,就开始进入开发环节,首先把SqlMap API进行封装处理。
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@作者: debugfeng
@文件: handel.py
@时间: 2025/04/07 09:05:35
@说明:
"""
__all__ = ["format_params", "is_scan_finished"]
def format_params(params: dict, separator: str) -> str:
"""格式化字典为参数字符串"""
return separator.join([f"{k}={v}" for k, v in params.items()])
def is_scan_finished(scan_results: list) -> bool:
for item in scan_results:
if item.get("type") == 1 and item.get("value"):
return True
return False
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@作者: debugfeng
@文件: sqlmap_api_rest.py
@时间: 2025/04/03 10:41:22
@说明:
"""
import logging
import requests
from urllib.parse import urlencode
from app.config.settings import SettingsParamster as sp
from app.schemas.scan_request import SqlmapRequest
from app.core.sqlmapTasks.core.handel import format_params
__all__ = ["SqlmapApiRest"]
class SqlmapApiRest:
logging.basicConfig(level=logging.INFO)
def task_new(self):
"""
创建任务
:raises Exception: _description_
:return: _description_
:rtype: _type_
"""
response = requests.get(url=f"{sp.SQLMAP_SERVER_URL}/task/new")
if response.status_code == 200:
response = response.json()
logging.info(f"创建任务: {response}")
if response.get("success") == False:
result = {
"message": "任务创建失败",
"data": response,
} # 任务创建失败
elif response.get("message") == "Invalid task ID":
result = {
"message": "任务创建失败",
"data": response,
} # 任务创建失败
else:
result = response.get("taskid")
return result
else:
raise {"message": "任务创建失败", "data": response}
def option_list(self, taskid):
"""
获取 sqlmap 支持的选项
:param taskid: 任务ID
:type taskid: _type_
:raises Exception: _description_
:return: _description_
:rtype: _type_
"""
response = requests.get(f"{sp.SQLMAP_SERVER_URL}/option/{taskid}/list")
if response.status_code == 200:
response = response.json()
logging.info(f"获取支持的选项: {response}")
return response
else:
raise Exception(f"获取选项失败: {response.text}")
def scan_start(self, taskid, data: SqlmapRequest):
"""
启动 sqlmap 扫描任务
:param taskid: 任务ID
:type taskid: _type_
:param data: 请求参数
:type data: dict
:raises Exception: _description_
"""
def deep_clean_params(params: dict) -> dict:
"""
清理字典中值为 None 的键值对。
"""
return {key: value for key, value in params.items() if value is not None}
logging.info(f"请求参数: {data}")
# 将字典转为 query string
if data.get("data"):
data["data"] = urlencode(data["data"])
# 处理 `cookie` 字段,拼接成 "key1=value1; key2=value2"
if data.get("cookie"):
data["cookie"] = format_params(data["cookie"], separator="; ")
# 清理参数
clear_nested_data = deep_clean_params(data)
response = requests.post(
f"{sp.SQLMAP_SERVER_URL}/scan/{taskid}/start", json=clear_nested_data
)
logging.info(f"开始扫描: {response.json()}")
if response.status_code != 200:
return {"task_id": taskid, "message": "扫描失败"}
def scan_log(self, taskid):
"""
获取扫描日志
:param taskid: 任务ID
:type taskid: _type_
:raises Exception: _description_
:return: _description_
:rtype: _type_
"""
response = requests.get(f"{sp.SQLMAP_SERVER_URL}/scan/{taskid}/log")
if response.status_code == 200:
return {"task_id": taskid, "data": response.json()}
else:
return {"task_id": taskid, "message": "获取日志失败"}
def scan_status(self, taskid):
"""
获取扫描状态
:param taskid: 任务ID
:type taskid: _type_
:raises Exception: _description_
:return: _description_
:rtype: _type_
"""
response = requests.get(f"{sp.SQLMAP_SERVER_URL}/scan/{taskid}/status")
logging.info(f"扫描状态: {response.json()}")
if response.status_code == 200:
return {"task_id": taskid, "data": response.json()}
else:
return {"task_id": taskid, "message": "获取状态失败"}
def scan_data(self, taskid):
"""
获取扫描结果
:param taskid: 任务ID
:type taskid: _type_
:raises Exception: _description_
:return: _description_
:rtype: _type_
"""
response = requests.get(f"{sp.SQLMAP_SERVER_URL}/scan/{taskid}/data")
logging.info(f"获取结果: {response.json()}")
if response.status_code == 200:
return {
"task_id": taskid,
"data": response.json()
}
else:
return {"task_id": taskid, "message": "获取结果失败"}
def task_delete(self, taskid):
"""
删除任务
:param taskid: 任务ID
:type taskid: str
:raises Exception: 删除失败时抛出异常
"""
url = f"{sp.SQLMAP_SERVER_URL}/task/{taskid}/delete"
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # 抛出 HTTPError 异常(如 404, 500)
response_data = response.json()
if not response_data.get("success", False):
raise Exception(f"删除任务失败,响应: {response_data}")
return {"task_id": taskid, "message": "任务删除成功"}
except Exception as e:
raise Exception(f"删除任务失败: {e}")
if __name__ == "__main__":
options = {
"url": "https://www.9first.com",
"data": {"username": "admin", "password": "123"}, # 必须是字符串
"method": "POST",
"cookie": {"PHPSESSID": "abcd1234"},
"headers": "User-Agent: Mozilla/5.0\nReferer: http://example.com",
"level": 3,
"risk": 2,
"timeout": 10,
}
# 创建任务
task = SqlmapApiRest()
taskid = task.task_new()
task.option_list(taskid)
task.scan_start(taskid, data=options)
print(task.scan_status(taskid))
print(task.scan_log(taskid))
task.scan_data(taskid)
task.task_delete(taskid)
基于上述的封装,初始化后再次封装一个扫描时的监控,扫描完成后则返回数据,报错后则进行异常捕获处理。
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@作者: debugfeng
@文件: sqlmap_task.py
@时间: 2025/04/03 10:41:22
@说明: SQLMap任务管理模块
"""
import asyncio
import time
import logging
from typing import Any, Dict
from app.schemas.scan_request import SqlmapRequest
from app.core.sqlmapTasks.core.sqlmap_api_rest import SqlmapApiRest
__all__ = ["SqlmapTask"]
class SqlmapTask:
def __init__(self):
self.sqlmap_rest = SqlmapApiRest()
async def create_scan_start(self, data: SqlmapRequest) -> str:
"""
创建并启动扫描任务
"""
try:
task_id = self.sqlmap_rest.task_new()
self.sqlmap_rest.scan_start(task_id, data)
logging.info(f"成功创建并启动任务: {task_id}")
return task_id
except Exception as e:
logging.error(f"新建任务出现错误: {str(e)}")
raise RuntimeError(f"新建任务失败: {str(e)}") from e
async def check_scan_status(self, task_id: str) -> Dict[str, Any]:
"""
后台异步监控扫描任务状态
"""
check_interval = 5 # 检测间隔
timeout_seconds = 3 * 60 * 60 # 最长允许 3小时(10800秒)
async def monitor():
logging.info(f"开始监控任务 {task_id} 的扫描状态")
while True:
try:
scan_status = self.sqlmap_rest.scan_status(task_id)
scan_logs = self.sqlmap_rest.scan_log(task_id)
logging.info(f"1111任务 {task_id} 当前状态: {scan_status}")
current_status = scan_status.get("data")
scan_logs_data = scan_logs.get("data")
logging.info(f"任务 {task_id} 当前状态: {current_status}")
if current_status.get("status") == "terminated":
if (
not scan_logs_data.get("success")
or scan_logs_data.get("message") == "Invalid task ID"
):
logging.error(f"任务 {task_id} 扫描日志异常: {scan_logs}")
return {
"task_id": task_id,
"status": "error",
"message": "任务异常或ID无效",
}
logging.info(f"任务 {task_id} 扫描完成")
return {
"task_id": task_id,
"status": "terminated",
"message": "任务扫描完成",
}
await asyncio.sleep(check_interval)
except Exception as e:
logging.error(f"监控任务 {task_id} 时出现异常: {str(e)}")
return {
"task_id": task_id,
"status": "error",
"message": f"监控异常: {str(e)}",
}
try:
# 这里加超时保护,超时后会抛 asyncio.TimeoutError
result = await asyncio.wait_for(monitor(), timeout=timeout_seconds)
return result
except asyncio.TimeoutError:
logging.warning(f"任务 {task_id} 超时未完成,自动结束监控")
return {
"task_id": task_id,
"status": "timeout",
"message": "任务监控超时(3小时未结束)",
}
async def scan_get_results(self, task_id: str) -> Dict[str, Any]:
"""
获取扫描结果
"""
try:
scan_status = self.sqlmap_rest.scan_status(task_id)
scan_status_data = scan_status.get("data")
if scan_status_data.get("status") != "terminated":
status = (
scan_status_data.get("status")
if scan_status_data.get("status")
else "runnning"
)
return {
"status": status,
"data": {"task_id": task_id},
"message": "任务尚未完成.",
}
result_data = self.sqlmap_rest.scan_data(task_id)
if result_data:
return result_data
else:
return {
"status": "finished",
"data": {"task_id": task_id},
"message": "扫描已完成,未发现漏洞.",
}
except Exception as e:
logging.error(f"查询任务 {task_id} 结果失败: {str(e)}")
return {"status": "error", "message": str(e)}
async def scan_delete(self, task_id: str) -> Dict[str, Any]:
"""
删除扫描任务
"""
try:
self.sqlmap_rest.task_delete(task_id)
logging.info(f"成功删除任务 {task_id}")
return {"task_id": task_id, "message": "任务删除成功"}
except Exception as e:
logging.error(f"删除任务 {task_id} 出错: {str(e)}")
return {"task_id": task_id, "status": "error", "message": str(e)}
封装完成后,则开始进行接口路由的开发工作中,这里我们使用了Fastapi框架进行开发。
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@作者: debugfeng
@文件: sqlmap.py
@时间: 2025/04/15 11:07:19
@说明:
"""
import logging
from fastapi import APIRouter, Body, Path, HTTPException, BackgroundTasks, Query, status
from app.schemas.scan_request import SqlmapRequest
from app.core.sqlmapTasks.sqlmap_task import SqlmapTask
from app.schemas.scan_request import SqlmapRequest
sqlmap_router = APIRouter(prefix="/sqlmap", tags=["用于sql注入接口"])
sqlmap_task = SqlmapTask() # 实例化一次,避免每个接口重复 new
@sqlmap_router.post(
"/scan",
summary="启动 SQLMap 扫描任务",
description="启动sqlmap扫描,扫描启动后返回任务ID,用户可以使用此ID查询扫描进度和结果",
)
async def api_start_scan(
background_tasks: BackgroundTasks,
scan_request: SqlmapRequest = Body(
...,
example={
"url": "被扫描的url",
"method": "请求方式",
"data": "请求参数, 默认值为None",
"headers": "请求头, 默认值为{'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': '随机的User-Agent'}",
"cookie": "cookie, 默认值为None",
"token": "token, 默认值为None, 传入token后会自动写入headers中",
"p": "注入参数名,默认值为 None",
"level": "注入等级,默认值为 1 (1-5)",
"risk": "风险等级,默认值为 1 (1-3)",
"verbose": "日志详细等级, 默认值为 1 (0-6)",
"timeout": "请求超时时间, 默认值为 120 秒",
"tamper": "混淆脚本, 默认值为 'space2comment.py,between.py,charunicodeencode.py,randomcase.py'",
},
),
):
"""
启动 SQLMap 扫描任务
:param scan_request: 扫描请求参数
:type scan_request: SqlmapRequest
:param background_tasks: 后台任务管理器
:type background_tasks: BackgroundTasks
:raises HTTPException: _description_
:return: _description_
:rtype: _type_
"""
try:
data = scan_request.model_dump()
task_id = await sqlmap_task.create_scan_start(data)
# 异步启动扫描任务
background_tasks.add_task(sqlmap_task.check_scan_status, task_id)
return {"task_id": task_id, "message": "扫描任务已启动"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@sqlmap_router.get(
"/data",
summary="获取 SQLMap 扫描结果",
description="根据任务ID查询扫描完成的结果, 可根据结果信息判断是否有注入漏洞",
)
async def api_scan_results(
task_id: str = Query(..., min_length=1, examples={"task_id": "任务ID"})
):
"""
获取 SQLMap 扫描结果
:param task_id: 任务ID
:type task_id: str
:raises HTTPException: _description_
:return: _description_
:rtype: _type_
"""
try:
if not task_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="任务ID不能为空"
)
# 调用异步获取结果的函数
scan_results = await sqlmap_task.scan_get_results(task_id)
return scan_results
except Exception as e:
logging.error(f"获取扫描结果请求任务 {task_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@sqlmap_router.delete(
"/task/{task_id}",
summary="删除 SQLMap 任务",
description="根据任务ID删除 SQLMap 任务, 删除后任务将不再存在, 扫描结果将无法获取",
)
async def api_delete_task(
task_id: str = Path(..., min_length=1, examples={"task_id": "任务ID"})
):
"""
删除 SQLMap 任务
:param task_id: 任务ID
:type task_id: str
:raises HTTPException: _description_
:return: _description_
:rtype: _type_
"""
try:
if not task_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="任务ID不能为空"
)
results = await sqlmap_task.scan_delete(task_id)
return results
except Exception as e:
logging.error(f"任务 {task_id} 删除失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
接口路由开发完成后,再写一个运行入口文件,用于启动接口服务。
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@作者: debugfeng
@文件: main.py
@时间: 2025/04/03 10:42:29
@说明:
"""
import uvicorn
from fastapi import FastAPI
from app.routes import sqlmap, xsstrike
app = FastAPI(
title="安全测试API文档",
description="用于扫描xss和sql注入的接口文档",
version="0.0.1",
)
app.include_router(sqlmap.sqlmap_router)
app.include_router(xsstrike.xss_router)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8081)
自此,我们的安全测试扫描接口就开发完成了,目前已经用于实际工作中。希望对你们有所帮助,感谢!
一直在努力,希望你也是!如果你觉得还不错,欢迎点赞+关注!
文章作者:李锋 || 微信号:LIFENG00078 || 公众号:全栈测试工程师

浙公网安备 33010602011771号