During penetration tests, red team or purple team engagements, I have frequently encountered issues that extend beyond the scope of the exercise. These problems often involve broken tools, variations in Python versions installed, some obscure package missing for that one tool to run, necessitating a significant investment of time into troubleshooting. My desire was to have dependable machine images at my disposal, sparing me the effort of installing appropriate packages, tools, and performing initial setup for each new machine.

The Solution

Enter Packer, a solution that addresses these concerns. Packer is a tool designed to automate the creation of various types of machine images. It seamlessly integrates with frameworks like Ansible, Chef, Puppet to facilitate the installation and configuration of software within the images it generates. The resulting Packer images empower users to swiftly launch fully provisioned and configured machines within a matter of seconds.

I have seamlessly integrated Packer into the following workflows:

  • Detection Engineering: Packer plays a pivotal role in crafting machine images equipped with essential tools such as Sysmon, EDR agents, osquery, MITRE Caldera, and more, all preconfigured with specified settings like log forwarding and PowerShell logging. These images subsequently serve as the foundation for creating virtual machines (VMs) tailored for purple teaming exercises.
  • Penetration Tests: Packer proves its worth by creating a pristine, standardized image stocked with all necessary tools. This “golden image” serves as the basis for producing consistent results when performing pentests.
  • Red Team Engagements: For activities involving red teams, I employ Packer to generate images dedicated to redirectors and C2 control servers compatible with C2 frameworks like Caldera, Empire, and Havoc C2. These images can be used to promptly deploy VMs in cloud environments such as AWS or Azure.

In essence, Packer streamlines the process of producing reliable, ready-to-use machine images for a variety of scenarios, freeing up valuable time for focusing on the core objectives of security testing and assessments.

The Building Blocks

Packer templates offer the flexibility of being written in either JSON or HCL. Given my previous experience with HCL through Terraform, I opted to proceed using HCL.

A Packer template encompasses four core components, each playing a distinct role. Further details about Packer terminology are available here.

Builders

Builders take on the crucial task of crafting virtual machines and subsequently generating images across various platforms. Packer’s range of builders extends to encompass auxiliary functions, including the execution of provisioners.

In essence, Builders are responsible for orchestrating the creation of virtual machines and the subsequent transformation of these instances into images compatible with diverse platforms. For instance, a VirtualBox ISO builder will fashion a virtual machine using an ISO file and then export this virtual machine in OVF or OVA format.

Provisioners

Provisioners serve the purpose of leveraging both in-built and external software to install and configure the machine image post-boot. This signifies that once a virtual machine is constructed, it becomes possible to utilize scripts or third-party tools like Ansible, Chef, or Puppet to carry out the installation and configuration within the virtual environment. To illustrate, following the creation of an Ubuntu virtual machine, one could execute shell scripts to install packages, create user accounts, and perform other actions.

Post-Processors

While not mandatory, these post-processors offer an avenue for actions such as artifact uploads, file re-packaging, and more. For instance, it becomes feasible to export a provisioned Ubuntu virtual machine as a Vagrant box through the utilization of post-processors.

Parrot OS

For solving hackthebox machines and CTFs, I rely on Parrot OS. In my current setup, I utilize a server running VirtualBox, within which I initiate VMs using Vagrant. My intention was to construct a Vagrant box image tailored to Parrot OS. While a Vagrant box exists for Kali Linux, I encountered challenges in locating a trustworthy source for a Parrot OS box. Furthermore, it’s a better idea to build your own tools.

Packer Template

The packer block furnishes Packer with essential insights regarding the active version and the requisite plugins.

packer {
  required_version = ">= 1.7.0"
  required_plugins {
    vagrant = {
      version = ">= 1.0.2"
      source  = "github.com/hashicorp/vagrant"
    }
    virtualbox = {
      version = ">= 0.0.1"
      source  = "github.com/hashicorp/virtualbox"
    }
  }
}

Subsequently, variable blocks are employed to define variables. Though not mandatory, this practice is recommended for enhanced organization.

variable "iso_url" {
  type = string
  description = "Path for the iso file"
}

variable "cpus" { 
  type = string
  description = "Number of CPUs to be assigned to the vm"
  default = "2"
}

variable "memory" {
  type = string
  description = "Amount of RAM to be assigned to the vm"
  default = "2048"
}

Within the source block, builder configuration settings are delineated. These sources are subsequently referred to within a build block. To realize the creation of the Parrot VM, we employ a virtualbox plugin builder. The source block essentially constructs the VM with the prescribed settings. The boot_command outlines the steps needed for installing the OS from the ISO. The vboxmanage option can also be utilized to provide additional commands to VirtualBox.

source "virtualbox-iso" "parrot-os" {
  boot_command = [
    "<down><wait2s><enter>", // install
    "<wait20s><enter>", // langugage
    "<wait2s><enter>", // location
    "<wait2s><enter>", // keyboard
    // Some output omitted
  ]
  boot_wait = "10s"
  disk_size = 40000
  guest_additions_path = "VBoxGuestAdditions_{{.Version}}.iso"
  guest_os_type = "${var.guest_os_type}"
  http_directory = "./http"
  iso_checksum = "${var.iso_checksum}"
  iso_url = "${var.iso_url}"
  shutdown_command = "${var.shutdown_command}"
  ssh_username = "${var.ssh_username}"
  ssh_password = "${var.ssh_password}"
  ssh_port = 22
  ssh_wait_timeout = "40m"
  headless = "${var.headless}"
  gfx_vram_size = 128
  hard_drive_interface = "${var.virtualbox_hdd_interface}"
  vboxmanage = [
    ["modifyvm", "{{.Name}}", "--memory", "${var.memory}"],
    ["modifyvm", "{{.Name}}", "--cpus", "${var.cpus}"],
    ["modifyvm", "{{.Name}}", "--vrdeaddress", "${var.vrde_address}"],
    ["modifyvm", "{{.Name}}", "--default-frontend", "headless"],
  ]
}

