6 L# @. O* v( t* D1 b) l3 o
7 U- w# v& |5 ]7 Z5 S. z, \0 l
2 k7 d$ S' T; l/ ?9 Z3 n0 [' p) l' Q; _$ D
前言5 x# \2 P/ V( E$ A- F( ?; D
; M. t# K3 n$ V7 A6 T2 @
) b/ z. L4 P2 E. a+ Q 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。3 P% ^$ ]$ e# o) I3 D0 h i& x
* B0 O3 y- y% x) O
+ B+ J+ p4 Z5 b0 H. S5 V
" D) d0 q; |* G, M
1 @3 n G' b$ L1 r# A4 Q: n# Z: L' n6 A$ A
漏洞分析
1 V' v9 d J% A- q0 R
# X- [* `% P# Z) W: {/ P0 y
2 [; H0 I! A& {) U0 o: C 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:% v/ e0 k5 |, q- v# w* {: c
- Q% x: V! }1 E% A" j6 I1 q* K y9 [# u# U! q
s( T- ]0 A, y, U) K 8 n& E' g. E/ d" J
4 V: Z* b# P( [ 对应着avatar.inc.php代码如下:6 y3 O$ _) E8 G7 E
: ?+ S# h n2 [: _
% [+ ? z! H; X4 E/ H7 j# A4 ~
<?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) {
# H8 w' a& y2 Y! H6 r
# ]7 Q$ r7 S* |" q n
% Y0 E$ h. c2 f case 'upload':
: t) R! S0 H( \ / M, A& m& J- _, ^1 j9 B
" v* x% W1 n- q$ g; h
if(!$_FILES['file']['size']) {
: d! W& R9 f# S: }
7 t; }" e, Y9 O
8 w5 G0 R' F8 ]4 H& C if($DT_PC) dheader('?action=html&reload='.$DT_TIME);8 W' g M0 `* ]. N
- a5 r" z" `3 y# Z7 {* Q
7 ]3 X0 ~% r" L, W( f8 @ exit('{"error":1,"message":"Error FILE"}');
4 [0 a+ ~" ]" p9 C7 M u; L" P # k2 C) B6 r7 P( W0 t- D
# r. P+ m& f& U5 f% ] }* f$ T b% p) Q$ ]. \
0 K9 @$ b6 S. o2 m, k2 r( v1 U9 C5 ?4 z6 L* U
require DT_ROOT.'/include/upload.class.php';" G! J; i8 q$ y
5 }8 ?% Q5 z7 a( Y
% l4 L1 r1 c( K( s9 D
2 m( q$ X L3 N$ g
8 j4 e0 f, _' p6 v+ I. E; @" X7 `/ F- d! I& | i# P- P
$ext = file_ext($_FILES['file']['name']);
( ]' A% Z. O; r' _( ^) S4 S
$ @5 q& r- F; f* T9 Q* X5 N3 N
9 x7 r/ q# C7 p( x $name = 'avatar'.$_userid.'.'.$ext;/ N$ R+ }1 _: K( O. ^7 a
' y& Y3 w, o9 Z- v
5 X6 M5 \6 @$ {: Z z" @
$file = DT_ROOT.'/file/temp/'.$name;+ k6 \) s# O1 E' U# [- _- }( r% K
5 ~; S4 ]+ n3 y3 T( b( ?
5 g O+ ]* N+ a4 d0 H! q9 m + b- |2 R4 Z# f
9 Z' e ^* K% U; E
: _* H' k, \2 ? if(is_file($file)) file_del($file);
/ _; S0 \/ n" i$ I# n5 t: c/ p5 p
( P* V7 P8 B3 o: l
' I$ o- y. J9 K' G2 _# } $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');$ |3 r1 M* G# f, A( x3 [2 [
! _/ K0 q1 L6 c& J' r
) [3 {& |) o& v0 { 7 V: t t$ P5 V% p
; N" d+ L# q% \7 k
' \4 T8 }& n5 [- ]2 e0 o+ A+ s $upload->adduserid = false;: i2 \; D$ K& L) Q/ G3 m1 f
T3 M7 D% I2 ?) A& O3 T
& P, O# V! ?4 s1 K) A3 d
2 W6 J2 k2 I' }* |. _. ?% ?0 s
1 ?+ h) {% W4 d: d' l y6 @: I
8 x' I+ O, A2 T6 @ if($upload->save()) {$ t/ }5 E" B, k+ B5 j& K! k' a
- q3 V* {4 N8 b" U4 A
1 A4 w3 d M& g& D
...
z0 r! @5 c. u : S9 L. h8 r- G4 B6 E
+ ]4 g, P' E+ q, }( L- l
} else {! v" \" ^$ m( W$ h! Z9 f! C# }/ S
# D+ @; r% z- D( Z9 J% D
3 e7 X* [5 s; z& a) k1 } h" ~ ...
- Y) _3 p% S, u9 i
" }. ]- N1 Q+ y3 V; ]( t4 ` h) C3 @2 x* x$ n
}
0 W5 _1 z# v1 ^4 ?5 a" R: r' o; l
* i. e# O0 G s$ u# J+ s; V% j/ a) I, x0 Q! \+ B
break;! w ~, K5 Y' _" A
9 V( t+ [, p0 U! x8 c
# V, W, v3 C. A! \, u
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
( ^* H+ O `* V7 {! W% X7 d! Q
, P4 v1 O9 f) G" V
, [2 a; k1 T& s4 U" M/ H: ]. H Y7 e6 s upload对象构造函数如下,include/upload.class.php:25:
/ y" \2 I: y$ J2 @) ~ * `$ F8 } z* u( \" i
0 V% r+ a! C' q6 t, ] <?phpclass upload {/ |/ l# }: V3 s& A
8 b9 B+ s+ F6 c5 y5 l
, O# s* A( v: k function __construct($_file, $savepath, $savename = '', $fileformat = '') {
" N1 _4 j, {# o. ]
$ A7 `% L5 t* G; F, x9 D
, `+ v& E3 y; b M# A6 } global $DT, $_userid;/ K1 [" N# @$ L9 j$ l8 v" Y
5 w2 B: p( F, e+ H; k8 K
3 K1 m) H J& n4 Y foreach($_file as $file) {
- a' U% }, q8 r2 Q; n: o0 m5 q
- u& [8 f8 ]. R5 U {
( G" z4 q A+ {( c+ z $this->file = $file['tmp_name'];# Q5 T" I: w% ^
! p; W# h0 l$ |6 A5 [$ I3 B+ t8 L1 F+ V* F) l# p
$this->file_name = $file['name'];
4 w1 h+ L6 U5 Q7 }8 w " N8 ^: O; O4 y
4 ?4 K" e9 P3 C# \ $this->file_size = $file['size'];
& u L$ X: p. } 9 g6 m4 ?. F# @% V/ _- t- Y4 Z0 I
, Z7 N0 A5 O& x/ j6 ^
$this->file_type = $file['type'];# A ?3 ^1 |8 H" Q( d6 h% s, r
9 ]3 C9 I& r3 o6 [+ R6 M4 w- C, G9 F- \+ b
$this->file_error = $file['error'];( l, C& V4 _: h/ m5 |: }
i4 H O! E( n% E8 g% o# L* j% \9 _( W3 j# Q) i
) \3 F+ E2 A m2 C* a9 `' y. \
" ~) \* f6 M) \" t7 s( g& @7 S4 ^) l% D. \+ ^) `. ?8 m
}9 l3 G* \% X' M4 A# e
& ?. ?# S, V& e$ _. s9 M
* h( h+ C$ N" \" Z $this->userid = $_userid;! v @: z6 D# ? S- X7 r
; ]+ m( @+ n" P+ R9 y
4 z* V1 Z. M$ r4 I$ _. U $this->ext = file_ext($this->file_name);, ^8 x0 v3 n, h
7 Y3 ]7 A2 ] Z/ d( p
g# B6 i% J' X" P* r $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
: } d% [% L+ O0 ~ 9 W# V$ E$ [; S/ w# R& x2 @
4 C& b# A. j2 o! I' {( U3 g
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;1 g8 O. Y9 i1 E% ^9 B
4 [- P1 ~9 ~* l0 I- J: `3 j: f$ {, [! [& V$ c* d
$this->savepath = $savepath;
! h8 D0 Y7 n! W: A
4 A( x# c: o5 M$ ]8 I( W& k; d! R* ?& x. j. Y9 T
$this->savename = $savename;
3 o# e, w) K2 n5 b( \ " `0 T+ }1 l* |- k" P
* F+ i% D( N- R9 _( O
}}1 V* X- [! M4 i9 M5 H
7 a, U7 A i+ m5 l
0 s9 m( v7 L0 q c+ Y
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
W' d$ K- a+ b4 I: I
; h; v, F$ @4 T! O
$ Y9 R+ v$ X/ m! ] 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 - P' T8 n* Q5 f" r2 i
7 y, C: Q* n! h& v0 q. |
' d. P3 ^3 a4 [+ t- Y
$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* l7 @8 W6 l* F+ X# u1 S
! s1 `0 D9 u! R: L' i) ~
' S9 p& R4 e: A" b6 n/ N 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:* K" ]" O; s4 V- K6 ?
/ v. U( }4 T, p1 g* `# K- o
$ J# {! o( O0 A' |7 K' e9 W ; r; V4 Z3 B) i. {% [
# }7 Y4 O. }$ E4 ]# R
6 K1 W, A0 d$ t% u9 l% H 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
& c Q$ u7 ~6 g2 m/ K* J+ R
n% I1 ^( r8 w3 G, o! p8 Y7 ]# [1 N: w
<?phpclass upload {) y1 T1 ^/ S. }5 O
1 r( ~0 q9 S; k- e: U$ s/ t4 d/ v' j) F4 t# D& r* X! x( o
function save() {
3 b: F1 z1 g: x/ h$ H8 W( i# Q7 Y' k* a
( e, J' C( ]! V3 o: o& \5 `3 g
) F$ c( ~/ u; g1 S+ w$ m0 @ include load('include.lang');
2 s+ H/ j, }, J5 x6 d. O9 |
( T# C+ z; ~: t0 I
+ T( X# b$ ]( i! E8 L if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');, {1 _) X, u, o
- o8 I" g! r, _2 e# e
( f. M) C& b1 C# \+ F + I6 t) X( j& d" m( y( n
+ v, j$ C @. f/ S. J, z. N B
3 u- ~9 h7 W: J9 O5 m! v if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');+ f- F0 {0 v- [- a5 i, t
2 [2 K, {& s8 l
+ {2 H @; `/ ^. Q) c4 T
4 R3 U- \4 h" W0 t/ x0 N 3 z4 J7 D. o) {* s
- C* }3 ?' V% k( g* x if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);% y9 x9 a- r: e
, Q: W8 g2 O, u9 |+ w& k# E0 `& k7 F n1 W8 t! ]; T6 \
! k" I7 [1 K O# ^$ Y
" ~2 z7 t& t! l% R$ b) \
) B8 x. h# F8 P $this->set_savepath($this->savepath);3 p9 Y5 d: n3 c2 e; L
. \8 s; r" L7 S9 u" {; P
, A+ {) k( p+ N4 e Y $this->set_savename($this->savename);
! T' t& _0 D4 _& t4 w3 B
' S' e3 ?% m+ i$ E
* y8 u1 ^, n7 p1 z. l! c * y, I! m5 n9 l$ z
' ~+ l% c0 @6 ~" W; s* W/ ^1 B# l
' M- q4 H# b0 L6 @: W1 @+ o8 c3 V* O if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);/ H7 J9 G4 {1 J5 n
5 b; H4 C" N" c( Q, j
! T6 Q2 m4 j7 h
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
! h7 [) \* O" f' o( a
% `7 n: K. G* y. Q8 w/ n
4 g8 p7 t( j& t8 h0 r if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
4 j2 H7 D! v9 n9 J
' j1 u+ S n% S; G
* h: S( Z$ j8 U7 @6 s' o2 ~ / |5 @8 s; p& z2 u
- ?: ?' d& A9 t+ e- L: a/ a/ t+ t
$this->image = $this->is_image();, Y2 c4 h: r' o* T
! R. J! v! J. {/ R; G2 v8 B, A& K( h
r. I' I9 d0 G# ^4 X1 M: O
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
5 C$ i( O2 N( K: J) h: ]+ ~
) k# d, w( H q% y4 f" c, P6 J$ g! }! ^: Q: f# M
return true;
% y& |* E6 N$ |
. C% r2 r' A, B2 ~+ `; o7 g$ ?7 Q/ M/ n
}}
" _# Q' p1 B1 a" z p4 F7 c/ k 7 [6 V# D. c. _8 F" x
$ Z9 q0 m: m. W' l( l9 c
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:) I2 V9 s, }/ h$ U2 y
0 [1 n5 K: i1 E/ S: @. ], M4 p
' `% s" z# _# i8 N( i* h
<?php r7 `8 p) D+ o
( V+ R1 ~1 b6 b) Z+ y% H
7 _2 Q2 O: \# C. b6 u( s: T function is_allow() {+ R# C7 |: n* }, @7 p
( n H3 J F5 Y" f5 w, S ]' m) k0 n8 R' Z
if(!$this->fileformat) return false;
" \) T: d7 K2 d/ I$ O0 q, U/ b# u ; q7 S% y# J6 s% e
# h# n+ {" d( S5 N1 {" }( {0 H if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
3 }; K0 h1 n5 r
9 R8 C2 Y( s) d, n5 f" q, d
s2 S; J" P. p3 T9 G: s% g3 R+ 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;! z8 b# T) u) k& C
- ~9 C- ?9 ^. O8 o J" K6 G4 Z2 Z$ G# n0 n$ _! I0 T
return true;) l: ]$ O/ O4 P/ V: e1 g" L. c6 ^
, I0 W) E6 L6 D" w/ D/ b7 i
/ j* e |) S- i* v
}
; r4 @8 x: C! x' j8 f, M
& [. L# O8 C7 ]. {9 } r, ` u* e$ i0 ?. F5 O$ t. v
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。 `5 X% H7 B9 R" I# w" i
F5 o; Z' S; A) V' ~3 A S" R+ N
: j0 A) c2 X+ G 接着会进行真正的保存。通过$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文件。; s) U5 T) V" D2 F' M; {( {* v
. v: U1 ~: k# y: A8 P g% Y4 w* C9 J6 }
漏洞利用 J1 e, f6 A/ j' k0 T) v9 R
7 ]3 N! r& }9 ?3 q* Q
" }; r2 S) V4 y 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
' Y7 |: |" t3 ` ! S! N/ S# ?4 ~6 J3 P& x
, r- j, z( S3 g# n$ i
8 T7 x# V+ u8 c( J) o ) D# ?- x/ Y6 u
$ }8 f' S7 M- Z9 R3 a% u: ~
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid) c- i! X% j1 e9 |
2 g* N* D$ I5 I! |" k; m6 V; a; U: D! o- E' @, S
不过实际利用上会有一定的限制。0 \. ]. L7 B) L. }* s
4 j' x) }) ~, U6 A
4 D& |6 N, A& S. K @; e 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。2 @; z8 ` G B; W' t( Z
9 k" B O1 e: y% [) w: u
' @6 D& V* S5 I6 X* ^0 O) u+ m
/ q( S4 }0 S" o2 Y
$ `1 ], M$ a# Y# T$ M
/ N I D& L/ q k" t8 z( N, q 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:; J4 j4 [ B. Q# s( n
! n2 M; B' g. H5 s; N
& Z+ t' e* l" H" \8 i 省略...$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]);省略...0 T" E' x- ?6 R( _
: N$ s/ m( M) F4 M
8 @3 @3 \8 s. l3 Y 因此要利用成功就需要条件竞争了。. s( k& i0 c; s- x$ N
% u. R6 E% d" @6 C+ ], `( i; q* g% G" v1 _4 x3 R
补丁分析: b7 c* E5 A% l: k$ y% D0 K6 p
& d$ ^* {6 F( x C7 O9 A( D5 N/ f: I4 A3 S# j1 p+ T
2 ~$ X$ E3 X# m* N+ O
& K* Y" q( c- p* g+ r: {, f& V$ Z6 ]
# P; l& g8 `* N9 N( P
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
. J! x# J* F' y* @- t/ ]
& c0 y9 \' q: B8 O* G' z. f( g
! D, I! P& T: G5 `( Y* [4 S function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
# u: L% I. n7 t; k + f2 ]- h0 U! c! f( q6 ]; c
' j$ p* e r8 M/ y R8 f+ ^ ~
% e' e% X# z; i e
! o- ^2 \& q: u9 P) p" n' r" k7 W. f4 q$ _8 w+ D( R/ t* Y
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
2 m/ W1 e8 h5 h# f( }2 y 6 S- V' J0 X/ Y% L& B. Q
9 b" Q: C5 z# \, L. q, L5 @
在is_allow()中增加对$this->savename的二次检查。- W3 W5 ?4 l$ ?! k3 r w
8 U; Q: d# b3 k' x% g, U V5 I. x2 m# g3 D; a( p2 f. ]: v& x- ?% m, y2 b1 U
最后
0 I4 c2 `1 q' U
9 d0 V$ _% y7 X
1 `/ R5 x' f! ]" x4 L! G 嘛,祝各位大师傅中秋快乐!/ |6 x& u! C6 \2 X5 n$ I* H
/ [7 g. I; q# K( E- O! f$ N
- I" ?& p9 Q2 d : Y# f# a$ i% \5 @7 s8 |
9 g3 ] f$ h2 M6 z
|