November 7, 2026research

Redis command sequences use SAVE to target cron paths

From 2026-10-24 through 2026-11-06 UTC, 49 qualifying source-aware Redis sessions requested cron-oriented persistence placement through ordered SET, CONFIG SET and SAVE commands.

#redis#cron#persistence#detection#hunting#linux

Finding and scope

In a selected source-aware Redis command cohort from 2026-10-24 through 2026-11-06 UTC, 49 qualifying sessions from 8 source IPs across 3 sensors contained 193 matching SAVE requests and 1189 command records. The finding is the ordered mechanism: the submitted commands used SET to place scheduled-downloader text in Redis, CONFIG SET dir and CONFIG SET dbfilename to choose a requested file destination, then SAVE within 10 minutes in the same connection.

The sequence matters because it attempts to turn Redis database persistence into placement of cron material. The filesystem destination and ordered relationship between content, configuration and SAVE distinguish the activity from an isolated administrative CONFIG command.

What varied across the Redis sequences

The selected cohort split across equal UTC windows as follows:

  • 2026-10-24 through 2026-10-30: 19 qualifying sessions and 76 matching SAVE requests.
  • 2026-10-31 through 2026-11-06: 30 qualifying sessions and 117 matching SAVE requests.

Captured cron destinations included /var/spool/cron/, /var/spool/cron/crontabs, /etc/cron.d/ and /etc/ with filename crontab. Submitted schedules used 2-, 3-, 4- and 5-minute intervals. The sequences also requested a backup filename before clearing keys and writing scheduled-command text, which helps explain why several SAVE requests can occur inside one session as the commands try different placements.

MITRE ATT&CK describes Cron as Scheduled Task/Job sub-technique T1053.003 and notes that cron is a time-based job scheduler for Unix-like operating systems used for scheduled execution and persistence. That context is relevant to the captured SET, CONFIG SET and SAVE sequence because the requested destinations are cron-relevant locations and the content relationship is part of the detected mechanism. Primary-source document

Local sequence hunt

The tested helper below expects command records from exactly one already-grouped connection. Each record uses ts, event_id and payload fields; payload contains command and list-valued args. It sorts by timestamp and event ID, detects scheduled downloader text, tracks directory and filename state, clears content state on FLUSHALL and returns SAVE event IDs within 600 seconds of matching SET content. Treat it as request-sequence hunting logic and inspect the full connection context before action.

def redis_persistence(commands):
    """Pass records from one connection; return matching SAVE event IDs."""
    import re
    from datetime import datetime

    directory = filename = content_kind = None
    content_at = None
    matches = []
    for row in sorted(commands, key=lambda r: (r['ts'], r['event_id'])):
        p = row['payload']
        verb = str(p.get('command', '')).lower()
        args = p.get('args') or []
        if not isinstance(args, list):
            continue
        timestamp = datetime.fromisoformat(row['ts'])
        if verb == 'flushall':
            content_kind = content_at = None
        elif verb == 'set' and len(args) >= 2:
            value = str(args[1]).lower()
            if re.search(r'(?m)^\s*(?:[\d*/,-]+\s+){4}[\d*/,-]+\s+', value) and any(v in value for v in ('wget', 'curl', 'http://', 'https://')):
                content_kind, content_at = 'cron', timestamp
        elif verb == 'config' and len(args) == 3 and str(args[0]).lower() == 'set':
            if str(args[1]).lower() == 'dir':
                directory = str(args[2]).rstrip('/')
            elif str(args[1]).lower() == 'dbfilename':
                filename = str(args[2])
        elif verb == 'save' and content_at and 0 <= (timestamp-content_at).total_seconds() <= 600:
            cron_target = directory in ('/var/spool/cron', '/var/spool/cron/crontabs', '/etc/cron.d') or (directory == '/etc' and filename == 'crontab')
            if filename and content_kind == 'cron' and cron_target:
                matches.append(row['event_id'])
    return matches

Triage and false-positive checks

Group telemetry by tenant, sensor, service, source and connection identity before applying the function. Useful retained fields are connection identity, request time, command verb, ordered arguments, event ID, destination directory, filename and Redis service identity. The comparison unit should be the session or connection, not a standalone SAVE count, because repeated SAVE requests can represent placement attempts inside one connection.

For triage, review the caller, the workload owner, the requested operation and the approved change record. A normal backup location fails this target predicate, while a recognized administration identity does not explain writing cron paths by itself. Where host telemetry is available, correlate the request time with unexpected cron entries and Redis service-account writes, then review permission to change runtime configuration and restrict untrusted Redis access.