oletools_rs 0.1.0

Rust port of oletools — analysis tools for Microsoft Office files (VBA macros, DDE, OLE objects, RTF exploits)
Documentation
//! DDE detection in Excel OOXML (.xlsx) files.
//!
//! Scans for `ddeLink` elements in externalLinks XML parts.

use std::io::{Cursor, Read};

use quick_xml::events::Event;
use quick_xml::Reader;

use crate::error::{Error, Result};
use crate::msodde::field_parser::DdeField;

/// Scan an Excel OOXML (.xlsx) file for DDE links.
pub fn process_xlsx(data: &[u8]) -> Result<Vec<DdeField>> {
    let cursor = Cursor::new(data);
    let mut archive = zip::ZipArchive::new(cursor)
        .map_err(|e| Error::InvalidOoxml(format!("Invalid ZIP: {e}")))?;

    let mut fields = Vec::new();

    // Find externalLinks XML parts
    let ext_link_parts: Vec<String> = (0..archive.len())
        .filter_map(|i| {
            archive.by_index(i).ok().and_then(|e| {
                let name = e.name().to_string();
                let lower = name.to_lowercase();
                if lower.contains("externallinks/externallink") && lower.ends_with(".xml") {
                    Some(name)
                } else {
                    None
                }
            })
        })
        .collect();

    for part_name in &ext_link_parts {
        let mut xml_data = Vec::new();
        if let Ok(mut entry) = archive.by_name(part_name) {
            entry.read_to_end(&mut xml_data)?;
        }
        if xml_data.is_empty() {
            continue;
        }

        let part_fields = extract_dde_links(&xml_data, part_name)?;
        fields.extend(part_fields);
    }

    Ok(fields)
}

/// Extract ddeLink elements from an externalLink XML part.
fn extract_dde_links(xml_data: &[u8], source_part: &str) -> Result<Vec<DdeField>> {
    let mut reader = Reader::from_reader(Cursor::new(xml_data));
    reader.config_mut().trim_text(true);

    let mut fields = Vec::new();
    let mut buf = Vec::new();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
                let local_name =
                    String::from_utf8_lossy(e.local_name().as_ref()).to_string();

                if local_name == "ddeLink" {
                    // Extract ddeService and ddeTopic attributes
                    let mut service = String::new();
                    let mut topic = String::new();

                    for attr in e.attributes().flatten() {
                        let key = String::from_utf8_lossy(attr.key.local_name().as_ref())
                            .to_string();
                        let value = String::from_utf8_lossy(&attr.value).to_string();

                        match key.as_str() {
                            "ddeService" => service = value,
                            "ddeTopic" => topic = value,
                            _ => {}
                        }
                    }

                    if !service.is_empty() {
                        fields.push(DdeField {
                            field_instruction: format!(
                                "ddeLink service={} topic={} ({})",
                                service, topic, source_part
                            ),
                            command: format!("{} {}", service, topic),
                            source: service,
                            quote_decoded: None,
                        });
                    }
                }
            }
            Ok(Event::Eof) => break,
            Err(e) => {
                return Err(Error::XmlParsing(format!(
                    "Error parsing externalLink XML: {e}"
                )));
            }
            _ => {}
        }
        buf.clear();
    }

    Ok(fields)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract_dde_link() {
        let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <ddeLink ddeService="cmd" ddeTopic="/c calc"/>
</externalLink>"#;

        let fields = extract_dde_links(xml, "test.xml").unwrap();
        assert_eq!(fields.len(), 1);
        assert_eq!(fields[0].source, "cmd");
        assert!(fields[0].command.contains("/c calc"));
    }

    #[test]
    fn test_extract_no_dde_link() {
        let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <externalBook/>
</externalLink>"#;

        let fields = extract_dde_links(xml, "test.xml").unwrap();
        assert!(fields.is_empty());
    }

    #[test]
    fn test_extract_multiple_dde_links() {
        let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <ddeLink ddeService="Excel" ddeTopic="Sheet1"/>
  <ddeLink ddeService="cmd" ddeTopic="/c whoami"/>
</externalLink>"#;

        let fields = extract_dde_links(xml, "test.xml").unwrap();
        assert_eq!(fields.len(), 2);
    }

    #[test]
    fn test_extract_empty_service() {
        let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <ddeLink ddeTopic="test"/>
</externalLink>"#;

        let fields = extract_dde_links(xml, "test.xml").unwrap();
        assert!(fields.is_empty(), "Empty service should be skipped");
    }
}