目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. OtenkiImp (Unsolved)
    2. 2.2. RceHouse (Unsolved)
    3. 2.3. ImpossibleUnser (Unsolved)
    4. 2.4. NoCommonCollections (Unsolved)
    5. 2.5. Ezdotnet (Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

DASCTF X HDCTF 2024

2024/6/2 CTF线上赛 python 反序列化 SSRF 非传统框架 .NET
  |     |   总文章阅读量:

前言

das偶遇b神出题,java&.net&python强如怪物,拼尽全力也无法战胜😭(也就是又爆零了)

应该是近期最后一次打ctf了,剩下的事交给复现了

官方wp:https://www.yuque.com/yuqueyonghu30d1fk/gd2y5h/ksfsfw8yf1u2xhhq?singleDoc#


Web

OtenkiImp (Unsolved)

python aiohttp框架

又是百分百晴天女

f12发现/hint

from aiohttp import web
import time
import json
import base64
import pickle
import time
import aiomysql
from settings import config, messages


async def mysql_init(app):
    mysql_conf = app['config']['mysql']
    while True:
        try:
            mysql_pool = await aiomysql.create_pool(host=mysql_conf['host'],
                                                    port=mysql_conf['port'],
                                                    user=mysql_conf['user'],
                                                    password=mysql_conf['password'],
                                                    db=mysql_conf['db'])
            break
        except:
            time.sleep(5)
    app.on_shutdown.append(mysql_close)
    app['mysql_pool'] = mysql_pool
    return app


async def mysql_close(app):
    app['mysql_pool'].close()
    await app['mysql_pool'].wait_closed()


async def index(request):
    with open("./static/index.html", "r", encoding="utf-8") as f:
        html = f.read()
    return web.Response(text=html, content_type="text/html")


async def waf(request):
    return web.Response(text=messages[0], status=403)


def check(string):
    black_list = [b'R', b'i', b'o', b'b', b'V', b'__setstate__']
    white_list = [b'__main__', b'builtins', b'contact', b'time', b'dict', b'reason']
    try:
        s = base64.b64decode(string)
    except:
        return False
    for i in white_list:
        s = s.replace(i, b'')
    for i in black_list:
        if i in s:
            return False
    return True


async def getWishes(request):
    wishes = []
    id = request.query.get("id")
    try:
        pool = request.app['mysql_pool']
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                try:
                    id = str(int(id))
                    sql = 'select id,wish from wishes where id={id}'.format(
                        id=id)
                except:
                    sql = 'select id,wish from wishes'
                await cur.execute(sql)
                datas = await cur.fetchall()
    except:
        return web.Response(text=messages[1])
    for (id, wish) in datas:
        if check(wish):
            wishes.append(pickle.loads(base64.b64decode(wish)))
    return web.Response(text=json.dumps(wishes), content_type="application/json")


async def addWishes(request):
    data = {}
    if request.query.get("contact") and request.query.get("place") and request.query.get("reason") and request.query.get("date") and request.query.get("id"):
        data["contact"] = request.query.get("contact")
        data["place"] = request.query.get("place")
        data["reason"] = request.query.get("reason")
        data["date"] = request.query.get("date")
        data["timestamp"] = int(time.time()*1000)
        id = request.query.get("id")
        wish = base64.b64encode(pickle.dumps(data))
    else:
        return web.Response(text=messages[3])
    try:
        pool = request.app['mysql_pool']
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                sql = 'insert into wishes(`id`, `wish`) values ({id}, "{wish}")'.format(
                    id=id, wish=wish.decode())
                await cur.execute(sql)
                return web.Response(text=messages[2])
    except:
        return web.Response(text=messages[1])


async def rmWishes(request):
    try:
        pool = request.app['mysql_pool']
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                sql = 'delete from wishes'
                await cur.execute(sql)
                return web.Response(text=messages[2])
    except:
        return web.Response(text=messages[1])


async def hint(request):
    with open(__file__, 'r') as f:
        source = f.read()
    return web.Response(text=source)

if __name__ == '__main__':
    app = web.Application()
    app['config'] = config
    app.router.add_static('/static', path='./static')
    app.add_routes([web.route('*', '/', index),
                    web.route('*', '/waf', waf),
                    web.route('*', '/addWishes', addWishes),
                    web.get('/getWishes', getWishes),
                    web.post('/rmWishes', rmWishes),
                    web.get('/hint', hint)])
    app = mysql_init(app)
    web.run_app(app, port=5000)

注意到aiohttp/3.8.4

尝试CVE-2024-23334失败,因为这里是app.router.add_static('/static', path='./static'),没开follow_symlinks

审一下代码

最终利用点应该是pickle反序列化,即getWishes方法里的wishes.append(pickle.loads(base64.b64decode(wish)))

async def getWishes(request):
    wishes = []
    id = request.query.get("id")
    try:
        pool = request.app['mysql_pool']
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                try:
                    id = str(int(id))
                    sql = 'select id,wish from wishes where id={id}'.format(
                        id=id)
                except:
                    sql = 'select id,wish from wishes'
                await cur.execute(sql)
                datas = await cur.fetchall()
    except:
        return web.Response(text=messages[1])
    for (id, wish) in datas:
        if check(wish):
            wishes.append(pickle.loads(base64.b64decode(wish)))
    return web.Response(text=json.dumps(wishes), content_type="application/json")

接下来往前看条件,首先是check方法

def check(string):
    black_list = [b'R', b'i', b'o', b'b', b'V', b'__setstate__']
    white_list = [b'__main__', b'builtins', b'contact', b'time', b'dict', b'reason']
    try:
        s = base64.b64decode(string)
    except:
        return False
    for i in white_list:
        s = s.replace(i, b'')
    for i in black_list:
        if i in s:
            return False
    return True

限制了黑名单和白名单,白名单是替换为空,很明显可以用双写绕过

然后是前面的数据库查询,传入参数查询数据库,把查询结果中的wish取出来,那么我们首先得想办法往数据库里插入pickle语句

那么就是addWishes方法:

async def addWishes(request):
    data = {}
    if request.query.get("contact") and request.query.get("place") and request.query.get("reason") and request.query.get("date") and request.query.get("id"):
        data["contact"] = request.query.get("contact")
        data["place"] = request.query.get("place")
        data["reason"] = request.query.get("reason")
        data["date"] = request.query.get("date")
        data["timestamp"] = int(time.time()*1000)
        id = request.query.get("id")
        wish = base64.b64encode(pickle.dumps(data))
    else:
        return web.Response(text=messages[3])
    try:
        pool = request.app['mysql_pool']
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                sql = 'insert into wishes(`id`, `wish`) values ({id}, "{wish}")'.format(
                    id=id, wish=wish.decode())
                await cur.execute(sql)
                return web.Response(text=messages[2])
    except:
        return web.Response(text=messages[1])

传入的data会以pickle字符串传入到数据库中

但是这里有一个问题

app.add_routes([web.route('*', '/', index),
                web.route('*', '/waf', waf),
                web.route('*', '/addWishes', addWishes),
                web.get('/getWishes', getWishes),
                web.post('/rmWishes', rmWishes),
                web.get('/hint', hint)])

我们访问/addWishes的时候会触发waf

image-20240602112659615

猜测得从aiohttp自身的漏洞出发思考绕过方式

稍微搜一下发现 aiohttp(yarl) 会对 url 部分字符自动 urldecode:https://blog.csdn.net/qq_31720329/article/details/82024036

本地测试也发现会自动urldecode

image-20240602162110539

但是进不去/addWishes路由。。可以发现它进入路由是在urldecode之前,然后path的值为urldecode之后


RceHouse (Unsolved)

import subprocess
import clickhouse_connect
from flask import *
import os

app = Flask(__name__)
client = clickhouse_connect.get_client(host='127.0.0.1', port=8123, username='default', password='')
@app.route("/status",methods=['POST'])
def status():
    if request.method=="POST":
        remote_addr = request.remote_addr
        print(remote_addr)
        if remote_addr=='127.0.0.1':
            command = ["clickhouse-client", "--query", request.args.get('param') ]
            result = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True,shell=False)
            return result
        else:
            result=os.popen(f"clickhouse-client --query=\"select 'try harder'\"").read()
            return result
    else:
        result = os.popen(f"clickhouse-client --query=\"select 'try better'\"").read()
        return result
