在当前的HTTPContext中生成一个新的ASP.NET会话

29
由于我们对管道中的某些产品进行渗透测试的结果,一个看起来当时很容易解决的问题正在变得棘手起来。
这当然不应该是这样的,我是说为什么仅为当前HTTPContext生成一个全新的会话如此困难?太奇怪了!无论如何,我编写了一个小巧的实用程序类来“只是这样做”:
(抱歉代码格式/高亮/Visual Basic,我一定做错了些什么)

Imports System.Web
Imports System.Web.SessionState

Public Class SwitchSession

    Public Shared Sub SetNewSession(ByVal context As HttpContext)
        ' This value will hold the ID managers action to creating a response cookie
        Dim cookieAdded As Boolean
        ' We use the current session state as a template
        Dim state As HttpSessionState = context.Session
        ' We use the default ID manager to generate a new session id
        Dim idManager As New SessionIDManager()
        ' We also start with a new, fresh blank state item collection
        Dim items As New SessionStateItemCollection()
        ' Static objects are extracted from the current session context
        Dim staticObjects As HttpStaticObjectsCollection = _
            SessionStateUtility.GetSessionStaticObjects(context)
        ' We construct the replacement session for the current, some parameters are new, others are taken from previous session
        Dim replacement As New HttpSessionStateContainer( _
                 idManager.CreateSessionID(context), _
                 items, _
                 staticObjects, _
                 state.Timeout, _
                 True, _
                 state.CookieMode, _
                 state.Mode, _
                 state.IsReadOnly)
        ' Finally we strip the current session state from the current context
        SessionStateUtility.RemoveHttpSessionStateFromContext(context)
        ' Then we replace the assign the active session state using the replacement we just constructed
        SessionStateUtility.AddHttpSessionStateToContext(context, replacement)
        ' Make sure we clean out the responses of any other inteferring cookies
        idManager.RemoveSessionID(context)
        ' Save our new cookie session identifier to the response
        idManager.SaveSessionID(context, replacement.SessionID, False, cookieAdded)
    End Sub

End Class

在请求的其余部分中它能够正常工作,并且正确地识别自己为新会话(例如,HTTPContext.Current.Session.SessionID 返回新生成的会话标识符)。

于是当下一个请求到达服务器时,HTTPContext.Session(一个HTTPSessionState对象)正确地使用相同的SessionID进行标识,但将IsNewSession设置为True,并为空,失去了前一个请求中设置的所有会话值。

因此,在初始请求中删除前一个HTTPSessionState对象时,必须有一些特殊的处理,例如事件处理程序、回调等,可以处理跨请求保留会话数据的问题,或者我可能错过了某些东西?

有人有任何神奇的方法可分享吗?


1
我通过为SwitchSession类添加一些状态(replacement会话)并为活动的ASP.NET应用程序实例连接SessionStateModule事件来进化它。当Start事件触发时,它会检查ASP.NET生成的会话是否具有相同的SessionID,并将所有会话状态值从上一个请求复制到其中。显然,只有在所有请求都通过处理前一个请求的HTTPApplication实例时才有效。我正在使用反射器深入挖掘SessionStateModule,但这不是很好看。请投票支持这个问题! - Rabid
我跟你差不多也到了同样的地方(是通过搜索“RemoveHttpSessionStateFromContext”找到你的页面的)。不幸的是,我也碰到了和你一样的困难 - 似乎无法生成新的会话。关键当然是SessionStateModule.CompleteAcquiredState(),但非常难以获得 - Yudhi的反射方法可能是获取它的一种方式,但我不确定值得麻烦。我必须说,尽管我喜欢C#,.NET的API令人非常失望 - 他们怎么会不公开这个! - marq
请注意:CompleteAcquiredState() 调用 SessionStateUtility.AddDelayedHttpSessionStateToContext(),这个方法会为新的会话执行所有必要的操作。 - marq
7个回答

43

