8 Y& C8 F- G* d7 T7 Q& ^8 q8 A! i7 E0 }& \- f% E2 L* l
5 j, u5 e% I, f1 l( c$ `0 ]% r4 X. }7 m: U. }. k# [7 u# R7 E
前言: P" O4 ~4 M+ y) H$ G1 P
0 j; U6 r" |) E* ~! f% d8 B
( k& b7 [. ^% w$ z: L 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
) K O) ?" E# n9 ~/ H* ], I
; x3 @. y/ z5 o* M9 c% P L: D3 T8 `5 w
# j( a4 c8 w2 _: e/ v- H
% A1 W: p2 v. Z- n% u; f$ l4 p3 t, N: b, J. L8 V( V+ I( b5 R
漏洞分析 ]5 t+ {) x4 c) U; D. ~/ J# [1 e
+ V! Z" t# q' U& z+ M* t5 c- ^7 [3 b5 S2 G
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:" I4 c3 v, O: X2 @6 ^' s, c, s
9 m7 h1 z0 m: g# N- A) {# Q- F8 q1 Q- W: k. L- j9 G
0 N8 e( K2 O4 E* @7 B \/ H
# V- ~/ q* E9 t. g- d. ?# U' N# y+ \" S6 B$ n+ L2 `
对应着avatar.inc.php代码如下:
! x8 y( P% y% n/ h+ B
' s& j0 o- [) c* _- Q; D1 B' m5 E' U
<?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) {# Z r$ J' j {) B2 i7 k6 t
# a' ]6 H( T! J
# j- }8 H& ] [3 o9 v case 'upload':
. ^7 q* ~5 n; i7 t* ] 5 w, s a) I/ n0 z$ ~7 `! d# z L& Q
9 v3 H6 O {" X v8 T: p if(!$_FILES['file']['size']) {
9 n) @+ D8 L' C7 Y! d% h * w" e: \9 H8 h4 Q
. c$ D' ]% M! f3 _* F if($DT_PC) dheader('?action=html&reload='.$DT_TIME);/ J: Y6 Y* x5 j% S5 s
, B0 L4 ^* Z2 f) h$ s4 j E
5 D N2 {: j; s; p7 g8 M exit('{"error":1,"message":"Error FILE"}');/ ?. n3 |5 g, S# Z6 N6 Q J9 [, [
. e1 S6 p! g7 B5 b7 H" D6 k7 U
$ A6 r, b+ B6 C7 @2 h$ E& f }
2 Y0 E$ u6 j2 M/ p, P4 Z" C7 z7 V
8 h( \$ N& ?: A8 t1 m# S5 T; R* p) L
require DT_ROOT.'/include/upload.class.php';0 O0 J/ |+ u5 M% K+ J; w$ J9 _/ \, H
1 r# B% P1 F5 d, i# B( v( k3 ^: K ~5 h# o
4 N6 w; w8 s( L5 E1 U
. ^) V# C( A/ ]+ V' w7 R
( [. h, C( Y) Q1 F$ n; v0 | $ext = file_ext($_FILES['file']['name']);
4 A3 o1 D) p. Z$ N3 ]: R / u z7 C! b: n. A V
- Y" S$ J2 \, G- a2 H" X8 v $name = 'avatar'.$_userid.'.'.$ext;
9 n ~. B8 L- \* W8 V+ n5 t. J+ S8 H! }
! t/ U" {( J: ?/ B% R$ [% r, X$ v7 k7 I- _# O- y. S3 F
$file = DT_ROOT.'/file/temp/'.$name;) G& e# z6 c( p' ^( M! a
; M5 L& W0 H/ U, y
+ o, Z/ F- x3 {% T) U. ?' }
' ~% e l& h! A- F/ Z2 r+ T: Z / n% y( \, a5 g6 U8 ~( Z
# O7 m# E g8 }, z if(is_file($file)) file_del($file);/ L1 [3 Q8 p$ u& A/ [$ s6 L
. l/ H. _- U( B
2 O* L5 ~. F, q& ?+ Z
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');, H; t4 @4 S/ f, R
& L8 M+ `( i ~& F( C5 M& g$ X" u: x* `2 s( \" m
& l/ }* @: W: [/ f+ Z/ S
) g+ {5 m \( K4 k L$ P2 C9 v- a$ h+ E% g6 ?5 M( Q7 ]. E
$upload->adduserid = false;: T) q9 y1 l. s a, Y; F
+ S" n+ z$ y: t" v2 ~/ }" M7 @- m
1 {# o: u9 u1 h" L1 u5 w5 A
' l W' d* @# z( D4 j' O+ o
8 v. P# g4 Z; |" p5 }5 Q! Q4 O* W6 j
* A5 m5 g% |2 Q4 v if($upload->save()) {
0 j2 V$ N6 o7 C( X
2 h, }+ D# i. y g+ K" I; ~ j* m+ i" v: ~/ Z( c4 M. {+ Y9 T" E
...
f) s( W0 [* E
+ K3 F" N8 B2 z& R1 d5 T
/ a) ]# W& {# B } else {
8 T/ D- I8 P( }3 b7 K: g7 G" O * ?" t8 y# ^8 E" R9 X2 ^0 D! O
G( q- M, x' v4 C6 m4 K3 Y6 a ...8 Y. }- Y% i* U' Z4 W; J: y. V+ C1 H
: @. }1 p! F8 y2 ]
4 d3 @! R- a3 n }4 B) B7 u9 i% S) A) C7 }# V
4 m# ^2 T+ }' E$ I2 l, H2 J
7 M8 U% Y3 C4 O# s" E( E break;8 X7 X8 r# _! T4 z
% `6 F( s, a+ n2 a
, R; h/ K: [$ O! A 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
; w) K/ {4 _/ d4 r0 c! { # b; f E. Q/ h' J2 I
& J, A) O7 w8 J( W. M8 v7 k* n; B upload对象构造函数如下,include/upload.class.php:25:- N. H, V8 r9 M: W& k' F
3 @, V% ]& w/ Y- o, D3 B2 Z
! n( H( U: W6 G4 U& Q0 h$ ~8 M) N1 W
<?phpclass upload {6 J1 L: v+ y- ^9 P
$ X1 }; N5 {' G4 ?3 l+ o' g
) D& l3 {' ]+ j1 K- J5 C0 e# E
function __construct($_file, $savepath, $savename = '', $fileformat = '') {# }/ H. R! k' {2 N% ^1 ^$ k
( P, u1 B$ h. r9 X O
- F9 M8 b4 p5 J2 `; S U( E global $DT, $_userid;
5 s+ \& H4 n) i: U5 S6 o
) g- J a- I8 |) p. A, x; S, f) F- w: v+ w
foreach($_file as $file) {
1 c$ H7 d5 g7 P$ q, m- g" S 9 a7 D6 Y3 k3 w! m- x/ e
' u' s9 G: h; M/ C- u0 J3 N% l $this->file = $file['tmp_name'];; L& ]& X; \" W7 Y' Y9 }
3 G) b Q2 [" P. B1 k5 a
; _* s, K% i2 d+ _! f# M
$this->file_name = $file['name'];
. y1 _; d( T% P B7 A0 x2 S
* z4 n$ ]- i4 P. `7 c [8 G* l8 V$ y2 u: \" [( v5 R, W" Z+ M
$this->file_size = $file['size'];& J: ^7 N$ y- d+ a
# F) o* k- f! j
) c7 ?! \( V/ e$ b0 o8 w
$this->file_type = $file['type'];$ h& E% `# U0 B- P* j2 Z
: h7 x+ J% F, G% \
4 @6 y4 H9 h( d
$this->file_error = $file['error'];1 p( f3 O; ^* k2 c/ R5 h) c2 o4 s9 h
. E8 r5 D4 q m+ S
! A/ z7 p V6 D c7 g2 ]9 j1 k
+ O+ a2 V/ L% a. j- D 4 m! ^/ G R* S4 ]% E' m# r! l
! w: t8 o( C/ n, C }7 P0 Y) ~5 s9 T' I
, G! H7 k) W' c( c: r1 y6 a8 L& V
( g+ L5 l0 O' b7 j; C+ g
$this->userid = $_userid;
0 y0 S6 L7 x* a6 F+ t' \- R- N 8 z& A, x9 z: |( P2 i; r: N3 F
( p' N% ?8 e e7 L& R2 F! w
$this->ext = file_ext($this->file_name);6 \1 O* v5 K/ f1 C! [* V$ O" ]
4 Q& Y1 Q( h& R: k2 H% m
! w( j1 I% v8 {% p% j% r $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];* c5 p/ S, y ~
/ F. R' [0 ?& Z$ `/ b
) \# e% ^3 {3 ?0 f# m6 x# v $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
3 ^" w: j' D5 ? * S6 s5 o4 u0 Q6 n" Y+ d
$ J2 N4 \' _0 ~: @( I $this->savepath = $savepath;
& M: ]3 i2 t' d3 o! H% a# {. k# Y
' }2 H$ }: {# B, S
, t% y) E" Z. W! x6 k4 Y: | $this->savename = $savename;
1 B0 U2 y5 W' j/ i( Y
, K* [' D, @( ~, C: l) \
1 u, C/ j2 }9 j( M$ F- F2 E/ ^ }}! b8 M% j9 A3 I# ]' M0 |! J
, X0 q& } b! Y8 @# z
: ]* V8 w2 U/ D
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。. d, [3 L2 }8 ?/ Z
" r' L0 s# G6 u: w2 \! _# I9 V u: a- {' L6 C. Y
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
. b7 f+ Y0 L# L# H' I) x8 s# g- p + u: z# K r( L! a4 Y3 m/ w
5 t& V, d$ y3 D* l. Z
$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% }; n0 y5 j; l- e- h
5 I8 j) g. [+ q
; \9 ? k( G% k5 n+ e6 ^$ Y 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:) T* s4 N! q0 i. b: p
. h2 o) i8 r% ]* U1 C! Z. B
, p( n5 e; J. q$ W- z
( S+ G* s0 l9 A . U- L; Y+ D8 W) K( a! b' J
1 y5 q% B/ g% A6 G) i8 v 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
+ j. c" m) v0 f/ D) i 7 U7 H7 ]) L) u& X/ |, q3 j/ K8 c1 K
4 }# ^! Y3 d/ U& | <?phpclass upload {
# ^( Q l g6 x % F/ }. z/ ]" e& t0 B9 W' P2 ^; n
6 W! E B; Q/ I; B f function save() {" @1 q+ E* Q" l9 {' w: ?
8 o; Q2 l- Z: H- B5 `" y! @
/ H: Z2 u6 f7 I. [! h include load('include.lang');
1 j* x1 V9 T$ o9 r- Z
: m* ], M, R; C! _! P/ q; u0 B) I n. `
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');. ~- @4 z4 {5 ]0 {* |2 C' b
4 H$ k$ S5 x5 l7 D+ [; k- C7 Y
- U* W" Y; _1 ^0 D; h
/ \* B0 Z5 L: b5 d# m; U' _/ ]/ e
6 C+ T1 t% \% t3 s. B3 {, u1 Y; x/ J5 i7 L5 v* Q( d
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');5 ~8 F. m7 h+ \4 { C+ l3 F
- o9 R, M2 q0 U4 k$ P$ a+ m* T3 [6 g
" h4 m( O; m o1 i; a
% ]" k) m4 b. D6 n, T; ^& K, u
! d7 x1 w7 S( h! N9 M% ^+ l1 T* y* x
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
3 {: W' k+ _% p7 Z7 U" v) q9 k
+ @( t/ Z1 a7 x/ |, a/ F- h+ i- g- o, F$ Z6 a* H) ^- Y3 k
: C( o3 |1 K5 N& v" O
5 t' Y. ~- ^9 v7 Z1 G% u ?0 c2 M; T$ X6 E1 e3 ^
$this->set_savepath($this->savepath);
& x9 ]+ k3 B9 i! p9 j% ~/ v/ C) g
7 y1 s- y' W2 c I3 r# h
# M A( a( P; {$ f4 y6 H $this->set_savename($this->savename);
+ B3 x+ I& T* I * M5 h' ?# Y; C+ R+ F* Z
W7 y {0 v# x' o
* p* b1 M8 Y4 y( y* U% l6 K' A5 U9 C% Y
# E0 c5 g4 K7 r1 c% G) ?8 y! c: C+ ^0 R7 r+ l- c! A9 F
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);( s/ Y& A( u' ^% y# i
' |5 F) g9 S4 O- V: P# X e
1 v" s( B" L+ j2 \! S if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
# `: x! ]1 E+ Y
9 N1 `' k O- G* [/ o0 I8 v6 V5 C
! N# u3 X8 J* ^9 H' x if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
8 s H5 Z' K* a t* p 8 ~3 x: g' A% j
9 ?- B$ n9 Z& T% h 7 m2 j1 Q3 ^! d
7 a! _8 U6 G+ m0 r5 S% W9 r3 S% j8 h1 e& ?* |. T: w3 ^, p
$this->image = $this->is_image();# ^" X2 r' @ l- f( `' r% l
) k; c' o+ z5 y' y
: _4 ^+ a; c0 f0 t0 `! w if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
, l! N. d1 y9 ^0 c
- O9 h2 N5 h6 W G) q/ z
' G. ^$ ^1 m* t; i* ^. E' A8 K return true;
# U/ S, o; A, w0 P& j ! q/ P& {# I- C+ H8 p) q
2 g: P+ z% @- z: E) K$ t }}6 o3 [5 B: r. d: j% d
" g" f+ {3 U0 ^. g) U$ \; ^* J" y
" ^) G6 B) t, K6 h1 V- p4 l2 B, ` 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
: E5 n3 G8 G M5 @1 @7 F& t# } ( k u: p5 K5 Z; u) M; s6 O/ M1 |/ A
, u+ M4 l h, G <?php; h, e2 b0 j7 m+ y7 d/ @+ R
0 H2 J: o3 N5 @# c
! K7 R' b2 s- n# }/ u function is_allow() {
Q2 G6 \" ]( y! m! _
8 S* Y& N, e: C$ t, K; }: @& ]3 _0 D" a) K% S; I, S( D
if(!$this->fileformat) return false;0 W+ `) @6 q6 q+ Z5 {( a$ A
4 S/ g1 M) X) c! w" I& _7 g/ A% b
+ W9 \4 y$ F& T# b if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
; f) Z+ J, z' W! Z5 D; H4 d / b: c- c9 w) [5 k" c+ }8 a, P
2 ^: E4 m& y4 O; K/ }3 o. m( c. t 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;7 `1 ^7 O* s( d4 W
6 h' ]5 l" s* b% p* t7 `+ {* F; f0 j8 D
return true;, O3 e7 Q+ s& x( c
! z' N* i3 b" I. \) O9 \7 C% H8 j* \0 y$ i: a; i
}
5 W; l& L1 P/ B2 [
7 I1 D6 f, c- V [. I/ o, `& s# X7 y$ M
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。- O( X/ ?5 F. F+ h
q$ R( B' k9 q; ~, i+ ~; ~) X" F, Y }. `) R
接着会进行真正的保存。通过$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文件。 N# K/ `, V2 ?8 l4 U
. ~( P/ L) x. q% O5 D9 e( s
- H0 ~' c! Q- O+ o# U 漏洞利用
9 z2 p! i5 R8 s' p- c
$ f. z5 S; z/ s9 \/ e f+ E1 c, O0 ~ M
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
+ y; B6 p. z2 d3 q ' @ p, z) |' ~/ U# o% a
: D \5 n; N8 ~, `+ Y! t! m
/ |, s7 b1 }( n : \3 o4 p- O/ w7 l* l1 `+ c* I L
; c* ?# X$ j8 T: @( A0 x
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
7 G6 R! t% g# B3 Y! {3 `1 a6 {
$ c2 [ p" a3 T' j- j4 v
. G% j# F' [, V0 a { 不过实际利用上会有一定的限制。4 L. E& u& e! M. r/ c
! _- P5 D! I, |9 `' l! d
0 ?( m! t9 v. u+ M
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。" }& S h- w% l3 A0 L5 h
4 V# H; e) h% n2 X& ~
) ~# _% n" ?. A% D, Y" _' i ' ?% y0 J) @5 S: S& N" p' n. D
N' H5 p3 U7 f! Q7 I* L. J$ g5 d
* R& P: b! ]5 R' ]2 I
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
! G" v4 a" z1 w1 n) x0 Q " M9 B/ y0 \; w9 u
9 n1 U) ?6 ]0 A, B) k0 T D* ]
省略...$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]);省略...
. f& w- I! s Z# B
0 F H6 l1 A. l
3 N# h' o: e2 x6 n7 _ 因此要利用成功就需要条件竞争了。) m. Z' B/ v* g" k+ V+ z
* Q# R- Q$ O0 }$ i7 K$ \
% I: z& _8 V$ ^) K9 @ 补丁分析. {. W( X$ \/ q1 C6 O; y
! B, P+ S8 F4 q" I+ h5 P; x9 y
9 k+ d- B! E' ~) T5 C1 o4 ?
9 P" ^6 L+ W# F0 ^0 l. H- B7 J
2 U/ r% r$ }. F. l T( w' [8 E) U; ]
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:- H! |: D' m% \
, _' M/ U" i: x
9 M* D8 ^, C: b' r function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}% v' V; _6 | }7 O% x8 z" p4 }
* R1 s9 q( h) |0 x- A1 |3 t
# X% z( k' h9 n6 W. i
7 d; J( ]! i+ |
p8 \. }9 u, T$ R. m
; h+ ~. a* ]: A Y" }5 N1 K' i 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。/ C/ D: ^4 O: Q- O$ ~
; G6 t$ {* D/ s" N0 L4 M f) B/ f; }$ e
在is_allow()中增加对$this->savename的二次检查。
& ]8 C6 c$ y$ e; P 9 F3 L% ?5 L l* g. e
0 C% z$ b7 s$ n9 [5 d) K
最后
8 R. D3 V1 I$ U3 X, ?+ S+ u# a* R
9 C3 D; ^7 _) @* i \
% w+ L I9 {1 s4 a 嘛,祝各位大师傅中秋快乐!% @* X8 i" Q9 W. X- `3 a
! k% J% l7 f# `% R- d9 p3 @1 z) m) x& N* R
1 A6 ?+ f5 m Y& T! k
. R' O% U' |, ~
|