Reply attack (tấn công phát lại) là một loại tấn công mạng trong đó kẻ tấn công chặn và tái sử dụng một gói dữ liệu đã được gửi trong một phiên giao dịch hợp lệ trước đó để lừa hệ thống thực hiện lại hành động mà gói dữ liệu đó yêu cầu. Kẻ tấn công không cần phải giải mã hay hiểu nội dung của gói tin, mà chỉ cần phát lại nó để đạt được mục đích.
Ví dụ về tấn công Reply attack:
-
Thanh toán trực tuyến: Kẻ tấn công chặn gói dữ liệu khi một người dùng gửi yêu cầu thanh toán (gồm thông tin về tài khoản, số tiền, và các dữ liệu xác thực khác) tới server. Sau đó, kẻ tấn công phát lại yêu cầu đó đến server, khiến server thực hiện một lần thanh toán khác mà không cần sự đồng ý của người dùng.
-
Đăng nhập: Kẻ tấn công chặn yêu cầu đăng nhập từ người dùng hợp lệ và phát lại yêu cầu này để có thể truy cập vào hệ thống mà không cần biết mật khẩu hoặc thông tin xác thực.
Quá trình của một cuộc tấn công Reply attack:
- Chặn gói tin: Kẻ tấn công lắng nghe mạng (thường thông qua một kỹ thuật như “man-in-the-middle”) để chặn một gói tin hợp lệ từ một phiên giao dịch.
- Tái sử dụng gói tin: Sau đó, kẻ tấn công gửi lại gói tin này đến server mà không cần thay đổi bất kỳ dữ liệu nào, nhưng yêu cầu đó được server coi là một yêu cầu hợp lệ.

