3 b- U d* z' [; [
% H* r# L6 C* s* F9 C9 s0 Q) |- A) w
* I9 t4 m8 c5 |
. T/ f7 [4 D* K 前言) W/ z# w+ n; A" j
: Q9 K1 q/ m8 Z: Z6 N
. q+ N& B3 F- X) W 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
( f& U' e9 w: A t( X/ h ( s" B& y& i' Y
* w. V2 y) N- p K/ _ , T' Q ]1 d8 u+ o4 x
- f5 d s0 |% z$ _& m1 A8 }3 {% B$ I
漏洞分析
8 s3 X# [$ P8 @, A( P4 H; \- p3 T; g# G
: P) O: Y% ?" o7 }% W3 x% W 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:) J# }' T! I6 c) D6 }' c4 T; I
% K4 k, D7 t4 [( G
, o5 g2 N; `, A: A% m$ m( q 3 F1 @7 J' g5 }- G3 ?+ [
1 K) Y) z% h+ f* j5 U" Q" E9 o: c$ w3 F/ }
对应着avatar.inc.php代码如下:$ {6 O* L: Z+ J4 G
7 R; \7 P- @( k1 y. F
7 V9 {" _7 G+ ] <?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) {# k& G( x& V" e: q- ]
9 f. X( Z! h; s( e" t
- }0 Q9 v+ `2 R case 'upload':
1 |# ?2 C4 v, {) { % D3 D; |1 ?6 ` i9 q' s
4 M4 U7 t# {' l$ R if(!$_FILES['file']['size']) {
% Y/ m l1 d7 F' Q( H/ h( `) V
' ^! P. e% P2 ]; F# ]! J8 j3 |) |7 l% {* u
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);0 O6 e3 a- }) H! U
+ [2 g6 p2 l( q7 o5 {2 r1 G3 j ^0 J$ M
exit('{"error":1,"message":"Error FILE"}');
" W- F6 l) c+ M6 p4 ]0 t! n5 g 4 G, l7 M% w- ?7 H: E
9 K' ?* j% F2 T& o/ T }+ r8 Y8 t3 h& O- `4 `; o4 N8 Z
7 i: O3 w/ K; n/ p8 g! K; ?4 ^' W
: n9 ?7 g) t- ?2 X require DT_ROOT.'/include/upload.class.php';
" U* q1 Y4 V0 k0 Z: P & X8 D" D( o( E2 Z+ X. c0 V
! I" R/ d# I, Y1 B2 ]. C
6 e9 ~6 w, _" p# n/ R9 s$ r# f8 L
# ?' c* R0 q) R3 }* p; y6 L. Q
# ^% A. A8 d* B4 Z- F9 S
$ext = file_ext($_FILES['file']['name']);7 ?; ^- H7 m2 r8 ^
$ m1 p; ^& r- r/ ^2 [6 C; S% |. c |
: e) j$ @1 s$ `4 j $name = 'avatar'.$_userid.'.'.$ext;5 m+ U5 @: Y$ g
- J p) c2 ]+ P+ \
+ _% z! R" a0 [8 V) L4 L/ z $file = DT_ROOT.'/file/temp/'.$name;: V# I2 f: q7 z: ^/ e/ ]
. F! B' L3 ~# Z& B: g, I h4 `# C( t/ E) C- u1 R6 I7 R
- K3 s' X) o$ Q/ C8 ?" Q 1 B1 {6 b0 U$ h* A$ o+ c2 l. m9 p
8 u( h8 L- u( j
if(is_file($file)) file_del($file);
$ k* T3 R# W& b 8 t' C p) [& J
4 }2 O. `; A! Q6 U1 } $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
/ P+ x! u5 L" ^. g7 D# o. [/ }; @
3 R# o/ }( Z) i; l/ D" X9 s; Y. a$ w0 `- t2 g" S
' q7 {) i$ U5 e( \4 y 7 c1 [6 d# U: w; k8 T$ t% L' n8 z
1 L1 g3 i" K' Z: } $upload->adduserid = false;$ h1 U( u- Q, Q7 H6 y( g, Q H
7 D) ~7 g: e) l0 D3 i) T9 y ~
7 e6 @! l7 i( m7 C' P" u 4 u n/ h1 d# e( V
# s5 j5 F1 p7 U. e J3 Q- G$ Q* L5 l8 m" \8 {% {9 N% o4 X+ D6 z
if($upload->save()) {( v/ r7 X, w. c, {
# ~9 A$ v$ Z* S7 ^
- ]3 G( ]( z9 g( z$ s1 F8 @ ...( v9 k, s: g- a; j. U
3 d2 O4 L. }$ q9 J3 r9 K9 z
, L3 a7 s5 [! }4 ]
} else {
, ]0 }$ C! |$ E
9 y" q; n, a. P
4 _1 b( P7 Z9 L z9 f ...( A4 k- {$ f8 N& P3 X/ ^3 c" G
( R% y. L* X( E( p5 S/ Q% X' [; P* Q( C# \& a) W
}# y, |7 v2 V: m0 {% v
! m6 u9 V1 b7 O/ i$ @8 S c
% ?" B, \, F4 ~' W. g) s- x( b4 M& Y break;
8 N, B2 C- }/ ]0 t* x
3 [; L, m6 M( G3 x" n* ?- u7 ~8 t3 |9 Z. Q& }. V* y; C0 M
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。$ l" H* ~/ G/ S! m: \2 H
# f7 G' Q8 ^. U T2 M; E* k" B
% x/ ^8 z6 b4 o: Y1 y: H+ s
upload对象构造函数如下,include/upload.class.php:25:6 Z' q. f) z @" n, ?! @1 k/ X
% w1 g# i2 E; I. {# L" ]
" l+ d: ~# ~0 w' D2 H <?phpclass upload {& ]& L& _9 g5 i; w8 Z# ]
( g, W# y7 C4 O4 D3 H
' m' T: ^6 e' P0 U function __construct($_file, $savepath, $savename = '', $fileformat = '') {5 ?. C7 W1 E) w
/ Q7 q N5 A2 J$ z+ U+ t9 {# b6 e6 r- K2 Z! h+ `
global $DT, $_userid;
* D9 c! i+ ^- f* \ ; {. x. a# A7 m: V* M! U8 [
6 k1 h, n+ S0 e) \; S: C) }9 m- d foreach($_file as $file) {
+ t0 ~6 O1 f0 s5 N% Z" @ 2 z6 | @+ A& W9 c
; }: D& t- r3 d- z7 ~# V2 r $this->file = $file['tmp_name'];
8 |" y. M6 d) k# g' z
4 y3 U6 Q) B6 {. E6 x- Q1 x) t4 d4 a: P; X' M% ?5 C. [5 K( E" i y
$this->file_name = $file['name']; h& v8 o' @! p& P T8 k
# d6 K6 l ^2 [0 O# j, q# V' L9 }4 _# p
- ]- I# l# y4 |5 `, V $this->file_size = $file['size'];: ]% h! \( d9 e5 w, {5 k
9 n O( a& }7 g" g; Y- A4 k" h% H! O7 R0 a% t
$this->file_type = $file['type'];
$ Q9 ^, g1 }2 d/ C . P- N2 S- O2 f" K% X5 n( `
1 {9 N4 i/ d. O2 N2 O: N1 J $this->file_error = $file['error'];9 V! q2 n9 h( D/ [! Z
8 `# | V8 N( P$ `# C o- L$ |
, ?$ {" W- R8 f3 R7 [ + b F, C8 r( B N- g3 h. T: U
% q3 [7 L5 A, M5 ^2 D- @2 g7 m
1 N! A+ @- x0 Y$ ?6 X }
' r) I% a, ?$ c) ?
3 X: N1 ?) f8 d q7 l
9 O/ B: k6 u# z0 x1 V6 E! w $this->userid = $_userid;- t) }( u5 T6 A. A
" e8 q. ^9 t' t1 h7 e2 v( l9 w! m( s. Q3 d+ W+ u3 x7 w
$this->ext = file_ext($this->file_name);6 o- E. P) g0 q' \: b7 _
0 ^6 J# O2 d% R. r& y' a4 N6 ~% N5 d0 L# e" [$ @. T! ?
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
) v8 l. U" G1 J+ a! |$ ?
5 d) M. D$ I* g! b
2 \/ }0 C- M' D5 K" C $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;' ]5 j7 _! a) h b. G7 S/ r* m; y
2 C- N; J6 a7 ^/ K* q
4 J# N+ ^1 R/ i. z2 C' T5 T $this->savepath = $savepath;* m; ?. z. S& ~! B- W6 Q- n0 j/ S
0 x& U# b( c) j9 M
" I! W/ G" Y+ b0 ^* y+ t5 x! ~
$this->savename = $savename;* H, G" _. y: H- N* b
2 o9 Q# p2 I. O
' k W4 J2 i6 Z* K6 O: l) q4 ]7 ~ }}9 z3 Q3 }) }) n) \
, ~4 m" p) j: X3 b; V
1 T' O" c* }/ t
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
! S, t3 h! q; h# Z' V7 S
7 M9 p! A4 m# E" p$ Z( f. I7 M0 C& L1 U5 O, z
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
* p& W3 _1 Y& p2 ^3 E$ \& R
' p4 g0 }& b, N4 F9 H3 L5 [) C& ?* D6 {4 F! I
$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
. ]- B s( {# s( {: u2 Q
5 d( ~% |3 X3 X( |' Y2 X/ r4 u/ f" s# `
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:2 i# b& k9 C0 t, a+ d1 A
, K+ @0 A$ H0 X" p0 K Q' m h9 [+ E
6 K8 j3 q4 l9 k8 R6 L
( V, O" j& Y7 o0 }7 O& j# f: T3 w2 K+ g0 p7 |
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
* Y0 {; k% ]: K
% h0 k1 ?! f" `3 T7 |" b1 r# b
1 C% Z' [% z3 J2 a ? <?phpclass upload {" h; M9 i9 S* \ {
* Q9 N: q' b$ k/ _4 s5 I6 T) ~8 L( H4 s7 P8 G2 Q/ k1 D: B
function save() { V+ {) Y" D4 S: r/ f: Z; ~
+ L* V9 B2 V8 R
) Y, s' x k3 C8 V. p6 Q! ^
include load('include.lang');
! _+ N. D8 j! C. K 6 A, w8 {& U! h% z9 A
0 ^; h# V! {3 ^, m( O2 A. @0 K
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');. c w2 l1 V G6 p$ ~ G: x
' U) Y% ]; N" R6 q
' U* n8 t# z j, @6 }9 P& ^% z5 L
% A! W2 m W; i) E% a
& j5 n4 b# b5 T# ~ a
# G D0 b$ ?$ n0 o* |- @" A if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
+ R$ X7 r, r+ D6 m. ]8 `! A ! G$ Q8 [2 l6 T' C4 Q* u* {
) |0 g9 n/ T' @& b
) q l# @; D8 @4 ~4 F! B' R; m
% Q& z- Y# Z5 E/ }" E; |! V9 E ?! o9 C0 q. ]% A( T: ]& {+ K% e
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
+ O: J; m) e. w4 V ' C5 b! @ ]$ y# V6 U
6 {/ Q7 V6 o- L& @( |! _
# e$ g$ U. u M3 K% V
2 [2 \) s, E/ U0 f0 f( K
$ D8 a, Q9 J) \( `. r. X/ U6 F $this->set_savepath($this->savepath);
& A9 R& j* |6 O$ {9 w* s3 o4 [$ L 3 j+ D/ Q1 d+ ?6 A
( c% Y7 G% u4 a4 @$ A0 [
$this->set_savename($this->savename);
: g5 F1 D7 v7 K$ i) n4 y" e ) g5 m3 H" P( h2 f4 r- _
1 X2 A0 g) R% g( n( j
: i1 L& M- q* \ 1 e( b9 X+ b" f4 ?: j
) k! F* F0 h, x7 j. l1 x if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);" [- M# l7 }- n5 Z, c+ Q4 ]
$ s) k1 K5 E8 k1 S. a! y6 P# K* W$ O
( \2 D1 w4 L3 S& I if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);5 E! d0 C0 ?3 {
$ f+ T& X* }7 P. _, w7 y6 ^
5 R; y$ w$ D0 |% |: ]+ `* F7 O if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
: U8 [6 `) K: b0 T6 Z$ u
& v0 Q( X) J6 Q' S6 M( s
% a7 x: j* h+ u! Y! ]: G
0 o& y7 R" m7 W% _4 v 8 i: @ m3 D' r. H
2 z' f- o8 k- @) N" [1 P $this->image = $this->is_image();
# ?6 ^7 V* l. L, q. F ( x6 s$ R$ q: o6 }3 @
3 f8 P; n9 Y9 P7 g- T8 Q8 j if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
" w( ~0 U1 e# R$ s/ B" r + ]; H6 g( X% W" D
& T) M0 P+ r) X; R$ B5 L
return true;
. S C. k9 _* o
5 q' C w5 n0 i7 B: ?. b! }2 z3 W, }! \( X
}}! B* B+ k8 [6 }+ `+ S. Q- E
) e2 G" f- j5 C c8 u: N
! I3 r6 g1 J2 u: r 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
; f; x: p, W, I! l* [ 3 r( N9 S4 I0 E0 s$ G4 q" }
c! s2 P- i0 k' J2 M* c6 c
<?php( j1 P5 d1 v- J" ]3 R2 y, N
0 S6 Q0 ~) O- W* [- Z3 w
' I2 Z% J! Z6 i6 }4 @' h+ k& e- Q function is_allow() {# R) B8 S, Q" S l
+ e; a6 C9 N% @+ h! v, ^
, X* P0 b2 C, {3 p( z3 @
if(!$this->fileformat) return false;! K' b, X, v4 k# g$ g! w
" w. L: F5 k, z G0 u) S9 C+ H' {; F! @9 c, D0 S0 V0 a; w
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
5 Q+ d) E" q$ ~) w, N0 H1 K U & }4 S, a" f+ A- F+ X! L
0 n4 a u ^3 f3 n! l; r3 H 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;, ]& D. ]3 x/ k1 a. s
4 U7 t6 R% l% |9 f* w! z0 S
* @# A# C2 ~ L8 W% V6 I& E
return true;- p+ e- v/ X+ D# Z7 y
/ f& ^/ N t/ _ v1 L
- v l6 h# r- h/ h9 ] w }. F- j# _% Q( g% A8 F W
, W `" l$ z4 B+ d- L5 L8 R1 T' m# ]
; A* T5 L7 Z% R( C! Q, I+ w
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。; L$ e$ H% h. R2 c& P Z
! o3 v8 d! W/ M; V+ A- _8 w
/ C8 L8 `$ q7 B3 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文件。
- U2 e# s: J" j& F6 E$ t5 r
. I% {( d' ]6 g1 p& ~4 ?4 _, r! k* t* m5 V. w/ y& q
漏洞利用: c0 t/ q6 q: F9 x' ~
' z* y7 m& c. J3 n2 c2 l/ z; e/ x- q6 o2 o
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。; ~; E, F! A4 j) k) u' S
9 {% ^* T/ g; @6 g3 N" L2 } w, m/ G' j) Z
3 {4 _2 g) S' M- r
( ~/ n; W' X) H# `" f/ z8 A0 f; a" }0 w# I( d% z z
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
8 }" O6 E. A8 |# t ! O/ G; C" {& m% h, J+ y5 I
3 S1 w0 y8 n8 x
不过实际利用上会有一定的限制。
) u+ g( f; h+ j6 l. t5 i
- V4 H4 Q; g: q, {1 B
& ?( b" Q. e$ ]& T% ` n. A- v 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
0 s7 M0 J6 x2 v9 t" [& H- z
/ g' v9 n. s7 }7 Z* d6 a! [( N& r9 \$ c! ^) a
+ ?0 B6 B2 _: G7 | 2 {& d7 ?6 q$ X$ s: u8 X
- ~* s0 F; _* u1 d9 } 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
, X1 ^3 j% B, D2 A* r- @" z$ l. B2 h* U 6 }, {5 E# m; M6 d' i K
# W$ S o. h3 R
省略...$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]);省略...
+ v' G% N( R0 P) ~4 C+ X$ t& C 4 D: x5 O8 G( d. ?% U4 q
( N* ^& O) c) p/ ?/ M6 A' }/ q- f
因此要利用成功就需要条件竞争了。
8 U% u2 L/ [/ \; c9 }& K0 z P + e+ w+ p7 I, X5 T! g4 G8 s: D
. `5 U/ _0 v- o+ N% o" N 补丁分析* z8 D c t, ~6 k$ b
4 x3 ?* l4 K* U! q% W8 S/ O
- P) k% b( r7 J) Q
& L* t. T$ Q* ]& A4 N) I 4 N# }& R/ G% [3 b
: W" i* t- G9 e( I 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:( X5 `. u; ]3 W
% Z* z+ k/ I! m) y J# [( ]* F
! C" ^9 x( H$ I s1 x. g* w
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}/ A1 z3 l% ?6 @. J8 _9 Y) {
! U- L( Z h0 M, O( O4 H
1 ~# h8 \# R' ^; d% ~
3 g5 s' C. W3 Y# A) M 5 r% p6 m, U7 m9 ^
2 b6 O. H& v4 ^+ u5 n 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。, h4 I0 F# N& f' t
8 J! l5 R$ w2 Q4 r; N
( I7 L- a4 l7 C ~) i1 }
在is_allow()中增加对$this->savename的二次检查。9 p; x0 u$ g. ]6 d
9 K# J' H2 P2 }; W# T4 W5 \# n/ s& H0 X# U4 Y' X
最后$ ?' M0 A: k6 S4 f7 e
- a: @# S J: a
# q6 x, R# l+ d" p! l: J 嘛,祝各位大师傅中秋快乐!
V7 O! e3 o3 P+ e8 {
8 n/ v; K2 k# N: p& x7 B5 t6 \
0 t6 x/ P, M, }% i
! z* [4 i( `# Q1 L7 ^3 g& @
. `0 x8 X/ r0 {1 o- _5 b
|