Tag Archives: SANS

numbat – AI agent observability, (Fri, Sep 4th)

This post was originally published on this site

numbat logo​​​​​​​

Enterprises face an unmanaged crisis of AI agent and MCP server sprawl, characterized by rapid, decentralized proliferation of autonomous agents, protocol connections operating with excessive privilege, opaque execution paths, and identity blind spots.
Absent agent-aware governance, modern enterprises struggle to prevent, detect, or contain multi-hop autonomous exploits, leaving environments vulnerable to lateral movement, shadow collaboration, and unauthorized data exfiltration.
More succinctly, in light of the recent OpenAI/Hugging Face incident, monitoring clearly lags behind agent capability. The tooling to observe what agents are actually doing, in real time, is not yet standard practice, even at the labs building the agents.
To that end, Perplexity AI’s open source numbat offers excellent observability and visibility to supported desktop, CLI, IDE, and gateway agents through local hooks and plugins, OTLP/HTTP logs, and on-disk session artifacts.[1]

Crafted as a Go binary, numbat works seemlessly on macOS, Linux, or Windows. You can download a static binary or install with Go. Read for all features, requirements, configurations, and options. Getting up and running is nearly instant so I’ll focus exclusively on usage. Note that if you install via Go, the numbat binary is then found under /go/bin.
As a regular user of Anthropic, OpenAI, and Google AI offerings, I expected to learn immediate insights, but I was a bit taken aback by the number agents installed on my system. Initial discovery is as easy as ./numbat agents, my insightful result seen in Figure 1.

numbat agents

Figure 1: numbat agent enumeration

A full breakdown of agents is provided, their available configs, what artifacts are available, if a hook is available, whether or not said hook is wired, and the steps necessary to do so. As you can see I’m already hooked for Claude and Gemini, but I did so as follows:

./numbat hook install --agent claude --emit all  
./numbat hook install --agent gemini --emit all

Agent enumeration is great, but with the hook in, the real value proposition emerges. While hooks start in monitor-only mode, the --emit all function writes events, findings, indicators, and applicable enforcement decisions to ~/.numbat/records.ndjson by default.
As such, detection rules come to bear; they are many, and effective. Refer to the built-in rule catalog for detected behaviors. Detectors are defined categorically, including secrets, exfiltration, integrity, execution, reconnaissance, privilege, lateral movement, impact, source control, tampering, persistence, and sequences. Run numbat rules list for the full listing.
After review of said catalog, I opted to test a specific detector and chose an easy one to stimulate: recon.network_sweep where “a named scanner is given an explicit scan or target-list option and a network range.” Easy enough with a quick prompt via ClaudeCode, as seen in Figure 2.

claude nmap???????

Figure 2: ClaudeCode nmap scan

Sure, an nmap scan isn’t exactly the most invasive or adversarial act one could imagine but it does qualify as something you’re not likely to want having an agent swarm unleashing on your enterprise unabated. So much so that Mitre ATT&CK tags Network Service Discovery as technique T1046 which, in turn, is conveyed via the recon.network_sweep rule and written as a finding, when triggered, to ~/.numbat/records.ndjson if you’re using default installation.
I’m a big fan of jq to render JSON as human readable: jq . ~/.numbat/records.ndjson
The result is a number of related artifacts written to the records file as seen in Figure 3.

numbat recon rule

Figure 3: numbat recon finding

Logically, there may be behaviors or actions you’d also like to block or prevent. numbat rules can be set to enforce as defined in numbat’s enforcement documentation. Specifically, “to enforce a shipped detection selectively, copy its complete YAML file from the matching release’s shipped catalog into a controlled operator rules directory, keep the same id, set enforce: true, and bump the rule version. Validate the effective catalog, then deploy the same directory with the hook”:

numbat rules check --rules-dir /opt/numbat/rules  
numbat hook install --agent claude --managed --rules-dir /opt/numbat/rules --enforce

Additionally, an investigation may be required, under certain circumstances, should inappropriate or unauthorized activity be detected.
Your IR team will appreciate the investigation packaging inherent to numbat.
Create a case ID, write to an output file, build the case artifacts, including a manifest file with SHA256 hashes for integrity matching, and verify the manifest.

numbat scan --case-id inv-03SEP2026 --emit all --output file --output-file investigations.ndjson
numbat case build inv-03SEP2026 --from investigations.ndjson -o inv-03SEP2026.numbat
numbat case verify inv-03SEP2026.numbat

