) I ], m. R5 w) e0 b( ^8 I
0 S3 S) s9 J7 t4 ~, j9 E! e6 `5 `! I
/ [2 Y/ z$ L* B3 p [' W: ?( s/ ~- L& @! |, M% L% u
前言) p0 {3 c( ]6 d1 ^/ W
. W2 Y8 o4 [, p3 ?) @+ }1 M! B) X" U
V" \0 _0 j9 r f5 Z/ K 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
1 f- {: g' R# z! Q 8 n9 [ F6 J6 j: C
3 c& z2 g# {' a2 T / J0 ~/ B1 c. l4 x1 ?% x
2 T$ e, I+ s8 \4 c1 N6 y A3 R# y* b U1 Q
漏洞分析( Q' v, I2 {* n% {. ^' S& a" l
% i E* {. ~3 f4 ~' D
# |/ P9 E1 e8 l# a3 C
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
$ B1 ~9 h m2 T4 o3 G
% b* e7 L+ d/ h. `/ \' }! \( {; N1 p6 u7 S' y0 o) x0 ]! A
9 W F+ W8 e9 S* i
' a/ @$ a: E2 u
6 j) G6 T2 |- s# u" R; |8 U8 _
对应着avatar.inc.php代码如下:
7 v1 k& W# p }8 r Y" s* v g' Y 5 a9 t* _! o8 n' J7 m2 Q
; r! m* @3 r! R( Z& F; O5 v2 s
<?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) {2 ~ ^8 @; o. O. d& a+ M9 P7 j
' Z" N/ C8 x0 W8 i0 ?- Z; k" U
+ r/ K( u. s3 o, A, w case 'upload':0 N7 z& ~& c5 L- H/ a* _1 J8 k5 B
4 v# P" o9 P6 T8 r/ J
0 z' m* D' m4 k
if(!$_FILES['file']['size']) {7 L0 @: `6 b! T% \' c2 L% N1 v
8 I/ T0 c9 I+ y$ d8 ?0 l3 `, R1 s
y3 u9 A4 F& }& f3 P* y4 r2 l if($DT_PC) dheader('?action=html&reload='.$DT_TIME);0 h# S' b/ @$ h, ^) O1 M
' G1 A# X5 p: n* p( O6 f( Q1 J! }
' w# v# L( |1 ^ x z/ O2 S
exit('{"error":1,"message":"Error FILE"}');/ d8 P( p% ~( w7 ^8 G0 l4 N
- O* A& |) S: l/ O8 \& w
7 G3 }7 G& N1 i+ ?% j }
2 r s' c8 r6 f3 Q3 S% C. p0 p. g
8 I" h& u3 n. Q, G# b2 j
- G2 P; x' d3 g L require DT_ROOT.'/include/upload.class.php';* ~* d( v" y/ U- e. u/ @
" D- [! \; W' t% w4 B' `0 d o% y
$ E: {7 H ~9 D3 P/ x7 t9 r ! k7 d% y, a; L4 J% p
4 o+ @$ _% M/ p9 v& _! e1 q7 ~& |, G$ L) ]: Q
$ext = file_ext($_FILES['file']['name']);
! Q. [- ?4 k' j2 w9 z 6 B: Q; g) c a
1 H; T( t# K* C+ G
$name = 'avatar'.$_userid.'.'.$ext;2 D7 m [) H; _/ \8 k
& d0 o9 A5 C( f4 V6 @# l- p, v$ P8 u- [6 ?! r8 l* g9 i+ N
$file = DT_ROOT.'/file/temp/'.$name;. V) c0 O# \0 I( q; Y1 `
3 R# o# @4 M/ c. b8 K0 T6 e
* S5 l x* ^4 k, i
7 ^. Z: N9 [2 v! H! z
0 p7 n/ ~$ y: @% |, |- v) a& F/ G
' p& ~/ ?) j3 P+ u r% f if(is_file($file)) file_del($file);
/ D; Y' h: _+ Z9 a" P% S
1 _8 ~$ s6 S* T
! E S! K+ A, u2 C: f6 B7 ]. z $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');/ Q7 {+ r6 v( T9 l- W$ a0 |0 v- T. V
$ _' h, B1 X& b4 x* @% _
# L# @; [, m5 a) s
( A- n7 f4 c! u
3 u3 o* V7 I4 w/ v- ?7 l# c; ^% q/ Z0 d' _, D
$upload->adduserid = false;. D @3 V" t1 `5 j$ X# U7 \. M
2 ~: w, C" D; I- E, f
: k2 E' v$ {+ A% t
/ ?* Y3 g7 I3 T$ N% p$ i 1 W9 R; D" A8 d- e; b% \5 g
" k9 g m" M2 w. q& |8 p if($upload->save()) {
, V9 H/ b% Y- a6 A' u ; U/ G; n4 G7 C4 \
' z+ p3 a) f6 j- o
...
& r& r% Y1 k' t% Q$ ?
. _3 h `0 Q& O' y/ l3 ~3 x2 l) I& t4 A* e
} else {- I4 m6 ~& ]$ d
5 `7 c) H. Q! |5 G" a6 \: R/ }; j" v2 F4 R4 \' O* C- t* z
...% f* D$ n0 O, E/ X8 m( D
4 n. n. }4 W, s! {5 V% P7 J
# s1 t$ l' O+ q5 P) L }
! C! ]0 w6 z3 L8 u
: u# p0 }/ P$ J0 ?% f" ?1 W3 w$ t( x4 x, A0 _
break;
' _3 T# y: \1 X2 B
6 v# g' A1 N5 Y e9 }8 A/ `( z& k
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。6 l9 J* l- n8 M p" \3 n8 Y
# F7 l- ~% G* e c8 o0 @8 ~2 {
+ G- X X3 h [7 O5 h+ ~+ L) D) g
upload对象构造函数如下,include/upload.class.php:25:
9 t" I Z! Q: A9 E
; O' D3 R4 W7 z; S& M6 |0 [$ ?$ Q7 @
% ~4 t& @( m% q! L( H <?phpclass upload {+ I; b! R% A4 [7 G- }1 T+ g& T
$ Q) W/ d/ f. O5 C) x9 \
1 Y, a# d! z$ h9 o+ O& [ function __construct($_file, $savepath, $savename = '', $fileformat = '') {
( A, V0 }' Q- H7 C/ ]* b" `
4 C6 c* J* C6 c' A- }
/ f! e: r; f) l4 ^/ l# E( z global $DT, $_userid;
( a1 H* Z7 f2 |( `# t
3 D8 ~+ B1 N8 V; t3 T
" }8 }5 C6 s6 F5 d9 N4 v% f% n' a9 V foreach($_file as $file) {
+ z3 i: ~5 @. \ / ~' d* J9 O8 x w9 R7 f: l
4 }& h2 G2 |$ F8 A0 n# X $this->file = $file['tmp_name'];
- T6 W/ O0 ?5 |; W: g3 `# q- E% x; ?0 G
7 g8 e' D$ S2 H) Z; Q5 e
+ \ E) s- A7 ?! A $this->file_name = $file['name'];6 S: S2 u9 w; f$ l! U
" C. b. c- {2 m8 {( {: I; B
; f6 e. D1 G7 T/ H3 d# o7 `5 n
$this->file_size = $file['size'];- Y$ q2 n3 M+ b+ G% S% `) b
4 X3 d& @9 B j& o8 i/ `
3 l1 E2 b& Z# W $this->file_type = $file['type'];
9 C: F2 g4 f) _3 N& S; x2 L - S; L# Q) U9 [% |
. X" ]+ v* n3 x! r4 E/ L- { $this->file_error = $file['error'];6 I& q: s( C7 u, l6 C
& Q( M7 x/ k$ x; H; e0 U( G1 s0 ^$ r3 P& }0 W; d
) y @0 C9 e7 u8 [2 c: O 6 t& }* h/ d/ x# M* g0 n: w9 M
( ~% T. O( a0 X5 t2 V
}1 E% L3 p* X, p. w) E
3 ?3 _* O- C" e% I, }( g& Y
; b; @6 ~2 \2 ~6 s7 R) L $this->userid = $_userid;1 v% t. A1 ^9 W% t3 R
( t; a4 z @6 a* T
! Y# G. q5 ~: V7 s& y
$this->ext = file_ext($this->file_name);
. @" ~8 J+ V8 @ ' U6 d3 O; A+ d( d
3 ?4 ~0 |6 A& R& c! v
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];/ D3 W1 ]) \$ p! J# ~! Y
/ t4 D! Z9 | q% I- B. L! c. G8 B3 _ N8 g( k/ `) u+ H, P; T- r8 v$ _
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
, P; H: q" ?9 p$ q( \2 w N, Z % x$ U1 K' }4 z) ]
/ q1 Z5 T1 }- R& N; A( c
$this->savepath = $savepath;1 B* z0 p. t6 H7 N
$ c( n. L1 D; A* x, Q
$ N- f, E) s. J& O# I $this->savename = $savename;
. W7 c) r: V( s2 t5 ^
) X& ^2 ]1 [- Z7 _& h3 ` @+ f
& i" N5 A% Q: u; [! _. L- m }}) T- O2 R* k- y
5 s. B$ U: r0 L# N1 v/ U1 A) G: g, m* s$ s) {/ \$ i# S2 @& m; u
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
8 P& `4 _# A4 C6 e. H" v 2 l/ u4 u: }1 M6 m
$ L" E+ ^7 h4 H. |
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 % Y. A) z' l4 S8 \0 i8 C0 j
9 x( u2 j3 C) Q) m; a6 R0 E7 o. q/ q
$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
6 r2 J8 E' F: b* e) W
) s7 @: U0 e0 e2 }2 N, ^: |! z
, A1 E% C0 c' O" k% U! x 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
# b$ v0 [3 H# K2 _0 M) v ) _6 L3 {' o+ a9 Q0 ^6 @5 J
0 P+ b7 V* l$ [* a' L# B
?2 R0 h7 k3 R' e
: h K$ I* q& y' d( h6 @. w5 d& y' H
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
$ j" ]" v: Y' s3 B# [
# b5 f$ T+ F( x( S
4 i& l& B$ F. z2 |6 T; { a <?phpclass upload {3 |6 T4 m2 g" Q' v1 R- ?! y
- y9 X, y6 A' S1 _( c7 g6 F
2 V; z" A9 M+ T
function save() {' M. P3 x5 Z! a8 m* l3 {3 {% o4 Y) ?. Z* r
# J7 D( I6 b. v4 B
0 \; R7 `$ D& Z7 c9 _+ ?/ ? include load('include.lang');/ Z G4 ^2 s+ f$ I9 x( D
) q, ~% x8 C: n: b' X8 i
( s$ b6 O. X; R" S' N3 y) J if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');+ Y3 m( a( O7 J- \2 t- ?8 z! L8 L
f% J! V3 l, I! g4 q3 V! M
$ Z2 M6 ^7 N" E3 J% ^! g+ l
v% j: J+ W% v , z+ f: ]' l# g7 }3 t( j+ k
& H" u, H0 x( `# N! Q" z6 w
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
/ d' V4 \" N) @7 G$ J/ U# I
' ~* J6 T. f x6 z9 ]
* m4 x% W" q5 m 6 a) R4 z! B6 F) R
. }# |/ \! y: _) V1 C& n4 V
# Q7 j1 z. @5 C if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);+ A' h9 X. }7 f. ]/ d
. D, @1 i( W3 Y8 s9 k, I5 y
9 K1 U) R) V3 P. q0 l' l# s) b% p
?4 o& ^2 k6 b. R" V( J4 ^ ! M/ L- h7 ]1 s X/ i. O. H/ F4 m
# S9 ]. P5 @! s! G $this->set_savepath($this->savepath);+ g* H p- O' A6 R
A6 l. A% Q4 Y' ]3 z) e/ I! I) n0 U* G! M4 n& H X& P, t
$this->set_savename($this->savename);
( U2 Z8 H# P0 w. I8 s / |+ J, Y* M( m& o2 `% {: z8 O
$ m S$ w+ P/ x6 v 5 I, `' o9 U }( G1 S9 ^" ]! D
4 h/ i4 s0 X$ k/ t
; w( T* ~! E& U( ]' l# Y* `# N4 } if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);# V* Z5 F: k, r5 S/ f
6 C% P5 H; c$ z) F
0 S4 N# Z; N* h6 P: [3 F if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
4 \5 V: K: |! Q8 V/ s
+ M# d }' j* f# T/ d& W/ L/ P8 ~
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
$ F3 R' Y! q2 L6 z6 Y+ q 2 o2 h6 O7 I# m! X& f4 X( |
% e/ b% a w; L) c/ D7 M' j6 V( d
+ ^* T8 H, W1 V* F/ o $ w) y$ e% K3 S3 f: l
5 t; O: x& {4 R2 X, {' D* x, e $this->image = $this->is_image();
8 M/ j+ w/ J& _& N( W1 z, h" @
3 |& _5 `$ Y9 j: e+ ~2 T2 R! U! O* \2 C4 A2 d. h
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);3 g' s) z1 D/ G' m6 e7 d
9 H; b" V- f( H- p; a s
3 ~% V, K* P# J: L: w7 D return true;; U0 t6 H: M1 ?2 R& }7 N. O7 p
4 m, y% p! }8 Y5 C9 T
0 z+ ?* B$ N- s& }6 P, D }}
: [/ \; R6 L6 i! }
3 L$ p+ O) j) @ |3 W4 T" [: W; |4 w% c" d+ ?# w
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
* j# R/ f( y) J) b D
9 p" [6 u. N2 W0 c
1 ?" H6 W4 I% q. L( r6 V* c <?php# b) L1 T. }/ S( a7 x- B
$ R. D; p# [( y1 O$ l; V$ N: P0 d; J: k
function is_allow() {
6 t. k# S* x1 k8 C, \# y - B( V9 v* Z2 H; S( ~: B: k% p! Q
0 R, p! `% E0 G4 A0 M
if(!$this->fileformat) return false;
$ m+ m# x+ K/ [& i5 o. S: N$ x
6 N9 z4 u2 B( C- N) I
+ F$ H/ t s* a/ v, C4 Q' \ if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;' Y8 D& S$ S& `* }% t/ `+ @
' C$ ?; ?' E# F' c
" A1 k. Q; B; {8 n( D
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;
/ z5 c k0 v+ W& U 4 O" z" ^! p, K3 `! m
* [6 V6 Q* _2 {
return true;6 {7 I" {. ^9 B# P. @* Q
9 {7 r; ^5 o9 X8 {% y
0 f! ~# M# m" z0 h7 ~
}
# j% w% O% _" C# s+ M+ |- r
2 w$ N/ ~# |( X2 q4 ]5 H4 ~! {7 _" F- J y: w& k
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。* G' s) d4 m4 d4 l V* A8 j% l' b
5 @. t9 o9 V5 C
2 C1 w3 C: t/ Z- w/ Z 接着会进行真正的保存。通过$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文件。" C. I3 L: F0 B
( {, G+ {) f$ E9 o. I( E/ T! p9 W
漏洞利用8 p# W$ X J7 o3 F
" n1 e6 ]8 j' `: X9 [2 O+ k* M' p
3 L) m6 O5 A F- n% e 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
+ p# U7 _2 @( b" ]& v5 D
A# X% V- A$ ~# \) U8 N: H) C7 \! _( a2 v- v" }- o+ e; c
, X, p, H( d/ C) I" r5 Z( Q4 L' E, B
6 @! z [# O$ p
. v/ D" n& V, e( p 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid: w/ _2 I' O" C' [/ c$ J7 p
) r4 [# @; z6 i
" g# m/ p' b. a! r 不过实际利用上会有一定的限制。6 n. R5 W. P4 [) y, p, ~3 a3 e0 z
, s# { N$ g! ?3 f: b/ v' K
- [9 H& r- g5 b; k% q$ T1 v. N 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。/ D3 L7 y+ q& C, z$ T" x
6 V" [8 J) Y- k% q% Y0 k
/ V! ~/ K7 G8 T1 P: h
1 U6 k0 O8 W- F1 y6 \" W! k 6 H' f# K2 [2 a) r3 \
$ a, y% M* V* C% L
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:/ x0 i$ e* o7 f5 p% }
$ O; H1 i) j' v* Z, J4 ?
, b$ q' {' P% Y+ 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]);省略...
1 R1 S9 _( @. r
/ Z; N4 C0 B& \/ E3 Q7 L. i+ U3 R) [! f
) o* { d6 j/ {: ?7 \ 因此要利用成功就需要条件竞争了。
3 g, g- F! v! h" q" }1 h # O/ j w( i! Q+ B" G, J6 V
4 d3 y+ {% C) C( {9 v$ E$ h 补丁分析
8 k; V. k' t) Y1 `9 Z- F9 t: f
2 D7 T: h- P( K, n7 b W" Y( Z
4 o1 R8 W; ^& b9 h* [4 h$ J* G
- b) w! k: Y# {' D6 j% w6 D 5 V- u: O% O" ?6 { Y
$ @, q/ J- Y& `8 A1 ` 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:- C7 M% E0 S2 D R9 w9 e
, r C+ j; e |- `$ |* o, `1 F# o2 E' P; I. A5 _5 t/ M
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
. r3 {% N/ H2 L% W3 H+ c1 k- ~
% U/ O* y# g ~3 _+ B$ j
! r5 r" c! ~( Z6 D& _ * R! x o8 Z7 r7 z8 g3 t. @* A
8 {3 g. A$ v D% ?
/ G- ~6 K. a# q& M! A e: o% ], r8 H 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
' O, w0 `5 F6 t2 X1 F
! z4 O* }0 \& p
# P, F! M1 l2 B% p 在is_allow()中增加对$this->savename的二次检查。
, n" @$ j) C/ J 9 X, H# I' `7 C( E
7 C9 `* U) l0 r. l) G 最后! }& P0 z% C1 w& v* N6 F. I
1 v8 V9 C3 x1 V% m7 a6 O Z' F, K. p6 s+ p
嘛,祝各位大师傅中秋快乐!) f4 u! D7 g7 o k; A) a5 n
. R9 N$ V) r3 ~; W6 R: q9 T) t; `( N
! d( Z) D/ \" o
- W' _. ]$ ^: p# K$ Z1 f4 P
|