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