@app.route("/sql", methods=["POST"])
def sql():
    try:
        #此处是clickhouse的查询语法,不存在注入问题
        sql = 'SELECT * FROM ctf.users WHERE id = ' + request.form.get("id")
        res=client.command(sql)
        client.close()
        return res
    except Exception as e:
        return e;
@app.route("/upload",methods=['POST'])
def upload():
    if 'file' not in request.files:
        return redirect(request.url)

    file = request.files['file']
    if file.filename == '':
        return redirect(request.url)
    filename = "Boogipop"
    file_path ="/tmp/"+filename
    file.save(file_path)
    return file_path

if __name__=="__main__":
    app.run("0.0.0.0",5000,debug=False)

审一下代码,总共就三个路由

/status路由:

@app.route("/status",methods=['POST'])
def status():
    if request.method=="POST":
        remote_addr = request.remote_addr
        print(remote_addr)
        if remote_addr=='127.0.0.1':
            command = ["clickhouse-client", "--query", request.args.get('param') ]
            result = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True,shell=False)
            return result
        else:
            result=os.popen(f"clickhouse-client --query=\"select 'try harder'\"").read()
            return result
    else:
        result = os.popen(f"clickhouse-client --query=\"select 'try better'\"").read()
        return result

一眼打ssrf然后命令执行

