Windows MDM Server proof of concept (#9178)

This commit is contained in:
Marcos Oviedo 2023-01-04 12:05:51 -03:00 committed by GitHub
parent 772caeaa09
commit 326bce8dbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1775 additions and 0 deletions

View file

@ -0,0 +1,18 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Mac System Files
.DS_Store
# Ignore The Folder With My HTTPS Certifciates
certs/

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Oscar Beaumont
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1RuIhpHWDudu07FfJ2bTAzm8DaD5F9WSxC8cKm6OCttN7tIj
FzyayThorwTZ37+VeJMZ7/bP/bqxTkF7Y59geEsFlDnLvQWm2Hh4f79MV08cWx/T
B3Wqgk6jPfr0dS2sQZv+JiS6/OR0BCTpUPZwM7eQiN3SoPDZEibIaLt53ECVc8ze
mD8cHrNmCk8HIWOmhzP5UzM21pXy8JOo2aEql6iVBoUa0QVfJVX9EHDPnmJmeA6f
d0uDZTvi8YSj+GqcmcBmGTe/rbpfaJjPiaXce0bF2JKjmK38bjgK29fHoEZ+Scw3
ul160fxMV1bBhIcaJt+uEQyOKkdqLYPZ+lZ+yQIDAQABAoIBAHbGfMZ8G/F8njGQ
53b/gVaH5D84W/0jxURg+XLQ4Yw9hOc56eL2nVLPhNEfhAuILVfhrRAo4O4LEu2J
46q31r3VGovt1pdIwiBerNKOnY8AAc7sIuNCesFb8PIHoB57UUnUFsfNqwZukhcJ
N50vbYP1qLIP6GhZNLNAOGzfKOFPfTA1hpgXkUAYFUC6D5DwIMNuxX/5QYjAw8fK
e0NsehjuzTb+HckazvB/B0SpPZyezQrpJU5AEWzxQgcL+Qx5BdcOPyR83vdH8PoJ
83uj4FhNA16dHmo87AGe6d2FncadCoLpxjcJgjctqcDnJFdAdPcJOcA76qYeDh4E
nkSali0CgYEA4POeRx1RnUI7YN1b4JwY8qTj8ucMTaZk/Kjb1BPXX5rqC6Vx7I4N
UNCLNvPn6U/1W9NXzijamyddxcNtHcy8jXqlYawOebE3MAZvayBlC4yPYCd230LJ
ADl5auQeNj2ktJbJwYgZQcwnMEu3SGYIMvy96pKdWilUenPbfO9pj/cCgYEA8oVr
W17vQJX+8hmItcnqEHoCFXi2hM/vKZCHF/MrknJcpANTo7rRQaxn3xIqZRkt6xJz
vp5yl20pzl/KDSMxLd4H51fKy6Fzk0hvFpdqbsxLidu+rf1o3Nice6WBLqz+XMtV
i/dTqvD9b+CVmnoVmwVHoAja2QTZCAQnLGqCNz8CgYEAwZ7PGFTS/7GXXEuLnmud
SZTFozhdraRP/ez1sbgWQ/MaCkYwJbUrHukxOm57qaUqAgyJ4ifl6W/b1bHdBK5J
iNkM6mHm37W6U7rmQeXTMzqb2d5+AbMBQRE3QdrxaixqzQmQxOR5INowzPAO5OD1
o7VJXlMt3wH99ZwtSn7jdIcCgYEAzxEDfNwtwyNOrj8G7tAbPT4vEU4j6HnxZbe0
4MoK5dsnJhKBE0aq7Dvb5CaKdA9vmUoD8Tkv9gKKs14uEdF+Z/8vGGNpDzwmhhZO
YyedBEUCKg6pW70GD6oS0a+aANRLyccCn6LomQdyHFfQ5Dhgwh9b7FQjJzBwbdu9
5rp5u9kCgYA1KZWTkSxjUEycTNKgQz4MKFCEbYkm8B6lfAY5GYhTI1fsoympgd4d
RYmQUuhHrLHP4s85NOSoRdrMD8MiRyhHna+FoaqIEIW5ErMkNDM9SYs4TyrIxf7l
R42IvNH76CcJpcWYhQ1PCLdsbuyO9lMkXBylPMI1VPZkjUZqj/Ixfw==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,27 @@
Bag Attributes
localKeyID: 01 00 00 00
friendlyName: mdmwindows.com
subject=CN = *.mdmwindows.com
issuer=CN = *.mdmwindows.com
-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIQGQpbyNZeY7hBkxKCbRRfaTANBgkqhkiG9w0BAQsFADAb
MRkwFwYDVQQDDBAqLm1kbXdpbmRvd3MuY29tMB4XDTIyMTIyODE0MTUxMloXDTMy
MTIyODE0MjUxMlowGzEZMBcGA1UEAwwQKi5tZG13aW5kb3dzLmNvbTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBANUbiIaR1g7nbtOxXydm0wM5vA2g+RfV
ksQvHCpujgrbTe7SIxc8msk4aK8E2d+/lXiTGe/2z/26sU5Be2OfYHhLBZQ5y70F
pth4eH+/TFdPHFsf0wd1qoJOoz369HUtrEGb/iYkuvzkdAQk6VD2cDO3kIjd0qDw
2RImyGi7edxAlXPM3pg/HB6zZgpPByFjpocz+VMzNtaV8vCTqNmhKpeolQaFGtEF
XyVV/RBwz55iZngOn3dLg2U74vGEo/hqnJnAZhk3v626X2iYz4ml3HtGxdiSo5it
/G44CtvXx6BGfknMN7pdetH8TFdWwYSHGibfrhEMjipHai2D2fpWfskCAwEAAaN9
MHswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
ATArBgNVHREEJDAigg5tZG13aW5kb3dzLmNvbYIQKi5tZG13aW5kb3dzLmNvbTAd
BgNVHQ4EFgQUsFD9edObvrbuLZopzjgGtpr8ZTQwDQYJKoZIhvcNAQELBQADggEB
AEEfa9BS75jG4D2fJ1/Q9Xn/SPsaAtwUYW+ilGCqYBfQ8lBmXGN8z8WETdw5xus3
FGdIYtw8SKF5fp3TOJlNkiF0LNhAEvwDEkNCtOK9XpqTScjDi2WT1c+gJmPyHj7M
+vn9+gHFI7tUT+JImqU1I6tzD2OsZS5H1Vow+QwD3/DswSoUKM+zQreJGKaKLZqo
i6B/fdS+XkYWymwXmQiu+7D8RwTGEMIrfPFon90I9APrDhOmjiDa7L+xs7zRfT4J
fzIbHn867msNrZzwPAmf3fRhEk8cwHD5jfgnXuoVL4icPG57rUvCfX+QI9FfpjH4
ZIbwI+HQg86S0hVwqhGs1RI=
-----END CERTIFICATE-----

View file

@ -0,0 +1,10 @@
module github.com/oscartbeaumont/windows_mdm
go 1.12
require (
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345
github.com/go-xmlfmt/xmlfmt v1.1.2
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.3
)

View file

@ -0,0 +1,8 @@
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345 h1:AZLrCR38RDhsyCQakz1UxCx72As18Ai5mObrKvT8DK8=
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345/go.mod h1:pw+gaKQ52Cl/SrERU62yQAiWauPpLgKpuR1hkxwL4tM=
github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,179 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"github.com/go-xmlfmt/xmlfmt"
"github.com/gorilla/mux"
)
// Code forked from https://github.com/oscartbeaumont/windows_mdm
// Global config, populated via Command line flags
var domain string
var deepLinkUserEmail string
var authPolicy string
var profileDir string
var staticDir string
var verbose bool
func main() {
fmt.Println("Starting Windows MDM Demo Server")
// Parse CMD flags. This populates the varibles defined above
flag.StringVar(&domain, "domain", "mdmwindows.com", "Your servers primary domain")
flag.StringVar(&deepLinkUserEmail, "dl-user-email", "demo@mdmwindows.com", "An email of the enrolling user when using the Deeplink ('/deeplink')")
flag.StringVar(&authPolicy, "auth-policy", "OnPremise", "An email of the enrolling user when using the Deeplink ('/deeplink')")
flag.StringVar(&profileDir, "mdm-profile-dir", "./profile", "The MDM policy directory contains the SyncML MDM profile commmands to enforce to enrolled devices")
flag.StringVar(&staticDir, "static-dir", "./static", "The directory to serve static files")
flag.BoolVar(&verbose, "verbose", false, "HTTP traffic dump")
flag.Parse()
// Verify authPolicy is valid
if authPolicy != "Federated" && authPolicy != "OnPremise" {
panic("unsupported authpolicy")
}
// Checking if profile directory exists
_, err := os.Stat(profileDir)
if err != nil {
if os.IsNotExist(err) {
panic("profile directory does not exists")
} else {
panic(err)
}
}
// Checking if static directory exists
_, err = os.Stat(staticDir)
if err != nil {
if os.IsNotExist(err) {
panic("static directory does not exists")
} else {
panic(err)
}
}
// Create HTTP request router
r := mux.NewRouter()
//MS-MDE and MS-MDM endpoints
r.Path("/EnrollmentServer/Discovery.svc").Methods("GET", "POST").HandlerFunc(DiscoveryHandler)
r.Path("/EnrollmentServer/Policy.svc").Methods("POST").HandlerFunc(PolicyHandler)
r.Path("/EnrollmentServer/Enrollment.svc").Methods("POST").HandlerFunc(EnrollHandler)
r.Path("/ManagementServer/MDM.svc").Methods("POST").HandlerFunc(ManageHandler)
//Static root endpoint
r.Path("/").Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
w.Write([]byte(`<center><h1>FleetDM Windows MDM Demo Server<br></h1>.<center>`))
w.Write([]byte(`<br><center><img src="https://fleetdm.com/images/press-kit/fleet-logo-dark-rgb.png"></center>`))
})
//Static file serve
fileServer := http.FileServer(http.Dir(staticDir))
r.PathPrefix("/").Handler(http.StripPrefix("/static", fileServer))
// Start HTTPS Server
fmt.Println("HTTPS server listening on port 443")
err = http.ListenAndServeTLS(":443", "./certs/dev_cert_mdmwindows_com_cert.pem", "./certs/dev_cert_mdmwindows_com.key", globalHandler(r))
if err != nil {
panic(err)
}
}
// drainBody reads all of bytes to memory and then returns two equivalent
// ReadClosers yielding the same bytes.
//
// It returns an error if the initial slurp of all bytes fails. It does not attempt
// to make the returned ReadClosers have identical error-matching behavior.
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, body []byte, err error) {
if b == nil || b == http.NoBody {
// No copying needed. Preserve the magic sentinel meaning of NoBody.
return http.NoBody, http.NoBody, nil, nil
}
var buf bytes.Buffer
if _, err = buf.ReadFrom(b); err != nil {
return nil, b, nil, err
}
if err = b.Close(); err != nil {
return nil, b, nil, err
}
return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), buf.Bytes(), nil
}
// global HTTP handler to log input and output https traffic
func globalHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if verbose {
// grabbing Input Header and Body
reqHeader, err := httputil.DumpRequest(r, false)
if err != nil {
panic(err)
}
var bodyBytes []byte
reqBodySave := r.Body
if r.Body != nil {
reqBodySave, r.Body, bodyBytes, err = drainBody(r.Body)
if err != nil {
panic(err)
}
}
r.Body = reqBodySave
var beautifiedReqBody string
if len(bodyBytes) > 0 {
beautifiedReqBody = xmlfmt.FormatXML(string(bodyBytes), " ", " ")
}
fmt.Printf("\n\n============================= Input Request =============================\n")
fmt.Println("----------- Input Header -----------\n", string(reqHeader))
if len(beautifiedReqBody) > 0 {
fmt.Println("----------- Input Body -----------\n", string(beautifiedReqBody))
} else {
fmt.Printf("----------- Empty Input Body -----------\n")
}
fmt.Printf("=========================================================================\n\n\n")
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, r)
if verbose {
// grabbing Output Header and Body
var beautifiedResponseBody string
responseBody := rec.Body.Bytes()
if len(responseBody) > 0 {
beautifiedResponseBody = xmlfmt.FormatXML(string(responseBody), " ", " ")
}
responseHeader, err := httputil.DumpResponse(rec.Result(), false)
if err != nil {
panic(err)
}
fmt.Printf("\n\n============================= Output Response =============================\n")
fmt.Println("----------- Response Header -----------\n", string(responseHeader))
if len(beautifiedResponseBody) > 0 {
fmt.Println("----------- Response Body -----------\n", string(beautifiedResponseBody))
} else {
fmt.Printf("----------- Empty Response Body -----------\n")
}
fmt.Printf("=========================================================================\n\n\n")
}
// we copy the captured response headers to our new response
for k, v := range rec.Header() {
w.Header()[k] = v
}
w.Write(rec.Body.Bytes())
})
}

