AI Security

Hunting ARTEX: The Four-Request Recon Sequence That Shows Up Before the Password Spray

Dark cyberpunk illustration of a vast cyan-lit archive hall of steel filing cabinets receding into darkness, with a long diagonal run of drawers pulled open and glowing amber as blurred robotic arms work down the row at speed

One second. That was the median gap between password-spray attempts when Wiz Research watched an attacker point ARTEX at Okta and Entra ID tenants through October, in sessions that ran 24 to 30 hours without a pause. Three vendors published on the framework inside sixteen days, and on November 2 a named victim confirmed it in its logs: WorkOS disclosed unauthorized access to an internal sales application and said its investigation "identified the use of an AI-assisted offensive security framework."

ARTEX is an open-source agentic penetration-testing framework published on GitHub by a Chinese developer using the handle Autumn-27. An operator hands it a target and an objective. A planner agent breaks the engagement into tasks, worker agents run ordinary tools, and findings return to a shared graph the planner reads before choosing the next step. The repository went closed-source in October after the abuse became public. Forks remain in circulation.

This one reaches you if you expose a web application with a password login, a self-service password reset, or a sign-up flow, and if your identity provider is Okta, Entra ID, or AWS Cognito. Wiz's victim set was IT and software vendors, which means managed service providers and the companies whose products you install. If your whole internet footprint is a static marketing site behind a CDN with no authentication and no API, the recon sequence below will find nothing; skip to the last section, which is about credentials older than your log retention. Everyone else has a day of cheap detection work.

What four reports agree ARTEX actually does

The evidence arrived in three independent tranches. Zscaler ThreatLabz published first, on October 14, attributing late-September activity against South Korean financial firms to a financially motivated Chinese-speaking actor with moderate confidence. The agents worked peripheral internet-facing applications rather than core banking, exploited SQL injection and an authentication bypass, and exfiltrated personal data on roughly 67,000 people including names, phone numbers, annual income and loan limits. ARTEX ran on DeepSeek v4.1-flash. Its default guardrail blocklist covers government and university domains and says nothing about banks.

Wiz followed on October 22 with activity overlapping Microsoft's PRC-aligned Storm-0940, aimed at identity providers and Microsoft 365 integrations. CrowdStrike published on October 30: one ARTEX controller run by the actor it calls VAPOR PANDA reached more than 200 organizations in at least 14 countries over an 11-day window starting October 17, with post-exploitation confirmed at nine. The planning agents burned about 1.4 billion input tokens in a single intrusion. A human operator steered in Chinese at decision points, and one instruction told the agents to find domain machines running no endpoint sensor.

Google's threat intelligence group placed ARTEX in a category on the same day, alongside PentAGI, Hexstrike and CAIRN, and drew the conclusion that matters for defenders: these harnesses reduce the time needed to combine known techniques rather than supplying new ones. They currently trade stealth for velocity. An intrusion that used to take an operator hours of typing now runs in minutes, and it leaves a great deal of log evidence while it does. Google measured 27 events in 23 seconds in one burst and more than 500 web requests probing file names in another.

The recon sequence in your web access logs

Wiz published the request fingerprint, the most useful artifact in any of the four reports. Before touching authentication, the agents mapped the application by fetching the files a front-end build leaves behind: a JavaScript source map, a runtime config, a Swagger or OpenAPI document. Minutes later they called the internal API routes those files had disclosed.

The requests, in the order they appeared

  • GET /packs/js/main-3a9f...js.map and GET /assets/config.json and GET /env.js - the build artifacts that name your internal routes
  • GET /swagger/?format=openapi - the API contract, fetched whole
  • GET /zzz-nonexistent-xyz123 - a deliberate miss, to learn what your 404 looks like before interpreting any other response
  • GET /company/1/logins returning 403 and GET /api/users/generate_redirect_link?role=admin returning 401 - the disclosed routes, tried for broken authorization

That baseline 404 probe is the tell I would build a rule on first. A human tester knows what a missing page looks like; an agent reasoning over HTTP responses has to measure it, so it asks for a path no application would serve. Paired with a source-map fetch from the same address, it is close to unambiguous. This pass groups access logs by source address and reports the addresses that read application metadata and then called an API route.

