! [- k; Y9 ?+ P+ z1 h7 p0 g( j# F. D' r! d
. J( T1 I3 G' V
% v# h9 u& B# A" W
前言
% A- L* g8 J( s7 Z* K" W
/ m+ J. u; E6 s/ b" ^$ I& b! B4 y' o* H2 Z$ \
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。7 a9 W) l" V$ ~" P
2 Y9 Y% u, S; [: o" D
8 U4 m7 _( T3 u
( _5 I0 l0 F0 \7 u
* n( ], u2 F3 Q7 E! b& w# k& C' t: b" ^8 a. a; l) t- x* [
漏洞分析
0 N6 g( h, E O; C6 w' n* s l0 ~5 u
2 U8 B% [( n* F8 L2 [7 T8 a- w+ h6 g8 {: r M4 C, q
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
1 L, K% I& W @- b, z9 R* j ' U# a: B# z. n2 x6 |! [
6 s" s* J, M; k( q- ~: j
2 H: F( D& t) z& U& g: _5 F8 u
; G( X4 }5 ]1 t5 x
; ?* [/ K H6 n5 v" i! G5 L, a/ V: v 对应着avatar.inc.php代码如下:7 p& J& ?, A! |; \ {/ x9 I
6 f) m9 \1 z& u9 a/ [
/ T+ G1 E5 Y" n- k% C7 M <?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) {
m7 o# _, x- a1 d# g
1 p+ ~6 H' p8 R" I/ ]9 v
! ]; _4 l* W! t5 a% m) O- Z* Y case 'upload':
( J/ G! S% N) O; Z7 |/ [
) g: u& m/ N% @
1 Z& f" N% C( a0 b if(!$_FILES['file']['size']) {6 j/ K/ P/ h0 s5 k
% K4 X( j8 s, m9 r7 O D
( n0 X. W. Y0 [3 @ if($DT_PC) dheader('?action=html&reload='.$DT_TIME); A6 w3 s7 O$ K2 V0 Y7 l5 v
: y( i p: [. P) U# `4 R
% z0 d2 m7 R* P5 Y J exit('{"error":1,"message":"Error FILE"}');4 _& I1 Z4 o2 I. j7 w: R1 L
/ p+ z4 T' _; }0 w# o
" s" O! ~7 |. d: f0 {5 U6 N }; F# W7 S) k/ l) E# _
7 j U9 [( M+ E$ Z0 n
3 T! z! S* h) \' Q/ ] require DT_ROOT.'/include/upload.class.php';
+ l; \4 z- t6 t" j; [; z : E2 ^' @- \# e% E# d) i! A
: e" b' n+ L7 p, p& K9 b 3 ^6 K# k/ L7 M+ W' |" r
$ M1 R( w2 d$ I5 }+ y4 n: W! Q; l; ]! H
$ext = file_ext($_FILES['file']['name']);
# Y' D0 v$ M' n0 R ) p% D8 t a5 O
9 J6 t6 u) H, j
$name = 'avatar'.$_userid.'.'.$ext;- Y( G0 W9 W" l$ @, w. g& a1 W
$ \6 X2 Q; P3 O. o. z2 a$ V0 ]
2 x' E1 a; K- A: H9 m( I
$file = DT_ROOT.'/file/temp/'.$name;- O7 A, \/ }, l8 J: c
% P' s; L# ~8 E- f3 w' F7 ~
; L% D' R& }( \
+ }( O1 f6 a* D+ k
( ~$ r& ]4 B5 ~: U7 Z. j7 o% @7 H' @4 s7 }! N, i
if(is_file($file)) file_del($file);
; f% ]; Q# C% m, F & L4 _1 m. J0 J- x0 m# M" x1 [
$ \3 l5 G ^5 P B
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
9 K I, L6 {5 ]$ W6 ^) g. b4 E
$ z/ _- V8 L; [, g0 x" T7 t/ t: o8 L9 K( j
3 d: V+ z# S) b0 p3 |& F + L7 J k' @$ t- n' x7 Q# @' ]
/ `( l7 R- q& u. c- U% M8 `3 \
$upload->adduserid = false;
) Q2 {7 U5 Q% ]9 J
! O R) Z1 a0 r7 I' y' P
* H# q- `- x$ |, S* d2 K
4 C2 ~, y9 w6 K; ]. n5 ]
4 I+ z4 w8 f/ z S( c5 {. i: z* A$ P4 f2 b2 N! o \. v1 p
if($upload->save()) {2 o$ O1 @% ^- l- s1 f0 E; x$ f- X
, r( v8 U! \9 m
4 |: m" ]( L% ~# ? ...; s3 ~- n- v; B8 }
6 c2 _+ ?* c( l1 J% T
0 J! T1 C7 m$ G } else {0 _* E, D$ P' h* ^; G
$ v7 p' e( w, H+ e9 S
7 w. O2 h8 T4 d4 e) g2 x' w: q( [ ...) D: D# U' o' Y b3 J
: F$ V: _7 g6 n8 a
" N9 G5 N0 c# D. |1 I9 h, ]8 y
}
6 J7 B. i; l* D- _2 _3 c& q* V' T 2 y- E; `" U* M7 H5 O- {) J
F; f# o1 k, {. |3 M: t6 r
break;2 m, ~4 j- `* t
( Q9 v# {7 r7 v4 s% q+ q: r
- Q9 M& x" \7 W" \ 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
9 x+ V$ @# \: w4 s# M ) |% m3 k6 T+ u2 g8 `. M Q
5 L# U: d1 @* L% W
upload对象构造函数如下,include/upload.class.php:25:$ h2 l3 W8 ?1 `0 v0 j+ Z
+ f& {, V9 R2 ?4 I0 T
" F2 J0 f& V a4 p! h <?phpclass upload {
: s$ d: b+ Z" t; P
# k) q0 e0 R4 j5 v3 q+ X7 g) k1 g& O, h- p6 p+ \# i$ q2 M
function __construct($_file, $savepath, $savename = '', $fileformat = '') {3 M1 ~$ Y* ?5 [( Y+ s5 a* w
3 G7 U6 b7 _9 d' A7 S
& ~6 e/ q3 i! | m- k3 K global $DT, $_userid;
7 q; |1 o3 o0 G1 y9 t, j
" j3 v! J4 S) g
4 q9 p) W; N& ?8 z foreach($_file as $file) {6 W" g) Z' C# s, k# |+ S
9 }8 F: F9 }) D8 V6 {( N5 A4 c9 N! z, k
$this->file = $file['tmp_name'];
- F& K" p$ S$ I0 R. f! M 0 L+ J$ c/ P6 Y2 Y+ x
, L# @! P# C; A+ w! }3 P' f6 [
$this->file_name = $file['name'];
9 o- v8 I/ S0 E% }3 l4 O, X z: n/ Q8 ]/ s7 k. ^2 L" {
# |1 b9 }' K/ {% x! u* V $this->file_size = $file['size'];6 \: c, B) h% Y9 G# Q7 T' R" b
% P$ k/ A! s, q& j5 ?
\! w+ M/ ^& X3 l $this->file_type = $file['type'];
+ ]- @2 I+ d1 i8 x1 |1 m8 e* W1 T
. H; v1 r4 z) n5 i9 a" b( ~7 d: V5 y0 _+ \9 n6 N5 B
$this->file_error = $file['error'];
3 r5 O2 R3 W- E2 e R5 w
; j& x }( ?3 C8 a# P0 |% A
7 l9 Q5 f4 } H+ c
# d- m0 @& T4 V& C& R
0 e6 D# n; {, ~. Y N6 \6 }0 j
& c9 k& @: i9 g; m4 H2 C }
, ?- u8 W$ m( g& X) @ h
9 L$ _8 O, P* H' O1 W! } a
- w% n5 Q: U/ y- @% y $this->userid = $_userid;
9 L/ Y X/ ^5 v2 r& m: f) w/ ^
& f4 F( v" [5 Q. S# S/ I2 } i/ u5 M2 E) r+ U. T
$this->ext = file_ext($this->file_name);2 }' f2 M) ^% q
! e, P3 L- U' Z% S1 M
, M, d! `6 a5 \5 }
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];2 N, Z$ C4 N7 o* `
$ w5 ]3 v; ?0 \/ R j4 _! e8 [
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;6 \0 N) \- T6 Y4 i# z! F1 |8 D
y) Z$ X3 M% Y6 x" }5 t
: I0 G; B: N9 Z$ A! x# ~ I$ H $this->savepath = $savepath;" [7 a5 p2 N* W8 Y
6 s" q! W) A2 ~6 C3 [# e' F
' n5 }: V( ]) h( Q( { $this->savename = $savename;
& W6 K5 E3 |* k" e% P3 V0 K
. `" P; u7 o/ b/ o
8 _- G6 E* }" d- q }}) v7 ~1 [" ]+ a8 J, g
. ^5 V' t$ V& p$ }6 ? F' E
& o& a& k* f _; r6 x3 j& K 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
/ V0 m% C$ @) Y+ V; ?% H2 \ . [4 ]/ X& U1 V6 k
5 }4 ?& b0 W2 Y9 j+ R 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ; G! C- u! X# l9 ^9 S
' i6 Y7 Y! O6 T! m
% m, I0 |; E/ r- f; ?1 W $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.php5 c2 V% w8 A2 ^4 A- O( N3 E
: K# \" |# d3 R9 ` M( N5 U! e! W
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
: c0 x9 N. P/ v
& V: W7 @4 b6 e: @0 e, T' x9 L' J" A/ E
) Z' `5 X9 K% m5 p3 `7 g2 F
# h" R6 r/ u8 w/ i7 n1 p. h- V
. E, u: x/ F. L5 v9 ^% T/ `& ] a 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
0 a+ e! k- K# g, @ 0 X! j" a6 {) _3 u
, V9 y* _; `8 Z1 o& o$ m
<?phpclass upload {
) V1 s8 K u' ^: W+ N& P0 s 2 h( g/ v; `, F) `$ w$ z: X6 [
D* u7 n# t1 j) {
function save() {
* m( h# g/ B( e4 I2 P6 ] B9 ~0 J+ s4 I& y& s
3 [7 q" b( }; O/ k# N" X5 [; Y. ?# k include load('include.lang');
, H& W* Z" `, q3 e, r
9 C9 H; n5 U; [9 _/ |( W2 v8 e7 R+ n1 h3 F. O. j8 N
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');# s* n2 e# q: Z& J& L" a1 {# t
& z: D" o$ Q# S. w5 l# ^" x! x5 @3 Z# N c" d
S8 L6 W; |0 E' R
: j7 O( h5 S3 z. | v+ q
* Z5 X# x; d) [6 G8 w4 E$ g4 \ if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)'); n% N4 z5 K# F
" e, H, v9 x+ R, l. N
! J* a- R1 h% v3 l7 q/ L* |% n
8 b, g- B" C- y& L3 s
, m, S$ G/ A) C; Q& D6 |& e% w5 h( [) f3 h& w5 z/ P
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
: f* Q. A' S y 9 K: v" Q9 f( a+ Q
; H/ i# V4 Y6 p7 M 7 E8 L B1 n- R4 M, G& H
$ m' E8 n% i, T3 U# o8 x' L5 [$ R
1 }: X) e/ q& h; Q: V! S $this->set_savepath($this->savepath);
3 g1 N: y: `: A; h5 @ J3 \; x: S
' G" q0 c) @) n2 |& T; ?+ k2 M
; v9 m7 V% q1 F" @9 x $this->set_savename($this->savename);
; H- Y4 M* L9 F5 V, h / V6 q D- C( [
! v; _+ I8 ^: C7 { . \' h" k% X0 b% c- l5 J4 P/ S k
* c# j1 i6 J$ b [/ I
0 d. f5 f$ }( D$ h4 S% \' Q
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
0 R' t6 i7 Y) S 0 ^. x9 @3 w, G/ o) P
( Y8 I' F& h$ c
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
& i, O0 S. v- n$ C 8 |' V- W, k; ~' j) \6 c- ]
2 v8 M0 ^- U3 C
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);2 \6 O, \" q& S& J
9 g ]; ]5 E5 u) ~: F6 M
$ A) ^) |* i& F6 k, t6 y% T0 s# a) h) z. e 0 U0 ]' d0 ~0 a+ h6 t* `9 j6 C1 Y
5 w; r, A' d$ K7 i, M
' t+ J) r2 f, ?' k2 G$ ^ $this->image = $this->is_image();4 U! t0 R/ r3 n0 E# t
! L D, x4 E% T) H4 ?) f$ L" o4 n
# I, n( X* r$ F' U1 Z- B# i# N# B' N if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
7 r, L3 K& \* N+ {, ]% O' v
8 B" r5 ^# G2 i% e
3 l4 S# X( n8 Z* H# x/ G4 o return true;3 F8 H" F8 H& ?2 H
4 m% U( S8 f, _& P& d0 u6 W1 S* D( k, \7 Y- W
}}" q$ v3 }1 _; _' H* n
3 H( r2 o" `/ v2 @" G8 `
! M9 M! W' t* X2 @, p4 _4 [
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:6 N) v% p' v2 r5 V& ]$ R0 f
, x+ \; x4 }+ }+ h- i/ D- g6 r! J! k, s4 t9 [% H
<?php q: P6 I+ m4 I9 p" [" V: s. v
8 r5 L( P$ @: {0 l
& K0 E1 P9 @$ b6 j2 Q* T* c0 W function is_allow() {* a" ^% ?- G" k% x# k
( p, N* Q( G2 B5 f- }/ K' K7 b3 f2 U) N8 t6 t2 c+ q# ~' o
if(!$this->fileformat) return false;; s- @% d% |& y
# o% E; z/ `* l; x4 p0 J
8 V! q Z4 m) H if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;& O, B* Z6 R1 z7 _
: D; |) S: E0 o7 s2 W
+ ?; T% R& a$ | j; ^+ G 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;0 b3 b* ~1 u4 {
7 v" Z5 y7 Z: x6 W- m
# p( c# ~6 L& B4 U, @ return true;
3 @. W& x7 f/ T# g0 F* @: _ $ q7 \# R1 H" C: T# w/ M. o/ s
+ i+ T9 T I( G; [# p }" |0 w% W, |6 S, R! C
/ r) m+ P5 Q$ E c: G* }- T
7 h& M( U: b( u0 U' r# }
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
2 J, b+ J6 F5 X3 u6 [/ d. Y4 P ; Z7 l7 X$ |( W7 j# i6 y' o6 w2 h
+ Z+ }+ N0 L& u% Q: d$ {6 p 接着会进行真正的保存。通过$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文件。
" n9 u( L w8 N7 K3 e' Y- `6 H6 A0 F
9 ~4 [5 ^3 W* ~2 M: }: d
1 v8 k0 P8 y& ?0 ^' n$ g& N 漏洞利用1 D' i6 I" o0 N- v4 k
1 ^; e3 J0 F( B2 Z: ]6 v2 ~
& X! U0 ]* P+ w1 a
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。$ z1 ^, |# F! z# D9 c; {
! C# b$ ~$ ?8 n; Z
0 H8 W6 {" N5 q5 C9 O3 b8 w. b
9 n/ u' G" Z) ]: Z1 R
- ~) j; F' e) s7 Y- h
" L6 y( K7 Y. ], e 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
! k( f* f6 _2 {
+ X0 r, u# @+ Y) K9 f' L5 ?9 r. @, B
不过实际利用上会有一定的限制。
1 b; r) x% p: u6 X# n" Y1 O, D & }4 }9 E" N& h& j
2 o( [! z& D/ \' ^
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
& {7 a; Q! ~! P8 _& s) G4 t q) J W
, L) t6 \( e; ~2 V2 V; n
{" ^5 w9 }2 V# i' ?& k
* \) U1 R/ P' o! B" l ' C; Y" b% I4 ^3 [
; d6 [0 [+ \; l) g6 z
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:; @: B1 D* J! t) S. h
; q6 T5 x6 ^ j* N- j
9 V9 r Y5 `( p# U3 t7 I+ A 省略...$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]);省略...- P4 Q% ~3 M% \ j6 G
: {9 F( g8 l% Z# i* C5 d
5 R" V2 h% {& a1 |+ t3 ^, k! ^, a2 X& l
因此要利用成功就需要条件竞争了。# b/ I' }1 ]7 k5 o: u$ F1 u
7 H3 P: U; F, a( R6 N1 ~' V& G9 V5 G( h# ~! F0 f
补丁分析
z3 c, W O9 q( n- s! l- l S4 s* V7 s
( d. n' q1 V T% E
* ]4 @) P9 ]9 V a6 ~ . N( }) F a' W& u- A& l/ e
/ E1 l' |: W& S4 M
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:9 m; g. ^% i5 y/ u* z, ^1 Q
5 b' P' R& b, w- y* j
( t# N( Z; `5 [ B; M, n) N; G function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
! Y) M5 K& J2 ?! t9 I. }$ E
# v5 }* I! O: `" _" s4 m' N
# Y: M: X( g; M0 J {, q' H
4 F- o1 Z' ^4 q 9 O. C, [# y( q0 A4 `
: Z7 \) _' B$ ?! `8 m. A7 W8 Q
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
6 u( T/ U: }' B% a [9 z+ F6 \) O 1 c9 {5 s7 Y* ?
1 X7 e$ |6 u" ?) y- N- H 在is_allow()中增加对$this->savename的二次检查。
: t/ O2 H$ x% q( U5 H7 [ ; Q. v% ~( S6 {; l
5 ?5 E. `; S* r7 S0 @( y2 a
最后
6 _: ?1 E4 }7 {! M- U
# n l9 S* t: C, K! s& L/ j8 V0 l) X+ @8 s/ B' Z9 P
嘛,祝各位大师傅中秋快乐!3 o4 I5 t, _7 m! D) E# ^ n
& ~9 R4 |0 H4 S, y+ V% G& s
/ J5 U3 ?* p$ q7 j6 w5 U
6 B, `1 w( b7 q4 ]& @$ d 9 F0 Y3 W+ {( ~' U& }# A# v
|