9 K. h+ j! Z* c- U. _, m8 \+ u) ?& o# Y0 k! c
. V" P& R6 z$ N, O7 m7 l2 _3 x+ M1 U% n1 V# I3 |# N+ Y7 }
前言
& L2 G( \ C5 u! o. r, A
8 O" D! n0 s% f8 X: x% S J
1 o$ ^3 N! S; U/ b# e2 ` 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。- x1 J* g7 v4 l- u
+ t1 `" k) H1 B# a+ A9 w9 N, I, M M$ p- w3 I% W
% ^! h# }1 ~( p7 Y. s9 c
# p( a: q+ b% [. |5 R6 ^9 [* B5 j# f. Q; [. E9 Z
漏洞分析
( X, z9 M* {( q5 X$ K' l7 e0 k! r# U. f7 ]
7 N0 q' B6 r/ X: R: @/ l6 m$ ` 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
4 i3 t6 ^: ]. G" k- Y. X( E ; c! W d r* Q2 H' ?
: }- o* L* z8 B9 y, |
( ]7 U( @, x, ^$ B& S
9 y/ v7 d7 v$ N( }$ d2 Y
) c% }+ |3 d- w/ S1 J, X* J 对应着avatar.inc.php代码如下:7 G" ~" g% c$ S, {4 ~
' y2 S( @% n ?' o& v
0 {$ j- n( N. q) 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) {+ M3 w* } s, m: T0 h2 s
6 c' k5 L. {% U. ]/ t3 x
[) a# O: ?: E* U, h: q case 'upload':
- I% t1 x+ M( W+ K; i% O6 s
+ ~. j+ G: q/ \3 w5 A' b9 e
: K1 \7 A8 P5 M* y I# ~1 a8 W if(!$_FILES['file']['size']) {+ D ]) S/ m; M. R7 I; S) {
M# ~ O0 e' K8 Y& L1 q1 N- ]2 {+ ?
/ I( o( Q9 F% _9 m8 Z: g) Y if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
) N1 y0 @: @1 Y9 | 0 z# _8 z0 Y3 z. q3 c3 s0 X
6 u M( U4 m' A9 A0 k [8 G
exit('{"error":1,"message":"Error FILE"}');; b& a! d# V. y
7 J4 Z( ?* X$ d! H& J5 |
- F1 I) F9 q7 a# F- k
}
; V( w* E6 u+ l " H& f9 ^* } B g p: ~
1 |" g4 `4 [( r require DT_ROOT.'/include/upload.class.php';
" Z0 n; J2 c! }3 C . W, [2 {( `3 b* m- j
8 R7 r4 j# y/ h z) H0 M; {& Z6 L2 z
8 M1 ]7 g$ q# p3 K6 W7 \ ' c3 O8 }; d' e6 N$ R- A+ p
" J9 e% z# L3 W: u2 ?0 N
$ext = file_ext($_FILES['file']['name']);: C* }; e4 F ]& `0 ^) c( N3 I
; x% [0 c2 R, D ]* K
! A" _) W9 }( ^ $name = 'avatar'.$_userid.'.'.$ext;1 M0 w7 A o+ l7 N9 }' w0 y* Y
1 s. @5 e- M- I6 E7 L
! Q) D1 v6 P1 M' a6 {8 c
$file = DT_ROOT.'/file/temp/'.$name;
' X" w7 u9 N1 [% J 4 P+ ]/ _+ z4 M- g W9 e: F* l7 `
/ @( S& J* N3 ^5 }# h ' H6 G/ ]$ d+ w( _$ J
5 ?/ H! B2 r, P! a) I
( H# M u, t t: u! P if(is_file($file)) file_del($file);; ]! W% C4 k' p9 Q, e
. T& _) b" N3 _3 V) v E
. V6 }. ?5 p" ]0 j' y2 D# L $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
( i6 ^& `5 k% e: P ) n0 c$ ?0 v: q7 }. q% d0 w
7 x% c- ]! E$ x* z: `( z* y( [( c- M
1 a: W9 b5 G/ Y, ]" r
9 x: O' Q H8 c+ g
& n. q4 X" o9 I5 P( {3 W $upload->adduserid = false;
% V) K% a) Y" n8 s" m4 {
2 o5 i5 A4 f3 q% ^( C, {3 r
+ p! s0 L! `0 @* H) G" h5 b6 N 5 x* B7 D& {0 U' P
# D0 z6 x0 Q& a: b( `% `( A1 `4 w- Q' X2 W
if($upload->save()) {
' b( t" u. U# B1 U4 T' ~
9 S0 l* n; S) N( |) }3 o0 @: ?# ^" q2 B6 X: _: @
...
, o( c4 L4 @# y& C
$ D+ G- m3 f$ H; T+ _3 ?2 Q+ K' ?3 H" s
} else {, L+ k8 e: n4 Z% ^: N
\2 Z3 X- E; w' s- O8 W8 n
+ `6 t5 O2 e/ t4 n9 f" D .... c' b, f! U- W# s% m9 i- W
! y2 d. O. p- ^0 j* U( C5 o4 [! z6 h& D$ P
}6 ~0 A9 {5 C* i+ A
7 j+ y! a# r/ w8 d" @7 \- q# ?# `4 |8 d) ]/ M% G3 s5 K$ i
break;
6 c0 I& u# E% ], k9 L1 S; z 1 s' C" {1 k: F8 s
4 @% L: h1 t+ p; i
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。% d% D! A! @) T6 Z6 r9 ]. k
" M: w% p& P* C% |1 k( v8 r
+ @2 }1 h4 b: u4 C4 q. u4 w
upload对象构造函数如下,include/upload.class.php:25:& ~$ P- U# }, ?' o+ c! u
" n$ _4 B, b, R: @
7 }4 H1 B* c2 c: V3 M <?phpclass upload {6 K+ S- `2 I# O
# g' L( z0 x) N1 g1 j* R6 f2 _
6 b) r$ `8 d: F4 } function __construct($_file, $savepath, $savename = '', $fileformat = '') {" O. q8 b8 l& z5 A2 f+ m* E: C) b
8 k3 Q1 F! P% y2 g7 b" k. @! l/ A
0 Q+ _. Z7 G* g* A* z' _6 I
global $DT, $_userid;2 u% i5 `, M; @+ Q
6 _+ g m$ f! b1 N. {
9 i; V2 u: B* P- n! N foreach($_file as $file) {
$ K) k2 J0 I1 Y/ B3 Q7 R
7 K% q" @8 b- G7 R2 z! o0 \; d( h
$this->file = $file['tmp_name'];2 @$ f# A" G5 I
+ d. R- t& D: `' ^
! o" V4 r( Q! l. E& G! s $this->file_name = $file['name'];' ]) D( O2 ?, E: A$ A6 R
5 p6 |1 G1 O4 F2 L
, c. |7 t" X% k8 M8 ` $this->file_size = $file['size'];
! j, g; ^5 L% {. g7 B/ A+ y * q& R4 V! S) G; ?# f# [ B
) F$ L* j! e7 D" [% v6 |; \6 `* ? $this->file_type = $file['type'];7 C1 [* Q2 u: E, M+ g0 P7 L
8 v( v+ F0 U! h2 A% F' [
/ l* N) U2 Y9 P* X0 n
$this->file_error = $file['error'];
9 U7 S9 ^' r7 x8 \
3 Q: p+ A2 H5 B3 j4 a3 p% Z" a; e3 Z0 Y
4 w5 G% V" x7 {& u- W
. c5 J* X* Z( Q+ J& q9 v' g' [# L5 e, a; p. F
}. B! q0 S% W W1 u
/ ~- e5 G1 H, V- W/ C$ X5 {
/ L& P3 y9 d4 S, B) K" O $this->userid = $_userid;
. ~" a+ \# G f& t- \$ a2 t3 I , @( Z2 T7 q+ s* M
6 J+ Y, O& `$ W1 \, m( o7 B/ s, v$ U
$this->ext = file_ext($this->file_name);. o" N1 L6 f6 [; W
4 [+ t6 O8 E+ p: _! R
) T7 p( o! e4 O( h( S $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
' @' k% @, n( w5 h
5 d! J6 G1 ~* B* |" Z
9 `6 o W+ B7 O. e: Q$ K1 v $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;6 b4 @& f: b% y# f* K: ~
3 ?9 e7 l: I. |4 Q2 M# I5 v
: _7 F0 R( c) g5 }, q $this->savepath = $savepath;4 U' Q+ u% b0 J! { A
1 t! H. \8 ^$ O/ u8 O
7 m* @+ O6 V3 e/ d: z9 [$ w( Y
$this->savename = $savename;
/ s- A3 s+ D( u) D6 p8 U) f $ t7 Y; G; |. w" n+ G6 m& \* ]( t
6 T3 l! A4 z% G4 e
}}
/ A9 t- ` e5 [0 F 8 v+ j( y8 O f, _: n7 [3 B
; e% [$ R2 G7 R l* ^" h 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
# ]3 Y7 I/ o" n0 R( K \6 U) M% R # X; P' j6 w2 G! z) c t
/ u! ?. N( }: f `9 ^& K7 {2 c! j; A 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
8 [8 @/ u( X, G" E: O; z" m' M# F7 O ) B/ J6 P5 h+ p. @" j, l/ p6 C1 u
, @5 z9 D$ L9 E$ C" A, D1 z5 T $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
/ p1 A9 O/ c# @6 t6 m/ v $ L# Y6 y; W2 W+ _; E4 A! i) [
- P' L4 `% _4 J- W1 S
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
. @. R' X& s# W- O6 H |
0 s) v1 @* m1 m' F/ [7 `' C& N2 G4 Z C
8 i4 k/ @% B3 L7 I) q4 Q
$ K0 B: F4 x# W& I* Q3 v7 s- B- W( M8 h4 f4 |! e
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
& w1 q$ U+ ?- b: u* \1 c; z . O1 k) R8 Z+ ~, c4 O8 K& {! M: h
1 ?/ x! f# Z" | <?phpclass upload {
9 M' _/ Z+ s. L: Q$ n6 D1 r
7 g* x y1 X4 O7 z( u, `) c% [4 G, M h j: ?# ~% O- s' e
function save() {) \0 Y" H: R1 f, i2 b. c
! w0 K) J) b% x% f" ^5 o2 L
0 C# }1 n& ?1 y' v# {$ V4 _) s
include load('include.lang');. E/ r6 ]: w5 b* c! f1 w, c
- Y! R. g% |3 @. q- J
0 w5 }6 A& x! `1 R if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
" i! ]' C9 A. q. |& w 7 q5 i; l: p l/ A3 m
N7 V+ l* s0 K/ C" ~ / t1 ^ D" `: K6 \3 M* w
8 A( |! w8 G% t' ]+ Y6 }4 p
* n [* h& T: g
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');3 G5 m7 `7 i- V. d) p9 O
3 r( o) V, r$ B% ?, T
* m- H0 [0 e' h5 o $ C# X5 t- w9 M" v, @. v
" ^6 F* v- H* J& v; f
8 |- c3 z; l" |( V/ q if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);! O; p! e$ }- U$ o6 y$ v8 z
, @# Y. }5 g+ z: J
+ [) y8 [1 ]6 D/ _$ z0 T: {
* s- g8 i F% @1 k. q
& {' o9 V) ]. V6 m4 J, h0 o5 {* W w* ]
$this->set_savepath($this->savepath);
}; B# t( D4 }; [/ i6 _ 1 d( u/ z4 g; L" L# Y5 |. i
+ \% ]) ?& X3 i& V* ]! c l $this->set_savename($this->savename);
: N5 K; g; ~# f' Y8 [ 1 Y9 M# @( C% m0 I! U
9 L- B( w' C1 p4 {# n
# M' ~6 E6 ~0 C4 Z5 f
; n" a) A2 c% O$ E9 H) a" R3 P6 H T: r6 W
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
% n2 n! l$ _5 [, a* c: e" P; ~5 I+ j : x5 L8 C2 x: L) x& t$ h
( O5 [+ E7 b H( R4 _
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);& A& `) {# E4 M0 T" ?: P) C5 g c
4 k, Y+ w9 ?, X
% ~4 y" k# F! ^/ \2 P if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);; F* }8 F; y, N- L9 A( e6 Q
% Y( F! j4 j, F
5 r8 x+ s2 C6 F2 O7 f
0 p8 S. I! i) T! q' w/ l- C
\5 B8 Z# _; t0 x; E" l6 _! i- D' M- R1 H" d9 t5 @
$this->image = $this->is_image();4 ^2 m3 N. q* S$ z, H. `( T6 f
; Y F! f% X4 \! O6 \
$ d- x: ]6 f' E3 h9 z) \* Y
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);4 | n9 J5 A( b6 v4 Y, q% ]
8 E4 y* s. |/ X! B. r2 B/ ]. L& r0 N" Q
return true;0 g: M% w: J, z7 a! ]! v5 O
8 N7 n' ^4 a; E4 c V& c' T( S4 l1 V
5 {4 E% @# _" ~6 d; B }}
2 K7 M/ V( ^( L! ?+ }9 i2 R; Q0 `6 m
, u n: {: P5 y, U- ~% `' s5 Q
2 Z9 U7 U5 P" X! A" x" Z 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
7 A) w; C& h. o# T/ [
4 S5 v% J6 c3 p' u* Q" p& D9 i3 O3 {' B+ Y1 L4 G0 C
<?php9 X d) M! s5 g: n2 y$ h
3 S" E9 g; |$ y Q
5 O. _) H5 r3 v& b7 }* @5 i6 J2 h function is_allow() {" ?6 ~4 @! s4 D, }& J3 j( }
2 t9 G2 s0 R1 X% f& G
. N& w" w0 y, D& W/ D$ @; } if(!$this->fileformat) return false;6 A/ |- \; }$ y5 g" |% X# ^
7 V( ^- @; G# R" Y
" ^9 c9 {5 I3 x4 Y2 r1 n$ ]
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;- C2 L8 [) f% B' {
- X4 q' x5 M$ H# y4 U# c5 I0 E; H' b8 l6 l2 w0 w- k
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;1 a6 @' X9 A* V8 S6 l4 F
" y0 L' P* y1 c8 R
( Y2 D' a5 D8 ]6 n8 M
return true;8 } Z+ G1 e# m4 d
# K& B7 ^" \% |0 _4 C/ i" ^( A
7 ^4 m( C' Z. C0 o1 c+ x$ V N4 ]
}3 u8 B0 k& w: b; l# d
; `7 |# [8 ^. C/ ? h. D v
- F* ^4 ]3 o0 f9 X0 K) ]5 S; O 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。1 J4 v+ J2 U4 `1 @ p+ }! x
% _/ |1 |% w8 E& E# ^, G- k
4 j6 d# u- ]% l# o4 Z4 o! I 接着会进行真正的保存。通过$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 k1 C! u- E$ t: K$ p8 m" f
7 k9 B% q- ~: b9 G- M6 c
$ r! J) Y' f: w2 R6 X
漏洞利用
& }$ t) Y* G% w/ S* o9 O7 a! z# |8 L9 L' J" l& \
# l3 f7 g' O* r2 z F
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。# d0 {1 k3 w0 S. _ w4 }
, p6 ?' [) N. N4 c7 R/ w3 K
+ X* b3 P2 Y/ I& F0 W' |4 }
* B) \7 W' T3 w* U$ A" B ) V& |; @/ i7 q8 `$ T$ U
: T( F; e, t) e9 H: ?3 y 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid- J0 B$ T2 N) d: u Q
* E6 _% i8 X$ s1 Z% P& P9 @, H4 I1 C
! p c2 m' {+ I' w 不过实际利用上会有一定的限制。
3 K# r4 z# K1 K& | ! l8 H3 C" ~8 z' h7 X
# s8 Y7 k4 s6 J1 K. `4 r, W2 n 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。' q! x- G+ I/ K: a
6 \' [/ X3 o5 g A
1 |; F# T1 {8 I! N/ l 4 {# P8 w& o' p9 x5 T, H' a; U
' ]8 |: [4 n3 f* K9 x
! S/ \& z' z9 b2 `8 B 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:5 z1 k' N# s" I8 z! l" f/ {- y
& j. H$ N, C. @$ w* w& D
& M/ `* o$ r1 m- p6 { 省略...$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]);省略...; Q: _" U8 ~- i+ p) A8 E8 r; u
8 K7 P/ ]5 |+ w% D6 Z7 V+ G! r, D
" q+ O S: e r- j0 p 因此要利用成功就需要条件竞争了。0 e( a4 b8 H2 N0 I, T. P
( u+ h u( G r' F4 A
5 G5 A. }6 L, U0 [$ e 补丁分析5 t6 L' ^: w9 a" v/ t
7 J3 S1 z: h& B$ ]. P }; N% p1 u
. W% m: j2 ?. y# U ' E2 v. l) T* W# ^/ z; [
, y: ~/ t% [- Q# H! [- d
5 }( r1 C4 b, S# ], L
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:3 p1 b1 @6 {6 e' f
y9 F. v" p( a; Q p8 x: n
$ Q5 J) I1 S" D- e function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}: H8 `6 \* P6 }! y9 |: q( A$ N
1 Z1 L1 F9 p; m5 H! x" Z
% G; M' J+ }! }; q4 y9 {$ r
$ t, R# ?! n& T" [* H- P3 M, B
: K: B% K5 ^' O, s. i
: N) ? _; ~9 m, U5 }1 X" v 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
$ S+ R6 ]9 A/ A! F4 ?) ~
8 F8 H7 r, _1 T% j7 t! W+ R$ p
6 T, ^4 }2 u% @( w7 `4 m' t: b 在is_allow()中增加对$this->savename的二次检查。! u) j. F6 t: Z) T* I
. ?- f. B# i' C0 A# h7 K
" _/ M! B. i- d5 { 最后4 A& e7 ^! z0 T: o1 g
0 ^$ {8 |: A" N4 y
/ V: L0 M, |+ E/ |. I
嘛,祝各位大师傅中秋快乐!
: P/ ?2 Z0 d7 i5 z % i7 i! `" E3 Z$ U' o# M4 H' y
3 b) O* Q z% h/ k* j- o0 B2 Z
, H0 s2 I& v2 n) c, `
, S& s$ l T8 l# R* T
|