// Ruby for Pentesters · Part 7

Becoming root

The cracked password is root's too — plain password reuse. We spend it to escalate, and learn the handful of ways Ruby runs external programs — system, backticks, Open3 — ending with a PTY to drive an interactive su.

RubyPrivilege EscalationOpen3PTY
4 min read

Part 6 handed us monkeybizness — the OFBiz admin’s password. Here’s the payoff: on this box that’s also root’s login password. Password reuse is the most common privilege escalation there is, and it’s why cracking one hash so often ends the whole engagement. All that’s left is to su. We’ll do it from Ruby, which means learning how Ruby runs other programs — a toolkit you’ll use in every script you write.

On a box with no Ruby you’d just type su and the password by hand. The point here is the Ruby toolkit for launching programs; the escalation is the demo.

Three ways to run a command

Ruby gives you a ladder of options, from blunt to precise:

system('id')          # run it; output goes to the screen; returns true/false
output = `id`          # backticks: capture stdout as a string
output = %x[id]        # exactly the same as backticks, different delimiters

require 'open3'
stdout, stderr, status = Open3.capture3('id')
puts stdout if status.success?
  • system runs the command, lets its output flow to your terminal, and returns true/false for success — good when you only care whether it worked.
  • Backticks (and %x[], the same thing) run the command and hand back its stdout as a string — good when you want the output.
  • Open3.capture3 is the precise one: it returns stdout, stderr, and the exit status separately, so you can tell real output from error noise. Reach for it whenever failure matters.

That’s the whole spectrum. But none of them can escalate us — and it’s worth understanding why.

Why su is different

You’d think echo monkeybizness | su would work. It doesn’t. su reads its password straight from the terminal — the TTY — not from standard input, on purpose, so passwords can’t be piped in from a script. Backticks and Open3 hand the child a pipe, not a terminal, so su refuses them.

The fix is to give it a terminal — a fake one we control.

A pseudo-terminal with PTY

Ruby’s pty library allocates a pseudo-terminal, and its expect companion lets us wait for prompts and answer them — driving su exactly like a human would:

require 'pty'
require 'expect'

PTY.spawn('su - root') do |reader, writer, _pid|
  writer.sync = true
  reader.expect(/Password:/, 5)
  writer.puts 'monkeybizness'
  reader.expect(/[#$] /, 5)
  writer.puts 'id'
  puts reader.expect(/uid=\d+.*/, 5)&.first
  writer.puts 'exit'
rescue Errno::EIO
  # the child shell closed — normal once we exit
end
  • PTY.spawn('su - root') starts su under a pseudo-terminal and yields a reader/writer pair (plus the child pid) — the two ends of that terminal.
  • reader.expect(/Password:/, 5) blocks until su prints its prompt, giving up after 5 seconds (returning nil on timeout).
  • writer.puts 'monkeybizness' types the password; we wait for the root shell’s # prompt, run id, and read back the answer.
  • When the shell exits, the pseudo-terminal raises Errno::EIO — the normal way a PTY signals end-of-child, so we rescue it.

Run it:

$ ruby root.rb
uid=0(root) gid=0(root) groups=0(root)

uid=0(root) — root, reached entirely from Ruby, on the strength of one reused password. That’s the box.

Takeaways

  • Ruby runs external programs three ways: system (status, output to screen), backticks/%x[] (capture stdout), and Open3.capture3 (stdout, stderr, and status separately — the precise choice).
  • Some programs, su among them, demand a real terminal and ignore piped input — a pipe isn’t a TTY.
  • PTY.spawn allocates a pseudo-terminal and expect drives it: wait for a prompt, write a reply. It raises Errno::EIO when the child exits.
  • Password reuse is a first-class privesc: one cracked credential is worth trying against every account and service on the box.

Exercises

  1. Rewrite the id check with Open3.capture3 and print stderr only when the exit status isn’t successful.
  2. Wrap the escalation in a root? predicate that returns whether the PTY session reached uid=0, catching a wrong-password timeout as false.
  3. Generalize the PTY block into as_root(cmd) that escalates, runs one command as root, and returns its output — your own tiny su-runner.

Next — Part 8: eight scripts, one shape. We fold recon, bypass, RCE, and loot into a reusable class with a real command-line interface.

← All parts