一、概念了解及环境搭建
1. 一些新概念
- PyCharm是一种Python IDE,带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具,比如调试、语法高亮、Project管理、代码跳转、智能提示、自动完成、单元测试、版本控制。此外,该IDE提供了一些高级功能,以用于支持Django框架下的专业Web开发。
- IDE: 集成开发环境(Integrated Development Environment)是用于提供程序开发环境的应用程序。简单来说就是提高程序员开发效率的东西。
- web服务器, 应用服务器:处理逻辑的服务器,处理php,python,不能直接通过nginx这种web服务器来处理。eg, uwsgi处理Python;
- 常见web应用框架: flask, django
2. 环境搭建
工具准备:python3 & PyCharm
1.flask搭建过程
1.安装python3虚拟环境
1 | python3 -m venv venv --> 安装虚拟环境(第二个venv指虚拟环境的名字) |
安装虚拟环境主要是为了不让flask测试框架所安装的一些python模块, 影响自己本机的python环境。
1 | pip install Flask --> pip版本过低 |
2.PyCharm使用:
File –> New Project 创建一个新项目
File –> Settings –> Project:Project Interpreter 设置python路径, 然后选择pip然后可以安装python相应模块
参考文章:
- 《pycharm使用教程看这篇就足够了》 https://www.jianshu.com/p/2a4d388b86e9
二、flask框架学习
一、用flask写一个hello world:
1 | from flask import Flask |
- 导入
Flask
类 - 创建一个该类的实例
- 用
route()
装饰器来告诉Flask
触发函数的URL
- 函数名称被用来生成相关联的URL
- 最后返回需要在用户浏览器中显示的信息
二、让这个 “hello world” 跑起来的方法
powershell
1
2env:FLASK_APP="flaskr" --> flaskr指项目名称
flask runcmd
1
2
3set FLASK_APP=flaskr --> flaskr指项目名称
set FLASK_ENV=development
flask runlinux
1
2export FLASK_APP=flaskr --> flaskr指项目名称
flask run
三、如果使用PyCharm:
如果是使用PyCharm来运行flask的话, 就会很简单。代码写好之后直接右键选择run, 就可以运行了
参考文章:
- 参考教程w3cschool https://www.w3cschool.cn/flask/flask_overview.html
- flask框架参考flask中文文档 https://dormousehole.readthedocs.io/en/latest/
if __name__ == '__main__'
的理解参考《如何简单地理解python中的if __name__ == '__main__'》
https://blog.csdn.net/yjk13703623757/article/details/77918633/
三、解题过程
题目源码如下:
1 | import random |
分析题目源码
该题目定义了两个路由
@app.route('/', methods=['GET', 'POST'])
@app.route('/syc')
1. 配置里的flag
路由 @app.route('/', methods=['GET', 'POST'])
作用为:
如果是POST请求,则将用户输入(nickname)赋值给参数p。否则直接返回index.html
如果参数p中(即nickname中)含有 .
, _
, \'
(点号, 下划线, 单引号)则返回值: Your nickname contains restricted characters!
如果参数p中不包含非法字符, 则在数组nicknames中随机取出一个元素, 将用户输入(nickname)格式化输出。并且在输出时受到 render_template_string()
函数的影响
对于 render_template_string()
函数
查阅flask官方文档可知, 在Flask中, Jinja2默认配置如下
- 当使用 render_template() 时,扩展名为 .html 、 .htm 、 .xml 和 .xhtml 的模板中开启自动转义。
- 当使用 render_template_string() 时,字符串开启 自动转义。
- 在模板中可以使用
{% autoescape %}
来手动设置是否转义。- Flask 在 Jinja2 环境中加入一些全局函数和辅助对象,以增强模板的功能。
也就是说, render_template_string()
函数会将字符串以Jinja2模板的渲染规则进行转义。
查阅Jinja2官方文档可知, 在Jinja2中, 默认定界符(delimiters)有如下几种:
{% ... %}
for Statements{{ ... }}
for Expressions to print to the template output{# ... #}
for Comments not included in the template output# ... ##
for Line Statements
简单来说:
{% ... %}
是用来控制程序流程的定界符, 例如条件语句, 循环语句。使用语法与python类似, 但要注意在语法结束时要添加{% endif %}
,{% endfor %}
…。一个简单的利用for
循环, 遍历username
的示例如下1
2
3
4
5
6<h1>Members</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>{{ ... }}
是允许使用基本表达式的定界符, 并将结果输出
例如:{{ 7*7 }}
的返回值就是49
{# ... #}
用于注释一整块语句, 注释用法如下:1
2
3
4
5{# note: commented-out template because we no longer use this
{% for user in users %}
...
{% endfor %}
#}# ... ##
用于注释一行语句, 注释用法如下:1
2
3# for item in seq:
<li>{{ item }}</li> ## this comment is ignored
# endfor
参考flask模板 https://dormousehole.readthedocs.io/en/latest/templating.html?highlight=render_template_string#id2
缺省情况下,以下全局变量可以在 Jinja2 模板中使用:
config: 当前配置对象
request: 当前请求对象, 在没有活动请求环境情况下渲染模板时,这个变量不可用。
session: 当前会话对象, 在没有活动请求环境情况下渲染模板时,这个变量不可用。
g: 请求绑定的全局变量, 在没有活动请求环境情况下渲染模板时,这个变量不可用。
既然config是全局变量, 而 {{ ... }}
可以将基本表达式的结果输出, 并且 render_template_string()
会将字符串以Jinja2的规则转义。那是否可以利用 render_template_string()
先将 "{{ ... }}"
字符串转义成Jinja2模板格式, 而转义之后就成了 {{ ... }}
定界符, 再利用这个定界符输出 config 这个全局变量呢。
于是我搭建了一个测试环境, 代码跟输出结果如下:
1 | from flask import Flask, render_template_string |
很好, 可以看到成功读取到了 config 全局变量。
感觉到第一个flag就在眼前!
访问题目, 输入 {{ config }}
是flag!
不过有点遗憾的是, flag是加了密的。回到题目中, 再仔细看看代码。找到加密flag的部分:
1 | def encode(line, key, key2): |
观察 encode()
函数, 发现返回值是由 x
, ord(line[x])
, ord(key[::-1][x])
, ord(key2[x])
四部分亦或得到。
亦或参考文章
1):https://www.cnblogs.com/tmdsleep/p/9933647.html
2):http://www.elecfans.com/d/653831.html
通过学习异或运算可知:
如果a、b两个值不相同,则异或结果为1。 如果a、b两个值相同,异或结果为0。 异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。
而关于异或运算, 有一个推论
e = a ^ b ^ c ^ d 与
b = a ^ c ^ d ^ e 相等效
又查询资料可知
ord()是将字符转换成ASCII;
chr()是将ASCII转换成字符
在加密flag的时候, 是将flag作为参数line传入。也就是说 for x in range(len(line))
的作用是获取flag中每一个字符的坐标, ord(line[x])
就是将flag中每一个字符转换成ASCII。
即加密flag部分, 可以简单理解为:密文 = x ^ flag ^ ord(key[::-1][x]) ^ ord(key2[x])
根据推论解密flag, 即:flag = x ^ 密文 ^ ord(key[::-1][x]) ^ ord(key2[x])
解密脚本:
1 | def encode(encrypt_flag, key, key2): |
解码得到第一个flag:Syc{this_1s_Twice_interv1ew_F14g}
其实仔细看一下, 构造的解密脚本其实和原加密脚本一样。
2. 根目录下的flag
任务中说, 第二个flag在根目录下面, 于是想到利用模板注入漏洞达到命令执行的效果, 从而读取到根目录的文件。
查阅跟模板注入的有关文档可知
- https://xz.aliyun.com/t/3679
- https://www.kingkk.com/2018/06/Flask-Jinja2-SSTI-python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/
想要利用模板注入来达到命令执行的效果, 需要调用python的基类
首先利用 __class__
查询全局变量 config
的参数类型, 即 {{ config.__class__ }}
。执行查询, 结果如下。
感到疑惑, 再回到题目源码。看到在 @app.route('/', methods=['GET', 'POST'])
路由里, 过滤了参数 nickname 中的 _
和.
。调用python基类所必须的下划线被过滤了, 所以查询全局变量 config
的类失败了。
既然第一个路由已经把路封上了, 这时候去读第二个路由 @app.route('/syc')
的源码。发现过滤了 .
, '
, "
。点号也被过滤了, 感觉题目已经无解了。这时候想到, 虽然调用基类的下划线是必须的, 绕不过去, 但是是否可以绕过点号的使用。百度查询很久, 没有发现任何绕过点号的方法。然后在群里看到师傅说, 把点号的限制删除了。这时候又可以继续做题了!
分析 @app.route('/syc')
这个路由, 利用 request.args.get('name')
以GET方式传入了一个name参数, 再通过 render_template_string()
函数将name以Jinja2模板的渲染规则进行转义。
先尝试读取全局变量: http://114.116.44.23:58470/syc?name={{ config }}
, 成功得到config的值。
这时候发现, 对于配置里的flag, 不仅可以通过第一个路由读取, 而且第二个路由也可以读取到这个配置里的flag。
继续读取根目录flag之路:
再次尝试读取全局变量 config
的参数类型, http://114.116.44.23:58470/syc?name={{ config.__class__ }}
得到返回结果 <class 'flask.config.Config'>
然后利用 __init__
初始化类
再加 __globals__
全局查询所有的方法及变量及参数http://114.116.44.23:58470/syc?name={{ config.__class__.__init__.__globals__ }}
全局查询之后, 发现有python3的os模块(在题目中也调用了os模块执行系统命令)。
查询官方文档之后, 得知 os
模块执行系统命令有两种方式 os.system()
和 os.popen()
其中 os.system()
无法获得输出和返回值, 所以这里采取 os.popen()
方法。使用 read()
方法可以看到输出结果。即 os.popen().read()
这时候一个完整的命令执行payload如下:http://114.116.44.23:58470/syc?name={{ config.__class__.__init__.__globals__['os'].popen('whoami').read() }}
这时候想到, 单引号是被过滤了的, 需要绕过单引号的过滤。查询资料得知, request.args是Flask中的一个属性,为返回请求的参数。我把 module
作为调用模块的变量名, 将调用的os模块传入。而shell
作为执行的命令的变量名, 将要执行的命令传入。
这时候构造的payload如下:http://114.116.44.23:58470/syc?name={{ config.__class__.__init__.__globals__[request.args.module].popen(request.args.shell).read() }}&module=os&shell=whoami
尝试执行, 得到结果如下:
命令能够成功执行了, 接下来就是拿flag了
列下目录:
很好, 看到flag了。
…居然是假flag, 再看一下题目描述, flag是在服务器根目录。再去根目录下读一下flag。
拿到真flag了Syc{this_2s_Twice_interv1ew_F14g}