5 L9 S" v# ~+ E4 j8 V
% z& S( e5 Y- H/ r- Z5 i9 R4 O5 ]
) u; I- L2 j( b5 t) B9 @
( \1 V1 ^/ ^9 W. k/ S; t7 f+ _* [! k- p 前言
+ j6 T% X- P/ D- J
: z2 X" l$ a ^# A9 v! g2 Z# u" T
# w2 `( s( j" W: u3 e, e0 w1 d 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。: g0 V5 U; n& O# \# k2 q p
6 t5 y% B; m! b: O( w$ T1 D# C6 K' ^
' F4 t1 U. M6 S$ S. M. D
7 c& V9 S7 C+ j1 {, S/ G9 d 6 o! R# e- D) ]1 K* ^! [
$ g/ [$ Q6 e: X 漏洞分析4 |$ f- _9 N% E
+ }# r. k1 c% k& p
& j; Z& v& T' ]$ b0 T 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:1 _* E* H; @* x# T
4 ]' \1 D! c" I- d' C8 ^
( V) ^8 ~! a; Y! w ~0 y
, T( `5 f$ A' n8 f & t) S8 H! F. c. F- k7 [7 s' f& Z
( Y& K2 J5 z& \+ i
对应着avatar.inc.php代码如下:: z0 B& ]% @, O; o
" r# Y! o, H# z1 ^$ p/ L5 c7 m+ d) E) ]3 c( E. g7 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) {
: O& M$ T# K7 [) f, s9 U: U ; f& K2 n, ?3 b
2 Z) X# ], ]) p
case 'upload':
: J& Y7 J2 D5 ?7 q* p+ \( Q+ b' l 0 i9 `1 T2 n/ \8 r1 s( m
& j- Y1 _6 y/ _4 ?
if(!$_FILES['file']['size']) {
/ Y; L4 E0 F* Z
6 [: W5 f( t8 `. L- L/ X4 ?# ]2 X4 y
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
$ z9 ~8 `, i' g) L
. o7 C7 J' Q& _( c" e9 B4 k. [) t) C0 C$ e: f
exit('{"error":1,"message":"Error FILE"}');& D4 m( J; Y1 E: Z) z# x
3 j) s% B3 y& ~3 n4 M, L# @0 V# N" f* b0 w6 Z8 T9 C9 L" w
}
z0 T- ~- }, }8 Z' F V
( L0 Z& y% I& {* t0 s4 S$ z) F6 b, n; J. d* @1 k: ^
require DT_ROOT.'/include/upload.class.php';& ~4 j; P4 h3 @ y r# }* f- R
_+ V) V# s- W# F, Q! {
, y8 t" p* q+ E+ D7 \+ {: U , F+ y- ~3 T: S+ o/ Y
( B0 E( l; M" {' E
4 S0 \; Y* I: k $ext = file_ext($_FILES['file']['name']);5 Q5 E0 P0 v4 g, y
$ ~- Y( b" W: a% D$ o* b8 t- G! U
$name = 'avatar'.$_userid.'.'.$ext;
, b, r# M! K4 z; E, x6 i$ y$ m# i4 q" m
: }! R) g9 n+ c F; l3 A, |4 `' O" m+ H1 u* `
$file = DT_ROOT.'/file/temp/'.$name;
- u ?9 V# d! q - S8 O4 _" x- d7 e/ i, H; c
6 z' y2 Y4 l& M. Z 3 `9 b! ]# |& N1 {3 u
7 R$ r# Q3 n, @. u. C4 v$ Q6 p& x. l
5 P! D% P e; N+ o( N if(is_file($file)) file_del($file);
; a( s$ q9 v& V6 j
" @5 c( _4 s# }* F$ d. i$ d: Z# R8 x3 T w; o4 g
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
3 d8 r$ z: k, G" D, T
1 E+ ]! Z. w# O
. w+ w+ i2 J( T; z/ Y6 d1 Y' E $ Y+ d5 R% v) \+ b; q3 A
) I% z3 J& @0 }/ M7 \9 Y8 L) N N
$upload->adduserid = false;4 ?- x+ v& ~: N7 x) H9 l
) Y0 Y1 y+ h5 b$ m4 C. h% s# H* o: T/ c
7 ~; O( O# R. M; s7 {1 ]8 [9 q
2 D* r/ Q1 g5 I6 ~. |7 E
9 P# m8 u" e. S, {5 y0 o if($upload->save()) {
* M! j0 U: S% o! \' b; S& m7 G : B7 L. p+ {; b( s
z$ ~& M: ]; V5 i1 ^% J; d2 [ ...
; h2 U! C% u- G
; o# | F& n8 [* @4 [7 O/ k0 Q$ Z3 \* I) u" p
} else {
F! k; ?# T: T4 {) B: H
' z/ k0 W3 c5 P: G) t8 c1 P
/ P- Z4 |1 H2 L) P ...7 O) A4 Q- L }5 K& X( c
- W- k- P! O7 B: i
' q& u; I6 j; y1 b% y! l$ d }
+ M5 c3 K1 ~8 l; P! B" y
% M) E2 K8 ` b) H& q
, B2 b" [6 Z7 k5 U break;
$ _# u. D4 O9 a$ y % K& ~! n$ B- O7 T$ ^
0 B4 w5 F2 m9 Q) K2 ]" s @9 g" C$ | 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
) h/ c* ?+ V. c; F$ H7 r 7 g' g. y1 T `7 r% u4 L. g
3 @0 M5 @2 I6 C ~8 e. U9 Y' m* E
upload对象构造函数如下,include/upload.class.php:25:% U4 A& @2 g* [. T8 J
5 j2 Q* {" B v! \7 J
. v) E& K) s4 b <?phpclass upload {7 h% T; A5 X* ]; T' O
8 n6 z& s5 b0 F6 f7 o& A/ F/ n
k: h- U2 m( z' V
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
: h' e/ p. m p$ m9 z, N* Z
% x; F. _. s/ H+ _1 K4 _7 c( I
global $DT, $_userid;( P/ x, Q6 c" z" s1 C$ o# g- z1 ]
1 O$ V; R; B% K, D2 Z a* N
* _; W1 w' s) X/ }, V' U* j+ z7 Z foreach($_file as $file) {3 F6 D8 i. `, b
" F) `0 [* U9 v$ B' n
k c0 o7 G1 o: K$ u0 M: C $this->file = $file['tmp_name'];( e: ~ S4 p- z2 I
) o, i( F+ L/ X; f- f
1 a |* H/ d8 d0 e N- p& E" Y9 E
$this->file_name = $file['name'];! v; K% Z& ~8 w6 |" R; p
/ u; r A6 C/ \+ ]2 `
* Q! [6 i9 H% @) M A! b& w $this->file_size = $file['size'];7 A$ K9 K; r3 ^
- A5 y) N4 _; O/ B5 H5 d, p
+ M% y# j, O* d5 }) d $this->file_type = $file['type'];
, E& C8 y8 R; B" G5 j % y. `6 }* v K' V, w
: ?+ K' T% O1 n $this->file_error = $file['error'];4 n$ k3 U" E, {& T
3 D6 x; i6 P+ L3 H+ V& n' D9 \
9 S. \% A- |/ z" ^9 k2 j
: T5 D7 A$ k( b
! `, i5 d- C: V# i* }) \
}
' K6 A3 S& N5 M7 A+ K
+ Y- W7 {: R I9 k( _3 k, G
7 k# O I' y3 n& P$ z# V, O- x $this->userid = $_userid;4 H% Z% T+ P' u9 ?
0 }# A/ @7 c1 Z& e1 m5 }% X1 a; R+ Y
; p+ e: p) L0 W $this->ext = file_ext($this->file_name);" K7 L2 p- W' A; o% d
# p9 d1 i2 @ e3 o2 G
8 p. r+ c. c& D; L $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
4 |6 H5 K* D- \) O0 h5 F3 F
2 f1 c2 Y: k: |( i; Y$ e* e! `0 J1 }- c2 R, D! g1 k- k4 q" P
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;+ m. {. X. C: K( d
+ |1 Q! n8 N- v: X
2 U' m* D$ z8 H \, y $this->savepath = $savepath;8 E" _8 a0 N) v3 \
$ b: x8 N- _0 b" n; d
. F! k# w. A( M7 `# g $this->savename = $savename;
/ g/ \& u! l; l
6 {: [: H6 r& y! n0 \/ u2 p9 ]# [5 l; Y' s7 w6 F2 S
}}
8 z, ~" a) W8 T- m ( l. T3 c% t/ K8 t4 A
/ q* e5 r$ f" W+ q# t+ ?, Q0 b! c 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。( ^2 Q* a( Q; Z$ j0 n3 S
+ i* a5 Z7 O F8 W* p, S
+ A* t4 d3 r$ G) R/ M5 l 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
4 l" B% I, A) f' C2 c2 I( Q
. f$ e4 ?$ H! q! U$ m/ B- A8 l: x% I; G' _4 s
$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% t& G. s4 b, s( @
( O2 \7 y3 y7 j4 D
2 B: j, Z2 }% H# v* C 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
% p2 i7 h* f- A9 p# Y; k1 @) s+ h $ m0 z7 X# }+ D/ v3 [5 F4 t; R
; s/ m4 ^6 q {7 V" I, E8 t
* Z: R4 j3 [5 _: k4 Y
9 }, W6 x6 i v5 n! @9 E, f6 j" S* B! ^9 B+ b3 V- p
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
) U2 M( L' m, ^2 T, c0 s/ C6 t7 ^ ; j6 Y$ Z7 `! u8 L" p
/ S2 w6 ^9 {3 ^0 o* Q8 @ <?phpclass upload {
( @; S% p$ J8 ^! W( \' h ; @4 n* Z; P: U2 Z) ~
" p! c8 Y" f! h, g3 q3 R
function save() {
r( k. H5 M3 H# E: }: s+ s
! v0 r' o) U+ \ r
9 }) O2 O) d3 J) @ include load('include.lang');
5 V$ w& w9 c1 n8 E: r# Q- r+ q 5 v: _/ h% }1 T9 F( ^6 h2 {+ h
& w* S7 a5 [8 t8 Q" b) {" M- n' v if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
$ J, W2 [9 j! N' h8 e2 s
. Y( N. v9 C& E3 ^& {1 s- t+ D1 F0 n4 C
5 t( N2 x$ n& U! n
/ P, E, g$ v5 {9 w3 f; ]' [" N- }
1 F" }! @% c+ n* ?2 i3 d6 q+ p9 @9 o
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
9 Q8 Y+ @0 k7 J2 U5 W
/ }; N/ D% k* }5 J$ N$ g7 d/ U. `1 ^1 q2 Z4 Y! p
: v) W/ } Y) J 1 k3 ^. I! x" {) G- ?( N! A
2 {, F2 y1 R9 Y2 w: X if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);2 B. S& a2 T U% q4 u( o
( r3 K8 b5 s. B/ }4 w, q
) Q7 F9 b$ x7 \& ~8 e$ S
9 Z, i% ]2 w8 |
( W: X; C4 U. @" f9 a3 M5 \
, p2 S( N/ q! @0 H
$this->set_savepath($this->savepath);, t( D1 g# [- B6 f
& f# K* ^' ]7 D' H
. V# _ T2 \, w# x8 ^9 X- I $this->set_savename($this->savename);/ I) H2 i' U' s
8 I% G; i9 S M; l: ]( K
6 s3 J# a$ Q0 x$ P |8 p
& x) ~; Z/ ^; n6 x) P# u 7 s; x' {. f2 l
3 G- _- v* @6 u/ B- Y if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);* g8 u5 h. C" O# a
- o6 U7 Z3 E1 E0 z9 x
' T7 z- F0 A* I7 |8 p( V if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
. F1 q6 ~3 l; J/ C5 a
! B, d5 S. B* Y6 c( s% d9 S1 L- p3 i! Z$ M1 R: U: o0 s; b9 ~/ }. z
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
* f; C5 s! j' ^- V2 |$ g, _
9 }( [" B8 n7 M: l. q2 u; h& ^/ n4 q6 W) a) J7 O/ y
) r1 K# |) Q/ W+ X
/ O, _- Q9 l; C5 i$ x. v
6 u0 K) E0 X6 E $this->image = $this->is_image();" M! k5 J3 y5 S' E
% M7 z$ T9 J) T
& E4 A" U: u0 e& ^ if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);, [8 J- t5 g, s, w+ \5 U
1 I# X4 g: F7 h! v
. \% R! A9 q1 f D' [" y return true;0 V5 ?& K8 M' @* ]- R
3 `6 L0 O& w7 v5 x6 c* B& T
- ~+ E" |3 }6 |2 |/ Z/ n
}}
8 q7 V# C3 H/ E
1 m4 Z& v( y4 i2 k! Z% z7 ?
6 {: u+ ]3 U$ {5 S 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
' n* m3 ]3 p9 P* T7 f* p0 r }) o
- {8 ^! J& e. C% _/ E! t- R R; W2 W
<?php. q) d5 {9 R9 X* {
, @/ u# Y. Z0 y
" E& S2 x+ ~' H$ [7 H& p( L& h function is_allow() {3 N1 ?; S5 u8 W: N" U
3 d ?0 U, Z6 ?4 P! Z
( e v/ |3 I8 j8 M/ J$ m+ A if(!$this->fileformat) return false;1 ^) B* a, n! W# z5 s4 B
5 \+ T1 _* i* t
2 |# i% \. l! B5 z. q$ _. } if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;6 ^+ S% z* O5 e& D4 Z$ B6 g- u
( Y( b) F: l; n& H1 p3 a9 h! p, V6 g, G% k4 E$ T. q8 u
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;4 y( V: e% x# p: N8 K& ~
" ^& n7 J8 `' ~/ L. P
* a- \: H! u% b9 c
return true;8 x* W) ?' `& y2 q( V! [
" v: |1 { x R9 j% M
) {7 t: H. o* H @
}, t2 i1 B5 l- n$ a8 \7 n" `) x! ?
3 U7 V, y* M0 _& D
% K0 A+ V5 N, \; x" \6 |6 O 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。$ ^8 z# I0 ?8 ]& U
4 \3 v3 g1 z7 t% r
0 s7 t4 q7 D4 d) c& x( _4 S* l( g 接着会进行真正的保存。通过$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文件。2 n7 l4 V+ |! o1 K0 ~- j6 |3 H1 f
7 k- R: @# P1 m( M. l3 G$ `9 K5 I2 |4 o$ `! B1 M3 P: F8 K
漏洞利用; D, ]( P0 n: A; l/ v! V
$ k3 F: D( V! {/ P, F
5 D8 a; O7 C ?
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
1 a7 J) _! }. f# t- O. g _, R' s9 u* X7 ?, C3 [0 I
( i6 C/ V. [7 {# e: Z " b. B8 \" Q7 w9 s# P
6 P0 S# A6 y* D9 U. o$ e
: e: @: g9 G$ O8 U; C 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
/ K; p; P, f( a& O8 ?" p 1 W& g. O+ h1 A/ { ^7 z% v: T
8 z1 G5 _* b" P( | 不过实际利用上会有一定的限制。8 d% P& R3 b S3 K
3 L. g9 K) K3 b: i
" Q+ N# c' ` m- D2 Z# ^6 g
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
+ x0 r4 v v7 _* u. O
& C6 ?- X" g% m' _
- I1 \, f# a' i' z7 U
- ?# f1 w" v! |* b. @8 @# Q5 Y
$ @0 C/ @: j* |) g9 x2 N* C8 D% G
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:4 \6 O2 ]$ E+ S; U* g* d
/ R% ]# n2 ^' H
! h H8 u7 g2 p/ B& 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]);省略...0 }) V" M) @2 G' Y( a
M) v$ ]: P8 \ V( e, [
* q2 O) d2 {$ y 因此要利用成功就需要条件竞争了。
9 L0 N3 w+ ]' r/ g G
, N7 l+ X) L% x) F e$ B' e, P* R7 A6 t2 W6 Y
补丁分析
" X0 H& b4 _! y' C$ l# g4 C0 D7 W
8 ?/ x- T3 |7 D% I7 O6 D
& w8 e0 J( b( }. Q% C ?: i$ u, ~
1 ~: ^% \" O) ~* W. P
) @+ A- b: u0 Q/ T% H 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
G4 A8 Y; O0 }# B $ d% d! K2 e" V5 ?" B
1 }- e; Z9 x, t4 [5 Z8 i function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}( d, y6 I" `) B2 d" L
+ _" j" C9 t+ N7 Z& \7 |8 j6 b
' ^% y, M) v) [4 i, u * v4 L8 X. }+ P# x" r
/ r7 p/ q+ V) ]6 J% H
" |: {/ h" Q/ u. u5 z1 s6 t 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
+ j- ~7 w6 l6 z& |; c
0 q/ a5 s ]: \7 i
9 r9 ~7 M" c# ^! n( ?3 N; z 在is_allow()中增加对$this->savename的二次检查。
- j5 Q& \6 f0 ^8 y7 c: {! n # S0 n C0 D/ s* D7 |5 H
* D. ^# y. b3 a% ?* Z) v* D- _. E% \
最后: N6 @" v/ Z4 R' p* @+ r$ T) ^
7 T" j" x& p/ q) o* M
) U7 ]' l, ?, D. a/ R8 ~ T5 v: w6 ` 嘛,祝各位大师傅中秋快乐!4 T8 D# X% K* G4 l2 Y# C. |: O3 J+ S
6 K5 w6 j+ d) [3 d( |# f) {/ j* x' Z! c* w$ g6 a% L
1 ~( f0 i- P1 k$ K+ Y7 i7 j, S
; b$ x9 _. J# K" h$ h8 b' y0 `
|