某次.net代码审计的一些收获,通过machineKey伪造任意用户身份

起因[#]

某次机缘巧合,领导给了一套国内某主流.net技术的BPM系统源码,要求审计,于是便开始了这次审计之旅。。。

BPM(业务流程管理系统),这套系统采用前后端分离架构,前端ExtJS,后端.net的一般处理程序+windows服务开放socket端口1580,一般处理程序与服务器本地1580端口通信。数据库Oracle/SQLServer等。

环境搭建好后,使用VS打开站点,使用附加到进程,选择IIS进程,开启调试,配合Reflector查看DLL封装。

场景[#]

看了没多久发现一个任意文件下载的漏洞,SQL注入,文件上传任意代码执行也存在,但是只有任意文件文件下载是不需要验证的,其他漏洞都需要验证身份后才能利用成功,如何才能获取一个身份扩大利用面呢?仔细查看登录逻辑代码并不存在sql注入,系统支持中文,数字,字母格式的用户名,弱口令也是不好搞的,况且也未发现敏感信息泄露。等等,不是还有任意文件下载吗?下载web.config后数据库直连不可以吗,还真不行,由于业务流程一般涉及到企业的核心,基本都部署在内网,通过NAT方式映射到公网访问,或者纯内网方式访问不对外,这类情况基本没有对外映射数据库端口的(主要看运气,后来发现这套程序的所有密钥都是相同的..未随机化处理,牛逼....)。

仔细观察登录后的身份信息:

Cookie: ys-dLogin.PositionId=xxx; ys-dLogin.UserId=xxxxxx; ASP.NET_SessionId=atn2xhww13wfclf3ycs1045l; UserDisplayName=xxxxxxx; .ASPXAUTH=DCD91077D0C3FAF548406C2E885A9E202C7D16C832E991807B7C4E8718B3D2760C01F50BC3007CAA1FE6E48AB84822F8AA27E47B8FCF0D6216321B4410E034A24F0090406B283354FCFABF9AF

使用这个cookie通过PostMan调用后端接口发现其真正具有身份验证功能的只有.ASPXAUTH这个值。那么这个值是否可以伪造?

答案是肯定的。web.config里定义了这个:

<machineKey validationKey="ABAA84D7EC4BB56D75D237CECFFB9628809BDB8BF9999CD64568A145BE59719F" decryptionKey="ABAA84D7EC4BB56D75D237CECFFB9628809BDB8BF9999CD64568A145BE59719F" validation="SHA1" decryption="AES" />

看到这里老.net开发肯定就明白是怎么回事了,是的,这套系统使用了微软FormsAuthentication类作为身份认证使用的。该类的实现方法在System.Web.dll,通过该认证,可以把用户Name 和部分用户数据存储在Cookie中,通过基本的条件设置可以,很简单的实现基本的身份角色认证。

FormsAuthenticationTicket类用于创建一个对象,该对象表示 forms 身份验证用于标识已经过身份验证的用户的身份验证票证。 Forms 身份验证票证的属性和值与存储在 cookie 或 URL 中的加密字符串进行转换。

FormsAuthentication类提供了一 Encrypt 种方法,用于创建一个字符串值,该字符串值可以存储在 cookie 中,也可以存储在 URL 中 FormsAuthenticationTicket 。 FormsAuthentication类还提供了一 Decrypt 种方法,用于 FormsAuthenticationTicket 根据从 forms 身份验证 cookie 或 URL 检索到的加密的身份验证票证来创建对象。

FormsAuthenticationTicket可使用类的属性访问当前经过身份验证的用户的 Ticket FormsIdentity 。 可以 FormsIdentity 通过 Identity 将当前的属性转换 User 为类型来访问当前的对象 FormsIdentity 。

生成Cookie的代码,微软官方的Demo:

private void Login_Click(Object sender, EventArgs e)
  {
    // Create a custom FormsAuthenticationTicket containing
    // application specific data for the user.

    string username     = UserNameTextBox.Text;
    string password     = UserPassTextBox.Text;
    bool   isPersistent = false;

    if (Membership.ValidateUser(username, password))
    {
      string userData = "ApplicationSpecific data for this user.";

      FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
        username,
        DateTime.Now,
        DateTime.Now.AddMinutes(30),
        isPersistent,
        userData,
        FormsAuthentication.FormsCookiePath);

      // Encrypt the ticket.
      string encTicket = FormsAuthentication.Encrypt(ticket);

      // Create the cookie.
      Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));

      // Redirect back to original URL.
      Response.Redirect(FormsAuthentication.GetRedirectUrl(username, isPersistent));
    }
    else
    {
      Msg.Text = "Login failed. Please check your user name and password and try again.";
    }
  }

