This commit is contained in:
Miroslav Stampar 2019-06-27 17:28:43 +02:00
parent c938d77be9
commit aa9b5e4e0c
20 changed files with 1790 additions and 34 deletions

1641
data/txt/common-files.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ from lib.core.exception import SqlmapNoneDataException
from lib.core.exception import SqlmapUnsupportedDBMSException from lib.core.exception import SqlmapUnsupportedDBMSException
from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import SUPPORTED_DBMS
from lib.utils.brute import columnExists from lib.utils.brute import columnExists
from lib.utils.brute import fileExists
from lib.utils.brute import tableExists from lib.utils.brute import tableExists
def action(): def action():
@ -199,6 +200,14 @@ def action():
if conf.fileWrite: if conf.fileWrite:
conf.dbmsHandler.writeFile(conf.fileWrite, conf.fileDest, conf.fileWriteType) conf.dbmsHandler.writeFile(conf.fileWrite, conf.fileDest, conf.fileWriteType)
if conf.commonFiles:
try:
conf.dumper.rFile(fileExists(paths.COMMON_FILES))
except SqlmapNoneDataException as ex:
logger.critical(ex)
except:
raise
# Operating system options # Operating system options
if conf.osCmd: if conf.osCmd:
conf.dbmsHandler.osCmd() conf.dbmsHandler.osCmd()

View File

@ -1346,6 +1346,7 @@ def setPaths(rootPath):
# sqlmap files # sqlmap files
paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt") paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt")
paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt")
paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt") paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt")
paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt') paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt')
paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt") paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt")
@ -4637,6 +4638,8 @@ def decodeDbmsHexValue(value, raw=False):
def _(value): def _(value):
retVal = value retVal = value
if value and isinstance(value, six.string_types): if value and isinstance(value, six.string_types):
value = value.strip()
if len(value) % 2 != 0: if len(value) % 2 != 0:
retVal = (decodeHex(value[:-1]) + b'?') if len(value) > 1 else value retVal = (decodeHex(value[:-1]) + b'?') if len(value) > 1 else value
singleTimeWarnMessage("there was a problem decoding value '%s' from expected hexadecimal form" % value) singleTimeWarnMessage("there was a problem decoding value '%s' from expected hexadecimal form" % value)

View File

