: T" N, p* s6 W1 f! v
5 b; L4 F& G k0 F$ } n5 d9 R' i( a4 P% `) T! _$ P
; O% a/ W) ~3 N, b) x9 B
前言- k1 S, i& P) }! P
8 u) ^. b% y, t0 l% z+ q: w
! ^" `0 Q% X6 B; D( Q. p 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
( e% f8 z( @! O/ R; | " N% u8 J! Y. @1 L: H
% {4 l$ z" i, ^8 Q
' @. X5 D- \/ u( P# h7 u " J: S9 u$ r; m9 z+ |9 h. I5 R) y& }
5 j% g6 w- E6 ]0 m0 ~
漏洞分析7 Z( ?9 z7 a! P9 q% x. u1 p, }- G
# \ z! Z& h' O- a9 r( J" h6 C) p1 t# C, }
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:! ?4 {4 `# ?. p* n! h: I5 p
- Z7 k% `2 e" o5 b3 a% q
) }; M4 l& E2 X
2 ]2 ~# y5 `4 |4 l% @
2 Z6 J- j! k# Q" ^# e
z$ f; H c$ h 对应着avatar.inc.php代码如下:
2 q+ E0 Q- J1 }- |4 V9 h7 {6 o ! `4 G- H& x& e0 a3 H
! X! m7 E9 Y- X; V7 Z; D/ I <?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) {7 u4 P4 l) R- r! H5 Z
! E4 p# e' D. P3 U B4 z/ ?
A% C0 {3 G5 y case 'upload':7 W, S! Q: [! L7 w
* i9 k! V3 |6 r4 q% f4 X
! F) h& H9 @6 }( s9 x: O8 V if(!$_FILES['file']['size']) {
~. |; q3 x0 O ` V1 R g. N0 Y) h7 C+ F% Y9 H$ r
: }: H3 y/ }8 K1 m, u) |# l
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
1 H/ t( f5 q7 K* ~, g1 F0 B+ u: ?: u5 b
9 u3 _* [/ _9 V8 t6 r) ? I( \/ j( u
exit('{"error":1,"message":"Error FILE"}');6 D2 i$ H/ w* }, h
`2 d! M O) G4 i- F
6 B+ j- }6 t! X
}
; g5 t! U' s! P( ]. T/ U( j
0 {" a8 p( G5 [9 f7 g; i4 |" V% b, `+ s
require DT_ROOT.'/include/upload.class.php';8 m7 l" s! p' q$ Q( m9 L( F. i
9 z- g7 }: I* A$ w( f
: u' j/ j Z5 \0 Q8 t1 D0 g# y 4 W" h ^8 I2 U. V9 u
/ c/ j9 U* y, R5 S7 q
8 L1 j" X% S* U! J. } $ext = file_ext($_FILES['file']['name']);
, h$ j( l, d6 o+ a" f8 ~9 I0 h+ H5 Z _' q / @3 V) r0 Q+ V' t, {( V: M
- \5 m+ }8 ]: t! z9 u $name = 'avatar'.$_userid.'.'.$ext;
( O5 ~) v& z o ! L6 f* e8 s4 F) y3 n
! q* f- v2 A, x9 O8 X" x5 e $file = DT_ROOT.'/file/temp/'.$name;
m0 `6 ]* S( F
* Y9 g5 _& K! t
1 P% l) V7 Z" ] / G }& e& L+ z5 z2 S( x2 Q" q7 `+ ~, X
1 z* b' S5 I0 ]! ?
+ F5 }6 j3 r8 G1 j8 P8 s if(is_file($file)) file_del($file);
- ], o0 o* ], R8 v" n3 E ' _4 y9 z7 |' D, ^! c
' q7 T `- y+ D% A1 L2 W" |% M/ Y5 h $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
7 j/ ^& ~% {7 B3 L' C - S5 r- R; g; ?8 W* E, F" @
+ d8 {3 T1 g1 `9 r1 G
- {( h( ?- e% \+ X9 a9 P$ r+ w
% [/ d0 q$ [4 J- l/ \- I2 j! s, ?4 \
$upload->adduserid = false;0 m1 o9 U6 ~2 L! D
2 U2 @6 m3 C6 B( T
5 e" U2 p& o4 f8 u$ I
7 {, G" s$ P/ d- y8 X! r( h
) x$ M" e, O9 [1 f) p+ P; ^" K5 o; m j2 e% x
if($upload->save()) {
& |; d5 E: Z- c0 S - B6 U+ N! ^6 }9 P S' ?
! Q* F: Z# N& _8 X/ r ...
, c5 A6 k' W* a8 k/ n- m 1 J: u8 F2 O% k0 ~2 j
e# S- H$ E3 z0 {
} else {% \8 E" t8 l, ~0 ^1 G5 P0 H! g
$ \( D% a& m' _5 q5 Z8 q' a- ^6 D, E2 ?$ p2 h
...0 C% y' l( z& }! [
. x. v% C4 ~1 H" f
' @1 h' i9 _ s4 F* |6 a) F
}6 ?$ X8 F, f; M
) Z- @8 ]( D; ^ D% B. \" g- `3 A8 z# D
break;3 L, l! y4 w# Y& i# c1 ~
- t5 U) w* G L: @# \! o
8 q# f; K1 I/ h8 X( W8 k 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。9 C- c+ N6 y& h* b6 s
2 Y: w+ J" g- y+ w1 Y5 E8 r z1 P s; a0 ~ P
upload对象构造函数如下,include/upload.class.php:25:
$ k! q Y. r9 q7 x # S9 B8 t3 Z2 |" c# T# @+ Z, T
0 s2 _7 h$ j' w+ l3 Q
<?phpclass upload {
5 d2 ~# }# _* O4 @) ?
- R! H5 @& L& K+ \
: k- Q h" v6 t& h6 P3 m function __construct($_file, $savepath, $savename = '', $fileformat = '') {
+ w# O5 Z# B+ h$ L) X! B 3 H" b) w6 y1 Q- e% i( T) f
9 U) X! W C- Y8 H global $DT, $_userid;5 X7 l$ z* _) v1 t4 r! V- N
2 X q% C6 I; |. l3 F K& e7 a; f( s$ q/ m a3 a& _
foreach($_file as $file) {
Q5 W! x3 ]) I; M% H% |
6 Y+ x6 q4 F$ c. m
, h9 k; X; b* y $this->file = $file['tmp_name'];1 P- ^ ^9 s7 E( i8 R
: ^1 a: w0 q- F; c' u& ]( F- f
5 P `" L: A$ ]* ]- u $this->file_name = $file['name'];: n/ O: u! O: ? G2 l6 m
. m0 X( k8 [$ @1 P
$ l: ~( g Z9 E5 x( s1 D $this->file_size = $file['size'];
9 j |/ v" k! Y) |, _& I! M. R
2 K, H/ O3 Q4 l4 o
3 R6 ]4 z; c! `( r8 F! {3 S; F $this->file_type = $file['type'];
. ^3 p2 h) f; M$ m5 D
9 I6 [; N+ _( {: m) g! r# X2 X! |, R5 {# x6 u2 ~% J( y
$this->file_error = $file['error'];8 E; u0 H+ P X5 Z
. C" d6 t/ ?* l/ P7 n& ~3 F& O1 k& K8 g9 M% @
8 E# E" S6 P) O5 S- m+ N. G
& d8 V7 T7 F- h( a1 t/ K/ w, N, N& A2 O$ j6 t1 r
} {; _; ^2 n+ F" H c$ |
& U7 e( Y+ ]) C& c; R( {6 y: `
, ^$ K) N6 m; D q $this->userid = $_userid;" J% [* v% ~0 j9 Y# B$ c
* d9 P. G& j* b2 [6 `( m, f
1 q' w6 x; U' V $this->ext = file_ext($this->file_name);" z% K7 F, R/ V. ] ]9 L/ S8 A
J6 w+ \2 F. L0 k s+ r9 M' x7 M" w4 N1 o1 _
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
y5 d4 t8 e. K& q3 K' q/ b/ \ 6 d1 a8 p3 ?+ F9 k) R
[$ C1 D& D2 K# U9 V! G $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
- c2 ]7 X# H7 z# o/ l 1 q9 ]' M' a8 P
1 g" V, b; U! H( A/ Q* c! f $this->savepath = $savepath;
8 L# c8 z# @- L) ]7 h X
0 H4 W! H) |8 w0 P
/ H$ w' w" v q9 k4 h; F $this->savename = $savename;
( A* L+ u7 H( @2 F. ^8 g3 Z5 G - `) L" r# ]8 I4 W2 L4 b# }8 z
7 w, {) A( x: F) S+ v
}}
; P- G% [7 V3 u; j # C: c2 _4 }1 m. u/ [+ h
- t7 s6 b# i& a 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。0 J8 h: d5 [$ _( i2 s
+ S' ^" f' U% e7 u
|1 m1 _/ x- x$ ]4 p+ G 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
' b/ R# X. D5 U; ^ * L, A- B+ Z8 B* q
; f% t T* C! }2 X
$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- z( z# z* o+ Y+ P
' O3 J) [6 z' O" J/ `0 C% S
: i/ m! s S$ y) F4 q 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:+ g H! u5 R1 a
6 `5 V2 P5 K5 H* A3 n
1 J t0 b E3 J; q8 ~ $ J* O* J. T B) R7 P* Z: |" V4 n
: u3 l5 A/ {: A- E' P K* k+ J7 R
/ p1 {5 z2 P1 [+ @
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:# }# y5 u2 J# d1 z2 j+ F h1 O
$ E& B5 |* }. ?) o; A* }$ E$ f9 ^3 k
- m) K# Q+ D e1 T/ F0 M2 l <?phpclass upload {, q u! y! b# Y# k0 L. a9 w
" D5 a7 J) q% t* c
" ~( c4 ?% a, T! U* ^ function save() {
- `7 U: _0 i+ a) v9 K' Y
# u* @* l+ S% T6 u
7 e Y8 O3 e9 |9 f; B include load('include.lang');
# `2 v4 }" m: g3 S 6 s( x; ~+ I4 \1 N8 W
6 A$ Z8 A$ x' f& D if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
) Y2 N% p& G' @# k: R
7 _' |5 {8 d$ E) U
% |. r8 [7 x0 R# d$ N ) Q; `% `* K. g# @- a0 V4 N
! V; m H$ k1 F6 c
+ Q' n) _4 ?$ X2 M# t$ s5 N' |
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');+ ?* M, C1 S( {; L1 V. j2 b
% t1 a9 W0 Y9 @) |6 f: C
( O, M$ y! f: p; l6 I7 g% W 1 Z- m- d$ I4 P* ]
+ B$ U" x$ k" ?. N* i. ~% q4 z
1 _4 y1 b4 l" C' y6 H5 I if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
+ E: K' ^" T0 V. G( ] ( C2 u @$ e! e6 ]
' e3 V7 g6 a, Z- S6 {
X% L0 b7 J8 z9 G4 a1 h, l4 f* Y
$ ?' F- k7 I- g- y7 G1 i
5 v4 |. ]$ W" O; P* g+ _1 ` $this->set_savepath($this->savepath);
\ a8 ^* |2 l( ` j 2 G# V, [# E4 ^9 L6 f: g
5 `8 X- l! ]- g4 g
$this->set_savename($this->savename);2 `9 [/ i# g3 q6 s1 N- i0 I
- s: _5 c; S7 F& w' U+ U7 o
$ `$ {7 D4 d" y1 \ R ; O! ?& R" f/ D$ L: {7 z
0 f8 \; X: }4 h
+ E$ b" F2 [6 t/ t8 N9 m if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
. P0 ^; P( H0 C5 [; w' u/ f& D
$ b4 n0 n* }7 v3 c
& ^. Y4 U/ W4 m! P if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
* K- m0 r2 Q& f3 h* o& k 9 _& R% P$ ~3 g
2 G1 F- Z y1 R4 V if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);9 U3 b( q2 i7 k5 K7 }
- W8 g" V) s6 v! J
/ [6 }$ s0 B0 I) o! c1 @
" S$ X- c) r2 y1 f6 f+ `' k+ U 9 d# Z9 ?& W: E; @& Q
D+ R4 V8 h" e0 E $this->image = $this->is_image();# d% E, f/ R8 S8 {
. a: F) z5 e9 k" J4 p+ Q: `/ G
8 m7 ^8 f$ k. O/ n; C; ]3 H9 F( E* X" I if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);5 k& T5 N( p3 x7 C; D. l4 f
5 O0 I. W: M/ X
7 c3 S3 Q J+ C: i) ~
return true;
6 ]- ~6 a% w& c7 m 1 B; b' Y; y& v6 ~- x# R
; E, |% K" r) n
}}. A' p6 b+ z/ K8 Q
& Z. C7 Q2 o( V# @5 D
, C. N' c W& k9 k2 V2 V 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
/ u+ h/ U. z, s6 X' l- e: D ! _8 I- q9 ]! {) ?+ J1 @
1 z, V0 N/ l6 } <?php
- h5 H% Y! H3 B3 G, v" ?
! V, g) G& f3 D3 N; K7 D- Y) f) Z
# @7 r) `* l* W$ I function is_allow() {( @: G* i4 s; C9 E ^3 h" I# _' w
1 a2 B: [% C( r0 A7 r9 Y: K% Z% x
: u6 C" e" ~% R& |+ l( b if(!$this->fileformat) return false;& k" s- U* d) E% v2 Q
* J. c( t. {' E- ?1 l4 F- c
+ [$ P: c$ Z; L; t- Y0 g if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
2 d% x6 e" F1 U) a2 ^ , J: ?/ Q- M+ ^" G9 x; z" _
j9 B+ g+ j7 z+ j7 ^! v3 N7 N
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;
9 {- G; B# j/ S2 L8 b 1 @1 Q* I8 A5 Z% a! I: o8 v! `
Z9 ?9 x3 f7 R" f+ A+ z
return true;- |; n2 U2 h' z9 U# i t! o9 V4 l
0 Q( [0 b: h. D) U5 k; G: m8 k
, o# `$ L& j, M+ Q. E! } }# F7 C/ g7 o) A" Q0 X2 t7 S
$ i0 }! X5 T' R3 ?/ p* x; V
) f# V" b6 J; q+ |2 E0 J0 O8 v
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。/ C5 F2 `; o! @6 g; [
& ]3 \/ P) I1 S6 ?& j
/ V- w/ q% g6 p
接着会进行真正的保存。通过$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文件。. B# Y1 l1 Z7 x( i, Y
$ C" j- v& [( w$ d- O) _9 Z0 J
$ ~ G: n$ j$ ~1 T 漏洞利用3 s3 X; S4 c5 l9 H* L. m
7 J1 U) a/ Y9 J- U0 p9 v+ P1 Q, _* A
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
+ N8 W t" N3 H2 s4 B7 u2 U" n1 R
5 Q- R( |& T/ m) m1 ?: f: ?/ S" S) L* r6 M
2 `) c9 N% k+ t- O) ]" C0 R2 {; b
0 V( q, m5 `1 e% o
' r% k; C* i. |! a8 B 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
3 _' }1 a( {5 H
5 O, j* Z1 I3 X, j
# f- H( \5 s$ {- J" O8 F- W1 V 不过实际利用上会有一定的限制。
: r4 A- v: T- v1 L. ^& i# D. b: {
. b* M4 c; i3 _9 R
) T9 F; e. K3 X G# W 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。 o. d% _2 f$ U" W) \) c
3 D" u0 ?& e3 T% H1 e1 o
' e" p$ j2 d9 F+ ^- A5 c
9 b# j! w, U2 F) n Y+ d
# ~/ Q" l8 m* {. [) ^: t: F7 {* |3 Z( x* t) B% [
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
z6 d* x/ P& f
2 c% q( @: S4 B' o4 M; L& A) K3 A- u4 M6 v% p$ M
省略...$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 k6 Y: f7 y* E9 x ' s7 p; p$ J4 d: |! c6 K
6 E# v9 S/ I! B' f 因此要利用成功就需要条件竞争了。
% D4 `3 I+ |4 D- M- |2 D. }# b
; E" Z% V$ c7 K, J5 t6 ]& |0 M3 w. v) E7 k
补丁分析
5 ` z. f/ Z/ X0 |2 R& G6 d. e" z8 c4 j8 ]
, \: F7 p7 q. ~0 q0 S! S4 F9 U$ F
* U! Z" F. k; O8 J/ \
% i' k Z2 ?7 i, M7 u: F7 S
* P9 ?/ P) I' m, m* s) z0 a+ s 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
8 W, u& {9 u. [. |+ J S
) s# e) k+ V6 v1 ?7 M; B! T
! B) F7 \ R6 e3 ^ function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
, l# Q" J' ?5 @+ E; u! G! f
5 z+ j# u4 `( H) B5 F7 r+ k: x( S+ g0 t5 q+ N
3 S1 @9 I+ B2 N4 R& Z) J2 o. m. J9 h
2 m8 O; Z5 N; W) n$ r% z
; R" v4 g( [3 P7 c8 B 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
* ^0 y5 a) Q! l1 D( ?
, s* H! U. t. B
6 C0 c5 s' [7 q 在is_allow()中增加对$this->savename的二次检查。& k9 u: p( d5 K) ?+ J' o3 l
$ {8 a8 S R. `* J' |6 m
6 e( o q1 [2 {( |; ?
最后
/ |" C m% @. j4 j8 ]. h/ a. N1 N$ L0 k' q4 Q8 |/ R: J: ?
. Q y. G! U; {1 ?1 R4 |& `& L8 G
嘛,祝各位大师傅中秋快乐!
7 r, d' H) Y3 U, c% H9 i
1 K- j4 v' J# K% {# D* b. I" K
2 q* o+ p+ Q' o( |
5 p# Q' ?3 U/ M" `$ _
+ Y0 ` X% s) n, x
|