( i! N# w! Z% r* R% P2 w; j6 b, D
' Z- Z; {+ A0 k. |5 e/ [
& Z0 M7 x! R* A% q @
! S$ h \" j% C- X 前言
+ K# G# y* z& T$ i. D0 Q/ z5 K/ y! Z* }8 @0 f* `9 I
# |2 R% S0 O5 J$ p1 o7 C
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。6 R+ a0 K3 Q! V o
+ L$ A/ }- R6 \' D; w; b
. U& g* I1 l& ^9 m
* C. `+ M8 N& z: D( Z0 n' f w
6 K' b* _/ z+ U3 P; g0 w% k' I2 u" p* W) Z* v" T+ `
漏洞分析# r1 j( ?1 @: i: m; k$ I3 e! Y
4 [2 Y+ g( Y/ G9 A
: C) z+ i+ x: n) f9 j) S) D% P
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
* U* ]0 p3 q7 _3 p7 i$ z) m
. b% C$ r# R8 z+ s3 N4 e7 b& i
: }0 @: g/ o0 p9 z5 h" ?1 s: i
+ [$ u. l, q! ^) f* M , Q$ y: w } c! i
( M8 Z$ l+ Z8 r# E/ ]$ F0 o' H
对应着avatar.inc.php代码如下:
/ `3 R% H% C' p$ h3 h1 l* t/ O: Y
) ~. a7 k9 C9 i+ g/ M: D2 H7 G! @! t Q) l" I8 Q+ O
<?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) {
( L5 f" |4 {1 v! w3 t) F" F- g
. ^- O, @+ M F6 I X: a: c
/ M: g4 Y* d* J7 B case 'upload':% B' {3 Z. B6 F* j/ e" C- C
) C# l; K7 q) N5 `% T9 t
* }% W0 Y5 G$ N9 [/ Y0 D4 O3 @ if(!$_FILES['file']['size']) {
/ R# o0 [+ s8 H/ P4 _, s/ {
( K( Z7 U9 V- H6 j% ?7 M7 A# @
8 w; H/ n: c+ i+ F, X( _" d$ U if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
* |( v+ d5 q$ J2 g7 C
8 m1 J( ~1 t2 O! v' V
- r" c3 T/ ~# o; |6 t exit('{"error":1,"message":"Error FILE"}');4 N9 d5 ^7 c7 u3 e4 ~! V8 I# d
/ a* n& N. b8 E& l* F& n' V
5 b/ k- ?+ e& t5 H' c) D4 @ }- A0 ?7 _) ~1 E( J
1 \3 V: f, y. u$ C, s+ j, _% d0 h: `+ h" O3 U% m
require DT_ROOT.'/include/upload.class.php';$ n; g& y/ [+ X# H
; V8 m- [% ?' e( Y
( s e2 ^; B/ I7 G* {9 r7 z u C: H
; m9 `& l. D+ w/ N . U+ I; }* {" E. k: U* s
- X+ ^# e0 Q. ]3 K
$ext = file_ext($_FILES['file']['name']);
6 L, o X( a' Y
4 p* C( d& U Z. \" C7 ?6 ^" Q2 I$ f
; C/ J* E& d; Y1 G $name = 'avatar'.$_userid.'.'.$ext;
8 }6 F- g) A, {5 @6 Y , m; z# M$ M( u X9 @6 l0 v, |
, j4 Z% l6 s+ o2 t $file = DT_ROOT.'/file/temp/'.$name; ~3 r, t) v/ u* b
& G" L7 b4 H1 U' O! |& Z$ k s% y+ ` T% T8 Z% k
" T( X" m d3 e) o0 d ) Q7 { u( I3 g* ?4 C) ?8 d
9 @8 ^2 b# A7 o: P- M" a) }/ ?
if(is_file($file)) file_del($file);1 r* L% l# V: n& E; W' W$ j
- u, a" Z& ^ O3 _* J3 A
0 a9 ]% A! x" {5 l
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');& Z* f7 x5 M8 L4 b8 o
: P5 y& | t: `5 u* t
* V+ V y3 h, ]( b3 e , S4 B* O0 \4 g* o. y5 ?: s
/ V. W! v+ X% m% [$ a' T0 l% u* `5 S+ D* m; D. g- T5 n( q N
$upload->adduserid = false;
- ~3 Z/ v1 T; V) G, e5 d " p; |' f. e5 }& X7 }
( g1 v d! B g% g
2 H2 M2 C* I5 j9 I6 q
3 e2 T5 U* U$ {8 j' l% h% R- F) A( f1 K0 j9 S, _
if($upload->save()) {
; m- O2 n% d3 E3 l" ~$ p0 s " \+ W4 T/ X* z1 J
: u3 M( j0 F5 l
...- B' B, E$ ~: h* b
/ g8 m/ G1 Z/ t# Z# N
9 I$ e6 r! g% t8 F } else {* a' [( c5 h& ?4 T
& t6 z: e; ]: J$ O
4 Q* o' F0 q5 Q% I3 P7 A% c .... Q% _# v9 `) `9 N* Y
+ _8 H3 D, E5 I! Y7 y7 A3 m0 A+ X8 \2 r, G0 a* H. T
}
! C( J) L# \0 i+ Y, j* |/ Y
Q* {) h# F, h: t* O& R0 N# h# v7 v+ Y
break;4 q- | H; K2 ^. g" j, V
/ p3 {* Q0 ^+ L1 o+ ]5 [
1 Q5 [& M" r: ~
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。' x9 W1 |$ x1 r- o8 X( ^; l
2 q5 U7 Z1 ~7 k! k" v: T4 K4 u E% \
upload对象构造函数如下,include/upload.class.php:25:' H; n& M9 W5 _+ |2 a- }# m
2 S) K) Q% V# S4 l" w) L
* b2 e9 I4 z; b' X/ t8 B1 }& v! U
<?phpclass upload {
9 ?" Q3 E- g: h$ l2 C & O, Y/ X6 f, c. i- ~6 r
$ K8 `& |) c9 x- Q
function __construct($_file, $savepath, $savename = '', $fileformat = '') {, w; S! K/ I' I: Y# T3 ?
9 O# m# ]4 P. }2 l- M! R I/ r1 z" |8 r8 x# [
global $DT, $_userid;
' _1 y$ x3 m% m1 | " `: c# {4 Z6 G" x* k5 D; z
4 c0 [- E j% D9 K
foreach($_file as $file) {
6 m, A- l3 K) `9 D$ f
$ c# j# \" X" R' `
& w$ a0 p+ V# ]5 z/ n; i1 r $this->file = $file['tmp_name'];
4 C- X& k7 _7 M. \ 5 v% z% i8 E3 A7 {" K$ B
: }" m% w5 `6 t: Z+ R4 I $this->file_name = $file['name'];7 y( g7 R9 V$ ?0 K! l2 Y7 A
0 h" i8 ]& e8 }4 T$ p; b
( _0 m2 L- Q1 }; F $this->file_size = $file['size'];$ \- \$ ]* p+ G
* y9 h0 ]) W; X3 e7 J( v o" C5 r/ [) u! @. t: R2 a" w
$this->file_type = $file['type']; [# @! P! Y8 G
2 `5 y% t4 v3 |) c# I; X5 o h
* s" ^4 p! n* v $this->file_error = $file['error'];7 ~8 A4 A% N$ T1 B# P, [$ O: U
% }- M# x- B# j) R- R, F
' C0 p, e% S# J4 ~, q+ \0 m2 @/ H) u! | h9 x0 t% n# U
( ^/ B# n7 Y; j( P+ t$ K+ ?
2 n" E% x4 r/ s4 d! R# Z5 E }' v0 x4 u" b( n! ^9 N; B% Z- K
/ E! O- z" G: ~( D Y ^ q
8 L* K4 K' s L+ `6 U$ } $this->userid = $_userid;! {! h) z m! ^8 A; g; D d1 i
* k( R( j2 D/ m* X$ Q4 ^$ Y4 h0 t+ ^% c9 V
$this->ext = file_ext($this->file_name);
$ ^, I) D5 j# c! ], t4 i! \1 b% v6 A , S& N/ j- V' A2 F
& d! @- Z, Z: p" E& }; l( }* W $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];8 y) g* B! K; F7 W6 Q
7 d+ Y, i: ~* P5 R, q {
+ s, M2 y) {- q8 A T $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;# r9 E3 D& ?' o
& J- \- ~' ~$ c9 x; q5 n" e" L
" z, z9 q# F! A4 @* a1 Q, H5 W4 R+ B $this->savepath = $savepath;
4 |! U& V8 n+ j4 a+ v W9 A / @* u e8 x+ u
9 e. i [) E/ U* Q$ y8 q
$this->savename = $savename;
$ f3 B( B% Q7 S' ?# {$ D
% g6 c; e" ^. C0 J O/ t7 _3 V# f3 _5 V1 L0 W+ S1 ?& ]
}}
) @7 x; H) w: \% R X3 ?; \- v
|4 k# t2 L! j" c; n( w+ @9 V$ A5 m% I6 m9 U. g7 a& c0 G: v$ K6 l
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。0 k. `2 u- A8 i7 d
& _. M: V2 ]* T4 _. N
9 M. b) s# K. {$ c% n: X! a 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
. h4 n& n4 f0 F% q: Q* V6 g
6 Y$ g2 q( j' p
- z: r8 g: c: @6 W $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) E" i' u! i* w/ z) W: c
* H. x6 u6 g5 C5 J6 X
, q& Z: i+ I; y7 B! n3 Y
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
- k Y" J( B0 W" C3 q2 }, W i$ o
0 [ c, O( [0 j" q5 G: A2 N4 _- \' U: U) X: {
3 ?9 ?8 D/ E+ k, I( r# h
+ a$ R3 U) U/ Q, D# G9 x
( d& ~# {9 l* O, q# V9 |. f 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:* M" Q" {7 W- u
( l6 z6 |" z4 [- j4 }
+ ?5 _% H+ c5 f# ]/ O' d- p; S <?phpclass upload {
f% ~0 J4 t# x% }! Z3 i% G' V $ ~* c+ K6 ~/ T
; X2 F7 W7 @( |/ ]5 e* n7 ?4 Q function save() {6 n5 O R$ j3 y; \# h2 I: c9 p
6 t! d. O( K& {
; C# C1 C6 s! O9 K include load('include.lang');: K) `; Z( X" B1 t
! z: }2 b0 O# }3 |
; L4 E$ g* m4 V3 ] B& u5 l if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');2 E. k! x: B, b% l1 G
% T' C6 p+ |# P# k- }, u @7 @9 h, C0 v
* Q' y8 M; G& B/ j3 J( @3 t ) @) ~( _1 U6 M6 x6 t/ p [
, ?/ r2 T2 g( Y* @) G
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');, Y% W' u9 k, \) Z; q
9 ?/ \7 h0 {: g i
$ @- ]( U9 o: u6 P; y8 f# V0 e
# m; A7 h$ W" b0 Y3 J / Z/ U5 b& a* x8 P, @( w1 p& j
* [8 S# _2 L d. W
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
6 ]& P5 ]6 c# d ~$ y% ]8 y& j5 O
2 k( N1 f9 V* N; g5 S9 P) ~2 [( N- N3 k
% C: g2 R ]- q7 U' M+ G% C4 h * A1 T; [# N+ w4 L
/ O) m% u0 V2 F4 `$ f; y" ~ $this->set_savepath($this->savepath);) p3 I; f W, a* w. r
0 t: [- M* N. \$ O: s2 b+ I
. e8 y" G; R* N! v3 Z$ R* P
$this->set_savename($this->savename);+ z" C% E5 Z+ P, E1 a3 ]% l
& o9 x$ v r0 V4 |8 M
! ^( {- o/ R7 K4 T6 ]# g
! @9 {. |- q0 M( ? 5 M4 g! E9 m) k6 |" R
% Q; g+ u$ H( y: g
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);8 F. d) h6 s- m% ]
" L1 l- z4 ~! T$ b
. A' y% `8 Z4 @- b
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
1 d$ _: Y* ^, e 2 X* b5 z7 W2 a2 s! U. }
4 a% P0 X$ J9 Y, g/ T$ D0 f
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
" u( @4 q! o7 x7 T2 `& d9 y0 h2 s4 L
1 W4 g' x1 }+ n+ [* p+ r
# |4 r7 c6 h& S7 {, k5 u $ J4 x) r, \" r2 |' c
; f6 v A) }1 i/ V: [
3 e# z1 y3 O: H! v+ I $this->image = $this->is_image();
+ K1 j; }2 v4 C$ i1 ~: q& z$ q' p! m; b
7 B4 k0 H$ O+ r& x# O2 h: Q: O
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
5 T% d& u8 H. ?6 e8 J# {
; s" r2 f. Y+ a% p1 I+ E' w4 _! \ h
return true;1 @4 j( Z5 x; [
: f& O1 X2 d7 o9 Z' m3 h7 E3 Y0 |: `1 N
: H* r! @8 e0 s0 \. Q( G }}1 P. P; P& y- b5 E3 z8 y9 ]
% ?( d3 l. W3 I
1 j& g/ O. j7 k, N1 `% q 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:; C/ D0 `3 E2 \6 n* W9 l; U
5 r3 K2 m# P# e9 J/ a0 M, {
4 w1 c$ ]- R6 E' G+ }
<?php# J8 x/ f, U' z, @) Q
' L; d: D/ `) [' Q1 ~ [ k& j7 [. t3 V$ C& H' h3 d) M
function is_allow() {
& {" ?7 c' o- Y
M! p" [3 t& X$ p/ W* D3 y
( F+ r6 d) L* n+ W) r; M1 u( P if(!$this->fileformat) return false;
6 M7 [8 A7 r0 g: b* q* N
4 a1 {8 P: U: U& @6 v$ ^
# O1 t) G: w, `3 y* [: U if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
2 w) Q& }8 \2 ~- z4 ]' ], }. k4 x
2 N8 \; q0 q7 F- y
: |$ Q5 @- U+ d 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;
* K- z$ I: t; q: `: s7 t
6 P+ @/ D/ h0 X, b6 ^/ b$ a, h+ K: t, L+ J2 b% P, t: f% W- C
return true;
, t) ?4 g7 q3 l( `8 r5 T, ?2 T* u
/ M; P+ |: A8 W" I6 U* E* J0 l& Z5 Z( ]% w+ {$ f' x1 |( Z
}( @2 @) J3 D7 g, w
/ B0 u/ M- x3 e! H
% V% b1 j7 Z0 L* J- [ 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
+ ]2 D. G* {6 U& C9 h 9 n) G7 q" `/ ?7 W
- c/ B' e; v! w9 w( m 接着会进行真正的保存。通过$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文件。
; E% {0 I. M: b( i! l! [. | 6 q3 [6 C+ f: M( C8 m/ m
" m" ^( j9 u6 [ 漏洞利用
/ E3 a. n {3 V9 c, Y9 p, w$ y2 L+ x$ [9 y+ O( n
7 B6 W4 y; w0 v1 {4 A5 W
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。6 K7 j4 b# F! b7 f* \- {
2 R: p7 b \! G4 b. [
! f9 J( g. D/ Y; n! I/ o: W" }
H2 _9 A$ p( g4 b6 d9 ^+ E
4 @7 K% ?* @3 z% g: p" v o' h4 N) ~$ o/ Z' d8 n& J6 e- C, O8 y
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
4 @7 `& R; d7 a! \0 U0 M6 h& o
; z( w! I% a% z2 E) v% }* N; X' J; c: l& s U# h
不过实际利用上会有一定的限制。8 l; [0 M5 o+ h; ^; l! D H8 ~
( I1 O, J* G2 P' p" M$ ?! h P
- p' R) p0 d( G 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。. A, W/ ?: x" \* P
; {( f! e7 k2 t+ m. h' v
( b; z- o6 Q& M% G! s0 d0 Y* ] $ _1 B* N: F) C. k! c. T4 A9 Y
; U0 Y1 V$ n3 e4 I. t6 {. ^+ z2 O
2 u& F1 I& ]. @8 O3 n/ F1 C
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
3 ?8 N8 V. p( R
$ O. ]! Z; g, x* e* Y5 l- n1 E4 i8 k# N+ h1 c! b
省略...$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]);省略...
! o( k5 N( e) y2 X' @ # F/ ^- c5 v6 a/ k2 A. o" J
& L. _5 d9 S5 a; u6 H: c. M
因此要利用成功就需要条件竞争了。
1 W: i1 H; y' o( B 3 b$ O6 U) n& c# l" ?* _
' @+ f3 R" h, X" Z R
补丁分析
' j- R$ H+ a" m' m/ N1 V# i% _8 d0 e/ R
3 ]6 G: O e( t: _7 n" s + N8 p, ]* v& D0 Q2 i
# w. E, z! {: f. l. _; G" f% g& K0 H8 I6 b, U6 W
在upload的一开始,就进行一次后缀名的检查。其中is_image如下: Q3 c+ \. o# L* t! {7 ?1 [
9 [0 |. H: z1 b* K# z' ^* E4 c
# Q, d- S/ Y5 E0 w0 G+ e/ ]$ t0 g function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
4 u, w! `- g3 b5 b5 u
, W8 n% u$ |2 `% i
, G3 k% z8 ]2 ~' b * N4 D9 f. _, {9 G- O. h
0 q! G N- V' V
2 u9 }# g8 U% H- {
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
F" m4 M) {- y8 e' E9 \! X 3 S1 y2 Y) o! x7 ~, X# g
, N) N' m/ k( [$ ?7 H
在is_allow()中增加对$this->savename的二次检查。9 N' N% w/ F2 `4 a: |
) ?: T" }+ B* F; ~* z8 c! D, o- o7 O9 W0 I7 D1 c) H
最后
: I8 d3 N6 _* R" a# i
3 Z+ _ p% Y `2 G) N) p2 P5 ?9 f" s& c
嘛,祝各位大师傅中秋快乐!5 u6 M" c; T: ]5 p, W
8 F% [9 L _+ m! G" w& `2 m8 T
* g& O) K3 [0 a9 V6 z9 \2 c7 [8 S
1 s- ~) u( T0 t. x3 O
$ J7 f1 K# c. ]- t
|