View file

@ -0,0 +1,65 @@
package main
import (
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
// DiscoveryHandler is the HTTP handler assosiated with the enrollment protocol's discovery endpoint.
func DiscoveryHandler(w http.ResponseWriter, r *http.Request) {
// Return HTTP Status 200 Ok when a HTTP GET request is received.
if r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
return
}
// Read The HTTP Request body
bodyRaw, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
body := string(bodyRaw)
// Retrieve the MessageID From The Body For The Response
messageID := strings.Replace(strings.Replace(regexp.MustCompile(`<a:MessageID>[\s\S]*?<\/a:MessageID>`).FindStringSubmatch(body)[0], "<a:MessageID>", "", -1), "</a:MessageID>", "", -1)
var extraParams = ""
if authPolicy == "Federated" {
extraParams += "<AuthenticationServiceUrl>https://" + domain + "/EnrollmentServer/Auth</AuthenticationServiceUrl>"
}
// Create response payload
response := []byte(`
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse</a:Action>
<ActivityId CorrelationId="8c6060c4-3d78-4d73-ae17-e8bce88426ee"
xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">8c6060c4-3d78-4d73-ae17-e8bce88426ee
</ActivityId>
<a:RelatesTo>` + messageID + `</a:RelatesTo>
</s:Header>
<s:Body>
<DiscoverResponse
xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
<DiscoverResult>
<AuthPolicy>` + authPolicy + `</AuthPolicy>
<EnrollmentVersion>4.0</EnrollmentVersion>
<EnrollmentPolicyServiceUrl>https://` + domain + `/EnrollmentServer/Policy.svc</EnrollmentPolicyServiceUrl>
<EnrollmentServiceUrl>https://` + domain + `/EnrollmentServer/Enrollment.svc</EnrollmentServiceUrl>
` + extraParams + `
</DiscoverResult>
</DiscoverResponse>
</s:Body>
</s:Envelope>`)
// Return response body
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
}

