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 thevagrant
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
- My github repo for Packer templates: https://github.com/zer0ttl/packer-templates