[Pentesterlab write-up] Web For Pentester II - Authorization & Mass Assignment

Continuamos con dos bloques más del lab 'Web for Pentester II',  esta vez con ejercicios para:

- explotar malas implementaciones en el esquema de autorización del aplicativo, que permiten acceder a información (URLs) que debería estar restringida, al menos sin previa autenticación.

- manipular registros de la aplicación para modificar elementos a los que el usuario normalmente no debería tener acceso, como contraseñas, permisos o roles. Es lo que se conoce como Mass Assignment.

Como veréis estos ejercicios son bastante sencillitos (quizás demasiado) y no requieren apenas esfuerzo, pero os recomiendo echarle al menos un vistazo rápido.


AUTHORIZATION

Ejercicio 1:

El primero ejemplo es un fallo común en el desarrollo web, el desarrollador creó la página de inicio de sesión pero no bloqueó las páginas que tienen información sensible a través de cookies u otros tokens de seguridad, esto significa que cualquiera que sepa la ruta (o la obtenga mediante un crawling o fuzzing de directorios) puede acceder sin autenticación.

http://vulnerable/authorization/example1



En esta ocasión no hace falta ni tirarle un wfuzz o un dirb, la URL es totalmente predecible:

http://vulnerable/authorization/example1/infos/2

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

class AuthorizationExample1 < PBase

  def self.db
    "authorization_example1" 
  end



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

  SEED = "MagicS33d_AuthorizationExample1"
  class User < ActiveRecord::Base
    establish_connection AuthorizationExample1.db
  end

  class Info < ActiveRecord::Base
    establish_connection AuthorizationExample1.db
  end


  configure {
    recreate() if $dev
    ActiveRecord::Base.establish_connection AuthorizationExample1.db 
    unless ActiveRecord::Base.connection.table_exists?("#{db}.infos")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample1.db}.infos" do |t|
          t.string  :title
          t.text :details
        end
      end
    end

    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample1.db}.users" do |t|
          t.string  :username
          t.string :password
        end
      end
    end
 

    User.create(:username => 'user1', :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
    Info.create(:title => "Confidential", :details => "Do not share") 
    Info.create(:title => "Confidential 2", :details => "Do not redistribute") 
  }




  def self.path 
    "/authorization/example1/"
  end



  set :views, File.join(File.dirname(__FILE__), 'example1', 'views')
  use Rack::Session::Sequel
 
  get '/' do
    @infos = Info.all
    if session[:user] and User.find(session[:user])
      return erb :index
    end
    if params['username'] && params['password'] 
      @user = User.where( :username => params['username'].to_s,
                  :password => Digest::MD5.hexdigest(SEED+params['password']+SEED)).first
      if @user
        session[:user] = @user
        return erb :index
      end
    end  
    erb :login     
  end

  get "/logout" do
    session.clear
    redirect "#{AuthorizationExample1.path}"
  end
  
  get "/infos/:id" do 
    @info = Info.find(params[:id].to_s)
    erb :info
  end
end

Ejercicio 2:

En el siguiente ejercicio el desarrollador corrigió el problema anterior por lo que no accederemos al recurso si no nos hemos loggeado anteriormente. Sin embargo, lo que no hizo correctamente es bloquear el acceso a las páginas según el valor de la cookie o mediante cualquier otra manera, por lo que si iniciamos sesión podemos acceder a la información de cualquier otra cuenta de usuario.

http://vulnerable/authorization/example2/infos/2


http://vulnerable/authorization/example2/infos/3

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

class AuthorizationExample2 < PBase

  def self.db 
    "authorization_example2"
  end

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


  SEED = "MagicS33d_AuthorizationExample2"
  class User < ActiveRecord::Base
    establish_connection AuthorizationExample2.db
    has_many :infos
  end

  class Info < ActiveRecord::Base
    establish_connection AuthorizationExample2.db
    belongs_to :user
  end

  configure { 
    recreate() if $dev
    ActiveRecord::Base.establish_connection AuthorizationExample2.db 
    unless ActiveRecord::Base.connection.table_exists?("#{db}.infos")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample2.db}.infos" do |t|
          t.string  :title
          t.text :details
          t.integer :user_id
        end
      end
    end

    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample2.db}.users" do |t|
          t.string  :username
          t.string :password
        end
      end
    end
 

    user1 = User.create(:username => 'user1', :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
    user1.infos << Info.new(:title => "Confidential user1", :details => "Do not share") 
    user1.infos << Info.new(:title => "Confidential user1 (2)", :details => "Do not redistribute") 

    user2 = User.create(:username => 'user2', :password => Digest::MD5.hexdigest(SEED+"unkn0wn"+SEED))
    user2.infos << Info.new(:title => "Confidential user2", :details => "Do not share") 
    user2.infos << Info.new(:title => "Confidential user2 (2)", :details => "Do not redistribute") 
  }




  def self.path 
    "/authorization/example2/"
  end



  set :views, File.join(File.dirname(__FILE__), 'example2', 'views')
  use Rack::Session::Sequel
 
  get '/' do
    if params['username'] && params['password'] 
      @user = User.where( :username => params['username'].to_s,
                  :password => Digest::MD5.hexdigest(SEED+params['password']+SEED)).first
      if @user
        session[:user] = @user
        @infos = @user.infos
        return erb :index
      end    
    elsif session[:user]
      @user = User.find(session[:user]) 
      @infos = @user.infos
      return erb :index
    end
    erb :login     
  end

  get "/logout" do
    session.clear
    redirect "#{AuthorizationExample2.path}"
  end
  
  get "/infos/:id" do 
    if session[:user] and User.find(session[:user])
      @info = Info.find(params[:id].to_s)
      erb :info
    else
      session.clear
      redirect "#{AuthorizationExample2.path}"
    end
  end