@ -160,6 +160,7 @@ optDict = {
"Brute": { "Brute": {
"commonTables": "boolean", "commonTables": "boolean",
"commonColumns": "boolean", "commonColumns": "boolean",
"commonFiles": "boolean",
}, },
"User-defined function": { "User-defined function": {

View File

@ -18,7 +18,7 @@ from lib.core.enums import OS
from thirdparty.six import unichr as _unichr from thirdparty.six import unichr as _unichr
# sqlmap version (<major>.<minor>.<month>.<monthly commit>) # sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.3.6.56" VERSION = "1.3.6.57"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

View File

@ -586,7 +586,7 @@ def _createFilesDir():
Create the file directory. Create the file directory.
""" """
if not conf.fileRead: if not any((conf.fileRead, conf.commonFiles)):
return return
conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname

View File

@ -204,7 +204,6 @@ def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardExceptio
traceback.print_exc() traceback.print_exc()
finally: finally:
kb.bruteMode = False
kb.threadContinue = True kb.threadContinue = True
kb.threadException = False kb.threadException = False

View File

@ -502,6 +502,9 @@ def cmdLineParser(argv=None):
brute.add_argument("--common-columns", dest="commonColumns", action="store_true", brute.add_argument("--common-columns", dest="commonColumns", action="store_true",
help="Check existence of common columns") help="Check existence of common columns")
brute.add_argument("--common-files", dest="commonFiles", action="store_true",
help="Check existence of common files")
# User-defined function options # User-defined function options
udf = parser.add_argument_group("User-defined function injection", "These options can be used to create custom user-defined functions") udf = parser.add_argument_group("User-defined function injection", "These options can be used to create custom user-defined functions")

View File

@ -109,7 +109,7 @@ class UDF(object):
return output return output
def udfCheckNeeded(self): def udfCheckNeeded(self):
if (not conf.fileRead or (conf.fileRead and not Backend.isDbms(DBMS.PGSQL))) and "sys_fileread" in self.sysUdfs: if (not any((conf.fileRead, conf.commonFiles)) or (any((conf.fileRead, conf.commonFiles)) and not Backend.isDbms(DBMS.PGSQL))) and "sys_fileread" in self.sysUdfs:
self.sysUdfs.pop("sys_fileread") self.sysUdfs.pop("sys_fileread")
if not conf.osPwn: if not conf.osPwn:

View File

@ -43,6 +43,7 @@ from lib.core.dicts import FROM_DUMMY_TABLE
from lib.core.enums import DBMS from lib.core.enums import DBMS
from lib.core.enums import HASHDB_KEYS from lib.core.enums import HASHDB_KEYS
from lib.core.enums import HTTP_HEADER from lib.core.enums import HTTP_HEADER
from lib.core.enums import PAYLOAD
from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapDataException
from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD
from lib.core.settings import MAX_ERROR_CHUNK_LENGTH from lib.core.settings import MAX_ERROR_CHUNK_LENGTH
@ -123,7 +124,7 @@ def _oneShotErrorUse(expression, field=None, chunkTest=False):
nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, kb.errorChunkLength) nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, kb.errorChunkLength)
# Forge the error-based SQL injection request # Forge the error-based SQL injection request
vector = kb.injection.data[kb.technique].vector vector = kb.injection.data[PAYLOAD.TECHNIQUE.ERROR].vector
query = agent.prefixQuery(vector) query = agent.prefixQuery(vector)
query = agent.suffixQuery(query) query = agent.suffixQuery(query)
injExpression = expression.replace(field, nulledCastedField, 1) if field else expression injExpression = expression.replace(field, nulledCastedField, 1) if field else expression
@ -134,7 +135,7 @@ def _oneShotErrorUse(expression, field=None, chunkTest=False):
# Perform the request # Perform the request
page, headers, _ = Request.queryPage(payload, content=True, raise404=False) page, headers, _ = Request.queryPage(payload, content=True, raise404=False)
incrementCounter(kb.technique) incrementCounter(PAYLOAD.TECHNIQUE.ERROR)
if page and conf.noEscape: if page and conf.noEscape:
page = re.sub(r"('|\%%27)%s('|\%%27).*?('|\%%27)%s('|\%%27)" % (kb.chars.start, kb.chars.stop), "", page) page = re.sub(r"('|\%%27)%s('|\%%27).*?('|\%%27)%s('|\%%27)" % (kb.chars.start, kb.chars.stop), "", page)
@ -247,7 +248,7 @@ def _errorFields(expression, expressionFields, expressionFieldsList, num=None, e
if not kb.threadContinue: if not kb.threadContinue:
return None return None
if not suppressOutput: if not any((suppressOutput, kb.bruteMode)):
if kb.fileReadMode and output and output.strip(): if kb.fileReadMode and output and output.strip():
print() print()
elif output is not None and not (threadData.resumed and kb.suppressResumeInfo) and not (emptyFields and field in emptyFields): elif output is not None and not (threadData.resumed and kb.suppressResumeInfo) and not (emptyFields and field in emptyFields):
@ -298,7 +299,7 @@ def errorUse(expression, dump=False):
SQL injection vulnerability on the affected parameter. SQL injection vulnerability on the affected parameter.
""" """
initTechnique(kb.technique) initTechnique(PAYLOAD.TECHNIQUE.ERROR)
abortedFlag = False abortedFlag = False
count = None count = None
@ -460,7 +461,7 @@ def errorUse(expression, dump=False):
duration = calculateDeltaSeconds(start) duration = calculateDeltaSeconds(start)
if not kb.bruteMode: if not kb.bruteMode:
debugMsg = "performed %d queries in %.2f seconds" % (kb.counters[kb.technique], duration) debugMsg = "performed %d queries in %.2f seconds" % (kb.counters[PAYLOAD.TECHNIQUE.ERROR], duration)
logger.debug(debugMsg) logger.debug(debugMsg)
return value return value

View File

@ -312,6 +312,7 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
return validPayload, vector return validPayload, vector
@stackedmethod
def unionTest(comment, place, parameter, value, prefix, suffix): def unionTest(comment, place, parameter, value, prefix, suffix):
""" """
This method tests if the target URL is affected by an union This method tests if the target URL is affected by an union

View File

@ -376,7 +376,7 @@ def unionUse(expression, unpack=True, dump=False):
threadData.shared.value.extend(arrayizeValue(_)) threadData.shared.value.extend(arrayizeValue(_))
del threadData.shared.buffered[0] del threadData.shared.buffered[0]
if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta: if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta and not kb.bruteMode:
_ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items])) _ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items]))
status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_)) status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_))

