4 H, f. h/ O9 ?- d: ^
+ Q Q0 B3 Y. I/ P
% G9 j3 _* g0 D4 _: q
3 ]- h; o- y5 I* [* X$ ? 前言
+ D6 i7 p- `3 }$ e) U; {2 F- d4 V+ I- d3 ^
1 u- A! k0 U8 S; e2 O 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
* e0 t, q( H- i w3 H& V+ H " R9 q9 a q n1 ]
* J' y6 U5 o8 p2 s* ^/ q6 o! {0 L
% Q5 B+ _9 j1 D$ A
- b9 _/ n/ z5 {! m9 {2 S1 J" y7 @
$ R' e7 \' i2 j( `3 n. }6 w0 ]) A/ u 漏洞分析
8 j1 I8 b6 x8 y* o
5 E% s! r6 R' N) e/ Q& J) k" L$ t! j' v6 G$ s# C4 M5 A
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
$ L: S: W3 U) }" h# L
$ C9 p9 M x" g- K1 C% ]1 D- L w' Z
/ Q+ X7 X2 Y# Q
! ~2 T5 W. c, {, ]% c' R( P
1 t2 y: Y2 J, q( {/ t3 w 对应着avatar.inc.php代码如下:
( `, g4 H& g! q$ X+ | - E9 { }" b+ E3 l Q
+ u/ s! Y# _: c! q5 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) {5 i4 t$ J |- n4 O1 @) A/ p: s
1 {. v9 `) k/ e5 w2 i
3 P% R, Y" D6 U8 }' R case 'upload':" v, j3 v# [% _3 }2 N
1 R& u* Q$ |! J# ^1 t, ?
- U+ N1 H8 Y! O3 _1 ^
if(!$_FILES['file']['size']) {$ h {9 m6 e! W$ F7 K, ^! s
0 x, B9 @: X0 T) X9 e
3 W4 D* D# r J! n; B if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
3 j+ L K- F- J! O$ B 6 z" c( }& ^( ]) h$ \/ L N2 _
* j* [/ U; l/ G: ^ exit('{"error":1,"message":"Error FILE"}');* E: C/ }/ d' K+ u& w0 q, h
/ h3 w# T' p1 Z) G" n" Z
/ y( r* b' n! n8 L# ~% s; H5 P
}
# D( H7 D4 K2 ^$ M9 z) } ) [6 N$ `5 R$ V- ]) ?3 F
; X( a, o9 ]: N z( r( c
require DT_ROOT.'/include/upload.class.php'; a0 {8 H1 E; H- e' z- Q
% Q8 {& D* c# s' a
7 x, |( f2 Z+ O/ I 6 ], r1 N+ H# w4 z* Y$ B* _1 V
0 ^4 Z" r* h3 i, C) v
% n" s& ?& q( v $ext = file_ext($_FILES['file']['name']);5 o$ h; }9 h2 K- B. q Z
4 x( K" f7 d! ~+ C
! j+ B% m! Q6 M4 l4 _6 E; N7 U
$name = 'avatar'.$_userid.'.'.$ext;2 n+ A7 t. I& n/ R S4 n0 L* u
8 v, b, h6 G5 S6 F# \" V' a+ ]9 Q) Q) t, D0 D4 w; v9 ?
$file = DT_ROOT.'/file/temp/'.$name;
9 v2 |/ K6 e- e ' d- n3 l u8 d* U& d* a, v7 S
2 V8 D- @' d8 ?( L# _+ L
3 P6 l/ l" Q1 I# g0 L5 K W1 C- e2 ]5 ~/ H & E' @8 x4 V" i4 Z# L
$ a: ^. f+ ?1 D
if(is_file($file)) file_del($file);
( B, v- [: S% K) \2 A1 x + ~* x, V3 D4 E2 w% [( h5 V
9 F3 S! \- Z q: _7 I
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');: n4 ~* j a9 v0 f$ U' c7 D
) {4 d7 Z _. z$ F& }
; ^3 V: g2 t U( W* f7 W. b ; e, Z0 e' O& f" v
0 G4 _6 {( }1 h9 n* x
9 d5 o5 O! `% Y, M$ ?
$upload->adduserid = false;% A2 \* q p6 `- t2 d w
) S* ^$ ^: d8 s2 m# [5 l+ v! Y! m V
8 {+ F) a* y- e% m( n
; P. x) z" i9 @& Y, X6 g3 x3 C" p Y7 C: T: b" |1 A8 c
if($upload->save()) { x+ }' z2 _* l1 h! Y3 n5 x* q
" q: e& x: p, P5 z3 L
1 o6 Q6 O+ \7 z" r ...
. e3 x3 W8 d5 G1 G+ W$ M+ D . W: R& p6 x" E0 Z- x$ g1 B
7 @' X \ c8 R1 ?; Z
} else {
! }, z' o; m _0 r
: B) J; N3 K5 ~* r8 V: E4 V1 J% p5 C7 u6 X- S7 i7 u
...
/ x, X$ m& i8 S5 ]: j, o . o J- f/ ]1 v9 s& y. Y# N
H; `0 W3 m3 K3 x; t! A# K5 o
}
% c- }' r6 N; ?
' l, l/ ^* C1 V" H0 l" g+ A8 h2 y8 F& Z, N
break;9 c" p2 q7 X3 F* I* G0 ~! f5 N0 {
1 j) r) W \' {/ q! W& p2 G
" z9 x- I( J. T8 E" Q6 h9 z 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。# |3 y0 l! F8 T% F: W' k1 s: [
7 o: n5 z1 d. Z/ n
3 y; Z% n. L3 ?; w7 k( t
upload对象构造函数如下,include/upload.class.php:25:
. s+ U4 ^, O( v8 |
0 n- \" h8 p: L4 Q% r& V1 \8 D% `& D5 b8 _
<?phpclass upload { K9 k5 @$ d- ~- {
O+ Q- e, _& t4 r
0 N% e3 I9 s! b function __construct($_file, $savepath, $savename = '', $fileformat = '') {
; }+ n- {& e! f) B
* _, R7 W/ Z' C/ _3 l5 |4 ^2 y6 z6 h: h, Q7 z3 T1 e. i e! e, O
global $DT, $_userid;
r7 n8 f8 S/ ]1 V: s : L7 B8 q) v$ F
/ ~$ F( B. p" F* E7 n( @
foreach($_file as $file) {
* Y$ w. H! W0 `1 w0 t) ?, W9 }) h8 y+ m 6 [ j) U. D! X$ H7 Z; @# E5 d6 ^
4 U v2 j* \+ ?, u $this->file = $file['tmp_name'];( m. M# R. w6 D' N E! O
0 |7 Z1 y$ g4 E) M
* K3 A* A8 `" O* N- Z6 k $this->file_name = $file['name'];
8 E- {6 ~/ R9 ]- o% c0 s9 Z ! p0 v" Q$ i, `0 B3 g$ k! c
% {, F! X, M/ w+ C, i2 V! Z5 c3 x. l
$this->file_size = $file['size'];
: m) J0 ~$ N- o5 r; s" A6 u: i0 V 9 I9 S6 ^2 |9 h U% \$ A
' `7 i1 j' s* H
$this->file_type = $file['type'];0 V. O: k! s& g4 S5 L
* P/ z. `8 T* W. H' ~* m4 q; r
: v4 J( H: }8 z/ k" J
$this->file_error = $file['error'];, D' l u; M+ s) ?% n, d
5 g* B: A; m( e7 J4 @1 W+ d& B/ ]+ @2 {( S: ~
, Y" c8 N0 G% ?& j; s% s. R# { 5 v: k; c* n! Z4 c F
( z+ l$ r! J6 h2 o8 _9 X2 Z
}, {6 e9 S8 O! P$ Q! C% F8 `
* ~( A& @# T4 t; {
8 h o' w4 Y- Z/ T+ j# z: P
$this->userid = $_userid;
/ b3 m0 }7 v" a, ~+ L- L 6 @0 ]5 o, C9 w, t$ S& z( w
! R8 `5 O/ W7 `5 T
$this->ext = file_ext($this->file_name);8 _ m. c' f& D9 y- C9 Z- s" V
7 n7 P4 d- E9 M4 Q8 Q' H- L
# `! ~% J* n% R" Z+ t $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
$ y7 ^$ g3 f8 @* f8 t - }( h) j8 ~, K$ ]
( m3 m% z M9 @/ k0 o/ ]$ x $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
' U, }. ~( F# j: o
# M) G! N& m* `3 A) ]% U9 U
9 Q$ y+ R- J# R1 c5 F% O $this->savepath = $savepath;' J6 \8 }. S9 c( Q5 O6 J( N
. @9 p- k2 A k7 P
" _! z% n9 D2 n( b* [8 J$ q
$this->savename = $savename;1 i% z: u: S$ Y% I. z; }
4 W- }+ F- p1 n7 w/ z+ h: x- @; Y
. ]! \$ n. T( U) v+ d- T6 P! f/ I
}}
. j# X) q* I; s O5 Y/ ?) g/ L( D6 Y 9 V; k4 W# Q$ [$ y
, a8 d; C p5 F9 {# Q* H- g 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
. [" W2 z) x9 w4 _9 R( I
7 H6 c0 M* _+ g5 {' Z0 K) s* }
/ S/ o! b j% D4 |+ g i; \6 x* s 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
i. @* P q; i; q5 q" ~7 {4 ~
+ [- I- |6 Q0 n8 _, \/ Q; \3 _ T
' z( [- b& r* a7 N' H $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
- b3 [) |3 R; L& Q5 e/ M
4 R4 X# v2 X! N6 |, E* a0 ~3 x( _ _
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
+ j0 D$ M' a0 Z/ Q/ t
& c0 [% w2 ~5 f, ]* a4 H9 C" V8 X: Y$ y; Z" h& J4 e5 d3 W3 E
. X! I$ E3 ^$ T' W4 z6 d
! c& H4 d9 {, V" N$ b. Y* i% d
3 O% X6 {0 ~& U1 D# _1 P 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
1 ]7 f- a/ x3 K" i0 T9 H; Z
* M+ p* z4 _( R9 i! z: e
4 I! T2 `' v) L7 K: D* r <?phpclass upload {$ y6 U: V! T. N2 {0 Z- F
( F: x$ _1 c3 i$ S
8 A2 M) S# P5 F8 C Z function save() {
% S: n! Y" \* ~0 b( \/ c+ R 4 _" m1 C: W( K
& L& e, |# h) k include load('include.lang');- K! r0 V& y' t+ V$ V$ M9 ]8 C
& }+ {% [$ V4 N
7 _, B3 a/ g& X: U. t5 b/ M if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
: e$ S; ~( e8 H1 k c! g2 R+ ]$ N
, S0 W' V8 Q' {; C
! l3 m5 y4 s) p - J8 o' Z9 [% {- E+ @
4 {# k% S {' t- [$ ?+ q! b, q" ~, o( o
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
% t2 J: R/ q8 v) R8 u, ?4 W( ~* H
, _0 V$ z% P7 X8 Z7 L2 h' [& x/ \1 X! x, }% f: ]7 s8 g
/ u$ E# g/ ]# o' o4 e8 \ 6 v$ n, W0 s8 D% J, R6 X) i$ y
/ ~# J7 v2 ?- ~
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);6 {+ F; B" W/ s2 g2 U( v+ _
* n1 V0 ^) l+ |; H& q) C$ s
[3 v6 Q( d+ S |! T
# M/ H! ^' p' [
3 a. P: w6 Q4 t$ s2 x( U$ W' `6 N1 V- S+ m
$this->set_savepath($this->savepath);
, g3 l5 V" @/ Z" y5 \& y ?* ]2 l+ C& ~9 Q" U4 m
3 Z% r7 N4 m) ^2 \ $this->set_savename($this->savename);7 v+ Z4 x2 G- D) k$ o! A
( e ?, B9 Y' Y) W8 e: j
4 m6 s4 G( O; d; u4 _, Y ; _7 ~3 o: o f4 {* c
) @3 H. M+ e9 x- o) i
! Q& ^' |5 r; J4 U7 Y9 h
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);0 D- q5 q, G7 [
( @& G* _6 r% I, Y: I( m$ `
- c' J4 N. J2 a; O# i. L7 f if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);9 v! I1 s9 Z( [5 t9 @& @
) z+ F8 h! H5 q' O7 r0 p% U
) D( M4 {$ d5 g if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);! ^6 H" F3 c3 _
; c7 P! j6 V+ i7 H6 t; z. _ ^8 A
( L& ` X9 W5 Z' ]
$ A8 i: X1 _5 h: `# u' F9 K P + ]9 Y. |0 O$ e8 n5 |" @& R
9 T+ f+ i8 |' F3 a+ H2 Q
$this->image = $this->is_image();- |9 s4 y: n8 P
# E& N; V/ p4 ]6 H# L2 [6 l2 b7 ~
4 A, z! d: f/ u2 g- _5 r if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);# ?6 C& p) x" i& d; ` g* S
3 M8 P5 J' B# f+ M$ `8 \4 g
4 S8 @, e! o8 ?7 Z' g, D) F return true;1 U( ~( ^& B4 h6 v/ w* y, x+ _0 m
, Y' e2 R/ N: X$ {# D+ t8 U9 {- ^
+ `% R% V& h) Q, S- M
}}
# Q' S( y( x# q& h# ^9 r: Q; b $ n W: Z4 A* m- b: N
, y) \ t6 D% C1 T6 ^$ z' M 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:6 A5 r) ?" o0 Y" }' X% U9 r3 b0 y
" _& R9 P* S1 H& D) }3 M: }+ }& z: D \% {
<?php; c4 n k. g2 Q+ O/ V# x
$ w- a& g5 G* w- \
4 t X4 w' v, z$ H" \# c function is_allow() {
' {( _7 M9 m$ ?0 J. C ' J: n% ~% N" G" `. `) `1 B
1 b+ t+ ^8 U/ i: C
if(!$this->fileformat) return false;
! p& D# F2 ]1 ?# d; a4 |. J
; Z1 P5 j6 u: V: k/ O) K8 D& {
3 J5 ~8 E; h0 q" y* @2 e ]9 t" A if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
2 J2 J7 N0 ]% J' S3 m ^ ) D" q( c1 X1 a
3 c$ l7 f2 T. z# s# t: |" m 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;6 m) M- T8 r9 u @$ }6 l/ b
2 l7 q- |6 ]- c U# s0 w
z1 D Z; R8 W1 o' y% I return true;# g% c7 I* X% B8 I$ Y
9 Y5 d8 J2 g+ `6 m* O! J7 h& [9 r7 c. k: a+ P, v( B$ j
}9 @5 P( e+ O. b4 u4 u
9 L+ d3 X4 X' i/ d) d/ {2 S; J: \! `8 O8 g" d$ L% _* Z' J6 D
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
5 W2 V1 Y" g$ v* c : i/ P3 {! ^" P5 d u
) L4 v! ?! K5 }! z& o8 O
接着会进行真正的保存。通过$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文件。7 }- Q% f1 T0 }0 X. k. t- R$ d( Y
3 o* j5 w0 r- M4 Z+ O6 \8 e/ K
漏洞利用7 P' H% c; G& O; s9 E: M; b0 T
: h( E6 O6 L" `# H1 I5 a M5 L7 T2 }3 J! s5 U" A
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。+ h% x% P# y% V8 B
3 G; J8 P% V5 \+ l5 h* Z5 `4 @* t2 e6 R. k- v
& b" @& R5 }5 u4 W/ T, P! T+ S o
6 [# W$ [8 y" R o, D9 p
! A) A3 T `5 @) h$ G 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid: U( z9 {, x" k. C* o# z
# v f( A9 @+ b# x
" e! G# [% v) F. c# B 不过实际利用上会有一定的限制。- s9 ]3 c7 J, V, N2 c; P
' M# w2 r! | Z, t' `
$ b) W9 F- ?, P0 }
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。6 C3 _" p" a9 ]+ Y. I( o
# q, x: E4 l, g) w) C
" T! F7 A: \6 z( Q* s
( f+ }. `/ {% Y* {, r$ |. s , }4 R. ]1 \1 R q0 m4 w
" K& t6 |. C0 N6 s* K( x% i
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
( p" [1 Y" z3 k2 a% f% b Y
3 u# R2 ]4 @, S/ C+ X
6 F4 s4 U' r y( \ 省略...$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]);省略...( F0 `- s% H0 g; n) R
0 b( O- k; E% ?0 s# }- u# g$ d' Z0 `+ d+ x$ z/ k
因此要利用成功就需要条件竞争了。
9 K) i: X, S O8 l# Q# l 6 b: |. \* T4 [4 O4 t6 l
3 K$ O7 ]- \& P2 }$ M7 V, m
补丁分析
, t) Q* ~3 T( O: R2 ^+ K0 W
" U! n1 u- R1 v, C. G) T4 ~5 v+ `7 [' b& W7 B
* l9 m1 j5 C/ A$ \- J. m/ r 3 r" r5 J8 [6 ~0 @1 d" @, ^
( ]6 [8 H) M, O' w O 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
) E9 A# O* m( H9 c0 O + s3 t4 a3 O C- ~
( w$ N; z G1 g t2 K3 b2 X& A function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}* e- o7 }1 G# V$ n* ?8 }
4 F; s7 s- } z
, {/ g3 n% y: R& A 3 T; w3 m$ X) x: M8 A
8 X$ o6 f" J& ^4 g2 t' }- Z( n/ C$ A# @& |7 t6 N
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
) ]: P' ?7 J- N6 L2 b' Y3 _( W- \) f2 |
8 }2 ^: f0 t, X4 P4 K. G' h3 D4 u3 M# s6 V7 A2 H# |
在is_allow()中增加对$this->savename的二次检查。; C- K$ s' Y0 c" D1 @
L9 H* D5 E6 V1 l' ?2 r. B- P9 K+ q# ^2 o' i
最后* W! O; a0 l, X* v3 Y) x
0 c; J/ P. R: c$ A0 x
! f9 _/ X3 H* I# a 嘛,祝各位大师傅中秋快乐!
% S @0 A/ F( r- f9 k : w9 \3 e: d0 S% x7 R/ P) |
% U; `- P4 w* K; Q
) l J1 O9 G, J/ i% g9 k2 |# S
( x7 j3 b* m }, t3 v* Q. I$ S
|