$ d1 p$ U: }: ~9 g( a3 {
. U- j! ^# b" O' b+ Y$ ~
* I1 B1 H6 v1 G" e) \8 K% S) q
1 ?9 J% A) \0 ~4 {, `. y
前言
5 g( x- P5 |$ D. H( R4 ~# n* r' F) _( [* d- ?2 Q" x
% J% ]: q2 j. X) {% M6 W% q1 x2 ~
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。1 R, J7 R& q) X& G* q
5 E5 V7 g% v% c% Q9 L2 q) c4 u
# X6 P% v: I0 v' V3 {3 J Y0 I0 A( }) B1 G& i5 N4 C d
' j- T& c0 ~3 E) c0 D( D
, v, F( d% l \6 z& _' ^
漏洞分析
7 Z; t5 O4 |! _" N( j, o7 Z& R) J
# ^$ H4 y( _/ f1 s4 R; J" M k2 h- ~% R& K0 H# f, H0 E
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
- K& R9 K: D; U3 I; v% A: ?4 y! Q% k
6 _5 |0 Z; M; U: f5 y
# ~ s2 B2 o! U; B! t ' G- i8 F0 n+ ^) f
8 o# O7 B0 y' v7 ?7 `; l5 ~1 o( l& b! a. a P7 O( L- v
对应着avatar.inc.php代码如下:
; U3 I+ H9 B4 K s/ _9 J0 ? 4 {# F1 G, ]& w5 m" `$ T$ f. g
9 l; L4 k% Z; g1 U
<?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 M* ?' _6 x7 o+ j: [% m
& K6 o: I x$ j4 V; p
( k0 \- U7 {5 Q0 h( \; v case 'upload':
5 O( w* |: [+ W& B* w
! x0 b, c) [! z7 G& p
( H) r5 Q& Z, W# j; Y% I if(!$_FILES['file']['size']) {
" e( A% J/ y9 m$ L) T 5 s* u% v7 |7 ]) J
( j+ K4 c4 ~$ ~
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
9 \3 o) ?2 w; b7 r 9 N7 o& ~6 [) k. W! d/ c9 o3 l
/ B5 m& Z2 y3 f0 U) l( I exit('{"error":1,"message":"Error FILE"}');
# Y3 S0 Y. ]/ r
" d& |* z$ Y: D6 a* ] V5 X; E; \/ v& _4 d
}
o; u1 n* y6 u+ U( g# _
4 N$ w1 T1 j6 a2 \+ n! `. I
: L6 u4 }3 E5 o require DT_ROOT.'/include/upload.class.php';
0 f0 A3 p9 E8 h7 ]0 m* Y 2 R' `5 P1 m* H
/ G" a; k) q0 k U2 g
8 k3 F4 H$ e2 H% H& z4 d1 E
) ^. V) [! w _' K) ?2 P! h) w+ l+ Q9 b$ k2 K7 y8 x
$ext = file_ext($_FILES['file']['name']);, Z6 H8 @& X- h) F
0 [! ^6 p$ d( ^
- g) t6 c% G' A# J4 D7 o6 k $name = 'avatar'.$_userid.'.'.$ext;
/ Z$ f/ z9 X+ l9 ~ 7 ?0 n6 _, m" T) R
. J- G( P; @. Z6 J $file = DT_ROOT.'/file/temp/'.$name;
4 c1 Z7 `% i: p' U! n E. f. p P - ^5 r, c1 R$ ?/ j7 P2 @8 t
, E1 j- Z9 B2 E) k5 _8 A 1 P) B: y# e& q8 e5 M
0 B/ U8 ]! u$ h, X* r* |% B, z
" l7 ^/ C0 l3 p( ]" P) ~# k if(is_file($file)) file_del($file);' f& @3 d% y0 q3 m8 _: _5 }6 U
8 d, i4 |9 h$ H3 d) Z! F5 O2 L6 y; l* U- r2 Z
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');4 z6 y$ D2 G( W% v
6 F( o, A7 \) g) H. `
5 y# k& G* ^" l- @' t# Q: Y/ y7 k
& }0 h. @3 S W1 d3 k
7 Q! f6 @7 N$ n1 x: ~1 R7 Z
! S/ B6 c2 v: P9 D: C3 Y $upload->adduserid = false;
7 r: u9 R2 }, r e $ k9 P7 g, C" ^/ f& L. ~; T3 a
3 S: }3 L; X1 w0 j + u# w1 _. I0 @
( {/ N) R* G7 B; _6 _9 S
5 r* w$ V! W! h/ u5 J, L. f if($upload->save()) {
: E/ C3 }: k# Q7 f S
. c: [+ {6 V7 C- `0 U. ?, P; M" X0 R; X% d2 C/ j5 V
...) w' u7 B" O" e& N$ K; v
6 C, J- |, Q0 T u
; l M( P( v7 a" r5 D5 L } else {6 p9 R9 c7 S5 d
# ^ N. I9 T$ \% C7 R
" `) R9 D/ l+ }
...2 x# u" u! `. X( D3 m) C5 ]
+ \9 B% V: u& C0 t& }
+ @2 o1 c& L: Q' k }0 s. K& I v- K) ?
+ j' T+ `0 Z6 G4 z
3 `, g1 l1 R5 i6 M
break;
6 c/ @* M+ R s8 N1 F 5 i; l0 b8 L, b3 Y+ n3 E
; _' V3 u. N5 e8 B; l7 c" ] 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
+ t! T/ J$ d2 K; x4 Y( J % @! R, n3 c, i4 r# m6 B, n A9 I
5 @( p8 Z; {" d' v' A: ? upload对象构造函数如下,include/upload.class.php:25:
4 L- M' S9 L" ^( u7 [
" s, w: {7 M% M/ W2 U5 W: F- q( b, P/ U6 Y
<?phpclass upload {
" k" _# L+ y- L$ r4 z ' T+ d x ?" a3 F/ k+ U, e
, `$ Z" r0 @$ z! v* ?! n function __construct($_file, $savepath, $savename = '', $fileformat = '') {
6 K) L8 c. F! S
0 |3 P( x7 Z9 _" I" B* c- `; F% r. X. G
global $DT, $_userid;# `2 ~) j1 I! m0 W; J4 T) D I
! D7 R! @- I; g; l+ k9 w- b+ `
/ u5 l* h1 B4 A! q
foreach($_file as $file) {
9 T8 c; }8 |( \ V0 @ o* G4 M& E 9 v& M6 e+ e; l0 Y" D8 V/ y
' J! V( V+ S4 G/ e& R. f+ s6 j $this->file = $file['tmp_name'];
) t$ F# x1 |4 z) ? 6 P: D5 e! \0 W" |
' N9 b* }# M5 v i: D
$this->file_name = $file['name'];
9 W' a" t+ j; u( m 1 U1 m' Z! Y; S. z! w% b
7 O) G/ Y( ^- S- z
$this->file_size = $file['size'];: d* K$ D2 ]6 C% R3 E
( t- g) e3 T/ L! Y: q/ e& J
/ E/ Q5 J( O9 K( A- s1 F' M $this->file_type = $file['type'];
6 Q) a4 m' H* V' r 4 t; E! ]# p5 d9 q' S, d- p+ U
" g7 V% v- @ f8 B' ^/ B7 {: G
$this->file_error = $file['error'];: F7 M1 }+ v- `3 H
. i( f% T& p- k5 W+ ?& I4 V6 k' u7 E/ |5 p! [" A" s2 i, [. ?" K* Y" p
4 w" j0 n- l" C3 b
6 u$ |; f8 R2 f; h; ?" V. [, A$ i! n1 e1 F2 q9 e
}6 P7 ~) \' ~2 x8 v b* D
2 S. T# A* ? Q5 r8 D& k' Y2 a# N+ `" T8 }1 d7 `. _
$this->userid = $_userid;% U, y. P5 _7 I. N8 \
) U( H q/ p8 D8 S( v) L5 c: f/ V1 Q& K z- A$ k9 b# }
$this->ext = file_ext($this->file_name);
/ e' w- u6 v T2 g
5 D! u4 u' p: u/ _2 J W; L4 G
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];; T7 }. [: y/ Q% J& P% X
% G3 s$ n2 \* y: j, @
% r( K* c% |1 b3 @& m% @ $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;0 R3 W( ], P$ n: K
, G; W" ~+ v f t" {4 R. m1 o" B5 F i0 j, o3 m
$this->savepath = $savepath;: y7 J+ P; R( k7 g& N& F; I J
8 q1 T: l4 [& ], j& W
' g) q2 T' m* G: a0 P. B9 p$ W $this->savename = $savename;
7 Y& H2 [" ^% Z. t* P' f( ?0 } , J3 v3 i& p" Y
, ^/ @4 b+ i" T7 \! n F# q
}}# [% J' n7 B( q* I9 U$ a$ \/ {/ b
- L9 ]8 X" M4 v' Z
/ D. K' @' T5 o* b; H 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
5 b. y6 O/ ~+ P& \8 g& N( ]
/ E5 z3 o' H" E# ]* B( X" ], v/ w' C* O% k7 @
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
$ |: `* s' |, ^# L! `6 d7 Y
3 S ~( E4 L7 W2 }$ F D! `, k+ b( d
# i4 C' M' h4 t. J% u$ w1 } $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.php6 \9 K0 @$ c4 K# K2 }) c
; u e2 p; }+ T$ r& e4 K! e
, L( Q, B8 M* M( k: T+ K 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:# J/ M) W5 j. l+ C
4 ]) `* k6 V- u& I% |7 ~. {& u/ q
B; a4 z4 u }2 m" I8 Y! l 1 ?' \; l1 e! X P4 H3 Z
4 ?, ]# X1 }7 `( {5 U) a+ W
& S& B4 O! y. `7 o$ o
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
6 K) A2 {( J8 E& H
/ P3 X! s, [4 ^6 R6 L; J: ~) Z9 N0 x1 ^3 i! C
<?phpclass upload {, n2 q/ A5 R1 w- J+ W) b! P4 I
: T2 m. U" T- f% D5 \% m* Z
# D2 W c1 ~$ ]+ c5 y; v, A7 V
function save() {
$ U. K# O0 i3 q' P : k) V1 ^+ O& n/ t
2 I8 S8 [: l8 q include load('include.lang');4 P3 z6 P1 }5 K2 w9 s
7 ~- j5 `: D+ k9 h. [7 X0 r
# T% G4 ?$ w, R+ Y: z' i3 h; o if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');3 g: I8 |: ^5 p3 W1 ^9 _' W
. C5 E4 g; H+ s3 d( j
" X) q7 i" \0 d; v; y
8 E4 ]' z; o$ S h
$ x7 t' T# j3 r2 M
; k5 \/ j. N3 j9 v) l; K, l; P
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
3 J5 G" n: S K5 K5 t
4 X h; A) F7 q1 T/ r
8 A0 n+ T! u! m) q0 J6 f7 C ! a7 Z) i0 Q) K4 t2 g2 m H1 U
( d# \1 ^8 Y( S3 ~
2 C; K8 P1 }. }) x1 H; V2 Z
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);3 I9 L+ y, J1 D& {3 T) l
9 W: K6 J/ ?+ I; ?3 F1 Z7 t& i' \2 O$ g
1 V0 h' Q; b9 A% Z# d% k, r5 {7 N
- k% M/ s& }+ X9 t4 O
4 R! {$ I) q4 g' m& x- b& h$ v+ s8 _+ R/ E0 u; _- M6 K+ `6 B
$this->set_savepath($this->savepath);" ? a1 d. o: o5 N1 Z/ n, c4 [
9 h" P/ s2 R; Z
5 f; a4 l- C& L: w' |6 C3 v5 J $this->set_savename($this->savename);) m! o" M$ I! z0 d, `4 M, j
/ ^: t6 \$ z2 T- Y& i
& m7 {3 g7 V' U" d
' K3 y( i" A7 y! C
8 M$ m r& Y1 [) I8 M1 M
' Z3 n8 U/ t4 }) v o7 ~: H% _ if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);- f9 k5 M* Q! |" L) Q& p8 _ q
8 A" ~: v) @2 l: x9 [7 I) ?, j
1 P1 C, T% _' x& R7 l if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
* f' J( \* @% l+ y & h- E; z. Q3 T- y; F& D
+ _9 ]$ h9 Y# j0 r; D' x5 Q5 k
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
$ z' T. \) j/ j8 j$ {! [( \: k
% x6 S+ d4 |3 Y: ~+ l0 m5 l2 t7 D9 J$ q- b
+ D+ B8 @. }' C! ]9 g/ e
) I! l$ L. M9 p7 I" q6 Z+ c$ z3 ^# l9 {* G3 \" `; H& O- t
$this->image = $this->is_image();
- H: {. J: R0 z* {9 @ $ {- g# x( }' H8 @) P7 n* V- y
5 x1 n! |1 k5 Y2 J5 @+ t2 ^ if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
9 h+ e& J) b5 q( f! M: W) n% [ ] 2 t; |% v# y5 K; l! U2 e
$ q" Q$ z# `1 z0 t4 @
return true;" x" P v4 j' f( [3 e
, p- M- M# \# \% c' ~# F. o# X
" S, }5 E" j1 I: a$ X$ W$ v8 Q8 {
}}+ v! D* F$ n I- Y5 C
B. `* V# E0 [/ J0 O
) i4 l+ E- W" E0 Z
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:6 @& q8 b$ }& ]2 U* g w
. Q: m& E, D/ h9 o; P7 w9 `2 _
, H. B/ N1 A8 L+ }. y <?php( x3 M+ l( m; {. {
4 s) a4 t2 P1 P9 Q2 r
5 x% Q/ |: f0 b4 }/ p9 m2 V/ f$ W$ U0 D
function is_allow() {3 l! |- h3 C' _3 o) c
0 T' y* v, l* [8 O" F+ D$ z' `, O" V6 q# i! I" P5 X, V
if(!$this->fileformat) return false;
( \. \5 B. A7 u! {
+ Z$ y1 C5 I1 p: l$ r- H+ l& n5 a7 D- u9 E
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;/ S% E2 |5 E9 c* o) H9 Q
) E% h/ x4 h3 P! e v- _! g7 H. y5 Y7 x
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;/ F) @# P( F. m7 r2 O3 ]% c
1 w; N5 S/ B. x6 N* T
/ K4 Q5 |' Q, X/ v! _5 ^: M' I! S! m
return true;
) z/ ?6 w2 d7 X8 q1 n- W
% _. O- l9 x7 ~* k0 C5 O% d; m, M, y- e, Z& @& e5 [
}, v, [. d9 p0 o4 N
; P: }3 J% L) g* O; _
, Z9 c9 g1 _9 Y 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。: F. S2 N+ z5 K
8 g; X0 R* u, l. I4 T! C# Q/ @ Z4 h9 R) g4 T) Z. c
接着会进行真正的保存。通过$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文件。7 H/ \1 F, A' F
) j$ m& F [% m& W4 M
0 e# h4 h: `( |& a 漏洞利用/ g8 S* k6 x" a1 H' l
/ [+ ^; ^8 d Q( Z ?* _3 x% f. L# k: @; u, r* a2 }2 ?
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。: _, c6 ]+ U( ~. `7 [
( d% p" L# h" |3 z, a
5 n% b2 v/ j% y8 {' F
- t. d, Y! j% ]/ A( Q, S" F % P6 ]: P3 |0 S* [0 l
" P; m1 Y N. g W8 u
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid) p% g" |) X% w2 z
1 z! y. v6 {! ?
# s+ p+ F, W3 L" w6 s: J
不过实际利用上会有一定的限制。0 r1 i4 \% m6 t, `8 ]1 v( L' E
, D, i) Y6 F! q: A' k7 x/ Q3 O' p, { J, A9 k9 n. u: h
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
/ Z$ q6 A0 [$ S6 M3 B$ n" L
5 t6 b$ q' Y( J& l
1 e) x F$ I& w' C$ Z6 Z) F2 |# f4 x
4 Z0 r+ n# ?9 v' L8 T9 |. E
+ x2 J, ?% p1 b$ c* y# Z
* w- q& F2 ^! M/ w, F/ c/ E% D- s, d 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
; ?+ p3 H2 n( u! H, u 4 ]% U9 J! ]3 T% {" U4 Z
, \& a4 V( o$ ]: N; U1 T0 c
省略...$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]);省略...5 P/ L Q8 b& ]1 j
$ r$ N. n0 i# N$ N* [: d& v
1 S: ?0 _8 u2 @ 因此要利用成功就需要条件竞争了。
8 W' [: Y4 A# I) M0 R1 c0 Q
* c# P) S, a1 c' u! |' z4 d% Z$ y# x& S* @, Y5 K* i4 V
补丁分析
& `7 A& z/ F: z2 E1 |
" ~. O, [1 {( p- u: B& c4 U8 t6 J& a0 T" L! C' `3 H3 |
# n- M9 W. Y9 b3 O i$ C2 ? e$ H9 E / L* Z% s" C2 k) G$ `
$ E& C+ T0 T! @9 P! k
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
- j. _* x/ v3 i ) i. a! K# n+ v# D+ E* N$ f) R
( W0 l: c4 _/ Q
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}% h. J/ q) R% J: p- W- P
5 p) K1 l/ K& h* @8 q, [9 M
! ?( g# V7 F$ R) e1 T% ?
$ x. A6 p4 H! h
6 h9 b8 s. I% q- ` z5 o$ Y' W1 m
# u1 ]# c: u. ^% V! q4 x 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
$ o6 }* U& k5 ~4 w. t7 G/ v5 K4 A: l/ q 7 O# [( U, O' J0 h8 \# j" ^1 f
9 z4 W( B% ^+ f0 O! H2 { 在is_allow()中增加对$this->savename的二次检查。% K7 _4 n9 e- D& ]$ e2 j
/ n4 N; b) Y& R1 f( |9 o& }' G* E& Z) r8 m$ o) j) n, L/ V
最后5 T; D; z8 Y7 g$ {. V/ e0 }
E4 B+ g1 |2 ^, u
% R. X9 K2 o& I% R/ b 嘛,祝各位大师傅中秋快乐!
% q- |6 n$ p" H8 V% G - j6 {6 E x* J
. `" E7 _+ O$ p/ X
. S% i# r" M& N" v2 F+ b: m
0 o& ^" Y8 A) X% k/ ^9 [4 w
|