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.

Happy 20th Birthday, Amazon EC2

This post was originally published on this site

Twenty years ago today, Jeff Barr wrote a blog post that launched the Amazon EC2 Beta. That single post introduced resizable Linux virtual servers in the cloud, billed by the hour, with one instance type (m1.small) in one Region (US East). It was minimal yet useful, and it changed how the world thinks about computing infrastructure.

In 2021, Jeff covered the fifteen years of EC2 with the backstory and memorable EC2 launches. Over the last five years, AWS has continued to push the boundaries of what cloud computing can deliver, building custom silicon for general-purpose and AI workloads and expanding EC2 into new form factors and deployment models that our customers in 2006 could not have imagined.

The 20 years in brief
In his 15th anniversary post, Jeff chose important milestones of EC2 that established the foundational building blocks that customers still rely on today. Amazon Elastic Block Store (2008) provided persistent block storage. Elastic Load Balancing, Auto Scaling, and Amazon CloudWatch (2009) made applications scalable and highly available. Amazon Virtual Private Cloud (2009) gave customers logically isolated networks. AWS Nitro System (2017) enabled faster innovation and enhanced security. AWS Graviton processors (2018) were designed for cost-sensitive scale-out workloads.

Over 20 years, EC2 grew from one to over 1,200 instance types to meet customer needs across general-purpose, compute-, memory-, and storage-optimized, accelerated computing, and high-performance computing families. These instances expanded from one AWS Region to 39 Regions globally. AWS also extended EC2 beyond the Region boundary with AWS Outposts (2018) running EC2 instances locally, AWS Local Zones (2019) place globally, and AWS Wavelength (2019) inside global 5G telecommunications carrier networks.

While I hate to play favorites, I want to choose some of my favorite EC2 launches of the past five years:

  • AWS Inferentia for ML inference at scale (2019): We introduced purpose-built ML inference instances (inf1) with AWS Inferentia chips. Amazon EC2 Inf2 instances became generally available in April 2023 for large-scale generative AI inference workloads. Together with the Inferentia family, AWS Trainium instances now give customers a full stack of AWS-designed silicon optimized for every phase of the AI lifecycle both inference and training.
  • EC2 Mac instances (2020): The first Mac instances (mac1) were built on Apple Mac mini with Intel Core i7 (Coffee Lake) on the AWS Nitro System. Mac M1 (mac2) instances launched in July 2022 as the first Arm-based macOS instances on EC2. M2 Pro Mac instances followed in 2023, M4 and M4 Pro Mac instances in 2025, M3 Ultra Mac instances and M4 Max Mac instances in 2026, giving Apple developers a complete range of cloud-based build and test environments for macOS, iOS, iPadOS, tvOS, watchOS, and visionOS apps.
  • AWS Trainium for full-stack AI workloads at scale (2021): In November 2021, we previewed Trn1 instances with AWS Trainium accelerators optimized for high-performance deep learning training. In December 2024, Trn2 instances powered by AWS Trainium2 launched, with Trn2 UltraServers linking 64 Trainium2 accelerators via NeuronLink for training trillion-parameter foundation models. At AWS re:Invent 2025, Trn3 UltraServers powered by AWS Trainium3 deliver the best token economics for next-generation agentic, reasoning, and video generation applications. A single Trn3 UltraServer interconnects up to 144 Trainium3 chips to train and serve the largest frontier models. Now, AWS Trainium3 delivers the leading price-performance for high-performance AI training and inference at scale.
  • EC2 Capacity Blocks for ML (2023): This new EC2 usage model further democratizes ML democratizes ML by making it easy to access GPU instances to train and deploy ML and generative AI models. You reserve the GPU capacity you need (initially P5 instances) for a future date and only for the duration you require. In November 2024, EC2 Capacity Blocks for ML added supported for provisioning in a matter of minutes and extending up to six months. Now, EC2 Capacity Blocks for ML supports P6-B300, P6-B200, P5e, P5en, P4d, P4de, Trn1, Trn2, and Trn3 instances in addition to P5.
  • AWS Graviton5 (2025): Building on eight years of Graviton innovation since 2018, we previewed Graviton5 chips in AWS re:Invent 2025 and launched M9g and M9gd instances powered by Graviton5 and built on the sixth-generation AWS Nitro System. C9g and C9gd followed in June 2026. Now, Graviton5 features 192 cores, a 5x larger cache, and up to 33% lower inter-core latency, making it well suited for the growing demands of agentic AI workloads such as real-time reasoning, code generation, and multi-step task orchestration that require continuous, high-throughput CPU compute at scale.
  • AWS Nitro Isolation Engine (2026): Customers wanted to see, not just hear from us, proof of workload isolation in the Nitro Hypervisor. The Nitro Isolation Engine is a purpose-built component inside the Nitro Hypervisor, harnessing formal verification to provide mathematical assurance that customer workloads are isolated from each other and AWS operators, pioneering a new standard for mathematically proven cloud security. This feature is also based on the sixth-generation AWS Nitro System which has continued to evolve since our introduction in 2017.

