2025强网杯web
SecretVault
基础知识补充
了解
SQLAlchemy库的一个关键用法:db.relationship比如以下题目代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26db = SQLAlchemy()
#这是进行了继承吗,是的进行了继承。
class User(db.Model):#定义模型,每个模型类代表数据库中的一张表。
"""类里面的变量都叫元素"""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)#盐和密码
salt = db.Column(db.String(64), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)#时间
vault_entries = db.relationship('VaultEntry', backref='user', lazy=True, cascade='all, delete-orphan')
u = User.query.get(1)#自己加的
entries = u.vault_entries
class VaultEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
label = db.Column(db.String(120), nullable=False)
login = db.Column(db.String(120), nullable=False)
password_encrypted = db.Column(db.Text, nullable=False)
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
v = VaultEntry.query.get(1)#自己加的
owener = v.user这里需要讲解一个概念:即把两个数据库中表连接起来。
怎么连接
逻辑上习惯在母表中(我们以本题母表元素对子表是一对多的情况为例)写入以下代码:
1
vault_entries = db.relationship('VaultEntry', backref='user', lazy=True, cascade='all, delete-orphan')
- 首先该代码为目标的类添加了一个属性
vault_entries, - 其次,在
db.relationship方法后的括号中第一个参数的名字就是你要连接的子表的名字! - 最后,就是
backref参数,该参数是站在子表的角度看的,子表的目标,即子表的”主人”就是”user”,这里因为代码解析的原因,定义User表的时候按类的写法,首字母大写,但是在写参数的时候就直接统一全小写。
- 首先该代码为目标的类添加了一个属性
对于子表,我们需要写这样一行代码:
1
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
这里
db.ForeignKey('user.id')即代表连接哪个表的哪个属性,即User表的id属性
连接效果
从User表的id查另外一个User表对应id相应的元素(比如一个人的密码)
1
2u = User.query.get(1)
entries = u.vault_entries#这里为属性名子表,密码表,得知一个密码,查该密码对应目标哪个用户:
1
2v = VaultEntry.query.get(1)
owener = v.user#这里就是backref的值,'user'。
其他flask数据库基础知识解惑
什么是字段
- 就是一张表里的一个字段,它专门用来指向另一张表的主键
我们创建类到底创建了个什么
我们定义一个继承自
db.Model的类,只是定义了这个数据库应该长什么样子。1
2
3
4
5
6
7
8class User(db.Model):#定义模型,每个模型类代表数据库中的一张表。
"""类里面的变量都叫元素"""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)#盐和密码
salt = db.Column(db.String(64), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)#时间
vault_entries = db.relationship('VaultEntry', backref='user', lazy=True, cascade='all, delete-orphan')之后进行实例化:
1
2
3
4
5
6
7
8user = User(
id=0,
username='admin',
password_hash=password_hash,
salt=base64.b64encode(salt).decode('utf-8'),
)
db.session.add(user)
db.session.commit()这只是按照表中每行元素的格式创建了一个新元素,然后依次采用
db.session.add(user)和db.session.commit()对表进行数据添加和更新。
装饰器自定义
题目解答流程
寻找关键点
寻找关键字
flag
如下:
针对该段代码我们可以开始分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24if not User.query.first():#前提就是没有用户,即初始化的时候会添加一个用户
salt = secrets.token_bytes(16)
password = secrets.token_bytes(32).hex()
password_hash = hash_password(password, salt)
user = User(
id=0,
username='admin',
password_hash=password_hash,
salt=base64.b64encode(salt).decode('utf-8'),
)
db.session.add(user)
db.session.commit()
flag = open('/flag').read().strip()
flagEntry = VaultEntry(
user_id=user.id,#为0,写死了,含义就是,只有id=0的user才对应flag的数据。
label='flag',
login='flag',
password_encrypted=fernet.encrypt(flag.encode('utf-8')).decode('utf-8'),#加密flag的内容
notes='This is the flag entry.',
)
db.session.add(flagEntry)
db.session.commit()
以上代码大概关键点有:
if not User.query.first():#前提就是没有用户,这段含义既是,系统初始化时(因为没有用户)会执行以下程序。- 首先在
User表中创建id=0的admin数据。 - 其次,把flag文件进行读取,获取flag的值,将flag加密为密钥存于flagEntry这一数据中,并且flagEntry有且仅有与user_id=0的user数据关联,因为我说的这句话包含的运行逻辑,只会出现在初始化中,所以当User数据库有其他数据的时候,不会执行flagEntry数据与user数据的绑定。
- 得出结论,flagEntry的值与id=0的user数据相绑定。此时继续看,怎么把vault呈现。
寻找关键字
vault,我们可以得到,该路由定义函数中有对密钥解码的行为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#寻找我们所需要的数据库,即vault可以被呈现的路由。这段函数装饰器下面的函数就是单纯一个渲染作用,把user对应的秘钥渲染出来到前端
#所以,这个登录识别修饰器很关键。我们需要id=0的用户。
def dashboard():
user = g.current_user#g相当于是一个只存在于http请求一瞬间的记事本,里面可以写很多元素。
entries = [#所以要查清楚g.current_user是谁给他赋值的,值是多少。
{#所以我们往@login_required里面去找它的来源。
'id': entry.id,
'label': entry.label,
'login': entry.login,
'password': fernet.decrypt(entry.password_encrypted.encode('utf-8')).decode('utf-8'),#这里把密文解码为明文
'notes': entry.notes,
'created_at': entry.created_at,
}
for entry in user.vault_entries#该user对应的所有vault表中的密码。如果可以把admin user的对应密码信息爆出即可
]#这里for循环看起来是反的,其实是列表推导式
"""entries = [] for entry in user.vault_entries,这样就一清二楚了"""
return render_template('dashboard.html', username=user.username, entries=entries)
该段代码主要逻辑就是在,该函数会将user对应的entry数据中的password解码,由于id=0的user所对应的password解码即为flag,所以我们就要去找,该user是由谁决定的
顺藤摸瓜,该user是受g.current_user控制的。
下一步就是去找g.current_user怎么来的,那线索就只能指向自定义的装饰器
@login_required
关于装饰器
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22def login_required(view_func):#定义该装饰器
def wrapped(*args, **kwargs):
uid = request.headers.get('X-User', '0')#这里接受的是反向代理请求头的X-user参数
print(uid)#如果反向代理头中无X-user,那就用0;有的话,用X-user的值
if uid == 'anonymous':
flash('Please sign in first.', 'warning')
return redirect(url_for('login'))
try:#那么X-user不存在的话,uid值就是0
uid_int = int(uid)
except (TypeError, ValueError):
flash('Invalid session. Please sign in again.', 'warning')
return redirect(url_for('login'))
user = User.query.filter_by(id=uid_int).first()#uid为0即可
if not user:
flash('User not found. Please sign in again.', 'warning')
return redirect(url_for('login'))
g.current_user = user#这里对g.current_user进行了赋值
return view_func(*args, **kwargs)
return wrapped我们可以看出user是由uid决定的,代码会根据uid在User数据库中查询对应user,所以uid为0即可
uid怎么为0?关键代码:
1
uid = request.headers.get('X-User', '0')#这里接受的是反向代理请求头的X-user参数
即,如果在请求头找到了
X-User字段,那就将uid定义为对应字段的值,如果没找到,就定义为0,所以我们就要让后端收不到有X-User字段的请求头
go代理
我们需要明白,要多个心,这类题目可能有反向代理。
代理和反向代理的区别:- 代理是你自己配置的,你明确知道代理存在,你主动配置代理,如vpn
- 反向代理是你不知道的,服务器配置的,后端服务器 只信代理,而非你直接发过去的请求。
现实例子:- Nginx
- Cloudflare
- API Gateway
- 本题里的 Go 服务
所以本题中发送”删除X-user字段”想要实现,需要让代理发出的请求无该字段,而不是你自己从客户端发送的请求没这字段就行了
看如下源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func main() {
authorizer := &httputil.ReverseProxy{Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:5000"
uid := GetUIDFromRequest(req)
log.Printf("Request UID: %s, URL: %s", uid, req.URL.String())
req.Header.Del("Authorization")
req.Header.Del("X-User")
req.Header.Del("X-Forwarded-For")
req.Header.Del("Cookie")
if uid == "" {
req.Header.Set("X-User", "anonymous")
} else {
req.Header.Set("X-User", uid)
}
}}几个关键点:
ReverseProxy库
- 它的作用为:自动执行 HTTP 协议规定必须执行的规则
- 官方定位:
ReverseProxy实现了一个 HTTP 反向代理,
用于把客户端请求转发到后端服务器,
并返回响应。 - 它其中有一个功能就是:
removeHopByHopHeaders(req.Header),该函数的作用就是:代理在转发请求前,必须移除逐跳头。
这是遵守了http协议的规则:即代理在转发请求前,必须移除逐条头 - TTP/1.1 规定了哪些是逐跳头?
包括但不限于:
ConnectionKeep-AliveProxy-AuthenticateProxy-AuthorizationTETrailerTransfer-EncodingUpgrade
并且:
Connection头中列出的字段名,
也都必须被视为逐跳头并移除。
利用http协议
即:删除标准逐跳头
我们可以制造以下payload:
1
curl -H "Connection:close,X-User""题目网址/dashboard"
含义即为:
给发去该网站的请求加上”Connection:close,X-User”字段
即:
命令代理端要删除X-User字段才可以继续向后端发送请求所以,这样,我们就可以向后端发送无
X-User的请求。
最终payload
如下:
1
curl -H "Connection:close,X-User""题目网址/dashboard"
可获取flag