# Nginx/Apache combined format: $1 IP, $7 request path, $9 status code.
# Reports source IPs that fetched build metadata AND then called an API route.
awk '
  { ip=$1; path=$7; code=$9
    if (path ~ /\.js\.map$|\/config\.json$|\/env\.js$|swagger|openapi/) meta[ip]++
    if (path ~ /^\/api\/|\/v[0-9]+\//)                                  api[ip]++
    if (code == 404)                                                    miss[ip]++
    seen[ip]++ }
  END { for (ip in meta)
          if (api[ip] > 0)
            print seen[ip], ip, "meta=" meta[ip], "api=" api[ip], "404=" miss[ip]+0 }
' /var/log/nginx/access.log | sort -rn | head -20

Read the 404 column against the total. A crawler or a monitoring probe produces a handful of misses. An agent enumerating file names produces hundreds; Google counted more than 500 in one case.

Why the user agents are the weakest indicator published

Press coverage settled on two strings, artex-enrich/1.0 and norma/0.4, after WorkOS founder Michael Grinich named them in a post and said they were likely attributable to ARTEX. Check what the research actually supports before you write a blocking rule. Wiz's indicator table contains norma/0.4 and ReconProbe/1.0. It does not contain artex-enrich/1.0, and WorkOS's own bulletin names no framework at all. Wiz also counted more than fifteen user-agent strings across the activity, most of them unremarkable: curl/7.88.1, python-requests/2.34.2, Python-urllib/3.12, and a long tail of truncated Chrome strings such as Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.

Two of those strings are worth a low-priority alert. The rest will bury you, and the agents rotate addresses and user agents automatically when a request gets blocked or rate-limited, so a signature built on either decays the moment it fires. Google's framing is the one to adopt: the signal lives in the identity, origin, volume and sequence of the requests rather than in any single action. Keep the user-agent list as corroboration you check after a cadence rule has paged you.

Identity logs: the cadence and the recovery flow

In the spray phase the agents concentrated on software-engineer accounts, one second apart. Human spraying is slower and lumpier; credential-stuffing botnets are faster but spread across many addresses. A sustained sub-second rhythm from one origin against dozens of accounts is a narrow, high-confidence shape. This query finds it in Entra sign-in logs.

// Entra ID sign-in logs: one origin, many accounts, machine-speed cadence.
// Tune Accounts and SecondsPerAttempt to your own tenant's baseline.
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType != 0                      // failures only
| summarize Attempts   = count(),
            Accounts   = dcount(UserPrincipalName),
            AgentCount = dcount(UserAgent),
            FirstSeen  = min(TimeGenerated),
            LastSeen   = max(TimeGenerated)
          by IPAddress
| extend WindowSeconds     = datetime_diff('second', LastSeen, FirstSeen)
| extend SecondsPerAttempt = round(todouble(WindowSeconds) / Attempts, 2)
| where Accounts >= 20 and SecondsPerAttempt <= 3
| order by Accounts desc

Keep AgentCount in the output. One origin presenting several user agents across a single burst is the rotation Wiz described, and it raises confidence rather than lowering it. The same cadence logic ports to Okta system logs and to Cognito, where the events to watch are InitiateAuth, ForgotPassword, ResendConfirmationCode and SignUp.

The three endpoints the agents reached for every time

  • Password reset token generation. Wiz logged POST /api/v1/Users/GeneratePasswordResetToken returning 204 and POST /forgot_password. An endpoint that answers an unauthenticated caller tells an agent which usernames exist.
  • Self-service sign-up and confirmation. POST /v2/register_user returned 201 and POST /confirm returned 200. The agents registered disposable accounts, then used them to probe for cross-tenant access.
  • Single sign-on entry points. GET /sso?jwt=eyJhbGciOiJub25lIn0... - an unsigned token with "alg":"none" - plus forged OAuth callback parameters and variants of /sso/login and /sso/jwt. Old bugs, tried fast.

Check your user-enumeration endpoints too. GET /Account/ValidateUserName?UserName=admin is a stock ASP.NET helper, and an agent that finds it gets a list of valid accounts before any spray begins.

Host-side markers worth one read-only sweep

CrowdStrike and Google published artifacts that survive on disk, and these are specific to the harness. CrowdStrike names the directory C:\ProgramData\artx5v\, command-line parameters containing artx or artex, cloud roles with an artx- prefix, and administration-command comments carrying art18-. Google adds files named artex-probe- with five-digit incrementing numbers, local accounts containing ArtexProbe, and sequentially named .jsp files in web application paths.

# Read-only sweep for the published ARTEX host markers. Run per server.
$sinceDays = 14

# CrowdStrike: staging directory and command lines carrying the harness name.
Test-Path 'C:\ProgramData\artx5v'

Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4688;
    StartTime=(Get-Date).AddDays(-$sinceDays) } -ErrorAction SilentlyContinue |
  Where-Object { $_.Message -match '\bart(e)?x' } |
  Select-Object TimeCreated, Id, @{ n='Detail'; e={
      ($_.Message -split "`n" | Where-Object { $_ -match 'Command Line' }) -join ' ' } }

