$ k6 U' M* R6 c# f w
+ Y, G# J1 s4 Q
8 Y$ \8 D9 z( c" J, [9 O6 J
# l- k r3 S, y2 I- w8 F# s& e3 n8 |
前言1 [* e/ N2 k3 E* E @1 ^- I+ i
6 q5 T% a; P C, ]) ~1 @ B$ K. v! H- T3 Y* w4 w( Z' x
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
7 _# W; M9 K% g+ L' S
2 K9 V3 t- M9 I! [3 }! }! Q0 U; x2 ]3 X" ?' y* b
) _2 N j! C6 m3 ]
+ R* x! c' x; t/ v& S
% s9 e' e- H( Q1 I8 t6 f) b( @) e1 _ 漏洞分析
& ~( _( y0 [# p, p7 W: T/ o' h. a F9 H7 \/ d+ u
3 l. p4 e& d; D9 a- I4 d 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:0 u5 C4 h2 ~4 ]' r$ d8 R
/ k# _6 _: ]# m) j, a
+ N" T D- Z5 J! T5 c0 \ . r |9 o9 Y! o) `4 r* \
% ~% n! k: ^+ _" K. {( L) r0 s. `2 K3 F
对应着avatar.inc.php代码如下:
: [# U+ X5 u1 g2 x) I3 F# p
4 I% x2 q5 q+ d1 Q" M
" r) R; Y2 ~$ z/ r- E, I <?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 u+ Z4 U# g* C( V {* _4 P; N* ~3 D $ ^2 u* o- _% h9 ]" j6 ~( y
, X. J, q8 v) U! D
case 'upload':
4 b8 U) q& n% z( y: L. f
5 O$ g* o! v" g' T' C" v) h+ ]' \# o: K, m5 n
if(!$_FILES['file']['size']) {
! w# N! s: L/ l2 g6 @% ` * c W$ f8 [) o6 R2 R
) D( T2 Z9 C m- b7 z if($DT_PC) dheader('?action=html&reload='.$DT_TIME);* d% o% o* v% Q q% k2 t
6 G& x, c/ c1 c5 R# l2 {
4 w- H% ~* ?; x3 L9 O7 c exit('{"error":1,"message":"Error FILE"}');' n& E. [" w* J& o9 e
2 u4 c+ a& r! u( h# [" c+ o
! u# c1 B( m; H2 e }
0 m' U3 H6 y- _7 a! d" | % s# \2 l5 q L9 P* G( Z$ j
" s: L1 w5 p+ g2 j$ E, j0 G3 l require DT_ROOT.'/include/upload.class.php';/ T. K7 W8 Q% \1 W5 x U
% {0 v; ~6 o1 m7 w8 o% ]1 g- ]
4 Z8 l- h' F# t# r0 O5 a' ~+ s( y
5 z1 z( S T M% r, ~4 A6 j2 C) H1 P
% X8 |5 z, D% `* q4 R4 k" I4 m+ ]6 [ d: K" A2 ]
$ext = file_ext($_FILES['file']['name']);. ?+ |3 g5 m) s' J( t: L
. Z) z' V4 d* Y- \
: n6 r0 I! h& y$ q
$name = 'avatar'.$_userid.'.'.$ext;: Z! E ?/ S3 g
; k( q* Z- v" E) B, n" R9 _5 e! b( w+ M/ @$ ], Q7 p" v
$file = DT_ROOT.'/file/temp/'.$name;
, v3 f o! E5 {3 J F1 m
, q* A3 X9 M, d; ~; {
& T1 D o* i; t1 \% U- u
; H- s" q j# | $ `' _. r" Q( w& m- X+ s, x
1 y- D) u) Z( c* B2 C; H
if(is_file($file)) file_del($file);
. D! J; K6 U0 }" K 9 a/ t/ t1 ~# B2 r/ {
1 C ~) B5 `( }) [ $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
# B8 T `5 E, p6 z, I( A 7 N& J! N! A& O, M0 q; K7 h+ m
' L. Z- |3 r5 Q; c" Y3 B
/ k; v g$ d2 d2 n
: |7 ?' z; f# D% S$ ]" ~2 i+ j
8 n/ T" f. x' H0 A4 T2 m5 u $upload->adduserid = false;. K O7 k* f) X5 v4 Y& Q7 ~# L
4 i" }' j- f) I: n" e) \. w! M
' q1 Z9 O* l/ L) f# c, ]& o, @( T 5 |/ i4 X: C% X) A4 S7 u$ }2 ~
, j! v" ^2 R% o# _ B$ f
9 S) N9 A" G: q+ e/ I; G6 o* t( s if($upload->save()) {' V! n2 [2 Z! S/ `( ]
' ]$ [1 C# _# f& Y; v1 U0 x* ~9 K& M
3 J: c5 a- Z1 `6 T( B
...
2 \4 y( n1 U1 S& l2 L & @9 ]1 {, ?; {' @
4 A6 z0 k! V, b$ a8 {0 i6 m$ \
} else {
) g. y' p- p+ B3 d: R* s f- o& I( Q: G: O! M7 B: B" w+ A1 d
! S7 h/ Y6 d, ~" ~, E
...
% T3 t% n/ R0 S( C
6 q; k$ Z0 h) d1 X& G( v) t+ @# k$ `, D, R
}$ g Z* g; k P$ c3 G
/ c/ a) n4 l; t# h$ U, |8 Z/ }2 A
+ j. A, h% }# A1 C! b% M break;
$ a. i7 R5 y+ P
4 E/ t( R: T3 t8 I" w' P
9 k& W! K6 z) J& X 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。4 l& L; S/ ~5 k
7 r' m6 X* v7 G, d# @+ D2 b5 I% j( r* R& n _2 j
upload对象构造函数如下,include/upload.class.php:25:
4 X1 i2 X/ p% G2 c( W) r- m1 K% p
) O+ @/ {" N* |0 \$ Q+ m2 h O1 q8 G1 I
<?phpclass upload {
# ?- I; ]! g! @) ^
# i; y) `2 X# |# y" m
; {$ h0 E1 ?- C9 c) O6 B8 T function __construct($_file, $savepath, $savename = '', $fileformat = '') {9 {4 e, S) ^& e8 L
: u! E5 f. a% M* y9 R& j+ r* G9 g& u4 x
global $DT, $_userid;
! ^! f- ]. b y; o5 q3 q0 M , N0 o. t2 u2 {1 n& X# G
6 C$ E5 s! q. |0 g& h( d$ b, M
foreach($_file as $file) {) Z7 o3 w5 c2 X4 K; }+ P6 g
8 a: v! N$ E$ o& o6 ^4 e8 x" O3 ^+ \" i# W0 q3 [# k- e
$this->file = $file['tmp_name'];6 t) e% y# c q: f8 o% k
, b: l1 {; ]6 _- r# D% D( E- G# n' z f+ Q0 @) _5 o. H1 g+ g0 @: x
$this->file_name = $file['name'];: n3 C/ H6 |; F( g
7 R4 j6 B6 y" e; S. L- w
/ X3 g0 b6 P- e+ L6 v& V- r $this->file_size = $file['size'];9 n, N& K0 i4 X9 n4 i, `$ t ]
; C. w* O' @7 S* H
6 u4 e- a# r/ G3 J $this->file_type = $file['type'];# h/ ]0 C8 G# `5 W
) ^3 f$ @- u% R8 L$ |8 s' A1 f I
0 q( l; \+ S! F b0 K% t. l: {4 t $this->file_error = $file['error'];
) E; E: o9 ^4 V/ `3 p
4 {9 t% z5 w8 b# _ K0 G' o0 ^+ N" C; J E( \% t$ j
3 |9 n8 U4 V, Y + E. S1 n& L% b3 a* ?, j; J% W
# C3 X) k) a3 f O5 h: J
}+ C; i! Y7 o0 Y: q+ W
+ ], ^7 d' {- v% @1 H: d& Q
2 ~# |$ w# o6 ` $this->userid = $_userid;2 _/ @1 c$ e$ D* S& p
$ X% b R( q! u, a% M1 D0 U9 K0 o3 m! [/ j; F
$this->ext = file_ext($this->file_name);0 ^' E3 m/ i* v$ p
2 c. F( z# b2 K0 n! P
3 N7 {3 \! a1 u& O: R' W% h1 M3 b! E $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];+ x9 T, _$ v. z# @. S
2 |6 I, d4 N% q3 p
7 k5 z3 T+ Q1 z6 i4 |" @+ C' g $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
# ^& C: W7 S! q8 }3 k+ c
1 Q5 \4 T0 q4 \6 v
3 F2 L$ ]6 y% y. x $this->savepath = $savepath;
+ P. _! {' T( J2 X7 b @
. k2 N* c4 i5 N f8 o0 {! F/ v% [! U4 h: l/ j6 K
$this->savename = $savename;
! D) j* Q( C+ r8 Y: |5 x" w
' N' o9 u; _+ m" v8 S- Y6 U- c0 |" p; G7 \; w& x- B
}}
' t' _; X! s) `0 o7 z- p # X- i/ b% k# ^: t
! d8 O1 H Q Q% D" _* w 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
5 p7 l6 i! `) y$ J3 Y2 O % T! L$ Q! s F% B% ^
) h9 `, t3 [5 J+ k6 B+ @ 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ) G; V% C3 _6 B: G X* [
0 a5 S2 }2 T" l# Q7 |" x7 Y. M" s" Z( q$ o
$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
! Q( J# m7 M( U/ d9 g( i5 S, M
1 m6 l. G. ^; Y2 N+ r7 U9 T5 _! S9 O! s! M8 J$ t- a
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:1 Z U' e! q q( V; N. D
8 b6 t6 ]. V9 }
. h' b$ N0 J) @. x' n# V, m, X
. l I& i+ ^. w( A. W* E' W { ; o8 m! L3 G+ a2 A& L6 t( R4 p1 w
! D2 \0 l! {; ?2 i
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:4 i( {7 \2 c" A ]
3 T. R# R) Z7 R& b+ ?) k. A5 h
& r/ J) K$ h' F$ ? z- l/ [ <?phpclass upload {$ I% `. C! ]$ t2 B8 \( k \ D
/ I- G! N" O Y
8 A$ U) d( C! n5 v; {2 H5 N% t
function save() {. l$ T( k1 }! X/ I* B
/ ^* m0 r- v* {" c1 s: Z
% l; f5 ^! o& @2 ` include load('include.lang');% e# k9 ?' W& h4 C) B
0 b V! L. r: p+ I6 |5 {, K
6 S9 S# W1 y' Z9 E! p( w3 a
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');/ a* k4 I* z |+ o
1 p% @# _* j% U8 ^: \+ }$ ]- a$ }
# ?+ R. i& g: Z6 V. r9 ^; G: }# p
( L5 E" {* E* a" C- S 4 w' Y9 u* K4 Z4 `
# F; [6 V' w9 v% Y* P if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');/ l. ~0 r4 B0 l/ P, h o0 _/ O3 u
1 X$ y3 r; k+ M# z3 z K
" J6 a3 `- c+ f# D. v# L& Q* ] - H5 i2 s* ?. V, f7 @' J! a! N* F
! @5 H& ?. q3 x7 G$ m
6 d9 b! w5 x, V if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);; @, _7 H9 K) s! ~1 }; I# L ]
& w5 f9 U z8 _' F/ }0 o2 \. n2 h
0 Z$ ~' V4 h9 }. C6 p7 f" o
! S! J4 v U9 N+ F: P
) B+ |- Y* o) `; K- Q
* C+ z9 M% O0 _3 w+ j
$this->set_savepath($this->savepath);
/ S5 \! H- Q& J8 d! P$ @
1 `9 o- a- M7 b9 d1 b% q# r4 L
' W1 W: `% C+ O- J5 m $this->set_savename($this->savename);
( p- \* f7 R# p! j3 E / @) Y- V- P6 W
6 P7 Q. K3 y! ^ 9 y- o5 k( Z- I L0 K
/ w+ w- J! P% c! _
; |( ~' |: s6 V- c
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);) j7 I8 I( |$ v( n; O
( z) g0 W! D$ o3 |
( e7 }# a/ d0 W l! ] if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
% k9 ^- D+ ]! k; b
4 G3 [+ m0 \' H' W" j+ y: Y6 Z; @! j1 r5 ]. Z6 M" t) x
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);" n* i: `# R" S( G H" W
4 L8 [# Z' I I' F% G: P/ n: ^ z) x6 G) Y# H) ^ e) B
! i( \; x" J+ R
" O8 t" ^6 a0 A& J! [" o
: z( R( h) B$ F( _: {: K
$this->image = $this->is_image();
$ H8 I& {) k& U6 b8 M
3 _% W7 D4 |" A+ j9 u( o+ G, k! a: @
- F. Z9 u& V. L* B8 \* U" x$ x7 \ u if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
) e; e ^$ `3 b# t, r
# T( a4 _2 r" y
/ e2 i7 O$ c0 k: S+ T" ~ return true;2 {) h( d; C2 N" C+ X
[4 ]$ i5 K; Z d; o
, |7 h" H. Z; y }}! r4 T# g6 x& d4 o: H- p' K
0 b5 ? \5 j6 h# ^: N
/ f9 f3 J/ h* e: t9 A8 Z H8 M0 @ 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
2 t/ c6 Q3 ~, M. f( S 8 {- o! k( Z2 F% Z) B8 x/ p
/ U' O6 ^- N2 l/ a
<?php
- J7 x* J) u1 s5 l/ H
3 R# O' i: j2 v1 w& g5 Y a6 Y: g3 S" \4 e* i$ f( k
function is_allow() {0 k) p, K9 X& T$ U/ I' B! `
4 o: P0 z6 i8 X, ^0 v. N5 x$ h; s+ A# k; G
if(!$this->fileformat) return false;% N0 r5 T$ T b
; j }8 W/ L/ |- a! L5 R' y) G/ F
# v5 @$ s5 D4 Z( U4 M" j0 K8 J if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;, g0 Q: _) V8 Z' p. G: {) L: r; Y
+ \% K% E9 i( \" r, z
: H4 v: z: N% |* @' Z
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;
2 D; L" @. m0 c6 q6 i9 u1 u' v / x4 ^& K3 b3 i3 u* _
) ^1 w! |/ r, m/ |" Y return true;7 [+ }6 t4 W# ]; D$ k, g- }
9 g/ h, p. m. f' _8 @# D. D
5 c9 f6 i8 l2 D$ n
}
1 q% ^1 i% x# H o$ P
/ i9 D4 @5 \4 b; h/ Z3 l' l2 S) ~: {' v9 R- {! H9 M
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
8 @0 u0 P3 E% L8 k9 `3 v/ j
: N% I5 {* P# P K& X
4 d( B7 D6 R6 f 接着会进行真正的保存。通过$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文件。
# M! H& f5 Z5 f2 c* I! i* \4 v
8 C$ }5 }( g0 K2 I" S- N# q2 E; [1 M/ @+ Y; w
漏洞利用 J5 q: C& e1 {8 Z( b- }* s
7 P+ F: G i \
$ C0 a1 t# f! `7 g9 ` 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
; j% C9 z! `. {7 F
- s7 K3 [3 ?, t3 a0 P; t u n* h! A) K: x8 M" s
9 e! y- ]6 C3 @, G
6 ~ ^ v, y1 O% {7 p" x) p
0 ^, U; m0 p1 U 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
: ~& B. X+ M" y2 s# ?6 F/ _
4 @% K5 Y! [6 l# ?6 s, E5 P6 Z1 I' S6 p
不过实际利用上会有一定的限制。5 y! [* o# h" `: Z6 k- l( C4 [2 k
6 Z6 l# F& P2 s V+ E8 v* i
8 ~% k+ l# c+ m7 |4 Z8 e 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。1 H2 p" b! b; F' {! i
; G0 Q, N. U4 a3 V
5 P( a# H- H6 Y. e, F
" G4 b5 B6 X- U1 Y, m
5 O: S. |7 P* D
7 s- m3 B8 `7 y% U" Y 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:" f9 f' |( a5 `" i9 O- T
* N, v/ d% X/ S: ~ q' S4 t
# Y' \6 k4 v, ^: F, G7 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]);省略.... F: _, v, L% c9 Y
$ ^5 W, ?# C1 r: C# J4 E
* p/ Q+ O9 | o/ I) }" I
因此要利用成功就需要条件竞争了。
; ]+ c5 H2 l: v' @0 E " p# s4 h% e5 l3 j8 S1 ]
7 N: h, v5 _+ h% L' q4 u" f; ?
补丁分析
0 X! E! m) e1 N# D
* v% h6 g7 a0 G5 j" {* L
; N5 d" Z& z# e. i
& U1 _$ g7 s# u, F% x6 p9 k/ \. G& | w+ `. {, l* y. T6 O/ a
t4 n# [ x; {6 J' j
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
9 ]4 F) }0 f& O0 R8 D 9 m. W8 D- ^* @( o
) P& v1 X9 F: e6 V7 t t) n4 u function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
# ~6 q. ^) q/ y2 y3 S2 ^ 5 x* L; h8 F: ?0 g: b8 f4 Q( s1 J; \; R
, s {$ Q( y1 i5 e7 y$ i- a: ?% a 9 Q A# ?# s7 a$ |5 H
z" U# G6 m( m. s, q; L, m: V- V* f4 F4 U# D% i
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。2 o9 e1 r+ M5 u% }
' a- M% d" c1 S1 Q
/ U# o8 f/ V% b6 y& o 在is_allow()中增加对$this->savename的二次检查。: V& i k- C2 C3 q2 m6 D6 T( d# w
- I* j4 {" u- ]
2 B! ]' n: Y$ z# y8 L) y! B 最后
7 t* m4 M2 A$ Y$ u/ u8 r" ~' l, ?! U1 m& F; ^
% y f! L1 F# p- L 嘛,祝各位大师傅中秋快乐!
. C; u* i4 v. g5 D! n* b- Q/ Z( Z: M . h: A/ @) Z' {6 x* N9 c. F
( D: R+ o+ H! P " ]* j- D7 F! r
6 D% j6 c, }2 m
|