* d0 I% Q& F/ ]( l( V/ l2 l2 j8 l
7 o1 p: C0 {: ]2 l) ?: `( M; m
' H) g5 h8 @6 q4 B A5 t' m" ~+ H* X! i {
前言
$ W% r; E& T2 R& R Z7 f7 V) p% y8 n- g E0 E3 @: o) ^
1 d7 I4 I" i% L/ U
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。- k: }2 U, w1 b; K* F
) L; D2 z% y7 u8 h1 G/ ^# b# Q! m+ R" \- k3 v; P9 ~5 `
& H: u# u. b: t9 W; ^# y
) D- L0 f2 r+ Z9 c' t/ \5 \
0 q' i4 X% ^ L) I
漏洞分析5 ~+ b) |, n- X/ c2 J/ I, j
: a' _( j: Z2 M9 ~
0 ~" J7 N1 u3 W- I* e; {/ o) {
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
* X! e. [0 D# M* v3 j
: t* t8 E- {3 |
x# T7 s% w2 c+ _ % F: c% C- Y* \3 |8 Q4 Y) E6 l
; J4 }0 {, K2 Y. v- N* q4 q% l7 q3 T
" L& s2 z$ ]! k0 W, d* c 对应着avatar.inc.php代码如下:" H9 y F) i' V2 D$ y+ Q9 r2 q
; r2 e: i/ w0 m8 e
' k+ X4 O: K* _. P& N* w4 W
<?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) {; F7 r- G1 L1 ?, |
0 C5 y& a, w7 }6 v- M+ |( Q# \2 I# k: [
0 d) e: t- U( i! V. T1 R( S3 C U: L
case 'upload':
" b3 ^" ~4 ~4 V+ |/ ]
& H& D i( S) D7 q% {
9 `* K) f$ r+ Z- [ if(!$_FILES['file']['size']) { f$ R& L* P# o/ ~
: ^8 v) X/ x" k0 z$ }5 H
% M1 v8 H% s5 D$ e! w+ V8 {* b, Z
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);$ U% O3 X$ f5 t* E
9 o6 q( \& [* D7 Y4 w8 ~
i2 z7 |* a4 ^+ a$ f2 D exit('{"error":1,"message":"Error FILE"}');
, z8 u, S; `5 m T+ G$ F I 2 q5 r8 ]) S, u* o! `
+ A- a/ y; o/ f7 r+ O" A
}! f/ C7 @' a% x" S0 T4 W
2 n3 g# c$ A( A( F2 l
2 M- B2 D% [6 E" w4 S/ F require DT_ROOT.'/include/upload.class.php';
1 V/ v* ^4 g. T+ c+ j! ]3 S) l 9 @ Z# D. c, s, L
& C! L$ J, t4 I$ Z + L; I4 W6 w% J) Z: n
+ L/ P( Q$ O% p4 e5 z8 R' U& l0 G
$ext = file_ext($_FILES['file']['name']);
( i4 ]$ X. g( ~. d! T * O1 O1 z1 E% h
8 l7 r* ^) O8 J/ r
$name = 'avatar'.$_userid.'.'.$ext;
+ s0 a9 L* J$ D4 f
7 D) a% v# y O1 d7 Q* Q* d- s$ S* |0 w8 C, B) E7 y; M
$file = DT_ROOT.'/file/temp/'.$name;
$ }: _# L, P6 _/ q
0 g/ h5 T. L- `' q+ K- U
2 i0 I( z1 l+ s
0 U# k" c( [6 ? 8 D$ Q3 f' V7 g
5 P3 d U+ m1 U0 M8 g1 R9 Q if(is_file($file)) file_del($file);8 w& D0 V9 r4 o) V
6 u+ M1 [: t3 a; i, G
/ w# c$ Y- `3 s& R: {
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
# R7 e( {/ d3 _% f) o
& I d5 ~& B( O: @# H( I: q
1 s7 A7 f$ ~, {' ^
/ \, i3 }0 I9 |& u7 d! q- S) j 1 ^' ^: b5 `# C, q! W! d) Z
: \: u5 U0 l2 ^ $upload->adduserid = false;
$ i& A9 y! Q2 j0 ?+ N0 @9 i! ?8 ^
7 x, Y) J% V, R6 }; j: Z
0 b5 n! W9 m0 k0 S: Y" [) h
! z) d9 k8 r. |6 U- u
" j- |6 p5 \, u! [% Q8 m' ~" h- _7 |0 r/ d+ ]5 g
if($upload->save()) {! l# h1 g' j! Z4 _' `
Z* T- q6 M ?/ V8 D1 a8 A' G1 t8 @7 \4 R2 y, q" _) T! ~ o& a; \
...
9 [" u1 _- U& X( w. ]9 ?6 q
0 x0 [# N# k, {# j/ S5 |8 H& T, W% b* S3 q/ i9 X
} else {
1 z! D2 c. u8 ` E7 A: Y- c8 e ; ?* ]( L4 L1 M8 s; ]
. R0 {) H/ n( e% Q4 s1 d7 C6 x7 g
...+ E q+ r% G7 S S- d) ~+ c" L
. I6 j7 K2 m6 b5 F T9 }( W2 l2 m
7 X+ H- j+ B# H% g6 L' E }, q& K' J" Q6 s, H3 j/ m. X6 ?
3 q( i; Z# _8 F o# u+ g" R
* H! _6 p7 _5 A% a3 A9 P break;
8 e& ?! ^/ {" |) o; J5 p8 r 8 r+ Z$ D6 _# d5 O+ C" l. ]+ a( [+ F
9 h) s h+ R% T) J6 X2 g6 C 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。0 I+ G& e3 D: A, a
( n: X: s: R% o9 V1 Z* B0 g
: q9 p4 M d7 ^+ f) ] upload对象构造函数如下,include/upload.class.php:25:
/ w4 p4 q$ t8 B* }$ M c% P4 u, Z
% x$ s7 w0 p- ^6 P2 k
' M `7 t4 T/ m5 h( u1 X <?phpclass upload {4 V1 h2 {: a5 r- m& F6 A7 F! I
4 v! C) o# I3 L/ c% a4 H- b
$ A( R2 @" N: U+ x% w/ C1 H
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
3 ^( R* O% n) w' [" L3 H / z8 h/ Z& e" r/ f b. r! _6 Z% X G* y
! k( u2 D7 Q% f. n
global $DT, $_userid;
( ]- A- M/ V; c1 F ( r v4 U5 [5 e- C7 q
& O) A) u( s, H6 n9 I: h foreach($_file as $file) {
9 M+ c" |. ~/ G2 S6 A6 I, c) a% v
* q1 g" h, s& D) Q. O; v7 k+ {
* o5 } X4 E& X& }5 `! [2 ^ $this->file = $file['tmp_name'];
$ G& h' B( M. R Z/ u7 U5 }
4 a" }! M3 G/ |" P$ J- I
2 m" b- \5 t8 @ p5 Y2 _: B $this->file_name = $file['name'];
$ D, J1 p& ]% |$ L4 O2 `4 Y- S" K5 @ : G7 V) W9 s! X
; f. k# A! r- l $this->file_size = $file['size'];
4 X! z+ i3 K; o/ w! q ; j- N- V* c; Q. i1 T: `
* G0 t8 k) i- B- j+ i) f $this->file_type = $file['type'];
" i; T5 o. l* j9 D f7 {: V ! I7 D; r8 m% O
9 B8 ~% i5 d; Z5 B: |' v $this->file_error = $file['error'];
+ I7 g+ q5 z# M. c
9 Z7 G; d. r' r6 g% Y U8 l, h6 f7 g" h4 q# D' V' G5 W7 `
6 m6 @" k2 ^& h5 q m9 ^4 ~
4 }% m1 z d! w' t' a$ U {, ?; K- \; d( X
}
' C; x4 l" U( a/ K8 B7 `/ j, k
% W/ n% _( L K( O$ ]/ G, F) q) V4 f7 y% \* n; `+ R& i
$this->userid = $_userid;
9 w7 s4 S! B# c; N
: T* P }, |$ H: ]& I$ {1 n {, P- Z. {! s( W" [2 J
$this->ext = file_ext($this->file_name); W* N3 a% i+ c( R
: u- E" Y% x& D+ E% v6 {* p# ]4 Y
& o- ]& p( n$ q: _. [: p: H( Q
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
- W0 n, q) j" T: A+ [ 8 ~5 _3 V8 y- h) _- R2 f9 k& m% M
% i Q5 |# O3 D2 A0 R, \ $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
* G. M9 z1 `3 w5 Y! Z6 m . z: U$ P: t' [' y" e, N
0 u9 b1 |) h; w5 Y; f. L $this->savepath = $savepath;
. R% G* W7 A. ~2 B; Y- x
2 S, i* G' {* q, @, K& {( ^ T% q. C& {
$this->savename = $savename;! f8 O& j) r/ M8 y$ y/ z3 w% f
: I( Y* e! w; V! O+ q
- y- i( k7 ~) c, E: p# o }}
& u3 Z8 y3 @# a
3 I7 T* P7 y9 S) }" ?# c0 ^! I/ H' c/ \8 u
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
# V V' \% F; g0 X/ f( @6 l9 E ( w" P! q0 X0 Z& h
; M6 a/ e+ v0 F2 e% t5 M
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
7 F. n, Q, F7 d+ Y
* W6 B- t4 c/ k* m1 z$ F5 s$ m3 ]2 k( X) q/ @$ G* o0 _( g
$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/ Z& z" A6 ?" p: J0 u d' K
$ W+ g6 m* c! z- p, @+ w9 H& a8 ?8 }9 U# u- u& Z
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:& F$ p7 g( u( x, t v# \0 Y
( `+ C. M; G2 v8 q
2 E7 w8 m& z- {( f: c7 y/ f6 N1 C( |
" _. T# T$ n X
2 s1 x. f" r7 \( O% R
# H5 b% ], u( s0 G% @# \ 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
. R( i: e9 F6 B4 v( {2 a: E
3 l/ k7 i9 I4 m
' }* z0 v7 F2 i* c* a3 a$ T: T <?phpclass upload {
! T0 O. O- K y' ^* n4 S
4 a" [0 U _: t* r! p6 Z
. K* j6 z7 Q3 r+ D% e) q function save() {$ b" R5 R) _ v& u8 d9 _
- G; y* S, C d5 w3 T1 \8 f
% e+ R; Q( Y+ E6 ^7 T- Q include load('include.lang');
- ^& i- t$ {2 ]4 ~# Q 9 Q: u1 n) B4 ]) B
5 H2 T5 O9 {+ P2 ]; l! P if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');6 y8 q$ l9 f/ V' w# Y' a7 K+ J
4 n% {- t" v( F& Y: o0 _# ] X8 e1 h4 z
( q: S8 [$ l* `/ u9 p1 I
7 G- V* }5 _1 N6 K1 K7 y6 L* Q
9 H! L S L# @; L6 ` if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');1 `8 {- |+ r' N1 {& H, y: o! U6 ?
: g; ?& N+ Q4 m3 C4 g! y5 s, T
" z" n7 m m- D0 ^ 8 e q+ p, d, j' u5 \
* m; a( Q( A P, [* y- ]) ^9 J% N4 q0 Q1 _# G! X, i# U( l) ^
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);; X. o# z& H R" Q5 D& @
1 H$ c4 J/ t7 \
7 I: Y2 `3 l9 e' Z; ~! O
/ ]) } b7 e( h0 V/ P ?& b7 {
- Q' Z6 v- L% u* P* b9 |+ B$ i+ i) g0 y- h z+ Y
$this->set_savepath($this->savepath);
2 ]7 X0 G* H: E' [$ p 1 v* u6 l% r( Y# f9 w+ N- }: u
8 V7 {7 F2 Y$ u8 c
$this->set_savename($this->savename);
" R3 E% M t2 ?2 s. s' S. H: R 2 x4 V5 N7 \8 `
- a+ H8 L" c2 h4 p2 o& C* q 1 x2 G1 X) ~, x' {8 }# {) ^' c
2 K& d2 g U$ ]" x' b( v
T/ @3 K3 v/ Z" _ if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
p$ }2 o( V5 K7 h+ d* m8 a
, b8 b1 U, \+ a2 J8 }* A: V4 |' u) B0 e2 d
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);* ]) u3 }1 E% l2 K7 N) T/ R
) l) D$ b6 w+ s: w
: }' q# Y# V8 ~. d: F: X) T- Q if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);) y0 U9 j* B6 p9 ~
1 X4 j7 t# c6 x# T: h7 A& ^6 _' V
! h3 c! r$ V. \! c& J9 w7 B% C/ A
, c& h! x0 }& l; A 1 {2 w, ~+ [# R- ]3 h9 Y
) m; ~# x5 \ X
$this->image = $this->is_image();
( L5 ~, u, `: x+ ~ L% T ' @# i4 ]3 a5 s# x% j% v
; f# ]' j/ z4 g+ Z# d8 N4 s- Z if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
5 |4 }7 P& F$ F4 a& e3 D- I
6 _# ]9 N# H6 L; G! y$ i& F* U) `5 {& i$ P
return true;# A, W- v+ k. I) v# @7 j2 @
( P4 a7 {3 l; N7 n1 B3 n8 N5 A' F6 Q: `2 t# U2 o8 B, w
}}
! G7 L9 W- p- T$ h 2 p7 s; R$ T* c# ~, f! q2 \' d
. p2 J% R3 X! B$ X/ p2 H7 \ 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:; `/ |; g& ^% J
0 z3 T+ d, j3 t- h1 }2 U$ T9 T4 H6 g, }- q
<?php
+ n3 m; ?0 S) g2 Q
1 q( i7 H. o" f; \( `, G* \
# q; |9 F: s3 Q* o% w function is_allow() {
6 d2 K4 G5 u( ^& | m' o1 K U q5 M3 t' ]! i; G: F2 g
) v4 I4 ?5 H' z) D5 ~; H' M
if(!$this->fileformat) return false;
, R; H( W6 ?+ u1 E( s; u+ a
- |& b S- Z$ A, ~$ D$ x
# K# u( j" r9 B3 ~ f9 ?2 Z# a if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;3 d* {' N6 m* `' J+ y' U! i& _. r
3 R) t5 J# B* T9 t: w0 t" u6 u( i0 V+ \4 ]
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, d) B) _+ ]% U
) t% K$ }' y& f5 C; b9 D
) @' }8 m. ?8 Z- D( v5 y
return true;# _7 ]# G4 X5 p1 e* ~
1 _' W h1 B% f$ m( e- M3 C3 ], O" K: x( Y, S
}
/ E/ |& ~5 |7 X( g: m/ @" X& A * T2 T3 H2 r% ?' L$ Y7 F
0 x+ F- L# d7 E- a8 n 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。# d% o, M& o% U/ x4 ~: r
1 d' [# _1 T0 e, w! P/ R7 u: N
- ~, q* R7 K6 L4 Y$ 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文件。3 H, V9 r% G* S. W) B
' b7 F! @0 \- B( M9 w8 M2 p
) x/ y2 F2 I1 i2 i 漏洞利用! N* q5 _9 G" ^* }: i! A6 m
6 S' L- j1 I/ V- C6 W9 j W4 _, I+ s3 S( t/ X
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。- p- Z+ c7 _5 H5 ~" @# D
: E. |* }: P. }! f1 b
! t; [. H, b/ v& o E! ^ ' {( K1 t7 l8 n
. c+ ~2 x* u$ E6 D) r# d
& {# b, C& f' s8 p 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid" t, b, n' |9 U
) ]: N( z/ k" j [5 i7 N% ^0 F: j: u! O9 ?1 o
不过实际利用上会有一定的限制。4 B' V: D( Z! p2 p+ v
$ N( c0 R. v! F( j- w3 g) ~6 U3 r* z7 Z# J% H. q7 x6 n
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
: ^" @9 k" w, v F6 D0 d& Z; ?1 X ! @+ \0 A$ V. M, [- O( W
- a9 k1 N/ A. h
& a$ E8 g3 N6 G
3 v( A5 K; i3 l: A' ~" z- S
; l& Y8 b8 v% i! F5 L$ [; B
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
( B* t/ u: m5 s9 ?* N* } 9 l s& E" P3 P
9 o( H& ^7 [# o, 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]);省略...+ c' ~9 |; g2 N$ r G0 T. L
$ M. y, c- H9 F. x3 P% X& p
4 x k6 O; }% q" y0 M5 _
因此要利用成功就需要条件竞争了。, J/ W7 B' f' [3 C+ ^; g
; r! | N& x% ?0 B/ z
8 a" Q; R' C# `" m7 }8 T
补丁分析
% m) `- G9 O8 k* q$ S( B* ~
5 b" i, {# E' f( f% F
, \2 o, i# H+ X6 J& n1 p4 G1 k 7 q* W7 i: Z9 s
[9 Q4 _& q1 U) `& ~
8 Y' G4 o9 j8 f& o& s4 m- v, s 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:9 _8 \8 k+ g5 h/ O- e$ L
) V8 y. i4 j0 |0 G
' d- `, W' c( Z; R$ y" C6 \4 o+ ~ function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}6 F* k4 g: e8 A! \
6 }1 p; M1 l+ K" I6 }* m. T; L1 ` u; V
: h% r u3 ?- h5 v/ P
/ D) k- |9 I1 L/ V( r; R& `1 m3 h9 M
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。' L/ k+ d3 A; f# g
( c0 Q) U% s3 K6 h7 {
, B# R. j$ W" z3 Q 在is_allow()中增加对$this->savename的二次检查。) G# c) x. L1 V
* \, M# w: G+ n e. ^5 w6 B L3 ]
2 X: v& V) p7 Y4 c# @1 i
最后
8 I' R; h. G0 W8 G' m5 q; {% O4 U/ x' v- m' y, {4 j6 E
& \3 W" M! s4 Z5 W 嘛,祝各位大师傅中秋快乐!; A* o1 b5 I4 e, H* i
2 V% V n) P0 S- B+ B
& J7 X# [% Y% M7 T- Q: M- Y
7 I9 V$ u: ]0 ` : c, O9 R* Q4 g8 k+ I. e- R. G
|