Tấn công Path Traversal & LFI: Kỹ thuật khai thác và phòng thủ trong lập trình an toàn

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Sau sáu tháng vận hành một micro-service phục vụ file trên môi trường production — và chứng kiến hai chiến dịch quét Path Traversal riêng biệt làm bùng sáng access log — tôi đã xây dựng lại tầng validation đầu vào từ đầu. Bài viết này đề cập đến cơ chế tấn công, các thủ thuật bypass mà hacker thực sự sử dụng, và các mẫu phòng thủ đã chứng minh được hiệu quả.

Bắt đầu nhanh (5 phút)

Path Traversal và Local File Inclusion (LFI) có cùng nguyên nhân gốc rễ. Ứng dụng lấy tên file từ đầu vào người dùng và truyền trực tiếp vào lệnh gọi filesystem mà không kiểm tra trước. Kẻ tấn công chèn các chuỗi ../ để thoát khỏi thư mục dự định và truy cập file tùy ý.

Phát hiện hoạt động quét đang diễn ra trong log chỉ cần một lệnh:

grep -E '(\.\./|%2e%2e%2f|%252e%252e|\.\.\ %2f|%c0%ae)' /var/log/nginx/access.log | tail -20

Các mục tiêu điển hình mà kẻ tấn công nhắm đến đầu tiên:

  • /etc/passwd — liệt kê người dùng
  • /etc/shadow — hash mật khẩu (yêu cầu quyền đọc root)
  • /proc/self/environ — biến môi trường, thường chứa secrets
  • /var/log/nginx/access.log — đầu độc log để leo thang LFI-to-RCE
  • Các file cấu hình ứng dụng: .env, config.php, database.yml

Nếu ứng dụng của bạn phục vụ file hoặc include template dựa trên tham số tên file do người dùng cung cấp, bạn đang có một bề mặt tấn công tiềm năng. Hãy kiểm tra những endpoint đó trước.

Phân tích sâu: Cách tấn công thực sự hoạt động

Path Traversal cơ bản

Hãy xem đoạn code Python Flask này — đại diện cho kiểu viết tắt hay xuất hiện trong production:

from flask import Flask, request, send_file

app = Flask(__name__)

@app.route('/download')
def download():
    filename = request.args.get('file')
    return send_file(f'/var/www/uploads/{filename}')

Một request đến /download?file=../../etc/passwd sẽ được resolve thành /var/www/uploads/../../etc/passwd, và OS sẽ chuẩn hóa thành /etc/passwd. Đó là toàn bộ cuộc tấn công — không cần công cụ phức tạp nào.

LFI: Khi Include trở thành vector tấn công

Local File Inclusion là anh em gần của Path Traversal, nhưng thay vì phục vụ nội dung file trực tiếp, ứng dụng thực thi hoặc include nó. Đây là lãnh địa cổ điển của PHP:

<?php
$page = $_GET['page'];
include("templates/" . $page . ".php");
?>

Gửi request ?page=../../../etc/passwd%00 (null byte injection) sẽ cắt bỏ phần đuôi .php và include file passwd — điều này hoạt động trên PHP trước phiên bản 5.3.4. PHP đã vá lỗi xử lý null byte trong các hàm filesystem từ năm 2010. Tuy nhiên, pattern include không an toàn vẫn xuất hiện trong các codebase cũ chạy stack lỗi thời.

Kỹ thuật mã hóa và bypass filter

Hầu hết các biện pháp phòng thủ đơn giản chỉ chặn chuỗi literal ../. Kẻ tấn công có cả bộ công cụ mã hóa để vượt qua:

  • URL encoding: %2e%2e%2f giải mã thành ../
  • Double URL encoding: %252e%252e%252f — server giải mã một lần thành %2e%2e%2f, ứng dụng giải mã lần nữa thành ../
  • Unicode overlong encoding: %c0%ae%c0%ae/ (IIS cũ và Java containers)
  • Mixed slashes: ..\ — chuẩn hóa đường dẫn Windows
  • Redundant separators: ....// — khi ứng dụng loại bỏ ../ trong một lần duyệt, ....// sẽ thu gọn thành ../ sau khi xóa