The result is a folder named for the case ID that includes events.ndjson, findings.ndjson, and the .numbat manifest file. The findings are complete and accurate, and as they pertain to my misbehavorior with nmap, robust in their evidence collection, as seen in Figure 4.

numbat findings???????

Figure 4: numbat investigation finding (snippet)

All good investigators love their timelines. numbat offers a tidy timeliner, called as simply as numbat timeline --agent claudetimeline is a read-only view that groups events by source_agent, source_type, and session_id where each chronological step retains its evidence reference.

Consider the CLI reference required reading for your immediate use.
Recognize that I’ve not covered a number of excellent additional features including all the Deployment options.
These include local use as well as Live OTLP/HTTP capture (numbat collect) and Async HTTP delivery with no external shipper (numbat ship).
Importantly, there are managed configuration (MDM) opportunities for fleet deployments, including a small guided MDM pilot. The options are many, and the flexibility is welcome.
numbat is a really solid offering from the Perplexity crew, and I thoroughly enjoyed the opportunity to investigate the use cases. I see enterprise utilization and deployment opportunities here for sure.

Cheers…until next time.

Russ McRee | @holisticinfosec | infosec.exchange/@holisticinfosec | LinkedIn.com/in/russmcree

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.

The Coding-Agent Trap: When a "Free" LLM Endpoint Is the Adversary, (Mon, Aug 31st)

This post was originally published on this site

One of my internet-exposed inference honeypots was discovered, relabeled with sought-after model names, and incorporated into infrastructure apparently used to provide "free" LLM backends. It then received a real coding-agent session — history, filesystem output, working paths, and the agent's local tool manifest. The honeypot did not request or cause any tool execution; what the request exposed is what a malicious operator in that position could do.

YARA-X 1.20.0 Release, (Sun, Aug 30th)

This post was originally published on this site

YARA-X's 1.20.0 release brings 14 improvements and 13 bugfixes.

One new CLI option is –ignore-invalid-rules that allows one to skip rules that fail to compile.

 

There have also been new releases of YARA:

YARA 4.5.6YARA 4.5.7 and YARA 4.5.8 with 32 bugfixes in total.

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.

Some Malicious PE Stats, (Thu, Aug 27th)

This post was originally published on this site

During my last FOR610 session, a student asked me if I had some statistics in mind about the compilers used to generate malicious PE files? A couple of months ago, I shared some stats about the trend in 64bits VS. 32bits malware[1]. Can we go a bit further? I (vibe-)coded a Python script based on the pefile library[2] to extract some info from the PE headers. Indeed, the PE file format contains a lot of metadata! They can be accessed using a lot of tools, like Detect It Easy:

Note: When you assess a PE file, a gold rule to follow is to never trust what you see because these metadata can be tempered!