The foundation underneath it all
Despite two decades of innovation, the fundamental value proposition of Amazon EC2 has not changed. Customers use it to get secure, resizable compute capacity in minutes, pay only for what they consume, and scale on demand without long-term commitments. That same flexibility now extends to AI workloads at a scale no one anticipated in 2006.

EC2 remains the foundational compute layer of AWS. Amazon ECS, Amazon EKS, AWS Lambda, AWS Fargate, AWS Batch, Amazon EMR, Amazon SageMaker AI, and Amazon Bedrock ultimately run on EC2 capacity. Every architectural pattern customers have built over the past twenty years, from simple web servers to trillion-parameter foundation model training clusters, starts with a decision to launch an instance.

We made strong foundational decisions in 2006, and we left room for the service to grow. Twenty years later, that strategy of creating services that are minimal-yet-useful, launching quickly, and iterating rapidly in response to your feedback continues to guide how we build. The next twenty years of cloud computing will demand capabilities we have not yet imagined. Amazon EC2 will continue to be the foundation where your workloads run.

To learn more about Amazon EC2, visit the Amazon EC2 product page or check out what’s new with EC2.

Channy

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.

AWS Glue 6.0 now available with 30% lower price and full Apache Iceberg v3 support

This post was originally published on this site

Today, we are announcing the general availability of AWS Glue 6.0, delivering 30% lower pricing than previous AWS Glue versions and introducing full support for Apache Iceberg v3 features. AWS Glue 6.0 is built on a fully modernized runtime, Apache Spark 4.1, Python 3.12, and Scala 2.13, delivering faster performance.

With this release, AWS Glue provides the most complete Iceberg v3 implementation on any fully serverless managed Spark service, along with new capabilities that simplify ETL authoring, improve PySpark performance, and enable real-time streaming with single-digit millisecond latency.

What is new in AWS Glue 6.0
AWS Glue 6.0 delivers the complete Apache Iceberg v3 specification, built on Iceberg 1.11.0. The headline feature is the VARIANT data type with shredding support, which achieves faster query read performance compared to traditional string data type columns for semi-structured data.

With VARIANT shredding, you can store and query JSON, logs, and event data without flattening schemas, eliminating duplicate data copies, custom parsing code, and pipeline breakage when schemas change. This capability transforms how teams handle semi-structured data at scale.

Additional Iceberg v3 capabilities include:

  • Geometry and Geography data types: Enable native spatial processing for GIS analytics, location intelligence, and geospatial data pipelines directly on managed Spark.
  • Nanosecond-precision timestamps: Support IoT sensor data, scientific computing, and high-frequency financial workloads that require precision beyond standard milliseconds.
  • Unknown type handling: Process data with unexpected or evolving schemas without pipeline failures, providing resilience against upstream schema changes.

