~1 @- Q& V/ d" @1 \* W! z
: s4 {7 y* y) r" Z$ C+ M
9 ]9 j/ K* ]6 y+ Y5 j/ l$ U1 j4 @: U% X
前言
& D3 A7 o% [# |. b* W) j' Z/ }0 _1 f
F1 E1 E8 y l% k/ G" r# C
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
: P: V' q _( \; s; g$ A. p2 J
5 D2 O4 G9 K# B1 r9 r8 V6 P
% D4 K+ F _ T, c' O : i4 N; k( r j( Y
3 c9 [" H# S. ]$ j% f
( y0 G8 a0 b, @6 E: G) f
漏洞分析
% B: D O4 ]7 ], B" o
; O0 S9 B: q) u! L4 B- Q1 |# _, `- s! h; F$ a. U6 T4 i/ f$ O d
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
3 Q$ i: c! d& _ ! n n/ _( D5 ~6 e& a- w4 p
7 y3 n! G( W$ ~, C
+ x8 O) a9 Y6 W& u/ o # N$ [% z+ R) N0 c9 u0 M
, F. {4 t0 r) P. w; m4 z3 O
对应着avatar.inc.php代码如下:
6 Q% T1 H- T+ S# v1 O. p * c/ {6 E- @6 ^5 z- y# `1 M
" u" }, x* ^( ~% k0 o L
<?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) {
- \7 f. t0 h# Y i) X
7 O. A" H& Z, z4 `0 ~6 n1 s( Y9 r; C) i3 M3 L# X) l' Q
case 'upload':
\) v m- U! `9 ?4 S+ {4 U 4 d1 U2 d/ `& i: n* |% X
5 ^2 k% B, T0 A0 ^% b% `' C% V if(!$_FILES['file']['size']) {
1 g! G' G' E0 v1 v5 ~: `; @ ( ]! @& a) |; w/ j+ `7 b2 } F6 ?
_; {8 ~$ C" Q& t if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
: L" S$ L2 f3 ?$ `* g
( p8 h0 G# g9 Q# V- x
" h1 }; w' L: N! H$ n exit('{"error":1,"message":"Error FILE"}');
/ D& H: p$ T) E" b; m' m* H+ S8 T $ D' m: a3 O: B* |9 M$ r6 i' h. ^5 w& I
6 r. E7 H3 Y1 F } {* D) R6 C! A2 t
: @* f% R# N0 h# G9 d$ p7 }# e5 `( R6 G, L
require DT_ROOT.'/include/upload.class.php';
5 ^( e6 M9 t+ Q8 n6 L9 v9 d5 B 3 X1 v+ ? i. G. |& D
. L9 b3 P9 }! D$ ~9 g8 m
! D& y9 l$ F4 j ^: p2 C, n
/ ~1 o; i9 v, b- l+ v0 H5 b" ~
3 g) K' R& Z$ U1 e1 |; r $ext = file_ext($_FILES['file']['name']);
2 a W: y6 A6 }% C; A! P 6 \/ W2 h2 h- F k' Z+ C2 }6 {8 E
$ \% L9 O4 b" ~ $name = 'avatar'.$_userid.'.'.$ext;1 w. f8 v. e, R( q1 I! ]" C+ n
7 P2 N$ i N" {- V) _% I
+ G' V9 C" @0 m4 n) n6 ?" _5 _) n $file = DT_ROOT.'/file/temp/'.$name;* e$ P7 C& f- S8 e0 h
4 P2 F7 e. r& H0 K
3 O& c1 w% T- P# \! n 5 E L: c1 f4 o& f, T" b. O
! m/ S+ d* i- ?
+ e7 h+ Z' Y; l0 U3 Y f$ ~7 H if(is_file($file)) file_del($file);5 j( Q6 w3 X; X' B2 k6 g
# [" }" S- f! J. [* n
: M0 F1 w0 f& s1 V $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
Y, C0 w1 Z6 B5 f3 ?
* U4 G: R2 B2 v4 M1 ]. [5 e! d. ^8 p+ q6 f4 V+ @$ |
; Z+ {# M# F( [
- t. E! D" [: C
0 X5 c. s$ A" k2 _. z! |6 ^# W $upload->adduserid = false;
% h( {2 b; O) B & f8 B8 N6 R: i5 Z
2 J$ J+ |4 K1 P
) C0 q" e0 Z" V' r
9 I1 r9 H; U% m! f" U0 r
2 n/ A' O" ]$ m9 d. s if($upload->save()) {
T4 i6 Y; J) I9 r' ~
4 n( q0 y$ i. Y2 t1 J0 M
/ c$ |3 z$ v. U- P; _! Q \ ...
' u( }9 y, G5 F4 I0 H; Z6 M
0 n4 a& o. A, d' o
4 K. Z- H: S" W0 v9 K } else {
/ M+ c0 c# K8 P
; z5 @1 Q1 U7 j8 v; e! E- H, p8 f9 ^) w2 |
...; Y+ b; c' E( n2 v9 u6 x
5 Y- D$ h- c6 M( i* x) z
$ v: i1 V! D- E. ] }
) w' d7 C% M& ^' A: J0 k `2 ? % ~3 T" O: E" H, h
" d/ H# }1 b/ G; D1 i3 w7 U$ ?9 k
break;' z5 N( M& S8 P, ~
& p! Z# ^9 |# ] B! i* D% A) _
1 J) h* T' Y( u- I2 _ 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。, A0 U) ~! B/ ], f3 y
" G' z! o. S/ x: o+ \
! b7 z6 X$ J: P+ u/ R6 i upload对象构造函数如下,include/upload.class.php:25:6 n* x* N9 e( }# @9 U
0 T; p: c+ Q% ?5 r1 d( g3 y/ B$ N5 \3 r6 S- Q
<?phpclass upload {) Y( K9 [1 K; B+ c a2 |3 D: s" R
( k& Y' E5 D1 [: f! Q! c5 U
' _" G0 @9 O) X function __construct($_file, $savepath, $savename = '', $fileformat = '') {
( Y0 d) ?# {' s- r9 e. u
7 N/ J" A1 {7 Z" W- O2 b/ Y; N# H4 x+ c: c
global $DT, $_userid;8 I2 }3 K t5 }' F7 Z. w
O8 m' l s6 P3 l V1 b% d m( e8 ?0 f& Y
foreach($_file as $file) {% z6 I4 J6 {" l. c* n6 x5 ]" D* M8 W
# r4 r9 |! B! p
" L- V; \* Q5 X2 X$ v6 S# z $this->file = $file['tmp_name'];6 a) o, R+ m. A2 P
+ b, z# S$ Q% \7 C
0 ], G4 p5 d+ u& J/ L9 m9 O $this->file_name = $file['name'];) _6 ~3 B4 K! _7 P* W
9 i# \' D H) s3 B. X7 y
% S* D* H; z: y3 Y3 ?5 A
$this->file_size = $file['size'];! e8 `1 N! w% ]8 }0 F
2 t1 P5 Y, K& m- F7 Y( T4 z8 b1 C: t3 o
$this->file_type = $file['type'];: I4 G! J+ o# d9 t! `/ [
4 {! h2 K' g* w9 c7 m' Z
4 Q$ V8 e" w4 P8 _- j $this->file_error = $file['error'];4 e6 k5 p0 e7 F% N
( s9 Z8 ?! X' w# ]
7 B, Z, _4 X! T5 T0 ^$ L* Q9 a) F \. z, o4 X& d: L4 ~
6 g8 X' I9 M/ K& h# K/ o
1 b( ~' E! ]7 K5 T2 \2 J0 }6 C }
1 F5 J, q% B4 U$ i9 q0 s# ] - q4 Y9 R6 C+ _6 [: U' q* p: N; [! f
3 I8 j6 }! w" v8 g; O% p
$this->userid = $_userid;
4 T8 E; z" ?: D" J( r; L4 a0 y: u0 ]! u 3 z6 Y5 C, V, t! g2 h& \/ [, {
% b) I; H0 A) ^1 T- ~9 @8 q $this->ext = file_ext($this->file_name);3 m! d3 u+ J9 A, a* ^2 C6 w
# {2 a$ L/ ?1 H% X% r: U. T, V
" q: s( M" c4 L! J1 c+ b8 D $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
* C. i8 {) o2 z! |0 |
$ d! r5 Q1 {+ N/ a q9 D9 P8 F, b' r3 _
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;; T( k8 [/ u- v! x+ g
$ Z9 ~7 W3 P( ?( l% `4 c6 [2 V9 O: F
$this->savepath = $savepath;
7 c! @9 O% A9 I! L/ a# s
" [& k! V* p% w( J" K8 L* N; `% y- S% p" F
$this->savename = $savename;' }: K, C/ x* b' ~8 ]
$ H( k& w: L6 K( _2 F3 L. A$ w2 I+ f$ a$ } f
}}
7 b* f, s( P6 z8 |* O7 V7 g& c . N3 t0 h5 [+ N, y( H
& |* m7 N" H' G5 |( N: F! g) J" o
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
7 p* h$ Y$ J! @3 I: Q : N0 }8 r/ B) e+ V
7 D4 ? V |; J& A' p
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 4 X! O5 s5 Q; i: x) g) l$ v
6 ~- J2 u! b! n# v& Y7 `7 S5 e6 X# \, y
$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
% L6 I/ X% V8 K$ d+ p8 n
8 p- Y( e1 {# W/ b7 Q8 @& n! W& K- h8 G+ Z
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
- T" e6 b2 X/ Y) `
' i0 j0 }$ ]/ e* {; ?/ D9 e# W
& N3 a) T. u! H
. q7 m8 q' d, O6 z) u+ I 0 n& d2 R, S) U2 e7 @! Q8 P# c S
" G. m+ C. N5 r$ s& s7 f4 n1 f 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:: j E+ o5 X. f1 ]. L' T
! D" F! b$ U4 E+ B) F0 U: p. u
9 u6 K1 q. ^0 X/ Q1 @" l& E4 B- H <?phpclass upload {
( ]! Q' N2 z2 ]. I& d' T$ V , o- D; p& a* V- }! ^3 R ^
# o2 q: D, U) b' s% q" v
function save() {) Y9 |5 I9 x; d; A5 J& s* A. K
( k$ @$ b Q5 W$ a) n% H6 c) m# Z; g
% w6 P9 _9 O3 X% V9 c. \. I
include load('include.lang');
9 o7 C& ?: z. _ , g* C t# u+ g2 @
, Q0 U. t& t0 X0 t& F% F
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');. m* Y% \$ d- b2 h
' o E! ~: s' m6 I& `9 V
U) A% T( d4 \. w( F% s- R
. e4 H2 p. j4 d' o. _) t
- t6 w$ }2 V' D! p* J+ B+ P- c* ^3 m8 J
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');4 g# L/ G! f: ~9 m6 A* E6 n e" `
! ]0 R/ J F0 f* T/ |1 g! N
. N9 ]9 B6 K/ i3 N* T$ X
7 w _( d8 E& k) P
$ L2 b1 d. J% T% F
+ R' P1 ?9 u& r- p
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);4 P5 Z) J: X+ a. q
( w1 S3 p% S9 E, w( r
/ r8 u" I) b$ J0 c3 v" T% s
; g* {, P7 S: A% a8 Z/ H ) i2 S" \8 J: g; O
* J. o$ I+ C/ z! _! `8 i( J1 Z
$this->set_savepath($this->savepath);
, e+ M( G/ I( l- M w
7 P8 [( E! T! Z2 Z
# C2 P2 q$ }! Q! P4 V $this->set_savename($this->savename);
. R( ?# s9 K u. o
2 n1 |+ i9 {. Y/ s( o9 H g) ]
% S. O$ I! a0 T0 a! k# P3 L
+ d) _' V5 c9 r, e/ b& E
' s6 g8 J$ G, S
; l+ A# @* z' `* }9 D- b if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);0 B% ]. d# M9 O M% v7 @% i$ T
: f9 n8 O: v+ I+ r
- r, t- ]! f3 r2 b5 Z" a7 h9 ]+ p
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);5 ^) t3 Y6 Z1 z+ T! V6 b
. S9 J( |( }2 j$ p
/ e7 T+ X8 R; Q# Y
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);: j4 z; e4 S8 A3 B( p: @
2 I+ ]! {7 s5 F( m% t
+ J* j+ O$ e# b8 Y }' e4 ^% W
7 t6 g+ j1 q) s4 m0 o7 X/ c/ z 6 A4 y7 Q; V: B" S; y& u
& ^. u2 a) x m# T l $this->image = $this->is_image();
/ v6 \+ V5 V% t/ D % N/ j. D; S& G" v
: F; a1 U2 I" J6 a
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
. x- `* { Y$ ]
3 O6 M) U7 O) a4 k: Q1 Y# I! y$ T. q6 r# [0 \
return true;
! _$ K( X. H+ ~* C) {7 C5 X3 X : u3 l, d5 C4 h6 c' R$ Y1 D/ x
V* }8 A& }+ T q% q; F4 ?/ Z" ^, O! b
}}9 y- x6 D, p% o& l- o
0 E- E- q7 d2 e. U, ]$ `& E; y- s: ^. b6 z* x0 T
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
) G( W: ^0 ~* e0 s: a7 i ! W+ x0 P2 i( t' C
+ A7 U. e( Z$ `2 W! v6 D6 Z. Q <?php
; {8 t- r$ B& f, h/ R8 B8 ?
6 d# a4 N% p! y; f' S8 \2 v, R! }/ i0 v) y* a4 R) J# ^ I9 \
function is_allow() {6 }4 M0 `3 y% V0 r3 q
# W' @% W& z* u
1 p y1 `1 n o& ^' ]
if(!$this->fileformat) return false;
$ D6 C2 u+ _1 H# m) ~- G1 { E5 u D( Y: ?& [' _7 L" T
/ G; K4 T; `2 f" Z8 m/ t if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;% }0 Z0 Z. l6 J% q& I" h
9 w( s, b5 W$ }0 N. \! i
, \2 w! V& |7 F; i5 \& {* k
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;5 T) A% g ^3 v. g
' e! @5 B1 R. j8 W
M! S$ _8 C1 h9 [ return true;- S# Q' l& [! o8 G2 K7 B3 d5 t2 ]
8 Z9 B% F+ r0 m1 A/ C- [& E& m+ L
; u0 E7 Q% A4 N
}% A# J6 C" D% ^& u `' {3 [+ N
F& Q6 z5 ^! V, W4 H& V% c( m6 T7 P& }& m% _8 Z* `
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
# m; o9 u" a% u9 M( h
& t* w& ~9 E- |9 V' n0 `" p- n2 X+ _. u) 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文件。
( \8 O3 V6 m. ^0 D
6 `3 ^& }- u& v& Y$ P
- w& S, F( P4 J( d6 o# G. M/ E; [ 漏洞利用8 h; W3 A% ?( O3 O" M* [
9 g% S3 B8 n5 t4 p) V
! `6 e. p3 T8 T5 H( D ]) }, Y t
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
7 C& j+ S. P& A( v" B( M% P
, b6 b+ I, N8 i# p& V# Y
3 H% F2 V0 v4 o 9 u4 D6 o2 h7 a; W9 w6 G E/ o- W
. `! ^9 |; O. G( x6 J' S. R& d3 ]5 w9 R% M5 m
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid9 s+ g7 g, v( p" C3 C! C& E4 _
" T1 B* ]- G1 M0 R: n7 h4 l
6 @) z' x$ @+ p9 F/ m
不过实际利用上会有一定的限制。
% a" o5 @4 Q# E$ Y0 r9 I2 x% i , G/ Z# L( f* k- J; Y7 \
" `$ \3 @, m6 P: w" z$ n0 o 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。1 W7 s" f3 ?4 x- H6 w( X+ {
* d; u* h [. |* t: P
7 o. u Q3 d% T
0 V2 t4 j* { ~# h7 [& x+ M: v 2 u: ~1 s! ]2 k+ i
) b. G: |- @3 B' I6 M$ ?4 i# F- }4 p 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:8 h9 D) ?7 ~) Z: a
" m c# K8 s" }' }* W b; N! G0 D3 f
4 u p' r& q# J$ T& b) Q 省略...$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 E# U. o4 q- B/ z* l% p R
" @8 ?# C6 M. M$ T9 i! m
9 }! N$ X: j% i% Z 因此要利用成功就需要条件竞争了。 @$ Z' {/ A, |, b1 `
4 P+ E$ t, D; B0 H/ _; [$ w8 Q D' {
补丁分析
3 Y7 o" I% T3 V% `5 ]5 q8 ]# l$ `3 C T5 B- ~- g3 l
3 G4 N0 [* |: E& A4 J8 J
" X3 {0 a* z5 \) a6 G
" i: e! k% l1 _+ k* b f
s$ V5 @1 Q! r! n" m: o0 L! N
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:6 M2 L2 m i3 `* Q* p Z" U
" ]" {6 [& ]0 ~, ^3 p5 y
1 }) B T: E/ W; l
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}' V' v8 @1 W6 M7 c
( Q( _1 K5 H: {2 c* Z
% ^" [# z: A& f" M+ c
0 E7 q) B+ e& }. d- u
- U4 n; p2 _8 D+ K( M9 Y
- m5 ~1 d6 A" |: L2 v 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
4 B/ t. n$ v( l" B0 u, B. X1 a6 T
# u, J+ h) M4 v( z7 N( t
& \* ~7 l9 z/ R# y- ]0 V 在is_allow()中增加对$this->savename的二次检查。# A7 v$ R( w: [0 Y( M
+ x# S( H+ A! B4 ]$ r
7 k7 h3 Y. v1 n0 q' u: _- r) R 最后0 h: l6 A9 l7 I. l
3 B: I N3 A* X2 ]" A0 ?
/ R0 F* ~8 s) R9 y
嘛,祝各位大师傅中秋快乐!
v R0 p8 k7 @6 ?
$ c/ G" D' N+ c5 p3 X: p/ v# D3 H& p" t5 g2 d# W6 E2 e
2 W9 e9 Q4 p) f' W& r0 p1 x
" b: E( u1 @0 ^1 ?/ M4 R) @
|