那么知道了怎么生成的,于是定位到代码的Login方法,关键逻辑如下:

string privateKey = (string)XXTempStorageManager.CurrentStore.Load(keystore);

                    RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(1024);
                    rsaProvider.FromXmlString(privateKey);

                    uid = System.Text.Encoding.UTF8.GetString(rsaProvider.Decrypt(Convert.FromBase64String(uid), false));
                    pwd = System.Text.Encoding.UTF8.GetString(rsaProvider.Decrypt(Convert.FromBase64String(pwd), false));
                }

                if (BPMConnection.Authenticate(XXAuthHelper.BPMServerName, XXAuthHelper.BPMServerPort, uid, pwd, out realAccount, out token))
                {
                    XXAuthHelper.SetAuthCookie(realAccount, token);
                    XXAuthHelper.ClearLogoutFlag();

                    rv[XXJsonProperty.success] = true;
                    rv["errorMessage"] = Resources.XXStrings.Aspx_Login_Success;
                }

首先从上下文获取请求中的key,此key为页面Load时由后端动态生成的,然后分别获取加密后的用户名和密码,使用key进行解密,调用:

BPMConnection.Authenticate(XXAuthHelper.BPMServerName, XXAuthHelper.BPMServerPort, uid, pwd, out realAccount, out token),

此函数调用了后端的windows服务,逻辑复杂,命名及其不规范(个人水平太菜...),总之没找到生成规则,推测是和web.config定义的SecurityKey有关,这个函数返回一个token,该token并不产生变化,所以可以写死。
继续跟入:

XXAuthHelper.SetAuthCookie(realAccount, token);

这里就出现了熟悉的画面了:

public static void SetAuthCookie(string account, string token)
    {
        DateTime cookieIssuedDate = DateTime.Now;
        var ticket = new FormsAuthenticationTicket(100,
            account,
            cookieIssuedDate,
            cookieIssuedDate.AddMinutes(FormsAuthentication.Timeout.TotalMinutes),
            false,
            token,
            FormsAuthentication.FormsCookiePath);

        string cookieValue = FormsAuthentication.Encrypt(ticket);
        HttpContext.Current.Response.Cookies.Remove(FormsAuthentication.FormsCookieName);
        SetCookie(HttpContext.Current.Response, FormsAuthentication.FormsCookieName, cookieValue);
    }

所以winform简单写个工具,把machineKey放入app.config中:
14ccbba471608bbe1953ae15250412b7.png-quanzi_meitu_7

接下来就是Burpsuit一把梭,拦截响应头信息,替换cookie值,放包,刷新页面,成功伪造默认管理员用户登陆。
.net程序在某些只有任意文件下载的场景下,配合web.config的machineKey或许有新突破。

相关URL:[#]

CSDN:https://blog.csdn.net/zerommc/article/details/80832281

MSDN:https://docs.microsoft.com/zh-cn/dotnet/api/system.web.security.formsauthentication?redirectedfrom=MSDN&view=netframework-4.8

4 个赞

很不错,作为一个也经常搞.net审计的来说,写的很完整。我就懒不少

@ admin05,您好管理,申请一个wiki账号