0x00 前言
本文需要读者对 ViewState 有一定的了解。
ASP.NET 的 ViewState 数据的序列化和反序列化均由 ObjectStateFormatter
完成。对于签名/加密,是由多个参数因素决定了生成的数据,其中 CompatibilityMode
的值是分支的节点:
-
1. 当 CompatibilityMode
为Framework20SP1
时,可以由GetEncodeData
或EncryptOrDecryptData
函数生成最终的数据。 -
2. 当 CompatibilityMode
为Framework45
时,只能由Purpose
生成数据。
本文主要是针对 DOTNET20
和 DOTNET45
两种类型的 ViewState 数据的一个结构进行解析,通俗的解释爆破的原理。
下图是作者当时在学习时编写的导图。
PS:由于 Protect 的结果是直接调用代码的输出格式,但实际上把 ||
换成 +
就好理解了。
核心思路:由于 ViewState 都是数据+数据签名,因此最直接的爆破思路就是根据签名的长度,拆分 ViewState,然后再使用对应的签名算法进行签名,对比前后两个签名是否一致,一致则继续往下决定是否解密。
0x01 当 CompatibilityMode
为 Framework20SP1
时
当 CompatibilityMode
为 Framework20SP1
时,存在三种情况:
-
1. 仅进入 GetEncodeData
函数,那生成的数据就是不加密的 ViewState(仅序列化)+ 签名。 -
2. 当签名算法是 3DES/AES
时,那么先进入GetEncodeData
函数,再进入EncryptOrDecryptData
函数。 -
3. 仅进入 EncryptOrDecryptData
函数。
1.1 GetEncodeData
函数
当签名算法不是 3DES/AES
时(也就是非加密时),那么我们的 ViewState 数据就是 buf+hmac(buf+modifier),那么从爆破的角度,它是有更优解的,可以直接解析 ViewState,从而获取到签名长度,就得出对应签名算法。
由于 Buf 是经过 ObjectStateFormatter
序列化的,因此看看反序列化的代码(挑重点看):
publicvoidSerialize(Stream outputStream, object stateGraph)
{
if (outputStream == null)
thrownew ArgumentNullException(nameof (outputStream));
this.InitializeSerializer();
ObjectStateFormatter.SerializerBinaryWriter writer = new ObjectStateFormatter.SerializerBinaryWriter(outputStream);
writer.Write(byte.MaxValue); // 数据开头直接写入 255
writer.Write((byte) 1); // 第二个则是 1
this.SerializeValue(writer, stateGraph);
}
privatevoidSerializeValue(ObjectStateFormatter.SerializerBinaryWriter writer, objectvalue)
{
// 创建一个 Stack 对象,用于存储需要序列化的对象。 这使得该方法能够处理嵌套的对象结构。
// 通过循环进行值类型处理
// 也就是说这里通过 white 对控件等数据全部进行序列化
}
publicobjectDeserialize(Stream inputStream)
{
if (inputStream == null)
{
thrownew ArgumentNullException("inputStream");
}
Exception innerException = null;
InitializeDeserializer();
SerializerBinaryReader serializerBinaryReader = new SerializerBinaryReader(inputStream);
try
{
byte b = serializerBinaryReader.ReadByte();
if (b == byte.MaxValue) // 查看第一个 byte 是否为 255
{
byte b2 = serializerBinaryReader.ReadByte();
if (b2 == 1) // 查看第二个 byte 是否为 1
{
return DeserializeValue(serializerBinaryReader);
}
}
}
catch (Exception ex)
{
innerException = ex;
}
thrownew ArgumentException(SR.GetString("InvalidSerializedData"), innerException);
}
privateobjectDeserializeValue(SerializerBinaryReader reader)
{
byte b = reader.ReadByte(); // 第三个 byte 表示该进入什么分支对数据进行处理,自己编写生成的 ViewState 固定为 50,但是实际环境是多层嵌套,不是固定的
switch (b)
{
case100:
returnnull;
case101:
returnstring.Empty;
case5:
return reader.ReadString();
case102:
return0;
....
case50:
{
int num = reader.ReadEncodedInt32(); // 第四个 byte 是使用 7-bit encoded integer 格式来表示的长度
byte[] buffer = newbyte[num];
if (num != 0)
{
reader.Read(buffer, 0, num);
}
object result = null;
MemoryStream memoryStream = GetMemoryStream();
try
{
memoryStream.Write(buffer, 0, num);
memoryStream.Position = 0L;
IFormatter formatter = new BinaryFormatter();
result = formatter.Deserialize(memoryStream);
}
catch (Exception exception)
{
if (_throwOnErrorDeserializing)
{
throw;
}
WebBaseEvent.RaiseSystemEvent(SR.GetString("Webevent_msg_OSF_Deserialization_Binary"), this, 3011, 0, exception);
}
finally
{
ReleaseMemoryStream(memoryStream);
}
return result;
}
default:
thrownew InvalidOperationException(SR.GetString("InvalidSerializedData"));
}
}
在实际环境当中,由于 ViewState 是多层嵌套的数据结构,是根据页面上所有控件的状态序列化而成,因此在反序列化时是需要逐个解析。但实际过程中,我们不需要去理解这个过程,我们只需要记住:
对于未加密的 ViewState,数据开头的 Hex 均为
0xff,0x01
,也就是对应代码当中的255,1
,接下来则是所嵌套的控件依次反序列化解析内容。当解析控件的反序列化内容后,剩下的就是签名。(如果没有剩余的签名,那么这个网站就是可以任意进行 ViewState 反序列化攻击)
在得到剩下的签名数据后,可以根据签名的长度,可以得出是什么签名算法,因此可以排除掉其他的算法,大大提升了爆破的速度。
下面算法对应的签名长度:
|
|
|
|
|
|
|
|
|
|
|
|
编写解析 ViewState 的代码可以直接对标官方代码中的 DeserializeValue
,也可以参考:BurpSuite 的 ViewState 插件:https://github.com/raise-isayan/ViewStateDecoder/blob/master/src/main/java/aspx/viewstate/ViewStateParser.java
随便一个 ViewState 解析的结果如下:
ParseResult {
Value: Pair {
First: Pair {
First: "-162691655"
Second: Pair {
First: Array[2] {
[0]: "Counter"
[1]: 0
}
Second: Array[2] {
[0]: 3
[1]: Pair {
First: null
Second: Array[2] {
[0]: 1
[1]: Pair {
First: Pair {
First: Array[2] {
[0]: "Text"
[1]: "Counter: 0"
}
Second: null
}
Second: null
}
}
}
}
}
}
Second: null
}
Remaining: Array[32] {
[0]: 84
[1]: 191
[2]: 239
[3]: 49
[4]: 29
[5]: 150
[6]: 237
[7]: 229
[8]: 189
[9]: 125
[10]: 35
[11]: 215
[12]: 159
[13]: 252
[14]: 192
[15]: 87
[16]: 16
[17]: 119
[18]: 72
[19]: 86
[20]: 112
[21]: 3
[22]: 7
[23]: 228
[24]: 112
[25]: 215
[26]: 22
[27]: 202
[28]: 155
[29]: 165
[30]: 31
[31]: 248
}
}
Remaining 就是余下的签名数据。
1.2 EncryptOrDecryptData
函数
其实,进入 EncryptOrDecryptData
函数生成的数据,都是经过加密并附带签名的数据,也就是图中的 E(iv+buf+modifier) + HMAC(E(iv+buf+modifier))
但是经过加密的数据,没有像上面那样具备标识值,因此对于签名算法只能通过遍历进行枚举。
这里我们并不要求能够解密经过加密的数据,只要有签名密钥就可以生成 ViewState 数据。但当签名算法是 3DES/AES
时,会出现一个特殊的情况,由于签名算法是使用 SHA1,因此旧版本爆破结果会显示为 SHA1
。所以这种情况是需要对后续的数据进行解密的,在解密完成之后,还需要对解密后的数据进行解析,查看是否有剩余的 Remaining,如果有剩余且长度为 20,那么输出的签名算法是显示:AES/3DES
。
当签名算法为 AES/3DES
的时候,必须要填写DecryptionKey
才能连接成功,并且 ValidationKey
可以填写为 SHA1/AES/3DES
,这三种都可以连接成功。
0x02 当 CompatibilityMode
为 Framework45
时
本质上和 EncryptOrDecryptData
是没有区别的,仅多了使用 SP 800-108
的派生算法,并且数据中多了加密所使用的 IV。解密后去除这些内容即可。
0x03 当前工具的爆破结果
由于某些特殊环境,因此新增了一个 Proxy 可供填写。
3.1 常规数据
3.2 加密的数据
-
• .NET 20SP1
-
• .NET 45
3.3 当签名算法是 3DES/AES
时
原文始发于微信公众号(RowTeam):【ViewState 指北】校验和解密数据
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论