最后,该http远程代码执行漏洞虽然在类型上仍属于UAF(use after free),但该漏洞实现exp有两个比较重要的前提条件待解决。1是该漏洞的触发原理,需要服务端的http代码中,包含一个流程错误的构造操作使HTTP FastTracker因为意外而提前释放,这需要http服务端开发人员在他的代码中刚好包含这样的一个逻辑。2另外则是该漏洞的利用实现方式,即通过布局HTTP FastTracker的未初始化的内存,通过漏洞触发去操作我们自己伪造的MDL指针结构。为了要执行代码,除了我们可能需要继续分析一种可能的信息泄露方式(如构造读写源语或者利用MDL本身的一些机制),但我们仍有大量的后续代码执行方式上的尝试工作要做。所以,就目前短时间来说,该漏洞被利用的困难程度可能较大。
The authentication bypass depends on the error value getting ignored. It was ignored on line 1121, but it's still stored in the error parameter, so it also needs to be ignored by the caller. The block of code above has a temporary variable named implied_error, which is ignored when implied_result isn't null. That's the crucial step that makes the bypass possible.
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>The systemd Project</vendor>
<vendor_url>http://www.freedesktop.org/wiki/Software/systemd</vendor_url>
<action id="org.freedesktop.systemd1.manage-unit-files">
<description gettext-domain="systemd">Manage system service or unit files</description>
<message gettext-domain="systemd">Authentication is required to manage system service or unit files.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.imply">org.freedesktop.systemd1.reload-daemon org.freedesktop.systemd1.manage-units</annotate>
</action>
<action id="org.freedesktop.systemd1.reload-daemon">
<description gettext-domain="systemd">Reload the systemd state</description>
<message gettext-domain="systemd">Authentication is required to reload the systemd state.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
** (polkitd:186082): DEBUG: 00:37:29.575: In authentication_agent_response for cookie '3-31e1bb8396c301fad7e3a40706ed6422-1-0a3c2713a55294e172b441c1dfd1577d' and identity unix-user:root
** (polkitd:186082): DEBUG: 00:37:29.576: OUT: Only uid 0 may invoke this method.
** (polkitd:186082): DEBUG: 00:37:29.576: Authentication complete, is_authenticated = 0
** (polkitd:186082): DEBUG: 00:37:29.577: In check_authorization_challenge_cb
subject system-bus-name::1.6846
action_id org.freedesktop.timedate1.set-timezone
was_dismissed 0
authentication_success 0
00:37:29.577: Operator of unix-process:186211:9138723 FAILED to authenticate to gain authorization for action org.freedesktop.timedate1.set-timezone for system-bus-name::1.6846 [python3 agent.py] (owned by unix-user:dev)
可见我们的 Authentication Agent 已经正常工作了,可以接收到 PolicyKit 发送的 BeginAuthentication 方法调用,并且PolicyKit 会提示 Only uid 0 may invoke this method,是因为我们的 AuthenticationAgentResponse 发送用户为 dev 用户而非 root 用户。
** (polkitd:186082): DEBUG: 01:09:17.375: In authentication_agent_response for cookie '51-20cf92ca04f0c6b029d0309dbfe699b5-1-3d3e63e4e98124979952a29a828057c7' and identity unix-user:root
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: RET: 1
** (polkitd:186082): DEBUG: 01:09:17.377: Removing authentication agent for unix-process:189453:9329523 at name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent (disconnected from bus)
01:09:17.377: Unregistered Authentication Agent for unix-process:189453:9329523 (system bus name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent, locale en_US.UTF-8) (disconnected from bus)
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: error
Error performing authentication: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient disconnected from message bus without replying (g-dbus-error-quark 4)
(polkitd:186082): GLib-WARNING **: 01:09:17.379: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.6921': no such name
** (polkitd:186082): DEBUG: 01:09:17.380: In check_authorization_challenge_cb
subject system-bus-name::1.6921
action_id org.freedesktop.timedate1.set-timezone
was_dismissed 0
authentication_success 0
** (polkitd:192813): DEBUG: 01:42:29.925: In authentication_agent_response for cookie '3-7c19ac0c4623cf4548b91ef08584209f-1-22daebe24c317a3d64d74d2acd307468' and identity unix-user:root
** (polkitd:192813): DEBUG: 01:42:29.928: OUT: RET: 1
** (polkitd:192813): DEBUG: 01:42:29.928: Authentication complete, is_authenticated = 1
(polkitd:192813): GLib-WARNING **: 01:42:29.934: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.7428': no such name
** (polkitd:192813): DEBUG: 01:42:29.934: In check_authorization_challenge_cb
subject system-bus-name::1.7428
action_id org.freedesktop.timedate1.set-timezone
was_dismissed 0
authentication_success 1
同时系统时区也已经成功更改。
0x07. Before The Exploit
相比于漏洞作者给出的 Account Daemon 利用,我选择了使用 org.freedesktop.systemd1。首先我们摆脱了必须使用 org.freedesktop.policykit.imply 修饰过的方法的限制,其次因为这个 D-Bus Service 几乎在每个 Linux 系统都存在,最后是因为这个方法存在一些高风险方法。
$ gdbus introspect --system -d org.freedesktop.systemd1 -o /org/freedesktop/systemd1
...
interface org.freedesktop.systemd1.Manager {
...
StartUnit(in s arg_0,
in s arg_1,
out o arg_2);
...
EnableUnitFiles(in as arg_0,
in b arg_1,
in b arg_2,
out b arg_3,
out a(sss) arg_4);
...
}
...
同时,作为一个 Web 漏洞的安全研究员,我自是将所有的东西都类型转换到 Web 层面去看待。D-Bus 和 Web 非常相似,在挖掘提权的过程中并没有受到特别大的阻力,却收获了非常多的成果。希望各位安全从业者通过 D-Bus 来入门二进制,跳出自己的舒适圈,也可以增加自己在漏洞挖掘中的视野(什么,内存破坏洞?想都不要想了,开摆.jpg)。
BIND_OPTS3 opt = new BIND_OPTS3();
opt.cbStruct = (uint)Marshal.SizeOf(opt);
opt.dwClassContext = 4;
var srv = CoGetObject("Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}", ref opt, new Guid("{00000000-0000-0000-C000-000000000046}")) as IElevatedFactoryServer;
随后调用ServerCreateElevatedObject方法获取ITaskService实例:
var svc = srv.ServerCreateElevatedObject(new Guid("{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}"), new Guid("{00000000-0000-0000-C000-000000000046}")) as ITaskService;
var xd=new XmlDocument();
xd.LoadXml(task.Xml);
Console.WriteLine(xd.SelectSingleNode("/*[local-name()='Task']/*[local-name()='RegistrationInfo']/*[local-name()='Description']").InnerText);
Xalan-J uses a JIT compiler called XSLTC for translating XSLT stylesheets into Java classes during runtime. XSLTC depends on the Apache Byte Code Engineering (BCEL) library to dynamically create Java class files
As part of the compilation process, constants in the XSLT input such as Strings or Numbers get translated into Java constants which are stored at the beginning of the output class file in a structure called the constant pool
Small integers that fit into a byte or short are stored inline in bytecode using the bipush or sipush instructions. Larger ones are added to the constant pool using the cp.addInteger method
// org.apache.bcel.generic.PUSH#PUSH(org.apache.bcel.generic.ConstantPoolGen, int)
public PUSH(final ConstantPoolGen cp, final int value) {
if ((value >= -1) && (value <= 5)) {
instruction = InstructionConst.getInstruction(Const.ICONST_0 + value);
} else if (Instruction.isValidByte(value)) {
instruction = new BIPUSH((byte) value);
} else if (Instruction.isValidShort(value)) {
instruction = new SIPUSH((short) value);
} else {
instruction = new LDC(cp.addInteger(value));
}
}
As java class files only use 2 bytes to specify the size of the constant pool, its max size is limited to 2**16 - 1 entries
BCELs internal constant pool representation uses a standard Java Array for storing constants and does not enforce any limits on its length. When the generated class file is serialized at the end of the compilation process the array length is truncated to a short, but the complete array is written out:
// org.apache.bcel.classfile.ConstantPool#dump
public void dump( final DataOutputStream file ) throws IOException {
file.writeShort(constant_pool.length); // 对 constant_pool.length 进行了 short 截断
for (int i = 1; i < constant_pool.length; i++) { // 依旧写入了 constant_pool.length 个数的常量
if (constant_pool[i] != null) {
constant_pool[i].dump(file);
}
}
}
根据字节码规范要求,this_class 应指向一个 CONSTANT_Class_info 结构的常量,也即如下图中 Class 对应的下标 #0006
The value of the this_class item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Class_info structure (§4.4.1) representing the class or interface defined by this class file.
For a class, the value of the super_class item either must be zero or must be a valid index into the constant_pool table. If the value of the super_class item is nonzero, the constant_pool entry at that index must be a CONSTANT_Class_info structure (§4.4.1) representing the direct superclass of the class defined by this class file.
Apache Solr 是一个开源的搜索服务器。Solr 使用 Java 语言开发,主要基于 HTTP 和 Apache Lucene 实现。Lucene 是一个全文检索引擎工具包,它是一个 jar 包,不能独立运行,对外提供服务。Apache Solr 中存储的资源是以 Document 为对象进行存储的。NoSQL特性和丰富的文档处理(例如Word和PDF文件)。每个文档由一系列的 Field 构成,每个 Field 表示资源的一个属性。Solr 中的每个 Document 需要有能唯一标识其自身的属性,默认情况下这个属性的名字是 id,在 Schema 配置文件中使用:id进行描述。 Solr是一个独立的企业级搜索应用服务器,目前很多企业运用solr开源服务。原理大致是文档通过Http利用XML加到一个搜索集合中。
Solr可以独立运行,打包成一个war。运行在Jetty、Tomcat等这些Servlet容器中,Solr索引的实现方法很简单,用 POST 方法向Solr服务器 发送一个描述
Field 及其内容的XML文档,Solr根据xml文档添加、删除、更新索引。Solr搜索只需要发送HTTP GET 请求,然后对 Solr 返回Xml、Json等格式的查询结果进行解析,组织页面布局。Solr不提供构建UI的功能,Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况。
private void handleConfigUploadRequest(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
if (!"true".equals(System.getProperty("configset.upload.enabled", "true"))) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Configset upload feature is disabled. To enable this, start Solr with '-Dconfigset.upload.enabled=true'.");
}
// 获取上传的配置集文件名
String configSetName = req.getParams().get(NAME);
if (StringUtils.isBlank(configSetName)) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"The configuration name should be provided in the \"name\" parameter");
}
// 此处开始配置集上传逻辑
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
String configPathInZk = ZkConfigManager.CONFIGS_ZKNODE + "/" + configSetName;
//判断ZK中是否已经存在
boolean overwritesExisting = zkClient.exists(configPathInZk, true);
boolean requestIsTrusted = isTrusted(req, coreContainer.getAuthenticationPlugin());
// 获取上传一些参数
String singleFilePath = req.getParams().get(ConfigSetParams.FILE_PATH, "");
boolean allowOverwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, false);
boolean cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);
Iterator<ContentStream> contentStreamsIterator = req.getContentStreams().iterator();
if (!contentStreamsIterator.hasNext()) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"No stream found for the config data to be uploaded");
}
// 获取上传文件流
InputStream inputStream = contentStreamsIterator.next().getStream();
// 是否为单文件上传
if (!singleFilePath.isEmpty()) {
String fixedSingleFilePath = singleFilePath;
if (fixedSingleFilePath.charAt(0) == '/') {
fixedSingleFilePath = fixedSingleFilePath.substring(1);
}
if (fixedSingleFilePath.isEmpty()) {
throw new SolrException(ErrorCode.BAD_REQUEST, "The file path provided for upload, '" + singleFilePath + "', is not valid.");
} else if (cleanup) {
// Cleanup is not allowed while using singleFilePath upload
throw new SolrException(ErrorCode.BAD_REQUEST, "ConfigSet uploads do not allow cleanup=true when file path is used.");
} else {
try {
// Create a node for the configuration in zookeeper
// For creating the baseZnode, the cleanup parameter is only allowed to be true when singleFilePath is not passed.
createBaseZnode(zkClient, overwritesExisting, requestIsTrusted, configPathInZk);
String filePathInZk = configPathInZk + "/" + fixedSingleFilePath;
zkClient.makePath(filePathInZk, IOUtils.toByteArray(inputStream), CreateMode.PERSISTENT, null, !allowOverwrite, true);
} catch(KeeperException.NodeExistsException nodeExistsException) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"The path " + singleFilePath + " for configSet " + configSetName + " already exists. In order to overwrite, provide overwrite=true or use an HTTP PUT with the V2 API.");
}
}
return;
}
// 单文件上传允许文件覆盖
if (overwritesExisting && !allowOverwrite) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"The configuration " + configSetName + " already exists in zookeeper");
}
Set<String> filesToDelete;
if (overwritesExisting && cleanup) {
filesToDelete = getAllConfigsetFiles(zkClient, configPathInZk);
} else {
filesToDelete = Collections.emptySet();
}
// zk中创建节点
// For creating the baseZnode, the cleanup parameter is only allowed to be true when singleFilePath is not passed.
createBaseZnode(zkClient, overwritesExisting, requestIsTrusted, configPathInZk);
//获取zip文件流 在zk中存储
ZipInputStream zis = new ZipInputStream(inputStream, StandardCharsets.UTF_8);
ZipEntry zipEntry = null;
boolean hasEntry = false;
while ((zipEntry = zis.getNextEntry()) != null) {
hasEntry = true;
String filePathInZk = configPathInZk + "/" + zipEntry.getName();
if (filePathInZk.endsWith("/")) {
filesToDelete.remove(filePathInZk.substring(0, filePathInZk.length() -1));
} else {
filesToDelete.remove(filePathInZk);
}
if (zipEntry.isDirectory()) {
zkClient.makePath(filePathInZk, false, true);
} else {
createZkNodeIfNotExistsAndSetData(zkClient, filePathInZk,
IOUtils.toByteArray(zis));
}
}
zis.close();
if (!hasEntry) {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Either empty zipped data, or non-zipped data was uploaded. In order to upload a configSet, you must zip a non-empty directory to upload.");
}
deleteUnusedFiles(zkClient, filesToDelete);
// If the request is doing a full trusted overwrite of an untrusted configSet (overwrite=true, cleanup=true), then trust the configSet.
if (cleanup && requestIsTrusted && overwritesExisting && !isCurrentlyTrusted(zkClient, configPathInZk)) {
byte[] baseZnodeData = ("{\"trusted\": true}").getBytes(StandardCharsets.UTF_8);
zkClient.setData(configPathInZk, baseZnodeData, true);
}
}
private void initLibs(SolrResourceLoader loader, boolean isConfigsetTrusted) {
// TODO Want to remove SolrResourceLoader.getInstancePath; it can be on a Standalone subclass.
// For Zk subclass, it's needed for the time being as well. We could remove that one if we remove two things
// in SolrCloud: (1) instancePath/lib and (2) solrconfig lib directives with relative paths. Can wait till 9.0.
Path instancePath = loader.getInstancePath();
List<URL> urls = new ArrayList<>();
Path libPath = instancePath.resolve("lib");
if (Files.exists(libPath)) {
try {
urls.addAll(SolrResourceLoader.getURLs(libPath));
} catch (IOException e) {
log.warn("Couldn't add files from {} to classpath: {}", libPath, e);
}
}
List<ConfigNode> nodes = root.getAll("lib");
if (nodes != null && nodes.size() > 0) {
if (!isConfigsetTrusted) {
throw new SolrException(ErrorCode.UNAUTHORIZED,
"The configset for this collection was uploaded without any authentication in place,"
+ " and use of <lib> is not available for collections with untrusted configsets. To use this component, re-upload the configset"
+ " after enabling authentication and authorization.");
}
for (int i = 0; i < nodes.size(); i++) {
ConfigNode node = nodes.get(i);
String baseDir = node.attr("dir");
String path = node.attr(PATH);
if (null != baseDir) {
// :TODO: add support for a simpler 'glob' mutually exclusive of regex
Path dir = instancePath.resolve(baseDir);
String regex = node.attr("regex");
try {
if (regex == null)
urls.addAll(SolrResourceLoader.getURLs(dir));
else
urls.addAll(SolrResourceLoader.getFilteredURLs(dir, regex));
} catch (IOException e) {
log.warn("Couldn't add files from {} filtered by {} to classpath: {}", dir, regex, e);
}
} else if (null != path) {
final Path dir = instancePath.resolve(path);
try {
urls.add(dir.toUri().toURL());
} catch (MalformedURLException e) {
log.warn("Couldn't add file {} to classpath: {}", dir, e);
}
} else {
throw new RuntimeException("lib: missing mandatory attributes: 'dir' or 'path'");
}
}
}
if (!urls.isEmpty()) {
loader.addToClassLoader(urls);
loader.reloadLuceneSPI();
}
}