我想分享我的奇思妙想。实际上,不,它还没有那么神奇...我们需要测试和改进代码。 我只在with-cookie、InProc会话模式中测试了这些代码。将这些方法放在您的页面内,并在需要重新生成ID的位置调用它(请将您的Web应用程序设置为完全信任):

void regenerateId()
{
    System.Web.SessionState.SessionIDManager manager = new System.Web.SessionState.SessionIDManager();
    string oldId = manager.GetSessionID(Context);
    string newId = manager.CreateSessionID(Context);
    bool isAdd = false, isRedir = false;
    manager.SaveSessionID(Context, newId, out isRedir, out isAdd);
    HttpApplication ctx = (HttpApplication)HttpContext.Current.ApplicationInstance;
    HttpModuleCollection mods = ctx.Modules;
    System.Web.SessionState.SessionStateModule ssm = (SessionStateModule)mods.Get("Session");
    System.Reflection.FieldInfo[] fields = ssm.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
    SessionStateStoreProviderBase store = null;
    System.Reflection.FieldInfo rqIdField = null, rqLockIdField = null, rqStateNotFoundField = null;
    foreach (System.Reflection.FieldInfo field in fields)
    {
        if (field.Name.Equals("_store")) store = (SessionStateStoreProviderBase)field.GetValue(ssm);
        if (field.Name.Equals("_rqId")) rqIdField = field;
        if (field.Name.Equals("_rqLockId")) rqLockIdField = field;
        if (field.Name.Equals("_rqSessionStateNotFound")) rqStateNotFoundField = field;
    }
    object lockId = rqLockIdField.GetValue(ssm);
    if ((lockId != null) && (oldId !=null)) store.ReleaseItemExclusive(Context, oldId, lockId);
    rqStateNotFoundField.SetValue(ssm, true);
    rqIdField.SetValue(ssm, newId);
}

我一直在研究 .NET 源代码(可在 http://referencesource.microsoft.com/netframework.aspx 获取),发现无法在未修改会话管理机制的内部情况下重新生成 SessionID。因此,我就通过修改 SessionStateModule 的内部字段来实现这个功能,这样它就会将当前会话保存到一个新的 ID 中。也许当前的 HttpSessionState 对象仍具有先前的 Id,但据我所知,SessionStateModule 忽略了它。它只在必须将状态保存到某处时使用内部 _rqId 字段。我尝试过其他方法,比如将 SessionStateModule 复制到一个带有重新生成 ID 功能的新类中(我计划用这个类替换 SessionStateModule),但失败了,因为它目前有对其他内部类(如 InProcSessionStateStore)的引用。使用反射进行黑客攻击的缺点是我们需要将应用程序设置为“完全信任”。

哦,如果你真的需要 VB 版本,请尝试这些:

Sub RegenerateID()
    Dim manager
    Dim oldId As String
    Dim newId As String
    Dim isRedir As Boolean
    Dim isAdd As Boolean
    Dim ctx As HttpApplication
    Dim mods As HttpModuleCollection
    Dim ssm As System.Web.SessionState.SessionStateModule
    Dim fields() As System.Reflection.FieldInfo
    Dim rqIdField As System.Reflection.FieldInfo
    Dim rqLockIdField As System.Reflection.FieldInfo
    Dim rqStateNotFoundField As System.Reflection.FieldInfo
    Dim store As SessionStateStoreProviderBase
    Dim field As System.Reflection.FieldInfo
    Dim lockId
    manager = New System.Web.SessionState.SessionIDManager
    oldId = manager.GetSessionID(Context)
    newId = manager.CreateSessionID(Context)
    manager.SaveSessionID(Context, newId, isRedir, isAdd)
    ctx = HttpContext.Current.ApplicationInstance
    mods = ctx.Modules
    ssm = CType(mods.Get("Session"), System.Web.SessionState.SessionStateModule)
    fields = ssm.GetType.GetFields(System.Reflection.BindingFlags.NonPublic Or System.Reflection.BindingFlags.Instance)
    store = Nothing : rqLockIdField = Nothing : rqIdField = Nothing : rqStateNotFoundField = Nothing
    For Each field In fields
        If (field.Name.Equals("_store")) Then store = CType(field.GetValue(ssm), SessionStateStoreProviderBase)
        If (field.Name.Equals("_rqId")) Then rqIdField = field
        If (field.Name.Equals("_rqLockId")) Then rqLockIdField = field
        If (field.Name.Equals("_rqSessionStateNotFound")) Then rqStateNotFoundField = field
    Next
    lockId = rqLockIdField.GetValue(ssm)
    If ((Not IsNothing(lockId)) And (Not IsNothing(oldId))) Then store.ReleaseItemExclusive(Context, oldId, lockId)
    rqStateNotFoundField.SetValue(ssm, True)
    rqIdField.SetValue(ssm, newId)

End Sub

1
感谢您的贡献,我的结论也类似,会话状态模块的本地行为需要进行操作。提升到完全信任权限是一件遗憾的事情,特别是因为它是为了解决安全问题(我记得这里的问题是会话固定)。不过还是做得很好 :) - Rabid
运行得很好。Session.SessionID在请求的剩余部分保留旧值。有任何更新吗? - Vaibhav Garg
非常感谢您提供的代码!我在使用从当前上下文创建的SimpleWorkerRequest调用HttpRuntime.ProcessRequest()时遇到了一个问题,即如果先前的请求已经写入了Session,则请求会开始出现问题。使用您提供的代码在子请求执行期间切换ID解决了这个问题。+1 - mawtex
这种方法肯定会带领我们走向会话ID的乐土 :) - Rabid
这实际上并没有删除旧会话,仍然容易受到会话固定攻击。你应该使用 store.RemoveItem 来代替。SessionStateStoreData 参数可以通过 _rqItem 字段访问。 - Can Gencer
显示剩余4条评论

