" @( _% P% J! F, {, r: @7 a
! n# S K" q2 V6 J) P7 C6 P5 N( y3 B3 z: V( `
- q* S+ Y7 M" w6 N1 y# V$ U8 D9 o- i' } 前言
; q* q2 I& p) { v. N
6 R6 o, A# z' q' p$ [/ ]+ o
/ a/ N, ~3 g1 Y0 X0 H- k* k6 q 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。0 u% K9 D4 m% [ H' K7 \
' H0 y1 n y. j" f. [& G
6 X+ s% f6 S9 y$ I' @6 B3 p
7 O9 u0 _* n1 E# N1 Y5 s, w
- c) C1 t$ p% Q3 ~+ _
. L* p5 `1 p3 A5 A# } 漏洞分析5 {: [' n4 w! ?/ f
: C# c+ d1 a0 J. x+ C
. [: q8 I# G6 Q4 l
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:4 y7 K3 |/ O( e9 w
! i6 C2 |2 I1 \9 E5 f$ ]
, W# G# H7 Y( @+ q: H; x
9 O, R5 s( z# W4 ]# y1 f
& J- H1 m" u0 H$ O$ ~. q/ A# c9 \9 D. D4 l4 F" }
对应着avatar.inc.php代码如下:2 r/ I, c( |& P# L3 u
# r4 o! C4 ~- G) j4 x2 Z
5 W' [4 l1 q( s4 f( V <?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) {
5 \# |) T( e& g8 Z# _+ t
# t4 A, v* o* t+ V9 q/ N' ~ y0 \: u/ S/ m
case 'upload':
$ w8 T- a/ ^( l/ K5 S5 h # c4 R& D8 \9 {* K. Q* [% i
1 s. u+ b: C) N
if(!$_FILES['file']['size']) {+ R) N( m1 ~( @$ x: c& s
* S& M; _/ X2 s# }4 p# G" ? i, e% Y5 f. W0 R
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
5 ^! }0 ]# E+ X- X/ F6 p + p( s5 d! q4 l% X9 Y
5 q8 \5 ~* D8 u- \' J* ], R
exit('{"error":1,"message":"Error FILE"}');/ G, w# z3 K9 [% }4 C* V
8 F5 O$ [4 a. D7 \* s
3 ?' r- i+ v7 m$ I }
' z4 L! a) k F' h
4 o0 e2 M' { W8 z; d0 V
2 s/ I0 v3 }% S1 ?0 U# f1 | require DT_ROOT.'/include/upload.class.php';/ [/ t5 w' w) B8 z
' J2 e) w* x/ y2 y8 [/ b" o+ l2 H- c6 Z% D4 X# B" [/ V6 I
7 y, f. @9 Z) R# o' J 7 K! W. |1 ?/ w3 e" T+ V$ \
8 k/ j9 C4 e: n3 e1 r) O $ext = file_ext($_FILES['file']['name']);% k7 U M" f3 h, T* P
( r4 k7 W5 u9 H* I9 B& R3 ~ N9 R3 A
$name = 'avatar'.$_userid.'.'.$ext;
0 h# }% v5 G% u! e" k
# a7 N( b( [: R5 F1 V2 g8 y- {8 E/ y9 e$ d& A+ M4 _
$file = DT_ROOT.'/file/temp/'.$name;
6 t5 g7 O. u8 T1 o5 Z: S
1 [: s3 R6 O7 V3 K1 }
0 |) l0 d" l$ N2 q, N% _4 c
9 P, ^9 m" d0 I- D
U4 a# n9 @1 E- c8 u
v% N# P- r& n' y if(is_file($file)) file_del($file);
5 q- b8 P& w( i0 {2 B1 @
: t2 X, u/ Q( g8 c! Z: T
& k3 T3 x; T' [1 i: k3 @ $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
2 X. s T0 Y" m- t " H: U3 }* Y! Y* X9 m. G f# l
$ e; E$ g$ L1 h1 m& Y+ J2 X1 } ' N, E/ L$ J( O' K& ?/ }% }! K
) n. f2 M6 N8 E' N
, n. g2 V( O5 N $upload->adduserid = false;3 U7 O8 S* ^$ M& b" x6 C0 D
! U9 e$ S! Q- u( n2 m- W
4 {- `# D/ |# I8 @6 ]+ e : g; w; q% v9 Q5 m; ^7 U
. p: d* K4 P: @; f7 W+ V: j/ q. U- M- S, O' p" Y2 z# e" j8 p
if($upload->save()) {9 S' s- x/ q% M, y0 r
- U. _9 X1 a. ?8 d7 N2 E$ x
! A* O" M+ C3 |( a ...
7 b: m! C* H: V6 N' v2 o' k$ k$ Z P- L2 k - l: ~. ^0 b' V/ I% V4 N
/ {3 [. R$ W8 J6 P# g/ O8 z5 j$ F/ R
} else {4 S( m2 O' B; J0 V6 Z5 p; l
- c; e' z0 F; j9 F+ Q! v* x5 U9 H1 F$ T% w% _5 A" O$ k' q' E
...' j1 W" i; [7 d u- T5 `) N) t5 r& g
" \8 p9 z) c" [0 Z- I( I
6 J. U$ h% L: X+ Y# ?7 t3 J
}& x w( W7 k4 y: v7 G: s
% r& a0 Y* L, e- T+ P u7 f; M3 {' P7 |8 \
break;
% S9 X, }! p. A: U1 G8 Q" `+ ^
- a9 C+ C8 l0 H! J3 Q" `5 H6 v4 f
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。7 q: ^- i# B4 k( B
4 u7 m% E, v: c3 {# E
' r, y+ i7 A, Z( }$ {) y4 j
upload对象构造函数如下,include/upload.class.php:25:
1 Q( O( L, V6 v- `) `6 i: @& z, v
, D8 B% A) U$ a/ E8 f$ R! Z$ U7 F; ]+ L
<?phpclass upload {! S* }; ]/ [# L! R5 a9 X
. T+ b, p* N% \- l. P
1 g/ Q5 a. w4 n/ j function __construct($_file, $savepath, $savename = '', $fileformat = '') {% L1 \- D- p$ A/ ]! B
0 @- {* Z% ]" } B
# j' x( f' }& v z# g$ c global $DT, $_userid;
. d, d, c1 W. P* q& g" q5 o# S 3 l6 w+ S$ C7 k' a6 p
- J# t' M- {" G$ S) p A- p
foreach($_file as $file) {
# O$ R% l3 }) F
3 @$ l- H( e! V% e: F F; [
/ u* c1 B* @3 N- L' y $this->file = $file['tmp_name'];' `" |% g5 ?8 p) g7 e* `3 K! ?4 z: t
, \& T% Y+ b6 \6 ]1 T
' X6 L" T5 T7 b: V* S $this->file_name = $file['name'];
# M% t" f; k# o5 s7 N# G1 y 9 `5 z8 i V2 {7 ~
4 o N2 ]* U8 `+ [5 k+ ^ $this->file_size = $file['size'];+ A; o1 g5 l3 H6 P
: i, }& H4 H) y \! n9 ]( M; G. Z: J, U& e& B) E7 g" Y
$this->file_type = $file['type']; {/ J8 k" t1 K2 D" i
1 ^; @2 ^7 w2 N1 s+ e# A4 Z3 t
e3 [) ]% _5 ]
$this->file_error = $file['error'];2 z# m% v) W* O% t, e# I
3 ?' p" B# B" U0 v% ?/ s$ ]7 ^0 \) m% B! _) f6 n
- E+ `6 B% a2 f' U d( F' {( m5 A, W
6 ]5 _& ~3 {! m( ^5 G8 M. _8 q3 Z0 A$ m, \1 Q- {( @7 i
}
+ ~! M* }& S9 { # b, z+ T m, a( `
+ ]1 @% C J" X $this->userid = $_userid;
+ v$ Q; \) A. U X+ m# N
( K: W8 T1 v) G) t+ s/ V0 p
4 I3 B9 D5 T5 w+ ~# N/ C $this->ext = file_ext($this->file_name);
2 _0 l; ^' t: D3 O3 A- R# O d9 T
2 E: ^" W6 C( s) N' R: [' Q
/ }: J- A7 k/ O W7 ^ $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
) ], X6 c8 b1 D
\) B8 B9 s' g" ?: q! e2 [; e* s7 e' O/ g1 {/ C; D, [) H, v- R& q$ r
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;; Y7 r, k @) g3 L6 ~) p
3 O- F: o4 N& B: S/ f, s0 _
6 D2 E" ~2 J3 U' G
$this->savepath = $savepath;
) H# m+ v' F) ^( M5 o
3 Q) j c7 O* x/ {
2 g8 ~2 z, m" m0 T $this->savename = $savename;1 @3 N- d. k) Y' r4 c; W. C! Z0 m
* j' H' F3 [. b1 t
. @/ } S9 c% h: _! R1 G& @$ J
}}& w+ B6 n6 [. ] I( L# l6 n% ?4 Y; W8 O
. p O$ a( G6 n5 v( Q" \" G
0 R5 R- g$ x+ K, H6 S- ] 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
+ o* e, g( w t: T& Z 2 H" f' M; b) t3 H" C- I+ V+ N1 V
7 x: z" x Y4 D: D 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
5 W w0 ^. q; D: b3 I6 Z
2 N/ O" T" L n' Y9 z B- T: |1 I& ?
$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
* j# c3 M4 s, e" M6 F 5 D# P5 y% w: z& p4 c
0 Q3 N3 Q5 n4 t3 o4 [2 M 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
& F5 g w) X# p( W9 L6 { e \; t; K . t7 C$ w8 i$ x* C" `6 X6 l' N
) V- @* u1 c2 _! O3 W) A, N |& Y0 z 6 m7 \0 ~. T. }9 z8 E, o7 k$ g0 q
" j. b* m* J6 i! s6 p4 g$ w+ A+ l; l" k+ ~: @
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
5 q8 A1 E% A% G* [8 Z& y
& M, I u0 K. q# g8 \6 x
; Q- t6 a2 m3 R1 n# D: {9 N <?phpclass upload {
( j: f4 `6 z# N. K6 m" I1 z - x1 @/ x! X( e8 s
g2 e* N- j6 i# b0 y V' x+ N function save() {
% D9 U- R! ? Z- g, N+ m c % W B5 `( v! f7 k, z
c1 j* |0 w: \- S
include load('include.lang');
D8 g6 J2 Y7 F$ t8 |. N7 ] 6 x3 Q% C& L5 `5 K
/ a/ ~' {; h# ^ if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');9 r4 W. h7 a9 P: S" V
4 M2 R) }; d& \' A2 `1 T) _( N' R5 Y# w
' w' H7 F J1 U! S: p
5 `- G! n \! e g1 K6 N
, Q( ^7 E) a# `; g9 o1 G1 m
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');3 [0 N- i" r/ h
; d, d! k, Q* T6 A) \( c3 Q! r) n; }) e7 }( u& D+ ~
# c1 t1 w% I) m' c( W. d
* b, g$ p, R, p" I: @
' ^" t' f+ w+ P8 O if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
+ Y1 j. F- A% o' n+ f
3 z: H$ l5 }& r" \7 Z( Z2 m: i- w+ F0 J2 p0 Z3 Q
" W9 K1 n% M- b- `( Z! C. A1 ]
* k5 k9 x J7 v8 |& L6 P% D' r( r3 f/ G
$this->set_savepath($this->savepath);
( q9 \; s+ c( F: f9 c( j* ^
" p. }% A7 [; Z
! K5 n7 T1 g, ^ P, S: T $this->set_savename($this->savename);6 R& o" P. {; q5 q: l
8 R4 f4 i. ~* D/ Z* ?+ ~2 E% }8 c" N
% ^1 ?3 L* [/ o+ V, F
/ E# J! \: u: v% P$ D9 M; U- {8 O
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
B0 K1 G( I4 P / S: l, E4 n2 \* ?
& u5 Q4 m: \4 r# g: u
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);- y5 `; \. p, ~# M
: I9 y+ ^+ L: N) x' @; k; F8 I
! c4 I6 q7 S& X2 R1 W" Y1 ?: O if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
2 L+ a3 M2 e- Y1 w) s* C 1 i5 {* s" F+ L4 U. k- F
/ ~. p! x( k0 y4 Z/ | 8 d4 N1 ]$ Z& J M$ [
* j# U; l1 s4 z4 Z/ C! @; p% y% i; ]* t5 t* J
$this->image = $this->is_image();
& j, f' w. M, _. D* d3 X - I, z7 y8 @2 l3 T
+ @* r+ {1 S0 R7 n) j) T
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);7 N& b! C1 h% S
7 S0 K& V/ m3 L4 d
, K. r ~ o# }2 N/ T1 K return true;
3 x7 I1 N- y! [# f2 K+ Q$ V& L9 J* U3 g 0 y% V$ ? d& a8 j3 [
6 X# D: O$ I* Z Y
}}
; W; D7 |& a6 L- _" @- }: D I ) G+ E8 C9 s6 m1 O8 R
: {& i+ z+ U% b7 B1 }
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
% g7 _, V" X5 f7 K. o. H+ _/ O 7 o6 z' R* q2 J, Y
1 U9 }1 |; c2 L# S <?php, E5 \9 v. ^4 y8 O. K9 H+ E9 O
2 N. }; K8 e9 x! U7 a# E( F+ |+ X1 K0 l; T) Q0 o. C
function is_allow() {
; Q* u) ~% k O% ~! j0 W8 v: a( U
2 E. e. F, Y! a8 I- k& k+ A6 Q/ K' C& Y3 k8 R/ F, [4 u! u! m
if(!$this->fileformat) return false;
p, a* l; R/ J U! J" A
+ `$ R6 u0 M3 |, i% l+ ^2 \0 z( R. a: P* k/ \/ d. `
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;" [! _, ~: c$ W& B
, y. r! C9 M; N5 r j& E7 ]; c: {: e% Q: d6 e
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 l- h* J0 W7 E) L7 `' J' D$ b* D- q5 z
: y- A! q3 f' E+ ?7 \' G; E0 e+ F5 g9 d9 j G
return true;' o5 {- Q$ L, m# g; a3 i8 l
- N: L* H7 T1 N$ O
. f* ^, O1 Y) e }/ [7 f1 f8 E& m& n/ N5 y
" u' K( _ [" N( `3 \6 z
" C3 @! h S& H4 h! V" U, e 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
# O& q; ]3 v {7 I 9 ?* }7 ^* J* C7 A; p( ~
! N3 { r0 m+ m' s) \ 接着会进行真正的保存。通过$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文件。
: w2 d! E( b3 q) B* Y # q9 G; ]" E) h
# C9 t# ]4 C0 c( F* `' F: j
漏洞利用- F {+ @* w$ k4 @2 i$ H. ^( ^9 i
/ A1 G6 `% f. o* a ?3 l
0 p8 p- @+ Q/ A 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。6 M; J( C/ N; D" v; c- y* ? z: [
! Y6 L( {; V2 T- x: k% ?$ K
& q) ^2 e, }! T0 m9 P3 V+ G) b r
8 k4 V# t6 S {) t" y, \1 U - |5 f# _, A" G/ w N% r" ]7 {
/ U+ W; p4 }0 f$ V" V
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
( R( w% a8 s8 j2 c
; C& b, A* k9 v$ P7 s7 e7 Z3 ^3 G# f8 R! I
不过实际利用上会有一定的限制。* V# u% b* i9 L' q8 `: y2 I
7 y8 {0 f: C7 R T( p
* ?* p$ d( r+ p$ \; }: j! |9 r: R3 s; N
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。+ |- k+ F+ l5 ]% C! @
* S9 U e) Y5 b" f
3 |! ^+ L" G$ I: M ( v9 y# t' z: v- D ]7 @% l% E0 h
7 L& k1 {/ |0 z8 Q; {
; y$ [' ~: [; m Q6 H
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:! g) l+ O. u r; }2 Y* N
# q, v0 G' T4 |; B
2 ?. f8 @. X0 y: I3 D7 f+ d3 ]
省略...$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]);省略...
8 h2 {8 C- c5 H0 D4 ]" M+ P$ C
+ o! k4 O2 r( q0 ]& I; o. A
# v% }' L* E* y* H" h 因此要利用成功就需要条件竞争了。
7 Q. P* r+ M- `6 p) N1 D; S" y # e/ `( m9 u& B" G- z
& d, h; j9 Q/ g O
补丁分析! L3 U$ n g# z3 d
% B6 L8 C5 @- {8 Y. u- G
" D C" z1 n, ~3 ^+ h: Z
9 s4 b: o* Y, y - V5 Q9 N. b' F: ~
q. L4 [$ R" `
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
" k. Z) s3 y, b n
8 m" _/ B+ }. W+ U
/ n% E9 {; ^( U# S function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}) r! [) p3 I6 \5 i0 Z. H9 R2 n
# Z2 Q: d! {5 H& X5 A# d& g& s, \1 A
' J% h2 }) f0 C$ p2 X
+ ?) t3 e8 H7 ?, P4 Z/ c5 s
# e+ ~( K" y7 ~! U" C# J! T- j 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。/ @0 `, T; s+ R# }. L/ q
. I& C, P2 F% p: H
/ z `1 B3 O* {/ @ 在is_allow()中增加对$this->savename的二次检查。5 P# r- N4 x: z3 f7 r
- {# A5 |- `8 w; ^) S- b
2 b4 t( e+ ]1 R3 z1 {$ i# X
最后
+ J- e$ L6 d5 |5 _6 V: u! f- M6 t" }/ \' I
1 M) a9 _* q; A1 ]
嘛,祝各位大师傅中秋快乐!
# q. t; W0 ~- K+ O& F 9 s# g f* X0 Q/ z
6 w1 k y' K* x- q* ]: h( | 0 i' g8 Y3 L" S# J( z
$ R1 d. q1 [+ Z0 o* d
|