网易首页 > 网易号 > 正文 申请入驻

从PHP底层看open_basedir bypass

0
分享至

  

  
前言

  

  有国外的大佬近日公开了一个php open_basedir bypass的poc,正好最近在看php底层,于是打算分析一下。

  
poc测试

  首先测试一下:

  

  我们用如上源码进行测试,首先设置open_basedir目录为/tmp目录,再尝试用ini_set设置open_basedir则无效果,我们对根目录进行列目录,发现无效,返回bool(false)。

  我们再尝试一下该国外大佬的poc:

  

  发现可以成功列举根目录,bypass open_basedir。

  那么为什么一系列操作后,就可以重设open_basedir了呢?我们一步一步从头探索。

  
ini_set覆盖问题探索

  为什么连续使用ini_set不会对open_basedir进行覆盖呢?我们以如下代码为例:

  <?php

  var_dump(ini_get('open_basedir'));

  ini_set('open_basedir', '/tmp');

  ini_set('open_basedir', '/');

  ini_set('open_basedir', '..');

  运行后结果如下:

  string(0) ""

  string(4) "/tmp"

  默认的open_basedir值本来是空,第一次设置成/tmp后,以为设置将不会覆盖。

  我们来探索一下原因。首先找到php函数对应的底层函数:

  ini_get : PHP_FUNCTION(ini_get)

  ini_set : PHP_FUNCTION(ini_set)

  这里我们主要看的是ini_set的流程,ini_get作为信息输出函数,我们不太关心。

  我们先对ini_set下断点,然后再run程序:

  b /php7.0-src/ext/standard/basic_functions.c 5350

  r c.php

  程序跑起来后,首先是3个初始值:

  zend_string *varname;

  zend_string *new_value;

  char *old_value;

  然后进入词法分析,得到3个变量值:

  if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS", &varname, &new_value) == FAILURE) {

  return;

  }

  我们可以看到

  pwndbg> p *varname

  $45 = {

   gc = {

   refcount = 0,

   u = {

   v = {

   type = 6 '\006',

   flags = 2 '\002',

   gc_info = 0

   },

   type_info = 518

   }

   },

   h = 15582417252668088432,

   len = 12,

   val = "o"

  }

  这是zend_string的结构体,也是php7的新增结构:

  struct _zend_string {

   zend_refcounted_h gc; /*gc信息*/

   zend_ulong h; /* hash value */

   size_t len; /*字符串长度*/

   char val[1]; /*字符串起始地址*/

  };

  我们可以看到varname.val为:

  pwndbg> p &varname.val

  $46 = (char (*)[1]) 0x7ffff7064978

  pwndbg> x/s $46

  0x7ffff7064978:"open_basedir"

  然后new_value.val为:

  pwndbg> p &new_value.val

  $48 = (char (*)[1]) 0x7ffff7058ad8

  pwndbg> x/s $48

  0x7ffff7058ad8:"/tmp"

  即我们最开始传入的两个参数。

  然后程序拿到原来的open_basedir的value:

  

  

  然后会进入php_ini_check_path:

  

  由于第一次没有设置过open_basedir,所以直接跳出判断,进入下一步:

  if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {

  zval_dtor(return_value);

  RETURN_FALSE;

  }

  我们跟进FAILURE,找到定义:

  typedef enum {

   SUCCESS = 0,

   FAILURE = -1,/* this MUST stay a negative number, or it may affect functions! */

  } ZEND_RESULT_CODE;

  当zend_alter_ini_entry_ex的返回值不为-1时,即代表更新成功,否则则会进入if,返回false。

  而经过比对发现:第一次设置open_basedir和第二次设置时候,正是这里的返回值不一样,第一次设置时,这里为SUCCESS,即0,而第二次设置为FAILURE,即-1,我们跟入zend_alter_ini_entry_ex进行比对:

  b /php7.0-src/Zend/zend_ini.c:330

  发现两次不同的点在于如下判断:

  if (!ini_entry->on_modify|| ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS)

  第一次时:

  ini_entry->on_modify = 0x5d046e <OnUpdateBaseDir>ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) = 0

  第二次时:

  ini_entry->on_modify :0x5d046e <OnUpdateBaseDir>ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) = -1

  可以确定是on_modify,那么我们单步跟进,到达:

  PHPAPI ZEND_INI_MH(OnUpdateBaseDir)

  发现在进行如下操作时,返回FAILURE:

  if (php_check_open_basedir_ex(ptr, 0) != 0) {

  /* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */efree(pathbuf);

  return FAILURE;

  }

  正是php_check_open_basedir_ex()未通过才导致我们ini_set失败,而第一次的时候,这里是通过的。

  所以最后的问题落在`php_check_open_basedir_ex`上,如果想要利用ini_set覆盖之前的open_basedir,那么必须通过该校验。

  