5

如果您注重安全,并希望删除旧字段的C#版本,请使用以下代码。

private static void RegenerateSessionId()
{

    // Initialise variables for regenerating the session id
    HttpContext Context = HttpContext.Current;
    SessionIDManager manager = new SessionIDManager();
    string oldId = manager.GetSessionID(Context);
    string newId = manager.CreateSessionID(Context);
    bool isAdd = false, isRedir = false;

    // Save a new session ID
    manager.SaveSessionID(Context, newId, out isRedir, out isAdd);

    // Get the fields using the below and create variables for storage
    HttpApplication ctx = HttpContext.Current.ApplicationInstance;
    HttpModuleCollection mods = ctx.Modules;
    SessionStateModule ssm = (SessionStateModule)mods.Get("Session");
    FieldInfo[] fields = ssm.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
    SessionStateStoreProviderBase store = null;
    FieldInfo rqIdField = null, rqLockIdField = null, rqStateNotFoundField = null;
    SessionStateStoreData rqItem = null;

    // Assign to each variable the appropriate field values
    foreach (FieldInfo field in fields)
    {
        if (field.Name.Equals("_store")) store = (SessionStateStoreProviderBase)field.GetValue(ssm);
        if (field.Name.Equals("_rqId")) rqIdField = field;
        if (field.Name.Equals("_rqLockId")) rqLockIdField = field;
        if (field.Name.Equals("_rqSessionStateNotFound")) rqStateNotFoundField = field;
        if (field.Name.Equals("_rqItem")) rqItem = (SessionStateStoreData)field.GetValue(ssm);
    }

    // Remove the previous session value
    object lockId = rqLockIdField.GetValue(ssm);
    if ((lockId != null) && (oldId != null))
        store.RemoveItem(Context, oldId, lockId, rqItem);

    rqStateNotFoundField.SetValue(ssm, true);
    rqIdField.SetValue(ssm, newId);
}

1
被接受的答案相比,这个代码段捕获了_rqItem对象,因此它可以调用store.RemoveItem而不是store.ReleaseItemExclusive,这似乎是使旧会话失效的重要步骤。 - mlhDev

