Skip to main content

string_analyze/rules/core/
flag.rs

1use std::sync::LazyLock;
2
3use crate::rules::lazy_rule;
4use crate::tool::{has_chars, has_keyword};
5use base64::engine::general_purpose::{STANDARD as BASE64_STANDARD};
6use base64::Engine;
7use hex;
8
9const FLAG_PREFIXES: &[&str] = &[
10    "flag",
11    "ctf",
12    "bugku",
13    "htb",
14    "thm",
15    "picoctf",
16    "nssctf",
17    "dasctf",
18    "ciscn",
19    "cyberpeace",
20    "qwb",
21    "xctf",
22    "iscc",
23    "seccon",
24    "pctf",
25    "actf",
26    "mrctf",
27    "vnctf",
28    "litctf",
29    "adworld",
30    "roarctf",
31    "flare",
32];
33
34
35static FLAG_PREFIXES_B64: LazyLock<Vec<String>> = LazyLock::new(|| {
36    FLAG_PREFIXES
37        .iter()
38        .map(|s| {
39            let encoded = BASE64_STANDARD.encode(format!("{s}{{"));
40            let trimmed = encoded.trim_end_matches('=');
41            let invariant_len = ((s.len() + 1) * 8) / 6;
42            trimmed[..invariant_len.min(trimmed.len())].to_string()
43        })
44        .collect::<Vec<_>>()
45});
46
47static FLAG_PREFIXES_HEX: LazyLock<Vec<String>> = LazyLock::new(|| {
48    let mut hex_patterns = Vec::new();
49
50    for s in FLAG_PREFIXES {
51        let bytes = format!("{s}{{").into_bytes();
52
53        hex_patterns.push(hex::encode(&bytes));
54
55        let escaped: String = bytes.iter().map(|b| format!("\\x{:02x}", b)).collect();
56        hex_patterns.push(escaped);
57
58        let ox_formatted: String = bytes.iter()
59            .map(|b| format!("0x{:02x}", b))
60            .collect::<Vec<_>>()
61            .join(","); // "0x66,0x6c,0x61..."
62        hex_patterns.push(ox_formatted);
63
64        let space_separated: String = bytes.iter()
65            .map(|b| format!("{:02x}", b))
66            .collect::<Vec<_>>()
67            .join(" ");
68        hex_patterns.push(space_separated);
69    }
70
71    hex_patterns
72});
73
74lazy_rule!(
75    RE_FLAG = r#"(?i)\b[a-z0-9_.-]{0,20}(?:flag|ctf)[a-z0-9\s-]{0,4}\{[^\{\}\n=\t()]{4,256}\}"#,
76    "发现标准 flag",
77    100
78);
79
80lazy_rule!(
81    RE_PLATFORM_FLAG = r#"(?i)\b(?:bugku|htb|thm|picoctf|nssctf|dasctf|ciscn|cyberpeace|qwb|xctf|iscc|seccon|pctf|actf|mrctf|vnctf|litctf|adworld|roarctf|flare)[a-z0-9_-]{0,4}\{[^\{\}\n=\t()]{4,256}\}"#,
82    "发现 特定平台/赛事的 flag",
83    100
84);
85
86lazy_rule!(
87    FLAG1 = |state| (state.input.len() < 256)
88        && FLAG_PREFIXES
89            .iter()
90            .any(|&prefix| has_keyword(&mut *state, prefix, true)),
91    "存在已知flag前缀以及 '{','}'",
92    70,
93    |_,input| has_chars(
94            input,
95            &['{', '}'],
96            false,
97            false
98        ) && !has_chars(
99            input,
100            &['(', ')', '[',']','=', ' ','`','"','\'',';',':'],
101            true,
102            false
103        )
104);
105
106lazy_rule!(
107    FLAG2 = |state| (state.input.len() < 256)
108        && FLAG_PREFIXES.iter().any(|&prefix| {
109            let chars_vec: Vec<char> = prefix.chars().collect();
110            has_chars(&mut *state, &chars_vec, false, true)
111        }),
112    "存在被打乱的flag前缀字符并且含有 '{','}'",
113    45,
114    |_,input| has_chars(
115            input,
116            &['{', '}'],
117            false,
118            false
119        ) && !has_chars(
120            input,
121            &['(', ')', '[',']','=', ' ','`','"','\'',';',':'],
122            true,
123            false
124        )
125);
126
127lazy_rule!(
128    FLAG_B64 = |state| (state.input.len() >= 16)
129        && FLAG_PREFIXES_B64
130            .iter()
131            .any(|b64_prefix| has_keyword(&mut *state, b64_prefix, false)),
132    "疑似 Base64 编码的 flag前缀",
133    65
134
135);
136
137lazy_rule!(
138    FLAG_HEX = |state| (state.input.len() >= 16)
139        && FLAG_PREFIXES_HEX
140            .iter()
141            .any(|hex_prefix| has_keyword(&mut *state, hex_prefix, true)),
142    "疑似 Hex 编码的 flag前缀",
143    65
144);
145
146
147#[cfg(test)]
148mod tests {
149    use crate::analyze_with;
150    use crate::rules::get_rules;
151
152    #[test]
153    fn test() {
154        let input = r#"
155flag{5tgb8uik,0ol}
156deepseek_key = "sk-267afd638c45485789bb3a9b96b576f1";
157buuctf{aaac38f5-046b-43e8-a089-03813226d280}
158fgaag_!l{_oun}amb_ob # railfence_group 3
159hex_flag = 666c61677b68656c6c6f5f63746665727d
160b64_flag = ZmxhZ3s1dGdiOHVpaywwb2x9
161b64_ctf = Y3Rme3Rlc3RfMTIzNH0=
16266 6c 61 67 7b 68 65 6c 6c 6f 5f 63 74 66 65 72 7d
16366%6c%61%67%7b%68%65%6c%6c%6f%5f%63%74%66%65%72%7d
1640x66,0x6c,0x61,0x67,0x7b,0x68,0x65,0x6c,0x6c,0x6f,0x5f,0x63,0x74,0x66,0x65,0x72,0x7d
1650x660x6c0x610x670x7b0x680x650x6c0x6c0x6f0x5f0x630x740x660x650x720x7d
166\x66\x6c\x61\x67\x7b\x68\x65\x6c\x6c\x6f\x5f\x63\x74\x66\x65\x72\x7d
16766,6c,61,67,7b,68,65,6c,6c,6f,5f,63,74,66,65,72,7d
168"#;
169        for line in input.lines() {
170            if line.trim().is_empty() {
171                continue;
172            }
173            println!("{}", analyze_with(
174                line,
175                &get_rules(|m, _| module_path!().split("::tests").any(|s| s == m)),
176            ))
177        }
178    }
179}