Following this is the build block, which encapsulates details about builders, provisioning methodologies, and necessary post-processing steps.

build {
  sources = ["source.virtualbox-iso.parrot-os"]
  provisioner "shell" {
    execute_command = "echo 'vagrant' | {{.Vars}} sudo -S  sh {{.Path}}"
    scripts = [
      "scripts/base.sh",
      "scripts/vagrant.sh",
      "scripts/ssh.sh",
      "scripts/cleanup.sh",
      "scripts/zerodisk.sh"
    ]
  }

  post-processor "vagrant" {
    output = "${var.distro}-${var.version}-x64-virtualbox.box"
  }
}

In this context, the shell provisioner is utilized to execute scripts within the VM after the OS installation finishes.

  • base.sh - Perform system updates.
  • vagrant.sh - Establish the vagrant user for Vagrant usage.
  • ssh.sh - Configure SSH settings on the box.
  • cleanup.sh - Remove outdated packages.
  • zerodisk.sh - Zero out the unused disk space for improved Vagrant box packaging.

Upon the execution of the provisioning scripts, the post-processor Vagrant exports the VM in the box format.

==> virtualbox-ovf.parrot-os: Gracefully halting virtual machine...
==> virtualbox-ovf.parrot-os: Preparing to export machine...
    virtualbox-ovf.parrot-os: Deleting forwarded port mapping for the communicator (SSH, WinRM, etc) (host port 2732)
==> virtualbox-ovf.parrot-os: Exporting virtual machine...
    virtualbox-ovf.parrot-os: Executing: export packer-parrot-os-1692021920 --output output-parrot-os/packer-parrot-os-1692021920.ovf
==> virtualbox-ovf.parrot-os: Cleaning up floppy disk...
==> virtualbox-ovf.parrot-os: Deregistering and deleting imported VM...
==> virtualbox-ovf.parrot-os: Running post-processor:  (type vagrant)
==> virtualbox-ovf.parrot-os (vagrant): Creating a dummy Vagrant box to ensure the host system can create one correctly
==> virtualbox-ovf.parrot-os (vagrant): Creating Vagrant box for 'virtualbox' provider
    virtualbox-ovf.parrot-os (vagrant): Copying from artifact: output-parrot-os/packer-parrot-os-1692021920-disk001.vmdk
    virtualbox-ovf.parrot-os (vagrant): Copying from artifact: output-parrot-os/packer-parrot-os-1692021920.ovf
    virtualbox-ovf.parrot-os (vagrant): Renaming the OVF to box.ovf...
    virtualbox-ovf.parrot-os (vagrant): Compressing: Vagrantfile
    virtualbox-ovf.parrot-os (vagrant): Compressing: box.ovf
    virtualbox-ovf.parrot-os (vagrant): Compressing: metadata.json
    virtualbox-ovf.parrot-os (vagrant): Compressing: packer-parrot-os-1692021920-disk001.vmdk
Build 'virtualbox-ovf.parrot-os' finished after 48 minutes 31 seconds.

==> Wait completed after 48 minutes 31 seconds

==> Builds finished. The artifacts of successful builds are:
--> virtualbox-ovf.parrot-os: 'virtualbox' provider box: parrotos-5.3-Electro-Ara-x64-virtualbox.box

Running Packer

Packer build can be run using the following commands.

packer build -var-file=variables.pkvars.hcl parrotos.pkr.hcl

The variables.pkvars.hcl file contains the necessary variables for the build process.

iso_checksum = "sha256:93b2d751a4ca4aaf162ce400b66ec3e1b2aaf7cd258909009119323e584f8d46" // architect-edition
iso_url = "https://deb.parrot.sh/parrot/iso/5.3/Parrot-architect-5.3_amd64.iso" // architect-edition

guest_os_type = "Linux_64"
cpus = "4"
memory = "8192"
ssh_username = "vagrant"
ssh_password = "vagrant"

Vagrant

Following the creation of the Vagrant box, integration with Vagrant is achieved through the vagrant box add command.

vagrant box add ./parrotos-5.3-Electro-Ara-x64-virtualbox.box --name parrot-security-5.3
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'parrot-security-5.3' (v0) for provider:
    box: Unpacking necessary files from: file:///mnt/hdd01/projects/learn-packer/parrot-os-ovf/parrotos-5.3-Electro-Ara-x64-virtualbox.box
==> box: Successfully added box 'parrot-security-5.3' (v0) for 'virtualbox'!

Once integrated, a straightforward Vagrantfile can be created, and the vagrant up command initiates the VM.

Vagrant.configure("2") do |config|
  config.vm.box = "parrotos-5.3"
  config.vm.guest = "debian"
end

Challenges Faced

Preseed File Installation

Despite my attempts, I encountered difficulties while attempting to install the OS through a preseed file.

Vagrant OS Detection Issue

Executing the vagrant up command resulted in an error during the virtual machine’s operating system detection process. As Parrot OS is derived from Debian OS, resolving this required configuring the guest type within the Vagrantfile.

Vagrant.configure("2") do |config|
  config.vm.box = "parrotos-5.3"
  config.vm.guest = "debian"
end

In Conclusion

Mastering Packer can be accomplished within a few days. This tool proves invaluable for constructing consistent and reproducible VMs, which find utility in testing novel attack techniques, formulating new detections, or even automating a detection pipeline.

I trust you’ve found value in this article and gained new insights from it!

Code

References

Packer Terminology

Packer Plugins

Packer HCL Template

Parrot OS