! I# b7 u. [2 Y. }% u3 O) E: ]* P$ N! P H1 a2 c( R3 k
1 y$ \5 H" o! q2 U* k6 e+ k2 Q* B1 [0 |! ?
前言: H& |+ c- L, I6 ?+ ^( ^- R
; p/ S/ W8 D! c! k8 o# X+ K
& i9 y. }$ i/ B' H, b% y' K" \
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。- C- @% ^1 \$ S5 [- I" ^
& [- i8 c" Q. X ~; `& Q$ \8 y. ~- M3 A8 h& a
2 o/ l( [ i( [ M
5 D; A/ ]" u* {, Q0 T% k2 L. N8 S2 Z. W* M3 x7 a
漏洞分析- \. k$ G$ ]7 O
5 U& ], y& ? W+ N5 U9 ]* ~
5 |) ^4 t, F# M5 y& `2 Q, i$ s5 V; F 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
. H6 D6 I: A5 L5 H8 R7 l' p' m
- N3 T& q( b7 b/ E# W8 P" p# q& D8 w, m4 H8 q' A/ B1 O
) Z8 v0 A( \5 x0 r
8 z4 l. c5 A6 K2 p5 b, v/ b8 r
" m: _2 s! a! J6 O 对应着avatar.inc.php代码如下:/ G; c( P! a& }; Y8 R4 g
9 `- w) g0 O; W0 q2 j0 b
) [1 v: D& ?0 O, q/ `; U, S3 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) {+ l( k2 C: L) s
4 ~) @" K% [( u5 u
1 P5 O# W% v& n+ R; k case 'upload':8 u3 B. J- V+ Y' e( S
. Q6 r4 l, e- J0 }8 \
3 I, J# H) J+ n9 G if(!$_FILES['file']['size']) {- M! Q+ M' t- f: B! t! \4 b, P% U
' b3 A* w' ^4 q' y* }' u- r9 C* Y. _8 `. r2 s
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);/ u _) ^9 i8 ]' p
% `( B, n- f1 ?
5 S n3 h O' \& k
exit('{"error":1,"message":"Error FILE"}');6 ~* l/ m( y- O# K* {0 m/ l1 @
6 f9 P7 q6 y- e& `: h
$ a `' E5 G* j& E- U1 z6 m }( ^- Z) F+ g/ _
! `* |, @: F4 b% y
) |5 s" [% f0 u9 u) ]+ O$ u9 _ require DT_ROOT.'/include/upload.class.php';# F) N2 O5 s- d" U- J
7 Z" P* q: q, l$ a" l, ~( k- Q& F! z9 p! i6 v' S
* [5 _7 |9 R. o! D& A
6 Q- y7 m8 P. V
6 P- w; J% o1 {+ C# I( o $ext = file_ext($_FILES['file']['name']);7 e! ?0 w) e% V/ W- z0 c' d
. b% u8 Y6 e6 k/ M% h+ r3 l: l, R4 |1 f- D6 O' D
$name = 'avatar'.$_userid.'.'.$ext; y' G2 B! d/ q& V& f' q
9 p" f; f+ x/ R# a" |, t. t
' _2 G* J) R6 C
$file = DT_ROOT.'/file/temp/'.$name;4 F( g7 `" ~# \8 D) t9 B8 o( o
7 l7 f7 C, a# O0 f v5 ~6 z8 `4 F9 Z, v$ t* _# B; q
& P# M `+ }5 W. h! Z
" @, v3 q _8 f! s
* }- g6 C' M& P if(is_file($file)) file_del($file);
% ^% f% g ~. Q # V+ K- u! O" D* i, ?
3 M `. V* v& m; A $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');2 l& Z( X5 ]: i( ]7 a- r
, c" [1 f1 x$ \# I, O. b3 E0 f
# e ?& _' l1 _2 A4 _/ q
6 t& E4 q& z7 S4 }- a% d" u+ {
9 B* O( D0 P$ l/ I6 U
" D7 a1 L6 n8 x/ D" ^
$upload->adduserid = false;
! H8 |% x8 K: a3 _2 i
0 }8 U ^7 I0 Z" n. J+ o- h& e
8 D2 ~+ a& }' o$ S) N% r
. o$ {; f0 Z* L8 y5 n- l 1 U& Q0 M& `2 E
# u& P* N6 T6 W& l
if($upload->save()) {$ Z$ u# H c+ ~5 y
* j. ^7 e ^' _) w" t# M+ K7 l
8 B7 Z# s2 d$ _
...
- i9 N- N. q |) G1 C \ 3 H9 e1 I9 N5 D- r `0 Z" J4 S) L3 {- E
& y8 T1 D$ E: }; D s: Z } else {/ U0 X/ \( k$ R
5 {9 r t8 L2 ?( z, _' t! i/ B' j" d! o; \
...4 p3 K; S7 E5 Y$ q
" x8 k9 V; j0 b3 M
( j- F, b) ~! Z9 A \* i! V6 {
}
6 {6 l C; O' s1 a$ b e/ P9 z
9 ~ N$ W0 r9 Y/ [+ K) W# a- W8 u% I$ B! Z
break;
3 e% V7 ^$ a, ?
# n0 \/ r9 A# p3 @9 s+ \
1 ]+ B0 D8 X' w1 f& W/ @% H/ I 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
$ q' {" L* B1 j/ a9 g8 u$ ]
$ {7 G. u' ~) Y+ C/ f/ }: N
* t/ I4 V0 `+ H) [; V1 w6 Q upload对象构造函数如下,include/upload.class.php:25:' z" _; G. m7 R+ n% ?
! Z; `, Z Z7 a( M7 @5 @
- m& }! |9 F: I5 G [0 l0 U( N4 [; O <?phpclass upload {
8 R- L. \% c4 ^( r" v/ G
/ s* `1 K P1 b7 Y& p) I! j* r/ Z! n6 Q
T8 k" w5 c: z3 E K/ ^: S( {1 L9 d function __construct($_file, $savepath, $savename = '', $fileformat = '') {- | m; }. `3 A
6 |' e6 s/ _" x+ `7 U8 w: G% M
* }6 N5 l8 r& b& ^6 Q global $DT, $_userid;7 y1 R/ q5 Q5 K: M3 z, n
+ k1 U) Q7 @( g' g2 |# e
- y( N$ d, @3 l. \- L2 ]7 ^7 m foreach($_file as $file) {
$ S C' }* {: S% d& f# n2 [4 p! p 1 m# Q3 P/ H4 k
: H' ~0 R) j& j) ^$ g! H( C4 x' N $this->file = $file['tmp_name'];
, y' T$ h1 M5 U
4 B% Y- F( L$ ]# u4 ]% H5 A
5 b1 ] W4 h1 U2 J5 I3 i! V $this->file_name = $file['name'];& ]1 }' F* d# x) ?: d
# _3 o) u' {6 T+ S3 I; m
& G( _% M7 j" R& ? $this->file_size = $file['size'];
0 C) D7 g+ G! I9 O1 R, N" C1 e
! E" _2 ?; A& g+ ~8 t
: _* F: B& b0 s $this->file_type = $file['type'];
8 z7 g6 x& P- G# s6 u+ ^( E
6 Z+ N; g% ~- W# [
2 C0 _' W! k$ b; l) H6 I7 ^ $this->file_error = $file['error'];
6 B" @7 z/ W( Q% f5 E
* w- M' ?( a1 H
# } d' u- E+ D) F
" X7 {7 U4 g: o w$ y/ k
! S7 m4 C0 v. U7 g+ Y9 N* v% ~9 S Y2 w6 x0 |% G K
}
6 ^$ v& P; I4 b5 T9 M/ a. _
6 o/ A7 p P+ k+ h
- L2 G8 G* u% R. T) [ $this->userid = $_userid;
3 N" e$ [. }& J) ~' P, b- Q6 t & x8 h% {! b. t+ i
) f7 }( o/ V, d9 C+ e $this->ext = file_ext($this->file_name);
A$ V4 t9 ~ D" C% k- l s9 u
* l; Z% d1 ?' L# q; Z5 v) [8 C
; e( q( l( i9 E/ ~7 z6 J $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
) N/ t( Z [( p0 ~/ e5 L( \. n" B 3 @1 C4 _5 ^+ }6 R; {; t
+ w- _- x) t+ B% K8 C! e
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;6 k; @( n5 t: V' v: ?% r3 |+ [( p
: ?4 f2 _. c( a& S% o( o; S/ ?4 a4 J2 y4 @" E
$this->savepath = $savepath;
- Z$ V! M4 L' g
. q. l" |- j( a! X: P& E ~
3 j" ^$ w+ G+ ]- x $this->savename = $savename;8 X$ S4 s6 t/ {0 N
) u. o a7 K b) G o6 d! z
$ R; i w. J! p/ o% [) c4 z7 G }}
, \( p/ Y, ~( ]/ R# W $ `8 r! i! `( u+ _. i! K
7 `/ P% \1 b7 ? 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
$ u+ {% Y) _- Y" r4 L/ e6 ]; a ) g1 w8 M3 ^0 ] s( |% f4 @
8 i. D2 Q6 t8 B. T) h* ] 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 2 s9 {/ @ T3 x
) E! @$ L7 ?9 B) S1 l
( \0 a7 J. Z% k# d- v( N, k) 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
$ n( V, _9 q' G0 P8 T
8 }" F( O6 b0 `: _$ K4 V0 L( o( H8 U# {- S3 C! R' K
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:. J- k0 M+ z4 X/ z+ V3 u R* Z$ |
* B7 \, ?8 r9 _( t- S. D5 M% M5 O8 n9 [: J* n7 q' N
( l) _+ d; C$ l4 Z* _) B1 c: E ) w4 u0 n J5 B5 f2 ~
k; @: [ W; F; H9 T 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50: L* ?5 {1 [9 e, p/ r
/ M u* |- o1 ?2 K) \1 v
; d9 z* i8 U# [/ V# N7 _4 `
<?phpclass upload {9 m: I; O1 ~* x( c( G4 ?" h, I
9 N, |8 p8 X0 y& o
/ d! }" a0 t k: j3 G
function save() {
! a7 b! d! m( v) N t# g ; _: E; @/ i9 ~
" v5 i5 p9 q" ?1 ?( Y7 T# c9 n8 b
include load('include.lang');" D" m# W2 u" J, e" V8 C
3 m; w q: ?8 c; p! g* ?
* j1 D l/ z% |% a- W) h if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
" b* K! a& }' d9 |: S
7 D+ r3 c# ~4 w8 O$ s+ o, g" s9 a1 k% q0 o9 H
& A" a" b7 q6 `! u ; y, X+ `$ r. a8 }" Y- R, {
. X3 v+ X2 ~% E V) s
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
$ F$ W8 u2 [) q3 S. O' Z
0 B/ g! f+ b5 q9 @8 n" E; m; h, Z5 D( ~4 V8 a) Z
( n$ G6 I& B/ X* W$ u8 P
, v# s7 f; k5 v0 U" A/ H3 W, L8 ]7 j+ j- K( d, r2 }' Z3 p0 Y
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);8 j5 k" x& s2 X2 }/ B6 C- \
; X5 n: o/ y+ e* ]9 {
" e: c8 R8 V% _- _ 6 T( {/ G7 j; a7 o1 p/ x! X
) A/ m9 Q, z$ g8 Q7 I1 e* O: {& }; t( J$ ?6 C
$this->set_savepath($this->savepath);
5 H. ?$ m6 _3 [
' Q* J a- g# Y: i4 Z6 \- O9 d1 L6 C7 B
$this->set_savename($this->savename);/ y, G( O) Q" r; r
! q/ Z2 {" D% V- O/ E, B% k7 ?# p
* b; t/ R4 X5 X# b+ s* p2 E Y
* m9 |8 B6 v: R' z4 C ! C3 v8 W' g2 W/ t
4 W" W' A, v$ Z G5 Q7 A- Z( C
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
M3 P K, H* d1 W$ D0 [$ }+ x8 r
3 T: d! T% O9 x; b" P
) b2 `; b9 b s# Y4 `) _ if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
6 D! P8 w1 n/ W) X# |
. U2 x% D; b3 ~9 k* M
9 G3 X% o1 f# M6 o if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
- J( V3 @7 ~6 \0 c ) ]& W9 R7 @, y; m3 ^5 G
. y# O3 ^/ T7 O( o' V0 z4 ~' r5 O
7 i# Z* |! \ q* L% D$ }& N( r
& v# V. f N. \! D
7 V8 V' n: ]' ?2 {4 S3 j $this->image = $this->is_image();! Q1 J- M! Q) A+ X; F
' P) A" ]4 w" d: Q* t2 y
. N, b3 v% p* V+ ?6 K if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
) d, j _4 l1 E# e6 N. }! | # J1 M0 ?& W6 w1 m; Z
' B j, l" R# Q- _ return true;3 d: y# w2 S1 q% J, e$ Z/ v
. X# k6 ]9 W8 i. ? `: E9 n6 C; o2 R" m5 g. W6 L# N
}}8 t) s) ^# b0 ]( g8 |. A& i1 R
8 J, k$ y2 d8 e/ d0 Z* A9 p
2 H$ W; G2 H( n( \2 e( P4 o 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:# P+ v4 G; r! V P
* Z! k$ }, ^3 J9 m6 G/ |6 s
+ R: m7 C) t4 e' m( N0 x* y$ m
<?php* W b0 ?9 Z; O! U, D5 q4 E
/ v ^# J. O L- b8 T
' Q" }4 b- v8 _; E% z function is_allow() {
6 v8 G, S! h1 |/ E% P ( J& G) p& z( @% k
% f% u; h# `. V) @* k) K1 Y
if(!$this->fileformat) return false;6 h% s8 C. a/ G/ P% \+ z
9 i9 J' A, ^% d* z# H
6 N1 w; X5 U {2 s; ^4 }
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;- ?) w% A* \' s K+ Q
1 B" D9 v3 E( Z* b" R. p
9 f- L+ ^. N1 M( [/ B$ x! _ 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 \. V% U# |+ j, u3 d 8 J! y7 P. w% X6 z$ Q6 a! E
4 m4 v$ N: z3 D* @" y8 o
return true;3 j6 a6 o! q! Q, U6 Q! z! l9 n
& Y$ ~% ?4 H$ r
! C t7 }, J! ` }& Q- F3 \% \2 p! `
}
/ n# z* x# B. K! r% g
! C8 t# T! j4 w, E$ Z
# o5 ?% c1 _) ~, r2 y# O, a9 V 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。4 U. ~, F3 Z5 `2 a. \
- ^! u t! @2 y) {3 D3 P, x
8 j: l: F/ w( G+ h# S 接着会进行真正的保存。通过$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文件。; |7 {# L: q- q% o; i0 t
' D+ M3 i5 V- f: l, K o/ V
8 N; H" O0 Z- T" ^ 漏洞利用
2 b2 u6 w/ r, B9 M2 {8 ^! ]$ G0 d' H& s. m, H- s9 [
3 b7 Q- U: Z" f2 `5 L/ y 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。$ I( W+ T8 d) u1 b- G3 X5 J
3 }* K, t2 q( ]) h6 m4 y5 b2 l- S
1 Z% ? C+ u! j( A' w8 d
# a& Y6 p9 w+ y1 ~ 1 ]2 _7 g: O; j: }, i; p
, G/ g1 s7 U3 W. u 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid3 j! K* V. [/ Y" S
, v) D0 w1 A' @8 G# X
; m! p$ z' Q6 B) P5 K 不过实际利用上会有一定的限制。
# H4 B, q+ p3 a8 U8 q4 ^: F# P3 m
0 v" s9 J: L$ A3 |. t2 c. E; j) b9 e6 R+ [# W' ~
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。' z, D: K' w1 R* D$ b: \9 Z W
9 x8 I2 w$ B( D9 o X2 |
4 R' ~1 Z# p }. B- N& q/ S) i( l6 w
I9 f5 O: R$ W- G- n% F; \ ^# [" K5 Y) f0 j9 m
2 Q/ L0 K ^" O. g 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:8 d$ F" U# R8 H9 T/ \
+ L0 D- ^) d/ C/ |$ I" E, b+ f' V" c
3 F1 _5 k5 k7 V" D* `- S( B
省略...$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]);省略...! o6 A, X" L$ J1 s
& I4 [! n0 g9 M7 G8 v' @9 {& o2 S% F; f
因此要利用成功就需要条件竞争了。
- o% j' B2 N4 m& E 1 U4 J0 B. Z$ j! Q6 ~# k! P* T
- y) a* a# I$ S( G- X* Z
补丁分析* g1 w6 K* \5 ^5 w& |/ R7 Y0 h
) { S& i2 ^5 \" H, b, a. O- x
3 Y( Q7 R( O( f& ^, g1 \
2 Z0 ?9 i6 o" Q6 v* b& ?8 `: f+ X& \
4 X+ t/ }9 A1 g0 F! k) U, Y0 \+ I& c% t& v; s# V; l# L4 i6 J
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:* D' ~0 e: b) v
. Z3 G( c* ?) w/ M+ D6 f! {) e
$ I, n0 I" n) J# {- H
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
7 ^( c. E' a8 p* O R* M p % ]. h+ u/ Y6 J. v" G* i$ M- m+ a
+ u7 j/ F+ ]$ }' E+ u$ D
0 X; m' S. q, }9 D9 t% T 5 e! D2 O( t/ ], h5 g1 {
; D' C, E g% p" \ 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
2 u1 q2 C$ t! `
w2 o2 V d: u1 P# J9 v0 {8 B% G/ i" O9 l3 z0 t" M4 ^
在is_allow()中增加对$this->savename的二次检查。 N/ C( M: n9 p. U( T
8 p9 S: v; M {! q7 w4 g6 x7 ?+ y U
( ]* L; e+ p6 f- t0 }( a 最后8 \, }- r4 W/ ]: ~- c7 G- |. N
! P+ C" g8 n# @& H( f+ P# P. j; g* y6 Z( j- f
嘛,祝各位大师傅中秋快乐!2 Q6 d/ c3 @0 B7 J4 O) ~
6 W4 D: C/ U4 M+ _; q, m
' ?( O/ V4 Y4 y" w % Q4 N5 d& j1 @/ Z
6 l7 D8 K, m! \# J6 ~
|