# Các payload bypass thường thấy trong log
curl "https://example.com/view?file=%2e%2e%2f%2e%2e%2fetc%2fpasswd"
curl "https://example.com/view?file=....//....//etc/passwd"
curl "https://example.com/view?file=%252e%252e%252fetc%252fpasswd"

LFI leo thang thành Remote Code Execution qua đầu độc Log

Đầu độc log là hình thức leo thang đáng sợ nhất. Nếu kẻ tấn công có thể include một file log mà chúng đã ghi code PHP vào, chúng sẽ có remote code execution. Đây là chuỗi các bước:

  1. Gửi một request với PHP payload trong header User-Agent: User-Agent: <?php system($_GET['cmd']); ?>
  2. Chuỗi đó được ghi vào /var/log/nginx/access.log
  3. Khai thác LFI để include access log: ?page=../../var/log/nginx/access.log
  4. PHP interpreter thực thi log entry đã bị nhiễm độc

Ba vector leo thang khác cũng đáng lưu ý. /proc/self/environ — chèn PHP payload vào HTTP header rồi include file đó. File session PHP tại /var/lib/php/sessions/sess_SESSIONID — có thể khai thác nếu session lưu dữ liệu do người dùng kiểm soát. Và ảnh tải lên có PHP payload ẩn trong metadata EXIF — một thủ thuật phổ biến khi validation upload file chỉ kiểm tra phần đuôi.

Nâng cao: Các mẫu lập trình phòng thủ thực sự hiệu quả

Cách sửa đúng: realpath() + kiểm tra tiền tố Allowlist

Lọc dựa trên chuỗi rất dễ bị vượt qua bởi các biến thể mã hóa. Cách tiếp cận đúng là dùng chính OS để resolve đường dẫn chuẩn, sau đó kiểm tra với tiền tố đã biết là an toàn:

import os
from flask import Flask, request, send_file, abort

app = Flask(__name__)
UPLOAD_DIR = os.path.realpath('/var/www/uploads')

@app.route('/download')
def download():
    filename = request.args.get('file', '')
    # realpath() giải quyết tất cả chuỗi ../ và symlinks
    requested_path = os.path.realpath(os.path.join(UPLOAD_DIR, filename))

    # Kiểm tra tiền tố nghiêm ngặt — kẻ tấn công không thể thoát khỏi UPLOAD_DIR
    if not requested_path.startswith(UPLOAD_DIR + os.sep):
        abort(403)

    if not os.path.isfile(requested_path):
        abort(404)

    return send_file(requested_path)

os.path.realpath() giải quyết tất cả symlink và chuỗi ../ trước khi kiểm tra chạy. Không dùng blacklist, không regex — chỉ là sự thật tuyệt đối ở tầng OS. Hậu tố os.sep trên tiền tố ngăn chặn một thư mục tên /var/www/uploads-secret vượt qua kiểm tra startswith.

Tương đương trong PHP

<?php
$base_dir = realpath('/var/www/templates');
$requested = realpath($base_dir . '/' . $_GET['page']);

// realpath() trả về false cho đường dẫn không tồn tại — luôn kiểm tra
if ($requested === false || strpos($requested, $base_dir . DIRECTORY_SEPARATOR) !== 0) {
    http_response_code(403);
    exit('Bị cấm');
}

include $requested;
?>

Mẫu Node.js / Express

const path = require('path');
const fs = require('fs');
const express = require('express');
const app = express();

const STATIC_DIR = path.resolve('/var/www/files');

app.get('/file', (req, res) => {
  const requested = path.resolve(STATIC_DIR, req.query.name || '');

  if (!requested.startsWith(STATIC_DIR + path.sep)) {
    return res.status(403).send('Bị cấm');
  }

  if (!fs.existsSync(requested) || !fs.statSync(requested).isFile()) {
    return res.status(404).send('Không tìm thấy');
  }

  res.sendFile(requested);
});