View file

@ -0,0 +1,220 @@
package main
import (
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"fmt"
"io/ioutil"
"math/big"
mathrand "math/rand"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
// EnrollHandler is the HTTP handler assosiated with the enrollment protocol's enrollment endpoint.
func EnrollHandler(w http.ResponseWriter, r *http.Request) {
// Read The HTTP Request body
bodyRaw, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
body := string(bodyRaw)
// Retrieve the MessageID From The Body For The Response
messageID := strings.Replace(strings.Replace(regexp.MustCompile(`<a:MessageID>[\s\S]*?<\/a:MessageID>`).FindStringSubmatch(body)[0], "<a:MessageID>", "", -1), "</a:MessageID>", "", -1)
// Retrieve the BinarySecurityToken (which contains a Certificate Signing Request) From The Body For The Response
binarySecurityToken := strings.Replace(strings.Replace(regexp.MustCompile(`<wsse:BinarySecurityToken ValueType="http:\/\/schemas.microsoft.com\/windows\/pki\/2009\/01\/enrollment#PKCS10" EncodingType="http:\/\/docs\.oasis-open\.org\/wss\/2004\/01\/oasis-200401-wss-wssecurity-secext-1\.0\.xsd#base64binary">[\s\S]*?<\/wsse:BinarySecurityToken>`).FindStringSubmatch(body)[0], `<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">`, "", -1), "</wsse:BinarySecurityToken>", "", -1)
// Retrieve the DeviceID From The Body For The Response
deviceID := strings.Replace(strings.Replace(regexp.MustCompile(`<ac:ContextItem Name="DeviceID"><ac:Value>[\s\S]*?<\/ac:Value><\/ac:ContextItem>`).FindStringSubmatch(body)[0], `<ac:ContextItem Name="DeviceID"><ac:Value>`, "", -1), "</ac:Value></ac:ContextItem>", "", -1)
// Retrieve the EnrollmentType From The Body For The Response
enrollmentType := strings.Replace(strings.Replace(regexp.MustCompile(`<ac:ContextItem Name="EnrollmentType"><ac:Value>[\s\S]*?<\/ac:Value><\/ac:ContextItem>`).FindStringSubmatch(body)[0], `<ac:ContextItem Name="EnrollmentType"><ac:Value>`, "", -1), "</ac:Value></ac:ContextItem>", "", -1)
/* Sign binary security token */
// Load raw Root CA
rootCertificateDer, err := ioutil.ReadFile("./identity/identity.crt")
if err != nil {
panic(err)
}
rootPrivateKeyDer, err := ioutil.ReadFile("./identity/identity.key")
if err != nil {
panic(err)
}
// Convert the raw Root CA cert & key to parsed version
rootCert, err := x509.ParseCertificate(rootCertificateDer)
if err != nil {
panic(err)
}
rootPrivateKey, err := x509.ParsePKCS1PrivateKey(rootPrivateKeyDer)
if err != nil {
panic(err)
}
// Decode Base64
csrRaw, err := base64.StdEncoding.DecodeString(binarySecurityToken)
if err != nil {
panic(err)
}
// Decode and verify CSR
csr, err := x509.ParseCertificateRequest(csrRaw)
if err != nil {
panic(err)
}
if err = csr.CheckSignature(); err != nil {
panic(err)
}
// Create client identity certificate
NotBefore1 := time.Now().Add(time.Duration(mathrand.Int31n(120)) * -time.Minute) // This randomises the creation time a bit for added security (Recommended by x509 signing article not the MDM spec)
clientCertificate := &x509.Certificate{
Signature: csr.Signature,
SignatureAlgorithm: csr.SignatureAlgorithm,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
PublicKey: csr.PublicKey,
SerialNumber: big.NewInt(2),
Issuer: rootCert.Issuer,
Subject: pkix.Name{
CommonName: deviceID,
}, // The Subject is not used from the CSR because the characters in it are causing issues.
NotBefore: NotBefore1,
NotAfter: NotBefore1.Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
// Sign certificate with the identity
clientCRTRaw, err := x509.CreateCertificate(rand.Reader, clientCertificate, rootCert, csr.PublicKey, rootPrivateKey)
if err != nil {
panic(err)
}
// Note: SHA-1 Hash OID is deprecated
// Fingerprint (SHA-1 hash) of client certificate
h := sha1.New()
h.Write(clientCRTRaw)
signedClientCertFingerprint := strings.ToUpper(fmt.Sprintf("%x", h.Sum(nil))) // TODO: Cleanup -> This line is probally messer than it needs to be
// Fingerprint (SHA-1 hash) of client certificate
h2 := sha1.New()
h2.Write(rootCertificateDer)
identityCertFingerprint := strings.ToUpper(fmt.Sprintf("%x", h2.Sum(nil))) // TODO: Cleanup -> This line is probally messer than it needs to be
// Determain Certstore
certStore := "User"
if enrollmentType == "Device" {
certStore = "System"
}
// End Sign binary security token
// Generate WAP provisioning profile for inside the payload
wapProvisionProfile := `
<?xml version="1.0" encoding="UTF-8"?>
<wap-provisioningdoc version="1.1">
<characteristic type="CertificateStore">
<characteristic type="Root">
<characteristic type="System">
<characteristic type="` + identityCertFingerprint /* Root CA Certificate Fingureprint (SHA-1 hash of Der) */ + `">
<parm name="EncodedCertificate" value="` + base64.StdEncoding.EncodeToString(rootCertificateDer) /* Base64 encoded root CA certificate */ + `" />
</characteristic>
</characteristic>
</characteristic>
<characteristic type="My">
<characteristic type="` + certStore + `">
<characteristic type="` + signedClientCertFingerprint /* Signed Client Certificate (From the BinarySecurityToken) Fingureprint (SHA-1 hash of Der) */ + `">
<parm name="EncodedCertificate" value="` + base64.StdEncoding.EncodeToString(clientCRTRaw) /* Base64 encoded signed certificate */ + `" />
</characteristic>
<characteristic type="PrivateKeyContainer" /></characteristic>
</characteristic>
</characteristic>
<characteristic type="APPLICATION">
<parm name="APPID" value="w7" />
<parm name="PROVIDER-ID" value="DEMO MDM" />
<parm name="NAME" value="FleetDM Demo Server - Windows" />
<parm name="ADDR" value="https://` + domain + `/ManagementServer/MDM.svc" />
<parm name="ServerList" value="https://` + domain + `/ManagementServer/ServerList.svc" />
<parm name="ROLE" value="4294967295" />
<parm name="BACKCOMPATRETRYDISABLED" />
<parm name="DEFAULTENCODING" value="application/vnd.syncml.dm+xml" />
<characteristic type="APPAUTH">
<parm name="AAUTHLEVEL" value="CLIENT" />
<parm name="AAUTHTYPE" value="DIGEST" />
<parm name="AAUTHSECRET" value="dummy" />
<parm name="AAUTHDATA" value="nonce" />
</characteristic>
<characteristic type="APPAUTH">
<parm name="AAUTHLEVEL" value="APPSRV" />
<parm name="AAUTHTYPE" value="DIGEST" />
<parm name="AAUTHNAME" value="dummy" />
<parm name="AAUTHSECRET" value="dummy" />
<parm name="AAUTHDATA" value="nonce" />
</characteristic>
</characteristic>
<characteristic type="DMClient">
<characteristic type="Provider">
<characteristic type="DEMO MDM">
<characteristic type="Poll">
<parm name="NumberOfFirstRetries" value="8" datatype="integer" />
</characteristic>
</characteristic>
</characteristic>
</characteristic>
</wap-provisioningdoc>`
wapProvisionProfileRaw := []byte(strings.ReplaceAll(strings.ReplaceAll(wapProvisionProfile, "\n", ""), "\t", ""))
// Create response payload
response := []byte(`
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RSTRC/wstep</a:Action>
<a:RelatesTo>` + messageID + `</a:RelatesTo>
<o:Security
xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp u:Id="_0">
<u:Created>2018-11-30T00:32:59.420Z</u:Created>
<u:Expires>2018-12-30T00:37:59.420Z</u:Expires>
</u:Timestamp>
</o:Security>
</s:Header>
<s:Body>
<RequestSecurityTokenResponseCollection
xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
<RequestSecurityTokenResponse>
<TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</TokenType>
<DispositionMessage
xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollment">
</DispositionMessage>
<RequestedSecurityToken>
<BinarySecurityToken
xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ValueType="http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">` + base64.StdEncoding.EncodeToString(wapProvisionProfileRaw) + `
</BinarySecurityToken>
</RequestedSecurityToken>
<RequestID
xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollment">0
</RequestID>
</RequestSecurityTokenResponse>
</RequestSecurityTokenResponseCollection>
</s:Body>
</s:Envelope>`)
// Return response body
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
}

View file

@ -0,0 +1,65 @@
package main
import (
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
// PolicyHandler is the HTTP handler assosiated with the enrollment protocol's policy endpoint.
func PolicyHandler(w http.ResponseWriter, r *http.Request) {
// Read The HTTP Request body
bodyRaw, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
body := string(bodyRaw)
// Retrieve the MessageID From The Body For The Response
messageID := strings.Replace(strings.Replace(regexp.MustCompile(`<a:MessageID>[\s\S]*?<\/a:MessageID>`).FindStringSubmatch(body)[0], "<a:MessageID>", "", -1), "</a:MessageID>", "", -1)
// Create response payload
response := []byte(`
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse</a:Action>
<a:RelatesTo>` + messageID + `</a:RelatesTo>
</s:Header>
<s:Body
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<GetPoliciesResponse
xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy">
<response>
<policies>
<policy>
<attributes>
<policySchema>3</policySchema>
<privateKeyAttributes>
<minimalKeyLength>2048</minimalKeyLength>
<algorithmOIDReferencexsi:nil="true"/>
</privateKeyAttributes>
<hashAlgorithmOIDReference xsi:nil="true"></hashAlgorithmOIDReference>
</attributes>
</policy>
</policies>
</response>
<oIDs>
<oID>
<value>1.3.6.1.4.1.311.20.2</value>
<group>1</group>
<oIDReferenceID>5</oIDReferenceID>
<defaultName>Certificate Template Name</defaultName>
</oID>
</oIDs>
</GetPoliciesResponse>
</s:Body>
</s:Envelope>`)
// Return response body
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
}

View file

@ -0,0 +1,299 @@
package main
import (
"encoding/xml"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"strconv"
"strings"
)
// SyncML XML Parsing Types - This needs to be improved
type SyncMLHeader struct {
DTD string `xml:"VerDTD"`
Version string `xml:"VerProto"`
SessionID int `xml:"SessionID"`
MsgID int `xml:"MsgID"`
Target string `xml:"Target>LocURI"`
Source string `xml:"Source>LocURI"`
MaxMsgSize int `xml:"Meta>A:MaxMsgSize"`
}
type SyncMLCommandMeta struct {
XMLinfo string `xml:"xmlns,attr"`
Type string `xml:"Type"`
}
type SyncMLCommandItem struct {
Meta SyncMLCommandMeta `xml:"Meta"`
Source string `xml:"Source>LocURI"`
Data string `xml:"Data"`
}
type SyncMLCommand struct {
XMLName xml.Name
CmdID int `xml:",omitempty"`
MsgRef string `xml:",omitempty"`
CmdRef string `xml:",omitempty"`
Cmd string `xml:",omitempty"`
Target string `xml:"Target>LocURI"`
Source string `xml:"Source>LocURI"`
Data string `xml:",omitempty"`
Item []SyncMLCommandItem `xml:",any"`
}
type SyncMLBody struct {
Item []SyncMLCommand `xml:",any"`
}
type SyncMLMessage struct {
XMLinfo string `xml:"xmlns,attr"`
Header SyncMLHeader `xml:"SyncHdr"`
Body SyncMLBody `xml:"SyncBody"`
}
// Returns the MDM configuration profile SyncML content from profile dir
func getConfigurationProfiles(cmdIDstart int) string {
files, err := ioutil.ReadDir(profileDir)
if err != nil {
panic(err)
}
var syncmlCommands string
for _, file := range files {
cmdIDstart++
fmt.Printf("\n--------- Command Request %d ---------\n", cmdIDstart)
fmt.Printf("Command payload retrieved from file %s\n", file.Name())
fileContent, err := os.ReadFile(profileDir + "/" + file.Name())
if err != nil {
panic(err)
}
syncmlCommands += strings.Replace(string(fileContent), "xxcmdidxx", strconv.Itoa(cmdIDstart), -1) + "\n"
}
//input sanitization
sanitizedSyncmlOutput := strings.ReplaceAll(syncmlCommands, "\r\n", "\n")
if len(sanitizedSyncmlOutput) > 0 {
fmt.Print("\n")
}
return sanitizedSyncmlOutput
}
// Alert Command IDs
const DeviceUnenrollmentID = "1226"
const HostInitMessageID = "1201"
// Checks if body contains a DM device unrollment SyncML message
func isDeviceUnenrollmentMessage(body SyncMLBody) bool {
for _, element := range body.Item {
if element.Data == DeviceUnenrollmentID {
return true
}
}
return false
}
// Checks if body contains a DM session initialization SyncML message sent by device
func isSessionInitializationMessage(body SyncMLBody) bool {
isUnenrollMessage := isDeviceUnenrollmentMessage(body)
for _, element := range body.Item {
if element.Data == HostInitMessageID && !isUnenrollMessage {
return true
}
}
return false
}
// Get IP address from HTTP Request
func getIP(r *http.Request) (string, error) {
//Get IP from the X-REAL-IP header
ip := r.Header.Get("X-REAL-IP")
netIP := net.ParseIP(ip)
if netIP != nil {
return ip, nil
}
//Get IP from X-FORWARDED-FOR header
ips := r.Header.Get("X-FORWARDED-FOR")
splitIps := strings.Split(ips, ",")
for _, ip := range splitIps {
netIP := net.ParseIP(ip)
if netIP != nil {
return ip, nil
}
}
//Get IP from RemoteAddr
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return "", err
}
netIP = net.ParseIP(ip)
if netIP != nil {
return ip, nil
}
return "", fmt.Errorf("no valid ip found")
}
// ManageHandler is the HTTP handler assosiated with the mdm management service. This is what constantly pushes configuration profiles to the device.
func ManageHandler(w http.ResponseWriter, r *http.Request) {
// Read The HTTP Request body
bodyRaw, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var responseRaw []byte
var response string
var message SyncMLMessage
//Parsing input SyncML message
if err := xml.Unmarshal(bodyRaw, &message); err != nil {
panic(err)
}
// Cmd ID variable with getNextCmdID() increment statement hack
CmdID := 0
getNextCmdID := func(i *int) string { *i++; return strconv.Itoa(*i) }
// Retrieve the MessageID From The Body For The Response
DeviceID := message.Header.Source
// Retrieve the SessionID From The Body For The Response
SessionID := message.Header.SessionID
// Retrieve the MsgID From The Body For The Response
MsgID := message.Header.MsgID
//Only handle DM session initialization SyncML message sent by device
// Retrieve the IP Address from calling device
ipAddressBytes, err := getIP(r)
if err != nil {
panic(err)
}
//Checking the SyncML message types
if isSessionInitializationMessage(message.Body) {
fmt.Printf("\n========= New OMA-DM session from Windows Host %s (%s) =========\n", string(ipAddressBytes), r.UserAgent())
// Create response payload - MDM syncml configuration profiles commands will be enforced here
response = `
<?xml version="1.0" encoding="UTF-8"?>
<SyncML xmlns="SYNCML:SYNCML1.2">
<SyncHdr>
<VerDTD>1.2</VerDTD>
<VerProto>DM/1.2</VerProto>
<SessionID>` + strconv.Itoa(SessionID) + `</SessionID>
<MsgID>` + strconv.Itoa(MsgID) + `</MsgID>
<Target>
<LocURI>` + DeviceID + `</LocURI>
</Target>
<Source>
<LocURI>https://` + domain + `/ManagementServer/MDM.svc</LocURI>
</Source>
</SyncHdr>
<SyncBody>
<Status>
<CmdID>` + getNextCmdID(&CmdID) + `</CmdID>
<MsgRef>` + strconv.Itoa(MsgID) + `</MsgRef>
<CmdRef>0</CmdRef>
<Cmd>SyncHdr</Cmd>
<Data>200</Data>
</Status>
<Status>
<CmdID>` + getNextCmdID(&CmdID) + `</CmdID>
<MsgRef>` + strconv.Itoa(MsgID) + `</MsgRef>
<CmdRef>2</CmdRef>
<Cmd>Alert</Cmd>
<Data>200</Data>
</Status>
<Status>
<CmdID>` + getNextCmdID(&CmdID) + `</CmdID>
<MsgRef>` + strconv.Itoa(MsgID) + `</MsgRef>
<CmdRef>3</CmdRef>
<Cmd>Alert</Cmd>
<Data>200</Data>
</Status>
<Status>
<CmdID>` + getNextCmdID(&CmdID) + `</CmdID>
<MsgRef>` + strconv.Itoa(MsgID) + `</MsgRef>
<CmdRef>4</CmdRef>
<Cmd>Replace</Cmd>
<Data>200</Data>
</Status>
` + getConfigurationProfiles(CmdID) + `
<Final />
</SyncBody>
</SyncML>`
// Return response
responseRaw = []byte(strings.ReplaceAll(strings.ReplaceAll(response, "\n", ""), "\t", ""))
w.Header().Set("Content-Type", "application/vnd.syncml.dm+xml")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(responseRaw)
} else {
//Log if this is a device unrollment message
if isDeviceUnenrollmentMessage(message.Body) {
fmt.Printf("\nWindows Device at %s was removed from MDM!\n\n", string(ipAddressBytes))
}
//Acknowledge the HTTP request sent by device
response = `
<?xml version="1.0" encoding="UTF-8"?>
<SyncML xmlns="SYNCML:SYNCML1.2">
<SyncHdr>
<VerDTD>1.2</VerDTD>
<VerProto>DM/1.2</VerProto>
<SessionID>` + strconv.Itoa(SessionID) + `</SessionID>
<MsgID>` + strconv.Itoa(MsgID) + `</MsgID>
<Target>
<LocURI>` + DeviceID + `</LocURI>
</Target>
<Source>
<LocURI>https://` + domain + `/ManagementServer/MDM.svc</LocURI>
</Source>
</SyncHdr>
<SyncBody>
<Status>
<CmdID>` + getNextCmdID(&CmdID) + `</CmdID>
<MsgRef>` + strconv.Itoa(MsgID) + `</MsgRef>
<CmdRef>0</CmdRef>
<Cmd>SyncHdr</Cmd>
<Data>200</Data>
</Status>
<Final />
</SyncBody>
</SyncML>`
// Dump Response Payload
for _, element := range message.Body.Item {
if element.XMLName.Local != "Final" && element.Cmd != "SyncHdr" {
commandStr, _ := xml.MarshalIndent(element, "", " ")
if element.XMLName.Local == "Status" {
fmt.Printf("\n--------- Command Response %s - Return Code: %s ---------\n", element.CmdRef, element.Data)
} else {
fmt.Printf("%s\n", commandStr)
}
}
}
// Return response body
responseRaw = []byte(strings.ReplaceAll(strings.ReplaceAll(response, "\n", ""), "\t", ""))
w.Header().Set("Content-Type", "application/vnd.syncml.dm+xml")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(responseRaw)
}
}

View file

@ -0,0 +1,62 @@
package main
import (
"bufio"
"fmt"
"os"
"path"
)
var (
sourceFilePath = path.Join(os.Getenv("GOROOT"), "src", "encoding", "asn1", "asn1.go")
patchedFilePath = path.Join(os.Getenv("GOROOT"), "src", "encoding", "asn1", "asn1-patched.go")
)
func main() {
// Check for the GOROOT env varible. Should be set by Go automatically
if os.Getenv("GOROOT") == "" {
panic("Plese set your GOROOT path")
}
// Load The file and create a scanner
file, err := os.Open(sourceFilePath)
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)
// Open Output File
out, err2 := os.Create(patchedFilePath)
if err2 != nil {
panic(err2)
}
// Loop of each line of the file checking it
for scanner.Scan() {
out.Write(scanner.Bytes())
out.Write([]byte("\n"))
if scanner.Text() == " b == '?' ||" {
scanner.Scan()
if scanner.Text() != " b == '!' || // Windows MDM Certificate Parsing Patch" {
out.Write([]byte(" b == '!' || // Windows MDM Certificate Parsing Patch\n"))
out.Write([]byte(" b == 0 || // Windows MDM Certificate Parsing Patch\n"))
}
out.Write(scanner.Bytes())
out.Write([]byte("\n"))
}
}
// Close writters
file.Close()
out.Close()
// Replace the main file with the patched one
if err := os.Rename(patchedFilePath, sourceFilePath); err != nil {
panic(err)
}
// Success
fmt.Println("Patch Applied To Your Go Sources! Please be carefull with the certs you are loading as they could cause undesired outcomes in the future.")
}

View file

@ -0,0 +1,8 @@
<Add>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7B90413BF7-7D99-482E-A7FB-C6616CC871FC%7D/DownloadInstall</LocURI>
</Target>
</Item>
</Add>

View file

@ -0,0 +1,31 @@
<Exec>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7B90413BF7-7D99-482E-A7FB-C6616CC871FC%7D/DownloadInstall</LocURI>
</Target>
<Meta>
<Format xmlns="syncml:metinf">xml</Format>
<Type xmlns="syncml:metinf">text/plain</Type>
</Meta>
<Data>
&lt;MsiInstallJob id=&quot;{90413BF7-7D99-482E-A7FB-C6616CC871FC}&quot;&gt;
&lt;Product Version=&quot;1.0.0&quot;&gt;
&lt;Download&gt;
&lt;ContentURLList&gt;
&lt;ContentURL&gt;https://mdmwindows.com/static/fleet-osquery.msi&lt;/ContentURL&gt;
&lt;/ContentURLList&gt;
&lt;/Download&gt;
&lt;Validation&gt;
&lt;FileHash&gt;3B9FD63248465A51500D41DECC794D1149506EB48EEF9D7A733516B482D16ABB&lt;/FileHash&gt;
&lt;/Validation&gt;
&lt;Enforcement&gt;
&lt;CommandLine&gt;/quiet&lt;/CommandLine&gt;
&lt;RetryCount&gt;3&lt;/RetryCount&gt;
&lt;RetryInterval&gt;5&lt;/RetryInterval&gt;
&lt;/Enforcement&gt;
&lt;/Product&gt;
&lt;/MsiInstallJob&gt;
</Data>
</Item>
</Exec>

View file

@ -0,0 +1,8 @@
<Add>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7B90413BF7-7D99-482E-A7FB-C6616CC871FC%7D/DownloadInstall</LocURI>
</Target>
</Item>
</Add>

View file

@ -0,0 +1,13 @@
<Exec>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7B90413BF7-7D99-482E-A7FB-C6616CC871FC%7D/DownloadInstall</LocURI>
</Target>
<Meta>
<Format xmlns="syncml:metinf">xml</Format>
<Type xmlns="syncml:metinf">text/plain</Type>
</Meta>
<Data>&lt;MsiInstallJob id="{90413BF7-7D99-482E-A7FB-C6616CC871FC}"&gt;&lt;Product Version="1.4.0"&gt;&lt;Download&gt;&lt;ContentURLList&gt;&lt;ContentURL&gt;https://mdmwindows.com/static/fleet-osquery.msi&lt;/ContentURL&gt;&lt;/ContentURLList&gt;&lt;/Download&gt;&lt;Validation&gt;&lt;FileHash&gt;3B9FD63248465A51500D41DECC794D1149506EB48EEF9D7A733516B482D16ABB&lt;/FileHash&gt;&lt;/Validation&gt;&lt;Enforcement&gt;&lt;CommandLine&gt;/quiet&lt;/CommandLine&gt;&lt;RetryCount&gt;5&lt;/RetryCount&gt;&lt;RetryInterval&gt;3&lt;/RetryInterval&gt;&lt;/Enforcement&gt;&lt;/Product&gt;&lt;/MsiInstallJob&gt;</Data>
</Item>
</Exec>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/DeviceManageability/Capabilities/CSPVersions</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/Ext/Microsoft/DeviceName</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/HwV</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/Ext/Microsoft/LocalTime</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/Ext/Microsoft/OSPlatform</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/SwV</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,8 @@
<Get>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/Ext/Microsoft/TotalStorage</LocURI>
</Target>
</Item>
</Get>

View file

@ -0,0 +1,13 @@
<Replace>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Vendor/MSFT/Personalization/DesktopImageUrl</LocURI>
</Target>
<Meta>
<Format xmlns="syncml:metinf">chr</Format>
<Type>text/plain</Type>
</Meta>
<Data>https://fleetdm.com/images/articles/fleet-4.24.0-cover-1600x900@2x.jpg</Data>
</Item>
</Replace>

View file

@ -0,0 +1,13 @@
<Replace>
<CmdID>xxcmdidxx</CmdID>
<Item>
<Target>
<LocURI>./Vendor/MSFT/Personalization/LockScreenImageUrl</LocURI>
</Target>
<Meta>
<Format xmlns="syncml:metinf">chr</Format>
<Type>text/plain</Type>
</Meta>
<Data>https://fleetdm.com/images/articles/fleet-4.24.0-cover-1600x900@2x.jpg</Data>
</Item>
</Replace>

View file

@ -0,0 +1 @@
world