( \& B2 i. v' l. m5 O9 E5 E4 o* x
' @1 G' l8 M }; ]: d: B- n- [
8 b/ P' |) f* B3 ^
" S4 r5 J* |( P R) y 前言
- E9 V# J7 [6 f
" M- O, J) w& I5 l- n+ E% s
6 }& w) z; ~" p 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
& ~0 c" ` y7 M1 ]6 d
|& u% o4 G! H8 R6 d0 U
! m. [- o( f# M6 C% ~/ p2 k' i* _ ' |3 `! N+ g7 }8 l( x
1 N- L7 g( U v5 D
! S$ M" M4 m3 ^' t: d% T) I 漏洞分析
" u! ~% S3 L5 L1 n0 c, t9 \1 i3 {% s
% B1 E5 ]* K# z3 n% ^; [/ u) \
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
. v+ k: b. {4 G$ D 7 x/ ~4 R. _* L& w) p' @
3 }# S0 d" E/ Z( |
0 E9 V# h4 v" U
V3 X/ o* F4 W8 q& P6 B, t7 _1 K3 r9 @9 Q3 b: o7 _: Y
对应着avatar.inc.php代码如下:( I, G) x# s; P; Z
' }' I3 U3 v- y1 z8 K) V, y& s- h+ s- m) w
<?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) {- Y% o+ l+ T$ Y W9 P& w% c
, Z, \/ X# O4 N- a' h y" P
. W( s* R! R. q case 'upload':
; n i/ {" I. S2 D, ]- V ) ?& z" @, |$ J- \* C
) E% n9 I3 H3 \" n+ p
if(!$_FILES['file']['size']) {7 I$ b! ^! }4 h2 E5 w
0 p( O( g1 Y' A* {/ g% W
9 [" C1 d K) J$ H5 b0 }8 n7 u' S0 b if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
2 W O0 x0 N6 b
- g) ~6 Q9 n( |) v/ b7 V8 }5 ]: t$ I! C6 X
exit('{"error":1,"message":"Error FILE"}');8 ?, z6 p) Z$ c' Y5 u
! W; X1 |9 U7 j+ P* I y
2 i9 n, y, o& j9 p3 B
}8 e5 J' @3 H7 v g3 U% [. F+ m. k
' @ c8 Y v/ D) z# @
q E9 N# q+ ]6 _9 }+ q require DT_ROOT.'/include/upload.class.php';
B' F! ?$ S: g0 A6 h
/ r. G1 E. z/ B5 Q& G1 [9 d2 i0 P f. c9 t
~7 f. o- i6 i7 ^* K
2 }6 U9 D7 C$ q4 f
9 ~& @$ t7 y Z; s
$ext = file_ext($_FILES['file']['name']);! j. [5 ]& H! @! i/ [+ j
9 k |% u7 j+ @; V% F
) ~5 s/ b/ z& J7 a. j $name = 'avatar'.$_userid.'.'.$ext;9 n* h9 n0 h' V Y9 D/ Q* K; D1 q ^
8 u% F; Y" F4 |6 }% `' d' J: N4 y! Z! c; h' V
$file = DT_ROOT.'/file/temp/'.$name;
. _' ?' ~/ F2 p. K: E G 3 L( }+ P! b/ A G$ E
5 ^7 x3 B& y. ?& @! n
$ {) L6 ]& p' {1 T& X B7 s& G' T ' { y% H) f$ n0 N
2 ~! `# c/ q, i# R# S if(is_file($file)) file_del($file);
- Z. u; E/ H8 z; H# W1 C ' ~# t# @1 @# q* L' |
+ w$ q. J* v. t9 X8 n5 Z $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');; k$ p: _5 Q$ Y- }$ y+ j Y
9 l- }( u7 q) B5 {7 i; @$ i, |/ Z8 \5 I7 V& }
4 L+ p( ?! i# f5 S5 k ; X& e; B$ r! |" {
/ ~3 m6 {& }/ s2 Q3 M) B3 z* V% J
$upload->adduserid = false;1 F" J/ ~' [* |% _+ f
0 Q+ U& h7 R w! ]- I! e" d+ {( _6 h& \+ w: L
* ~; x9 h d7 c1 [4 I
2 l. ?; a- N1 }% ~( g
: \* }1 c Q9 b% W) N* K# H if($upload->save()) {2 N( l+ N5 D( W" | P+ I& j2 C+ C
7 w& [* n! l8 B7 f1 B% N7 R4 L- ^5 J2 x
...
; G; ]1 l1 c {3 M& i6 C
4 s0 L# _! c7 Y# j3 W
- s* Q: D+ M8 X( a( r) v! j } else {+ z0 f5 I# p3 s
' J- r/ K) J) u- _+ {
$ E4 T; _1 k0 ?2 X, d7 q ...
+ @9 N2 ~- W g5 z, O
+ X1 f' @/ T- A; S3 v) [3 g; @
* l8 o( G( v3 g3 S' k$ G! v6 l" G }2 n' m* V; A% c! f' K$ m
/ d9 {1 D" m6 t
' G8 s0 P" k. A* o" V# Y9 Q
break;& ?, B1 E5 r5 R# m
+ Z1 p, J& Q- `% M- c0 r, @
* r# u: K1 _" h& y 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
& F* a2 K! [" {/ t0 Z$ E 3 c( O; C$ |/ l3 W/ a6 R0 n
9 ?" X4 v) l" G% \5 J
upload对象构造函数如下,include/upload.class.php:25:
5 P* Y, J- ?6 [$ m0 y
/ O0 f' x! D* I( I' k! p/ ^7 s9 F8 B+ m/ [
<?phpclass upload {: ?2 p: x( R9 X3 f& M# e$ a
3 a% f; w, n" e$ k5 u+ V4 F' d* j" l# _# x
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
1 L/ ]% N7 k( y6 j2 d; x! ?! a ' X8 [( |, ?! N! d) s8 I1 r
/ u }+ W9 r" q+ ] global $DT, $_userid;% b% z# s: u( @
# n6 k; s0 |. d, x/ ^) S1 T: v
' W0 |4 _! v% }( _( o2 p
foreach($_file as $file) {. t6 \7 [3 m3 C; G I
9 Q0 f. P( A: r0 H) n) f! n
$ F' k/ B6 Z: ^. q8 C3 n+ u" P $this->file = $file['tmp_name'];) ~* h# O+ b; ~5 e' A
7 R1 n }, R1 R0 S8 p8 ?0 t3 F* Z3 Z3 C7 o$ T7 q/ e
$this->file_name = $file['name'];+ U" d: @* F3 e& |
+ {- a' S8 j# s; |; y# L# w
8 b7 {: T# j$ W $this->file_size = $file['size'];
0 l: W5 i* j3 K; M. |; p% y ; ?; h1 F# D' ~
% [- p/ U3 t8 ?' {$ H# h $this->file_type = $file['type'];' M0 Q( ?8 b7 t' m$ c; D
& S* \( ]. s& {5 u. F, o. i+ T/ H0 \; C4 q
$this->file_error = $file['error'];# u8 b1 a; H/ l" U& I* E
! _2 j" Q- [; n% ~( p" v. |. R
$ e: l c6 s) B1 w
+ H- U ^- t+ V: u, A+ v4 \# ]% I$ E* J
. q" S: S7 P ^/ r( t" x3 T, z8 d% e, P
}
f4 p5 ^6 p% W% I& Q4 Q
( k+ a- ^. m6 w4 R! \; A8 T! W3 B4 n/ v: V- n
$this->userid = $_userid;
, A2 w2 c) r# `. |8 H- F 7 {8 z0 _, w+ ^" Y
; n* u; Z8 m5 b3 t" }) e \9 Q0 m3 A
$this->ext = file_ext($this->file_name);
5 g# z; j" n, N. E6 k+ V; | % x$ |' t) _' T2 z5 t: x- Q: D
2 |) E5 y5 Z7 g3 W $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
; L- {% d0 U- c8 w
" y5 e' [* Z8 _7 z" B5 O0 Q4 R o
6 D/ L2 v3 ^$ `$ @+ `- e $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
& U, v( E2 q: W , g) ~* M: T& e. U3 T
4 u) F: f$ N2 c) H8 K
$this->savepath = $savepath;6 z" R( V( N/ b. [
* b8 r' B; a$ x( k* Q h4 a4 D
% F2 o" G2 T; O4 F7 l) m: Z $this->savename = $savename;
8 {4 t; f+ O' G" G% E$ r" h
- v& ~' X& I6 a( o2 e
J a3 {9 E% V" m }}
* C) X1 \) }8 V
8 n+ B% j* k P+ {
8 ~+ |0 t, R: V! q) d/ { 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。) Z* I' ?, k3 b3 d
* p; i6 Q! q9 K
, U* A% o% B) i5 L
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 # m; v9 s" b4 ]/ j# q$ c2 A
4 q* C5 x' a7 u& @
7 q0 k& O! D8 @5 M2 Q8 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
- d- U0 b, C9 y+ {& _- h" G 2 B5 K3 z0 A$ |1 }# i3 p
5 I6 b- F5 a$ E @" R 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
3 s! |; |+ R: g* A7 W; C
2 F! c8 \7 q0 e# m8 W/ t
$ ?" {. L/ B/ E4 J' \ m + Q/ S( H: i. p' F7 H$ q6 P/ X1 R
% ~9 Y+ {/ N" o U5 l5 e
# F" x2 P) L" }& b 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
$ M3 f0 d; X) T D' H/ Y% | ; w, Q) Z( \. O
6 E' z( n. e7 N) K4 h# } p
<?phpclass upload {- l" o; Q% G' }' L) c# }
$ j5 @: U' H' d8 U
1 i0 T! k: m: C% a* {4 O function save() {7 l" e/ |6 ` N' j" E+ Z8 |" r/ b
" g! H q" ]/ X' ]2 o" C2 S
& i9 @0 Z- K: K2 P7 U include load('include.lang');; a! h% |; _# j8 A6 o0 f; W
5 s, e1 ~5 D7 \- D6 e- a( h; o3 M8 ~. U
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
! M2 \" j& ?% T6 ? . P- c9 Y) F# `/ M/ r3 ?2 h2 ]
7 z! L6 ?; `2 V9 H6 ? 6 l3 }: g# y3 }4 K! F0 ]# [
/ \" s6 Y3 z; E/ Q& R5 m9 @; r9 u, C. \; k6 R0 S$ l
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');2 h" e3 a# V# E' w+ s' a: d2 `
, g0 m- u+ k' D- P" n
9 P7 J. C9 N' X
* _: A% a! z; k
, n9 q D3 v3 t/ s' }! c
. E+ T8 @: `' _6 B& ? if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
7 F8 @+ {6 {3 G0 j2 i: _3 d
0 U, Q4 p& E6 S: z& ^) W2 V
6 Q5 p: w9 w0 m3 u2 Y& G% M# H
, u0 Q4 R. N+ S# l ; J% f$ t |+ F! Q* `6 {$ d
) u% s9 I2 }% b$ ^7 e9 @
$this->set_savepath($this->savepath);
% t) Z0 L- ^4 [0 W3 b/ M8 A9 u
?( q7 Z; O$ ]# W6 p ~8 n& c2 j% C: |
$this->set_savename($this->savename);: a/ O& O9 }3 l) R; I- U+ {
5 o, K& @# J% Y+ U7 Y Z& f6 g; K! h5 G5 m% M# D N7 V
$ ?. ?4 m/ [% z4 f; G
+ ~9 i5 f) F" q& [2 z% X6 L3 D$ h; b3 t' r" p/ c" g
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
' K8 A9 T/ z8 u3 j3 @' H
; ~1 V8 U4 b( `# ~$ j1 S
- e+ n% u( T' r7 {( B7 Y if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);5 d6 j. O2 J, S7 }0 N
6 n% V! ^( G7 Z6 W
& a7 ]" {+ h; c4 i& b' J4 T6 J if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);1 E! Q1 o/ W- F
. D: I6 l8 k3 Z' R* Y& W
l/ y. o ~4 q e+ I. y0 t; u + ^! T6 U$ e" {0 ^* G6 }. }$ K1 H
9 I* f$ I% J1 [
% n, i- O! P: ]1 T
$this->image = $this->is_image();0 Q8 i+ P' B# o; u
! G0 x( j3 [% i
& v/ L8 |# ^/ `4 x2 Y0 H if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);4 O9 @. ^: k; c8 K2 n1 y( C* h# m
% D$ A- V3 b7 D- \$ g1 D2 T) ~% A! m! G. K) k% U
return true;: C4 Z `( y, b2 v% D) _
: E& p9 o' b( s3 U* g6 i k6 n. ?$ p: K5 G% P4 ~
}}4 V* y X6 W; `+ T( X4 U
$ R. |. w6 c2 V% ~0 C; l' P
( y* |" \+ B0 ]! v+ P 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:" e: j9 c+ s- J( o1 v7 [
5 y0 H! f" `+ f
8 H( c; E( R6 j; o6 [9 Q% Z <?php" ]# s# I2 O! d5 N! o
F* n/ |2 ~& ~4 ~ B
+ r) G: W# t( I6 t$ `( M; D' }. u- ^ function is_allow() {
* P1 {3 C4 d `$ b5 Z0 ]
Z4 e# S1 U1 j
, q( K5 Y3 g; F& f4 U- L# P if(!$this->fileformat) return false;5 G& c1 x, T3 ?$ O
9 |" @3 \! y! W: D
1 j7 @, L! e" R. b
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
( i4 Q1 @, d# C/ q% P0 n6 Q
* c5 T% }! I9 B. [
: a1 | i5 _7 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;
/ r0 n8 z4 k8 s9 Y+ N1 t5 ^6 r
: S2 `& w: T B7 u$ q$ p8 B3 x: m" ^- c$ Z& m$ h
return true;+ n$ P0 ~: g! K4 u) G$ c" [! r$ ]
* [5 E6 A0 h; l% O
5 x9 a+ j+ ~! W# F4 I* h }4 { C2 |$ o% \
1 L6 L$ Z- X( _& c, A3 U) e! Z
6 o1 v* J8 I( B* h' H; C: |% ?: J 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。7 |* ~9 [+ @! `7 Y
; F& K3 y, j! l# v4 z) o% O1 f& W/ f: w
接着会进行真正的保存。通过$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文件。
3 j5 K' R. Q* M. d/ _
! o0 G- a0 e& q9 _' \, O3 C
b6 V! o5 P0 j u5 [9 s 漏洞利用$ v7 ^% ~' q/ H3 K3 f, I, I9 a
( ]+ R2 C# h* ~' U/ x; o8 x
$ X# t( N) r# Z! D( S" h: c" m" z
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。. e0 L# |2 W8 K5 m% q5 Y1 j" f
& E, {" \) G$ L: p! Q: I0 I9 F1 z0 [2 t0 m
% L F6 T) o; H5 ~+ S5 j% i
% v% H Q5 {# H
& ~8 G3 N$ F" l, {% b# O 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
/ K4 D7 h- t# X+ A 0 E7 A& D- b0 W9 U0 ]
/ C: |* D) F, l1 ^8 q5 i1 F e 不过实际利用上会有一定的限制。* ?3 n8 ^) V% v( Q6 D5 M" Y' z
% i, V* n& d4 I! D- z; }
( Q$ l" t: c1 ?7 \9 u4 W
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。- U! Q/ p, B2 T. i( C
) B6 u6 Y9 q4 L6 x: L! P s3 u* X" m: n$ q7 n# z2 b
# f! k6 U/ ]5 P7 @/ f
( t* d; G. N. v7 M' A. M( _+ k* z1 o) O
( ^& ^+ N. h/ g8 `
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
# T2 L& t* I6 W' W J) l; X% t - C7 L+ r3 k5 n; ?$ ^ E
5 ]9 Z8 h' F 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]);省略...
% F3 {) M( ?$ A" m5 B ) a& a( Q% u- `" o! ~7 Z0 e
* W# Z6 O( H. H8 f( B2 }0 x 因此要利用成功就需要条件竞争了。2 z( X8 P, S: b% b! ]) r, E8 T
0 l T* M/ ~# }9 X: V9 Y% J
5 Y% S7 [3 Y% r: N3 c7 ]
补丁分析
3 p1 {+ L$ B( F# f1 L; t( Q ~% Z# p2 Z* Y
2 a5 }# [/ Q9 F% M
7 X& d1 T( T4 Z
; d' ?! o8 k0 C3 o. `
, \7 z5 w% q! \) o# i& J; {, m' P( c 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
9 k# Q# {' P& u8 b3 y
# D* M+ ?: g$ W5 ^0 V" R! `% A+ S5 O+ D7 D& \- r' W: I
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
- {; A$ h5 T: {1 }, g5 o
2 O) j# G1 v) t, r! w
: |) Q. m" \0 d: P) c " J6 n* c [0 C4 E) K# B
7 e3 V* o4 n& B i4 Q- }: M( U
. r5 s5 m" z8 X; r( E0 K! N* T$ [* p 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
$ ?' R* q, S+ x" a8 L$ Z. P! }5 i
7 ?$ \' V; @1 _, p# k
' A8 `* F! @/ z! s9 O 在is_allow()中增加对$this->savename的二次检查。
n9 L% b1 W8 [: p
- I/ v6 S& \; }1 k( v X* D
$ `# l/ v: o7 ^* Q4 c1 Y6 ] 最后! ]6 T5 v, n) e. o/ G4 G2 C( ]; |
, P6 z. S1 L1 [7 V# i" L
1 z& v: C: d n
嘛,祝各位大师傅中秋快乐!
v2 c" ]" p. W( v5 Y: n' p: D . m8 U$ q" G2 |; f
( w" ~2 o. B# Z" H. H) Z7 l7 x( D / x9 k* {0 [+ D$ s
+ {4 i) r: J6 r; `
|