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