Trong các đợt Redteam việc duy trì quyền truy cập đến máy chủ là thiết yếu để có thể tấn công sâu hơn vào hệ thống cũng như thực hiện các mục đích khác nhau, sử dụng webshell là một trong những phương pháp phổ biến nhất thông qua khai thác các lỗ hổng ứng dụng Web. Việc này đòi hỏi phải tải các file webshell lên server để thực thi command vì vậy phương pháp này cũng dễ bị phát hiện hơn, do đó cần có những kỹ thuật nhằm che giấu webshell tránh việc bị phát hiện cũng như gây khó khăn trong quá trình bóc gỡ của Blueteam.

Bài viết này mình sẽ giới thiệu đến ba loại jsp webshell level memory trên môi trường java/apache tomcat bằng cách lợi dụng một số thành phần của nó như listener, filter hay servlet. Memshell sau khi được inject vào memory luôn có thể truy cập được mà không cần file shell tồn tại trên server do đó khó bị phát hiện hơn, cho đến khi server bị restart. Trước tiên cùng tìm hiểu listener, filter hay servlet là gì.

Tổng quan ba thành phần trong java web

Listener

Theo dõi các sự kiện khởi tạo hay hủy bỏ của những đối tượng như application, session hay request để thực thi các đoạn mã tương ứng với các sự kiện.

Filter

Khi request đến servlet và trả về response có thể đi qua một hoặc nhiều filter(còn gọi là filter chain). Filter có thể thực hiện kiểm tra, sửa đổi các thuộc tính của request hay response.

Servlet

Là module chính đóng vai trò như lớp trung gian giữa client và application phía server. Servlet tiếp nhận client request, xử lý rồi sau đó gửi trả response.

Ngoài ra, để hiểu rõ hơn cách tomcat xử lí một file jsp bạn có thể tham khao qua link sau.

Thực hiện tạo jsp memory webshell

Listener type

Ví dụ thêm listener trong tomcat

ListenerTest.java
web.xml

ServletRequestListener có chức năng lắng nghe sự khởi tạo hay hủy bỏ của ServletRequest object và invoke hai method tương ứng là requestInitialized() và requestDestroyed() mỗi khi request được thực hiện. Lợi dụng class này ta có thể override lại hai method trên để gọi lệnh thực thi OS command, ở đây mình sẽ override method requestInitialized().

Đầu tiên, để lấy được các tham số truyền vào từ request thì method requestInitialized() có nhận parameter là một ServletRequestEvent nhờ đó có thể lấy được ServletRequest thông qua ServletRequestEvent#getServletRequest().
Tiếp đến, để có thể trả về output của command được thực thi cần có HttpServletResponse để gửi trả response. Tuy nhiên, mặc định thì không thể lấy được object này từ class ServletRequestListener, do đó ta cần tạo một custom listener có constructor nhận argument là một HttpServletResponse object.

Vấn đề tiếp theo là làm sao để thêm custom listener trên vào tomcat trong runtime.

Thử debug thì thấy khi request đi qua StandardContext#fireRequestInitEvent() thì method getApplicationEventListeners() sẽ lấy tất cả listeners được định nghĩa ở file web.xml rồi kiểm tra từng object có phải instanceof class ServletRequestListener hay không, nếu đúng thì gọi method requestInitialized() như dòng 3752

Thêm vào đó StandardContext có method addApplicationEventListener(T) mà ta có thể dùng để thêm custom listener:

Cuối cùng, làm sao để lấy được StandardContext từ request.

  1. Đầu tiên có thể lấy ServletContext từ request bằng cách dùng request.getServletContext(), thật ra method này không trực tiếp trả về ServletContext mà trả về ApplicationContextFacade, cái implements ServletContext

  2. Hơn nữa, class ApplicationContextFacade có thuộc tính (ApplicationContext)context và class ApplicationContext lại có thuộc tính (StandardContext)context. Tuy nhiên có thể thấy các thuộc tính này đều là private nên ta cần dùng cơ chế java reflection để có thể lấy được.

