% Q, F, k* ^' C9 U& g% x& x8 d; k+ B/ |
* I# ~4 g1 x7 b p& L
u. x6 R9 {8 g 前言
( B" L, c+ k5 {( j6 D: y. j' L
' a; i3 e/ e; W% m; q
$ F2 u' V O. U+ s0 n4 }/ R 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。$ V6 ?- \6 Y3 ~( W; _. p% F
( y% f. H7 Q# `$ Z
, ]* j: G$ H' x7 E# [2 E$ b6 V
% ]( g( I& ]0 G
+ n( b* K* Q; i9 y; O( L
& V2 `; |; N2 [. w% A% P
漏洞分析
/ P7 J: l' s8 d2 a$ |1 s- p- C l
, s4 |: K; w0 d2 W' r; d0 O 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:5 S1 s! c- z# K
4 ]# G6 _% R6 z+ D
/ G' x- _, [- d* C
- d$ o. }* ?9 G. s
/ l `- r0 c7 z- M9 O/ R. k t' T
& v0 L5 f' c [: r: \5 Q( y1 } 对应着avatar.inc.php代码如下:
6 L! ?7 j% d* f6 o" D
) b7 A0 h0 s& l. f. T' L
" Q. S2 O3 \ `+ W7 z3 m N <?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) {
/ n6 z$ z% o2 A" V$ W' H( f0 ~ ( [3 y- c7 O; p+ U
. J! d$ B+ r2 E1 {) }* \
case 'upload':
2 j ?8 C( A. e% O N4 z
& n' T1 G `$ P7 Q9 T5 y
$ j) T5 o: X; F* F2 f) |# z1 n( S" [) m if(!$_FILES['file']['size']) {
$ J: H1 q3 [4 ] B
; ]% B7 I2 s. W. a3 x& [4 v3 o5 F% N: x. u; {3 u
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
6 S8 h) C; c8 ^8 H( m4 c % x7 }9 {3 g, }$ _+ n N2 P
; g* o c0 M' h N
exit('{"error":1,"message":"Error FILE"}');
7 {* f. D4 {+ Y, J" ~" s 7 \* Z* w- D- C" P2 b) v; z. T
; i1 V. O( i7 D4 x! I2 |0 \# i, Q }
) n( m- M+ Q e' u* _ . R( f4 j" T* c
/ v% D7 _- }- z, f) {3 r7 j require DT_ROOT.'/include/upload.class.php';
# o- T& Z6 f& E# K
9 g# B) J' r$ t3 T( ~. `
3 S6 s# X, M& n
8 a7 [3 K, ? S, |' H* @* D6 @
6 U7 y ^" f) l) z) {* d+ }: ~3 _- Y
$ext = file_ext($_FILES['file']['name']);+ |2 L- k# ?3 E) T- q
9 ~: P7 R7 t' B3 L" l }0 e% a6 M
0 r) L/ ^% I. o8 O! c9 b $name = 'avatar'.$_userid.'.'.$ext;; w# J. T4 h% K; e% m4 N5 T
* o' @1 `1 [" b. O
" g% B4 L8 a2 n& v% C$ U' ^ $file = DT_ROOT.'/file/temp/'.$name;
8 w$ a' ?+ P6 L3 u/ ]* q" M
) v) \, D' I5 W& w% Q
" C% T/ E) {$ w
l5 F9 z( k9 v9 i$ d+ `
( p: U! }" g$ r: s% q( i3 g% p1 Q6 G. B5 n
if(is_file($file)) file_del($file);
* H' z) w0 O. ` 2 y) g. j1 X' I F
( n+ M ~9 p9 T( N $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
2 e# e; p7 I" [3 l% c
1 H4 e( H# T g W9 s( z s: p
# J& ~: O$ \/ E: O
' p4 D5 }5 Z7 |& w1 g4 r
$ g' F; Z, P) \) l& A$ f7 y6 O- I8 \- P; z1 X/ D9 D m h: b$ V
$upload->adduserid = false;
0 x' a0 p; j: D2 I! a 1 _, ?: {6 [% V" T( F
" ^5 {# P4 i' x' _! J' h+ I1 { {
9 l3 g: h2 f- H+ f/ _: \
5 S* ?. I/ I3 L! k: z+ d$ _( h( b( g2 y" {5 y. \
if($upload->save()) {
5 [7 d! W& r% c3 F* ~3 u
$ c4 ^5 ^" V# P) y, [' h0 @
3 A# t k4 R7 w9 k6 ]' g ...1 {+ w. L# B o, ^; I( Z
% f8 ]% _7 |4 N9 y# b; Y a+ v1 @5 s! U7 K
} else {" s! W: ^3 e* I5 ^+ i% {- ?' n8 A
; V& T7 ^) F) |' u
& l3 ]9 `4 c: E- v8 `2 o ...
9 N& \! @) q4 F0 O1 ` 5 b; ^( L$ B$ |5 i. F
( ?6 [ A, ^5 ?) z* t6 D$ V }
$ L# S" O! p5 S" I* \
5 K) I8 u4 e Y# m
/ @7 Z# o- C5 u4 D! n break;
2 h5 M& F G( y2 u: f$ ?0 X- ]
4 j0 T# l: L* o5 [) r6 f) ^5 w% I9 Z5 m: N
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。. Q: w. u) u4 Y7 H5 T Y, w" n" H
; W! \+ E* u2 _
( v; ?6 s6 r; @+ P! `. G- l+ Q
upload对象构造函数如下,include/upload.class.php:25:& T7 M2 W+ E% `' n
: v' X- P+ j. ] N& @; ?% B
% c2 `$ `. B- m7 `9 g) x
<?phpclass upload {
3 _* G6 y/ x) E( v* j 0 ~3 x1 G8 ]" f) N6 X5 R
! V' d' v- S G0 ~$ b function __construct($_file, $savepath, $savename = '', $fileformat = '') {
) }0 c! e' l+ w7 T
. H7 X0 j$ T$ w: y. |: @# A1 z, ^) ^3 J \
global $DT, $_userid;
# X+ T7 \# |6 S7 `. p9 F 6 A7 ~2 B7 Q' g- B% G& g- u6 g& I
, w4 k2 b; r& t3 w' P0 H- a foreach($_file as $file) {% _# j' Y5 B# G9 N4 v
7 |8 f, T+ r6 {; Q4 g: M
7 I% A. H r+ l4 L $this->file = $file['tmp_name'];
/ [ @8 I5 x, M+ g7 d2 z7 ?3 Y; @+ F
+ r0 `' }4 q2 ^6 ?+ _
: @4 ?) x0 [0 s- | $this->file_name = $file['name'];: \- I$ R p1 J! `5 ]
" w* y! c/ O1 J/ t" K( k: X
# O j6 n* `6 r5 q+ U $this->file_size = $file['size'];8 l# \+ K, E+ m! }8 R
' H4 |- Q1 j9 Y2 o8 m2 V7 v+ ]* s
) t4 K% ]0 g& c $this->file_type = $file['type'];; M C7 a% T. s. d; g) G
' O, y: O9 x( k8 S( q7 O" n3 |. Z" {
$this->file_error = $file['error'];. q5 \7 |6 x. \2 W/ ?3 ? Q
8 F3 }7 z7 |) ~0 }! T
1 u( ]9 w& E" o" m8 p2 }+ I! o
7 Y3 y) i1 `2 w, k* Z; ] 7 k1 ~6 ~/ _) S3 Q1 ~. j5 s6 G
- A/ z- Q+ R9 t$ h: L/ c }
) a3 i, k: Z0 q* z+ \' ^+ W7 O 4 ^0 e" j/ Z/ [ @2 a
3 P$ l7 ~& G/ {8 J4 Z' |5 a
$this->userid = $_userid;
) d- ~: N* _: b8 K3 U# k: ~2 v - ]9 }( h& i! r) V; f4 T6 U
9 ?% ^. ]# J2 ^$ K0 r3 w $this->ext = file_ext($this->file_name);
. |2 C: i! F$ X; i/ c2 Y# s * [' \& ~, b1 \5 R9 ^
0 @& O! b3 T! W" d; Q. h( C3 v
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
# f7 }) [! }2 X* v2 Y 8 r: v( g' H( I! N7 t
+ B/ T4 l8 f+ w1 S5 k $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;+ v5 D9 U/ X$ L! P w3 j- |3 y
, B# v( g3 F$ B5 j6 ~2 E5 M/ ] f+ I+ Q) F# Q9 Z) {
$this->savepath = $savepath;. ^. u3 S. j5 Q- x0 p
7 Z, b' w! N( G1 ]" G* @" F
( Z: M1 o; j! H( ^; i2 i
$this->savename = $savename;
7 P* S0 ]9 Y6 l9 F: L# P& ^
+ Q5 X# f7 @. ~
" ]' u9 o/ h) i6 }; V. Y }}9 K- T4 p* ^/ e$ p( e; n
+ G" _# v7 b# z- @9 N
5 q* I3 Y8 ?9 M+ t; j. q 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
& i6 O- s6 d& O8 y * ]# w& C; F1 y$ A' |
$ t+ d- u! J$ f8 H
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 $ V8 A+ t+ ]) N* a- Y& W1 e
6 ]# H* c3 G8 e8 v! J7 |' s
3 T- G* {. a( X! R5 q6 d3 x3 X6 K3 D
$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: D+ S9 `4 t2 g0 a& m9 v7 _! s
' s, g ^& J2 t8 I' K4 C
4 K# L% Y# j) ^6 X) a7 a 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:+ T: Z b3 V6 d& h" ]: u% z' [
% K9 d# i5 g# b: C- J! h3 ?6 h; x5 l2 ] k4 C1 M6 m7 l
7 l. x9 G( W C- n( |2 K& o
) ~3 j1 ?3 Z! q! m: R* l) ~4 }
' b; g& {5 S" U# F1 A 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
3 m( n# |* c( [0 ` ( Z& R* m, \4 q E- ?
8 A8 a1 m7 [/ r* l% j
<?phpclass upload {
: J2 h E+ |* v: B8 J- j
- q2 N7 A- i' `2 o
9 _0 D! i4 x# ` function save() {
/ w( o1 @. T, S5 M " a; o' r0 C& M7 U0 c( k
0 f, g0 G. i- c+ u
include load('include.lang');/ |$ H; z8 ?% g9 p
( C0 f# B$ Y# x2 w, R5 O' v( Q! |& X. m
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');; Y2 o% X1 Q$ s1 z
0 C, w4 K0 h5 u) k X
0 U! l J# j* L; \% L 5 \ B8 \. w3 M4 X" j C, ~
& y; {! t/ O5 H" v5 [& p; m2 U5 |! n" K+ b w
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
: o) v: c+ ]& _1 g ; k/ V6 i; X5 k
& N3 i) O8 s+ Q3 i' z
( E" R+ U; \% ?9 `: ~. a& h
! M0 @" g5 ]* E3 M: P
x9 j7 s3 ?& C! S7 m9 y- O/ } if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
" v1 q! d# K0 k! Z
1 {" l0 A7 y0 n
9 j6 y9 b* D9 b( ?7 z ! v/ e7 A5 z) A$ p
: s. a& V* j5 [" h3 X; p* P: B* H9 c* w
$this->set_savepath($this->savepath);
3 `# q# `) L/ X6 b2 ` ' y, h$ l' l5 u+ M* F& l! `" Q
, P( j2 ^0 W, {8 k4 o7 a' O [
$this->set_savename($this->savename);) e# p2 {) y3 Z& `4 G u
7 g p' [6 M( @+ ~' M# x5 U: W- \5 A, s; h. l+ z1 T
5 y+ z- l5 b6 z, l
; Y& u& F* C& d" ^0 r! H$ C$ Q
7 N' r/ C' p7 p$ q$ N& T& n+ E if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);8 Q/ f3 f& m/ V
% j, N, n) t& `; H: e/ z0 \
7 T2 o. u' [ F
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);1 t, F" [6 x' H' m
8 q g! U9 _& T' | T0 {) J! P
3 e$ |. L( x' e: ~! c# f2 e
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);3 w+ J* d: |8 g) j, s. u
, B2 F* Y8 [* T4 i$ u" S
' E' |0 I% g! }8 f8 n4 ?3 o) r " ~" G* @! u, B! i
0 n+ [- a8 w" O
# F; D: g7 n1 K1 N- y% V7 T
$this->image = $this->is_image();
3 _. s( E6 R, s/ Z9 a, x
: L0 V( O9 U" T
8 t# r: h a/ c if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);, K# [$ Z9 K: o( N9 q+ }( p
9 k, U$ m/ G+ G( Y; X" S0 ^ F- @% l1 H( i4 \
return true;1 ]7 T% d! g+ O9 L) r& k
& Q4 f: v8 x7 e- o6 e
4 l5 Z- w: n+ @, r; {+ }% y. G1 [
}}
h) s# w, ]- s* ~2 L E( N4 p
7 \& K+ {" J4 S! B( \
* ~, i1 s& F1 b# ]4 d( q 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
" P! U/ H2 u4 D4 r0 r: M" T
- {- x& j" N! M) w4 X- ?; O
p. z4 Q# i; l' f2 T; G8 g <?php2 k2 l1 B5 l& K
) Q4 V j' M0 R7 F& U0 ~/ g, N% z
/ g& O$ s B# W1 O! R/ B+ J' a
function is_allow() {; ?" e1 y `) I, K7 z
* W) Y5 q `' ^" ]4 z
3 a9 D& o# h4 p4 r L7 \( O6 U
if(!$this->fileformat) return false;2 w5 e& U. ?" U4 Q. b( b; @
9 j' p+ u" J( u$ g+ H* o# r
: t" Y+ N) {. j- h O, \
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
8 b8 f& B7 o% ?$ n1 L 0 L' k8 ?6 @' N# A8 y5 q4 s9 z
& N5 F+ W0 D e1 |5 e) Z' s8 t7 o
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 f$ O/ D& @+ h: @7 F
& l6 U% ~$ I8 G+ N. a/ X$ U+ E& @' F+ k B. N5 K) X2 X% l
return true;
, M( o8 ?- y9 I7 M! @) }
' ~9 D' q+ k1 @7 u5 H
, |5 M/ U9 E$ ~" g5 k }1 F- o( X8 F. ]$ r/ S* u3 q* K/ o
" v4 v9 o" j) d/ i
, Y0 y7 c. ~" y+ W, |) h, [: @ 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。* m; i9 N4 Q9 n' A6 z
5 ^! _6 ?- y' `, [ B* u3 W
6 q1 O- V n$ H
接着会进行真正的保存。通过$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文件。
2 j N7 H$ p5 n: `1 @( e( x
4 ?, M- \! w5 D& |9 {; D8 n' e$ f1 b2 V% v1 e9 H
漏洞利用9 ~7 S: O4 T* Z$ l
9 f* ]4 W; b9 I- m! p* o
9 C$ f/ |$ F% j3 x 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。& [& t. X) w+ W+ ]: y4 r' I
5 ~0 C( j$ S( w' C. D( q
0 R% Y$ B- R0 _. D) T( ]2 i2 V
. a0 U9 F* M0 d. a {2 ^: o
@. W& ]' o* I/ w# z, `
2 w5 E8 j/ V8 ~" N( [ w! W 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
0 y& \, w. @: |7 d( M; G 1 l& ]9 @5 S6 D3 C+ S3 \8 @
* T0 I- H/ C" l8 b+ f6 ~) ]; p 不过实际利用上会有一定的限制。
2 x% C6 e/ L5 ~" P) h! [7 M( V 5 p, w9 }/ L# X- P2 H* ~2 T8 z
$ P/ g0 y9 U1 e3 v" }% L- c 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。. r& v3 z: t: P0 W
- L% ]/ N! i" O+ b" Z2 i
6 _3 r( c' q; E
! {5 D( s2 b0 l* ]6 k
; ^1 y3 _3 N9 F/ f2 d8 B8 X5 L. I, a8 t
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:# L) t3 v6 a/ s( _- `" [/ W9 M
5 ?, n3 X0 v: ]
: ]6 P$ {& W+ U
省略...$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]);省略...
( P# S+ U$ M; P2 G ( @2 L% h! q8 @
" a* e/ S0 }9 T' X# k
因此要利用成功就需要条件竞争了。
. ^, T, @/ `; n; ^3 _$ ^
* Q- ^) u: P' e W4 `! w- ?: ? u! |
补丁分析4 C; ?, Q& X9 `! f. Z4 t- l
4 o% Z" A- [4 a% D
, u* ]/ U0 X) \( P! R+ o4 X
+ g: `4 c/ T1 C$ X6 T% ~% g' z4 Q 4 Q1 f' y- a% \; i: N9 h% y" U
% c: Y# W" D5 {) ]0 C: G" x 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
% @4 j+ r7 ]: |- ^; g+ x" f / M6 E+ }/ @* Y- D( N
& F5 j$ {5 N5 t3 _( O7 @: L( T$ q function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}7 R8 e1 O3 ~- `
) G% U7 k) t3 i
+ }& L( b. f S( s7 Z4 } F' r/ j' U' E5 k4 |! @
9 h' ]! b- |) A! O% A' q! Q" k
- G# }) _* h2 S1 N
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
{5 z( Y9 ]1 E. D- Y: X- b
" `) J2 C1 \; O5 v
7 y; u% v0 |5 O8 w) L2 r 在is_allow()中增加对$this->savename的二次检查。& A/ f* r0 c' ]+ `3 F. @& u
: v% t q: b& p5 g3 n6 g& _3 K S1 B8 \. T/ l! }. [
最后
# K m f1 Y1 t9 |9 \% g) P, v! V
* J: h4 B6 i$ B8 ?; I8 B' `/ w) s, [! E, G) `) C- Q
嘛,祝各位大师傅中秋快乐! a# E, I1 Q- j; z! q+ d
: q; f# d, B% V& F$ Y- |, n0 E
0 D* V2 H3 N1 a0 y* P! G0 W
+ y( i5 ?5 \7 n3 g2 o; v$ V
! Q/ |+ f h2 Y+ h) g5 ]
|