& k/ B8 W, H: d. X2 O 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。% \" U' L; m! a) ?) X* B8 ]6 ^
" f" o' x# T- z+ J' c
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
7 D5 x1 g3 C. N1 A$ @% _! E u& l
$ z+ e1 w) Z0 ?: r" ? 对应着avatar.inc.php代码如下:2 j% P: D2 l7 H' J* n
+ p* Q& h* w$ v- Z3 o$ j8 L <?php defined('IN_DESTOON') or exit('Access Denied');login();require DT_ROOT.'/module/'.$module.'/common.inc.php';require DT_ROOT.'/include/post.func.php';$avatar = useravatar($_userid, 'large', 0, 2);switch($action) {5 d% e% a5 P; L: y' l
case 'upload':
if(!$_FILES['file']['size']) {
# n9 {" \, k. c0 Jif($DT_PC) dheader('?action=html&reload='.$DT_TIME);. ^ K! } G8 S' d
/ M4 B2 Z$ ?5 A% S exit('{"error":1,"message":"Error FILE"}');
. j w: {& J1 \! W* |4 Y} a* r3 R7 g" D( ?% G" @2 x
, E9 [: f$ `5 j, W$ d3 p h0 g% a' D1 z( _6 ?; O; _) q require DT_ROOT.'/include/upload.class.php';# C% A: b2 G" s/ x P" o& i# Q. p
2 V, k/ L7 P$ l+ ^5 m0 G* W2 i6 C d- @+ F2 o
/ v( S* }5 h: p/ S' j4 l, X1 _$ext = file_ext($_FILES['file']['name']);2 p, G6 r/ q7 ^6 ~; Z
$name = 'avatar'.$_userid.'.'.$ext;. x# w9 |) v3 ^& U& f6 \2 ~
$file = DT_ROOT.'/file/temp/'.$name;; {0 I q/ Z" f" T
$ f. [ h* { Y& j% p" f! U3 z & }' y. x3 z, H- {+ ?6 s6 c
. ~! W0 r# [: U! D! X/ A9 D& L% M& h0 A3 t1 I/ l' P( e, j if(is_file($file)) file_del($file);" ?: g% p& [: _& j. x/ @
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png'); X5 J' t# d* P. ~) M- Q
5 F& R& J/ h- S) p0 _3 F3 V* }$ pX! F* _/ C: ^- m4 ?
- v0 Q$ [8 T1 I" m& H9 D# a8 T, O. g $upload->adduserid = false;
: J7 A, }% c7 I9 N/ l+ x) X7 b# h4 |( C
/ ~' Y4 j5 [8 b3 bif($upload->save()) {3 Q) ]% r. n' l4 P. A3 W0 h
4 r% V( l$ y$ a- H% W ...
% r# y8 w t3 z; a4 H+ \ F" Q} else {
1 }5 L! f @* q V...
3 ]4 q" u3 M" F$ y& n1 O Y% Y7 z}
; {* e8 T/ W, f: z2 J" l2 y) H: q _1 o0 w2 R! k break;
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
. j' }8 j! B$ N6 y4 P5 f3 C+ `& N* T" r4 w upload对象构造函数如下,include/upload.class.php:25:
3 k' [) Q# X% }+ z( t3 O( |5 M, [4 ]) T! l; g; c$ h7 M! v$ s# { <?phpclass upload {
function __construct($_file, $savepath, $savename = '', $fileformat = '') {! f9 o; x3 P& |: _* S+ Y2 Y
' ^. j8 ?. B, d5 }. M global $DT, $_userid;' ~7 e! |5 V2 y% V
# D5 x5 q# E% M, ?) L3 v foreach($_file as $file) { S) j# S4 z$ z% ]" n8 {" n
' T9 \* T5 u: L $this->file = $file['tmp_name'];5 H$ z5 |4 H$ ]' Y# Z% N# C6 v3 n
$this->file_name = $file['name'];
2 v) d3 O1 p! A" P, U' M0 \0 u9 p1 @; O: v- \; G) ? $this->file_size = $file['size'];
. _ u0 K% m& f. e $this->file_type = $file['type'];% ?( F% q0 I4 P: Z8 x( N1 D0 x: ?3 n
% t3 F5 l2 r* [$this->file_error = $file['error'];
3 Q6 c' x# S# a1 ~
}
/ l. @, c7 W# ^: z, u$this->userid = $_userid;
$this->ext = file_ext($this->file_name);3 W& N* p0 ]* J$ v1 v
e: y) }. u" ^( D: u) F$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
6 v& U* s' A% \$ l" B $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;! E) i4 L& G- f9 D( i
$this->savepath = $savepath;
& I. b; r# p @" }0 B' k, f8 v5 C" g9 w- L7 b" N k# N $this->savename = $savename;7 I& Q; B2 R5 g) c- p
/ [ j+ a* e0 s$ ^+ y; ]8 A# I9 t% G/ `# O' w }} B$ {3 C: U" x7 ^( D) |( G
7 c. g$ m w$ C8 _5 j7 Q 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。) ?% o: U* z1 N M$ ~# J9 q' A9 Q
1 r6 D' Y. B" `! y6 E. q5 u5 I6 `& c0 {1 g 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
& ?- [1 t% \2 r+ z7 h: Z $ext = file_ext($_FILES['file']['name']); // `$ext`即为`php` $name = 'avatar'.$_userid.'.'.$ext; // $name 为 'avatar'.$_userid.'.'php'$file = DT_ROOT.'/file/temp/'.$name; // $file 即为 xx/xx/xx/xx.php
2 B8 x; H3 l' W2 q9 e( o: l6 Y- y" c9 L; f; T* c" s1 |$ a7 t 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
7 q3 |+ a$ {8 _& X% J; \3 r" k; I' J, J$ w, S- r7 Q& E; J% S
# n( P- ]0 ]+ w" O# R! N% X 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:( }6 s: e2 Y4 v# Y+ l
6 ?$ i/ `# w Q5 V<?phpclass upload {
function save() {' b$ \% p& h0 z/ S+ b. D
" m9 ^; A9 m4 t/ n. Y8 ` ]. q: P% z include load('include.lang');- {3 s5 d$ B$ }& T1 A
3 J1 G3 d& k' Y( w) \" I B% {. y, g if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
+ \' O e; p% l4 O4 Q if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
! Y' ~) d1 P6 w7 r+ }0 i3 R+ `0 }) B
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);2 O. Z2 f8 ^- Z# E7 ?
. v) B% e1 C3 d% R6 Z* e# F! T8 u8 c' h( D* j* m9 r# ~; }
$this->set_savepath($this->savepath);" h! S% k: ~6 [: e7 Y
% s7 ^4 R; R- R0 m* f" t* Z9 ?" j8 r0 b $this->set_savename($this->savename);
$ M: s& q4 w+ ~
' A+ A; A5 m% I, |. |$ O P# D5 ^2 E2 N/ ^$ Z; E8 `2 g7 G. d" o K if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
; s& |* l* @; D. @* d3 H# Jif(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
0 b0 T6 z5 r. U$ k0 g
$this->image = $this->is_image();
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);& G2 r, p1 v9 x5 {* P+ P
+ w, C0 m& |2 qreturn true;2 c( l, q8 P9 _# @5 J0 x. e
}}
9 Q6 E! S, t2 n' f4 L先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
3 K8 Y$ R0 y2 M/ B8 Q8 a" W<?php+ y t) R7 N! x$ b& b
function is_allow() {: M. u# F1 J9 k, V2 s8 n3 E2 o/ l
if(!$this->fileformat) return false;
8 K8 [2 e C o' i! ` if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;3 H t# W: Z) D& S2 M
, V, V/ d# \2 c0 \- \1 G1 p! S. |2 e- V4 G$ ? if(preg_match("/^(php|phtml|php3|php4|jsp|exe|dll|cer|shtml|shtm|asp|asa|aspx|asax|ashx|cgi|fcgi|pl)$/i", $this->ext)) return false;3 y3 T3 D9 S* f
% N( {4 R3 j* i I return true;4 {: ^7 D7 l( s* I" j1 g+ D8 A
3 X) v k3 O* Z }
4 c5 {: L- ~1 L7 z, ^' W7 d2 z9 m) Y1 C, Z s5 s 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
接着会进行真正的保存。通过$this->set_savepath($this->savepath); $this->set_savename($this->savename);设置了$this->saveto,然后通过move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)将file保存到$this->saveto ,注意此时的savepath、savename、saveto均以php为后缀,而$this->file实际指的是第二个jpg文件。
' _$ L2 @# E! u1 t, [6 y3 D综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
+ Z) M9 Z4 s: r/ M& |! s1 J5 ^8 p
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
( O3 y' u! n# l/ J1 j不过实际利用上会有一定的限制。
, [) z! t1 O, S3 b1 Q第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
4 m( F) r7 B# @2 F1 ?
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
省略...$img = array();$img[1] = $dir.'.jpg';$img[2] = $dir.'x48.jpg';$img[3] = $dir.'x20.jpg';$md5 = md5($_username);$dir = DT_ROOT.'/file/avatar/'.substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/_'.$_username;$img[4] = $dir.'.jpg';$img[5] = $dir.'x48.jpg';$img[6] = $dir.'x20.jpg';file_copy($file, $img[1]);file_copy($file, $img[4]);省略...4 S0 |, D T4 M
7 b4 v6 g2 T3 a. m3 ]( H5 @/ z7 X+ n& x3 e3 \ 因此要利用成功就需要条件竞争了。 \2 h- U" V1 _- F* }
, ^' x7 W+ p [. P8 e/ A5 u9 U4 k
; X( g# l/ E( G
# w8 G" p& D/ \; D) |: t 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
/ J) z- P* A- Q! k9 I function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
, l6 d; L! l: y! n
: f8 D% Y3 K' Q. y& w! _! T4 h 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。4 u; T+ A8 W- h' U! S% r0 h7 c
! w7 T3 ]( Q3 c4 f9 M2 ?" P在is_allow()中增加对$this->savename的二次检查。
嘛,祝各位大师傅中秋快乐!$ x- v e& Y; H- b! y! i) z. ^
{7 d' T( j& V: @) z( N0 e2 d+ x2 ~6 @7 y: y4 g
| 欢迎光临 中国网络渗透测试联盟 (https://cobjon.com/) | Powered by Discuz! X3.2 |