SQL to RCE
有一些陈旧、庞大的系统中,因为一些复杂的原因,往往仍在使用 sa 账户登入 SQL Server,而在有如此高权限的资料库账户权限下,我们可以轻易利用 xp_cmdshell 来执行系统指令,但是这是几乎不可能的,我们取得的数据库账户必然是低权限,但因为发现的 SQL 注入是堆叠注入,我们仍然可以对表进行 CRUD,运气好可以控制一些网站设定变数的话,甚至可以直接 RCE
就比如我们可以发现某些特殊的数据库:
1 | Database: ASPState |
这个数据库的存在用途是用来保存 ASP.NET 网站应用程式的 session。
在 ASP.NET 网站应用程式里,Session(会话数据,比如用户登录状态、购物车资料)通常是存放在单一网站应用程式的内存里。也就是说用户访问某个站点时,Session 被保存在那台服务器的内存中,如果用户下次访问还是分配到同一台服务器,那么能顺利找到他的 Seesion
但如果放在做了负载均衡,网站后面有多台 ASP.NET 应用服务器,对外表现是一个站点,用户的请求会分配到不同的服务器,但每台服务器的 Seesion 并不共享,就会导致用户状态丢失
为了解决解决上面的问题,就需要集中存储 Seesion,一种常见做法就是所有服务器共享同一份 Seesion 数据
在 ASP.NET 里,只要在 **web.config**
配置文件中添加相关设定,就可以启用 SQL Server Session 状态存储模式。
常见的写法:
1 | <configuration> |
而要在数据库中新建 ASPState 的资料库,可以利用微软自带的小工具 aspnet_regsql 来建立或移除 Session 状态所需的数据库与表,路径在C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regsql.exe
1 | # 建立 ASPState 资料库 |
现在我们了解了这些前置信息后,且又可以控制 ASPState 资料库,便可以做到 RCE
ASP.NET 允许我们在 session 中储存一些物件,例如储存一个 List 物件:Session["secret"] = new List<String>() { "secret string" }
,对于如何将这些物件保存到 SQL Server 上,理所当然使用了序列化机制来处理,而我们又控制了数据库,所有也能执行反序列化,为此我们需要先了解 Session 物件序列化与反序列化的过程
核心操作是通过SqlSessionStateStore.GetItem
还原 Session 物件,我们简单了解一下就好,详细的可以看下面的文章
https://paper.seebug.org/1186/
关键“反序列化链”是怎么打出来的?
(1)读表→拿二进制→反序列化
- ASP.NET 取回 Session 时走
SqlSessionStateStore.GetItem()
→ 内部执行存储过程ASPState.dbo.TempGetStateItem3
。 - 这个 SP 实际上等价于:从
ASPStateTempSessions
取出**SessionItemShort**
(二进制序列化内容)。 - 取出来的字节流会传给
SessionStateUtility.DeserializeStoreData(...)
去 反序列化。
(2)反序列化的分支
SessionStateUtility.Deserialize(...)
会先按固定格式读头部字段:timeout
(Int32,4字节)hasItems
(Boolean,1字节)hasStaticObjects
(Boolean,1字节)
- 然后根据布尔值走两个可能的子反序列化:
SessionStateItemCollection.Deserialize(...)
(针对 Session 里“普通键值”)HttpStaticObjectsCollection.Deserialize(...)
(针对“静态对象集合”)
- 这里我们选择第二支(
HttpStaticObjectsCollection
),因为它内部会调用**AltSerialization.ReadValueFromStream**
,而这条链 ysoserial.net 已有“现成 gadget”。
ysoserial.net 已经有建立 AltSerialization 反序列化 payload 的 plugin,所以可以直接掏出这个利器来使用!下面一行指令就可以产生执行系统指令 calc.exe
的 base64 编码后的 payload
1 | ysoserial.exe -p Altserialization -M HttpStaticObjectsCollection -o base64 -c "calc.exe" |
但是这个生成的 payload 还需要加以修饰,ysoserial.net 的 AltSerialization plugin
所建立的 payload 是攻击 SessionStateItemCollection
或 HttpStaticObjectsCollection
两个类别的反序列化操作,而我们储存在资料库中的 session 序列化资料是由在此之上还额外作了一层包装的 SessionStateUtility 类别处理的
让我们回头看一下程序码,会发现 SessionStateUtility 也只添加了几个 bytes,减化后如下所示
1 | timeout = reader.ReadInt32(); |
对于 Int32 要添加 4 个 bytes,Boolean 则是 1 个 byte,而因为要让程式路径能进入 HttpStaticObjectsCollection 的分支,必须让第 6 个 byte 为 1 才能让条件达成,先将原本从 ysoserial.net 产出的 payload 从 base64 转成 hex 表示,再前后各别添加 6、1 bytes,如下示意图:
1 | timeout false true HttpStaticObjectsCollection eof |
- 头部补:
- 4字节
timeout
(随便,可 0) - 1字节
hasItems
(设0
,否则会走另一支) - 1字节
hasStaticObjects
(**设 ****1**
,才能进入HttpStaticObjectsCollection.Deserialize(...)
)
- 4字节
- 中间放:ysoserial 生成的 主体 payload(先从 base64 解出来,再用 hex/二进制拼接)。
- 尾部补:
0xFF
(ASP.NET 反序列化校验用的 EOF 标志)。
修饰完的这个 payload 就能用来攻击 SessionStateUtility 类别了
最后的步骤就是将恶意的序列化内容注入进数据库,如果正常浏览目标网站时有出现 ASP.NET_SessionId 的 Cookie 就代表已经有一笔对应的 Session 记录储存在资料库里,所以我们只需要执行如下的 SQL Update 语句:
1 | id=1; UPDATE ASPState.dbo.ASPStateTempSessions |
分别将 {ASP.NET_SessionId}
替换成自己的ASP.NET_SessionId 的 Cookie 值以及 {Hex_Encoded_Payload}
替换成前面准备好的序列化 payload 即可
假如没有 ASP.NET_SessionId 怎么办?这表示目标还未储存任何资料在 Session 中,那没有 cookie 我们就硬塞一个 cookie 给他,ASP.NET 的 SessionId 是透过乱数产生的 24 个字元,但使用了客制化的字元集,理论上可以直接使用以下的 Python script 产生一组 SessionId,例如:plxtfpabykouhu3grwv1j1qw,之后带上 Cookie: ASP.NET_SessionId=plxtfpabykouhu3grwv1j1qw浏览任一个 aspx 页面,理论上 ASP.NET 就会自动在数据库里添加一笔记录
1 | import random |
等到 Payload 顺利注入后,只要再次用这个 Cookie ASP.NET_SessionId=plxtfpabykouhu3grwv1j1qw
浏览任何一个 aspx 页面,就会触发反序列化执行任意系统指令