I tried to detect the compiler using three techniques:

  • The "Rich Header" is a block of data containing useful information (but undocumented by Microsoft). It's an XOR-obfuscated block that the Microsoft linker embeds between the DOS stub and the NT headers of PE files built with the MSVC toolchain. It records the @comp.id (product id + build number) and use-count of every object file that went into the link, which lets you fingerprint the exact compiler/linker/assembler build used, as well as, even the count of source files. pefile is able to handle these data smoothly.
  • The .NET CLR header (IMAGE_COR20_HEADER) + CLR metadata root, for managed (C#/VB.NET/F#) binaries. This gives the CLR runtime version and the embedded metadata version string (e.g. "v4.0.30319"). This is manually parsed per the public ECMA-335 spec (there's no MSVC Rich Header in managed-only PEs).
  • A light heuristic string scan for common non-Microsoft compiler signatures (GCC/MinGW, Clang/LLVM, Delphi/Borland, Free Pascal, Go, Rust), since none of those toolchains write a Rich Header. Just because strings are always easy to process and may reveal juicy information!

As said above, there is no official Microsoft documentation for the Rich Header, and no single authoritative mapping of every product-id -> tool/version exists. But they are community references that helps! The well-known "comp_id.txt" is one of them and constantly updated[3].

Now that we have a tool, where can we find fresh meat? Malware Bazaar is a good candidate because it is pretty popular and get new samples daily. They allow (but don't abuse) to download their data set for free! The first step was to download all the archive they offer[4]. I downloaded a total of 1.3 TB of ZIP archives, one archive per day from 2020-02-24 to 2026-07-08.

Because PE files can be embedded into other files and to avoid using to much storage, I rewrote the script:

  • To unzip files in memory and avoid touching the disk
  • To perform a recursive scan up to 3 levels

Here are the stats I gathered after “a few days” of processing!

High level stats

Total scanned files 23.501.548
Not PE 22.580.068
Valid PE 690.689
Encrypted or unreadable 227.755
Invalid PE 1.508
ZIP Bomb 951
Invalid ZIP 519
Error 36
Skipped Nested ZIP (> 3 levels) 19
File Too Large 3

About the architecture:

32 Bits (or other architecture) 565.179
64 Bits 125.510

Interesting, this confirms my previous research: 32 bits PE file remain popular.

Rich Header:

Rich Header Present 371.103
No Rich Header (Maybe stripping, a non-MSVC toolchain, tempeing,…) 319.586

Top-10 linker versions:

linker 48.0 102.307
linker 6.0 91.788
linker 9.0 62.070
linker 8.0 47.829 
linker 2.25 36.673
linker 10.0 36.549
linker 14.0 29.786
linker 11.0 25.784
linker 14.29 24.655
linker 80.0 22.691

Top MSVC Rich Header compiler builds (useful for clustering samples built in the same environment/campaign):

build 26213 19.603
build 24213 15.389
build 30034 14.325
build 26706 7.755
build 24215 5.253
build 32533 5.033
build 33030 4.442
build 31823 3.738
build 25834 3.530
build 27412 3.294

Finally, and the most interesting status, what tools are used by attackers?

Unidentified (no Rich Header, no signature match)

272.439

39.4%

Microsoft toolchain (Rich Header present, no recognized C/C++ entry) 216173 31.3%
Borland C++/Delphi 20172 2.9%
Microsoft Visual C/C++ (Rich Header, compiler build 26213) 19603 2.8%
GCC / MinGW 13804 2.0%
Go 6254 0.9%
Embarcadero/Borland Delphi 6174 0.9%
Rust 1329 0.2%
Clang/LLVM 91 0.0%
Free Pascal (FPC) 1 0.0%

Interesting to see that arising programming languages like Go or Rust remain exotic in the data set! I expected more popularity!

[1] https://isc.sans.edu/diary/2026+64Bits+Malware+Trend/32718
[2] https://github.com/erocarrera/pefile
[3] https://github.com/dishather/richprint/blob/master/comp_id.txt
[4] https://bazaar.abuse.ch/export/

Xavier Mertens (@xme)
Senior ISC Handler | SANS Principal Instructor | Freelance Consultant
Xameco | PGP Key

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.

A polymorphic phishing page (that occasionally breaks itself), (Thu, Aug 27th)

This post was originally published on this site

As I’ve mentioned before in some of my diaries, from time to time, I like to go over phishing messages that get caught in my various spam traps or sent to us here at the Internet Storm Center.

After looking at enough phishing messages, one quickly gets used to seeing the same lures, the same credential-harvesting pages and, quite often, the same obfuscation techniques over and over again. But even something that seems to be “run-of-the-mill” at first glance can sometimes turn out to be quite interesting.

One such message was recently sent to our handler inbox, and as you can see, there was very little about it that would indicate that it would be worth a deeper look.

The link in the message pointed to a URL with the following, quite usual, structure:

hxxps[:]//addresses[.]performs[.]vu/communications.html?good=[recipient_address]

Nevertheless, what happened after the link was opened was somewhat less usual.

Instead of displaying a phishing page, the browser remained effectively stuck for about 30 seconds, while utilization of one CPU core in the virtual machine I was using quickly rose to 100 %. Since retrieving the HTML source itself was almost instantaneous, it seemed clear that the delay wasn't caused by the server, and instead something in the page itself was preventing the browser from finishing its work.

Although a quick look at the source code showed that almost all of the page consisted of heavily obfuscated JavaScript, the reason for the unusual behavior fortunately wasn't too difficult to identify.

Among other things, the script contained two functions, which are slightly reformatted here for easier readability:

function _il(m) {
    for(k=0; 64>k; k++) {
        m[_lV(_ie(),k)]=k
    }
    return m
}

function _YF(m,h) {
    var v="";
    for(k=m; k<=h; k++) {
        v=v+String.fromCharCode(k)
    }
    return v
}

As you can see, both functions use k as a counter in their for loops. The first function is part of a decoding routine, and its loop counter is expected to go from 0 to 63. The second function is a helper used by the same routine to construct strings from ranges of character codes – it is used (among other places) in the _ie() function, which is called by the first function. The problem is that k isn't declared locally in either one of these functions.

This becomes important because _ie(), which is called during every iteration of the first loop, uses _YF() several times to construct the Base64 alphabet. Its final call is _YF(47,47), which produces the ‘/’ character (ASCII code 47).

Since the counter k used by _YF() is global, this final call also changes the value of k used by the outer loop. _YF(47,47) first sets k to 47, executes its loop once and then increments k to 48. At that point, the condition k <= 47 is no longer true, so _YF() returns with the global value of k left at 48.

Control then returns to the outer for loop, whose own increment changes k from 48 to 49. Since 49 is still smaller than 64, another iteration starts and _ie() is called again. Its final _YF(47,47) call once more leaves k at 48. The outer loop therefore never progresses beyond 49.

The resulting sequence therefore looks roughly like this:

48 -> 49
48 -> 49
48 -> 49
...

This explained both why the page never rendered and why the browser was keeping one CPU core rather busy.

Changing the inner routine to use its own local counter was sufficient to let the decoding process finish. After removing the remaining layers of obfuscation, what emerged was an otherwise completely unremarkable credential-stealing page.

At this point, the most likely explanation seemed fairly straightforward – the authors of the page had simply shot themselves in the foot by using a broken obfuscation mechanism.

Nevertheless, this proved not to be the case, since when I accessed the original URL again a little later, the page loaded normally. Another attempt to load the page was also successful, as were several subsequent ones.

More interestingly, while all of the resulting pages ultimately displayed the same credential-stealing form, their source code wasn't the same.

Function and variable names differed across page loads, functions appeared in a different order, numerical constants were expressed using different arithmetic operations and a large encoded block of code, which contained the actual payload with the form, changed as well. Even the innocuous-looking page title varied between requests using words like "Solution", "Viewer", "Credentials", "Private" and "Authenticate".

It therefore appeared that the first response wasn't a permanently broken copy of the phishing page at all. Rather, the server seemed to generate polymorphic variants of the page and I had simply happened to receive a “broken” one when I first accessed the target URL.

To test this hypothesis, I used a simple script to retrieve the same URL 50 times and, with some help from an LLM, compared the resulting samples.

Among the 50 samples (which all had different SHA-256 hashes), there were 21 different page titles, and, more importantly, 49 deobfuscated successfully while one became stuck in an endless loop – just like the first page I had the luck to land on.

The reason was effectively identical to what happened in the first page I encountered. In this variant, the two relevant functions had different randomized names, but both of their loops had once again been assigned the same undeclared variable k. The inner loop therefore repeatedly reset the value used by the outer one and prevented the decoder from completing.

Once this collision was corrected, the sample decoded normally as well.

The polymorphism wasn't limited to the initial JavaScript wrapper. The 50 page variants (if we include the one I had to manually “fix”) produced 50 different versions of the final phishing HTML. Form and input names, CSS classes, element identifiers and parameters used when loading images were changed, as was the placement of zero-width characters inside visible strings, which were used as a further obfuscation/anti-analysis mechanism. In spite of all these changes, however, the page presented to the user and its basic functionality remained essentially identical.

Polymorphic phishing pages are, of course, not new. The concept has been discussed for well over a decade in academic circles[1], and phishing pages which generate random HTML attribute values for individual visits have been used in the wild for years[2]. It has also previously been shown that JavaScript lends itself quite well to producing multiple versions of source code which look different while performing the same task[3] (which is the basis for the simplest implementation of polymorphism at the code level).

The rationale behind such an approach is fairly obvious – hashes, randomly generated identifiers and many simple string-based signatures become significantly less useful if every request produces what is basically a completely new copy of a malicious page.

Although polymorphism certainly shouldn't be thought of as some universal mechanism for bypassing security controls, as the underlying logic and behavior of the pages remains the same, and many structural characteristics inevitably survive most transformations, it does raise the cost of detection mechanisms which rely too heavily on static artifacts…

Though, in this case, it apparently also raised the cost for the threat actor, since at least some victims would end up with a non-functioning page (at least on a first load), given that of the approximately 56 samples I collected (50 using the script + my original manual attempts), two pages were broken.

Although it would be unreasonable to draw any firm conclusions about the actual failure rate of the mechanisms used, it is clear that the original endless loop wasn't just a “one-off” corrupted response and that whatever generates the code can repeatedly create non-functional pages.

Which brings us to one final question – what was actually generating the code?

Given the current popularity of generative AI, it is tempting to consider an LLM-based backend. This isn't entirely far-fetched either – in January, Unit 42 demonstrated a proof-of-concept in which an LLM was used to generate syntactically different phishing JavaScript in real time, resulting in a unique variant for individual visits[4]. There is, however, nothing in the samples which would prove that an LLM is involved here, and a conventional polymorphic obfuscator seems to be a much more plausible explanation, given that the transformations between individual page copies are quite systematic, and the recurring failure caused by reused global variable names would fit quite nicely with a relatively simple random renaming and reordering mechanism which doesn't properly account for variable scope.

In any case, had the first page loaded normally, I would almost certainly have dismissed it as yet another run-of-the-mill phishing site. As it turned out, though, the obfuscation mechanism intended to make the page more difficult to detect was also capable of making it somewhat ineffective at stealing credentials… which made the sample considerably more interesting than it initially appeared.

And – to end on a positive note – the sample did also provide a good lesson to any aspiring programmers out there – never use undeclared global variables as your loop counters.

[1] https://link.springer.com/chapter/10.1007/978-3-642-02617-1_28
[2] https://www.zscaler.com/blogs/security-research/evolution-phishing-kits
[3] https://www.akamai.com/blog/security/the-tale-of-double-javascript-obfuscated-scam
[4] https://unit42.paloaltonetworks.com/real-time-malicious-javascript-through-llms/

———–
Jan Kopriva
LinkedIn
Nettles Consulting

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.

Obfuscating IP Addresses as Hostnames, (Tue, Aug 25th)

This post was originally published on this site

It is pretty obvious that hostnames can replace IP addresses. Pretty much any software accepting an IP address will also accept a hostname as an argument. Last week, I wrote about scans for the cloud metadata service listening at 169.254.169.254. These scans attempted to exploit Server Side Request Forgery (SSRF) vulnerability. One way to prevent these types of exploits is to filter requests that contain the string "169.254.169.254" or to add this IP to a blocklist of URLs that should not be accessed.

But as is almost always the case, blocklists are not the solution you are looking for.

In response to last week's diary, Sean wrote that they saw attackers use hostnames instead of IP addresses. In particular:

  • 169.254.169.254.nip.io
  • 169-254-169-254.sslip.io
  • test.169.254.169.254.nip.io (or other prefixes instead of test)
  • make-1.1.1.1-rebind-169.254.169.254-rr.1u.ms

The last one, as Sean pointed out, is likely linked to the 1u.ms tool. This tool allows attackers to define hostnames "on the fly". It offers numerous options. For example, you can configure the IP address to change after a certain number of lookups or after a certain time. IP addresses can use various encoding/obfuscating formats. The tool can also be configured with a custom domain, but 1u.ms is ready to go.

1u.ms maintains public logs for all requests sent to it, so you can check if it was used against one of your systems. The last 100 requests can be found at http://1u.ms/last and the

Similar hostnames can likely be configured with many dynamic hosting services. If you do retain DNS logs (you should!!), Check whether any resolution resulted in IPs such as 169.254.169.254.


Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.

DOUBLECUP's PNG Payload, (Mon, Aug 24th)

This post was originally published on this site

New malware that uses steganography always gets my attention, but I was disappointed when I looked at the latest DOUBLECUP write-up. It doesn't use real steganography:

You can see the PowerShell payload as cleartext: it has not been encoded into the pixels of the image.

It's even not embedded in the image (like inside the metadata), it's just appended after the PNG file:

Yet there is a clever little trick:

The PowerShell script starts with 0x0D 0x0A, Carriage-Return + Newline: that terminates a line of text in Windows.

That makes that you don't need a custom payload extractor, you can just use the FINDSTR command (Windows' grep) with a unique identifier to extract the script:

And then pipe it into the PowerShell interpreter.

 

Didier Stevens
Senior handler
blog.DidierStevens.com

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.