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
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.
Đầu tiên có thể lấy
ServletContext
từ request bằng cách dùngrequest.getServletContext()
, thật ra method này không trực tiếp trả vềServletContext
mà trả vềApplicationContextFacade
, cái implementsServletContext
Hơn nữa, class
ApplicationContextFacade
có thuộc tính(ApplicationContext)context
và classApplicationContext
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:
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:
- Tạo
FilterDef
object chứa thông tin về custom filter muốn thêm. - Thêm
FilterDef
vàoStandardContext
và regenerate lạifilterConfig
bằngStandardContext#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:
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:
- Tạo
StandardWrapper
object wrap custom servlet cần thêm - Thêm
StandardWrapper
object vào memberchildren
củaStandardContext
- Thêm mapping giữa urlPattern và custom servlet vào member
servletMapping
củaStandardContext
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.