View File

@ -7,6 +7,7 @@ See the file 'LICENSE' for copying permission
from __future__ import division from __future__ import division
import logging
import time import time
from lib.core.common import Backend from lib.core.common import Backend
@ -16,20 +17,26 @@ from lib.core.common import filterListValue
from lib.core.common import getFileItems from lib.core.common import getFileItems
from lib.core.common import getPageWordSet from lib.core.common import getPageWordSet
from lib.core.common import hashDBWrite from lib.core.common import hashDBWrite
from lib.core.common import isNoneValue
from lib.core.common import popValue
from lib.core.common import pushValue
from lib.core.common import randomInt from lib.core.common import randomInt
from lib.core.common import randomStr from lib.core.common import randomStr
from lib.core.common import readInput from lib.core.common import readInput
from lib.core.common import safeSQLIdentificatorNaming from lib.core.common import safeSQLIdentificatorNaming
from lib.core.common import safeStringFormat from lib.core.common import safeStringFormat
from lib.core.common import unArrayizeValue
from lib.core.common import unsafeSQLIdentificatorNaming from lib.core.common import unsafeSQLIdentificatorNaming
from lib.core.data import conf from lib.core.data import conf
from lib.core.data import kb from lib.core.data import kb
from lib.core.data import logger from lib.core.data import logger
from lib.core.decorators import stackedmethod
from lib.core.enums import DBMS from lib.core.enums import DBMS
from lib.core.enums import HASHDB_KEYS from lib.core.enums import HASHDB_KEYS
from lib.core.enums import PAYLOAD from lib.core.enums import PAYLOAD
from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapDataException
from lib.core.exception import SqlmapMissingMandatoryOptionException from lib.core.exception import SqlmapMissingMandatoryOptionException
from lib.core.exception import SqlmapNoneDataException
from lib.core.settings import BRUTE_COLUMN_EXISTS_TEMPLATE from lib.core.settings import BRUTE_COLUMN_EXISTS_TEMPLATE
from lib.core.settings import BRUTE_TABLE_EXISTS_TEMPLATE from lib.core.settings import BRUTE_TABLE_EXISTS_TEMPLATE
from lib.core.settings import METADB_SUFFIX from lib.core.settings import METADB_SUFFIX
@ -136,7 +143,6 @@ def tableExists(tableFile, regex=None):
try: try:
runThreads(conf.threads, tableExistsThread, threadChoice=True) runThreads(conf.threads, tableExistsThread, threadChoice=True)
except KeyboardInterrupt: except KeyboardInterrupt:
warnMsg = "user aborted during table existence " warnMsg = "user aborted during table existence "
warnMsg += "check. sqlmap will display partial output" warnMsg += "check. sqlmap will display partial output"
@ -252,11 +258,12 @@ def columnExists(columnFile, regex=None):
try: try:
runThreads(conf.threads, columnExistsThread, threadChoice=True) runThreads(conf.threads, columnExistsThread, threadChoice=True)
except KeyboardInterrupt: except KeyboardInterrupt:
warnMsg = "user aborted during column existence " warnMsg = "user aborted during column existence "
warnMsg += "check. sqlmap will display partial output" warnMsg += "check. sqlmap will display partial output"
logger.warn(warnMsg) logger.warn(warnMsg)
finally:
kb.bruteMode = False
clearConsoleLine(True) clearConsoleLine(True)
dataToStdout("\n") dataToStdout("\n")
@ -287,3 +294,81 @@ def columnExists(columnFile, regex=None):
hashDBWrite(HASHDB_KEYS.KB_BRUTE_COLUMNS, kb.brute.columns, True) hashDBWrite(HASHDB_KEYS.KB_BRUTE_COLUMNS, kb.brute.columns, True)
return kb.data.cachedColumns return kb.data.cachedColumns
@stackedmethod
def fileExists(pathFile):
retVal = []
paths = getFileItems(pathFile, unique=True)
kb.bruteMode = True
try:
conf.dbmsHandler.readFile(randomStr())
except SqlmapNoneDataException:
pass
except:
kb.bruteMode = False
raise
threadData = getCurrentThreadData()
threadData.shared.count = 0
threadData.shared.limit = len(paths)
threadData.shared.value = []
def fileExistsThread():
threadData = getCurrentThreadData()
while kb.threadContinue:
kb.locks.count.acquire()
if threadData.shared.count < threadData.shared.limit:
path = paths[threadData.shared.count]
threadData.shared.count += 1
kb.locks.count.release()
else:
kb.locks.count.release()
break
try:
result = unArrayizeValue(conf.dbmsHandler.readFile(path))
except SqlmapNoneDataException:
result = None
kb.locks.io.acquire()
if not isNoneValue(result):
threadData.shared.value.append(result)
if conf.verbose in (1, 2) and not conf.api:
clearConsoleLine(True)
infoMsg = "[%s] [INFO] retrieved: '%s'\n" % (time.strftime("%X"), path)
dataToStdout(infoMsg, True)
if conf.verbose in (1, 2):
status = '%d/%d items (%d%%)' % (threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit))
dataToStdout("\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True)
kb.locks.io.release()
try:
pushValue(logger.getEffectiveLevel())
logger.setLevel(logging.CRITICAL)
runThreads(conf.threads, fileExistsThread, threadChoice=True)
except KeyboardInterrupt:
warnMsg = "user aborted during file existence "
warnMsg += "check. sqlmap will display partial output"
logger.warn(warnMsg)
finally:
kb.bruteMode = False
logger.setLevel(popValue())
clearConsoleLine(True)
dataToStdout("\n")
if not threadData.shared.value:
warnMsg = "no file(s) found"
logger.warn(warnMsg)
else:
retVal = threadData.shared.value
return retVal

View File

@ -78,7 +78,6 @@ from lib.core.enums import HASH
from lib.core.enums import MKSTEMP_PREFIX from lib.core.enums import MKSTEMP_PREFIX
from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapDataException
from lib.core.exception import SqlmapUserQuitException from lib.core.exception import SqlmapUserQuitException
from lib.core.patch import resolveCrossReferences
from lib.core.settings import COMMON_PASSWORD_SUFFIXES from lib.core.settings import COMMON_PASSWORD_SUFFIXES
from lib.core.settings import COMMON_USER_COLUMNS from lib.core.settings import COMMON_USER_COLUMNS
from lib.core.settings import DEV_EMAIL_ADDRESS from lib.core.settings import DEV_EMAIL_ADDRESS

View File

@ -18,6 +18,7 @@ from lib.core.compat import xrange
from lib.core.convert import encodeBase64 from lib.core.convert import encodeBase64
from lib.core.convert import encodeHex from lib.core.convert import encodeHex
from lib.core.data import conf from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger from lib.core.data import logger
from lib.core.enums import CHARSET_TYPE from lib.core.enums import CHARSET_TYPE
from lib.core.enums import EXPECTED from lib.core.enums import EXPECTED
@ -82,6 +83,7 @@ class Filesystem(GenericFilesystem):
return chunkName return chunkName
def stackedReadFile(self, remoteFile): def stackedReadFile(self, remoteFile):
if not kb.bruteMode:
infoMsg = "fetching file: '%s'" % remoteFile infoMsg = "fetching file: '%s'" % remoteFile
logger.info(infoMsg) logger.info(infoMsg)

View File

@ -31,6 +31,7 @@ from plugins.generic.filesystem import Filesystem as GenericFilesystem
class Filesystem(GenericFilesystem): class Filesystem(GenericFilesystem):
def nonStackedReadFile(self, rFile): def nonStackedReadFile(self, rFile):
if not kb.bruteMode:
infoMsg = "fetching file: '%s'" % rFile infoMsg = "fetching file: '%s'" % rFile
logger.info(infoMsg) logger.info(infoMsg)
@ -39,6 +40,7 @@ class Filesystem(GenericFilesystem):
return result return result
def stackedReadFile(self, remoteFile): def stackedReadFile(self, remoteFile):
if not kb.bruteMode:
infoMsg = "fetching file: '%s'" % remoteFile infoMsg = "fetching file: '%s'" % remoteFile
logger.info(infoMsg) logger.info(infoMsg)
@ -64,6 +66,7 @@ class Filesystem(GenericFilesystem):
warnMsg += "file '%s'" % remoteFile warnMsg += "file '%s'" % remoteFile
if conf.direct or isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): if conf.direct or isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION):
if not kb.bruteMode:
warnMsg += ", going to fall-back to simpler UNION technique" warnMsg += ", going to fall-back to simpler UNION technique"
logger.warn(warnMsg) logger.warn(warnMsg)
result = self.nonStackedReadFile(remoteFile) result = self.nonStackedReadFile(remoteFile)

