+ y. }4 B4 N K3 _; M7 D9 {8 A. }
# I! q$ \# y5 T0 s' a# t/ |; b) d, l1 U# N- h, t3 U$ y) \8 H
9 D* H% [+ r2 ~$ ?. f1 A
前言
8 f) @) Z% o: |% N/ x- J& K( s' q" c
/ i/ u% b3 N: v( T# w) ?/ E4 A' ?2 k
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
$ j" Q% u- q% ^ , B( O7 N* ~2 a: l8 x' R
" k1 y6 p9 i+ y4 |7 n* T
! n- D4 R" e: A & x7 n' }1 E' c$ b% J
4 b: S; M3 o$ N S. _% d 漏洞分析
1 N- R# }3 Z! {: B
- {5 F8 X. e+ N' n
$ M Y& N5 D- V5 c$ x* L 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
4 a; d9 l" E' u6 V , I1 Q! P8 Q7 ]/ T
% i8 U. W: g1 B) R6 @/ \
* U% b% h+ W) E( J# b! o$ C
3 ~: x' P0 e# I% b% `# [( `9 D
! n% l. O; {+ A0 Z3 _! t
对应着avatar.inc.php代码如下:
% C V$ n+ n0 A1 J 3 J( l/ f) G9 a1 q: v- i/ F4 Z9 ~# F, P
/ m) v! r; _% e5 m; G" X
<?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) {
, o2 n5 a: V" e$ X - i0 d" D) C5 P1 i, T
7 S! N% S* _& i% S) O. D9 T case 'upload':
: t/ }& S* A! B $ z; y7 G2 h( z1 K. y5 Y" l7 d# H
. [8 l) q) r) j4 p
if(!$_FILES['file']['size']) {
" ^3 d2 k: o1 t( O 3 W/ T- D4 i( [- }0 |# z- A6 t
0 ~7 z9 s) P5 n% I+ ^
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);: ]0 F U @( t' Z6 o* F
6 T: }. F+ h% r9 z) y4 `" e
! Z. U" l. ] e7 J/ R exit('{"error":1,"message":"Error FILE"}');1 N) x! k% b( r5 Z" r c+ F U- `* w
2 F r' ?6 J- S6 A& Q ^
# C. v4 l9 f! ?( _ }
3 v6 i0 J/ ?( W) K0 m8 @, D2 f
6 |6 q0 Y" c0 N2 f* g3 @: x
5 Y# M: H# \ O# |, n' z; w( { require DT_ROOT.'/include/upload.class.php';
' k3 w7 r& k( `( b9 B `- i' O3 p U4 n 9 m/ A& D5 O9 B9 {
0 H4 E& H7 ~% C
# y0 K& n! f0 X1 p" h 1 V) ~ i7 @8 Z0 m' m3 j
2 n; p0 O7 T, Y& E, M/ x. [ $ext = file_ext($_FILES['file']['name']);
% U7 V- ]( }* t4 p7 ]: J+ j/ u; N# W , p$ a" `- @2 U P' g6 i* {# t0 h
- V/ q; F4 _5 C! g) L$ n9 t' E4 L $name = 'avatar'.$_userid.'.'.$ext;
" L3 E) y- v0 G- r ]+ i
# O! F. f! L" E3 N/ h# d: M: R6 A5 a7 S3 I( r
$file = DT_ROOT.'/file/temp/'.$name;
5 Z4 V8 g* F: {* t, }* i- s+ F" I J6 w * x( ]" b8 P3 m* G
" n/ j& o' g; P& r
; H% k+ Q' E1 @) F3 W- {: V; g
7 {2 G8 H2 a% a$ t& N
# e: K8 Z" S) _* E if(is_file($file)) file_del($file);
: w+ V0 g; _9 @% C
0 q% I( v* `4 r4 `1 k6 b2 F; j4 N7 _3 B0 n0 G7 g5 e' F" T
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');" I$ H8 V! |# v1 n, m- o; ]
' j/ ^4 x7 B1 K
6 O `+ ~3 x! ~8 \( Z6 |. a
/ T* [8 Z: ^$ n
) T. c0 H( I. Y: Q9 C2 W: g
+ Q0 M) u \7 }9 R9 ] $upload->adduserid = false;
6 v3 e# b7 \- Q+ b) {" { 1 B+ n- t+ ]% t4 I! g9 w
& S/ B* ~( ]: h: [ n
, u( {/ u! F1 [( M, J6 R! x ' ~! \! p: U6 L6 r6 S) Q/ V
4 ~2 A$ v6 }' Z if($upload->save()) {
% t1 L, a7 u( r$ q2 D5 A3 J4 e : P0 S2 @* V# d- y
K; |3 H c) ] L8 k- Y
...# [. H: A$ o* s: Q: ^
5 j, K- M% W* \8 W E- B5 B* r
* N, ~" b' y4 r3 d" U } else {3 @( B% N: H0 o$ F, D0 m& Q: D0 U
9 j7 Q+ S0 [+ _0 u- d! n$ _3 ?4 i3 o
& E/ O/ a l3 g3 v- U+ a, } ...
0 {+ g; k1 y X/ U9 x
: w8 f- g1 c7 W$ ~. g' y7 t, d- R
. B5 d4 a) `' ^% P) L }
% u Z- G9 q2 E' r: ~
9 z, a6 V" E' X* c& a# Q2 C- Q3 e, q) m' }7 @
break;
" F9 e" X+ o4 N8 U& t- I* H5 C + |2 j' n. R" N1 }8 U5 W0 R. |0 `
1 G6 L' M0 |: D& `
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
3 V, L8 p a% q0 A: C. s9 n) n # c! j3 M: G5 I N9 D
4 K6 O8 c( V. \3 l upload对象构造函数如下,include/upload.class.php:25:, i, ?0 j+ @) m$ A8 g( B
8 I% O) Z' }8 |( ^. k7 [
1 Y! d8 ~# G' P% p* {
<?phpclass upload {4 A: f$ A4 U% C1 r& v
! n& z, V- A4 |' r8 L0 t H7 ^3 s. F5 W9 ]% C8 E2 x8 m
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
. F1 ^5 ]( _2 C+ l7 J" H
. Z& [! N* @5 I2 w) {5 ^
, `; l0 J5 c5 T- A8 o% |4 H0 k global $DT, $_userid;
' V$ g8 s5 Q0 M: O) l B' J8 Y# H
& h" I1 a: L: Y( j' m4 u+ o3 g; {' N8 R* K; V
foreach($_file as $file) {
+ E, m: X( f0 K y) h# T
/ p, o' G- Y. T- W9 l
: C" m3 X- @* b. V% s $this->file = $file['tmp_name'];6 O5 y$ C- K7 J" p. T2 I
0 t; H9 ~6 z# i3 G+ K
) O% N! |( Q3 E1 e) u( I2 {9 I
$this->file_name = $file['name'];
: B, e* S; b+ c/ T# P0 ]% ?/ _; S . Z: A/ H2 d, r- f3 ? R! H9 z
$ y. n/ W( Z. |. y" I1 t
$this->file_size = $file['size'];
6 P; Z2 r9 x8 J - R5 r; y; H- k: P2 _9 I& u
8 {, K$ {- O3 p& h $this->file_type = $file['type'];
9 b" b0 o' v- I3 S0 B# n. [+ B, D 3 ]( b( w6 {& w9 F; M
8 P5 L0 Z/ h& d% X1 ~9 ` _
$this->file_error = $file['error'];' A' f7 F: q7 l4 B: {8 s Y
& c2 m! s1 M0 v1 Y# {; K) F0 E* A( x, B( L7 a0 N( N/ L& u3 H
: i/ @$ h5 C* H: P
- l/ x- n- R; x2 N+ r8 K$ c( R" T
' L7 ^7 f A" \4 W" i }! u2 L+ r6 d& }6 m; H( D f7 g J
. l0 j# C. ~' c( J- j9 g2 w8 Z7 m! p9 J- k5 E& A
$this->userid = $_userid;( A( Q! e5 x5 D
B2 x+ _/ K+ M6 \
+ ]5 Y0 P" T x" D $this->ext = file_ext($this->file_name);7 b$ ~# o5 g- Q3 ~: b9 z
4 _- w+ J+ r# _1 l3 b6 ?1 p, @1 t
: B' v% P; ~& i4 r) r! J $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];. k) @8 p8 ^: k/ t. e( |( B
9 L: L/ p9 C! D4 C3 k1 m
4 r# e# d6 {6 `7 h6 U $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;; Y+ v( l9 m# z) V
# { U8 R+ u2 a* S9 [
2 N% A: ]4 i- F s
$this->savepath = $savepath;
5 g( e6 f3 `7 a1 U$ c% F
1 a0 y g, X$ ?: z- e u6 Q6 ^7 c9 K% H! S
$this->savename = $savename;8 \, Z+ g: u6 g/ ?7 ]
; z q, n Y! N- Y
; Y4 S6 A" T* R" u8 q }}
, z1 ^+ x. _; A" i- O- l: R' e 5 p% r% Z( T+ P" A! E; p1 d
2 j% @6 }% D& x/ H. b
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。! P" H* i: ^- h% L6 W
" J5 O& ~4 X7 X& l
. |$ ^1 d/ N: ^
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
* P' n! f- c3 l& I; z& [ 5 ?1 l3 i( T1 g# \
) R; z/ |! H% T
$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$ ]- u/ s& Y, [
/ U ?5 S- } x0 z2 F! N% O" O9 T( j. P& N$ F! F
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
4 T, p4 G! a! ]. k, o. I0 b + y4 u+ s$ z+ B% Y
( e1 i, v) J; P8 j * E8 N J# _: k* x
- Y9 E8 Z4 T: t9 d# A: C1 H# F+ i
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:% ]8 `# y- m9 E- e1 l
& c" }; Q! }' u' r
$ \% m C- b% s! H <?phpclass upload {# l% F% `+ s' A1 s+ A6 ~* E
, V$ ]* Z1 G/ g, F; d/ C, \
+ p- ?" B7 t8 L4 Q" g/ f5 Q1 r' }
function save() {4 A% n8 R- N0 j. [% ]/ k
: _8 D! T1 Q2 B/ A# @% q" q' T2 s$ `; l
* `; k+ S: [1 d% C5 y( x9 b include load('include.lang');
0 a+ F2 K/ G1 V! D
! q- m7 `# A* k& i
$ `, W7 {" g. r' \; S$ g* s if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');6 f5 S- ^, F4 L& o% Z
5 _- E/ m% u2 M4 x$ u4 A& f
: s& e( H3 Q% D! H0 e
3 w2 A8 i9 `/ c" O: ] ; P2 F! f" t5 `6 ^& L
9 j; Q5 p; }( ~# s if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');# C- t, ?2 g9 d3 M$ I: o
: z' y* T0 Y& K, I* K
$ x9 W3 n# ]2 H: y: v# ] 2 h7 ]# U$ _3 [8 E0 i4 }6 ]; ~
1 o, A, R7 g) Q: J) ~7 p5 {, d
' V8 W9 J; K. n2 d7 \; p if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
4 i0 h5 u# W8 [" @2 f6 J' [ : d) ]) R! D* v3 n, b
, D. |( c! E; Y+ [* F# ~9 v9 m: Y# i
+ _5 ?: p* Z5 c: q8 { ' z, p+ R) Q- z' o2 B
: ^6 p& w2 M* L# M. W- `- n0 ~% X' z $this->set_savepath($this->savepath);7 A8 _. ~$ ]5 @
' m1 R5 _1 k$ A* u p8 v
" G. y* x( w o; t3 i% y
$this->set_savename($this->savename);
' Q& o- W+ V% }# c$ w2 v | : F, W0 \+ t, w0 j( ~ X) V2 M( a2 g
2 Q; B" c' F; @' v& j: G6 u . K% e9 r! a& c1 L5 ^% {& B
5 }# {0 q, S* ~* T2 i7 U z4 Q, Q" A2 \* T4 H- g
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);+ G4 J2 v. q" C) Q
5 t9 U9 j' f5 p4 U5 i/ _
3 c) U& ? b$ T' s5 Z0 z- b+ Y if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);) g+ C- o" O7 e) q* d
^/ E7 h) l! h" P. t7 t: J
6 f5 g0 [- M1 O) f- [& F& t
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);3 C w1 V# [2 y, I. S- \6 M
$ `6 @: z3 k) y; ?% [( @. `
0 O8 t6 X* m; ^' s
) p4 C R! A: r
2 s7 g1 r3 b2 Z' r- Y, G7 Y! w' V2 [6 U& `, w' D& e/ o0 r8 J+ q
$this->image = $this->is_image();
! E* A6 c& S T% O- i3 c1 X; Q
7 }8 G% t$ C4 m6 u
1 M4 y" O1 J1 `# b if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);- k* b1 F0 f: a" f# ^
# Z" `; n" |) j5 q: O: N5 j/ {' R( F* C
return true;
& G/ v: ?' @2 ?2 n9 _3 k
6 }/ }: y$ D( R; }4 h6 o$ @. k2 u8 y( v, e: H! g
}}
+ z0 x, {( ?+ X- t, Z0 d4 c: s) } 0 }# m6 H" L" W8 ?; G% D
% c8 u2 D w% G+ R
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:6 ]1 y! [. q2 N9 h% \
0 G" {: m6 @' ^2 V( s
1 q U7 |0 W+ } <?php
5 @1 x: p8 ~3 y
: v3 F! i! d3 T4 G9 a% T$ a& [2 H/ U- W1 O
function is_allow() {
, u7 L3 P6 v+ d5 ]& q2 i, }1 q0 k 6 i. L+ H- l8 R; @
9 Q5 ^; |" D7 V4 c" r/ R
if(!$this->fileformat) return false; C5 m) F a- B
+ @/ _# u" k! o, ~1 C; {
# J' r' R7 \! K% S7 _ if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;3 f* g. Q6 z& A
, V$ y' K" D1 r! |# j. z B
) H$ T6 L% K1 X" U' `( C
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;/ S9 C: [. x( W; C) T9 K# e |
9 Z& U( K( V% a r# J" `- K# X* B1 R
return true;
b3 \: b. X5 K. \& `% _
+ ~! G. G4 G; ~4 H8 z3 h7 u, l9 M" E0 j! L8 X" M' f
}( F! G H3 K9 W- v2 L; J
* S2 T3 S4 Y* g1 j! ]8 Z; {; D1 B
' K9 a4 Y/ h3 Q4 w( `- C) k 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
) ~2 C2 V+ _/ |8 H4 M! i# K/ ` 9 G( Z& v/ p1 J2 C2 z8 q' a
; |9 ~- i8 ?/ x; }. Z( j
接着会进行真正的保存。通过$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文件。$ w$ C( Q0 B6 Q5 q% S6 R
7 y8 A0 a1 t( i. D1 u3 Y n0 i& z
, D5 l- ^8 u' N2 o. f 漏洞利用
/ v) \9 C8 n. h) ?( A" W" [$ o2 S- Y! K* m+ ]8 J+ l
3 u! k. c- j' T0 S! b) j 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
) E. i7 Y/ U* T+ Q4 |7 E
$ G S t9 q l7 ?+ J1 \4 q( i6 |9 e- E: _3 h' `. U
6 Y- n, q- h& I6 a 3 \, U0 M7 |0 w" f" j
% U; C! B* I+ O" ?9 {5 Y9 o
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid1 Z; g5 y5 i% E1 P1 ~2 P) b' a; |
4 f/ R' g" ]6 N- k: s+ V% o+ d4 x9 E# `6 V% E- A8 m* c4 N$ W
不过实际利用上会有一定的限制。
* G9 Q3 p* ~2 f5 e
6 }3 P U1 [; c4 M9 t; D% ?, P4 z
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。: {) s' ]' X i. _; P
% A/ U# P0 _7 P) W! _) ^" z2 v G0 @4 n
9 h+ e# C: M! h- q7 I
/ U0 T: b. \' u+ o% I B
( T Y8 k5 @. j: P" S* t# L
* |. z+ b5 H* ^7 c. O) j# i) n 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
! C' T& E4 m; |, [: _! s" i
0 ?: b" R) t2 q* L; g3 F9 y+ b
* l9 C# h( Y' 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]);省略..., a" A' A; D) N6 _. m- m# L9 n' x
$ w2 _: L+ ?" S" u" q( ^, T/ L
7 b; {# }, X3 Y! L+ b1 G& o
因此要利用成功就需要条件竞争了。8 }- m/ o5 @' m6 |$ Z2 X f
' F: m& B$ Y; Z* T! L# o+ V. g7 W6 D {" ]
补丁分析
# ^! p8 ?" g5 X0 `" L1 J& p/ ~8 i. d" }) a9 ?% i
- U8 N8 B4 l1 d* |. M
4 P. x( A) s5 l- h: [ 2 {# U" A" m Q; o% r
3 j2 \3 E+ Z1 w0 e0 Z
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
0 t& x. f' F$ R" j" u1 P ! w" _" k% g! [4 X% D& [% d
' O5 ]! T* }- z$ N: {6 [3 f5 x
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}& `2 w& N( w b9 v+ e8 b8 }+ W
) h; \& M. X& N- R/ G- `9 i) A2 S" J9 z; l! i$ e
2 [0 y4 _+ z$ K4 ^8 Q
& Q- H# u1 O8 N9 M5 G* k: Y/ ^, ^, H% |, l& j
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。5 D5 {2 B: X3 u L
0 e1 x/ Q, | l1 R* ~9 Z3 X+ k E9 H
* G0 n# a, h: k" E3 @6 K; u4 } 在is_allow()中增加对$this->savename的二次检查。1 v, O: Q/ a3 ]7 K) h$ u. @; p
$ f+ P& C0 S8 K! @5 p& c
5 t: W- v6 t8 S4 v; I. |' G 最后
5 L; l7 i) x9 | N/ [; y* o5 n) s$ n2 J6 D0 _
8 ?7 s' m1 J# m* j( ?& }8 V 嘛,祝各位大师傅中秋快乐!- d. w, F! A5 k6 @
& l: S/ }; G2 B) ^8 k( D' y$ I: H
: x- A6 g: R/ j9 \; X $ \9 p9 _& u! R) q8 m/ V$ @
, k6 R) i. k2 _1 H
|