AWS Glue 6.0 also includes most significant upgrade in Spark 4.1, the modern runtime engine:

  • Spark declarative pipelines: Spark Declarative Pipelines introduces a simplified approach to ETL authoring. Data engineers declare transformations, specifying what data should look like, while the engine automatically determines execution order and optimization. This reduces the complexity of pipeline development and eliminates manual orchestration overhead.
  • Arrow-native Python UDFs and UDTFs: AWS Glue 6.0 introduces Arrow-native execution for Python User-Defined Functions (UDFs) and User-Defined Table Functions (UDTFs). This eliminates serialization overhead between Python and the JVM, improving PySpark performance for complex transformations.
  • Real-time streaming mode: For stateless streaming use cases, AWS Glue 6.0 introduces a real-time streaming mode that achieves single-digit millisecond latency. Built on Spark 4.1’s Real-Time Mode with Glue-optimized execution, this capability supports real-time event processing, low-latency data transformation pipelines, and time-sensitive data routing.

Getting started with AWS Glue 6.0
No API changes are required to use AWS Glue 6.0. You can select the new version using the existing --glue-version parameter in the create-job or update-job APIs through AWS Command Line Interface (AWS CLI)AWS SDK, AWS Glue Studio, Amazon SageMaker Unified Studio, and your preferred IDE.

To get started with AWS Glue 6.0 jobs in the AWS Glue Studio console, open the AWS Glue job and on the Job Details tab, choose the version Glue 6.0 – Supports Spark 4.1, Scala 2, Python 3. You can create new AWS Glue jobs on AWS Glue 6.0 to get the benefit from the improvements, or migrate your existing AWS Glue jobs.

To start using AWS Glue 6.0 on an AWS Glue Studio notebook or an interactive session through a Jupyter notebook, set 6.0 in the %glue_version magic. You can also upgrade existing jobs to Glue 6.0 using the Spark upgrade agent on AWS Glue Studio or use the auto-upgrade feature in their existing Glue jobs to automatically upgrade them to Glue 6.0.

To learn more, visit the AWS Glue 6.0 version detail and Migrating AWS Glue for Spark jobs to AWS Glue version 6.0 in the AWS documentation.

Now available
AWS Glue 6.0 is generally available today in all AWS Regions where AWS Glue operates. For Regional availability and a future roadmap, visit the AWS Capabilities by Region. If you want to call APIs, search documentation, find regional availability, and check troubleshooting about this new feature, try using the AWS MCP Server and plugins with your preferred AI tool.

You pay an hourly rate, billed by the second, for crawlers (discovering data) and extract, transform, and load (ETL) jobs (processing and loading data). For the AWS Glue Data Catalog, you pay a simplified monthly fee for storing and accessing the metadata. The first million objects stored are free, and the first million accesses are free. To learn more, visit AWS Glue Pricing page.

Give it a try in the AWS Glue Studio console, and send feedback to AWS re:Post for AWS Glue or through your usual AWS support contacts.

Channy

Using Microsoft Graph and Powershell to Mine for Information – Stale Accounts and Licenses, (Thu, Aug 20th)

This post was originally published on this site

Microsoft Graph is a newer API that is meant to replace several others.  OK, it's at version 2.3.9, so it's not all that new, but it's new enough that lots of folks (and commercial tools) aren't using it yet.   It allows you to Get and Set info from/to M365, Entra Users and Entra managed machines for starters.  Let's dig in!

Simple Scans for Cloud Metadata Service, (Wed, Aug 19th)

This post was originally published on this site

Cloud providers typically expose a REST API at 169.254.169.254 that allows code running on virtual machines to retrieve machine-specific data. Some of the data is more or less harmless, such as the region the machine is running in or its MAC and IP addresses. However, the service may also be used to retrieve credentials for IAM roles and service account tokens.