/sql路由:

@app.route("/sql", methods=["POST"])
def sql():
    try:
        #此处是clickhouse的查询语法,不存在注入问题
        sql = 'SELECT * FROM ctf.users WHERE id = ' + request.form.get("id")
        res=client.command(sql)
        client.close()
        return res
    except Exception as e:
        return e;

执行sql查询

image-20240602121309967

/upload路由:

@app.route("/upload",methods=['POST'])
def upload():
    if 'file' not in request.files:
        return redirect(request.url)

    file = request.files['file']
    if file.filename == '':
        return redirect(request.url)
    filename = "Boogipop"
    file_path ="/tmp/"+filename
    file.save(file_path)
    return file_path

一个任意文件上传的功能,上传的文件路径为/tmp/Boogipop


ImpossibleUnser (Unsolved)

先审代码

package com.ctf;

import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;

public class IndexController {
    public IndexController() {
    }

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/ctf", new SPELHandler());
        server.createContext("/index", new IndexHandler());
        server.createContext("/unser", new UnserHandler());
        server.setExecutor((Executor)null);
        server.start();
    }
}

/ctf路由:

package com.ctf;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class SPELHandler implements HttpHandler {
    public SPELHandler() {
    }

    public void handle(HttpExchange httpExchange) throws IOException {
        InputStream requestBody = httpExchange.getRequestBody();
        String body = this.readInputStream(requestBody);
        if (!body.equals("")) {
            Map<String, String> PostData = this.parseFormData(body);
            String payload = (String)PostData.get("payload");
            ExpressionParser parser = new SpelExpressionParser();
            payload = URLDecoder.decode(payload);
            MySecurityWaf mySecurityWaf = new MySecurityWaf();
            if (mySecurityWaf.securitycheck(payload)) {
                Expression exp = parser.parseExpression(payload);
                Object value = exp.getValue();
                System.out.println(value);
                String response = "Welcome to My Challenge";
                httpExchange.sendResponseHeaders(200, (long)response.length());
                OutputStream os = httpExchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        }

        String response = "Give me some payload Plz inject me";
        httpExchange.sendResponseHeaders(200, (long)response.length());
        OutputStream os = httpExchange.getResponseBody();
        os.write(response.getBytes());
        os.close();
    }

    private Map<String, String> parseFormData(String body) {
        Map<String, String> params = new HashMap();
        String[] kvPairs = body.split("&");
        String[] var4 = kvPairs;
        int var5 = kvPairs.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String kv = var4[var6];
            String[] splits = kv.split("=");
            params.put(splits[0], splits[1]);
        }

        return params;
    }

    private String readInputStream(InputStream is) throws IOException {
        StringBuilder sb = new StringBuilder();
        InputStreamReader sr = new InputStreamReader(is);
        char[] buf = new char[1024];

        int len;
        while((len = sr.read(buf)) > 0) {
            sb.append(buf, 0, len);
        }

        return sb.toString();
    }
}

用来打SPEL的


NoCommonCollections (Unsolved)


Ezdotnet (Unsolved)