3
如Can Gencer所说,ReleaseItemExclusive不会从存储中删除旧会话,导致该会话最终过期并在Global.asax中调用Session_End。这在生产环境中给我们带来了巨大的问题,因为我们在Session_End中清除了线程标识,因此用户会在线程上自动失去身份验证。
下面是经过更正的可行代码。
Dim oHTTPContext As HttpContext = HttpContext.Current

Dim oSessionIdManager As New SessionIDManager
Dim sOldId As String = oSessionIdManager.GetSessionID(oHTTPContext)
Dim sNewId As String = oSessionIdManager.CreateSessionID(oHTTPContext)

Dim bIsRedir As Boolean = False
Dim bIsAdd As Boolean = False
oSessionIdManager.SaveSessionID(oHTTPContext, sNewId, bIsRedir, bIsAdd)

Dim oAppContext As HttpApplication = HttpContext.Current.ApplicationInstance

Dim oModules As HttpModuleCollection = oAppContext.Modules

Dim oSessionStateModule As SessionStateModule = _
  DirectCast(oModules.Get("Session"), SessionStateModule)

Dim oFields() As FieldInfo = _
  oSessionStateModule.GetType.GetFields(BindingFlags.NonPublic Or _
                                        BindingFlags.Instance)

Dim oStore As SessionStateStoreProviderBase = Nothing
Dim oRqIdField As FieldInfo = Nothing
Dim oRqItem As SessionStateStoreData = Nothing
Dim oRqLockIdField As FieldInfo = Nothing
Dim oRqStateNotFoundField As FieldInfo = Nothing

For Each oField As FieldInfo In oFields
    If (oField.Name.Equals("_store")) Then
        oStore = DirectCast(oField.GetValue(oSessionStateModule), _
                            SessionStateStoreProviderBase)
    End If
    If (oField.Name.Equals("_rqId")) Then
        oRqIdField = oField
    End If
    If (oField.Name.Equals("_rqLockId")) Then
        oRqLockIdField = oField
    End If
    If (oField.Name.Equals("_rqSessionStateNotFound")) Then
        oRqStateNotFoundField = oField
    End If
    If (oField.Name.Equals("_rqItem")) Then
        oRqItem = DirectCast(oField.GetValue(oSessionStateModule), _
                             SessionStateStoreData)
    End If
Next

If oStore IsNot Nothing Then

    Dim oLockId As Object = Nothing

    If oRqLockIdField IsNot Nothing Then
        oLockId = oRqLockIdField.GetValue(oSessionStateModule)
    End If

    If (oLockId IsNot Nothing) And (Not String.IsNullOrEmpty(sOldId)) Then
        oStore.ReleaseItemExclusive(oHTTPContext, sOldId, oLockId)
        oStore.RemoveItem(oHTTPContext, sOldId, oLockId, oRqItem)
    End If

    oRqStateNotFoundField.SetValue(oSessionStateModule, True)
    oRqIdField.SetValue(oSessionStateModule, sNewId)

End If

Max,请你把它翻译成C#吗? - vkelman

0

您是否考虑使用 HttpSessionState.Abandon 方法?这应该可以清除所有内容。然后启动一个新会话并用您从上面的代码中存储的所有项目填充它。

Session.Abandon(); 应该足够了。否则,如果仍然固执,您可以尝试进行更多调用:

Session.Contents.Abandon();
Session.Contents.RemoveAll(); 

