" t% x1 B+ p& p0 z% i
: u: ]( e* {4 T& e, B) X* ~5 J- t3 Q4 Y1 ^% \, n2 M! G$ S/ Z& ^& ~0 n
8 H1 H5 n' V9 p& a6 R- ]
前言
8 {2 ~; e8 q2 U! Y- o$ m' f& i3 h' I9 d8 S) n
$ H" |1 ~3 n* L- F8 e0 c 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。, N7 k; B4 ]" M$ e* o ^
' V& C$ L- \8 I
" P' k# k- M) d9 U
0 U3 o" ?: N7 x4 @" j% }, [
8 q7 H1 J) s: R$ D$ J: m# Y, A
3 |& B' j/ l7 L1 I4 Y$ H 漏洞分析
! N: u8 `" `- ~% B, @6 ^; f# Z5 J2 T) u X2 L; m
+ D# O4 V2 a2 u% K; E 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
# E2 V, M, v* C, q9 q7 u$ U) m
% ^ P, ?3 ^' I
& G% B0 R5 N3 s9 R; s6 O( ~ ; o9 _ O- A' C- a+ l9 j
; J3 d: `( V! a- L5 ]" N4 k& z7 w- Y7 v
' L5 S$ x: ] M6 [( w: D
对应着avatar.inc.php代码如下:( D, g( e! F6 |+ ^) s
. Z; N+ p1 Q7 C2 G) o
7 h3 _. o9 r8 J+ r <?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) {- l0 c4 `" q. q# @* D
. v3 J/ j$ O7 {" t4 b
" R$ E! g. g6 A" v6 o
case 'upload':
& ~( M3 [( ^! X" q4 Z
: x H5 o* D3 ~) Z% P( n5 Y5 f4 A u) U+ t2 Q3 H& G
if(!$_FILES['file']['size']) {
8 P. ?, f- N0 x+ \0 k) E 7 u: N0 n5 P' _! R) |
3 J$ D3 R+ O8 k+ I
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);) @# A8 l9 x1 I! M
$ o, X. l- y/ J; ?# }7 y7 ~
8 l0 x- K( m3 C- _ S
exit('{"error":1,"message":"Error FILE"}');& s" u. B& E) S
" Z1 \5 z+ u6 \* d; E; w
1 i3 B$ b+ `" [( Y3 k6 ? }$ P q& |; a' A: C$ ]0 q1 N. s0 r
/ D6 g. l$ Q* y
; c& b+ S) N! \9 D6 i require DT_ROOT.'/include/upload.class.php';, }0 D0 @9 y* h8 ^; b
) I, p% y7 ~5 i5 y9 B! d+ r h+ Q; n0 w; Z3 N
5 J% O( F+ T6 d* q H' i+ o3 O
1 L& J: g1 e0 p- {% u3 f
0 y7 b+ t$ P9 t $ext = file_ext($_FILES['file']['name']);- o/ E+ `! N; P1 T: K8 r& m! e/ x
8 X, r! W) }/ ?! x6 ?! O8 A2 m* o& Q) \8 E7 @
$name = 'avatar'.$_userid.'.'.$ext;( ^3 `7 w& s% Q; F
r- K: I2 a' H/ m; J) L3 O# a( i& `: i7 o" `7 C
$file = DT_ROOT.'/file/temp/'.$name;
0 P* v% X! ]3 ^* ~
( N7 T1 ^2 y v; b p0 l; e8 v
$ f7 e: [. e! s7 l" ~- A5 W
" f0 G' L+ u# j2 N
; d* q1 G, n$ k/ w9 ^. w# E) G3 N7 v' {1 i2 F6 O
if(is_file($file)) file_del($file);
5 X* ?- ?" i% U- j3 x* G* t x
2 }2 z2 _$ F, q6 }/ B% j; j
5 c/ `4 k. k) j $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
* f+ B, y1 f* J
G+ J$ J* H! P4 l" k8 B3 ]# E: k
& ^# r+ k ^9 v; b
, }0 k3 E7 z, G$ t3 y 9 p! B$ u# Q; S N
: l+ u/ e3 B5 H' I7 d8 H4 J
$upload->adduserid = false;
8 Q( F. ]! [6 M9 b# Z1 ]% @
6 _* Q: a3 I0 W" w x: h C/ j0 S- z" m1 J+ [2 }: }" b' D
, c2 r: e( J5 o3 x* v* L
2 d2 N; V k- X2 b/ W O
- H% S3 @# ^) W, g- {& p
if($upload->save()) {! F# L0 x- J9 [* J
) E& O9 E+ Z" i; F) ^$ J4 _
! `+ [5 C& B3 q* `3 F) q( A; v4 u
...1 A/ j; c5 V0 r( _2 n9 w
7 T( u; O9 X7 @2 F7 l
: R/ @0 z4 i: a2 b+ P! ` } else {
, Q/ a e: Y2 M* x : `( [" ~6 u- [8 A" q* s4 h
N- k5 _6 H! `$ ~ ...
7 h8 F# e3 d2 b7 g6 t E. ] : x" x3 [6 v! N7 z8 u* P" P; f
, G% G- D) {- f# M0 ? }3 T/ q; V; B# L
+ k3 L5 h" L( {' {7 ]5 t6 x. z
break;
% n( T. Y8 {3 G- S0 i% g & P' g$ ?. Q) \/ I& ?% M
0 p% B) |! |4 q( G' d+ p 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
# A) b: h$ i% I& i- n
$ D6 Z1 d) H$ e" C, Z/ X" H9 t6 ~- e) E& D: K5 B) |
upload对象构造函数如下,include/upload.class.php:25:* q6 v( F0 [4 d( F' ~
* g0 P/ S7 }5 r! L/ U. J( g$ m
8 E$ I% F/ x- w3 ^7 a& n6 {5 ?( y <?phpclass upload {
% k7 n v3 u1 Q! w) T & Q y8 L$ B$ q5 F
, T8 j1 [% D9 e" ?# q) K$ b
function __construct($_file, $savepath, $savename = '', $fileformat = '') {# d2 q% n. ]( a) a) L2 L9 e% `) Q, }
) S6 z0 c& x0 m+ n: u
( J M4 K! z0 p \. S# _ global $DT, $_userid;* Q( i9 F8 N9 C# W8 p3 x
% b6 L4 \8 f5 d# h- k* C) |
1 z, I* F/ o- q" F# c# X) g! t/ } foreach($_file as $file) {
6 [1 C X& s7 a6 Q% K) ~+ B
8 [" F0 n* T" G8 ?4 `
; l7 ~+ @: }5 f1 T; c) Q $this->file = $file['tmp_name'];& ^7 M. w* ]4 z+ v
2 p1 @8 w% o+ c& |
, S. C2 Y( H8 u* B$ x" H( @ $this->file_name = $file['name'];
' e+ k$ x2 |9 } z! \1 h& M4 P6 k ) @% u2 V x: v9 d7 J8 J
5 R' l. S4 D& F( Z* B" H) k
$this->file_size = $file['size'];
2 b5 e% g' O3 }& N2 u# L
' Q2 Z2 Z# [3 y' R. ]4 J0 {
* f& |; t1 Q1 s3 [; L+ {8 j* l) E. [1 u $this->file_type = $file['type'];$ Z' G/ q: Q- j& T4 e9 d2 R
% ~( c3 F- K& N! g; W5 \8 ]1 u) H& a) r. z& A
$this->file_error = $file['error'];5 V! X6 h/ q) D& v% @0 N5 L" Q* l
' h# v! w( b) B! Z
8 H6 v* @, ^6 [
_3 d9 q& i$ l% k* v2 q 4 t$ u# j& I X* d& {- u3 Y& @
' x. Y* q$ A5 z" D1 K9 I0 s
}! u$ r5 y; i( G- ]9 J
7 t/ J0 r1 o5 V$ w4 ?: z
2 H/ F% Z _) I. Q( c1 }9 ^ $this->userid = $_userid;
' ^/ c8 @+ Q% d: ?) x
6 o! J7 K% |3 H/ b6 Z9 {' x+ Z$ \! g% P
$this->ext = file_ext($this->file_name);9 L' K( O3 \. `. ]+ a* z
% _1 J2 L9 ? A9 M" @; |5 ^, Q9 i6 C7 B* i) U
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];& t0 y. |% y, K" V7 a# M
6 u- J7 V# M2 L9 @9 I% f2 _* i* l0 |$ @0 R. r, {$ P/ d
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
. b: L4 G! T6 \) G% c" |
2 ~$ U! B4 `( ?. M8 M% v. a# }( {$ P
$this->savepath = $savepath;5 p! y& ]/ m; H7 V
1 x0 O) Y' L. E2 F& g, ]# c, t" i$ o2 x, X4 I' h
$this->savename = $savename;, [0 U1 F. k8 ^# [5 M
$ W: H0 c" h3 Z5 w
( m j7 f) `% Z
}}
% M Z p! ]: m- Z* z* ~$ [3 ~. j
1 k4 z+ y9 i8 M+ }! F H+ _& e6 N6 U7 p
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
/ p' h7 b+ t6 s
+ ^9 I8 J$ G0 i% U a9 K
. m9 t: K( H% Z- \0 e$ O& e* S* } 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
% O% S; _2 W9 z- C0 d% l4 s. c, y5 z: I 9 Q7 z3 I" `7 }8 [8 W( E) o' r! y. Z
2 y( c$ Z5 M, ~4 Z" @9 ^2 t $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
/ _, T( r- D) Z) b' f) `" H% z 0 A# V9 z( @, k- X' L6 o
1 t9 n- ]: M: B! h
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
! I) b7 }2 a" k2 N & r, S8 v; s0 `5 S Q
4 b) j! y- j- \$ b) w" U4 ~0 d: W 4 k' l4 h! g1 \. V" z# k& W
9 h# _- K {: m* M$ Z( Y
+ B m" j. X2 E$ |- T* A8 s4 { 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:4 I L L( K" i {! N+ i
2 D! B# y+ n# M& {* f" W% ?3 [2 W, e- P, T1 h; K/ ^
<?phpclass upload {
! j/ Z, j1 B/ a; k, t
/ h; o( d. T v9 h0 f0 e. V7 K
6 v5 s$ L6 L' I0 {# }/ y% p. A9 i function save() {
" F: i' U3 t5 o
6 m, t( | w8 @& v- B4 ? a! d6 F
7 x% D F# E3 ^" V# |: S include load('include.lang');- b& E) M' O M. }
0 ~. g- s+ q% ?9 O+ L
* g! Y2 Q* B( T if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');1 a& j% b3 g! @: S5 G
( B) a V e: \" G, f6 |* D% Z) o( P3 j2 I, c
7 ^) {+ C0 B' k" M8 w
S5 E- g* \; y. ~& t3 n
& N6 H' |* @, L. t/ a# F) N if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
$ g( _, k% w% Z( m2 F& M$ W# i5 o! T
( ^! g6 |- p6 j/ f8 {: ?! x# |( d; H2 ?% G
" J0 T9 V9 }' e- L( M
7 K1 X- c0 I2 M! `* Z. B2 @
& N; E! D8 Z1 ?5 M3 \: \ if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);% ?9 L4 T p9 h4 J' z
$ n5 G3 D. j5 v: B0 n2 L2 o
* V9 H) o) k* ~/ E8 E) Z
" w$ T1 N. ^; V+ ~ V2 K1 W$ q+ I7 ]+ ~
" ~' |/ \5 E v0 b" f6 {: u3 q Z$ e% s# v9 c
$this->set_savepath($this->savepath);
4 A0 O% n( A4 D , [& l& _0 D i+ w" L% Z
0 }/ J% C, e9 ?: ? $this->set_savename($this->savename);
! c6 t7 D9 t* n% B' A+ |# a
: ]* W3 ?1 x( A! V. ~% y+ k# V& X' s4 O6 q+ E' P
/ [1 i- b' m7 T7 W9 D , Y2 @) a4 {2 s# L8 \7 C/ G
6 I* f- c2 W% U if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);0 T. c$ ^, p5 _3 i# d- _
; y8 |1 i$ }) N" G* e g: X& r& c
, v$ [) a4 W& R0 [; V( k) A if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
) m% l5 x3 X5 A5 I- t- B! @ * a9 Z- ]' j g" r# ~* @) x
% J- R1 N5 P4 y& F$ r1 L% k: | if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);. S- a' ]/ E# I( k- G* y5 a" W
( ]1 N' V- Y6 q( k
( y( K& U: L) p- F( ]
* j5 F( u( C% V
6 ], k& T- \2 w$ o# h3 E, D/ l4 `: K7 b. }' Q. S3 s
$this->image = $this->is_image();
/ _% f/ k( L; K8 k, h* M! X7 d2 i! k
) E8 F, p/ ?- U* ?
/ r. c/ {- ~7 ` j/ F' V5 o if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
6 r: ?7 Z! m1 ]3 } ' d) U, ~( m4 D' K& c4 H
' N8 E5 |7 E: S" G8 |
return true;* x& ~: k3 }. ?3 F
7 w! c- }& Z% k, X+ `6 v
) C: a3 A5 F6 s T6 `$ y, H }}2 A- ]( a* R$ K/ W0 W9 k
+ i7 n3 S; j" j' E+ d6 ^' D& j
: F8 d7 J& D( x# B3 D# o
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
* D/ S1 ?' p' \5 L: t, s1 c + P$ T" C5 ?5 ]6 k% Y
: i: Q- ~& p+ E$ x
<?php
' G2 f$ c' J% m' Q- w2 { $ K( Z4 m% Q$ _/ e9 @8 M# d
3 t$ |0 ]) U$ G
function is_allow() {
) C! K4 m& S5 e0 c% O: S2 L
. k* B/ E& r4 k# G8 D- V( C& }" A# d" ^
if(!$this->fileformat) return false;2 M. |/ E7 k# ^
4 k/ c, Y# D3 b6 {8 M; o+ h- h- U# [6 W" q( R
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;0 D; V6 Z# ~; Q# j# |; {3 w) d
8 G3 B6 O& }4 P! ]# r3 K: c0 w6 h
/ X! e8 b" ^2 q( Q/ y( ^
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;
; Y5 {* e& t2 V! q " u3 a& |1 a3 E' Y% Y, ~3 t7 x( Q
# q, X1 d" q- s9 Z/ X3 B+ J
return true;+ e- ^4 A/ n- W5 v& c1 c, z
5 L# u, n, T' C# _
) O3 p; L! f* X5 @' b5 K; I
}3 C# ^# A! f! f# }( O) D
" w; G8 d3 E* K: o
5 _) k! z# D; C! G 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。. y* u. ^$ j+ l* P% Z
6 A( G- O8 Y/ W$ ]" e- B; m3 K
" e Z4 a: v2 Y" B# f 接着会进行真正的保存。通过$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. P( I; B7 n6 Z( ~4 e
5 k* }1 V, Z( g" T6 i$ M6 u& k( ~# ]3 Y! B, p) {
漏洞利用
& q' `" H" ]! w# E
! f: q3 L) l5 [0 P0 b; d& `( D! M) V% j8 ]* R. w
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。. U1 b% R2 p( c' F# ~
% }% {* g( V% u- {; I
- @9 e4 }' F) B ! L. @* q. [$ P8 N. v
2 S# b% S: } {
, V7 W- v8 n. Q" m 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid4 l- E) D0 u U7 M
2 M2 c) k% t7 C$ s" \. e0 N. Z* K0 Y. E8 L( x- T
不过实际利用上会有一定的限制。 Z4 v/ X4 ?: T; ~) [
* L F+ F) \ s5 b7 P+ J, I5 v# k! y6 P
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
, R4 \# n- H; [) d1 i5 d , I' ^) [ S; ~. g' s: ?
$ b1 w# t9 W- H) y
) p9 [. ]: T2 e! _ 3 J2 {) ]! G8 u; B, ~
/ j( n0 L9 o: s& ]
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
# B* A: H1 D" Q" m* V2 S 5 o) T% @, _4 B# w% l# ^
+ R& i/ ^. a3 i5 c4 ?0 F& l. h 省略...$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]);省略...
$ x$ E, k( `0 Z9 U3 ~5 u( o% s
8 h4 u2 m( G7 }6 g3 ^6 `
1 U% `5 ?4 D0 ?+ C% }- ~ p 因此要利用成功就需要条件竞争了。
& u/ j$ G/ w d: I# d5 _2 Q- v
c& \/ d- D& r! B4 G) [: g1 A1 v6 L* C
补丁分析
5 ~5 A8 y* R9 x0 }# P B7 X9 l7 @; n, a
# N" S3 I! T1 W/ E! E i: _) V. Z
* p N8 `: f6 O9 V! \" ^. h& E- v0 {" ]
; X/ y" i" n7 L6 R
' I Y {0 O+ X6 a7 Y 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
" o& x1 S4 F2 V1 E1 o6 L 5 R' \) z; ?9 ]+ w6 H. C
9 A0 a4 ^) `/ H7 V. q A1 |1 x
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
4 }7 k$ `1 x6 r4 ]) p7 N- K 9 N; y8 g8 y/ U
9 N; X) _. d; h
5 Y6 W) I7 @ ~
; L7 M1 r- D, [4 A/ y+ X9 d
t( g5 C4 g* S5 f' J5 v( U 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
8 d- q) w9 B" i. x; t' [0 [1 u6 Z% F ( v; i- N7 @, ~7 R
! P7 o6 C+ m& n4 L2 i% \3 ^
在is_allow()中增加对$this->savename的二次检查。
; \4 B' r% v" r/ R9 U6 g2 W# v; u. G
- Z5 h$ W! ?2 L9 e7 K5 E' E0 c& f- D4 p& P1 F( M
最后
/ i0 {3 d7 R- Y/ U7 n( P, u) L( K- q- h
' G9 }4 `$ F( s# W* }
嘛,祝各位大师傅中秋快乐!* X) i+ [( }! L2 [% S5 r# d$ ?
) o% }; V2 g# J1 O9 Y* \# W
; ^2 n" Z/ ~+ w: a' |$ E
8 z% x" M1 q% |) S- [
/ U7 D ?6 x' I% U
|