, w+ L9 ]5 r' T! X1 G
* i4 n& Y( y2 ~- E8 g7 j& S3 O
' F2 o. a. W4 q, l) H
) l [' y, M! X: l& I S 前言1 x! U! U0 [( y( b
+ M6 x! n$ G9 I. k" J( L5 N7 q+ a) _
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
5 j$ H8 \8 v: D4 B# e% `; V 0 d8 T9 w+ z+ S, Q
9 _7 @( S! [& O9 T; }! O. A
7 B2 s/ }& n8 a/ v) a
" @3 L% b% ?9 T8 s7 w) y( U( d% d
漏洞分析
6 C' y7 l: ^- r6 i- g& E8 S, `' f3 V# c/ v/ I
1 j; Q# r w* R8 q' G0 g 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:7 ?3 x+ h" u. f) b5 {0 V' p
# y4 o/ ?9 X- c6 x
) d" l+ f1 ?, d1 w3 O5 G, g3 }
- B$ X0 m' ?) o) g
. w$ h& _+ Y7 [# V7 S0 k0 f& t! i5 k' U$ P+ M9 [# N
对应着avatar.inc.php代码如下:
. _; k7 O! \( \ a/ W+ z- u ( s' |0 j; n7 o1 }7 f5 l5 e4 w
2 y, t9 ` j& i6 u; b" C& y5 q <?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) {# |$ Q7 K3 D9 s
6 _# `- C! _. C6 n8 H3 {
3 U% _3 A# i2 K+ L. C$ j9 R; z
case 'upload':
/ W$ o6 `; d' N( h. u A% U! V |" {, S. f* s' T
% t/ S* U/ A* p+ ]$ w if(!$_FILES['file']['size']) {; ]" [6 G2 r" W) X5 n6 U6 i
$ L- a \2 Z6 Z
. J- d1 G. `, k1 b/ r; |- H7 s
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
, [- w3 p4 [8 F9 L5 O
' V! H9 w: U$ \( g; j
7 [+ r1 d8 I" f+ t9 A+ p exit('{"error":1,"message":"Error FILE"}');
; o/ D0 L# Y4 ~0 ~5 s
- v/ q1 A+ U" |/ s
% `* H q. ?4 N% n. { }+ x) ]1 |8 I0 u4 r! V& Q
! {; {: b4 }$ m8 \( _
" p7 e- u, J3 o' z( y Y3 M require DT_ROOT.'/include/upload.class.php';
. J9 C+ }0 g8 j( J' b
: K# n; e& t+ ~
2 |2 ?' Q$ @+ q. r1 h; C) q: H4 l6 d 1 b) Y. ?+ P4 Y% Q' t1 `) ~
9 h1 d# g" x8 `, t* h9 R5 b1 n1 g' L& p" E, m; Z8 R$ N
$ext = file_ext($_FILES['file']['name']);
* e( m/ v, v8 \' R- b- _( q
) b9 {: s# M" ~5 F" t/ D* m
g7 S$ z! p" m& G! k/ ~! U* l3 v, Y $name = 'avatar'.$_userid.'.'.$ext;
. |0 Q2 `& a# _
1 p( B2 r0 U1 Q; y& [5 m' w* B: k# F1 u
$file = DT_ROOT.'/file/temp/'.$name;! d& }' d& i2 J) u3 i9 X5 A
! q" [6 W, T+ }- g; E$ i% B
' S+ w5 j8 \4 Y( r& \ 0 c9 o& A/ V* ?& u
1 g# n- ^) e2 P" _
5 ~) S' h# I7 g2 e& l5 j if(is_file($file)) file_del($file);- _% g" z+ H, l% @: r6 |; K
, ]2 t- C) {; ~3 g2 z7 b0 f' z; p- S+ M+ i# J1 h! U5 o
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
9 }/ d$ v* I/ y! Q. \
# @& j2 Y$ r Z
- i( ~) {' I" ~ ' J& F6 l1 S( W3 l' w* W- h* W% {
5 Y8 M, M2 I/ l7 w4 W
+ O# M6 z4 O* x: L& b5 e$ n
$upload->adduserid = false;; _% U' \9 t0 `9 v: N
9 B# J# i" E# h0 G" }$ v; u5 }$ }' G6 Q& P- ]( I# G6 _7 W
) }) l% Y9 K) J$ H& @! V
3 u( v: a2 d/ w+ a) C- ]6 l
1 A* E3 m+ G v4 Q& h+ ^ if($upload->save()) {$ c8 T3 a. R5 h: O+ X+ J/ x
- J; ~2 k5 M# i4 j2 [
. `8 P' C( Z9 V& j ...) _0 t8 ^' w; a% p7 Z! g& Z5 P
+ p% W6 j& R4 R* p) n+ }: f, K1 k: j9 Q! Y
} else {
( q; ~# a! x# c0 S + X# p/ _% W+ _) ]
7 H$ x$ O! e" V! w* A# |- u ...
P, d, z: n |% v6 g" H0 k8 S
0 B9 m! q4 K& p7 H# a' t6 R
7 t2 N8 G. D+ ?" v. K! } }
, {; e2 Y, g( s0 F' Y9 o% | ' X8 s# V3 f8 ~, J! m% |4 q
- W: I3 l& x6 Y; i& E7 U: T$ @! I; x
break;
6 T! b7 V9 V3 b- H 2 z, a+ i7 A4 ^" x# M1 d h
8 F8 v7 a- x8 V 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。* M: F0 X4 l4 p8 R# y+ V
) |) [( o I% @2 y% @
& W* ^# C3 Q1 N1 \& j
upload对象构造函数如下,include/upload.class.php:25:3 t3 t3 [; `( Y7 g$ N# z
! f- M, E/ _. d* N% y
, M9 D: J5 m. D+ G' k" X2 U <?phpclass upload {( m. W8 h/ I+ H- x: w
! t& D& O& h$ E$ v* D
" V4 v" W8 X" O function __construct($_file, $savepath, $savename = '', $fileformat = '') {
* A, }2 k- a- ?1 ~, C; c
0 u/ z( \/ O! c" n; Y: t
9 v1 Q3 g! A: ]6 n o/ ~+ P7 F; T global $DT, $_userid;4 ^( I4 {. N0 s$ L( w
8 j# o, f* P# ^4 h( f+ l# N* G8 M v
5 W2 X* v, _8 F( b foreach($_file as $file) {
, e% f! O3 \: y2 m* J 0 \1 a0 l* @) |; [ a, b! _/ k) i
0 m3 \2 x; ^. c' G' d
$this->file = $file['tmp_name'];! z+ Q" I% V0 Y" g$ `
: ^) K9 x0 z- g
, x$ F% K5 ]' j! B $this->file_name = $file['name'];
9 @; H w1 T) |* a; H $ a* X# \% J4 I& ^4 O) Z a2 b& N/ ]& K
- P7 R1 {) Q" e. {) V1 J
$this->file_size = $file['size'];0 N! ~" C0 Q* A4 ]5 L# \, j% ~+ {
9 A3 ?7 O) G9 G$ w1 w# f( l
% Q( M6 @2 O- B+ r' K
$this->file_type = $file['type'];8 u" A) _" G6 i8 o" p
0 d# v' k. _ Z3 }$ H
. U8 n, S Z2 ?; z9 Q5 I $this->file_error = $file['error'];4 k. @% {9 S* G1 r# Q J
4 r7 z5 X6 d, i. o9 z5 c0 M) x: x, v5 Q2 K( `% |
3 i R& b9 U# [! o9 k+ |; Q
# w! b& d. t% R) G* Y
7 Z1 z1 N; X* M+ _ }
& r5 W( G( s' H. G3 n
" f1 p7 w8 b& s
2 ]$ \5 T& o) d1 g $this->userid = $_userid;+ v2 O" j+ d: p9 `5 ?' w
) Y! {0 _1 g3 Y. R- p! R
) M e$ J9 i8 c) P6 U $this->ext = file_ext($this->file_name);. H+ d" Y6 E* W3 C/ U
8 _" @& R0 S6 u- u6 u
s* _! w, O* t, o$ ]# i
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
8 J! b4 z" b' ?6 p# Y- b/ a* W
% t9 V* p$ m9 ?0 T: H0 d3 A4 |- ?+ r
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
K! p: ^9 |" Q$ I+ d+ W: }$ N& S " m9 |' t5 f: I3 z
/ l3 E' ^# c7 E
$this->savepath = $savepath;
2 T3 l4 |4 y& X! G, b @/ D' t * F x5 f3 a6 C5 ~
7 M# `& Q* A; U4 l6 |" n# w7 n# m
$this->savename = $savename;4 V6 {3 J" s8 }$ I, }
# {5 t" ]5 `( n( ]0 y% Z! J
# h: c$ C4 K7 e" R
}}
3 Q# B2 B; L7 g- r' r: n # M# Z% P* o! G! ^6 S
7 K. d: y* C: F
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
$ b; S: q6 ~# v + K' }: n; P% g4 U1 K* O2 v
# v. d/ Q4 W) e* I& `" U) i8 P
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
* X* X% ^ q0 [' m( { / t- ~" i! s% N+ r+ W2 e
7 t8 R' f! }0 R R $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# o4 @3 I8 j. U' K3 x- e: z
- a; w0 \* r/ J
+ j" [ s& M8 e" B- `# B& { G! B
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:! k: e; P3 ~. c" n1 X1 b
1 |$ a7 p; J' P4 C
5 S0 ^/ P! D) D, n: O
' C% G- R0 q7 _, ^9 b
$ ?" }# [) }- [; {9 g3 H3 b$ g( g, P- O! ~% w' U2 k
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
5 V# b0 L; R5 i; W/ y* d
) V0 c! B+ v* L, t3 m( F
1 f$ \! C7 {/ J. U <?phpclass upload {2 Q9 A; @3 \, p' L" b/ b$ ?
% l" N$ j7 Z% t+ e
8 B( ^$ A8 Z0 t0 F; `& j' D; X9 [. m function save() {( C) B3 L- `( u% f; b
) A! s& g7 i. d o' }- f$ i
; y: U7 o% ^" k! a1 D, [% s# f include load('include.lang');
j4 F- b- f, { ; W# j1 M+ F% |8 x+ D6 G7 f
( \: m1 j( l' { _ if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
0 `+ |0 a* P- d8 O* j Y
5 q" q' p- D; J+ i0 ~) H$ p( l: j7 E8 g7 _
( w& H: s @& I
?$ M# H* C+ E: j
9 F/ P" A/ P. m if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
1 L& E; G% j" y2 b
- B" X: o4 G7 z% |( @/ m4 J4 j0 k. G. y$ e, x9 {
0 H* v, m; v) Q8 b
1 [" F9 c' M' p' z' z" c7 Z$ d* ]- }: K; G5 x
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
+ O p- t* W0 |& M4 K5 Q- {- j: G
. r1 y+ B& ~; v; f3 D
" Z, v7 m- e( E& t % D; j. F" d* D7 T* u% W0 C8 a
8 P5 m8 s4 q) b' Z; z9 t, J( x6 T9 Q) ]; W" {3 _
$this->set_savepath($this->savepath);
" u, F6 r: y* g7 T, { & H) w. ]! ^% }- Z
0 c1 j1 R! i% C' f- l
$this->set_savename($this->savename); m7 w# a( d2 y2 }, ]) x* M
: Z' d& b, X2 V. K$ o2 j' D* q3 @- q
4 I: A' D2 F% u Q+ x* J& X; N9 y
$ ~; x5 e1 Y2 g) h/ L, X8 C$ {
% c9 A6 y- W7 S$ [: F if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);4 K4 ]& T$ L+ I% i" A
$ d, ~4 \1 n5 [+ v4 m; |6 D! t
1 I8 o$ U0 O9 \8 _+ @- A! h- g: c
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
2 T- L$ Y& `( H! x2 e6 U2 L ]
+ a2 O0 D z! [ G7 m. L4 M3 y! t- U% d. d
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
. k" _) F# x6 H
0 f# s/ z5 i5 J+ A
3 `; n4 j6 l9 R8 x' I( h
/ m+ C0 d% F( |$ }7 G# u; r( V $ I6 f8 |% l) {. `* n( f8 V2 ^ X
& }, {( s7 _9 |, l" B $this->image = $this->is_image();# _9 s! `; t8 E
: E& A6 z8 c2 d9 @; O Y
' C" z0 b$ n* B: S
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);( c. o e! F; }& i
" _, I9 I5 ]# @
) J% c; Z% \8 E" J' [; A- ]% B0 t
return true;
; U6 {! V4 A* `+ m: d# k
! q9 {: T5 o: B# h
6 Q! A6 @1 e1 `* J, c W" y5 P }}
; m' I! ]8 Q! C4 E
- X" H; S7 I+ d c
' d- `+ A, } |! q- v7 `7 {/ w7 ? 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72: p1 ^3 q1 Y! R3 m* R: t1 y$ t
4 K# J8 _0 l# D5 ?2 N( \' q. u6 V% b5 ?4 _
<?php* ~( q4 \* L; d3 l
+ M8 A5 c' M, D
, v$ l5 M+ H: K8 o4 F& c" Y- B function is_allow() {) J) I* \" w+ w% {
9 H5 f$ G) b8 @5 G: p4 S
7 J8 ]+ {- Z5 ]& T
if(!$this->fileformat) return false;9 c2 X. [( R* g/ T
" f/ Y9 t9 U8 s( n( r3 O
8 m( S B) e3 L if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;. {. X) \& {6 a B
' {/ l3 z, l4 ?+ I, {: y5 [" D% F. n% `$ P/ 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;
$ q' n: `; _2 G% a
' N3 M6 }/ Z; W" A( f) S3 A- _5 w. @5 }6 {# R+ c
return true;+ ~/ {8 @9 W, r. t/ H- Y
5 s7 s$ w- ?7 j9 }" M3 t* s% Y* Y4 f' Q1 m, _/ J% M5 u# V3 j
}
* |* I2 m. |. x$ r+ ^' r- J 8 m. \! ?3 p1 s( w5 E4 ?. t9 i
! |7 W0 z+ K$ c1 I; C
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。7 g! ]7 j6 X9 {/ {; R( D$ f
% p* `3 G- h5 O- k9 S
6 \' z+ G" o/ x
接着会进行真正的保存。通过$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文件。
! l' ^8 j* A" Y0 Z8 m
6 B, e; b. j/ B0 D5 O+ {1 h/ u2 L* u3 k: K# Z9 S
漏洞利用
8 M( S4 e2 [3 [2 ]) x
U: X- ^7 Q+ I2 i, v
! X2 X F* M6 M0 ^ q- i. a 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。1 v, [# U1 f3 [5 c/ k; X! A" b
' Y0 v; X# \5 c J( D& H2 m
6 i. h- h% Q7 Z; F 9 n3 f( x6 X _% U
4 S# S* F! M$ U7 @3 N
" I$ g) Z: p% q 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
' N. A; p* `1 G& ^, G7 }5 T & Y% Y- R+ V$ U& l
) O( [5 c, J1 B, o$ S
不过实际利用上会有一定的限制。
2 U* h8 H& P7 a* c1 Z# _) B2 r
* F1 {: x' ]1 y7 \$ V, z! |' ?0 g. t2 V+ B8 R. g/ Q
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
9 k% y$ ~0 g8 c
+ Q- U) H; ~8 S( a& c6 z
- b: U k( E& n) C5 N " c; h% G+ g( e' J
. u1 K4 J% s- p3 Y5 Q
0 b9 F1 I: f8 `
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
- i) f, S" E9 _' b ! e9 \+ l& G) k, G5 b% d
: O2 m5 W% e, G% F) L/ { t 省略...$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 H, j. q8 M3 a8 s. n% d
7 ~- s+ c, h1 ~- [/ `% p* Q9 L! o% k D8 P* t
因此要利用成功就需要条件竞争了。$ } y# G( l% k* q6 x( q
, |2 K# L ^) S" k/ D# f
7 b$ Z9 F$ B1 W5 X1 B: a7 Q2 w 补丁分析" L% d7 b+ m- v
' v0 T$ M6 P' _
6 P6 d& j2 c2 Z3 v' a* v. D1 K
0 c* _/ l; d; j - _8 |! l* ~- Q( [* y, {" P
. t' l) z, k6 W# ` k 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
$ a' B5 q. Z. h# |& ~
$ W4 S7 ~ A9 a8 w- n( ~* A+ c' {1 b
: X, _8 K, }: A function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
1 D. j. A# Z3 ^% t8 C: T
4 S( A7 `- C0 [5 n3 ]
6 {) S/ S8 v0 Y+ @& S 0 |! F& z# n. Q( [
# u9 x4 n. M& }, R4 `
2 @6 {; w- }$ E9 l$ K% V 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。. D. S# t% o5 C! i. }
) \/ Q0 m2 V. B3 V* [* R. i
1 @& e0 h) K4 q a0 j 在is_allow()中增加对$this->savename的二次检查。
0 h9 G e7 z: q9 M1 P! j 4 Q5 A' @) t: e) I+ ]9 Z9 m# A
1 a! w" p7 L0 x6 B2 v 最后
& E+ o9 f; v3 `7 |5 }6 H' K/ ? L3 h; T) v5 i' d) B5 n
& m% z. O7 N5 f( h% d+ p3 z& M 嘛,祝各位大师傅中秋快乐!' a/ y7 v2 q, N2 D
; A" f9 Y: C8 p& _
& H" `2 f1 @7 P4 b3 E
1 {: v/ |( o# E 5 K7 n# Q% G( h: C
|