Nginx: Chặn Path Traversal tại edge

Code ứng dụng không nên là tuyến phòng thủ duy nhất của bạn. Các quy tắc Nginx này chặn các pattern traversal tại edge — trước khi request đến ứng dụng:

server {
    # Chặn các pattern path traversal phổ biến tại edge
    location ~* (\.\.\/|\.\.\\|%2e%2e%2f|%252e%252e) {
        return 403;
    }

    location /uploads {
        alias /var/www/uploads/;
        # Từ chối các loại file thực thi trong thư mục upload
        location ~* \.(php|py|sh|exe|pl)$ {
            deny all;
        }
    }
}

Mẹo thực tế từ kinh nghiệm thực chiến

Dùng allowlist map tường minh thay vì xây dựng đường dẫn. Nếu bạn đang xây dựng hệ thống template hoặc trang, hãy duy trì một map cứng các tên hợp lệ đến đường dẫn file. Đầu vào người dùng chọn một key từ map — không bao giờ chạm vào việc xây dựng đường dẫn:

TEMPLATES = {
    'home': 'templates/home.html',
    'about': 'templates/about.html',
    'contact': 'templates/contact.html',
}

template_name = request.args.get('page', 'home')
template_path = TEMPLATES.get(template_name)
if template_path is None:
    abort(404)

Chạy dưới user ít quyền. Ngay cả khi traversal thành công, một tiến trình chạy với tư cách www-data không có quyền đọc /etc/shadow hay các bí mật ứng dụng sẽ giới hạn thiệt hại. Tôi lưu các bí mật production trong biến môi trường và hạn chế quyền file cấu hình về 600.

Làm cứng thông tin xác thực trên mọi dịch vụ. Credentials có thể bị truy cập qua traversal không nên được tái sử dụng ở bất kỳ nơi nào khác — đó là nguyên tắc cơ bản để giảm thiểu phạm vi thiệt hại. Để tạo tài khoản và mật khẩu dịch vụ, tôi dùng công cụ tạo tại toolcraft.app/vi/tools/security/password-generator. Nó chạy hoàn toàn trên trình duyệt, nên mật khẩu không bao giờ được truyền qua mạng. Đáng quan tâm: ngay cả bước tạo mật khẩu cũng là bề mặt rò rỉ nếu bạn dùng công cụ online gửi dữ liệu lên server.

Kiểm tra phòng thủ của bạn trước khi kẻ tấn công làm. Chạy thử nghiệm LFI nhanh trên staging:

for payload in '../etc/passwd' '../../etc/passwd' '%2e%2e%2fetc%2fpasswd' '....//etc/passwd'; do
    echo -n "Đang kiểm tra [$payload]: "
    curl -s "https://staging.example.com/view?file=$payload" | head -1
done

Nếu bất kỳ phản hồi nào chứa root:, bạn có một lỗ hổng mở cần vá trước khi triển khai.

Cảnh báo về các pattern traversal trong log production. Một cron job đếm số lần xuất hiện ../ mỗi giờ và kích hoạt cảnh báo khi tỷ lệ tăng vọt là đủ để phát hiện các chiến dịch quét sớm. Cả hai chiến dịch tôi đề cập ban đầu đều xuất hiện dưới dạng đột ngột tăng 10 lần trong một giờ — rõ ràng ngay khi bạn có hệ thống giám sát.

Path Traversal tiếp tục xuất hiện trong các báo cáo bug bounty vì một lý do: code có lỗ hổng rất dễ viết vô tình. send_file(f'/uploads/{filename}') trông vô hại. Kiểm tra realpath() + tiền tố cũng đơn giản để thêm vào. Hãy tìm mọi vị trí trong codebase của bạn nơi đầu vào người dùng được đưa vào lệnh gọi filesystem, áp dụng mẫu này, và toàn bộ loại tấn công này sẽ ngừng hoạt động.

Share: