: |; f7 c$ {) @6 q
" K. }& \! f2 ~4 Z' f# V- A
3 e# I% \5 L5 }3 R6 W* P
; n L. u: t/ H, [ 前言
$ c* h! s& z7 r7 X- `3 d2 ]- g9 P5 s, Y- Z
; } U& ^& D9 K+ x 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。1 j& E; L, q# w
, M9 I: O+ j' g9 m. N+ V' l
1 d8 |$ t/ L. j* m' _5 ~ m9 T5 c- { T% }) F
/ C' i8 v, ^5 u7 v( z0 B% v# u
- r$ {5 U' V. S# t( S8 R 漏洞分析
, q& z% D2 F$ n) y# ^' L: |9 \( F# _- z" k0 K
& @+ x3 O5 O' o& q1 H E
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
1 H+ |% X& b, F( M- P& J, b
3 v- K+ I5 W* J A9 J
3 j" k3 B( B1 V4 {3 C2 U& q 5 W$ H) ]8 r4 r: i0 _( @
4 j9 ~' N* p$ g' t$ t7 k
6 T9 U+ G8 Y8 m: V
对应着avatar.inc.php代码如下:4 v7 H S, X! `6 f5 U
) N& G. h) s4 ?0 l3 U+ ]
) V: f8 }( h7 _2 ]- ?; s% J <?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) {1 ?/ u( j: u3 m2 Y* g
8 D4 V7 x* Z8 X
- j3 H" o K$ `: r3 l! R, O case 'upload':
, M7 |+ n0 _& {+ f; S ' D$ @+ P8 l/ P4 G* X
/ Z- V$ z" L0 Q' x# v8 ? if(!$_FILES['file']['size']) {
* E! e0 b/ ]; c2 m% j0 { 3 D8 y1 Y4 ?4 |! ]
; {5 a" Y. E! R8 \ if($DT_PC) dheader('?action=html&reload='.$DT_TIME);$ _" E8 t3 X3 d i5 P/ z8 B2 H
& a7 j9 i5 W6 s+ ^& ~# [# H
0 n4 b: Q( O$ B: {- H2 U
exit('{"error":1,"message":"Error FILE"}');
0 ^4 R- D. B5 m5 O0 M& @4 e ! v$ T* f1 q1 d! @6 O4 o
2 s, P" e3 J$ G* h: \) `
}
8 l6 S$ u7 o5 X$ y4 j2 \8 Y3 a% W / x4 X( L3 B9 m2 N+ q( I
# S7 C0 ?+ a/ y! Z i+ J require DT_ROOT.'/include/upload.class.php';2 V+ O3 p0 P$ p6 `
$ x# ?' S1 u( d+ N! R" Z( J& S
+ @3 `9 }$ A0 ^4 E
9 T2 y& j/ t7 T7 A 1 n! [* Y/ g% k
0 v, d% k! i* ]" n& U% ?4 x/ O+ ` $ext = file_ext($_FILES['file']['name']);/ B' b, o2 X8 i% V
/ f2 D0 I' P' Y. d* M) f! z9 }0 O( Q6 D
$name = 'avatar'.$_userid.'.'.$ext;" `1 _ z" E! x4 `
% A1 ]0 J' B) e
9 |) e0 m0 D1 { $file = DT_ROOT.'/file/temp/'.$name;! ? z% r$ z! I4 J. F% z
2 |( |8 A$ G* b( n' Q4 H1 D* q- ~+ t( Y
5 a; C1 o( l# N- _
( ?/ ?* ^, y* T" Y- [
! k- d. h) P8 W/ ~0 b" J; _ G if(is_file($file)) file_del($file);) _1 w) C/ n7 V3 c
. q6 E2 \$ {3 e$ B
2 C) W+ P/ [, N6 j* N0 \2 \8 D/ C $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
7 J+ }* o3 P, f7 r2 a- {
8 j/ z- n( W) W( M/ o$ H$ A- u2 M' U/ |* j; H5 A
( M, A- n8 H$ E% C
+ t% I% p; E4 o$ }; q
- G6 r% F2 _: a' k2 W# t( C9 j $upload->adduserid = false;0 p3 T5 p/ \. A2 ]; K8 ~
8 U+ a! R0 U$ \# \
; ~, V2 T, B( V" k# U0 T: s8 L
1 X! [9 l* s& [" ~' A9 `
. u) Z8 b7 P6 _( [3 G7 l1 R( O- U& _; k3 b' V/ o4 ^' P5 `: N
if($upload->save()) {
7 ?6 y8 R$ p) v2 K O5 v- V1 }1 d' J+ e4 I3 ^+ e- Q" f
7 M. u% ~) N( o1 } .../ Z, {* c! P; n- R! I, ~
' M$ d% }0 g6 t9 {, W2 I% Q% D0 k7 o
} else {$ W4 y- d" D1 G7 M- g* s- E( f6 g5 E
/ t0 f1 [* ]* f! t4 i2 _& }- r
- W, Z. r, y6 ?' p. P) M+ j ... n: r2 t; Q# V- T; D9 A
! B( N/ G& c# ~1 |2 t4 y
! t7 o& q+ t) y% M ~! W& x
}% B k2 Q- m+ k0 D0 f9 ?
A9 x: L2 S" h! e
4 J4 t8 O- q q3 V9 d. U& W" h$ d break;/ l% Z% F$ i( x/ @ t' K
1 O Q6 H3 L0 Y; ]5 z3 F$ U8 M" M. H; @. m* @
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。. ]! Z3 {$ J8 p8 K) U# q& ?/ K
1 m" E% I" Q* M% {5 P' V8 K+ [* s+ d; V* z
upload对象构造函数如下,include/upload.class.php:25:2 j o& T+ b$ J
" ^. R& \1 N7 Y8 K# u5 E+ E5 u5 t6 h" [" i3 m r! R
<?phpclass upload {( {" e" A# {# ?" V$ P" u
. m- o% \" o4 u7 P- [( c
- z' V1 z7 G) A function __construct($_file, $savepath, $savename = '', $fileformat = '') {
- S9 G+ s' T" X, N" _5 y1 t$ P) P9 O
4 K0 G9 v+ ~% F5 X8 j. j# `- c+ {9 L1 x: Y p/ z
global $DT, $_userid;
& q3 \$ O/ @' ?4 S# G8 o( F! J f& q$ ~
. D. [. d/ ?3 k. b( K& V9 X3 X4 a& x5 S L' g* T& O
foreach($_file as $file) {) U# W9 a, r4 r9 S7 d1 ~5 y# s$ ]
" z1 |% W: n0 X/ a, G, i& \- p3 l9 a$ V6 B0 g, T7 P1 G6 ]
$this->file = $file['tmp_name'];+ p+ a b; Z/ h/ ?
; M# Z- r3 S6 K# z
% e; Q0 H, \7 P: Q
$this->file_name = $file['name'];
) f2 i4 ^% |$ y1 u & Q. E3 G, T( D+ A8 K
* Y! N8 }) U4 G) S" h0 B3 \
$this->file_size = $file['size'];$ m4 J: s4 E# R( F6 S; G
* {3 i& w& e9 v4 O( e
( H- y- G, n/ i. ?, b$ U
$this->file_type = $file['type'];
. Y/ i- h1 f/ c3 J$ p6 ` ; S$ ]+ _/ h3 I# [
( N; q9 t. v% D5 _$ J- K3 u& q
$this->file_error = $file['error'];: F4 i( ^* N1 e9 U+ p- j2 p/ N1 m
^7 J* _5 H: S8 c8 J' Z
2 y; B$ x# u6 T! } / r4 z, z: F0 `9 g* L' r: I9 h- ~! |
5 k8 K- Y" j. p, f+ ?/ t0 V9 S9 e* `! ^ T, L9 c4 X: C
}
* [7 V& s- k. o, `' [0 E% u3 t7 L 5 d, Z* K/ G0 J$ a# x
6 E! U5 j, r* V0 h" Q $this->userid = $_userid;
* d/ i$ i2 t( \ " Q% d9 o: P, f) Z1 f. _6 s
4 q. N, r9 Y) d
$this->ext = file_ext($this->file_name);% @4 f$ t# X5 }; X
8 s9 B4 s, r* W( l1 ?% v2 I s
) T8 L" l; ?( n q $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];% Q9 P/ e; r9 g0 u9 z- P6 b
$ Z% t v( X1 [5 y9 e4 P% N% C5 U/ a1 V% J, j' }
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;4 z( c3 D2 {& ?* V7 P, J
7 X# {7 \: r% y# h$ u& j( n. e$ u" Z2 W9 N
$this->savepath = $savepath;& x" w5 z H' ~! f) [
$ J8 V3 ~4 m7 [# ?$ C
# Z. j, |( I+ M+ G& D $this->savename = $savename;
1 C7 O$ Q2 B4 w+ z5 y& ~; r+ r. e + i7 D, K* V8 u: \3 ?* w5 e/ @
2 P+ Z9 t0 U& H3 R }}! k! B3 i9 p. b" c# O/ R
5 ~1 W- Q. ?( I) G2 r7 B( b$ v b# k0 Z: T4 q) L. ~2 D
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
7 ]2 b: O8 @! u8 w 9 ?. f8 v: r) q% P
" F0 L, g. q, Q$ w; n6 t
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
7 z0 h- [/ Z) E; K; H' o! I - ~: A+ R2 w2 }. \$ x4 t! O3 \
2 J- [) [6 P! j
$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
+ K S$ d$ `: \5 O # ~: P# b4 z5 n7 D( s7 J2 F3 n9 y
) O. J5 s& v a- a# c( A" R5 o1 B
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:. |8 V4 n% z! F! u3 c9 S
4 v6 z* p# V# @& z
5 u4 [ Q! e) ^1 u" M6 a) \( j& x
" W$ Z) D& T7 k2 \1 p
; s" }4 Q: Y3 ~1 S' b+ F' c$ M, Z( {! Y* s1 {$ o3 H
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
3 P0 G" S$ z6 v: }* ?0 _+ T. P! u " T! ?% Y& n+ Z, @' P" S
4 Y; n$ J, q B7 g8 J <?phpclass upload {7 a' L2 i& L* i& U' L* N" R0 e) f
5 t- z& V/ ]4 U7 x2 e; q1 x; G; G
% ?1 Z; V; p& h$ Q" j
function save() {4 M6 I/ f0 g0 i% v1 A9 a
* n3 J' v6 q2 x; L" _( v& t
) A$ a$ W% B( z, y- a2 U
include load('include.lang');
4 q9 Z, n) |4 |- b9 M
$ B2 m2 p3 K9 k) t3 ?; a8 E7 y/ _8 W' q4 c' r) G$ J+ o
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
5 ]3 g& W& L5 m B
$ X: |1 d) K& V8 P5 S( `/ C) Z1 O) S/ o) f% c9 ~2 H
* i3 D0 i2 ?- E# Z6 i) l
+ q5 x" j b. M% d2 [. Y$ V `2 x4 F2 ^" _& [
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
: ]" v1 Q/ [. ^6 m" i `7 }: H( g0 [ % ~4 y, q3 D$ P
" H: D/ I: U0 c4 Z/ H- F& [* ?
4 _) p5 \1 J" h' Q7 F5 T+ V9 p* R( g# i
4 ?7 q! ^& U0 m6 a2 p5 v
2 A8 n# i/ C' ~! H4 ~ if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);/ ~+ V7 G+ ^' [4 e
& t: ?. D' F! e+ l7 j' Q
- p! r( z1 P8 p* a. d& @( J0 Z
/ A2 k! A# m" [& V, \" C 2 y* a8 X b4 y% u+ l( B
% T) `: a" f. M( }3 t $this->set_savepath($this->savepath);& ?2 _* y L" i" Y0 H2 f' t
: m e. E5 j4 p6 R8 ^
+ Q* d n; l4 ^. |- ` $this->set_savename($this->savename); S& q! e# z/ n8 k" L( P, J u, Y
- I3 P% n5 |: c) n4 z8 M+ e1 o4 B
" f4 R# x2 E. m) i% u& n
6 u( _, W% l: C& g4 R/ N
# P9 U7 M- E2 x" k+ |7 N2 G/ `) e4 b. W9 v2 e% X
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);: I9 N, h- j& l* M
( i$ A9 t' x2 f- l! \- Z# ]
4 R, f6 v$ p* Y& Z; W2 O if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
' O4 `" i2 S) C, Y2 m+ g/ R . z6 C- V: @5 a5 D
$ S! R4 b5 w! t- `7 N- {. \( t if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);" B0 g$ G' h- \$ {" d
# I2 ^% a, u. p% ? s0 ~% J# X! Y# m1 `
% l. X" {/ g$ z- V! w& S
. {0 \4 @7 o/ Q t. R
+ o. f, e9 r' S% V i $this->image = $this->is_image();
& b/ i" J; E2 U 4 _9 i( q9 d+ S
, x/ j6 u6 Z8 d0 z$ Y L
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);/ j2 M, G5 W1 s" T6 x' P
7 f: T. o% b/ i. {. _
' S: @- M/ A2 d0 R6 p
return true;
& F) X5 d7 s5 I6 B2 y- }. b) L
% H/ a% [* P; ] m: |2 |
4 e. I8 }$ W! |' P }}
* I$ \( _$ | g5 E
8 I% ^$ f) q* _. l& K B8 G0 W; S4 ?
, C1 \# i0 j6 b+ ]5 b 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
- C0 @9 }, h: W0 X; G - z- `- K$ Y4 X" v; v
- _! J, C6 E, z" M <?php, |& ?1 \. |$ F5 X3 B' K
- U& u. e6 L2 T$ q
" R- e( f' U0 V. }" F2 Q function is_allow() {
' Z2 i3 {* o8 O3 \6 D 0 H# D" A' k% |# z- e4 T, @
0 A! ]' W8 n( |) Z1 y: e if(!$this->fileformat) return false;
; \' R! |" t% q5 C/ K6 r( j 8 k' j& F/ `' S) L) V6 K/ [
* K: \; J$ b) j) V+ \5 g8 [; F
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;( K3 g( F) v6 e# B. e2 \" s9 G# W
' }: u( F0 [! w" i8 p! V" |% U
0 z% b! M' A- ]$ W 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;
! w3 C. u/ x* |9 }3 C" t0 T& z 6 e8 Y/ t5 ~1 i
/ D, v- |9 J) x9 s' F$ L8 m return true;
+ G3 X) [0 a% ^6 C' I6 v2 ?0 f, }
+ Y C8 @- ^5 }" y
6 p+ ]& m! [/ d) |5 h }
1 [ k7 X3 g, _0 X
1 x, U5 V( A# w2 o4 W& u8 K% s9 h9 @1 l* f; G& K6 m
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。9 K# G, Y# V# m3 o
% Y; ]( E$ `( l7 ?
0 P, D: {$ r" a& @: L: Q* u4 ~. U0 h: q 接着会进行真正的保存。通过$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 S0 d/ H7 s1 c; C4 g; z
1 B* b* |/ ~5 E! x$ Z$ v7 m5 c. [ ]: B; {6 x6 M. x6 i/ I
漏洞利用, h4 P: P/ x9 P4 X9 B" ^
u& |2 W! ?, P3 m0 l ?
7 }) O! X5 t- }( }$ F, M; L 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
7 o$ y H" C2 v9 L0 P
; f# o5 N% X& e
4 l0 e( P* T: u: r
. Z1 r" G0 \9 F* o/ z1 m
: m- C4 X/ m" b6 [* P
2 X6 f4 x7 F, v) V 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid% n. l9 N3 ~# e1 V" h
3 A& d% i8 S2 k
+ w; P5 C) l) U% A7 b: C: D1 F 不过实际利用上会有一定的限制。
5 e, L& p/ c+ R ) W. _ k& ~% C, `% p, i
2 @7 q) ]+ ]1 R
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
- T3 _' P }0 o e 7 Q) g4 X; ^- P1 _2 @
7 i+ W) c* @6 P9 e2 g; k
) z* U, H+ j3 K7 ^
w( d: k, O% P- N4 W7 ]$ ?, w$ v# R( z0 v
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:0 h V$ P9 s0 q" [+ `$ [
% _$ g; `3 m& ^; x
6 a/ T- m5 m: n J u
省略...$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) X& X5 Y& |, J: A8 t+ x
0 T N! b5 Q2 i. Y* ^
J4 ]/ ]3 a, R L1 k" T" }# s+ t0 a 因此要利用成功就需要条件竞争了。# a( [7 y6 p3 H
' `8 N {; H$ A' d! V& c9 J1 R/ L; N( Y! r, K- X9 ~% A a4 o
补丁分析
' ^! V. n3 T- [4 ?9 x: V
5 M2 v0 ?( g- H7 N9 ^5 n
M/ w) P* t7 s6 l 4 S7 F; W# @' ]( Z2 L$ J- l
) F2 J9 r* }/ l8 d; Z# V
! a5 L- T8 U L8 |' M1 ] 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:" j+ C- H+ c6 W p6 r! P8 u
x, ~9 h# J3 [1 [5 |
' R, V, n% M; w% b+ `; ~ function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
; p! q; i: z: P% W. k$ [
. s5 ]7 b( m7 Z" S# C1 s# |* ~ I x( B5 p1 U5 r# ^( j
L$ i( F* {7 S1 E# a5 h4 }
3 {: `7 I5 S' A2 s1 l0 T- p; L. _1 p* H
n6 s6 ?: V& r
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。3 T7 s+ d8 B/ U& z$ f, A
! M8 I6 g. c8 Q$ d9 m- X* f9 t
8 [5 m' [5 h! S 在is_allow()中增加对$this->savename的二次检查。
3 I" A" n5 M8 R! q
, B1 y6 W' z* Q# r1 u) d8 h' p
7 \; W: h ?; u" b 最后
+ P6 X0 M' o+ @3 `+ Q+ O4 _8 F3 E+ U3 b+ _* R8 E. C) v
9 A8 i: a1 n/ A 嘛,祝各位大师傅中秋快乐!
7 Q7 K" S+ u2 t8 ]2 g. S* K `* q 9 m2 @" B) t3 T+ K P
- u3 H0 ^- F7 H+ `7 s
" L8 l! P) K; q7 u, N. X
" I* k2 E6 ?/ B4 Q2 i r q$ H# N+ Y
|