end
Ejercicio 3: 
Este ejemplo sigue el hilo de los anteriores. Esta vez no podemos acceder a la información de otros usuarios... pero si podemos editarla. Sólo tenemos que ir a la página de edición de la cuenta user1 y cambiar el valor en la URL:
 
http://vulnerable/authorization/example3/infos/edit/1
  
http://vulnerable/authorization/example3/infos/edit/3
  
SERVIDOR
require 'sinatra/base'
require 'active_record'
require 'digest/md5'

class AuthorizationExample3 < PBase

  def self.db 
    "authorization_example3"
  end

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


  SEED = "MagicS33d_AuthorizationExample3"
  class User < ActiveRecord::Base
    establish_connection AuthorizationExample3.db
    has_many :infos
  end

  class Info < ActiveRecord::Base
    establish_connection AuthorizationExample3.db
    belongs_to :user
    attr_protected :user_id
  end

  configure { 
    recreate() if $dev
    ActiveRecord::Base.establish_connection AuthorizationExample3.db 
    unless ActiveRecord::Base.connection.table_exists?("#{db}.infos")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample3.db}.infos" do |t|
          t.string  :title
          t.text :details
          t.integer :user_id
        end
      end
    end

    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{AuthorizationExample3.db}.users" do |t|
          t.string  :username
          t.string :password
        end
      end
    end
 

    user1 = User.create(:username => 'user1', :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
    user1.infos << Info.new(:title => "Confidential user1", :details => "Do not share") 
    user1.infos << Info.new(:title => "Confidential user1 (2)", :details => "Do not redistribute") 

    user2 = User.create(:username => 'user2', :password => Digest::MD5.hexdigest(SEED+"unkn0wn"+SEED))
    user2.infos << Info.new(:title => "Confidential user2", :details => "Do not share") 
    user2.infos << Info.new(:title => "Confidential user2 (2)", :details => "Do not redistribute") 
  }




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



  set :views, File.join(File.dirname(__FILE__), 'example3', 'views')
  use Rack::Session::Sequel
 
  get '/' do
    if params['username'] && params['password'] 
      @user = User.where( :username => params['username'].to_s,
                  :password => Digest::MD5.hexdigest(SEED+params['password']+SEED)).first
      if @user
        session[:user] = @user
        @infos = @user.infos
        return erb :index
      end    
    elsif session[:user]
      @user = User.find(session[:user]) 
      @infos = @user.infos
      return erb :index
    end
    erb :login     
  end

  get "/logout" do
    session.clear
    redirect "#{AuthorizationExample3.path}"
  end
  
  get "/infos/:id" do 
    if session[:user] 
      @user =  User.find(session[:user]) 
      @info = Info.find(params[:id].to_s)
      if @user and @user.infos.include? @info
        erb :info
      else
        session.clear
        redirect "#{AuthorizationExample3.path}"
      end
    else
      session.clear
      redirect "#{AuthorizationExample3.path}"
    end
  end
 
  get "/infos/edit/:id" do 
    
    if session[:user] 
      @user =  User.find(session[:user]) 
      @info = Info.find(params[:id].to_s)
      if @user and @info
        erb :edit_info
      else
        session.clear
        redirect "#{AuthorizationExample3.path}"
      end
    else
      session.clear
      redirect "#{AuthorizationExample3.path}"
    end
  end

  get "/infos/update_info/:id" do 
    
    if session[:user] 
      @user =  User.find(session[:user]) 
      @info = Info.find(params[:id].to_s)
      if @user and @user.infos.include?(@info)
         @info.update_attributes(params[:info])
        @infos = @user.infos
        erb :index
      else
        session.clear
        redirect "#{AuthorizationExample3.path}"
      end
    else
      session.clear
      redirect "#{AuthorizationExample3.path}"
    end
  end


end

MASS ASSIGNMENT

Ejercicio 1:

Este es un ejemplo de lo que sería una escalada de privilegios en la aplicación web. Básicamente tenemos dos tipos de cuentas: el usuario estándar y el administrador. Capturando la petición y añadiendo el parámetro y varlo 'user[admin]=1' podemos conseguir el rol deseado:




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