# Google: sequentially numbered probe files and web shells in app directories.
Get-ChildItem 'C:\inetpub','C:\ProgramData' -Recurse -File -Force -ErrorAction SilentlyContinue |
  Where-Object { $_.Name -match 'artex-probe-\d{5}' -or
                 ($_.Extension -in '.jsp','.aspx' -and $_.Name -match '\d{3,}') } |
  Select-Object FullName, Length, LastWriteTime

# Google: local accounts the probe created.
Get-LocalUser | Where-Object { $_.Name -match 'ArtexProbe' }

The 4688 half returns nothing unless command-line auditing is on, which on most servers it is not. Switch on "Include command line in process creation events" before you need it; it costs almost nothing. On Linux web servers, Zscaler's equivalent checks are unexpected .jsp or .aspx files under application paths and outbound connections to api.deepseek.com, open.bigmodel.cn, api.x.ai or api.anthropic.com from hosts with no reason to call a model provider.

Every foothold was older than the framework

Read CrowdStrike's three case studies for what the agents had to work with, because none of it was new. At one pharmaceutical company an unsigned SAML assertion and an XXE flaw reached the cloud instance metadata service, which returned a role that could issue administration commands to thousands of instances. At another, an unauthenticated orchestration service gave root in a production container environment, and the agents then read LAPS passwords and abused resource-based constrained delegation. At a Greek government entity the operator supplied a 2022 mail server directory export whose authentication keys had apparently never been rotated; the agents forged an administrator token, read mailboxes, and used a VPN client key from an email leak dated May 2024 to reach the internal network.

Unsigned SAML, IMDSv1, keys from 2022, a credential from a 2024 breach, delegation nobody reviewed. The agents found all of it quickly and chained it without getting bored, and that is the part a two-person IT team should take seriously: the window between a stale credential becoming reachable and becoming used has closed to minutes. Three of Google's five recommended layers are configuration rather than detection - restrict admin consoles to trusted networks, move to short-lived workload credentials, and put rate and recipient limits on any API that can send mail or move money.

Rate limit the three endpoints the agents always find

Do three things this month. Put a rate limit and a lockout on the password-reset, sign-up and confirmation endpoints listed above, and stop returning 204 or 201 to unauthenticated callers who are guessing usernames. Ship the cadence query against your identity logs and the access-log pass against your web tier, alerting on sequence rather than on user agent. Then inventory every signing key, service credential and directory export that predates your current log retention window, because if one of those is exposed you will never find the evidence - rotate them instead of looking. Enforce SAML signature validation and require IMDSv2 while you are in there.

The detection work is a day. The rotation stalls because nobody owns the list. If you want help building a detection pipeline that catches machine-speed activity in your own logs, or a second read on what your identity provider currently tells an anonymous caller, that is work we do.

Drowning in alerts? We can help.

We help security teams optimize their detection pipelines and reduce alert fatigue. Book a session to discuss your SIEM environment.