3
不幸的是,这种方法似乎重复使用相同的会话标识符。我需要生成一个新的会话标识符 :( - Rabid

0

你能不能只设置:

<sessionState regenerateExpiredSessionId="False" />

在web.config中,然后使用Ahmad建议的解决方案吗?


3
很不幸,它仍然保留了活动的“SessionID”,当下一个请求到达时会使用它。在这个上下文中,“过期”的意思是什么?我没有使会话“过期”-我看不到除了使客户端cookie过期之外,还有其他使会话“过期”的方法。但那对我的活动的“HTTPContext”并没有帮助。即便如此,使用这种方法,当我放弃会话并删除所有内容后,我会认证用户并在会话中放置某些内容,因为会话已被“放弃”,所以在下一个请求中会是空的。:(:(:( - Rabid

0

如何在ASP.NET中登录后更改会话ID

对于那些现在正在搜索并看到所有那些反射黑客技巧并且正在努力解决会话固定问题的人来说,唯一的方法是让ASP.NET创建一个新的会话ID,就是伪造一个SessionIDManager,在登录页面上返回null。这样ASP.NET就会认为浏览器从未发送过cookie。

以下是步骤:

  • 创建一个CustomSessionIDManager,注意YourLoginController/LoginMethod
public class CustomSessionIDManager : ISessionIDManager
{
    private readonly SessionIDManager _sessionIDManager;

    public CustomSessionIDManager()
    {
        _sessionIDManager = new SessionIDManager();
    }

    public string CreateSessionID(HttpContext context)
    {
        return _sessionIDManager.CreateSessionID(context);
    }

    public string GetSessionID(HttpContext context)
    {
        var path = context.Request.Path.ToString();
        if (path.EndsWith("YourLoginController/LoginMethod"))
            return null;
        return _sessionIDManager.GetSessionID(context);
    }

    public void Initialize()
    {
        _sessionIDManager.Initialize();
    }

    public bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue)
    {
        return _sessionIDManager.InitializeRequest(context, suppressAutoDetectRedirect, out supportSessionIDReissue);
    }

    public void RemoveSessionID(HttpContext context)
    {
        _sessionIDManager.RemoveSessionID(context);
    }

    public void SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)
    {
        _sessionIDManager.SaveSessionID(context, id, out redirected, out cookieAdded);
    }

    public bool Validate(string id)
    {
        return _sessionIDManager.Validate(id);
    }
}```

- Put it on the `web.config`:

  

     <system.web>
           <sessionState sessionIDManagerType="CustomSessionIDManager" />
     </system.web>


Now it should return a new session for each login.

-1
请针对MVC4使用以下代码:

 System.Web.SessionState.SessionIDManager manager = new System.Web.SessionState.SessionIDManager();
            HttpContext Context = System.Web.HttpContext.Current;
            string oldId = manager.GetSessionID(Context);
            string newId = manager.CreateSessionID(Context);
            bool isAdd = false, isRedir = false;
            manager.SaveSessionID(Context, newId, out isRedir, out isAdd);
            HttpApplication ctx = (HttpApplication)System.Web.HttpContext.Current.ApplicationInstance;
            HttpModuleCollection mods = ctx.Modules;
            System.Web.SessionState.SessionStateModule ssm = (SessionStateModule)mods.Get("Session");
            System.Reflection.FieldInfo[] fields = ssm.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
            SessionStateStoreProviderBase store = null;
            System.Reflection.FieldInfo rqIdField = null, rqLockIdField = null, rqStateNotFoundField = null;
            foreach (System.Reflection.FieldInfo field in fields)
            {
                if (field.Name.Equals("_store")) store = (SessionStateStoreProviderBase)field.GetValue(ssm);
                if (field.Name.Equals("_rqId")) rqIdField = field;
                if (field.Name.Equals("_rqLockId")) rqLockIdField = field;
                if (field.Name.Equals("_rqSessionStateNotFound")) rqStateNotFoundField = field;
            }
            object lockId = rqLockIdField.GetValue(ssm);
            if ((lockId != null) && (oldId != null)) store.ReleaseItemExclusive(Context, oldId, lockId);
            rqStateNotFoundField.SetValue(ssm, true);
            rqIdField.SetValue(ssm, newId);

同意,这几乎是接受答案的精确复制。 - mlhDev
.NET Core 中是否支持这种结构? - Marty Jones
我真的很喜欢你所做的微小修改,因为它解决了我最初在代码中实现时遇到的许多错误。 - PKD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接