class MassAssignExample1 < PBase
  def self.db
    "massassign_example1"
  end

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

  SEED = "MagicS33d_MassAssignExample1"

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


  configure {
    recreate() if $dev
    ActiveRecord::Base.establish_connection MassAssignExample1.db 

    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{MassAssignExample1.db}.users" do |t|
          t.string  :username
          t.string  :password
          t.integer :admin, :default => 0
        end
      end
    end
  }

  def self.path 
    "/massassign/example1/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example1', 'views')
 
  get '/' do
    erb :index    
  end

  get "/signup" do
    @user = User.create(params[:user])
    erb :user
  end

  get "/user/:id" do
    @user = User.find(params[:id])
    erb :user
  end



end

Ejercicio 2:

En esta ocasión no podemos crear una cuenta con privilegios de administrador, pero si podemos actualizar la información del usuario autenticado y configurarlo como admin de foma similar a la anterior:

http://vulnerable/massassign/example2/signup?user%5Busername%5D=test&user%5Bpassword%5D=test123&submit=Submit+Query




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


class MassAssignExample2 < PBase

  def self.db
    "massassign_example2"
  end

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

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

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

  configure {
    recreate() if $dev
    ActiveRecord::Base.establish_connection MassAssignExample2.db
    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{MassAssignExample2.db}.users" do |t|
          t.string  :username
          t.string  :password
          t.integer :admin, :default => 0
        end
      end
    end
  }

  def self.path 
    "/massassign/example2/"
  end

  set :views, File.join(File.dirname(__FILE__), 'example2', 'views')
 
  get '/' do
    erb :index    
  end

  get "/signup" do
    params[:user]["admin"] = 0 
    @user = User.create(params[:user])
    session[:user] = @user.id
    erb :user
  end

  get "/profile" do
    @user = User.find(session[:user].to_s)
    erb :user
  end


  get "/edit_profile" do
    @user = User.find(session[:user].to_s)
    erb :profile
  end

  get "/update_profile" do
    @user = User.find(session[:user].to_s)
    @user.update_attributes(params[:user])
    @user.save
    erb :user
  end



end

Ejercicio 3:

Por último, para acceder a la información de “Company 2” primero tendremos que visitar la página que permite cambiar la información de la cuenta, capturar la petición GET y agregar '&user%
5Bcompany_id%5D = 2':



http://vulnerable/massassign/example3/update_profile?user[username]=user1&user[password]=pentesterlab&user[company_id]=2&submit=Submit+Query


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


class MassAssignExample3 < PBase

  def self.db 
    "massassign_example3"
  end

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

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

  class User < ActiveRecord::Base
    establish_connection  MassAssignExample3.db
    belongs_to :company
  end

  class Company < ActiveRecord::Base
    establish_connection  MassAssignExample3.db
    has_many :users
  end

  configure {
    recreate() if $dev
    ActiveRecord::Base.establish_connection MassAssignExample3.db

    unless ActiveRecord::Base.connection.table_exists?("#{db}.users")
      ActiveRecord::Migration.class_eval do
        create_table "#{MassAssignExample3.db}.users" do |t|
          t.string  :username
          t.string  :password
          t.string  :company_id
        end
      end
    end

    unless ActiveRecord::Base.connection.table_exists?("#{db}.companies")
      ActiveRecord::Migration.class_eval do
        create_table "#{MassAssignExample3.db}.companies" do |t|
          t.string  :name
          t.text    :secret 
        end
      end
    end
    company1 = Company.create(:name => "Company 1", :secret => "Company 1 secret")
    company2 = Company.create(:name => "Company 2", :secret => "Company 2's secret, access not authorized for Company 1's users!!!")
     
    company1.users << User.create(:username => 'user1', :password => Digest::MD5.hexdigest(SEED+"pentesterlab"+SEED))
  }


  def self.path 
    "/massassign/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
        session[:user] = @user.id
        return erb :index
      end
    elsif session[:user]
      @user = User.find(session[:user])
      return erb :index
    end
    erb :login
  end

  get "/edit_profile" do
    @user = User.find(session[:user].to_s)
    erb :profile
  end

  get "/update_profile" do
    @user = User.find(session[:user].to_s)
    @user.update_attributes(params[:user])
    @user.save
    erb :index
  end



end

Y hasta aquí esta serie. Os emplazo a la siguiente y última entrada del laboratorio 'Web for Pentester II" donde veremos inyecciones SQL en MongoDB y algunas otras cosillas variadas.


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

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

Comentarios

  1. Estos ejercicios sin un panel detrás que nos conduzca a un RCE no resultan tan espectaculares, pero son imprescindibles.
    ¿Hay algún candidato a ser una nueva serie tras el WebForPentest2?

    ResponderEliminar
    Respuestas
    1. imagino que publicaremos algún writeup de Vulnhub pero se admiten sugerencias... ;)

      Eliminar
    2. Pues hombre, por sugerir... bWAPP es bastante completito.
      Creo que yo tenía por aquí un writeup de alguno de sus ejercicios. Si no lo perdí en una catástrofe digital que sufrí hace unos meses os lo haré llegar.

      Eliminar
  2. ¡Buen Write-UP! me a servido de hayuda!
    Muchas gracias por vuestra labor! se agradece mucho.
    ¡Un saludo desde Guyana Francesa fariseos!

    ResponderEliminar

Publicar un comentario