View File

@ -32,6 +32,7 @@ class Filesystem(GenericFilesystem):
Request.queryPage(payload, content=False, raise404=False, silent=True, noteResponseTime=False) Request.queryPage(payload, content=False, raise404=False, silent=True, noteResponseTime=False)
for remoteFile in remoteFile.split(','): for remoteFile in remoteFile.split(','):
if not kb.bruteMode:
infoMsg = "fetching file: '%s'" % remoteFile infoMsg = "fetching file: '%s'" % remoteFile
logger.info(infoMsg) logger.info(infoMsg)
@ -42,10 +43,11 @@ class Filesystem(GenericFilesystem):
if not isNoneValue(fileContent): if not isNoneValue(fileContent):
fileContent = decodeDbmsHexValue(fileContent, True) fileContent = decodeDbmsHexValue(fileContent, True)
if fileContent: if fileContent.strip():
localFilePath = dataToOutFile(remoteFile, fileContent) localFilePath = dataToOutFile(remoteFile, fileContent)
localFilePaths.append(localFilePath) localFilePaths.append(localFilePath)
else:
elif not kb.bruteMode:
errMsg = "no data retrieved" errMsg = "no data retrieved"
logger.error(errMsg) logger.error(errMsg)

View File

@ -9,6 +9,7 @@ import os
from lib.core.common import randomInt from lib.core.common import randomInt
from lib.core.compat import xrange from lib.core.compat import xrange
from lib.core.data import kb
from lib.core.data import logger from lib.core.data import logger
from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.core.settings import LOBLKSIZE from lib.core.settings import LOBLKSIZE
@ -23,6 +24,7 @@ class Filesystem(GenericFilesystem):
GenericFilesystem.__init__(self) GenericFilesystem.__init__(self)
def stackedReadFile(self, remoteFile): def stackedReadFile(self, remoteFile):
if not kb.bruteMode:
infoMsg = "fetching file: '%s'" % remoteFile infoMsg = "fetching file: '%s'" % remoteFile
logger.info(infoMsg) logger.info(infoMsg)

