, l/ b# s8 |& L# l- B) Q2 a
% N. Z9 i. x2 b t" C3 z K9 i; {; ]+ b7 d& j3 i
* Y) P8 y6 Y$ E7 @5 |
前言, O0 E* @3 o" X' k1 W
/ m# `/ e2 ^0 y6 M" I: I1 D% d, h
% t$ |9 n2 ~$ Z/ E3 M 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
. d$ Z$ d) }4 m/ X
8 ? B' E4 V. j( x
( j: M% C W3 ?4 s
0 v3 ]$ w) e( P9 @ 7 i- R5 S7 g$ a7 {- V3 h0 D
$ ^* i, n! |/ N' k1 v1 p 漏洞分析) ^# f c$ r! D- l- k# }7 g' i
. E: t, G F' p H. g" H c8 u3 k5 b6 i, S+ q0 D
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
* Y% S: e- t& | C 5 T" S0 P9 P& y
2 e& a: i* t0 ]# ^& B" N5 M ; c8 o# V- D p* P1 l1 t
0 S7 H' q( N/ J7 s
) Y2 J5 F, Q" G' D
对应着avatar.inc.php代码如下:5 ?( B$ n' q& A, D8 G
; T! A% h% \' s& W1 ~- B( h# _: T$ B. 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) {
2 T* j0 {7 y, ` 3 u5 I) U( F/ O7 u
- [1 {: \- {( ]& `! D case 'upload':3 t7 J) R; }" B0 `0 ?7 w! z
, X4 V- w9 `- a) q0 i
; N; S+ I F: c1 T
if(!$_FILES['file']['size']) {( c' _7 [3 h9 C# p
, l# b2 h4 [2 p* U- e
- o" g0 [& ~& V" e' w2 R2 U if($DT_PC) dheader('?action=html&reload='.$DT_TIME);9 P) O+ p0 t2 C- ^1 L$ y
5 d5 p! w e0 q9 ?, L; p
* v/ J( a$ A0 O exit('{"error":1,"message":"Error FILE"}');, U. G( P- z6 j+ D0 N, W6 n# R& \/ G
6 q7 a# h! @9 o5 U
, U4 z, m6 _' i7 A5 P }2 `) T3 Z3 ~( ?$ S# e; v
" l9 N! M% [/ ~: u
2 g3 T* @: S5 L L' V: s require DT_ROOT.'/include/upload.class.php';
, e3 T& ^, a' c: U
7 j# {8 W9 }8 s3 G: u, F
5 ?* L. Y- _- ? & o; r! P+ Q. H; |4 T7 h
8 O m Q/ }5 L( W. k2 W
% [, F9 n+ s8 @5 Z; l* \( C% e
$ext = file_ext($_FILES['file']['name']);2 F% s# I6 [( B/ f4 C! M- `3 c
8 X; p* G* Z0 U' H
9 K0 u6 ~, r6 P/ b7 E# h
$name = 'avatar'.$_userid.'.'.$ext;- Q# m. c) v+ Z# t1 R8 _+ n( S7 r
) b" j P m# Y2 l9 l
+ X! [9 U+ P" h# R1 X $file = DT_ROOT.'/file/temp/'.$name;
7 v) V/ a( o5 @ . b& |1 [" f/ y* q+ F3 `" h" R
' N: W3 Z( P) Y. ~1 m
/ V8 `' K! u+ d- @* Z8 w
. |3 P, T' z- q! q. Z) F
# h% x/ V) M7 ^+ w1 ? if(is_file($file)) file_del($file);
; j6 z) w+ ?+ _ g+ Y1 L
4 t# S# d- Q, U& I; `8 t" m- G- r* ~( b, M7 n
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');/ w/ x& w2 A6 G6 H2 [3 ^
0 i6 q& G& e. t z8 s! O+ `0 t8 I9 |: [
) }, {) J# J% X
+ I' q+ u ^5 ~# |% C9 s0 b
* D9 T% n3 e- Z1 K0 i $upload->adduserid = false;
1 h+ d- m+ E5 G( X! G- B1 K
2 d; t( g8 j# n$ E9 u% |& a% J4 u$ y' l8 p$ ?
" T, i9 ~: O+ ?& _0 [
K+ {! s, O2 K; u1 G( ^( \
/ i* o) F- f7 }( [ if($upload->save()) {; K8 j$ o3 E1 m9 y& ` J
6 C9 B# _7 c+ t1 ?; ?6 [
/ R K& I$ E/ i- U4 z+ K ...
) d" G2 R6 J# T/ i# A , ^3 e9 x0 w/ n1 Y& i
$ U: L" X& D: `2 }/ j9 R
} else {
: s, A! H6 F% S/ v1 W7 L$ ]: v: z
8 b) C) n: V( _4 |2 O6 d3 @( w3 [$ |9 t8 N. ]+ Q! z* V
...
/ C& ?0 O6 G, W$ O- ?
P( @: v: C# ]$ h! E, @7 E5 L
# c9 m5 L; z" A0 v& K/ ] }
# l9 C/ h" O, }6 l0 l' r; m; m , N3 O: z |* `7 Q! u% ~* h' B) m% F
g: V' }: J) K6 T( X3 ?7 I& i
break;( k8 [$ L, \& u2 F+ N2 A" [' R
( h! t' n+ t- _& _% U/ V
0 X+ G3 x1 J- X! `+ ]2 ~" e6 E: c 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。& |5 L3 B9 e5 z1 g0 }
0 Y0 m, v6 P' f
l- q" \1 `# D( y, S+ s/ D
upload对象构造函数如下,include/upload.class.php:25:' J2 l% m) D# H; }& X
Q( f7 q/ `1 ?% s+ Q F
5 V" }! }; _5 k5 a7 ? <?phpclass upload {
7 `6 v. X. O9 s5 E/ v5 `% A$ k4 X
- a' K5 x$ X. B0 v3 B- U# Y( o8 J" D6 b) `1 {
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
4 t: N, |) ~. a* \( _# F / e! A j0 L) i: [2 @ {
$ P2 e' m% h9 \
global $DT, $_userid;, z7 g: c; I$ p3 t
, R1 v7 Q& x, _6 C" r% y% ~
. k5 U7 P+ L0 v$ L' D% Z foreach($_file as $file) {
+ D7 E# }; L; }+ N# }3 d9 t
2 a+ ~* R8 Z5 f1 m+ C; G. W1 i! M6 h( z7 E: c/ ~* s
$this->file = $file['tmp_name'];
8 L, R% P2 k# n2 M" N
/ G" u8 Q L- H( y' B! M) d& }! x" V5 [7 R/ y
$this->file_name = $file['name'];
) r) @; a8 c; O; v- K9 `, }
: I' G& s$ w4 o0 p+ v1 W
2 k0 W! o9 H5 z4 K. Q+ i $this->file_size = $file['size'];; `; W9 k1 y; {0 d Y; |) G
6 v9 X+ l9 _) a, [" @
' ~4 l& X, X6 c- N0 B* P4 H $this->file_type = $file['type'];
; s0 `: V* Q; |5 e, E 9 x* B4 l3 p8 c% ?
8 K9 q9 c) \$ |6 X
$this->file_error = $file['error'];
, O! l7 J8 O2 X: V; | " W. ]+ e9 y. u3 }# e: G" I
1 A% q0 N' G2 K5 ^5 z9 h $ }5 m- N' F: n: A; z2 V
( @: Y4 B! Z' N6 {0 M$ F( K, v
- ?, y4 `' ]% g, `, u' l6 a! d }
* t8 l6 |: ]$ y1 s9 a' e c 5 W) P# s+ |5 v5 i* ?3 q' b
/ t2 G$ v1 x3 Q& W' S/ M2 _
$this->userid = $_userid;
X& t* M; J+ [- v+ J( m; R
1 k) ?4 Y- l; g2 ~) A
' [5 S# F, p7 s9 E! W" X $this->ext = file_ext($this->file_name);
! i0 O$ z+ g! [( Q
, E7 }8 f5 Z* {( f% L* v( n( C1 C* z1 U( d. e' T
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
# d0 x7 @' J" s' e' W& p" o " Q1 a5 F, |8 G, M
* G& r7 k! U" ?- j0 k, J
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;' ?7 v0 p" e" Y; c) N7 u% {
( |9 U1 D: e8 _( k- w* o9 @: E# \
1 Z% i, c/ h4 y9 N $this->savepath = $savepath;
5 L. @2 [% v$ G2 O/ @ / D3 G6 d' Y p: o
) P- ~" i8 r, w# p4 q $this->savename = $savename;
* D5 y C& \2 ?4 T
; b- I% \! s7 U6 W# l. \% j* u: V/ M6 k J
}}" d6 u% T7 g* p4 A! I# _
, R8 ~; V$ b4 F2 ?! S8 M; b$ d# V* u
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
+ B4 L9 T. ~- @5 s0 R2 P4 U0 W
; q* J o% Q' G) O. P. u- w7 N' ]3 N4 G- K9 x2 U" M0 J5 o
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
" w( z1 n) B" ^- |4 L 3 G% h# T( M4 P! Y; j, [
* d8 |; W& u& i6 ^
$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- W! a9 X* R$ c- g: B2 m% k
" E' t3 E& u" C) C! Q
) X! D. q1 V( \" b
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
6 {1 U$ W7 z3 x$ e8 H ' c6 _" A8 A9 C2 q
& O4 w. R8 o0 F4 }
! f. F' K3 Y0 i4 T; G
: i4 D7 f7 J; X3 y# X; ?
8 d, k9 C" N% R. t7 I7 N 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
. ^( j7 P8 s& r+ u# V; g- y; I
. j% x$ R9 ^ g4 @3 O/ M, g0 O+ B) n( t4 K0 r7 e5 z) d
<?phpclass upload {6 e4 ~4 E+ i; {! ]: a
8 r* L4 s% `2 P9 C# z0 n6 _
7 I: F0 L& Z5 j5 C1 y
function save() {
1 j$ B' y! I* t9 x
$ r) s0 B6 }: I1 J& `; t
# c, \. ^0 Y2 Q! r. D include load('include.lang');/ I3 [( e' F9 {* ?- o
0 b6 C" f3 H- G5 N3 n/ j; M+ n
2 s& Y! J4 ^- c if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
. ?3 p( f' @+ I, F8 q) O9 W% t: r
+ e0 ~0 E% r& x E$ }, p8 s+ l5 @: `+ o5 ?
# r0 I5 a" ?8 C z0 G
5 f6 Z" o: V n* S
2 w6 q& R0 D- Y }+ }
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
- b5 x( Z' f- j6 e* a! }* ^ " N6 X/ ~7 b- p1 M% M1 c$ i* q
- B6 h6 e# v# Z9 U& ]( I
5 b* v3 |& m( ^& J- ~1 O, a) j ( i; g" `6 l# q; N
9 s% U0 `9 O( J0 q. _" E* N. I: a- Q if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
, X( x& h( [! ?) b1 `; Z5 Z 9 T5 E7 ^( a2 g
' I5 W' d) Q( W# ~8 n3 F" |
8 h) [8 x) s, @! t o
' u- m7 m$ e9 Y, r; T: c2 V6 M
6 M9 H$ g2 A ^: D( r& `" j j! v $this->set_savepath($this->savepath);% s$ O+ _$ t g M; ^# I
+ V q' X0 `7 ^' b1 ?& Z( P6 H
: `( B; j# D. C4 n# v$ ^ $this->set_savename($this->savename);
. L% D" c( o" t0 h$ L* G
$ u- G$ B6 l# Q8 N: D
9 x+ U' n. |7 C0 }0 T$ R' B, K ( Z0 W( {6 Q4 O& U \- j5 q, l; r
W# G* R, x, @5 s( N0 w5 I3 b+ {+ T, Z3 [4 a2 G
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
& @: X$ }* @6 t. X1 Z% m* j
, D3 t+ N5 B# S% t0 ^" T; g, y- c3 @2 e4 r1 H4 m* Y, c' N2 X, { Y
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);, k; E- w4 u3 h4 ]* b: R
0 V7 v, K, Q& g' k( i* g
1 L! U" ?' {/ k( g) S5 I1 \+ @
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
& }( v; a$ d0 t* [% F2 A
' Z' ~" m! \' n5 F8 V9 }& z
/ r! Y6 q: @$ f3 ]0 h4 j
9 m, j0 {4 y% @( \$ d # A4 j* `" r$ ]
8 m+ [9 G' s) f/ Z0 G! P $this->image = $this->is_image();
- t2 o4 q- e0 l1 H( y6 S 1 e, \/ i# B) O3 g% P
4 L" S$ L' f+ g' ^9 l if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);0 t" m5 M! P. `9 ?2 s
3 q$ _3 f& w$ b( j
- \% y# y: C9 c. Z( N! D) f; [ return true;! w+ q6 c% Z! q+ Z+ n2 y( N
; O$ h$ k, n# w4 i/ l, v3 b3 U3 r2 [) d; U# f
}}+ N+ U* b* W" ^# K7 Y g
9 s6 A1 n' J8 S% ^
# Y, ^1 k9 M F' i; O) ` 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
7 ^+ S1 B* S* \& P5 O5 n% U# u
) F) ^4 @9 z+ b6 I( w( U
9 {( ~8 M) T, n1 D3 [ k. g <?php% }# i b! [; m& Q$ s" w
$ {7 L" T, G2 ^# u- i m5 x
6 Y6 C9 A& L* t: c& x. E function is_allow() {0 c7 t) `4 _9 O9 M& O: D+ _
7 n5 c: b. s" S N8 Q
& h$ j. t; v; R# p( S5 k9 u if(!$this->fileformat) return false;1 \" l; \- a, i, D. g& s. T
8 o( i, V) E( g' u+ G+ d" C8 O
5 E4 g: w+ |# w, p if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
8 X4 Z3 J% c2 \0 C
- _8 S& V$ p( `! S, o8 b% d ~! {
5 w6 C# q3 \ ?/ ]7 N$ n 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 j( y7 I' o4 A% f- j
. G. p) q+ _+ w7 z7 z( Q1 h. f) J. k& i W. D
return true;
. U6 {0 K2 q, t( j9 d; j
5 n6 v! ]" F f3 v* d5 ^5 t- A+ l# U$ [) m9 e& J3 s
}8 Q# K" }, s' N! z0 P
, S0 h% ~ z5 j0 p8 m& x% X0 A; l* k; w
- J, ]3 V: e) ?9 k0 k8 u 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。* C6 e6 h* S I' F" u
q: h0 @! d1 z/ j. C8 H# p6 I! D: n3 r$ g" 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文件。0 J2 [* f: ~7 t, J) m6 E
" d1 f3 b# F/ k* X8 ]/ w0 D3 q
+ Y+ ]3 x$ K8 n. b 漏洞利用$ h; n( q; T3 f
0 v6 v2 l8 i9 J) D7 i6 f
/ T& Y/ T! G/ T/ E/ D, x( j, t
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
2 K* f3 d( m* f# u . W6 e9 d. \4 }+ e, D% y! |
, ^6 z" e7 S, q4 m f/ O9 Z " r8 h! _. J6 R/ x& ]0 I
; Q& j% z! s6 {
, p+ m+ K8 f0 h- \ 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
" ? V' b' M w' @
, ?2 C X5 H! ]) V7 H) ~6 D. k6 w
不过实际利用上会有一定的限制。. [0 q$ Q8 V- m- Y# i6 J3 \- ~
/ c' |# |, }& Q+ R
% ]% c `9 r$ E1 p4 G; d
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。$ Q+ I5 ~$ \# J
' Z5 `9 ^; i* C$ x8 z- f7 {/ M* i; N& a8 j2 ?% B
5 h) h2 T; X" |; }8 }
* j; t1 o7 K7 A, M* V
( b# O1 m1 I+ F' }& F. E) I 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:! {5 V' c2 z. k- y. r+ P
3 w. t! D0 S2 O$ Y8 l, r: R6 p: N- y3 O$ Y3 P
省略...$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]);省略...
7 p( l j( x: R+ C- p% R , ~( q# ?. h" {4 {$ x
0 d+ n9 m$ s& l
因此要利用成功就需要条件竞争了。" J- q; V' {, y
; D' H. P/ f; p
9 }1 \2 i* [% d8 |' g 补丁分析
# a' M9 ]1 W& u5 }* u1 K2 V' V9 x8 L& f/ D
' D6 k6 p4 n) ^' u( j
5 q+ c5 x3 c' Z
! u' A* V4 h8 p" q0 N" G, q
6 _' r) d* R! Q+ @5 @$ F% V 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
* ~ T! v- y0 e4 v5 j
! v1 o7 t* {$ \* w. {% h) [ Z8 p8 a8 d7 V+ I( L3 Z- p8 C7 ], K6 r
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}% _" h9 u( z" Y( M% y
- L* `; b' d3 ~6 ]5 G K
$ n4 a( X) ~ o1 R$ c: [/ r
6 w, ?5 f3 |: ?
P) F3 p* R1 z5 z9 a! Q) O' @1 e( Y. V: w" e; l
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。( g0 s% Y8 K% |; I' S
! i, a* w: c& k
p% O. F% c4 N8 Z. M 在is_allow()中增加对$this->savename的二次检查。
( E" A' a9 I$ }7 [# u! T
# {0 }+ W# I6 q" H7 @ f- T1 S# W1 C3 ~
最后
2 ~& _& L* f1 j0 Z
4 V1 u7 V" Z+ T
' c# |: R/ ~0 D# K7 @ 嘛,祝各位大师傅中秋快乐!: r: H# R' p' |; u
1 o" E( r R3 V$ `! p$ R9 M3 B6 \. \6 S$ g; b% ~8 `
* F- F1 C( A$ u( T0 `
3 p: Z2 z3 g. p3 k2 L' f
|