: I' d+ H a! b$ ]' u% J! I
4 l8 A# j4 d9 W' W9 S
$ e* v" b7 i9 E' X3 N3 |. s: o
# M" Q; q$ @ ^4 J6 { 前言' l5 g: Q* _& X2 K7 Q; y9 W+ k/ [
, U& j6 `2 z" c3 j; v. a C
' Z4 m6 B5 P$ x
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。6 L/ g& r) ?! e: Y1 n+ `* `
$ I* x1 T" {, s% p4 Y; ^' q+ U+ F) f- C2 J0 F* Q; l, [
; u, C) C2 b( Y9 v2 W. a {# [+ k9 H2 W/ J/ |* \8 U
! c5 i2 n* X& f
漏洞分析 K6 l" N/ U6 e
# K! N- P7 Q4 D" D/ ?
9 ^, B% k3 v2 a1 Q 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
4 ^% L0 m& x$ j: Q H4 {6 h 5 G. r' s1 N- y& e/ [
4 J0 w! p+ v# ~0 N" j
2 G# {7 X. c, S2 {- ? g
! g1 L) S5 ^2 g) y8 O9 N/ z0 M ^; \! G- q- ]5 R& L$ U4 h, d
对应着avatar.inc.php代码如下:7 Y; K" U: i; `- P: f5 I, R7 @
: g- }/ z: p- E* _/ F( j. P/ D
|! b9 B8 \8 J I! L$ _& | <?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) {
$ S2 G+ T( T! m% i9 Z
6 O v4 U: G/ g) c! Y$ v+ K
9 o( o- L( p, N+ P1 i( |% {) y, K case 'upload':
3 I8 Y' G3 `. h6 Q( T) Y & h5 }" {5 V4 z! R/ ?: e
6 U; y; u" J% p! ]& E j
if(!$_FILES['file']['size']) {% ^ H, o( C$ k, G8 N9 S' f
& J. f7 u$ v8 l8 O# ]+ k5 c& E, T/ v/ Q
) G) y' r( T6 s: C0 z' I4 A3 x+ ^
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);0 [! B+ i* L. O1 S
8 |# l; E& G5 T! O- w) C& n
$ q# s8 q( B! Z- \4 L( N
exit('{"error":1,"message":"Error FILE"}');
" H9 Y& }4 E! Z0 T 3 k8 a" c. @8 x" d, U
, v) n t- R8 e
}
1 I) E, x$ f7 h b
, y4 z& G& z' W* Y8 u$ [. Y7 M0 R# L5 i2 U g( n
require DT_ROOT.'/include/upload.class.php';
& Q, v# J# }" r. A5 j4 ^. o
d# c% V# f, I# t' o, c8 c# z% @. e* V4 t% j1 E1 q" Z
* W* x% I, a: J. l5 N! f4 i
) B" n4 p: s1 k! m% V8 f7 O& O0 E& C! {$ R: W
$ext = file_ext($_FILES['file']['name']);' a1 V+ P. K- e" ~- V
: i0 _ J \) k* Q# R* m' N
6 D9 c7 A, n+ ]/ [! \) g, ~" t
$name = 'avatar'.$_userid.'.'.$ext;
. K/ I! x# x- E/ h6 x6 o" h
7 j+ U& g0 C9 Z1 z; Y- j3 `
' T5 d$ W. }$ n4 H; q! J $file = DT_ROOT.'/file/temp/'.$name;
w' ]8 E4 {$ D* x' `3 A : d. P$ A: [$ n9 E3 m2 {
, X9 o* m6 \9 l0 W% l* |
7 `4 W" K' V% @) G. ~
7 W. ?; ?0 Z: H$ h+ t' i" h! o% S# x# ^+ y
if(is_file($file)) file_del($file);3 Z. [; B0 C, g9 M+ V' G
( \: J* O4 B. j. E7 p& J! h0 u' s! S4 C1 o) Q
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');7 P: c% s+ P* M- e
; `$ L+ H2 e8 a1 C T* Y
4 N) ]) l' P% ?' w; Y' C8 X2 h
, H, _7 S) { o! c6 N' Q3 w
3 @3 m) M! m3 q" Y9 A
& Z, A% i- A5 c& g2 Z7 S $upload->adduserid = false;
$ Z; B4 g) u3 i( g6 t 4 s r! f7 `4 ?
/ P$ g X8 e7 W9 j( Q# C
' @" C* F" {0 w+ N, V; @' q
# J+ R) S4 B3 M6 y0 C, S
( t) I" U$ } i$ f# l" _ if($upload->save()) {
$ p+ x$ V, a9 H. `6 n ' f2 P" _# ~% \& t ]( o. G0 d
( i C `% {# D. x, y' | ...
2 B1 t: Z7 y5 p+ B+ E5 }. C
* u! E+ a* c: s& _, p5 {1 [6 M# X: |. w# W; A4 _7 c
} else {
2 y9 M' K2 W R. u! ?1 ^/ G6 X7 Y2 i # T2 ^9 n( I: X+ @' c
$ R1 C$ c @! y9 m& M) q ...& g5 T2 S+ ~6 x! f( o0 H
& E2 P5 ^% f5 S. \9 f- l4 {
6 e, m7 @( J1 Y) W }( A2 r+ @9 v& b3 K
' L: l# F8 U' s# e* x
& v8 d3 r% u; g0 B/ `
break;
! J( c- W- \& T4 I& J( _ D. D9 C0 y+ A7 M g3 n
, {' v8 T/ N& g; _
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
. q, d3 f6 H0 {& U6 u4 b
1 O9 O+ \7 F4 M9 ^! M/ X' f) `9 Z
upload对象构造函数如下,include/upload.class.php:25:
5 k( }( R- N( f3 b. i5 K/ S 4 X6 P- G- M$ z5 W" N; F
~4 l0 R3 C3 t* U- H, b: ?, I3 J <?phpclass upload {. M$ C) x/ y3 m! N1 b( N
4 S6 b% g5 \& {, F# q; `) l& a! ] j5 w+ |9 h) i
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
# A% d" b/ \1 x4 l# ~8 \ $ A4 o: N! X# @7 |. V8 P, ?3 ^! |+ `
& m- d( ?) n6 o) a) j" I% j
global $DT, $_userid;
0 a, q& i5 P$ H! T: P 1 F. v+ P6 ~( y' O
- G+ W4 O9 t! y0 [9 l: `
foreach($_file as $file) {5 |6 `1 e0 ?% t! w3 ~
/ t) E0 p- h2 v; `& B6 g5 c3 G$ x& `6 O8 U4 [
$this->file = $file['tmp_name'];
& @ S6 S2 L; a( V % a/ W% V: N1 M/ J9 A' }+ R/ y3 Y
H- }$ ?2 C" \; C+ ?7 G: l
$this->file_name = $file['name'];
, _6 K8 ], E( c( e
: b. ]2 E4 M& f6 F+ o' q' H6 {9 [0 @( \0 u F" M; |. }) ~6 L
$this->file_size = $file['size'];
1 |( A9 p3 s1 w* K
$ y% f9 @5 c/ t7 p( ~$ j( K H0 `+ i! Q* E
$this->file_type = $file['type'];3 h6 ?9 z; X7 j
% [) S# }5 X) |2 J; d7 l/ [3 `: u U6 Z2 b8 ~2 r
$this->file_error = $file['error'];
' d$ h' }3 z3 U7 Q( U$ @
! N2 z. n, F( [! T2 ?& o- x% ^9 [! }7 _1 D4 E
4 K) A0 z; ?1 ~% Y2 p5 u
( l. z# X9 b! f6 x V* O
4 q& h5 g7 n/ s: ~4 X& r }
{8 x; W/ `+ C% w" c/ ?
3 Z4 Y& u) I7 I& `% |
7 H6 |: W" d/ | $this->userid = $_userid;
) v, N$ ?- ?4 n& p) G ) V }6 ]* t7 n0 I
5 u- G5 O# Y- k% n+ |$ ^, p3 C/ B $this->ext = file_ext($this->file_name);
% O" G: `' |3 G. C
- x! M. x. ?( D; a! n1 j3 I2 V' Q7 ~, H/ I: D* E
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
2 l* j+ b) O* h! N& g' V& w O : L5 I3 Y6 I' [$ I
$ }: c9 u- N6 ?5 q, ] J- `
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;& g0 ~" e$ _5 Q
( u9 e+ [# _' }' X" a! F. H" }
1 i: N- `2 n) r! K7 o
$this->savepath = $savepath;% f* s' |5 y- v% C+ j: ^# \( W. K
5 B* m# P( }+ x1 j0 Y
5 i1 C7 H3 w9 S# F: H
$this->savename = $savename;1 E/ D0 U5 N [" s0 C
}- d4 Y# x2 z, I; a/ m2 U
& Q- `2 Q- h: k0 r1 P; K/ f0 C }}
5 a0 j) [ b$ w$ s( X) u 4 _7 a) w7 P; O$ A/ w
$ ~$ M+ E. K) ~2 I6 R$ N
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
6 U& }5 j/ s" B! ]" O; ^& K% A ; l+ I, `1 M, [' A9 j
" _% i# J& t$ y# L1 M5 H; P2 W 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 : K$ D, v* Z, k `7 q2 c
; q( }- d/ D( C/ c
7 P5 E; y) Z1 R$ p$ g- W- Z! ]# {* } $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" u* D! o2 H% q" ^
# K" X- Q* w2 @& y) |# w# T8 E* a, P: u3 ]: u9 \$ T4 g4 K- G" ]
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
1 F6 \2 ^& b7 y: _1 P
: a. Q! J* A, w+ G% K6 j* G5 P
' T& @' w' [, o) Z1 a
* ?; T' Q- Z3 F6 U* A1 h - D2 X0 s6 z4 {
. q" a) T7 q: O. M0 [2 F* p/ l+ e 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
& I) E) i0 t. Z1 o! z( e: G- V3 J e' S1 c0 O1 H" p- c
' c2 b/ l: ^* ~7 O1 }
<?phpclass upload {* c+ q. I4 y* i! D7 G% U
4 V- t1 o9 b! D9 S4 p" s8 ^( Y$ Z }8 V5 N$ Y
function save() {2 f+ D* C- n* q2 {* i: M% Z. e' U
5 c: J9 }# x k) ^# b, F) p {
+ |3 l+ F' T D8 h( t2 ^
include load('include.lang');
) @! ]# C' d: z- r* C6 b, \0 o
3 N" Y. S1 ^2 K/ [# x1 e. }3 A9 @5 ]" d1 h3 C8 h
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
1 G4 s' s$ c4 l 5 f6 N. v4 X( x- k7 g
c) A6 C, y5 V$ T' v1 s! [1 _
' j) R; r, N; E5 w, O9 p
; Y/ m m# L# L9 b4 a: V9 ^' u% x. D& r
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');4 a' h* n! o6 A
: w' z! x5 @( C" ?) M, `; i! J( q) h! X
( ]3 |, ~) N% `, Q. L9 g' P2 R
5 Y9 k8 D2 @. E5 U1 u! y9 ^! ^$ H
& f- S$ F9 K4 X2 R0 `: [4 }: L) U% X0 z if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
* x0 p1 x; m: n9 U " w' B/ k; {/ R& a
5 |: J& A$ i3 N2 P2 @5 C! l! Y
* V& _4 a8 ~/ j% }* B7 K0 c6 Y i
. E; X3 @6 o; [% v! s
9 u( ~" X# V1 X: z6 i% H" `8 m $this->set_savepath($this->savepath);
9 h" F1 g# z7 v- x' P- \0 [; H) P
( Z9 C( K9 b5 z# B) w2 P z5 V/ P- J( C9 S/ a+ s" A! a
$this->set_savename($this->savename);
+ t, m- V6 v# U # R6 R7 z' D% O% L- I3 A1 m
; P: X" T# h! O+ G/ q$ p4 G
( J+ r0 o7 D' d: M1 c0 { / i: X* N Y8 d; h: W6 ^3 H" P+ {" k
& g/ ~7 O& x7 j8 U1 c( ^% D
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);+ V; c+ a1 `* Q# O0 f. [* l
+ ?7 ?# i0 B& M& V) _% k& M
3 _3 I1 l% ]6 O" D, c) S6 |) I if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);* {" S) a7 W; d5 K7 T( e
& l) o/ o2 X: a
4 I/ h7 o0 F6 u9 @) o8 J8 l
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
& h. {6 c0 f- ^$ C( g5 P ]5 X) _8 Q) S. p) ]9 v
/ K- x/ N, [7 Y+ J
& ~7 \# Y6 i& E" I4 d ^# D& u
- [, S( K' ]" j1 {6 v6 C( l8 v z- r
3 i! B* p9 j5 W: a% p- i $this->image = $this->is_image();0 k$ g: G/ g r/ H/ u# ?7 h' R8 M
' ]# |8 Y2 d C# B; \8 F
4 n1 C! Q6 X! d0 H9 I if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
: m& L3 |" c, I f( ] 7 ^( t1 |5 h8 [& c" b/ v% A
, ~# `: }) c5 j2 | return true;
6 @0 ~6 R: O6 O6 o 7 H+ u! I: N. R/ X
# G0 D$ Y0 [7 Q: M }}0 T+ Q! }) C" H) c9 U$ T' F$ k: u1 C
. L1 d6 P9 h! }+ f
2 V A1 F0 |& B# n
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
' z+ {$ x; e/ L ( H) F$ w# a- \6 P0 R. F, o( Y
3 |% G2 O: N, v% F0 x8 ?/ W <?php: x" u; R# u U0 i9 h
6 M' [ p" O# h" f' |' i
8 r3 c1 f3 n8 U1 ?8 D7 R6 u function is_allow() {, ?% `4 `9 x' ?
- h- [, R6 R- C3 `- t9 N
! c, b& ^+ _- ]; b2 f3 G- U/ I i2 O if(!$this->fileformat) return false;
1 J- i7 F0 v d2 K1 w" z0 ^
E. X; Z1 B' r$ m `$ B5 l% x2 B" }7 M
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;6 L6 l* v2 M" h) O9 r
! j5 A% ~# f# h( C
: w" y5 k( ~) |+ P 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 Z+ i% w: [) f( k ' J. o% B+ v: ^
- }9 y$ B( ?7 r/ J( Q+ E
return true;, i% T8 w) `' b3 ^3 l8 n8 T
4 o# C. [7 _# ]9 p
. c, H8 W- }, E" }* n }
: l9 ]2 G+ M) s4 ~1 m , @4 J$ @1 v N! E @
. b9 i+ M( `+ x4 \* b) K 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。5 ^/ `; L7 I0 u3 V: ]6 ~
. r7 B) p) A' Q$ S1 e2 |5 r
* Z& f V' Z W3 y 接着会进行真正的保存。通过$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文件。
/ R$ q A6 O3 X8 ~: c/ W& e% h 4 w) [& Q- w" y6 x* a
# d* o: O" l8 H, D) a, S
漏洞利用0 N% i8 r$ Z% `
* M- j _ H' R7 _4 ?2 ~
$ G) Z1 ]. V' @6 M, Q& Z5 }, X 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
7 m5 w9 v& Y* c8 S; f* L3 t
, C# }+ e' I% Y6 U8 }; V4 z, A2 z$ k/ ]1 y4 B9 U
0 j( y0 E! q# L& F I5 J4 } : V! B! G0 r2 l2 } G
6 a4 H) Q5 v4 w) y$ o 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
9 o/ [7 Z- D1 b2 c
0 b( v/ a0 `1 F& B/ {* @+ {- I7 s
5 D5 q% {. q2 `4 d5 a; B' \0 W. N 不过实际利用上会有一定的限制。
( @8 ~6 H. c- e- i& D( b , G- Z1 M' H9 `5 y
0 }9 C. h0 ~# ]4 u' H
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。% ~9 A% z" F' F$ H8 E" i# X- \
$ @# E* _5 q" `7 W1 o+ m& @
' Z6 g' d! E5 w% x/ T
0 h4 w7 ]4 ^7 p# I8 D6 \
/ a' ]! B! s, _8 s" Y: O$ f. H2 h, _: {9 h u9 o
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
- L, k+ U9 e+ j$ [0 a2 m
# N" e( M( H3 B' C. r8 o6 r- ~: \5 ?7 I: W: P" }. E6 [" t' O
省略...$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]);省略...
6 U- |) q% L: b- j" t6 p% [4 \
* |) F6 x: C! O J$ H8 }* b& p) i, }& e6 e3 u# i, C Q
因此要利用成功就需要条件竞争了。
8 z: P/ y, P( f, Q! L8 N- ? # P* p2 ]% W, @; Q. q+ C
! j, c4 f5 [3 S 补丁分析
, W& y [% S7 S. i& i Y, ^/ C3 u9 E b n$ ~1 E) r
/ i+ j5 T) p5 `; b8 O! F4 w
9 t$ W7 t$ [" s9 C9 S) W
1 h% k7 U. \' T% }9 p0 X
4 f( ^; ]$ t; C/ i' N 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
" {7 b) C: j* _
/ n8 b; y' v0 F2 ^% I3 d9 n2 Z$ _9 C2 X4 Q
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
' J2 W1 ]% Y" E, H! Q & O# [. s- f/ Q% g+ g/ K2 N
( y1 M; K0 H h, N; r
+ n+ a' Y8 z5 j; q: ? k2 ]
6 o3 F' u9 ]) Y' Q# G
4 O. v4 D% ?" G4 [& D
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
3 t) A8 }0 D! k4 y
! Y3 i4 z. x; @" x6 L2 h, x+ _% S, I6 u/ @* z% @
在is_allow()中增加对$this->savename的二次检查。& h5 [' Z4 _1 Q: f
6 O% b8 v, H8 k p) D
1 Z( [2 ^: T! B" U# C 最后
8 e- v$ C) {6 J, V: e6 N4 B F
+ b! \) S9 y8 a
! T7 z3 }& X$ y0 Q" ` 嘛,祝各位大师傅中秋快乐!
& u6 q- @5 h/ W: x" z
9 V& n* e5 w r6 c" ]- s' u( }! q: G
" ^+ D& h$ B8 C |9 M& P4 B
: v6 `1 }6 @7 D" f% g* e
|