7 O3 k2 Z1 o j5 Y/ D6 W
3 \1 q- a6 O* a) \! w
* t. b! [# q+ T$ d$ x0 U+ a6 k
) ]3 z/ a9 m3 n 前言 T. P* W7 S, }9 k+ q
/ a1 d9 c, V# V; D" `# p
6 a0 G6 q' y* B: P$ m% w2 D
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。 ] K. t4 J; P0 k% |% q, }/ k; _
! L/ _+ h2 t/ p3 L
$ K1 w. J9 b+ c3 r; X; ? 3 H* L( l q5 |! k. e1 |
+ k" h/ M) `" P5 i, R, G, q* N2 O- e0 @* |
漏洞分析
" `. s& ^2 k1 G# M' x: P8 c. x& d+ c! G# U2 @, T
: f- k7 q5 e5 n0 m 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:! R1 R; j- }+ B# v X
0 a' t$ ]: Y/ x! K" b$ [4 X6 O( E- ^9 s. g
" ^: z$ B$ m9 I, _ p
! \2 g1 S; \. k2 U& Z% [ R
. z6 [$ W, E) g+ \ 对应着avatar.inc.php代码如下:- k0 D8 I% k* R" }; O) V- d
3 Y6 d1 r( Y5 v8 B u4 M
3 e1 p1 r5 y U4 p <?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) {
6 r0 w: M# j! H K" o9 N: \: j2 W& X6 g9 p d+ {1 Q
3 X: y8 V2 w5 l6 E
case 'upload':
! |& |) K5 U' g$ m, P& u3 ~; p
/ b1 L' r8 ^: l7 R0 D
) G+ {5 O" o) w6 M. T, U# K+ a if(!$_FILES['file']['size']) {
9 E1 X; e) ~8 e% m* N 7 L: O- L T. l# n P% F
8 x L9 W* x+ ]. k$ I4 q) j
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);3 s, [6 D2 U9 u7 t+ g
2 j; N: L) ?# L- F2 A4 t2 [9 S# e {; n n. W4 m- J1 b6 e) [
exit('{"error":1,"message":"Error FILE"}');3 Q( c# t9 C2 z, T7 f$ @- h+ W
, G. M. n6 M/ x! K! V$ u7 t7 n
8 b4 } q. C, i4 k
}- L* e$ Z2 v# w$ r- Y7 H
& R1 {2 x3 |+ n* e6 Q( W) w; a
/ t# m- V& _. x, K6 D
require DT_ROOT.'/include/upload.class.php';$ z* `0 C! G# a
e1 o& G4 q4 \
7 w$ r! U3 h1 ~- f2 r9 K/ @
, e: c9 `9 p$ i- [1 c* J+ G 1 D! e# ?" j" ^
1 z$ b/ ]8 N8 Q- P* X $ext = file_ext($_FILES['file']['name']);6 C' j r( r# C6 j: M
8 S- N: j0 t, t" @) k+ u9 y
. ^2 s' Z4 s, [5 g/ A v1 Q2 [ $name = 'avatar'.$_userid.'.'.$ext;
; ]: C, V! I9 R. B/ m. k. X
w9 y, a6 {& m, z' n! U
" c1 ~/ Y& O/ A6 D G0 b4 q" d: I $file = DT_ROOT.'/file/temp/'.$name;0 @/ W2 t5 K _9 g& k6 t
3 Z- e1 K( H" [6 J1 k8 Y: }7 u. Y$ a8 G+ }, s
$ k. N- V$ c6 X6 l
1 S8 f4 d3 ^3 c z6 Y, \8 a S
g& Z6 w4 r% W! M) A9 J if(is_file($file)) file_del($file);
8 J0 b$ E, b% Q* C 7 |: m" q$ P4 K
p, x- ^! V7 I7 s- {
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');9 [! d" Q/ h" f/ O g- x/ c
5 n% v; k' N& _' Q& N' d; s
3 h9 i) t' v% d % o/ T4 w% e4 }
$ Q" t- q& H9 c) U* j# O$ C
) y- c1 Q9 w5 i: u
$upload->adduserid = false;9 I4 p$ ^3 G7 h7 a
5 P. U: |+ k- x7 T2 U' W# X
' g8 D4 Y4 Z6 D4 ~, ^
- ~' `1 x# ], ~6 x- R x
4 D# Z% W% u" S( m' X
3 X# m6 c1 B# N( ` if($upload->save()) {; T% i" U G/ [! F/ V4 w
1 r7 W0 ?- e1 s/ Z
7 U9 F5 y6 b2 t4 L+ C9 v* q5 v
...
! [0 W7 a. p5 k4 Z$ e * n* P& l8 P: M1 V& C2 { v
6 ~1 k( Q! W- W! v7 X9 f( m9 a
} else {( G# k2 {- D. B2 D1 a( C! {8 s" H
' X. z% Z( e2 p# n2 E. H
& P: q, h3 m: y8 H+ _6 `! { ...) w7 N, q) V8 i* v$ `8 e* U
& y9 y% i4 X" P& a) n
- ~4 V: R2 m+ z* ^0 t }
* x$ A( S' a8 h: [0 t. U 3 S( f% m5 o! A2 O* l. q
& |- X5 T$ z/ ~7 h! e! p' T6 P
break;
+ t: x) j, S* l9 V % I( I- l; c! ^8 V
* l" K% N9 h% Q( l& P
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。+ _# D( ~" I1 o$ i5 h5 E# I' ^
! x5 _" v: f" P/ W: ~. Z! y
9 k0 t& X, z3 [- i9 L! ?9 _: D upload对象构造函数如下,include/upload.class.php:25:
6 B/ S6 A2 ~0 K) q' [) b( Y ( z$ p0 X( _" S3 i+ h
) g0 [" v5 F7 {' z+ \$ U <?phpclass upload {
' b3 s* ^ G$ M8 Y7 Y# ]0 C4 C
/ p7 n, V4 j. |7 J; \6 c) X) `, X9 v( S, }( F
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
5 P7 P# g4 U1 ]! G # Z0 c$ Q7 v {7 o3 A* p- D) l% F. U
2 E$ C- T' r: r) @; | @4 z global $DT, $_userid;
0 }. H+ j0 U r+ ^ : A# Z8 W* P9 T! a H: I6 F, R% q
* k. o% R1 b6 O2 R foreach($_file as $file) {
3 r3 V) k4 J1 n" e$ [+ ]' C
& O) j' f+ O* }& R2 v, o* @- r/ i( H5 g& u
$this->file = $file['tmp_name'];
! l" N* _* |+ Y$ W- b
; b' A4 d: D/ { a- k W' K ?& P
$this->file_name = $file['name'];
9 X" F3 k$ X, z" [3 V 7 N$ v6 S3 j& f; r* O, |# _3 \+ f
2 E) J4 G' z$ d6 z# D
$this->file_size = $file['size'];# Z! I' O% f+ H4 `: Y! K \, j+ y9 U- `& t
a$ v( d/ |- E' d7 x; b/ n
1 m# ^6 s8 Z S% V; |, ^1 ?; n $this->file_type = $file['type'];
8 X; t3 s! ?, }( ]9 ^% O 0 T! M- g, _% L! h; ]
5 c# S9 `; `/ I' H4 t $this->file_error = $file['error'];6 { Y: v3 X. ^% d# B: |) P
: h3 I9 u1 a* l3 Q+ y: ?( O$ p, Q+ G
+ x5 G" G- ^! a1 g
, \+ } k! D" B0 ? # y1 V1 q/ t7 _6 l* j
9 H$ A+ k7 w6 R
}! o4 g1 F; r/ `0 v, z
2 S( a( q; O. C5 q3 }" T
5 R- a2 _+ u* |) ?" \
$this->userid = $_userid;5 U0 [! Q0 l5 T9 C0 B! t: c7 [
" t/ A$ _. x# k# i ]/ i
9 {# b" y: [/ I1 i6 W* I6 I $this->ext = file_ext($this->file_name);1 ~ W# J) z- U4 b2 Z& H$ n
9 A5 _( @5 G. y4 A5 x y/ l
( g1 Y" q8 p$ [. `. N2 ~3 Y- w $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];. E9 ]* K2 w* t& o- y
* c X; u6 w( V6 |/ y5 H: q1 O% v
5 B7 K# P# h2 W. q9 h $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
1 [/ [% L! E3 l9 P+ [: L @6 y ' G* G, n8 S- l
5 {4 l: V6 S+ }- I( e
$this->savepath = $savepath;( Z5 l& L; H5 [2 y' i, g
" e) s* i: H4 s* f1 b; x A% M" ~' ?0 j, ]
$this->savename = $savename;4 W. @. V! h J2 ?, J' q g
0 }4 u! G. v# F, ^
2 s* j: M% {2 X" |9 u' A }}2 w2 t) q& t% `" [( d- A
1 D0 }. ]5 s' H
. E/ W6 o4 I8 X4 j7 F* c9 R$ H 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
* e5 W1 A/ j. U1 A0 R! Q
. k6 E+ ]0 ~) a' d
2 f5 r- k: z9 o: p3 B 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 6 X# X3 W$ ^ q y" \$ m. i
6 h7 u& p4 I1 i7 T- o
+ J: K3 R7 f% M {
$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
$ }- X1 ^8 A4 P/ r 1 L# f% y% O$ f6 \2 K
9 |' l7 _) {8 Z2 z& }
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:7 t; {: Q! N/ \& Q7 }8 c
- [' R: y* f! @, G
9 p; ]; ?& N# z! ~
6 z) b' V5 m4 g R 5 q e1 n- U! J/ C4 N' X
9 s, W% b3 ]' r1 u0 Y5 J 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
7 ~0 l$ ?! t0 q) u* d$ e- _ 4 b/ i9 a$ h2 t+ }6 E: b, f
9 E6 E# z& t3 r: q <?phpclass upload {
% |" O7 R5 ] ^0 t a. R& N m, ?
: @8 N/ O2 E5 G3 j5 Y+ e6 j% }# R- C* R& I$ a5 Z# d0 S) a
function save() {
$ J7 ?) a+ E3 T9 y" U
8 O: r6 ]& Z/ X( {% I U# T1 ^) e4 ]) \0 {4 V$ L7 l! {
include load('include.lang');0 y, X3 s6 D2 @ k. m" x; p
6 e; h0 @- B& Q+ x4 D0 y' ~4 u" w$ u& h7 c K
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
- P, s6 ]% C- Q$ o6 j , R" n2 P C- N) Q. P) R2 d
J6 _ B! c1 @* k% m; y4 Z
) w9 x* C, ], u% F. V: I 1 K" _$ x$ S* N! Q* n
; v/ v2 W/ t* t3 `9 N; u
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');5 }4 e5 a4 T% D' N7 K6 F% B
0 @5 Q" ]* c4 n) @ b* S2 t! R9 p4 X
( q2 x& ^, Z4 L( s* Z" x
/ C1 q' D! s7 u5 v: |* @
# G0 K0 r1 L2 i0 J: A: N5 n, | if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);! g* S; K- `! h& e8 Z. @
% |0 Y. |. ~0 \+ O) U9 Y
0 P& e( h5 M8 W( Z. s) o
* s2 w7 J2 _. A: e9 A; t* D! s
5 w- u# n2 z1 L3 w4 A: A: T" F9 G' U' h- U1 a2 w' n( \2 Q" b/ O& X
$this->set_savepath($this->savepath);% Q$ e6 q4 `+ D4 p" N9 Z+ Z
) b1 B# C# s% I/ ~6 W
; s0 N/ O) \. d4 i( ? \) ]
$this->set_savename($this->savename);
5 i: b" l8 t4 e
2 B( I; R5 x! k0 ^( r: e. X! z' O1 H* F& m2 ]- }- E! p
# m" ]4 L& s7 ]* ]) g 1 a: I Q; |$ ]- {# ^
* y" ]) x1 v' n$ H if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
9 A* i! b& ~, _9 R3 t & c* v6 A5 F, M( k1 Z4 s
1 W1 L0 G: r) s7 `$ B. b
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);5 a c8 q7 C7 G1 C. ]4 H
4 U0 |9 h/ a! {% n3 k; ?2 n9 T! v" M) f7 L
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
$ Y; L' C% T+ u1 P9 U. J. U w
5 M( ?' c0 N7 A) o1 H: L. m+ F0 C7 A1 L
0 X/ P+ d5 V0 T# N
# g# j* ^0 X/ R$ g: u, s- h/ T' Z) k6 c/ B. d
$this->image = $this->is_image();
9 c( t6 _* M# Y& ?+ W' T* V: Z M7 c & E2 i0 {8 l1 x& u. p
( D H* _# |+ A) y. h
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
. O8 `0 ~! @3 w5 M+ t- F* s 7 d, ]1 B; r. \1 w* r' \% {
, Z0 b9 T! l" x, F% B
return true;: f( r6 W b; u g2 L# [- S
' n! ?2 M6 W8 W" Z/ M
# | p8 R, j5 K }}
! u& g" x" O0 Z1 F+ p
' a* z+ l. X J0 U5 {& j+ h8 Y; m* {! t- F6 ]
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
4 G" I6 y$ f, n4 _2 O% i1 b
$ l$ n+ G- }+ v2 l( g9 U
* v7 J% v3 {* E <?php: W, ?! Y$ }: b9 j! L% w
X; {, Z- x v: V% E5 G2 K @+ d( o, o r9 W$ l) L3 M% B, P
function is_allow() {
D. {- G$ `- S, m7 ~ 7 r: F; |) ]% S* B/ _
; \0 D" H i0 f- }4 c( k if(!$this->fileformat) return false;
* t; K/ t" i# y4 O; Z) W @% o& L6 \( S \, O" u% d+ t! ?
( D6 T5 ]; K/ ^2 K* v* g( S3 T4 d if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;6 }0 Z! p6 W; x
1 N! O" I5 w$ K S$ E3 K# I3 L
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;
' R8 L6 o* S$ e 4 [9 l w- A7 ?7 w) O
) a7 h2 G9 K. t3 C: j8 w/ O/ { return true; Y9 I6 Y N- |0 A. ^
- o4 C' Q% x6 d1 {- d
; K& ?" u; [& z/ [# A }/ ~5 H, o1 T) X0 D1 }& U
! R1 S& Q5 h5 c# P* R* d9 c. K8 S3 k
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
: f* y* l3 Z% `& N$ U: k6 q" I4 d + w; M" W, Y! p% U1 D5 R( m( d% Z
: A: Y( B4 ?9 S a% U& m ~! 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文件。' C4 i* k2 v8 ^/ P/ h
1 N& K2 D( f! u) v
9 j1 H$ L% o+ c0 Y# T
漏洞利用2 n% f: l4 {7 i0 i
' D/ Q" P8 _. ^0 n7 H: `0 Q
; d) Z/ X" f& M 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
. O/ D% U% ?3 S/ o! K4 B- @4 H - ^. ]6 j3 _1 l H- D2 M
& e+ {$ V9 s8 ~& q( J
6 r/ |" P% `8 p' A( h6 C 4 `& v7 J/ K, v& l4 _' |
% s! y: g8 @. x: b5 ^
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid9 S8 m- A( Y# v8 `& l& i
) k3 Q" E( n( Z9 u3 l: e
! n- G4 x- d2 m- G 不过实际利用上会有一定的限制。
- S c Q a& u' e1 c9 m9 f" ^
1 V% [5 V2 j% n9 _: S# _2 ^( B+ {7 e& \9 U
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
( U, Q% t' I; }' H" w R 0 h1 q; Y$ R) A/ {( J2 q
% X- z0 i: ]9 U& n
$ A O* v; t5 B) [ Y! { 1 e' [- K2 Q/ J5 E3 h" R
$ O$ W! T# m! |/ Z6 G# W 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:% w1 L' p8 C1 X; ~: A# w6 @6 u$ Q
1 k/ i3 D5 ]/ t: j3 X2 u+ F- e- G
! U0 l# ~- R- L5 D 省略...$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]);省略...% v# g* M) M+ A, V2 Q! k( X
3 n& n) D3 s3 |4 L, m0 F$ X* }
" N5 J; V$ Y6 H6 M 因此要利用成功就需要条件竞争了。
. C4 K8 H$ T; g/ ~6 H% E j- I8 c& o & B4 m, Z: V9 h, t# p
" d6 B% z n1 T
补丁分析
8 [1 V3 ~- Q: n5 q" M" B5 r; x/ @: n' b" S5 E- I& i$ {
1 Q" Z# Q6 k& @/ K+ k9 L! a1 l5 u
0 E6 K2 n$ i8 w3 P+ G! V
' A# ^: _0 o4 f# c" G$ o* g! u" u w3 |3 w N& x
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:$ r' @9 _; r# u, D
* o% J4 r8 v" [. b4 |/ C$ W. e8 |/ `5 f$ X& t |# w) K8 f* ~
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}0 z- S6 a+ \3 v7 c7 {# ?. @( N
( J$ `; B( \1 ?6 m! Y- I- F+ ^; U. W$ H
) O) a( S2 v2 h" z) h
* W' f3 o+ k2 {' u0 A/ l0 D
6 |& f4 u: W7 n/ h9 {$ \5 z
) q; g# w& |3 x- J. }" J& f& B9 O
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。7 ?, R/ X6 Y- y, ~# C1 R
! D1 F& }+ K' F( W U8 y. k" ?- m! V5 _& a0 \, W% }
在is_allow()中增加对$this->savename的二次检查。! Q& B( G3 _# q3 f1 Q. ]( b$ }
@; l3 |) D! y$ @( D9 r( `: j
% y& s% P/ x* \& \ 最后# M. W/ o0 e6 m: s( M+ s- V: d4 L
5 e' y3 i$ ?( J! h4 C% D4 g* ?1 v2 O0 Y
嘛,祝各位大师傅中秋快乐!5 g6 o* S% L5 e" M- W Y/ w0 ^) Y6 I
8 K; b+ O: q; O2 F
: D, P. m) a1 Q3 G" x. U
2 Y* k4 Q. \# ]# k$ M7 O& m+ r* h 0 j+ |3 @% b3 ^% Q
|