php_check_open_basedir_ex

  找到切入点后,后面就是进行分析,看如何bypass php_check_open_basedir_ex。

  我们源码跟进这个函数:

  if (strlen(path) > (MAXPATHLEN - 1)) {

  php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);

  errno = EINVAL;

  return -1;

  }

  #define MAXPATHLEN PATH_MAX

  #define PATH_MAX 1024 /* max bytes in pathname */

  首先判断路径是否过长,是否超过1023。

  然后是另一个校验函数:

  if (php_check_specific_open_basedir(ptr, path) == 0) {

   efree(pathbuf);

   return 0;

  }

  跟进后,该函数首先进行了操作:

  if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {

  /* Else use the unmodified path */

  strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));

  }

  比对当前目录,并赋值给local_open_basedir,然后继续看目录名长度是否合法:

  path_len = strlen(path);

  if (path_len > (MAXPATHLEN - 1)) {

   /* empty and too long paths are invalid */

   return -1;

  }

  然后进入操作:

  if (expand_filepath(path, resolved_name) == NULL) {

  return -1;

  }

  PHPAPI char *expand_filepath(const char *filepath, char *real_path){

  return expand_filepath_ex(filepath, real_path, NULL, 0);

  }

  将传入的path,用绝对路径保存在resolved_name。

  然后操作继续进入判断:

  if (expand_filepath(local_open_basedir, resolved_basedir) != NULL)

  将local_open_basedir的值存放于resolved_basedir,用于后面的比较:

  if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0)

  {

   if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) {return -1;}

   else {

  /* File is in the right directory */

  return 0;

   }

  }

  else {

  /* /openbasedir/ and /openbasedir are the same directory */

   if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR)

   {

   if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0)

   {

   if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0)

   {

   return 0;

   }

   }

   return -1;

   }

  }

  上述操作正是在匹配路径是否是open_basedir规定的路径。

  那么不难发现,可控点应该就要追溯到之前的:

  expand_filepath()

  因为关键路径resolved_name和resolved_basedir均由这个函数生成。

  所以要bypass php_check_open_basedir_ex的关键,在于bypass expand_filepath()。其获取到的path才是真正用来比对的path。

  我们跟进至:

  }

  继续跟expand_filepath_ex:

  PHPAPI char *expand_filepath_ex(const char *filepath, char *real_path, const char *relative_to, size_t relative_to_len){

  return expand_filepath_with_mode(filepath, real_path, relative_to, relative_to_len, CWD_FILEPATH);

  }

  再跟expand_filepath_with_mode,来到关键操作位置:

  if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {efree(new_state.cwd);

  return NULL;

  }

  跟入virtual_file_ex得到关键语句:

  if (!IS_ABSOLUTE_PATH(path, path_length)) {

  if (state->cwd_length == 0) {

  /* resolve relative path */

  start = 0;

  memcpy(resolved_path , path, path_length + 1);

  } else {

  int state_cwd_length = state->cwd_length;

   ......

   state->cwd_length = path_length;

   ......

   memcpy(state->cwd, resolved_path, state->cwd_length+1);

  即目录拼接操作,如果path不是绝对路径,同时`state->cwd`长度为0,那么直接将path作为绝对路径,保存在resolved_path。否则则在state->cwd后拼接。

  那么可以落点于path_length,这决定了我们拼接的长度:

  path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);

  跟进tsrm_realpath_r,不难发现主要操作用于:

  remove double slashes and '.'

  remove '..' and previous directory

  那么最后可以总结expand_filepath()全身心的投入在相对路径和绝对路径,没有考虑open_basedir如果为相对路径会实时变化的问题。

  
总结

  所以最后的bypass poc也变得非常清楚:

  首先需要构造一个相对可上跳的open_basedir:

  mkdir('sky');

  chdir('sky');

  这也是为什么要先创文件夹的原因,就是为了在当前目录构造可以..的ini_set。

  然后每次目录操作:

  chdir('..');

  都会进行一次open_basedir的比对,即php_check_open_basedir_ex。由于相对路径的问题,每次open_basedir的补全都会上跳。

  比如初试open_basedir为/a/b/c/d:

  第一次chdir后变为/a/b/c,

  第二次chdir后变为/a/b,

  第三次chdir后变为/a,

  第四次chdir后变为/,

  那么这时候再进行ini_set,调整open_basedir为/即可通过php_check_open_basedir_ex的校验,成功覆盖,导致我们可以bypass open_basedir。

  
后记

  这个poc还是很巧妙的,重点在于构造出相对路径的open_basedir,再触发其进行上跳!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐

网红项思醒事件最新进展!女方发视频称:后悔读书读得少

金牌娱乐
2021-04-16 09:37:00

直击“油茅”金龙鱼股东大会:董事长直言“低毛利、量大等于高门槛”

每日经济新闻
2021-04-16 23:20:12

高速限速120,汽车开到130会被扣分吗?交警直言:放心去开!

擦车工聊车
2021-04-16 21:41:42

失去华为这个客户,台积电从中国大陆获得营收占比剧降

柏铭锐谈
2021-04-17 00:22:15

错抱婴儿护士已找到,与姚策生父极其相似,可能为亲生姐妹

资讯快递小哥
2021-04-17 01:21:49

72岁植物人老太被78岁老汉性侵,动静太大吵醒家属,事后拒不赔偿

娱乐晓姐
2021-04-16 11:06:37

立刻取消投海决定!俄专家建议用氢弹,美俄态度云泥之差

东风军情
2021-04-16 20:35:07

张翰首晒领证照:错过就是错过了!

品牌营销官
2021-04-16 14:45:33

曼城决战切尔西首发曝光:撤换9大主力,双核领衔,巴西妖星冲锋

零度眼看球
2021-04-16 22:33:44

漯河美女赏析

当代广播站
2021-04-17 01:40:10

罗斯柴尔德家族有多富?苏伊士运河堵塞事故暴露了家族的真实财力

农夫绝杀球
2021-04-16 04:32:36

儿子中风住院,晚上儿媳房间传出男人声音,婆婆走近后红了眼

深夜聊情
2021-04-16 18:14:05

令人害怕的“巧合”,这是科学家都无法解释的现象吧

记录之路
2021-04-16 17:51:57

检出阳性,即日起暂停!

混在邯郸
2021-04-15 07:23:10

男子只请一位老同学吃饭,却一下子来了十几个人,结账时懵了

小聂生活
2021-04-16 17:34:04

楼市迎来“新禁令”,明年开始,3类房子或变得“难”转手

房子里有家
2021-04-15 15:59:31

克里访华难成破冰之旅,凭什么中国必须配合咄咄逼人的美国

火星方阵
2021-04-16 05:59:25

又火了!中国切断“世界最长河流”,西方各国直呼:这不可能

记录之路
2021-04-16 03:33:39

金朝灭亡有多惨?10倍于北宋靖康之难,窝阔台下令:完颜一族不赦

见证历史瞬间
2021-04-16 13:06:45

疫苗有效!美国疫情被控制了!官方公布全美完全接种疫苗7700万大数据,有效率99.99%!

华人生活网
2021-04-16 09:42:28
2021-04-17 05:01:08
嘶吼RoarTalk
嘶吼RoarTalk
不一样的互联网安全新视界
4900文章数 8734关注度
往期回顾 全部

头条要闻

BBC气候采访却问中国人权 美学者:美永远在攻击别国

头条要闻

BBC气候采访却问中国人权 美学者:美永远在攻击别国

体育要闻

半场-凯恩破门西古德森点射 热刺客场1-1暂平埃弗顿

娱乐要闻

40岁董洁穿羽毛裙露锁骨 满满女人味

财经要闻

科技要闻

7分钟视频!华为自动驾驶试乘,相比特斯拉如何

汽车要闻

自有范儿 新宝骏Valli是一台向往远方的休旅车

态度原创

游戏
旅游
本地
房产
公开课

老滚MOD改编《遗忘之城》延期至今夏发售

旅游要闻

奥地利森林有“女王”,也有教师

本地新闻

一段亲密关系中,如何让亲密感和新鲜感共存?

房产要闻

重磅!喜提房价涨幅全国第一后,广州这六区被约谈

公开课

复旦教授:90后00后是史上最不适合结婚的一代