$ H" ~4 Y) d8 G, @& C3 a- H
2 f0 y# T! q3 x& l, m7 X4 b
) g/ H! q1 d* g% e9 [* |* G
$ {8 q% U* N, H8 z5 f2 r
前言; z- B3 F0 j8 [9 i/ O) J4 Y9 W0 T5 R
5 r! ]; s+ M8 [3 F# `" B5 F9 y) N W9 a/ K+ P5 W1 z
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
8 c3 G0 I4 N8 |" R7 W / ^9 I# a2 B+ O5 q4 |( m
: _$ S1 i, G/ p1 y; k- K + B" j8 t/ P7 h4 P/ Q/ {9 x2 w
3 e# c: A/ a, {( ?' g; d$ |
! d; O, F6 w) L8 G 漏洞分析! g& X# S" e" }! b
8 R" {# Z7 M5 Y" `' |3 s: H
, m/ a i0 W4 V% M 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
^: s: M9 w+ q! w3 v2 N2 t# u
: ?( P! e4 A/ L% }! c
]4 C8 \; ]; q/ g* G. M F( I ; f6 P+ f h" ~+ t% `& F& H3 e
* g2 p k' }$ u, x% d1 E& q; a# c# J% o3 m# x4 k1 C
对应着avatar.inc.php代码如下:
$ E, l( \6 o: S( O& i & y# W, }$ _* g0 x+ b
; b' y* \& H; n
<?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) {" a% u- n; B" `* Y
. G. F2 M+ N7 }/ M }! \: N; Y8 O; ^& C$ a! J
case 'upload':% h. h: t+ D5 Z+ X, z
4 B: i: i0 M2 A% P' R# q0 T
' `# Z2 d' x" Y8 j- ^( y if(!$_FILES['file']['size']) {, @" F& W) u- b% @- S. x) Z
0 E% Q4 {' o; Q; k
5 S# x; H6 c J+ o if($DT_PC) dheader('?action=html&reload='.$DT_TIME);& w+ Y/ s% n6 R# L" v1 a
0 j9 g! S; L6 _# A8 p
3 U/ |. }% u4 D! N5 _( z
exit('{"error":1,"message":"Error FILE"}');
) v6 w8 @) v$ q
7 G" u$ q0 c: f0 {$ L/ @* Y; D* O6 x2 |2 i9 V. U
}
4 B; N6 R `& N7 w( s; ?. I * t$ X1 u: e' S1 n$ r+ I5 Y
9 s7 J5 T# N% C, a2 ]. _$ M, Q require DT_ROOT.'/include/upload.class.php';7 m' s9 g$ M( d7 N4 }# J
0 t; B7 i u. ?$ {9 k3 S7 j- G- X
8 w+ y6 Q D/ J6 ], O! m( F0 v0 N
a. L# i) V0 w' G% K# K ; W$ \4 ?" w9 Q
+ Y$ Y6 Y0 v/ w- f1 u% j$ w
$ext = file_ext($_FILES['file']['name']);0 U, \% ~( k; k6 C' T% e, ~! N
2 x1 C5 Z. @- Z0 _
& j9 U. ]( C; f* ]: x' H $name = 'avatar'.$_userid.'.'.$ext;
; `: q) @ J7 z2 E: \2 | 6 q% k c6 j! i1 h7 I# M# S
: Q1 v ]! \2 D7 X P7 y* _* x( j $file = DT_ROOT.'/file/temp/'.$name;' O& f% J$ y, E- t4 Z
. ?$ j6 R7 u2 C$ q! A; d
( A- {6 q/ B( m7 b7 ?
5 n( w1 w1 \5 s" g8 I0 {2 P - |( x8 P0 i; Y) F: x
+ Z! X; R" O. {1 \ if(is_file($file)) file_del($file);& E0 \4 R7 S Y( H U4 v/ u8 K' M
: a! O: m& _; K6 _
! e( ]( c0 q5 I& J. t3 d" a $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
2 S% T1 Y0 z: c $ R- i& W. _' B
2 `. q6 n6 w7 x- b 3 U! i$ `- x3 y) O G
1 ^2 l4 K: \* o& h) ^. O
( X: }- z/ s, A3 H, ?$ y $upload->adduserid = false;
W9 K; x9 W; B8 [. s& P
- Z* Z" {- q1 {6 G7 z
* x) H( Z9 s' y2 Q% r 7 Q! F# R! W% l
( p1 X- N! _4 A: s3 a8 j, U+ O8 y. ~3 w7 K/ ]4 i: R( B
if($upload->save()) {0 Q3 P0 l( a# s* p& e
$ K9 s; ]* t, I, d; N8 H. s* K, I8 f* K
...' p- R$ w" {* r$ D2 `5 G
# J6 `# z. {7 O
. E ]# s1 _" v
} else {
. o0 N+ v+ W- i" F: G' X7 b ^2 l
1 e* G9 S( H& v/ J- T" ^/ ?" @) V3 K: }% e$ L6 k) Y3 |/ D
..., Y! A/ K4 X1 ^( ^! U/ y" n/ Z3 G
# w8 z, j; m) c# n% J! m. ?% o2 k' h$ w% s
}+ J$ X6 k, ]$ E# q
$ V* a: C7 t( d0 D- _/ B% a: H9 n5 F, o3 _) u( F
break;. v3 N% P0 O+ y B5 m* S/ i+ p' o! T
; `6 K1 i' r. k. V' c2 I6 ?9 _
/ W( o- d) j8 U, F$ i \ 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。) _! L% c; v; C1 E. ?& { ?* q
3 X4 c" A$ ] Q9 {7 _- S% ]0 o. J; I0 f
upload对象构造函数如下,include/upload.class.php:25:1 g9 w2 V; F: t m) h5 e4 a' g
7 Z+ r* T( u+ Z" n* {
$ J8 z, ?& {: V% Q4 ] <?phpclass upload {
0 o8 U; U! ]+ U! h; |+ F! ] ; n% [. o- x+ E9 ~+ p
" w( q- u2 J* d# J& R5 e$ q
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
t5 m0 z, ~7 O1 d. E- ]
- i+ e; i* s5 I
2 p6 ^, `! D5 R( ~ global $DT, $_userid;/ k+ Z, M5 M) u v4 F
- Q+ X; b4 l# K0 c8 B9 t& S& E) e; [3 X8 X' n3 F
foreach($_file as $file) {
$ b) D$ E$ k0 | $ h. D6 S }0 W+ d3 J
2 D" z' b4 S5 b1 i& ?0 o $this->file = $file['tmp_name'];5 C/ Z* G1 d, z
( j* r* {9 ~( U
0 b# f% n2 D9 t) c0 Y ~5 | $this->file_name = $file['name'];# [/ p! c9 x6 s
0 O4 R& N4 B. m1 I+ t; V
$ k+ P1 Y2 r' |1 B $this->file_size = $file['size'];
6 h% W( b$ E$ P1 o/ t ! H0 }0 O# ?, G% d/ K! C4 d! m
+ Y- g0 \( k+ W+ a+ P* k% U9 w: U
$this->file_type = $file['type']; t% ~) X6 Y7 [# A( M5 a
: b/ f4 F. Z; Q6 Q
4 j2 L6 N* J* J0 L5 h) a6 U $this->file_error = $file['error'];
( G6 K$ w2 | h& {# `
/ d" Q4 M* n# q" W6 r4 \- D5 N7 s2 l. e' V" R
$ Y3 p: ]6 L+ g& w A 6 G3 L t: p( f3 r7 @: d
7 X: d# b" |' Q; o/ }9 o8 A }
9 K, z0 o+ j& s1 p; N2 W4 N4 t
% V4 W3 g" z5 F1 F
0 f3 b/ @6 ~- S, x- r( N $this->userid = $_userid;
5 Q/ @* c. H/ |: i " f+ Q' Z6 K6 P! C( Z' U8 n
/ B, V+ t' S+ [2 j8 A0 n: T8 L8 J $this->ext = file_ext($this->file_name);1 @ }) r+ _+ P9 |6 M& A: y
0 T3 E, w8 q3 X1 k$ t- k
) N" v+ W+ n8 L- }1 V2 p( P% L
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
) X6 J# i2 M8 k( m% i8 o* g & n) h( i. V+ K8 U
/ x. x; W: o I( z
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
5 R( a; P' _( Q% t ' ?3 f& ~& x6 H# F
' O( U5 b% v0 z( o8 n( O' N
$this->savepath = $savepath;
- w+ R% X) c' w/ \2 k$ r0 g3 I$ r W/ t1 J* u8 v- p. O6 g
# W( T2 A% i+ g' E
$this->savename = $savename;
, S# z. A/ X v! x5 J$ J
( e" i" k6 e3 E, w* S7 O0 k# \) y8 p% e, B8 b1 ~, U
}}6 \2 P7 X9 |3 G3 ]9 t8 c, }
' ~& m8 R' y" ~# p) M0 c( S9 U
{ x, r- \7 h4 d2 ~- ?. E7 h 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。0 |& g2 y4 d$ [5 P. d
8 Q: L i* Y( H3 I) d! X1 x) i
* i* s2 J- w9 y! { 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
- a+ G9 |! R' y" E. K
! O; O- B3 m" d- T' V, o% t
; r e+ J4 ]2 Q; c4 p+ u+ r8 I' B $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.php0 a6 ~8 f1 h' O+ c! J2 Q- e1 g
& ]+ I9 ~7 J* d4 C$ T: q. K9 n) W1 E( q% |$ ?
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:- |- k N% H9 f, d* |
: _# Y/ m! Z9 Q' }& B: z1 X
- p0 i- }% M8 }1 D% r6 r5 g0 b
0 m1 D' k! K) G- U& t; F# x
! F/ P5 c- J4 P! ]
1 L3 J6 b- U( }( U, b( A 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
9 D& Q4 }# M6 d2 Q
" G8 D* [9 F" }
0 p' r( D' i( I& P4 U <?phpclass upload {9 |' A7 L" c3 p! @+ w; n; `
& H: j% r- A% o0 h& k, ]& W) w
& }% m5 E3 _, W( e' s function save() {% c: H2 a& [* R3 O- i
8 F% }( c' z. }2 x" x6 R! O, N& g7 Y) e( ^" n' o! I$ B. E
include load('include.lang');. F _4 V D: J6 t% V( p
t2 Z2 p% e" N9 Q% j
( w+ [' {7 L6 O0 m. q; {" u if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
; f2 z% `, ^1 _6 U% o7 x2 R( M . `6 N5 o9 M/ n# M W6 ^
* ]. `* Q5 y% t; O) n! i 6 Q: i0 H( L" r0 m D+ {& K7 q
. s5 u1 ^: t1 V+ l
& m$ V- w- L: b; d( |4 }" h5 E* x* s4 s if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');; p( e3 Z+ i2 U8 A
) r2 i0 [# H5 c6 [
$ |$ i1 R' Y6 q8 U1 w C* u
' t/ Z* U0 x# {+ i% j: D) p0 v : L x7 Z/ D4 Z- _% ]
: W1 E- u& r, W# W
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
% l w1 T X: @" N8 D ( E E3 F( O! o
* g5 Y# z0 }3 `4 e 2 ]; L, @1 g6 r7 l1 Z+ n' V* Z
! F+ V- L& P9 A% J. C" c* `
6 x* H2 W; c, `2 U. ^
$this->set_savepath($this->savepath);
$ ^7 I& H& S' V, @" R, {# Q
; J2 T$ {+ I% Q5 P T2 n! j- ~+ @( \' |
$this->set_savename($this->savename);: _: ?* T/ l, s
5 e* r9 E- |. F' p1 N
5 {" |5 [, T# {: w 2 a, r! B; L( d7 ^1 l& R0 |
. y% i! e! Z1 L& m8 _: D
$ e- y4 u8 n( B' Z( F+ [- a% o0 v6 L if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
) B' n% e4 p% r+ A& v z
/ ]. b: ~- p G1 S4 l
7 \ O* Z3 L$ c! t" j& {2 g5 Y if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);+ A6 ^2 X5 O1 [9 x& n: Z( e: }2 z% |; X
, u% \% N) W! c \/ e
( H; x& C% X( [% x4 Z* D! p7 e if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
* |9 H' n6 {6 _* T# h' I/ O
2 b+ T: |; u: f% O( U4 e" ]* y9 @& z% h( L4 K8 U4 c' I
8 W3 J& p1 x2 h) b( ]: n0 U
) n! i+ K* P5 ]* U9 b3 i' x$ ]& G
$this->image = $this->is_image();
, d, g' X Y+ g1 }: N# P# P+ P
7 y* A1 V/ {$ m
- D3 e0 k+ O0 [" \8 ~ if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
& O; ?; g3 S7 S+ h) b * E+ b' \4 F7 }
, M# t$ k/ P, s! N return true;
* D: _0 b# t1 H# p
9 S& R; @2 e% V9 s$ ?+ B- H# k/ l2 E& {
}}' T: Y# ?* L( N3 z
+ N1 r$ g9 Y. m' E+ Q, P* b% ]+ A* {
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
( b0 y8 I& ] V% W 6 W7 s5 B7 ?# L/ q4 b
* @0 A8 ^$ s2 Y4 ~* b0 }& O" p
<?php3 u h g9 j8 g3 T. z
6 x% j" {) a* ~* e' B3 e6 W' Z& p( j1 b @/ R4 x, o1 C U1 b' T* @
function is_allow() {
4 G7 _3 f9 c5 _* ^7 {2 n6 ~
& @: Q' I' I# M8 t4 [* E5 b
Z( G. v5 f7 e if(!$this->fileformat) return false;/ _! v2 ^+ I' d- W( X# V
1 w9 N) h$ k& g7 k. @* s. T8 H7 ^9 |% T O3 f, E- R
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
$ J, { L: R& k6 \
3 v& F% d G$ b9 L: J
, A: n* l: g2 k; p 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 W! @$ J* Y/ F: D4 H : ~4 O1 p1 J% Y1 L; n
3 h( M# `& f, M+ ?, C) v& F+ | return true;/ d# g% S% p* U0 O+ L
/ Z$ M7 @8 M8 Y" Q8 d9 b6 ]( S
; h/ F4 J; j( v; R }
! W8 u, Y/ l& }9 Q1 `& e) p4 j) B . D8 p) S4 o0 V
! u+ s. F% S1 l: C! l+ U, } 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。7 ^8 _% c- h" K8 W
5 L) ?9 z! F2 p. A9 D& }. H3 f4 y
1 H* b( N1 ]/ L0 q( e5 ]" I 接着会进行真正的保存。通过$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文件。
( ]) g6 ^1 {& u6 J( y ' h7 T$ h7 L% G8 Z, o
. P2 v7 N( S3 r& n. C- ` 漏洞利用
: P' K* K) ]( a0 B# F+ @, r
- |" X3 k# e+ c& I/ }) E- T; Z( C( k6 v; [4 E# D9 i
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
: e' s7 l; r3 T! B6 t/ K. A
% D+ K" \4 `: ?- l7 u( y' _6 F/ }. T& [0 ?
6 |9 K: P0 R5 V2 h- ] 6 k7 m S& n0 T* m9 y( V
! I6 a$ o# m! M- F. s% X- o+ y7 [ 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
* [1 A/ O" F2 ^
3 |' v) l* p' f' v: ?1 P% Y1 M( m
不过实际利用上会有一定的限制。
, `% b9 }3 M2 P8 j " V' B7 ]9 }7 e8 z$ i' o; D/ U
, m1 S! \' [- i. w1 o 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。1 o4 O2 k( d8 e% R2 q
, Z. S; a' r3 k h: X. [! N
Q g( }( @' Z, m
- k d& a/ Q* [9 ]3 k5 y. d* v' E
7 M7 F9 v0 n7 M3 _4 Z
) b2 r7 P. }& A 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
7 K8 Y; ]. f5 V& P9 `; P; t 7 H S1 ?& f0 ^5 M8 }
- X' b. P( e6 ~( P* Y* q5 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 t- S) E( P }+ N8 B1 A8 E' P( ^0 @
+ y9 [9 t" c; [' L8 x" p) X$ G& c6 ^' S
因此要利用成功就需要条件竞争了。
, V# x y2 w4 u! R% g
# p) `# I0 j6 W; d: } R; X* |% D' B' Z3 p) e0 h
补丁分析) T: k# N5 Z0 ^& e/ H: B {: @9 ~
3 y, z$ P. q: C |- |# r# i) Y, V4 }& K u4 t$ l. g' d
) U- b4 r9 s: |6 j6 V7 N
; R. ~+ e7 G2 T! m! K3 ?7 K9 K* m0 j* P+ g" {; |! V
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
& M% m3 ?, o3 N* }: @ $ t5 J5 D7 r6 a
& |9 s# v3 D5 V- ]
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}0 V& |+ _2 C' j8 N' k& k |1 H) ~
7 M% o2 R1 G# t0 ?. t: F: }: {9 n M5 k+ I3 z- R* d( ?
9 B( y; u2 r% P% a5 O# z" W r6 o
' a8 i5 i8 v8 m; N A/ Z9 r" J% w$ _; @( Q$ e4 Z! h$ R8 c9 L
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。' ]" U1 H2 x# [: Y# j+ K# ~
1 ^5 g7 D& [" m' @: t
$ ^! w! T8 V0 m+ c) w1 y 在is_allow()中增加对$this->savename的二次检查。/ [5 ~ H C7 q O2 _+ n
' j$ S1 T, U+ l" a
, W ]: Z! Y4 N0 X/ O& a 最后
/ x4 `. i1 r% q5 y, }2 v% B p3 J# B, S3 v4 O$ i8 D
7 s" a' [' Y6 Z" r5 F+ d 嘛,祝各位大师傅中秋快乐!
6 J; z6 G+ Q* o; D% z 6 I8 ]) _$ Z: ~) @2 @2 r! r( X
3 n$ W0 @1 m" O # ~. R3 m) D2 w# t8 M9 Y
/ [5 A* l; W |; D9 P
|