Replay attack
Các phương pháp chống lại reply attack
Dưới đây là tổng hợp các cách chống lại reply attack, mình dùng client bằng JavaScript và server bằng Python để minh họa. Các phương pháp chủ yếu xoay quanh việc đảm bảo mỗi yêu cầu từ client đến server là duy nhất và không thể bị sử dụng lại.
1. Sử dụng Timestamp (Dấu thời gian)
Nguyên tắc: Mỗi request sẽ chứa một timestamp và server sẽ kiểm tra xem request đó có cũ hay không.
Client (JavaScript):
// Client-side JavaScript
function sendRequest(data) {
const timestamp = Date.now(); // Get current timestamp in milliseconds
const payload = {
data: data,
timestamp: timestamp
};
fetch('/api/endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
}
Server (Python):
from flask import Flask, request, jsonify
import time
app = Flask(__name__)
TIME_THRESHOLD = 60 # Time threshold in seconds
@app.route('/api/endpoint', methods=['POST'])
def handle_request():
data = request.get_json()
client_timestamp = data.get('timestamp')
current_time = time.time() * 1000 # Current time in milliseconds
# Check if the timestamp is within the acceptable threshold
if abs(current_time - client_timestamp) > TIME_THRESHOLD * 1000:
return jsonify({'error': 'Request is too old'}), 400
# Process the valid request
return jsonify({'message': 'Request is valid'})
Giải thích:
- Client sẽ gửi đi một timestamp hiện tại khi tạo request.
- Server sẽ so sánh thời gian nhận request và thời gian trong timestamp, nếu khoảng cách quá xa (giả sử lớn hơn 60 giây), server sẽ từ chối.
Đến đây có bạn lại thắc mắc là vậy trong khoảng thời gian 60 giây
hacker vẫn có thể tấn công được? Chúng ta cùng đến giải pháp tiếp theo.
2. Sử dụng Nonce (Số ngẫu nhiên duy nhất)
Nguyên tắc: Nonce là một giá trị ngẫu nhiên chỉ sử dụng một lần. Mỗi request sẽ chứa một nonce để đảm bảo tính duy nhất.
Client (JavaScript):
// Function to generate a random nonce
function generateNonce() {
return Math.random().toString(36).substring(2);
}
function sendRequestWithNonce(data) {
const nonce = generateNonce();
const payload = {
data: data,
nonce: nonce
};
fetch('/api/nonce-endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
}
Server (Python):
from flask import Flask, request, jsonify
app = Flask(__name__)
used_nonces = set()
@app.route('/api/nonce-endpoint', methods=['POST'])
def handle_nonce_request():
data = request.get_json()
nonce = data.get('nonce')
# Check if the nonce has been used before
if nonce in used_nonces:
return jsonify({'error': 'Nonce has already been used'}), 400
# Save the nonce to prevent future reuse
used_nonces.add(nonce)
# Process the valid request
return jsonify({'message': 'Request with nonce is valid'})
Giải thích:
- Client sẽ tạo ra một nonce ngẫu nhiên và gửi cùng với request.
- Server lưu trữ tất cả các nonce đã sử dụng để đảm bảo rằng không có request nào bị dùng lại. Thông thường sử dụng
redis
để lưu trữ.
3. Sử dụng HMAC (Hash-based Message Authentication Code)
Nguyên tắc: HMAC sử dụng một khóa bí mật chung để tạo mã hóa cho nội dung request. Server có thể xác minh tính toàn vẹn của request bằng cách tính lại HMAC và so sánh với kết quả từ client.
Client (JavaScript):
// Function to create HMAC signature
function generateHMAC(data, secret) {
return CryptoJS.HmacSHA256(JSON.stringify(data), secret).toString();
}
function sendRequestWithHMAC(data) {
const secretKey = 'shared-secret-key';
const hmacSignature = generateHMAC(data, secretKey);
const payload = {
data: data,
signature: hmacSignature
};
fetch('/api/hmac-endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
}
Server (Python):
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
SECRET_KEY = b'shared-secret-key'
def verify_hmac(data, signature):
calculated_signature = hmac.new(SECRET_KEY, msg=data.encode(), digestmod=hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated_signature, signature)
@app.route('/api/hmac-endpoint', methods=['POST'])
def handle_hmac_request():
data = request.get_json()
message = data.get('data')
signature = data.get('signature')
# Verify HMAC signature
if not verify_hmac(message, signature):
return jsonify({'error': 'Invalid HMAC signature'}), 400
# Process the valid request
return jsonify({'message': 'HMAC verification successful'})
Giải thích:
- Client sử dụng khóa bí mật chung để tạo HMAC dựa trên nội dung request.
- Server tính toán lại HMAC từ dữ liệu nhận được và so sánh với HMAC từ client để xác minh tính hợp lệ của request.
4. Sử dụng JWT (JSON Web Token)
Nguyên tắc: JWT là một tiêu chuẩn mã hóa thông tin trong JSON và ký tên bằng một khóa bí mật. Server có thể kiểm tra tính hợp lệ của JWT mà không cần lưu trữ trạng thái session.
Client (JavaScript):
function sendRequestWithJWT(data, token) {
const payload = {
data: data,
token: token
};
fetch('/api/jwt-endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => console.log('Response:', data))
.catch(error => console.error('Error:', error));
}
Server (Python):
from flask import Flask, request, jsonify
import jwt
app = Flask(__name__)
SECRET_KEY = 'shared-secret-key'
@app.route('/api/jwt-endpoint', methods=['POST'])
def handle_jwt_request():
data = request.get_json()
token = data.get('token')
try:
# Decode JWT token
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return jsonify({'message': 'JWT verification successful', 'data': decoded})
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token has expired'}), 400
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 400
Giải thích:
- JWT chứa payload được ký bằng khóa bí mật và server có thể xác minh tính hợp lệ của token mà không cần lưu trữ trạng thái.
Tổng kết:
- Sử dụng timestamp và nonce là cách dễ dàng và phổ biến để chống lại reply attack.
- HMAC và JWT cung cấp một lớp bảo mật cao hơn khi có khả năng xác minh tính toàn vẹn và tính xác thực của dữ liệu.
Trong các dự án thực tế các bạn nên sử dụng kết hợp các phương pháp trên để tăng hiệu quả bảo mật.
P/S: trong phương pháp dùng HMAC, secret_key
được chia sẻ và lưu trữ tại client
điều này rất không bảo mật. Vậy chúng ta khắc phục như thế nào, các bạn cùng thảo luận nhé.