3 u) B- `6 r" s( J: H o. j
" O2 u' @ \9 y# j8 a3 z
! M6 T/ y; S/ d$ F
0 H" {7 l9 c5 q) f) d. m3 e, h; X* S
前言! w; z* L, W0 B% v! ~) y
) N/ R- H0 y& n: C
: {5 M5 H% q2 d; B
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。. I" g2 g" g p( N4 r: z
4 c5 i' |9 n. B. j$ g9 X
& V- J: e6 i/ W9 x2 H
- ^7 ]/ _8 N2 [! u ( {' h" P- v4 Y) o, l' O0 t
2 X% j2 v3 z( X& K. F$ t7 B; ?' a
漏洞分析
6 {, W+ N$ p- O& M. p4 E6 v% C- X, e- }; { g
1 p' G1 q& B6 D; O3 p; \) c& Y1 v, R
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:# }* n! H1 d$ r% v! X2 f6 I& a
( v. T- d- d- w: ]: U I" l: y& f! P! T
# B+ t- i' Z* b6 C! |+ c- t
2 o7 B4 K7 c4 P8 }$ [# L 9 q) P. h& E J8 ^
0 ^( V' n, w+ x" f7 ^+ H$ S1 u
对应着avatar.inc.php代码如下:
4 G/ ^1 s6 u" S, e+ w8 P+ }
- v' c: g3 `& }2 x% {" R' ?- W, g! a. Q3 K, ]+ ^
<?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) {
H$ `. u0 S7 }8 w. _% u4 Z, X- G
# l L, }% F/ G# b1 {- V' x P# C, V6 }) s) F9 u& e1 S
case 'upload':$ d4 c; F! E" i0 ^
* o, a& I8 M7 y. `6 A) l; l2 M+ O( W! Y! x' C2 a! D# [
if(!$_FILES['file']['size']) {
9 S; i( B4 R* T* G% ^9 r- R& V9 j+ ~ . ^, @: s8 e! w7 {' Q
* l1 H) c) m2 T% n% l' A8 h$ H* ] if($DT_PC) dheader('?action=html&reload='.$DT_TIME);2 h8 K( {' G$ D8 t/ B8 X
) l1 Z( n g o0 T4 \/ E8 z3 Z0 m6 b" ^& v7 W' v
exit('{"error":1,"message":"Error FILE"}');
" w# e* y; k; a
) G) S' b: \8 x. Z2 w. [9 U) M$ _/ a5 d" d
}
- R8 S1 W+ i0 B1 r
/ y! Y8 @4 J' H F! r" j8 Z# G2 y4 ~/ j9 ?- E7 k" y- K
require DT_ROOT.'/include/upload.class.php';
$ e1 D6 L4 V% O G4 ^' o
p+ k$ l# R9 r
6 R R! N5 |/ d) L8 t
: I% p, l8 [2 n : u3 V# |4 ^$ K% p
7 V) X- [# ~$ z; m2 G0 i4 _7 ]) I# F. h $ext = file_ext($_FILES['file']['name']);
& c* x" c0 j) v0 o1 A" G- s
( h4 A* U& p d; ]; s7 c3 W$ G* W& Z5 P9 ^! L9 w/ U4 d2 m. O$ n
$name = 'avatar'.$_userid.'.'.$ext;& L3 V9 ~/ s7 {* W
D7 Q3 \: `; Y% D
- s6 |7 p4 F5 A( S $file = DT_ROOT.'/file/temp/'.$name;
. b1 @6 u8 P0 H+ P: S" U6 h2 w; j
+ X1 u. X1 _8 z7 ^4 a+ M( J
* i. B" N" C1 n+ w7 ^% |0 R T" m $ B# w8 S- T. \- j* ]$ _$ R
0 [; `$ { A) m" D
2 G- C: m+ ^( k, l4 i) y. \ if(is_file($file)) file_del($file);& E7 ^# a4 ^7 |; W/ J1 g9 i& A
* Z9 J% A6 o/ V2 W4 N9 Z8 R- ]) M
. s9 | k+ ?' a) A0 _3 r
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');& X5 b; O) B; M# i
3 m. H, s& A' @# d- z R: o
% } u& q& G* c' \8 q
i: F1 G3 @7 D 5 d! a, ~7 h6 T
+ |: Y) a1 W5 R' b# v& J- j
$upload->adduserid = false;
: _9 d8 y) ?! V+ Y1 @ ; F8 _* `5 |; S- ], C
6 [2 ~+ ?! M# j- h! V
6 H3 r3 @7 ^3 V* g6 {4 Q & e- ^$ |" K# N+ P* X/ T2 S
, o6 T# W8 W+ w" H" i if($upload->save()) {' K) L9 N1 g" n3 N
5 `2 j4 E; T# V! V9 ~0 S4 x: a& q+ ?% \
...5 k9 [4 t/ ? ` u, h( E" Z
1 q' {$ t8 m' c" P/ a7 w
& h4 |& F" g- l7 {; B h% P } else {, j1 G, h4 b) [0 x* ]
; r/ l/ r( x5 |" h3 p0 J4 k4 W
2 }! m$ \. b+ w: r# e
...
+ D; j* q6 Y( G0 e) O . V2 Q4 L1 S1 z+ o I6 m+ {
8 V9 [, Y! B# G+ Z }$ S- j: s# F9 P% E( ~& ]! U0 S/ e
& i1 w* `0 y/ Y
* T3 R0 B3 G2 O6 ~$ c break;$ V3 C5 d& |: [1 C$ O: Q
+ L$ h8 ~ t: v, A
4 w+ l3 p8 y9 k6 c2 y* Q" R& a 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。; [1 S- r; E! t( t# E7 v
/ L) p# e7 Y" t" i; |+ ` ?1 L* s9 p: J
" ?& }: P( h4 _/ i0 A+ v z+ N
upload对象构造函数如下,include/upload.class.php:25:
, t; S) U* W$ X4 V2 l( @+ R 3 @5 M( h) o2 m
; Q8 K4 C3 U ~! G8 A
<?phpclass upload {) Z8 {9 k4 d, L v
+ `5 Z. U- @3 _- K, p
) ?: U4 ]6 L8 C+ } function __construct($_file, $savepath, $savename = '', $fileformat = '') {1 R# k( [3 v7 J C' s
. s! o( J; C% T G( k: i& c
" @. Q! R% Y% P o7 ^ I4 @ global $DT, $_userid;& }6 N3 W( o1 s$ o
5 y6 I+ {. K- `8 y. I1 T) l4 u' }, z% P$ B- [! |0 i+ ~* a# v
foreach($_file as $file) {8 A5 [# Z: x1 c. L& r
1 ~! ^- E2 a$ e" g
}' d. y" \9 b* e/ \& d1 N7 \& w, k
$this->file = $file['tmp_name'];
% G H# A# b `, k6 p
7 w& s% K$ d6 c
; ^. m. B, v1 \1 Y9 T N $this->file_name = $file['name'];
, O' `7 E: ?$ p' `
9 B+ Z$ C- b6 V7 X/ g9 @, O- f* h) y" x' ?
$this->file_size = $file['size'];- v( K8 Z7 N: X9 u
. ~ ]0 ^8 U: }! ^1 u6 I' \, e" R
2 \' X6 J0 J- H! q6 `/ J; @7 e: ? $this->file_type = $file['type'];
- U2 x4 l y H! L4 y! `$ { % x" b1 F) ?" `$ R
6 b" j" ^3 ?8 m5 \ $this->file_error = $file['error'];
3 c) n! g: y3 C9 Q0 L
- j2 F1 ?$ l0 k- h2 \5 H
+ a0 F7 N8 `0 {: q. q# ~5 d . |7 N: J5 F5 \# L" t4 {
0 x2 E5 V! p3 Q: @2 }8 O, \9 ^( Y. H) B
}0 _% n/ u* b D3 ?" J1 u
: W4 v' q) _6 ~4 c# R( E! R+ N
6 j ?/ C& p, V8 G3 x$ S* t
$this->userid = $_userid;, ?/ }0 \2 t7 T
8 x5 B! P2 W, g/ R
* D' n) z' r$ L8 y/ O$ ? $this->ext = file_ext($this->file_name);3 a! u' t) z3 ]3 f- _2 A. z
! Y+ a. Z9 k; y5 v% M) t
* s9 z8 F2 Z( K! i, g2 y/ X$ S& N( g $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];$ t( T" j7 ?7 @9 w
0 k1 y1 E. ?8 H' T1 U" n* y4 C& g: N2 O7 S) j$ i
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
/ e5 F( \" E [5 K0 s $ j3 x @! X9 z8 T2 E- s
& f/ L! u0 l- \ $this->savepath = $savepath;' E$ `0 w9 |5 ?
/ W; l) C6 y, Q* i- n! [4 q o; n# [# M, Y, O$ _8 T. O7 c
$this->savename = $savename;; T' f* d$ U5 Z/ s1 r2 g+ m, Y3 g4 A
2 d) v* h2 h0 }) g0 |- l9 i U5 B2 t& e% ~, ^, o
}}
9 i* ]: k7 D4 z2 C9 h4 M. v
4 W7 m( n+ z! Z, Z' [' | \+ h1 w( L/ O- n& Y/ d
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
3 H& h$ m! ^3 Z4 [ Q: Y6 N" [1 V4 j5 f3 B
. t) p: a8 w1 F. j& h3 M* O0 |' l. s, _' B- G+ J% x
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ( _1 ~9 o+ E7 Z! u7 |) v
9 Q* A1 d) Y3 W u
9 E4 X5 j! c% m5 [ $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
* v* q& {- J4 m6 A4 {" M 5 H/ r% r- l5 R( T
, [1 ?! y: o7 ~1 i
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:( I$ T, M8 q) J+ _0 k9 }! v
% e5 @1 g: p2 Y6 n# U4 r' Z, R/ O Z7 e: N. C6 t! }
. J! s0 m; a2 ?' h$ |9 j- z! L
; x* j3 U5 j- j. \3 _' l. t2 d7 Y9 N
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
3 D. N- u& ]% U# e( _' v0 w* ] 6 X% a8 M( s; G) T6 [9 y. h V* ]% {3 B
& U3 K' D' ]; b- Q. B2 I- e7 @, o7 J <?phpclass upload {! R- p+ g3 J0 [* h; Q
4 B( p; T8 B% q/ h
/ U4 `' S2 b+ x function save() {
/ i \$ V2 X" ^# E* i7 g4 |! B/ Z
/ i7 a! g( g$ Y0 w5 \/ l' K6 d! N; S7 ?+ ]
include load('include.lang');2 s6 \; E* k! C+ J5 z
1 P$ y/ l* I: t/ v0 o+ }& x
6 q4 T; J% e j/ p) t6 B
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');1 D1 [( M$ I2 {: ^$ o/ h1 {
& S$ ^ p$ c3 k, ~/ f" Y) g7 e4 S
! {0 }; [# I$ l" ]
" R7 s- }4 h0 d2 P& g
, m, ^3 x+ G q, `1 s' ]$ e
# i/ e0 U/ \3 F N" d" i; y4 @ if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');, E4 v. V. _: C( J
- e) ~( K/ e- A' d; q4 P! }: t% f( D. x P) S* }$ i
+ K9 ]* I/ U$ S$ x9 E
- y7 N }5 X4 H' V. Y2 j
g' g/ A6 f- A4 S& F if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);' K3 x0 s' b, v
5 Z+ A/ F( N4 M' `, T5 g& W V8 w
$ {% H: w& O$ c
4 v% ?# J$ g* X! g9 {% x1 p
2 W7 d4 V. t. K& ?+ H7 g" x8 X# ^ $this->set_savepath($this->savepath);: Y/ [: A- O" s. R7 F" M. g
' O6 n9 ^7 Q# H6 @4 K( Y
0 m: E4 e$ O( g" t
$this->set_savename($this->savename);
: m/ ~" ^) z7 y% U: t$ o8 w
- O" `: T* U k v! J+ V- o3 I9 ^" c4 ]' q
9 E' X4 K3 X' F( s * ^: q; N- e4 @% c/ F
% L" x5 Q; @3 I l! {
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
1 A9 n0 U+ x7 h) x' [7 o$ ]/ s; o 2 S7 L' S1 V4 l% u x: i
: m# Z4 C1 i; U0 J2 o if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);0 L7 O6 ^% {/ R* p9 u/ c9 v
' I5 P- l h. i- C# k# z' T* N# b/ T& V r( Y8 ^
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
6 ^( E8 K" y" W- D0 T " ?. `/ X0 ]9 l/ D6 L8 c
3 r- H9 ]& B8 p+ B - T# D* Q+ M0 C, E! F2 D
7 N$ l7 o6 V4 q
" P' A0 f) F; f8 o $this->image = $this->is_image();1 N) r1 r4 W% H7 B3 H
4 i# n5 N/ T$ |9 b, d
- M! S# L5 P J8 K$ k+ u if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
, A6 J9 a+ Y& Q
! e `3 r! o9 B5 P6 H) a% i
" y# N# g" o1 Y1 X" ? return true;
( Q- v \7 I) b* s# {( z : a( K6 }% P/ I8 A8 \
2 M, P: C; A% V% n( L5 V }}
# \& B7 M+ b" f, f8 g! j
/ d) u) Q2 m$ O: C
% ^+ R h" F) G$ E3 l2 d0 t 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
) o7 I( d" K2 u& w
: D' ^% c; T, V- v/ Q: s
) c* [& R- ^1 P8 V9 L7 I <?php, n! f% x4 H2 M
( h i( K$ |9 k% i! c$ v1 g& i6 d. [% x. c+ {6 l
function is_allow() {- O- @0 K5 ~# E( D$ V2 k
1 n% L# n' A5 g) N9 G5 x+ o+ }1 d0 R7 b
if(!$this->fileformat) return false;
$ U2 b3 j; A0 T" q" y2 g " K `& ]; t" w& h3 n; f* B
7 ?; F/ L: s5 l M4 H1 u if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;2 J% m" O6 a: w) i0 k
6 G- l- X6 x4 L: w
& b% n) S4 p, G; |* w. r
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;
/ z& F% v# a: w! M6 i6 o1 x $ v5 g& ^" o+ A' m% m) X, i& D# L
( n: C, M+ f% v" F! v# V return true;
6 h+ `) Z' n$ A: L% a3 t
+ `8 ~! u& q- |7 n) D& Y$ b, [4 e/ d5 T8 [; k& O% _* H+ H
}
, u! I1 t$ V1 z6 ] [% p & {2 F2 J8 p2 j( E5 _3 t8 v1 j1 ?& r
# E+ D8 v/ _5 k, r* p# T& e
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
/ l& D, T' T; H. B0 b; P / f( e7 X2 P3 L6 K+ i% h
; i: F8 o5 t" b: Z; N X4 F
接着会进行真正的保存。通过$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文件。
+ t7 |1 X+ v- f' ]
1 `* H7 X( Q' |4 |& Q
$ V5 G6 X' t+ L 漏洞利用
' k( T# E- } ]# [1 F
% B* l: k, g' f9 r
6 F6 g# u% e' ] 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。9 X5 U( B: C' o( x
0 y% v6 ~$ Z. U. K7 r+ P# n2 M* u2 A) l
- j n) o1 P, G$ h3 \' ]( T 5 G" @) d, P8 j# g% Z8 S8 ?
! K# {# u1 W" T0 U& B- I; J
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid3 ^! t9 C( W; M) v
- t2 J9 U3 k1 U# Y' \' O2 u
$ m4 [- o( S, ~# N0 p9 C 不过实际利用上会有一定的限制。( X* y9 L. K2 a4 H" j$ H2 ~
% F# | d7 w# Y
3 |: ~ Z- h6 U& P+ \# J 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
; e' v0 v) w! r! Q8 u3 T% c
( P* a6 a- t$ C% f! j4 f7 s6 Q$ J( Y# r- j+ h4 U2 l
: d! d1 s# Q: Q$ A; i; m/ V
' ~0 |: I8 N. @: i
( n8 X. N8 ^3 E H 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:; t1 x3 j2 {- S- f) L, p. S
/ m: z1 m _8 Q( k& R
& T( g2 U: s& O* f1 n 省略...$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]);省略...
1 Q+ J1 i' m; G# P1 M - s, R/ U$ G6 e- b! r$ @( F. W0 t
. a2 ?4 J5 p$ T7 I" D 因此要利用成功就需要条件竞争了。
/ p9 X8 }, F J3 R $ b6 \, S4 {, I) \0 |, O8 v; R
2 d6 ]1 h7 p( J* K+ ]6 F
补丁分析3 D- }% p& C3 a% B3 {
- a- V1 v% ^: d7 j, X6 }' I
) R1 U+ Q% o& J, J: ]
6 ? A3 u h, v' Y6 z# \7 d! P
( V: |8 G9 i8 m/ M# {/ A* U
& N% V/ n( k: _" U: V 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
4 ~! q# N6 I1 S' R$ m * B5 ^" I' X& n9 a
9 `& a7 J0 L; d% p5 X' O
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}! {4 X. P1 V, d9 K& F: |/ n
$ b3 @! c+ z1 Q. F! x ?
+ }5 ?" [) T& C6 T: U
2 S1 E/ q* d! c. b, d3 U; d/ ~ % ?: U5 A1 o. k; h4 a/ ]
" r3 B o. b" Y; j0 G/ c, W- ] 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
: O; F/ p7 Z* x: x4 k F: y3 u. h
9 Y+ U: k5 L. H+ _3 H' `/ `( L. j" e t8 R) k
在is_allow()中增加对$this->savename的二次检查。
$ ?: m% X. [# w9 s+ J- {
0 z' {* `+ t$ v2 h0 Z0 g; [% t, z
1 ? Y' a9 ?2 e; c y* e% l 最后
( H: h" Q5 K8 y
( G7 A( g4 @8 M
% K& {" U1 r a! d( R 嘛,祝各位大师傅中秋快乐!2 k2 N+ \- c5 M" U% @9 Q
) Y& U7 N0 ^0 Q# D5 x: @2 w
3 W6 ^ |: Q J! V8 a6 H
" S8 [0 i, A" e% F5 o9 A; Z ) O0 j8 c' \% @; x
|