View File

@ -174,6 +174,7 @@ class Filesystem(object):
return True return True
def askCheckReadFile(self, localFile, remoteFile): def askCheckReadFile(self, localFile, remoteFile):
if not kb.bruteMode:
message = "do you want confirmation that the remote file '%s' " % remoteFile message = "do you want confirmation that the remote file '%s' " % remoteFile
message += "has been successfully downloaded from the back-end " message += "has been successfully downloaded from the back-end "
message += "DBMS file system? [Y/n] " message += "DBMS file system? [Y/n] "
@ -255,7 +256,7 @@ class Filesystem(object):
if fileContent is not None: if fileContent is not None:
fileContent = decodeDbmsHexValue(fileContent, True) fileContent = decodeDbmsHexValue(fileContent, True)
if fileContent: if fileContent.strip():
localFilePath = dataToOutFile(remoteFile, fileContent) localFilePath = dataToOutFile(remoteFile, fileContent)
if not Backend.isDbms(DBMS.PGSQL): if not Backend.isDbms(DBMS.PGSQL):
@ -269,7 +270,7 @@ class Filesystem(object):
localFilePath += " (size differs from remote file)" localFilePath += " (size differs from remote file)"
localFilePaths.append(localFilePath) localFilePaths.append(localFilePath)
else: elif not kb.bruteMode:
errMsg = "no data retrieved" errMsg = "no data retrieved"
logger.error(errMsg) logger.error(errMsg)

View File

@ -572,6 +572,10 @@ commonTables = False
# Valid: True or False # Valid: True or False
commonColumns = False commonColumns = False
# Check existence of common files.
# Valid: True or False
commonFiles = False
# These options can be used to create custom user-defined functions. # These options can be used to create custom user-defined functions.
[User-defined function] [User-defined function]