% J7 n, o5 u [9 `% K; z& _2 {. S/ V# ~* G; {% G J, o* k. c! e4 h
. k0 T+ u9 {* Y
x2 e; }+ O5 q+ q 前言% z) V- ~, q6 f; X. q
8 c, {' M: ]9 L5 D! F; j9 ~# q! e" n" E% W- \
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。# A/ K3 I" n' E! Z9 s" t
/ z2 ~1 ~" I l5 I" a$ i" ~4 e a' ]* o: I
" k2 i- x- q7 _5 o) T% @ 5 a$ r' v% A7 o' @& r4 |' |! Y
. {0 T; `5 J$ G; E 漏洞分析
5 j: ?- D4 d. B" ^6 B/ T
; R% R# m6 n3 n$ A3 k) r* i T3 v* g( [$ j( h
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:' i8 b/ o! L7 ` x/ y
* s7 J/ @9 D' } g: ~4 B& u5 _
' ]5 Q$ E1 a( e' Y( W2 @
8 I% Z+ J/ |5 J1 _- } . o L e' H+ T4 Y7 \( {) n& ~
3 C9 q) O/ x# B7 G" b 对应着avatar.inc.php代码如下:
0 R, r+ Z, r9 ^. c. d* i2 k3 a/ F ' L5 _: Q. H8 w v5 f
; w; N B: o7 Y8 M1 T <?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 R- B# Q1 c# y8 F: A8 j
- o% H9 r, b4 ~ k
& K- H' z+ H' R! U7 E' K( O1 T case 'upload':* y; T7 Q1 t( ]. {/ A, [% O
- U4 Z6 E% m& h! q; i T
& g* x- a Y. v$ p if(!$_FILES['file']['size']) {: z# ` y1 |0 C. T1 Z0 W4 d+ {$ @
6 p4 F5 l/ u' E" q% a) u$ {7 X: Y* q" P' ?& A
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);2 \$ y+ ~% a2 a, a7 I
/ N8 ~4 O" z c" d
4 [5 o) J! n3 f0 ?+ j' x- p2 B$ s exit('{"error":1,"message":"Error FILE"}');
% n8 I$ Q4 Y) i/ I
1 }1 A0 B) P* A$ X% |% z, m' E2 e3 Y- @; D
}
3 I* p0 W% q9 ] Z+ y% L
$ K$ b X' f5 X. M
9 B: Y. O, F8 D# | require DT_ROOT.'/include/upload.class.php';3 V+ f. f- i4 j" c) e
8 l1 X Q' r! j0 [7 o1 [$ N
/ ?8 Y' E( Y! f M 0 f0 i6 a( k U1 [: L% c7 C
( c- m$ X- a* ?$ K4 ]! v* \- Y! F
* j' A; }& g0 Z: ?5 e( ~+ d+ ^; l5 E $ext = file_ext($_FILES['file']['name']);. r5 G* \3 W* v4 o) Q0 m- G
8 o. N0 P9 {! w* l% j8 k
2 x2 x7 r3 Y& j- n $name = 'avatar'.$_userid.'.'.$ext;; \7 v' L/ R- r% E- n& T+ L7 d% |
9 v8 L- x; W* A; o/ q$ J
" c, n. W6 l- o" x/ v6 ^# w6 v $file = DT_ROOT.'/file/temp/'.$name;
W. v- R2 [( W( M) H* B& @0 M 7 L* B; U* _, |7 a8 }
; v! ~6 l R+ m( Y5 t) w& F
; i+ B) S c! P. X# l9 I
% R! A' S' B1 L9 S
$ l( G- Z, R0 A% ~6 y$ W if(is_file($file)) file_del($file);
0 W( _8 O. }+ p5 j6 g$ P " L$ v# R1 _8 e, b+ y
; j3 B; r- y. g4 H+ s# P
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');" Y: }2 ]" Y& O: z" P8 m
# z* `; A6 h5 q! y4 z
1 s3 V. I7 H" U& H% |. u+ O1 U
$ L1 O5 _" Q L. X
& ^( p4 u" d* t1 d
9 z; n. d. m. L" W" n $upload->adduserid = false;) c! I, H$ h3 |( G
. e q& v+ h: o3 f( _( T
, v" S7 P( ^" k3 N3 T# t) L& W9 q5 j
3 @) x8 p% r- e+ Z Z/ R
4 H0 `5 C0 [0 |8 u* T! W2 @4 W
9 Q4 k' ~. l% W% u if($upload->save()) {+ b- h! D! x6 ^5 i( e
: s" J4 l) n+ f
G: I s; b3 D1 U8 ~6 @6 a6 [
...
6 j: k* v) _# D8 G$ b
$ j- ]$ \$ f [& F& L# j% n
6 @. y' Z$ A- y+ R7 f& | } else {
4 O% n$ `1 r N2 }, G- [: i 3 _/ Y: Q; Z6 K7 _; \) X# A& M
8 k( z) R& t$ Y, y% F
...8 J; j6 n3 h" ~% Y
3 X- m0 G/ C8 w& V
: Y7 d; y. }! X( Z }
\3 ^1 U! o0 F- z 0 k* a$ P& `) j/ b
q0 [. @. a( B8 e
break;, ^" `) M/ t( ?* q6 m+ F' o
. I$ Q1 R1 m3 `& H7 i2 B
$ }0 ^% D) B4 g( [5 f3 F
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。$ c4 c- L: R$ ~9 a b/ ]: H/ ]
- }( U! k- s, H; {% ^
) f6 {' A, e9 A- Y upload对象构造函数如下,include/upload.class.php:25:% c5 a: B) j4 i( d) u
, w, M4 T6 S2 B/ S7 Z- Y' K( j
6 |- j v( t! r4 Y+ f- W <?phpclass upload {8 ]6 r7 h0 o( I0 W I6 C1 V
* Z, D: J/ u; y2 k2 ^5 ^/ ]1 \
8 v4 u! X1 v* J& z5 P; p" A( R8 e8 e
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
/ X4 l' |3 b: a L/ \" @
/ D& M5 v" }4 |* ]2 M( [" X0 p/ j" L' V6 {! k
global $DT, $_userid;
& D$ b+ t0 P9 g4 K2 p ' U2 p5 J. d3 H6 _/ L
) j1 Y" X3 J; y4 d/ b8 \
foreach($_file as $file) {9 I/ q8 U, @6 E- }
/ U. u t/ F$ X; T5 {$ t F$ E' O% S7 u6 x3 E- _$ f+ H
$this->file = $file['tmp_name'];
! F; ~ f3 p+ ]; L C * \' b1 R: [- _; Q, i9 ~
' U8 s6 J) s) P6 m( ^& h+ M $this->file_name = $file['name'];4 Y0 S" P) a: V
! k1 U, }# ?' a9 Q: i" @
( |! z, ]) c, B' Y( R" r- g7 L5 [1 Q $this->file_size = $file['size'];# J2 Y4 Q* v- }' D% X. F$ Z6 i& ^
3 J. k: k; e8 Q* b8 g& ^; i! A: |& v# e, K
$this->file_type = $file['type'];4 g2 h2 V. c9 s2 n* m
1 Y; u% n9 c4 d+ a: s7 e( g& n6 H
+ [& [$ L6 A; s* b $this->file_error = $file['error'];
2 T0 i4 B. e- Y* V& }8 k9 U
% e7 |2 }3 T; A) p" G2 P
* F. H5 k8 c* c" ^
7 X. u: R* } h 3 m" z8 F' W6 w0 R" L" Q: r
$ _$ X- N" ]) B1 M% T3 X6 i# F
}
2 t/ q7 B0 F: W - _# H5 i: b9 [2 z
# |1 o2 V7 r" _- y ]
$this->userid = $_userid;* V$ n) W/ v4 \7 z2 _% |# W
& n/ p3 K& D9 _2 v7 f
' q; Q2 {1 A% q
$this->ext = file_ext($this->file_name);0 u5 L) s. ?- z% m8 }9 n+ A
8 K: e7 C' p, G' I
) v' E$ b0 {$ S $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];& R2 r- G' [% F+ x0 U: K' B" y
% p6 L9 p% X4 O) C1 g ^+ ~
3 | A m' @6 W $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;4 N6 K% R- q- [2 |- Q/ z
9 f) M( t1 C3 s4 L. s
% s% z# A: `6 W; Z! _ $this->savepath = $savepath;; q1 V7 i8 o( i
* \3 n v9 d6 b5 l$ `1 e5 U& z
' W u) E ?6 Y( E. Q $this->savename = $savename;
: d8 |6 d. E8 `# K4 {/ K6 v R4 Y + U# A0 j5 l% ]: O+ N* D6 n
9 I' g- i8 S$ S0 \ }}
9 X3 [7 ?/ _: r # x% d$ Y% V X ^4 O m4 g
( n% U# _' l0 m6 e9 W! M% Z5 _
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
5 w- e! [; H: }* |' y5 e, D# t 2 \6 _# [9 a5 g. L$ y
; c" o+ ]( c9 B7 }. {" N4 D 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 O& k, g+ L: V7 |9 c
4 b( o( r0 P( y5 y
8 l: f/ o; a* G' |' _+ x $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
7 h: F8 Y& f3 w: @" ] f) C . s/ U0 t- z) |
9 t/ W3 s5 _3 E8 N& o4 @ 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:; x% d: ]- L2 B" p" U0 c2 e
v5 _1 P/ j: L1 u
# ]3 W% J+ [: M4 C" f
1 m% ]0 Z1 d/ ]% h* z
; w. i% w3 `" [1 w5 q$ V7 X" F9 ^, C: D
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:9 ^1 i6 O! i+ V% G5 P; q; {$ m
' ?+ r8 y$ ?, ~; F) h
0 Y6 m7 u& ^( S8 o; v# q3 } <?phpclass upload {
. L0 l2 c- p$ @7 U- q( `
6 x6 h2 P/ ]5 k1 M, b& b
# y& u; U: L. d1 g; O/ C function save() {
8 C1 N: l0 P6 t8 I
) R: S/ f! I1 h7 V7 [2 i% X6 ?. g2 `8 w! {# l
include load('include.lang');3 ?; ]3 b* K, l# Z% d2 \. k0 z
) `* K. Y8 P/ I; K- @3 ^
. V" t% l' V0 f, g# { if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');& b1 [( D0 J \
2 v: z$ Y. n8 h2 k
' H. \% Z4 _+ O3 b1 n e. C& l; E$ D- H# j8 E
) d; z* X' }* a$ c: Q& w& D9 g# U" N7 A& s9 D d3 H6 x9 {
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
5 u& b5 N2 R7 n, k
! p, x( \* l- R3 j7 t0 O
; z: k: a% q2 s# ]
/ V8 d. ]) Y7 ?; i9 M , ?+ V- @ r; D! u) A
5 W: ]5 |$ Y" j% x8 Y: l/ X
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
3 X" U0 ?# |2 x4 J- C2 M 4 p% M4 E. Y5 j6 _ E
2 ]- S3 A3 r4 c5 x( `
5 ?3 H" s: G% c4 W8 b6 Q
9 G7 K" w; G/ j* F: f% @/ ]( ^- L, a! o/ J( k
$this->set_savepath($this->savepath);
1 [1 X0 g: X" S
7 p' a8 e1 j, p f+ ^- e2 m0 F# p. T( p
$this->set_savename($this->savename);' {1 r2 z+ `. @* w, s m
+ y7 c. e$ _' w3 T9 B' [& `& W8 H0 W* w
& d! X% C6 I5 f' C" n( B4 H
b8 a: C4 p3 J& e
& R) D0 N% q+ s if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);% R. S2 @) c8 U) m% b
& B+ c1 a; D4 i/ B% u- V% z `, P
6 ]: u; `8 w. H if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);' A/ L; ?0 r0 p
6 D9 v& o* ?4 \) e ]7 T+ \1 Y; K ^6 `' U
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);, I2 g& J% r- p: Y( N" J: a) {7 s% @$ {
6 ~7 p+ ~7 ]+ Z/ z+ ^
" g3 w: `( q, u% Z- A
7 o5 i1 c! d( K6 V
* R4 L, F3 A. X# P5 p
; j1 B; e& g @8 I- t. h $this->image = $this->is_image();
/ n& O1 T; ]1 l$ D& [" g7 O, v
; ~# |; }. Z; f( n& x. y5 N/ S( G4 o! v! N3 R* R% F
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
2 t% O+ I4 G' ^3 a9 P7 r
2 a. V! P8 T' u' e3 p/ I
$ b% U( k) n1 T7 ]0 P" m return true;2 `7 b: a8 J0 [% s3 m: @ V+ {- t6 h
1 k3 s! k# y# t- p: u6 e' h: h6 D
}}! i# p6 R( x/ {, i8 w
$ B8 m! {" W3 M2 [4 W& D# M# f% [: S& n3 V! W4 U- b
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
" l4 p' g" P; N j
; `+ o5 I0 E; u; C" T2 C% L. p
* \# n) K: l6 r9 g; t- M: J <?php
- O2 H5 E8 T( H/ f
& N4 \. V' J8 N! w% G; |1 l F) v/ ]* \5 h9 Q
function is_allow() {
9 A, k5 w: e4 y7 A, Y4 u7 y
. t d3 `' s$ y# ^% c8 I, c7 }2 `) l; T: `; q U) E
if(!$this->fileformat) return false;
9 N/ X3 N4 [% q5 ~ 6 g. b/ Y4 x; K1 X
# D. Y" |) V R/ h$ i0 o; j3 ~
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;; ? n, P& D) [6 g& D# o2 e
* s1 \ z3 X6 `) b l8 b" K
/ p6 t" [! T T; I9 G/ L% x. W4 ~ 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;6 X6 }. t' h( @
# _9 }! ~. v8 B
8 y2 I" T' ]% U! h return true;) F) B9 J u8 @/ K
# a1 t! g/ i' J8 B# T
' q. X: k4 O0 U; K" @! N }
. S) N2 } R/ K0 ^$ t ! X; x& B5 i2 O; S9 a
: t: i3 e- c$ \ ]7 d 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。' `$ F" t' w/ ^% K3 K
8 `* o$ p: ~( z: X- u( |9 i" b0 ~8 _2 w( l3 H; A
接着会进行真正的保存。通过$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文件。
, Z* k+ u' V3 ~0 S, T2 c b( G A$ h1 W% M: L$ p3 h
$ t8 g8 I/ t5 u: U0 S# U: Z
漏洞利用
) a% D+ ?5 o7 ?0 A" g3 }
8 r5 I% R8 O( {# U' L ~! s- C
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
: _, }) m% e7 T & ~: F. K* R5 _4 K5 V/ O. s, [
! V: h3 e/ ^$ N4 f; ~5 |7 f* P( O1 V
& C1 L5 Y# X4 P3 d. y1 N: y * s% M# S! S9 u9 {' G; z
+ b: p4 t/ D4 M7 j- q" q 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid* Z+ o& n% G; s7 @ h, r0 B
* \+ Z- a( m$ [0 G( M1 v* _
" }* ?; ` l' F* U. n
不过实际利用上会有一定的限制。
+ u! L6 U( S8 y" J* g& t+ h) }% { ! i7 f" p7 q0 s
6 f+ t4 @/ n. P$ a 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。- S; Q, e M9 r3 r2 _2 _; T
& d9 h) t7 e; r7 Q5 s
1 r& I2 d) z" m+ E1 L7 j
5 D& ^2 N9 h# O . I# W) L2 I! ?4 M0 Y' [6 z
* t& E! w; ?+ A* W 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:5 C6 E! Z9 O$ r7 O; p
/ Z, ~ t4 v! U
7 q2 m$ f) L, Z. n2 h' O& [5 y 省略...$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]);省略...
3 \% x! y9 |5 D8 \0 o$ \% k. O ! p8 l9 m$ Q# g9 R& A
# X; {# j9 M8 v$ G; j
因此要利用成功就需要条件竞争了。2 k7 |8 ?9 T b, n
3 r$ I& T; g" C- i5 O2 o
# U% N ~6 v; Z" n& ? 补丁分析
# r0 r" l7 X7 v8 ?6 M: B; |& G: s- T
5 o$ F$ K0 [ r
$ ]4 `- a& _7 n& D# a. n
) w! ^: k3 L# }: T( m x( ?) g6 _$ C: f) x- O
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
2 n7 @! i& ^, A! E+ H: X
2 _7 J2 U9 o6 H H& a: b4 \# r- v+ h" r" l* J7 n
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
) i% Y0 X& d% m. X0 R& H3 ?$ l. R3 w
' g9 c4 i1 v; K* n5 ]( s# b8 f
4 o: h/ a, p s% x) q L2 J7 ?* S# q
4 G* W& s$ i4 a+ b' w
0 M3 t9 I F1 ]5 C9 i5 L 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。" b; w3 ]7 c5 G0 F; z X+ A3 ]/ o
, g$ O" g. {+ i8 ~* ], g, ^6 Q6 @& K7 ~
在is_allow()中增加对$this->savename的二次检查。
: H! X& F' ]& y& _* \: E; X9 E 8 V& X7 j2 z9 A8 Z8 Y% r$ @$ U
3 M8 M( {3 _0 ^" Z* L" ]) M6 m
最后
* F. `1 @, N; G9 {! |- u( m) \9 T
# Y6 a3 s9 A# d
! k# |' ~$ U7 Y4 \/ n9 V 嘛,祝各位大师傅中秋快乐!" t6 p8 w" d( V. |* ]5 s
" X+ k& H& ~" {, m8 l: m2 {9 c5 a* r7 K6 u1 w: A2 p6 c- q4 Y
" q6 g/ {1 h. b3 p8 M. p8 ]
1 V4 M o6 w' Y
|