' E, Q! ]+ t8 G4 g r; s
1 L7 M/ x. N/ n4 e5 n7 L- d- o* Y
# m C! d! p6 U% `7 L. j5 D
3 V0 }3 P: ~, m1 A4 \1 y9 C 前言8 m( ]' n) l# s/ C$ l
# D( @& n8 X- T$ _; c& n6 q0 P5 ]# P, I% |
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
) _+ d( r) G+ W1 l# Y
$ u b3 g" p) N
O, [9 x& x) ~8 L2 W
" g- Y' H; }; l; B1 t. B 4 x; v2 g8 @. X% ?
% y3 p" G9 s" Z
漏洞分析; ^+ J+ J2 o5 k3 \' B/ u
1 e$ L! W' s$ x4 W% K7 B0 l O6 ?! \! m
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:: E, q7 z9 z; E/ n
- `, j; K$ n+ @; _. E H( j
- e" [$ U% J. v2 c
1 r' e0 y; ~8 D0 _, X/ e& a
. e3 G& w2 g0 M# O" a: L! |
, ^" ~% u7 V9 S; F/ R 对应着avatar.inc.php代码如下:$ A5 m9 u8 K- `7 ?
H9 c& ?: `" `# W4 O/ a+ a4 @6 K
( c: S8 j# t3 ?& O1 B5 h' h. M" c
<?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) {. N, F9 C0 D5 e9 I
; Q; r0 q& W0 G( E, V1 n
; }! K! ~- _- @ case 'upload':
' w% ^* }0 E$ B; @+ K# B" y % w2 X, S4 I3 R1 O& C, L5 ]
) V3 g9 B$ x# e' F# @
if(!$_FILES['file']['size']) {
3 K1 S9 p6 Z( Y
, o. d1 t! @* d+ k4 v
$ v) n( d( ]; Q3 z2 ]( E$ l4 c if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
# x! w6 U& o2 @3 h/ [0 T5 x1 Z 9 E( _" V. ~+ }0 f' D
( C# [/ {3 t& v l& N3 p2 @ exit('{"error":1,"message":"Error FILE"}');( W9 @- C' K/ H7 C5 E: ~
2 }4 I$ x, d- \
$ T. Z3 s: C1 ^1 L+ s6 }9 t }
5 @ d' S4 m% ~; s0 f+ M: {+ K 0 V1 ]# h4 u" I' i n2 F
8 S4 v, p2 K# l% N" P! c$ O
require DT_ROOT.'/include/upload.class.php';
4 q0 n6 c/ d- T% `% p) j) h/ T' k 6 n8 d8 h6 R3 C1 t5 I3 x) V) Y* D
" I# I. Z1 T: B) ?
- O' o3 ^6 x1 B" p6 V
3 z3 y8 F* z4 S) e( z S2 X
1 p) C: O& q# F" ^% O: Z9 r
$ext = file_ext($_FILES['file']['name']);
* L7 T7 }1 |' g" ~; m5 Z7 K" u
# z# L4 g% q9 Y
, u* Z& Y$ t6 {+ D $name = 'avatar'.$_userid.'.'.$ext;5 Q; [) G6 w- B6 d
$ V% c: Z/ x2 `" u$ E1 j- ^+ X
9 k& N" e. \* n! ^( O* W $file = DT_ROOT.'/file/temp/'.$name;
E1 M+ o8 j M' D7 G, A4 w E& m X" D+ W' f3 z$ b
2 o z7 ]6 x7 b+ c6 o ; e; Y7 z7 i0 Z, i1 T
& u2 S: R0 H5 p# e7 P3 V
: ^0 J2 b# x# @% d- i1 Q if(is_file($file)) file_del($file);! a! C2 x) U* U3 ^) F1 z0 {* o
% {1 z( }2 Q+ r. y7 E( B# e
9 q' L8 }8 a) F( a3 H- [8 j$ Q6 S $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');1 L. \4 T4 q: b5 W5 c( G
0 o0 t+ [4 h3 @# Z2 I. r/ {
% r9 q: T1 N! t* W( N0 o 2 g9 t3 M6 X9 J5 W; C, D' _
- x$ q) x8 h1 G! K$ L* k
" |& E' v' ]* Q, h% z
$upload->adduserid = false;0 g; I1 ^; C- D s
+ I4 f1 k7 i3 w- H- I
+ L! u8 [7 [ k; D" F$ V
( A/ [3 ]4 K2 y7 R2 ?7 O2 u! f
) X5 u( d# ?- R+ Z2 a4 v
8 ]5 M/ ]3 y$ n/ O# k if($upload->save()) {
# T! e! l# X4 j8 \3 P
9 q, k! P; M1 ]4 c) l" F( ^+ h- e
% e$ M \3 F: X1 ] ...4 o# u5 k$ A+ S6 q; c/ L- t
& ~# ~, ?( W& @) j' Z: U5 v$ Y/ }% p% k) C: `6 N: M1 e: ]
} else {
3 F2 X1 G5 J6 j; `) B* R- f
* c1 S" |6 Y1 }2 ~4 e" N ]) r, L. t; i7 q ^* V# i
...
7 h- O# n+ S# Y! T- U
8 d( [% \' U" C( u% W3 e& w d# A* h
}
. I+ b- w2 }- [6 t' v
% A9 H/ ~/ b( E8 [ O$ S1 }6 Q
. x5 p% c1 r# @! S- }8 M6 O. j5 N2 O7 R break; z2 F1 O3 I" U
N& |) h, s" _9 ^) o- [9 e) t
0 f( `/ l" r; V+ A
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。& s: x d/ d+ d/ ?7 q1 h. x4 s
8 a# c( {9 f/ b' y; ^9 V( C7 C! @( [
& e+ Q) v/ D1 L( v* ^2 ` upload对象构造函数如下,include/upload.class.php:25:
# m# s1 W0 G& s
" j% T6 A2 ^6 l7 V! r( \2 V1 }
% F- b8 w- D+ M! T <?phpclass upload {
/ g& E7 Q' q9 t r0 K
: g, [& i: k1 q$ g& A+ r: S' r8 k# v; E9 U% V4 ]7 |0 m! O4 _
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
+ P* l/ J7 K: I7 Z' O4 x4 T( e% g0 H" G
% W; {0 ~; k# B/ w) W" y& T8 ]/ p( x3 h
global $DT, $_userid;* k$ S. h8 ]6 K; [( s$ g+ w+ o
' z! J# i+ \- J* T! q5 I- r$ X
( u1 [; U. J5 m k5 Z& W foreach($_file as $file) {+ h4 o0 T2 B6 A$ O- I, w
1 l* ?2 w8 i, d/ R a# [/ }! B% ]
# g3 d: m$ _1 `" g6 |' G" ` $this->file = $file['tmp_name'];
/ j. U3 v" ]% Q$ U5 t3 A
7 R! m$ L, C* v7 p" w% o6 j8 [3 {6 y
$this->file_name = $file['name'];* `- c; Z& j1 h4 R
+ v' m, b3 B* L. {
. G, X- F. \4 x; A
$this->file_size = $file['size'];
2 @1 V9 J8 c* ^2 _; Z8 M
, |" X& @9 M. V" W# _+ u9 N' N- c% r6 s
$this->file_type = $file['type'];0 R! p) h/ }+ S
: [; l4 r& L; |5 O+ O
/ p2 j3 S1 {. q$ e) @1 P* _ $this->file_error = $file['error'];
% _4 O/ R; G- R0 X- m* f" b
& w* U( d2 i' f
, Z( J7 @0 `+ t1 v8 i 9 t: D- H( t4 o1 Y$ U5 z# X
6 i4 C9 G2 G# b" H
3 E# ^% g! g8 i" A/ A
}5 k: }& E; `% N' w6 X% \8 f
+ x( X) Y+ B$ [. C4 C- }' H7 i }$ m0 _
* D/ k7 k% x7 u4 w2 {1 A $this->userid = $_userid;( {; g# c* ?. T/ C Z* d8 z
6 X+ b6 ^: w# q y g' |
% Z! i0 w g w( _
$this->ext = file_ext($this->file_name);1 f4 y( f9 {# P$ v3 @, o. K
4 N2 X5 S+ b2 N" j" W# }5 c) i* X& ]% f# x4 ?, a) o: F9 r! u& f
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
D# O: S* K( E: H+ Y8 t# q: m, P
6 k& m4 Z' v) l3 Y, b! C: r& F* ~% t5 _- _ @+ k+ u' T$ i0 |
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;4 _. z* ~. e+ ^3 a5 x
' P; V( j" S; @: N: o% d
6 p+ z8 G9 R0 x5 [! s: g $this->savepath = $savepath;
+ U1 Q2 v2 a' ~1 T! _ 8 M9 R2 H. _- z: \& B x
% h7 P- }: j4 w $this->savename = $savename;) ], v: s# u7 w' B& g: |0 y
6 u: ?3 U; g) e, Q3 a+ p+ T! [
% F9 J* f' J A* @4 g }}
" y2 s2 V% I9 \
. O6 M/ Y$ r g. Y
9 ^( }- \7 j9 ^ 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
8 A/ B+ C9 `6 p1 U( k2 B) b: k 8 T$ x3 k1 E: x
; c1 i1 W3 m u7 L. U" H
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
$ w/ y C3 ?6 g9 k& \$ Y4 H; G1 |
' }3 u: k; B2 P
1 E) ?/ H' c- q0 v: u" S $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.php2 _: Z, R# g) C7 p
) R/ h6 ^& [1 c! H6 O; c8 e+ E; E0 H; n( n+ V+ v" Z9 `
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:9 n" Z6 x5 d: |6 e
6 ]: g0 W" P6 \, M! d p
; A: f0 F! d& v- @ k1 U5 z8 p2 U1 D ) a0 h4 ^8 b3 g
& m* Q3 O. u \- _7 V' ?: I0 C' |
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
) C* p u# N, M % m) Z: z; k8 j$ |
- n+ n! X8 Q' T" F: s
<?phpclass upload {
+ E: @% J2 p% v1 l, h( L6 U0 k
3 V6 k8 D3 p) Z
& Z. s% }/ J4 S! n# a! J: P# g function save() {' \7 _/ K6 U. N H# s: }# |
- {* M0 ?0 P0 D0 k9 |
2 R4 T0 \4 z) p) k# q! i2 C5 S include load('include.lang');8 P" m+ Z# ?. l% N; J
7 y. W2 }* ^& @/ m0 I) j# L z3 `& m6 a' L3 v. f+ }9 h
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');9 G) J7 V5 Y+ l$ W# s9 C
8 M. b& a5 F8 q, M! O9 N
+ O% b: @$ z$ N1 Q4 n 7 @1 y+ ^7 a, W9 G4 U% o1 i L2 R, }
0 x) X" a4 W s& D3 }9 D
) `1 q6 J& l- A4 S8 d/ S8 N if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');4 L/ @2 m$ T& G h% r0 I7 K* Z
7 C: L9 x- n( d( {4 f/ e! T6 k# c
# X" f4 L7 _$ ~6 C + g3 K* E3 s4 s4 @+ U; z! I
- G( Y5 e' L8 U
" c6 r: H1 B( s7 V: q- z5 d7 J$ n; u
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);8 b9 B, g% P2 V9 ]! w
/ i" D8 \8 s$ r& B) |
, w& n! V, Q% x3 E( j; Y I
" l9 R/ {* x; u. I
' O1 d: |) r) }; ]
, A, k2 |0 g& |. L6 ^ $this->set_savepath($this->savepath);; D q" o' B$ T, ^: P& l( Z% E- I! j
J5 |4 g' F- l( |8 B, G( N1 F
# u9 `# {1 _7 P7 M# Y. c$ l $this->set_savename($this->savename);
- B' r$ s2 l. h2 j2 s0 A; o
2 K2 `7 `! l; C% V1 |$ d; A( V0 |$ e) i' s5 U' |. m& ~; ~% ?0 e
+ X B6 Z+ A! V1 H0 S
8 N9 `! W* |5 T+ c9 y# I+ ]4 b
' W0 z# Y7 \$ k: H; y! M if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
) M2 ]' W) y, e0 X
1 U# n' }4 k! Z1 ^, M) b
% a, l1 @2 v1 t- ? if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);+ n: i2 x! Y/ Q# J ~0 |
) }/ V, u0 O5 z2 \8 D* ^. B* } e
& l9 g' W+ h6 d$ M" ~# u
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
. J2 v& P: G# Y ) u* h# |' N I6 n& w0 |1 j
3 F8 p0 G2 _. `, E
( t* S$ q2 ~4 U( ~
# _0 ~5 [ H1 [* j5 E
% g0 ]: ?' [" y0 \ $this->image = $this->is_image();1 m7 C! h. r' d# n2 A4 L* E2 l" z
$ a9 c2 ^3 L) c$ Q
0 H, h& X2 J1 T4 ]1 ] if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);" V& z6 ^* C+ z1 w+ u
: P: F% P6 n- k2 g: U
, C. ?$ P/ T. S% A2 B. q
return true;
9 F6 `# |3 R e9 V2 w0 b3 X
$ S4 |% c! l9 O# u3 c |0 l) Q5 `& M4 r% a
}}8 Z8 m6 y# e2 X, y. }
8 i1 I3 M; D( Z( S, m0 K# D
" {) X' i, u" @% _ 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
. z& d% a) I# v; k 9 Q# s) V6 _- n' l- l
~' _# o4 F6 o. w/ ^$ b
<?php' p! X( l( {# M) b
" y3 v D. ^$ F, k( ~. f6 i
c0 C# p3 J% u2 d, r4 {9 u8 k9 Z
function is_allow() {
9 K1 M# @9 a) B5 L2 _5 I7 A
4 ^6 V: m1 }* r$ M: V
0 ~/ @5 j/ W- G- Y$ f' h5 { if(!$this->fileformat) return false;2 `9 n" R5 c2 ^9 m* H
: e4 b4 X3 ~' L0 Y: [0 L0 Y
6 M5 @* {# D8 b' T; `
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
) r8 T& r0 K7 r8 u7 a
4 D4 {: D! j, L6 J8 y6 z
$ ~5 s% F2 @& D; f# q/ _+ X 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;* j2 }: v* E& ` V$ b% }
! m! C/ V9 c& g
$ g+ @% T# k( n' j% a return true;* F# |. C& ]( ~5 f( L1 J
: u9 m- c/ ~: x9 N2 Z3 s
* H7 l5 t9 e6 y" X }5 }9 W5 Y, C- y& e* \ y: f. g
* T0 U$ _7 i# o% n/ i9 e* d/ y0 `) G1 x: p% w
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
, p2 n& x$ S* q
- `$ J5 o6 G' V" }6 |4 j/ j
7 V2 b# C. V) O 接着会进行真正的保存。通过$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文件。$ H: B( `% a0 b
& B. t8 g, O) i( @ M
2 h! {3 P9 h+ Y2 A0 \3 O; f ^) p
漏洞利用
5 L) t! p4 S+ G; h# v% I
# Q% @7 q1 K% |! R1 O+ B/ z7 O0 k. _1 Z9 s. ?
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。% y; H7 a) ?* r; b
9 ~& G. J. j; v" o5 F# A A% W& Q) |. s- ]3 u0 L8 c6 \7 x$ A9 Y
M! U8 u8 ` w* X' i
& H0 V, _ i3 d
6 C! k- H/ a& m! y5 u6 N- c: z, D
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
# i0 t$ d' b6 r+ c% d/ G, P' y, Z* i 6 n. X( ]) Y6 |* b: W, R
& [- k4 D+ E" s$ Y; J& S A
不过实际利用上会有一定的限制。
, G; `$ v: P$ V5 o. d7 n* v 0 X2 M9 {2 q% |, l0 t
$ Y" c' ]5 w6 I 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
! U) @# J5 H7 {8 M h+ l7 J Q: O& L+ h" {7 M; v( c
2 d, f6 C' R8 J
) t' |; C% L0 _- y) v3 V
) }# Z% i( x. z! B) u V: z0 V; _! s" p9 Y
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
2 k; g$ A9 W2 s, w - J' r* x& |' J# v4 ^
4 A4 e: t% V' F
省略...$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]);省略...
1 Q2 @3 ^3 s8 |/ d$ d. ` + X; X' `7 O/ O2 }, Y
$ F4 y' b( A/ s
因此要利用成功就需要条件竞争了。
u g, \ C4 v/ s& J, O 0 e- X$ B: d; _
; G# A/ J' Y+ p 补丁分析
: b. d& Z( |5 s! R# Z( E% y) D/ q, f! s) }/ B6 G& m; G
, A4 P! ~% t+ L2 m1 X) w : E' A& y. q. X
( c& h& Z1 L# S/ A2 P6 ]$ A2 Q4 Z& h2 R: L
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
, M' P2 P. i0 T6 g/ v # F7 Y% [# d( {* [# S
; A1 {$ M3 {- k8 C( G% w
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}) b; Q5 C9 r2 f
0 e, ?# c6 G1 Y4 i% b0 V
- |) y, {, K3 s7 V 3 Y! Q( i8 J# Q4 s
. s7 R+ R+ V) e. R3 @: y; a1 A; s5 }0 G2 T/ O( ~* e* c
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。. t5 g. W; f# s) P5 F. H
# i1 Y9 ?$ l {6 ^: V( ]; x
7 |3 \# F4 {! P- C# g u9 W 在is_allow()中增加对$this->savename的二次检查。
0 a) p( B( v; t1 a' Y$ n' c
1 O; v& B+ |, [
6 t2 e1 |/ M8 r6 H: P% ] 最后
$ e( f+ Q- d9 x
% W- j- V8 I6 E5 r6 s% r) c
' {, {- g7 y" a/ \( X6 r4 J8 h 嘛,祝各位大师傅中秋快乐!0 J# W# T+ e. |0 Q1 q8 _0 ]
7 L: w0 ~) v; D/ r7 T
5 G& {( n/ Y4 W0 l
/ z! m7 `4 S) n. Y4 k
$ Y/ s7 [5 V7 m5 p3 d
|