8 ?4 i) A- k: J0 g9 \7 R7 `% [: l# N0 q2 x; C, a
9 U# \8 N# V& Y8 @* q, J4 a6 X
; `: I5 w. Z; e9 r* t1 V
前言
2 Y0 L+ r% \, O! r! D% c# D4 r- {1 p% t# m
( s% {0 a2 h+ r- z 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。0 S& n' F2 ?0 J5 X
9 U v( p1 [4 i' ~- O o' K
+ `0 D% {# K: S9 _% V: f* u3 }
; H) ~5 M( V) @' ]' M
0 a, c! C# B" x& N/ Y$ k
- i+ |3 o( z5 O 漏洞分析
) U+ C! R& J) |1 k3 u U: f& y" x, ?2 j. k( A1 s1 g
" [& g ~$ A' B 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:, ?! I8 U4 U( `' i; m
- i; {# L8 I+ M" X8 w2 g0 g, _, B9 p' F' v' f* F: Y# i* Y
4 a& D0 v) W) S- S
$ ~; p! ?! E$ B- X5 r
E8 U2 n$ t/ @ ?6 P3 T7 E% k
对应着avatar.inc.php代码如下:
7 p5 y' g) T9 F
( w- z1 H& F/ \$ {- D% p, v Z% M* a- B. \& L5 D) L
<?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) {
. F0 }$ n" r5 q; T& J / L0 W/ _( B( `3 o1 z0 k- j
# z4 ~, \* c# N: e( n6 A case 'upload':4 J) V6 K' o$ k/ n/ O
6 k0 y" |! E L( ?( \( E
$ \- V9 h; K* t& L- ^! i5 W& p
if(!$_FILES['file']['size']) {
" E; |4 O) R- X# ~8 ~/ z% Y7 I 4 G7 G2 l* X$ u- F
) o0 k- M ?* C! W0 W5 o if($DT_PC) dheader('?action=html&reload='.$DT_TIME);" Z9 |6 `+ x" M! ^
9 U9 i5 P' F) E# _2 O7 K9 K: E5 C e' M; a6 T& s8 O& e
exit('{"error":1,"message":"Error FILE"}');: B2 Y; b# x& `, p$ J& T- k
* j- D% D9 c, }; o1 S4 A) e* V( G6 I) z" J( f- }
}
8 u5 x. F. |5 o8 H% X9 e
* D% c' o; U: C6 f# P+ x" t$ C+ l
8 f! W: L9 B0 O& F9 x require DT_ROOT.'/include/upload.class.php';
9 g8 y- j, L1 u# ^/ f
; O/ [/ v/ S9 ?7 B7 X
/ b) q* c2 |9 d4 V ( d* l0 m* Z+ U- i+ q: O+ E( W
' d" d! K/ b( S) R2 `% M
9 O( U- O3 s% X! a/ h( h
$ext = file_ext($_FILES['file']['name']);# u' R/ w: Q& p6 O( p; M/ {, w
$ S& V9 [( a: A
" W) V% |) N: }/ n3 u: ~
$name = 'avatar'.$_userid.'.'.$ext;
- Y8 i3 ]; j; ?' B( l - C6 I5 N4 t; e* p5 `# [. ^& d' n1 x
8 L5 ^) V% T* R* f, j! w
$file = DT_ROOT.'/file/temp/'.$name;
& J( h; O$ S: j. t* b$ g $ Y f; v4 S& f( B" a- a
% h$ k1 k" f4 S6 d
/ x3 o) t/ v' f, z
- f: W" ]+ K( x' @' u9 u# I: k& {7 O$ \
if(is_file($file)) file_del($file);
6 {( }+ |- y! W y' d& X
0 n6 s3 v/ G+ M- o+ `. {
# ]6 p) W1 ]2 K) K $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');6 E: F9 Y' ^1 u. z: G
+ y$ n1 \0 }7 }0 o. X% U. P
2 y0 Q2 w& Q. @+ v- U& P5 I
: `% P: c5 ?8 g* G$ g
8 T0 j8 ^1 g9 x) w" y2 R- J7 [& }: I4 B/ f# f Y. s
$upload->adduserid = false;% m' }, \; U: s
3 _/ t |7 n6 d6 l+ G6 d$ u& A4 z0 `8 p- b2 A; i4 o" P
/ Q& U( b! C0 ?" h; L6 h
# Z2 i" G% s' {+ N. _# J# q7 Z( q1 o+ R4 @) C5 t# w
if($upload->save()) {! x* _& _0 l4 h; i- X9 Z* b7 s3 A& [
" [/ W9 n/ h" a5 Y: E% D. a
, ]) f) p# d9 o% r5 V+ d
...
! h; c# G1 o* A0 {, d6 u* q
$ H% h3 j S5 |' v! B" ~ m8 N3 h# w" x4 E% U1 a
} else { g, c. L- S4 o0 U' |3 y! O
4 U0 O. A& g9 N+ i
- p% o6 ^. e) R$ \8 p: G ...! G: ~; U2 @/ {' r. x4 Y' d" L. @
9 e( _ J4 r9 }. q( G' {% V# Z3 N
$ g7 ?' A2 q; x) e5 X6 @ }
( c# e* X3 ?# N, Q( e5 J 7 `+ U8 k( i0 R; R8 z, S
8 ]- x& i% ]3 m E- a$ Z% W) Q* e
break;6 V9 h) ?7 ~* D% I( x: S8 u' D6 U
1 X) K% ^, w, m3 ^# O$ z( m6 C0 @# b
6 Y7 v, T& z2 w" p/ T 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
o* Y4 B' i+ y5 m; `( D
5 R1 S7 @) ]! t0 H( V' ^3 q7 b. O: `4 n+ q* I! e5 A
upload对象构造函数如下,include/upload.class.php:25:% H5 S/ y/ a& r
6 B% t: [- |; L! G& U. D' L" a
; \' q+ J9 |. n9 ^8 N- ~ <?phpclass upload {
+ p$ g, g/ P1 E1 f K4 k' E : c# e! a) e. [: n* [; g
/ W1 l/ L( M7 z4 y/ d% v. [, Q
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
4 ~' a6 M, H' C- P. {0 [ ; \+ ]4 J. H' y( M1 p% P3 |
- m1 e- T; |3 l N/ J" a global $DT, $_userid;/ ~2 w. [5 B. [9 G% G
0 m% N Z/ P- J. w! y- y N+ o* s4 N1 e- V) O# C- g m
foreach($_file as $file) {: z8 U: f% d6 \: J$ c! N
# V6 ~$ G# G% e/ [: _- A+ z0 d9 m7 D+ n* h5 A& T5 b8 k& S/ q' [
$this->file = $file['tmp_name'];
( y4 S" F9 ]3 L3 |( F& d2 m ) D- f1 m* `+ v. g7 w; Y! o4 s% E
3 g' P( b. Q( V4 x# ]/ w' ^
$this->file_name = $file['name'];
7 ~7 H& a- M, W1 l2 n, ]
) G! A; I6 _7 A6 ~$ c6 g3 e, J8 m) B: s y
$this->file_size = $file['size'];
# F* ?7 e7 `6 f2 J: z. Z7 v ' p6 f3 x% d0 x7 ^
! a. W. N+ q9 q
$this->file_type = $file['type'];9 j* i T8 \6 Q& ~4 y8 R
2 X3 b$ j: G2 i3 b' d j- P# c; Y2 V5 A& F5 s2 s Q! V
$this->file_error = $file['error'];
0 `* S7 S( P! ?7 J4 P4 a# c : a3 d! q _* f# }
! }, @1 k# }% G3 ?+ B' `
' k3 z0 }2 M4 J5 p
2 \ s% P! J& {: M8 d3 i
x: `$ m) s' Q2 a }
9 v* g' `' ?7 c% I H+ n2 w: T7 c % ]( }+ l. ?% ~: ?1 |
% ]2 ]9 k5 _# Y+ p
$this->userid = $_userid;
: E7 ~- V3 n! h 0 g, {; Q0 ]* W* O
" A, x) w0 |0 x/ c7 L. @
$this->ext = file_ext($this->file_name);
4 R! s% z1 p5 u9 S. l8 j
# s& l1 D0 I$ a* K* S7 L7 G+ Z3 o, m6 G4 ^ u" e7 r2 \& }- L
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
3 K5 _ d6 M6 x7 W y
+ [1 B/ y" ]5 x* @# L. \4 p$ T7 @9 K/ J! |# v" g! G
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
: a3 r6 n4 q! d. R5 ]" e P
) _. q9 u m' _+ }6 w, M ^ Y
^2 U9 C4 i$ `% D6 M $this->savepath = $savepath;9 J. Z2 W( o9 k$ ?" `
3 t" j2 b. y# X# w |
1 G" u' S, g8 E! @0 N $this->savename = $savename;0 f8 Q7 l! A: z C2 L
& w2 o% r* g3 _/ q1 m/ W8 {1 e
: t7 Q0 ]4 Y( l/ @- u9 w }}
1 N: \$ Q, a) t9 s. O! k6 {
+ u) Y9 S3 e$ W: F1 A; [5 d4 E( O4 S6 C0 x" m$ _
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
* b: P8 x& h/ A- M% s. T & q% X4 D, M: h. Q( V
) \0 g- `. ~% _! S1 N" J" @ 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ; Y- i- u6 x; B5 t0 a
$ Y( E2 X0 R9 o' W+ H" U% |8 L
& c0 Q7 n6 c! W, ?4 `. v+ \ $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.php6 D4 q* f- u- R- b, N8 u% ~" j
9 J! j% O' q' n+ S/ O, u4 w( c: i+ p( Y0 X3 @1 ~4 Q) k4 E. Q
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
; F) s, h% Q/ C9 \( }( N- S' H
& ?+ T2 d6 B7 G% k- W! r& n9 o: Q5 Y* T5 W! I! \
5 F: V. S& l6 c9 s i a5 K2 z
% Z8 T& m4 Y" n
8 C( g) h& j7 L! ]7 Z 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:% Q: U7 \1 e( \( F) z
8 j) b, d) l2 z% P2 a9 M+ S. H
" V. X, Z; h9 v4 O7 Q/ ` <?phpclass upload {( g9 }% t/ u, M+ K" d2 `% I
! Q0 A0 Z' i/ z& ^0 ?/ V* B- Z4 Y0 S6 f
function save() {
- ?! r) l( f2 ^1 N# P0 I) e0 G/ H8 v+ ^ 7 I0 _% G9 A! l7 T" Q
+ H" [6 Y% U* @4 n: K% c. k include load('include.lang');
8 v6 t$ Z2 x8 M. X8 E
# r, t$ |' ~3 c2 F' h/ N* M3 K$ P% [& w& O3 `
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
6 V+ B& I5 z2 O4 Z
" N7 u* F* r* ~& I6 @0 ~
9 `' V3 H! B+ G7 j- u
" a5 m9 @+ E: i' E6 u: d5 U : u8 {# i- R; a: L* W, R; k
- N4 K7 K$ y, p if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');' a" I) Y1 G- h8 e2 M
( G0 z, Q f5 ?( _- s. e! ]2 J! l6 f7 a9 w; t
' O$ v( z9 E# u0 L. a3 ~( @7 }
. n) q* E& n3 Z" A$ P" a3 f( K+ T3 x( G/ X) j
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
* q% N6 R, J& x
1 b6 J8 s+ r" {5 u7 ?; m
$ B" s/ z4 M' i( j# p " e4 B+ ^0 n6 e |
2 H8 J* S, h. k* e9 b; f
$ L" |$ P7 i% x6 J7 H9 K1 Q) N $this->set_savepath($this->savepath);
# n. D+ x9 ?& L$ v5 {& m# w0 g3 O
. U5 X6 S/ ]8 X9 W: u0 j( W% ]% E
$this->set_savename($this->savename);
1 y) \- _6 `# B Q * t$ p! v4 g; H! j1 o0 j
" W+ }% M2 s% C1 a1 y
( T# `4 A5 Q7 g, ~" U- ` . P2 @- I) s% @ e% k
4 x& f! Q! o) Q0 W- A+ m7 T if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
8 }: ]3 z6 g4 @
$ K2 D s* C- l' b! h
# \/ y; F* M8 X3 ?( Z+ C if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
) d _9 N% ?8 c7 X 9 l2 x% D+ ` i- R' v# I& c0 O* [4 k
* a* y3 E# k+ u if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
2 I- ^$ N7 E2 y
% i2 O6 h8 D( R
* E& [# J* s5 o) ~# H* x, h
4 M1 I. u3 C6 I, i! B
) g" g; O1 e Q& c8 Q
4 k& y) B- w' B2 H. u $this->image = $this->is_image();
9 g" ?1 D9 J5 k) K5 o
6 O& \$ O( A9 B' H) N# d1 q( T. B; T4 v3 }* k+ N& `9 P
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);9 M. G& T0 F- ^* Y0 \5 R. V- ]
' B' p" G7 g* e: L; x
2 W( V1 {- i) D0 ~, W8 R7 | return true;
2 B3 \2 y; Y& O
1 {: o- C" P# c! O% o
) S$ U' z3 [" X% Q2 Y1 K }}
3 h! L' A9 S: x# f# a( J! `' m 6 U( l9 @/ ^. n1 z" r
! A1 [& `8 i, A
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:* o: z8 f% i1 V) d' {1 p9 s
' l3 [% A) G1 d8 t- V+ b% k# a2 G! j8 L, g% q: o
<?php2 k4 ^ x' l- b/ T
/ C7 W0 S; N" ~/ ~( h- r, [" b/ U; X# I
function is_allow() {& v- s5 j% r. `3 I4 Q
% p2 I' y# n3 {) I& O' J) ^
4 R4 D% R5 t* Y8 Q8 Z0 Z" Q if(!$this->fileformat) return false;2 k1 M* H4 d) F2 c. B( ?' I
3 j/ c* l5 L P: d$ t& m9 c: L, K' P" e/ }# g- k
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
# R' \% w5 L: K2 E4 D3 r: G 1 ?: p; A' R+ ^/ u E( T
8 I) y! {0 Z0 `5 x/ j4 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;
/ ?, c. p) X u- @ " {8 b, r. e& n( F6 g1 C0 r
2 u6 d. p- y, q0 ^2 F9 f; o4 k2 L return true;( F0 _* Z( E4 n2 h
: b# ^0 D* F' ^) i
; h2 ?+ Z M3 W9 g! H4 k. u* N }
/ f- f4 K/ ^& i+ P0 |1 f& g
' T, x7 ]6 P& D- `4 K& ]9 ]9 A+ Z' w, O$ P! j- k6 N
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。+ }3 {/ e9 h( e6 K ^
4 z9 Q' a' P( N2 I! r
/ N' s4 o8 ]7 C# 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文件。8 R9 n4 d$ l6 k) a! o2 P% E
5 m4 X' ?; u$ |9 Z n$ |
7 S. L0 O; k v: N( \" G& X) R
漏洞利用( ~6 i; e& s1 H- r. o# B' d; Y
: e; l, {8 O; \3 y5 K) ]2 E
; C* o6 Q# F& N. D
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
D3 y/ E8 c( K: y , O: Y0 d: O+ M
2 q, X9 b* g$ } |% G. E: @
6 a) c0 ?0 M- E: M( r/ b/ `
% `5 |4 M+ X3 v* K A% O& h
" r4 L( z" m1 p* Z" V 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
5 w* J5 [4 h9 V! ~- Z
4 @- u( }/ C2 G1 e, ?; L1 u2 o9 k, I
不过实际利用上会有一定的限制。/ ?+ h, F0 t. [7 X1 }9 @' y
6 S% Y# o, C) Z5 v0 e1 ^* f
5 a* z3 U# {0 ^, p' {5 ] 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
/ h6 N4 @( X1 `+ n2 y- U
* Y& q/ s" K; Y8 L6 S0 \
6 X' M; a9 \/ q* ?5 K' p
; y; O3 D2 C9 R : u! B) n0 \( Y
/ Y3 S4 S2 K0 d) \% ~# M
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:) G6 i5 t" y- q2 m9 y! I! x+ b8 J
# Q7 ^. z* ~# Y* ~; r
4 D2 X. G+ V6 C. ^/ M) Z+ F5 } 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]);省略...
0 G7 R+ g* b( B, e) i
6 s% W$ k. w4 e7 N! e3 j* s( Z# |0 O: o7 y* ?4 O. T2 T
因此要利用成功就需要条件竞争了。. f( G8 M0 |& U5 y5 U( ~ Y
, i4 m) O, K" s% B0 }! B W
( g% ?' \% S8 o$ b
补丁分析
! H# b. w* Q8 d3 B/ ~- h8 H
: n- b5 W! ~" u) }0 S3 c
* f. y+ G' a1 [. u; L1 @4 l 3 }- J7 m! U! P0 C7 z
C z5 k6 k8 s* }
! o6 F- Y! R; C) V, Y) D 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
; o2 a% p& _8 _1 N; j( C0 [* }6 N
' y6 y6 Z+ y3 h" [1 p% [% d; @* X* U" a8 P- b% n
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}) n. J1 C% ^( ]0 [# D
9 m! Z: w4 U1 |) h& [6 `- ?4 D
5 \8 D& n% i. R: d7 R1 D : n J f9 z4 H3 X6 r
" H q0 l- I8 R0 l6 U% ^7 A
8 H9 |2 h8 V) ]: A2 m0 ?
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。% t; o; y' s( {$ `
. n1 [6 s" s; e9 T7 C" M2 ~* v1 l( Z
7 f! Y+ r+ S1 r; Y7 A5 U
在is_allow()中增加对$this->savename的二次检查。' _1 Z% `6 T) }& I. U* s$ ?2 ]
4 c i+ A7 |% P& c: W
. S: b8 H( ?) I! ]$ [, h' N 最后
! M1 M! d# z" W" r7 F1 }4 c, m
0 h N6 i4 ]! x1 g5 l6 {8 q% u" [8 v8 q) F& ]
嘛,祝各位大师傅中秋快乐!# e. ~: |" \3 O0 z6 G+ o
* }6 U- {* H! n
3 M6 k8 \5 ^2 A
/ D0 W0 s' m( {" X+ P% y
) S6 @6 |" ]; `, ]
|