// Ruby for Pentesters · Part 1
Your first recon tool
Install Ruby 4, meet irb, and write your very first script — an HTTP client that fingerprints a target. We point it at the retired HackTheBox machine Bizness and confirm what's running.
Welcome. Over this series you’re going to learn Ruby the way you’ll actually use it as a pentester: by writing your own tooling and pointing it at a real target. That target is Bizness, a retired HackTheBox machine. By the last part you’ll have a single Ruby program — and then a proper Metasploit module — that carries the whole chain from a blank terminal to a root shell.
No prior Ruby needed. This first part is the foundation: get the language running, learn how a Ruby program is shaped, and write a script that talks HTTP and tells us what the box is running.
Everything here is against a retired lab machine you own a connection to. Build tools, point them at things you’re allowed to touch.
Getting Ruby 4
The site you’re reading runs on Ruby 4, so let’s match it. Check what you have:
$ ruby -v
ruby 4.0.6 (2025-12-25 revision ...) [x86_64-linux]
If that prints a 4.x version, you’re set. If not, install it with your
platform’s version manager (rbenv install 4.0.6, mise use ruby@4.0.6, or
your distro’s package). The exact patch level doesn’t matter for this series;
anything on the 4.0 line is fine.
irb: your scratchpad
Ruby ships with an interactive shell called irb. It runs code as you type it, which makes it the fastest way to try something. Launch it and do some math, then make a string:
$ irb
irb(main):001> 40 + 2
=> 42
irb(main):002> "hello".upcase
=> "HELLO"
Two things to notice. First, everything is an expression — 40 + 2 and
"hello".upcase both return a value (irb prints it after the =>). Second,
"hello" is an object, and .upcase is a method you call on it. In
Ruby, even a plain string knows how to do things. You’ll lean on that constantly.
Type exit to leave irb.
Your first script
irb is for experiments; real tools live in files. Open a file called
recon.rb and start with one line:
puts 'hello from Ruby'
puts (“put string”) prints its argument and a newline. Run the file:
$ ruby recon.rb
hello from Ruby
That’s the whole loop you’ll use forever: write to a .rb file, run it with
ruby. If you’ll run it directly a lot, add a shebang as the first line so
the shell knows how to execute it:
#!/usr/bin/env ruby
puts 'hello from Ruby'
Then chmod +x recon.rb and you can run ./recon.rb.
Variables and strings
A variable is a name for a value. No type declarations — you just assign:
target = 'bizness.htb'
puts "scanning #{target}"
That #{...} inside a double-quoted string is interpolation: Ruby evaluates
the expression and drops the result into the string. It’s cleaner than gluing
strings with +, and you’ll use it in every payload you build later.
Talking HTTP
Recon starts with a simple question: what answers when I knock? Ruby’s
standard library has an HTTP client built in — no gems to install. Pull it in
with require:
require 'net/http'
require 'uri'
target = URI('https://bizness.htb/')
http = Net::HTTP.new(target.host, target.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # lab box uses a self-signed cert
response = http.get('/')
puts "Status: #{response.code} #{response.message}"
puts "Server: #{response['server']}"
Walking it line by line:
require 'net/http'loads the HTTP client;require 'uri'loads the URL parser.URI('https://bizness.htb/')turns the string into a URL object, so we can ask it for.host(bizness.htb) and.port(443) instead of splitting strings by hand.Net::HTTP.new(host, port)creates a client.use_ssl = truebecause it’s HTTPS. Lab boxes serve self-signed certificates, soverify_mode = VERIFY_NONEtells Ruby not to reject the connection — the real-world equivalent ofcurl -k.http.get('/')sends the request and hands back a response object.response.code,.message, andresponse['server']read the status and a header back off that object.
Run it:
$ ruby recon.rb
Status: 200 OK
Server: nginx/1.18.0 (Ubuntu)
An nginx reverse proxy — but that’s just the front door. The interesting part is what’s behind it.
When the knock goes unanswered
A real target isn’t always up. If the host is down, the port is filtered, or the
VPN dropped, http.get doesn’t return a response — it raises an exception,
and your script dies with a backtrace. A recon tool should expect that and say
something useful instead. Wrap the risky call in begin/rescue:
begin
response = http.get('/')
rescue StandardError => e
abort "[-] #{target.host} is not answering: #{e.message}"
end
begin ... rescue ... endruns the code and catches any error raised inside.StandardErroris the parent of the everyday failures — a refused connection, a timeout, a DNS miss — so onerescuecovers them;=> ebinds the exception object so we can reade.message.abortprints to stderr and exits non-zero, which is how a command-line tool should fail.
Exceptions are Ruby’s error channel. You’ll rescue them any time you touch the
network, the filesystem, or anything that can fail out from under you — which, in
this line of work, is most things.
Fingerprinting the app
The response body is the HTML the server sent. Let’s look for a tell. Add:
body = response.body
if body.include?('Apache OFBiz')
puts '[+] Apache OFBiz detected'
else
puts '[-] OFBiz not found in body'
end
response.body is just a String, and String#include? returns true or
false — a boolean — depending on whether the substring is present. We feed
that to an if/else to branch. Run it again:
$ ruby recon.rb
Status: 200 OK
Server: nginx/1.18.0 (Ubuntu)
[+] Apache OFBiz detected
There it is. Apache OFBiz — a big Java ERP suite — sitting behind nginx. That single fact decides the whole engagement: OFBiz has a well-known authentication-bypass CVE from 2023, and confirming the stack is what points us at it. You just wrote the tool that found it.
The finished script
Put together, your first recon tool:
#!/usr/bin/env ruby
require 'net/http'
require 'uri'
target = URI('https://bizness.htb/')
http = Net::HTTP.new(target.host, target.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
response = http.get('/')
puts "Status: #{response.code} #{response.message}"
puts "Server: #{response['server']}"
if response.body.include?('Apache OFBiz')
puts '[+] Apache OFBiz detected'
else
puts '[-] OFBiz not found in body'
end
Twenty lines, no dependencies, and it does something real.
Takeaways
- Ruby code lives in
.rbfiles you run withruby, or you experiment live inirb. Everything is an object, and you call methods on objects. putsprints; variables are just names you assign to;"#{...}"interpolation builds strings cleanly.net/http+urigive you a full HTTP client in the standard library —Net::HTTP.new,use_ssl,VERIFY_NONEfor lab certs,.get, then read.code/.bodyoff the response.- Anything that can fail — a request, a file — raises an exception; wrap it in
begin/rescueandabortwith a clear message instead of dumping a backtrace. - Recon is just asking a question and reading the answer. Ours found Apache OFBiz, and that’s the thread we pull next.
Exercises
Reading a tool isn’t writing one. Try these before moving on:
- Print the response’s
Content-Typeheader and the body’s length on one line with interpolation — e.g.text/html; charset=utf-8, 24187 bytes. - Add an
elsif: if the body doesn’t mention OFBiz but theServerheader containsnginx, report[?] nginx, unknown app. Only fall to the[-]branch when neither matches. - Point the script at a port nothing listens on
(
URI('https://bizness.htb:9999/')) and confirm yourbegin/rescueprints one clean line instead of a backtrace.
Next — Part 2: we turn that OFBiz finding into access, writing a Ruby function that walks straight through the authentication check.