- Tags:
Web
- Solves: 1 / 428
- Attachment:
release.zip
- CRLF injection to SSRF redis; setting serialized session
- Trigger RCE by using a POP chain in FluentPDO
- You can inject another Host header in
stream_context_create
to bypass the protection of redis. - Find the long (?) POP chain.
This is a simple website that you can login / register your account and upload your avatar by either file or URL. And the SQL query operations is based on the FluentPDO library.
And the main vulnerabilities occur in upload.php
.
In update.php
, it does some check for URL uploading. Basically, it checks that url should start with http://
or https://
, then checks the ip address of the url (gethostbyname($parsed_url['host'])
) is not local address by using filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
.
If all of these checks are passed, it'll fetch the URL by file_get_contents
and set the avatar.
But it's easy to bypass. Of course, you can use DNS rebinding to bypass the IP check, because there is a small delay between checking and fetching. But that's not stable.
Actually file_get_contents
(http stream in PHP, exactly) follows the redirection by default. So you just need to set your server redirecting to any URL you want, that's all.
The fetching part looks a little weird, it gets the file extension of the URL path, then urldecode it, then set to the Accept
header:
$image_type = pathinfo(urldecode($parsed_url['path']), PATHINFO_EXTENSION) ?? 'png';
$image = file_get_contents($url, false, stream_context_create([
'http' => ['header' => [
"Accept: image/$image_type",
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
]]
]));
Actually there is a (intended) CRLF injection feature in stream_context_create
. So you can use http://host/a.b%0D%0Ameow
to inject CRLF.
The first things you need to know is that PHP session is stored in serialized format, so once you can control the content of session, you got a deserialization vulnerbility.
In this challenge, you need to control redis to set arbitrary value for session. For example, you can use command SET PHPREDIS_SESSION:<PHPSESSID> <VALUE>
to set the content of your session.
Seems we've got a CRLF injection, so that's all? Not really. Redis implements a protection against this kind of attack. Once redis receives Host
or POST
, it'll cut off the connection.
And the http packet sent by file_get_contents
looks like this:
GET / HTTP/1.0
Host: example.com
Connection: close
Accept: image/<INJECTION_POINT>
User-Agent: Mozilla/5.0 ...
The injection point is after the Host
header, so the connection will get closed. And the bypassing way is just to inject our own Host
header, then PHP will automatically replace the old one.
For example, if you post url=http://host/a.b%0D%0ASET%20PHPREDIS_SESSION:48763%20NYAN%0D%0AHost:%20gg
, the file_get_contents
should send:
GET / HTTP/1.0
Connection: close
Accept: image/b
SET PHPREDIS_SESSION:48763 NYAN
Host: gg
User-Agent: Mozilla/5.0 ...
In this way, you can send any command to redis now!
P.S. You might want to use SLAVEOF, MODULE LOAD or other known tricks to get RCE in the first place, but those commands are renamed in
redis.conf
so that won't work.
Where can we trigger the POP chain? There is no __destruct
or __wakeup
in FluentPDO, which is a common entry point for POP chain. But that's not a problem, we can also use __toString()
to trigger it. In index.php
there is a code like this: $user = $fluent->from('users')->where('username', $_SESSION['username'])->fetch()
, which will automatically convert $_SESSION['username']
to string!
It's a little hard to explain the whole chain, so just read my exploit :D
See gadgets.php, the following is the call stack to execute arbitrary command:
$key($table)
Envms\FluentPDO\Structure->key(system, ls)
Envms\FluentPDO\Structure->getPrimaryKey(ls)
Envms\FluentPDO\Queries\Common->createJoinStatement(LEFT JOIN, ls, a, a)
Envms\FluentPDO\Queries\Common->applyTableJoin(LEFT JOIN, Array (), ls, a:, a:, )
Envms\FluentPDO\Queries\Common->addJoinStatements(LEFT JOIN, a:)
Envms\FluentPDO\Queries\Common->createUndefinedJoins(a:b)
array_map(...)
Envms\FluentPDO\Queries\Common->buildQuery()
Envms\FluentPDO\Queries\Base->getQuery()
Envms\FluentPDO\Queries\Base->__toString()
- Post
url
to set your session to that POP chain. - Get
index.php
to trigger the RCE.
See exploit.py.
Note that if your command contains /
or .
, your serialized data might not work because the pathinfo
will break it. In this case, you can try things like bash -c '\${PATH:0:1}readflag give me the flag'
or escape with another serialization format.