Cuối cùng, ta được listener-type memory webshell sau:

Upload webshell lên server, inject listener:

Sau khi inject có thể xóa file jsp này, webshell lúc này đã được load vào memory của tomcat và vẫn có thể thực thi command một cách bình thường:

Từ đây ListenerShell#requestInitialized() sẽ được gọi ở mọi request, nếu request có đúng param name như đã tự định nghĩa – ở đây là cmd_secret thì command sẽ được thực thi và response trả về ouput, nếu không request sẽ tiếp tục một cách bình thường.

Filter type

Ví dụ thêm filter trong tomcat:

FilterTest.java
web.xml

Request khi đi qua một Filter sẽ gọi đến method doFilter() để thực hiện filter tasks. Đặt breakpoint tại FilterTest#doFilter ta được đoạn stack frames đáng chú ý sau:

StandardWrapperValve#invoke()
    -> StandardWrapperValve#filterChain.doFilter()
        -> ApplicationFilterChain#this.internalDoFilter()
            -> ApplicationFilterChain#filter.doFilter()
                -> FilterTest#doFilter()

Có thể thấy tại StandardWrapperValve#invoke() thì filterChain.doFilter() sẽ được gọi, biến filterChain thì được khởi tạo ở factory.createFilterChain():

Đi vào method này:

Ở đây method sẽ lấy list filterMaps từ context.findFilterMaps() dựa vào cấu hình ở file web.xml, các filterMap chứa toàn bộ thông tin về filter-mapping bao gồm cả filterName và urlPatterns:

List filterMaps này sau đó sẽ đi vào vòng lặp for để so sánh urlPattern của từng filterMap có match với requestPath của request hiện tại, nếu đúng sẽ lấy filterConfig từ filterMap này và cuối cùng đi vào filterChain.addFilter(filterConfig).

Cấu trúc của (ApplicationFilterConfig)filterConfig như sau, chú ý member filterDef là object chứa các thông tin về filter như filterName hay filterClass:

Trở lại với StandardWrapperValve#invoke(), filterChain sẽ bắt đầu thực thi method doFilter() của từng filter:

Do vậy, để thêm một custom filter:

  1. Tạo FilterDef object chứa thông tin về custom filter muốn thêm.
  2. Thêm FilterDef  vào StandardContext và regenerate lại filterConfig bằng StandardContext#filterStart()

3. Thêm tiếp FilterMap chứa thông tin về mapping vào StandardContext

Cuối cùng, ta được filter-type memory webshell như sau:

Upload webshell lên tomcat, inject filter:

Tương tự, sau khi inject có thể xóa file jsp đi:

Servlet type

Ví dụ thêm servlet trong tomcat:

ServletTest.java
web.xml

Thực hiện debug xem sự thay đổi của StandardContext sau khi load các servlet:

Trong StandardContext có member children là một HashMap với các item là StandardWrapper wrap các servlet.

Một member khác là servletMappings cũng là một HashMap với key – value là mapping giữa các urlPattern và servlet:

Tóm lại, để thêm một custom servlet ta cần:

  1. Tạo StandardWrapper object wrap custom servlet cần thêm
  2. Thêm StandardWrapper object vào member children của StandardContext
  3. Thêm mapping giữa urlPattern và custom servlet vào member servletMapping của StandardContext

Ta được servlet-type memory webshell sau:

Upload webshell, inject servlet:

Xóa file jsp, khi requestPath match với urlPattern tự định nghĩa ở trên thì sẽ gọi đến ServletShell thực thi command:

Kết

Qua bài viết này mình đã giới thiệu ba loại memory webshell, ngoài ra cũng còn nhiều dạng khác cho các nền tảng khác hay ho không kém.
Loại webshell này khó phát hiện hơn bởi muốn phân tích phải đi vào tiến trình JVM, bên cạnh đó cũng sẽ có những kỹ thuật anti-detection nhưng bài viết này đã khá dài nên mình chưa thể đề cập thêm.

Tham khảo

1.559 lượt xem