6 X7 o' K2 b" M
% S" ?/ X1 e3 i3 }: z2 s0 }5 ~
( l# Y2 d0 ~3 E% R! C4 n8 z& r. }/ W4 z3 S) |! h8 }
前言
( J# S, n- D9 O0 ~0 a& g( R9 s2 }! [, M" l$ s2 `& O# i2 u7 V2 V$ |+ u
; T0 n* Q. u- C9 R
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
4 @+ }0 ^, g+ B: E2 E C( f$ R
+ m' X, `3 \/ L6 o" d, S
6 B r1 A( l4 w" b 2 b/ P5 l4 b9 T* T
& L, c. N9 ^. |, \
% }5 A- j$ w1 l2 O 漏洞分析! T" X7 e3 p d3 j7 |1 Q( D: o: j, K
{& x& |; P7 D3 u( V5 {5 x. o
+ f5 P. ^1 N8 n! B2 H: {+ H# \ 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:, g5 m& k7 Y- n6 B- L' _# q
) M0 e5 g9 k6 R% h$ {" `) z( s. l: \+ E
$ |4 _9 K9 \8 q, B0 D( t
" g0 n7 O/ ~# \! g1 I# L9 x/ @4 J6 ]
- ^4 o* u1 V- L% E$ D; [9 G 对应着avatar.inc.php代码如下:7 T5 N5 Y3 C+ h5 e: h) R/ ]9 f3 \
% _! Y4 Y0 H1 ], P3 n8 ]3 R
7 C7 v9 q% k! ^7 q& W, 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) {% T8 l6 N! g9 v) ^
- z/ j( ^6 E0 N: X! l
; N% @: D( S$ ^1 [3 c case 'upload':" u: V2 ^2 X. d7 a" X s
e% A3 {# U2 ~9 P! O% J0 Y
) M, L) E4 `9 r% }7 M, l if(!$_FILES['file']['size']) {: ^. d! U& X" V+ D. y* b
5 q& j$ p; l- A* j' ?( S/ j; I' a
/ l* W( }7 o& J! K" H( X$ T if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
, }0 [7 l. m7 F4 f' d+ l- F" O
- L- J7 \- V9 |# L' r/ {8 _8 N" N: P3 j/ b" f% B' J5 K
exit('{"error":1,"message":"Error FILE"}');
) K8 p0 Z5 ~+ }2 Z
2 V8 M3 f7 Z, ]# u
6 n1 N( e) E5 h! j1 _ }. J: m4 ?% ~6 U6 W
9 p \5 O0 X" A% P& ~
* A. t9 R" B/ z; W require DT_ROOT.'/include/upload.class.php';- N8 {! |* n' L j8 `1 `9 C A% W
( b7 c+ c4 k- O& ?9 s: V
- R+ R6 R! z6 O: M# K
% B& S) k' v7 V9 O2 ~ 0 w( ~3 o# d' {& |8 W8 n# A" s$ c
: c" J! g @% H( N8 E0 A
$ext = file_ext($_FILES['file']['name']);( s# \+ w( n8 p! @6 u, _
5 y. f% F" h) g; o; h1 w# z/ F! c+ _* |! l* A
$name = 'avatar'.$_userid.'.'.$ext;
! y/ i0 m; V4 S# Z " q0 w1 b& n# f0 f7 u! m' H0 n
4 r- E/ q% j+ A8 z' {0 ` $file = DT_ROOT.'/file/temp/'.$name;4 e8 f6 a! E( [2 Y7 w! s: F* { Y! ?
2 K; E- N) E) |( B; ]' D4 V7 i/ a+ {2 g1 c- D9 X; a
( ^3 o- J1 q- m5 R
2 w/ R& E: {" N% Q% B& Q( U* p
% s# l' I) R; w! v7 J if(is_file($file)) file_del($file);
3 e3 q7 y7 |. o! } : f a% b$ c$ R Z* o. ^4 J2 e5 o" P
: O+ m+ I$ } [7 a# S- ?$ i$ W $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');/ i" F1 \ b; f6 m/ D" h- p
8 r8 S% f1 a; U1 N" ^
2 {; ^# o3 |# V+ o
* n9 N$ z. d) I* j . ]( x& `8 R' T& j- h2 E ~" c
# b( y9 o6 v4 L; w8 p
$upload->adduserid = false;8 A8 k0 F, f. n3 O9 G
$ {1 k6 P( _% u) [) A
9 D# w/ T) v; V" N! V
5 @: \5 A0 f6 W$ f) D6 c1 X$ T
: H$ Q# K$ t& _) p) p, Y
8 N1 t& S( B6 g, t if($upload->save()) {& H- z6 ]3 p' Q$ H
8 z+ P, a9 S* ~9 c3 P, [( q! |" s9 |2 D
...( E" r0 A w0 w) U! K
2 {8 X4 B: ]2 \: j* N, C! a
X$ P( H$ i0 i% m } else {
+ D6 V1 j* K7 U) ^! H# d" { 1 q5 e1 s. ~* C/ u7 g7 O: _% R6 d/ i
$ g6 q' |, _( Z
...
5 f. J* q5 `' C& T9 k4 c " D, `/ \5 y0 h; G" D, V
1 a0 k6 G) s: N5 m7 k8 O4 H. G
}
+ H- b; w7 C- ]; C * i/ a @( T+ O- ~
! E: q" g4 {" G) v' l- u6 z | break;
0 ?) {/ k: g0 K! b9 E9 c: c 9 H1 q% L; I4 Y8 M. z4 r
7 o* x! N; \$ a% N; P7 V
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
& i$ {3 {) e9 M* u% g( b A + o& |. b( |; H( \# Y' n
# u' C+ b5 ]& K# ?; ]$ C, G1 u2 ] upload对象构造函数如下,include/upload.class.php:25:8 T( ~! v' ~+ R$ ^1 N3 k7 i/ n
& z B* P! M5 {0 S+ @
2 L4 u2 w! T; f/ l# y9 }# k <?phpclass upload {" ~, l6 @! b% _, I
! D& |9 p6 Q0 f) j( l$ k
N) Q0 q" y8 v, B function __construct($_file, $savepath, $savename = '', $fileformat = '') {
7 C+ [. Y" g: C* k/ K% _! Q
3 N+ f$ |0 f" J- i+ q( R& c
8 u9 u3 m# D) F& Z global $DT, $_userid;
5 d w" T I9 X6 R6 O4 S / i( t+ \% |# f5 y. p" C
4 E' E" s. l- L% G2 a: K
foreach($_file as $file) {; l4 G3 P8 s( ?
2 U: @: p! y; z/ I; t( l/ v/ C
' u, j) p" Q: h; l $this->file = $file['tmp_name'];6 D" w" g9 O# j- L
8 A% J' Y* f- w1 J; p& b3 b x2 L
- W8 _8 B! Y8 b% F( Q# e% o $this->file_name = $file['name'];
- m/ z# u2 s( k0 _9 S; @6 T9 [
0 r" x E9 d& a% l* i! h, T& m) U/ Z8 j$ ^
$this->file_size = $file['size'];. B6 p3 M5 W' q3 ?0 t, W
# O0 g1 H0 i; G# N" [0 l
4 T7 v% ~) t% v7 L $this->file_type = $file['type'];& k6 I6 |% e6 r$ J; T) C! ?' _5 ]
' g+ H$ R$ B; q, v
4 [; m+ t6 u4 h: [ $this->file_error = $file['error'];
& _( b& N/ v8 a( T8 z/ B
! R9 o% B% q1 F$ W4 ~5 s8 U' g/ o5 \6 ^: N/ w8 S7 Q
* F9 ]5 [& |: ]- [) M0 i
S1 h) P! c! I1 r% A4 o& l5 G2 `6 L7 q
* Z% T1 P( ?1 ?1 o* E }
1 d) Y% h/ i6 v : B1 o: T- v: C
4 s$ E' g. K% y% h' z8 D* U6 i $this->userid = $_userid;
) A% p1 |) j8 f( i1 O
" i; Q3 s! a* C J: [+ q6 T9 s0 R/ @
$this->ext = file_ext($this->file_name);
, q; I, Y& _: Y% x- \9 V ( H' M2 v5 N: i6 G
) {: B' i9 t& `3 W6 O/ J% |
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];& n9 ^0 L0 {' _6 [ h4 G! q8 j
# L, C1 A8 S, h5 g
- _( i) p% L& Y* t# h. _. v $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
. D0 ?! |3 q3 S' V
8 Y; W1 V) D+ x$ Y5 b7 |
5 Z: o; ~( `0 U) x2 A; t' V $this->savepath = $savepath;- [+ v. H+ W+ ]( W6 N5 L& M6 G& p
) z3 h, {% Q, l6 `) T! k0 C+ Q7 G7 s. u* e$ H) [1 F4 U
$this->savename = $savename;
$ M: q. Z- K/ P3 q7 q k4 T. g: K) q" T1 a
: d, z% H+ | D1 x8 d5 g% J' V, \8 y6 e4 s4 l
}}
- a6 S C: s8 ? O7 F$ z2 q & K1 g1 P) ?2 V! D$ }7 _6 f3 c
3 ?# N5 Y& B" [, R2 u% F/ g- H 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。& Z+ o/ e1 M. g, d9 A
6 ~9 a, F5 {6 e0 |% r0 \
9 X- ^1 ?2 Q: e' H% P0 _8 h+ v 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
, ~' u( T# J1 H( N0 [! r. m" [
8 n. ~2 Y6 }3 _; R7 q; s$ ~ O* X
8 k: I( \& g4 m0 p' n $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; d. D" F1 `3 U* b
0 T, g$ B$ h- X( C* i& S+ [/ n: e5 P$ E& I8 J' q9 o
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:2 e+ w* C! D. t6 j. |
1 A" c7 D" F2 Y1 n6 h6 ]) S/ U2 a% b5 K4 Q7 L
9 ?+ C; G" |0 I+ J! t2 q ! e3 N2 {7 }/ v) Y, x- l! g
8 [& R7 S" L @! ? 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:5 I$ f6 V" p5 B
& H- v( q9 _% G1 b f J) p5 Z
+ K. p' e7 o) y' i <?phpclass upload {
* w( j/ c) R) j0 i8 x4 r w5 K% l. M * i9 D4 s7 E) k+ P
: d7 p7 z1 \1 a2 B p8 m function save() {* m( u& O H5 |" `' E! v/ v: Z* X
7 J" x7 l9 Z) D' ?! {' t. S
- Q8 E0 k- i* Y5 F4 _+ b. U! x5 q include load('include.lang');8 K7 r' O6 J" l4 U: V: F
' H9 R7 e$ C/ N. `8 _ N& U
- a. I# Z7 p; y0 W% n0 n
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');+ D3 ~! n; d l' Q- K5 T: V
- |" Q# u! C0 r3 t" Y" c
! k9 u! i) t, h9 y# H4 l
2 n: H h% Z4 ^ O G; \6 F& y# c4 m3 X( D
2 D; o, z1 ]9 B' q& J
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');5 [2 b$ p9 |% Q! _2 S
9 Z6 V4 V8 Y7 a
7 K, g5 m6 Q* C/ l' \( u ) ^. I1 D9 J4 B. h: g! M
, N% I& P' a# w( Y, B3 m. s, w( v6 Y9 l8 T0 }2 D
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
! b. K+ b9 z6 L- Z 5 t; U) J3 F5 e8 N1 k
# t& I; J# }/ n& j$ v
! X" l `; h# M6 x
2 K }7 S9 R, z! G# ~7 d: a) }0 l1 S5 p# }3 |
$this->set_savepath($this->savepath);
. f- z1 O; K# S( m+ h4 V& Z - t4 V8 S6 s! x% e: V2 Z& d! [0 e% k" K
$ I% c! W* N4 J! `! C( s/ ?1 t
$this->set_savename($this->savename);3 b& D, y P# x6 t6 j+ L3 s
) N7 W& |/ I% y4 s+ ^: R6 o0 H# E" q! ?6 D8 Y6 @% k- H
3 j5 l, W, U9 w" d! _! t
4 B& f# I( m! i9 j/ x! U! M
4 N7 @6 _/ Y6 q% h) ^* M if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);# n9 B+ i) _/ w( G
6 b' Y2 O& g/ H8 S3 f0 Z
6 j- E) J4 L M8 N9 f# ^/ `% ?# Y if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);# O" v( m. k& e5 b; T
3 [! t% C+ w5 k6 D* h1 e4 g. d( _; F- D; T% _ ]* X. ^; z- x
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
5 }4 m" L; [, A+ [5 [2 ? 8 s- n& z0 Q& c' w5 C
) h; o( L4 y2 l Y; P$ o
" ?8 z. T" o) d+ H/ f
8 q+ G6 p2 @4 G& G3 _& Z/ c$ F1 S0 [+ i8 p6 G) ?" z1 E
$this->image = $this->is_image();3 a+ |! O3 H7 \ ^
- o; j" j7 G. j7 a
$ M4 s; B# c# R$ s) [& Z if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
8 L, S" u& a) d: Z' _4 V" k5 |% ?* T! B
[! b0 P3 v' _' n6 V
! j$ v2 B% c" C7 i8 t return true;8 V5 h% p. @; T* Z( ~$ j @! u
# ~; n3 `8 x. u0 C" m9 y$ t2 Z. e- p6 D, s7 _/ z- w; F, E9 K
}}
9 b0 p1 o3 ^1 K) R6 P+ F8 B ) l; Q# {$ h' f* ?% }
- \* d1 v/ C' @
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:0 D K% E. E0 B. K ^" n0 n
4 _/ `0 r5 M# @1 o" R1 [
0 a, l+ Y9 q$ s l/ r. U$ {: B <?php/ K1 b% a/ A- x1 Z R5 b3 C
K( [: h" ^+ y, l9 q* N' R
$ u% }/ |2 _) F% D& | function is_allow() {
/ w$ k* _5 v! U3 G7 P8 Y @
1 s) x/ ^, N8 Z1 s* J
3 G I2 n# c* ~; C& q ?: X if(!$this->fileformat) return false;1 }) `7 Z3 O5 X: u$ H
+ s# B5 [- \ H, s& x$ J7 \- _0 G. I1 o, p0 m" d
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
1 A* T q6 h/ L3 i9 s/ f: @% ~ 1 l% ^3 ]* n0 E4 {$ \
7 d% v8 [. `. X# A 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;9 o5 ^ c, a8 w: @8 I5 i: c
; Z! T3 z1 O2 b( u& i2 y
* _% ^ M' Z k6 U+ a
return true;
k1 C2 `: V& N# s , ?3 @) O* u( F6 M
6 O1 B# ]+ ~! @ }
( ]2 J+ N9 h, C! g% B$ X ) {. D/ n [" I
7 `/ Z& Y+ O7 U P- x% X+ A9 t" V2 V
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。% K: B. [- X% L
- g; n2 \/ k) m: |& q1 j) G' q% r7 E6 i
接着会进行真正的保存。通过$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文件。
3 S% |% Y0 ?+ N6 ?) r& v* Y8 f
+ N6 f! C$ l N
4 r' V% \) j2 i- V9 C T6 L 漏洞利用7 U/ b% L: k2 i5 v* x* J0 q( g- Z
& U& y2 v5 u& k$ \% d) c* V9 A, G
; T P/ n4 R; f4 m( H 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。, E7 `# @3 ~( i& ^1 B
0 s6 B. W: ~8 c5 h6 c y+ X8 s: W5 D2 Z- c) n2 e. Z1 Q3 n
E9 k8 T/ f3 O8 l; V+ f ! q2 `8 e3 z+ x! C+ p' g' ]4 q
# R3 |( Y! j+ x5 ^% z 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
' M K( _/ [! ?3 T
; ~9 @3 c3 ]& c |2 p4 { X) q
" i" ]" x% W+ Y; ^6 {8 H1 g 不过实际利用上会有一定的限制。
9 n) G" s& H: _: h1 s: L& c
+ K# _* h- o+ i" K) n) b0 i! H4 x) a+ R! z6 c
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。" P* C8 q3 k% T5 ]
7 B' M3 Z$ {5 m; R+ W! L
/ [4 o5 m/ `) G% M2 a
( r& B4 m7 W+ V& E6 O ) B$ u1 w$ s' B4 P4 z
5 i) g6 g( r4 ~7 T. _5 F 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
) F z& x; Z7 E. S8 L
7 y5 s( z% x1 I0 Z. A* c1 e( z0 m V$ W! ^! F; v$ F
省略...$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]);省略...
5 g2 J' g/ P3 n& [9 d# p: [ 0 k* @, h: }4 z& r% n9 I
/ [/ q5 Y2 |% `9 U5 H
因此要利用成功就需要条件竞争了。: @) E) B$ m3 f; V% ~
y6 X3 e2 \! V4 }% b9 e' F$ B7 E9 Y- s+ S- E: I" w' W
补丁分析
o3 ?2 i. z5 v/ h6 }! h. R, C& f! I; Y7 c
5 [$ i0 L0 A- d3 H
/ n7 L6 w- T' B9 H0 w: h5 e
0 S* x1 r$ ~- b7 u' |7 V% z9 T( z
& j7 X; I2 }1 L ]6 F1 d& s 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
& I6 Z, x7 `& }2 U " v$ N2 V9 Q6 K9 I% G
3 W9 K6 q$ V( W. o, `5 e function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
" x. ^5 a* n# q# Y# V
9 w3 q. U* M1 \! W; J: A5 G3 |5 z. T9 s9 g2 D* K
: F( U9 S1 y' m4 _! [1 ?9 u, Q
/ a# w9 c2 K* \; x; h4 E; D
R0 ?) r' _8 u/ a4 @. |' y
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。# i" I$ V& M6 R+ g& ^
6 J j' @% d& |/ G6 X3 c. I. H% x9 v8 t- d7 ]9 N5 O3 U4 l% X
在is_allow()中增加对$this->savename的二次检查。
- M2 F. h5 T, S, F, _. m. w) l , @/ p& ~ F, H$ v# F5 a, X
# `$ d9 M8 N# s. ?# c 最后+ L# U2 K5 b2 @0 |
4 b. r A( l: b- [) w8 _9 V: X p- u0 S
嘛,祝各位大师傅中秋快乐!5 o# \( F) K5 r N0 S% X4 L. o
2 @4 U" }+ t9 s* i8 f3 z9 I# O0 Q% a5 D- B8 u
- z9 x( j1 A, o* n' l ! r9 o) Z2 x9 O! o
|