7 W3 B, u* S5 x! i1 P0 p- p% S5 w1 @% S
3 w( r7 B H4 P- V3 p, w
" J O/ I# g: s2 R7 I ~* b7 ]' l2 O5 ? 前言' `6 g2 u+ D% U9 ~9 u3 ]" v
1 w# T* r+ Z; a2 y5 v; {5 S5 h5 B5 }7 R
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
4 W4 w" ^, N$ B
3 ^" E7 w7 f4 E, v% ~5 Y$ Q
7 R& H/ f0 o# V6 h6 V4 M+ M; R# Q
0 X8 D* p' Q5 b
# z; i! C* M- c% M- R% V6 g9 W4 [) y' Z8 }+ M
漏洞分析: X2 [+ \ c7 A- K! `$ u; y
: F5 \' M2 \) o2 k/ H
. z- f2 [. v1 X8 a5 s
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
& s2 \# h' J" n2 X " Z* _% P" t. ]
: \; f8 t- R! m' P& n1 y, p
, K$ p$ ]/ q5 ~7 J5 B; J
' L" A; R; V% S, K; ?* }
( o8 d+ n+ r5 h# l( `* K 对应着avatar.inc.php代码如下:
' J# k1 X8 p3 B6 K4 F( Q
9 D2 f' m! P! ^1 r
8 i! r- \7 d$ U <?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) {3 v' @) v7 D7 p
: \4 O, Q4 W# m! ]$ S3 q+ i
* L: m9 Y6 Y! N" Q( j2 W2 ]# R
case 'upload':7 U; N/ b" m% Y q" e5 O9 F
, c2 F) _* n( U, ~ G! m
. `: W9 |$ O* `3 a5 D$ j: F3 G) s$ B if(!$_FILES['file']['size']) {
. z5 b, H4 B: @0 F( z
1 ^. Z1 l# y3 o' c, } V( x8 t j6 O% {% |4 s5 E2 a6 i
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);1 v' N n4 V8 v
6 N5 C0 \& O3 X0 ]8 Z0 G' k* h4 n
' P8 p" s: A8 }: y9 g, s
exit('{"error":1,"message":"Error FILE"}');4 F/ b q: _- f) Z# Q
* t" \1 u3 B: E2 E7 S, D7 z# r- b. E8 w" d& s& `4 b3 x" c
}
4 M( O; D0 }* G% X/ A! G; Y
% r8 ^) }" \4 I. e
. n" A% b! |8 [/ X: q' Z L9 ]9 m require DT_ROOT.'/include/upload.class.php';
) L# v, e8 e4 N5 R1 N/ t- H : d- q5 [/ r, @ W, i7 f6 K
0 U4 R8 K# \' Q; x+ o! P! {8 z/ e
! o3 W: A. B* D, |" K- P
3 ~) [7 v% X9 o- }' @
2 A7 w; H. h: w9 V5 Z1 J
$ext = file_ext($_FILES['file']['name']); E6 R# [/ D3 s* s. J" ?4 {
. S; f5 G. I- a3 I6 S9 @' a& N7 v) E# M
$name = 'avatar'.$_userid.'.'.$ext;& I8 N: ~5 I0 Y
( L/ S/ O5 c$ E+ d2 m7 c& B) M
8 O+ O( A0 f: b: m" b+ q* W6 Q1 ?
$file = DT_ROOT.'/file/temp/'.$name;6 S7 z5 k1 s: h
m/ M5 K5 U3 f
, {; a% S& i- s c3 X7 Z$ p
; \' c/ Q" ~! D% j# b
+ f, @' p- p$ {3 R* f a& c5 _/ Q }/ F+ J+ P( o
if(is_file($file)) file_del($file);
+ e2 E" U) {" ]8 F/ r( r0 F# W2 p4 F
! j2 J+ b) U+ A& F* v3 w7 O& M, t. F" P" U" l4 a
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
$ Y: \, X3 ]: n5 A$ b9 W
* w! P* K0 c$ L: R: a: o1 w' p# p
" j: `0 H/ T, b( M @
4 B3 S: R2 I: ^; ]' h4 W$ s
$ x3 n* Q0 F0 {$ }% @( ]! h8 J+ J0 I% @7 W0 |
$upload->adduserid = false;7 g( P/ I- W" ^' j3 d+ ?
, ^) s3 o/ L+ ^
5 t. |& k) l! e. Y! o2 `# a/ Y
% ^( x8 T, O8 @0 F. W! D
* c& K9 o3 p# s$ i2 u! y
+ a' z! u6 G2 b/ n: {
if($upload->save()) {
* c( O* S5 t# T( }# }
' P+ y2 E/ }! ^ g/ f7 F0 z u0 T; \/ G6 q) X: }/ P
...
! [1 l+ E* I$ Q9 m5 u, y8 ]8 G5 X7 y & \/ v8 {6 ^; m1 f
/ ` z \/ ?- g/ L } else {
) ^6 ?! O- @" l- ?& j) j s- B( G
+ w7 L3 x/ C k9 v, ]2 z
6 H0 m# n- w& W8 C& j v' y6 n0 T* T ...
9 o* h7 E" N0 X) a
* Q5 H, k! e9 ?. i* B" v5 |0 G \* b# @7 p7 w q' V
}
! c1 \8 e$ Q& t, q( W7 N6 P, `9 ]+ v 0 ~$ ^3 G8 E- i& e4 G' G) Z0 h5 p
# C. Z1 ]. a1 H& D break;
. d7 c; q& E: E( g 8 O$ H8 Y( e& m- X
2 m4 \7 y! S" d0 Q
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
% f4 L$ H2 K, s+ E8 b
' b! S$ @/ F- }4 T `
+ P4 l; Y8 P% L3 i+ o$ J upload对象构造函数如下,include/upload.class.php:25:
7 Q* H- O' k" P" c, Q* E . P1 p$ V M$ G
3 B) S& _5 g' L <?phpclass upload {6 s7 H. U4 A3 B" X
# u# l+ j) g3 a
9 b6 N! U8 J$ z0 {5 J+ }
function __construct($_file, $savepath, $savename = '', $fileformat = '') {" u" }0 T5 _1 F+ s0 x
4 x/ s3 _% j2 G/ M- e) n4 R
J: N4 Y8 h4 D; A4 D3 m& V$ G" a) S global $DT, $_userid;+ D. L1 Y2 _' q) `% H
: w4 j# K/ b, a t% V" ^& D( T
* P: S. Y: [( |( p0 Z foreach($_file as $file) {' _6 N6 B8 K; I# P/ S q# ?
+ M g' o: ^3 x" \4 k( _5 r
; n4 ]4 ~4 ?+ |$ u ^ f' L
$this->file = $file['tmp_name'];
# i% V/ w8 W7 |. p \
0 t- P4 A. H& T F5 J/ t Q$ p& y, z& @: l5 y7 s
$this->file_name = $file['name'];
1 r# o! G: } e( M
& E! _7 F+ c% c
+ g* a- N. O5 g* M3 ~+ ` $this->file_size = $file['size'];
2 }8 X, q; |/ l# A. X$ e $ J3 a3 \$ Q: }
, r, [8 N3 N1 h- `
$this->file_type = $file['type'];/ r! ^1 Q( T5 }; C5 J9 O9 q$ D
" G' A- ]4 ?, M0 l: X- o+ P6 |
$ H* G) u2 J. `% M4 ]
$this->file_error = $file['error'];' m3 }3 L: Z& y3 e: p3 c+ `
9 ~, R6 N4 d( }7 ~* M- r3 w1 \
5 d7 ]: v( a/ s$ `$ b
" J, j" }# {# ~9 g8 l+ n ! W& [& b t% \6 ?! s1 L
. H0 F7 \& u/ ^. f
}
9 n/ h6 d! L* x: K6 V* g / u: P) S' _+ ]
: @7 i! S$ G/ Q% d4 f- i% m
$this->userid = $_userid;7 h5 B3 ^$ H- T. a0 J& E" j* d0 L
3 w" f. u- e l. R+ z
1 H# [/ ~5 P9 r# V$ F! F' [
$this->ext = file_ext($this->file_name); ~+ u3 M. m8 b I
& u. c& m; l6 y5 w
. s) O, P, @3 ^
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
/ b' ~+ y @/ t/ ^7 O! f# o! G- @ ( Z0 K0 o' m, Q; F" E, [
1 n7 E" y" p2 {6 H* m $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;4 }- `7 D8 X1 L+ W2 s! b8 Y: q
6 z; v, R. P: q( s# V9 t# m: l
) S ~- R F% j& B $this->savepath = $savepath;
% I5 w4 E6 K$ I$ W3 ~6 ]7 R . I' \, G! i7 w; c9 z8 J
" P/ C+ k$ ]8 |: h
$this->savename = $savename;' g. l6 g# V; ^
" i0 P( R$ E- M. b2 D3 @
7 K2 g& e9 U. y% j4 Z4 Q9 l
}}) q; m ]/ L. E4 w7 C
7 D5 c+ h/ O5 X3 p; C, D, N0 o! P# C
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。" p( b0 m( t: ?
/ {- D& v' P- F2 p
0 w; ~) V7 G/ v 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 - F( g4 {: O! F
) d6 J4 z) S1 P* V; W
/ S1 G1 O% k4 D& s
$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
3 |5 t5 z$ x5 V, X2 q 5 _ Y6 ]# H$ T; z+ X- G
; K: V' o* [2 O' x 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
6 p7 ?8 ^0 a: v' [
% E# c3 e7 s/ t4 l# I4 E J# a0 v) E; A1 | l1 A; w
5 h% x5 d: x: @- F, C/ z6 H
' x4 s5 s. n/ H
8 G6 b* S X0 ?0 b$ v4 F 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:8 H+ r' ^* U# v3 K
) [0 d% i, [7 B$ c. \( k, U! z4 [1 p
<?phpclass upload {$ K, g) {' K4 j: ~1 V0 f
* f. m; l) ~( o3 G- \( D: [
3 K4 S" ^# H, j2 } function save() {
, b( t9 _0 G- v
, P& O. |$ c/ \1 v+ a
2 X9 Z& O1 j, d) D! a/ B include load('include.lang');+ s: X: v, y1 ]' i* s
# a! V! t; @4 N6 t
2 j2 R- O+ e+ A1 c" B+ p
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');& m/ U% W& O1 c4 _6 O
- S, W$ R7 K) z" h0 v+ F! K" n+ m
% v T/ F) F3 b# Z3 D
: D2 ]' m/ d9 M6 D & Z9 |. U4 u1 J5 @
$ B0 j2 H7 W& j3 M5 ^ if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');( J5 F+ C8 x/ p0 j
5 A. ~5 f H- L+ L
" z+ v @) L# U# i7 \
0 b% j" s4 {2 I1 ~4 [9 D5 t & {6 `' k- A( J
/ u7 s; O7 |7 c" o0 b( J
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);+ ]! ?' I& R. k. A# _8 S5 w
/ H% @/ v: M3 B
% x2 i# D+ u' A% ~" \ $ l2 `3 G* H, f. @' k& X0 q
6 ?* w' [6 F7 V4 b! D, m
- `, `* a* U% Y9 y. P. u: r3 Y $this->set_savepath($this->savepath);5 y( X+ C F* e r) x3 I
7 w: `& K k" C
8 h9 }$ }. ^) Z- o) I' O. d4 P $this->set_savename($this->savename);) U6 W& r. Z x- |. W
, v2 ~6 j# `) F# _- M
0 G' e/ \2 l, @* H8 V, l1 q* r
$ B1 F+ M* e% x& \
- Z! S- u3 P- w
) v i( Z, D9 f# S. J# p if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);6 m$ U8 u; b$ a W, @5 f
# r: m) l3 G' g( X# p/ i4 ]: s" Y$ Z4 s" }
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);% X) }1 U* I, ~
9 M( d$ p8 @+ o
! \- ^) L6 Z3 u4 T- g. D. f
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
F- h) X! R A/ w5 y " ]4 z9 z' z) W9 Q% x6 _
9 r3 q# `& f3 M7 j6 r. E8 B+ m% M6 \$ @
5 ]! r# _2 {3 ]+ B4 |, f : i5 Q2 o' t1 p
5 R( o, p2 K8 E, s {
$this->image = $this->is_image();/ B/ V o. V$ Q
2 w% A/ _2 ]! b2 b7 ]0 D X
7 X+ i, u, P3 n5 M1 |$ _1 ` L: G if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
9 p( m$ u& A/ C# m7 } & l/ m7 _* Y( C: w8 O
5 b4 L/ k. e+ X7 _8 r# t
return true;0 I& }2 f8 q3 y! W) ~' q7 X, o
* s% H# d# t7 r8 F2 U4 Q) z
; Z' V( B5 \& ~" `- d2 @ }}% r: F8 Y" Z( l1 x5 j
: k9 S+ g- k* q) e4 Q
7 ~7 G9 S% O6 m$ S+ p/ U
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
L5 X) A7 J$ ~. H. F' x% V
" e y% o0 D3 @4 W' O$ _7 x9 I6 ?8 ~. ]4 w7 c L% b% y2 N
<?php
y' a. X. X' t
' B; [6 |% l2 U3 G2 p `6 ~6 v4 c, _$ }
function is_allow() {
; D( y N: c# G2 W 7 O. o( @- O% W/ B
" r# h% M8 r3 z/ b" D if(!$this->fileformat) return false;5 \: w7 b4 U; f8 w- Y( X/ b1 w
9 f$ U6 `# m3 ^5 F% _
3 i4 e" y3 l7 P. x, s0 i# Z if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
% k1 e9 a, h. t3 ]) s / ~% s# n+ K5 _) g5 H7 Y2 k
. Z* Q! G8 p! e$ R. L2 ? 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;8 m( U% o% W! _! P8 A% |; ]" ?
6 g$ |3 O( _* \( O% S- X3 ]' ]7 ^% `4 B! M ]4 i, |0 P8 q
return true;
' U3 p6 ~% W& c9 _8 r# H ( x$ i3 U; q6 D2 y: x2 [6 d" t
, g) R" T$ V6 f5 W K5 N4 ~; j }
6 {" B* w9 F! V! E( V# J: P
7 a1 o; W: q5 B$ _& |5 Q4 R4 S% L" ~* j. }
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。6 Y2 X$ P" a4 N" H# |; ^' m5 a/ S
. X$ b$ H6 @# a# @& D# }' }
0 o& G. G! b% Q) c 接着会进行真正的保存。通过$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文件。
1 g4 n y1 |0 H- \ % d. S4 v5 b) t% v4 k( b
( o* u3 v) t( q. c( W 漏洞利用 R* q. g) {9 }' H/ o+ v; v
& H+ v' @6 Y; K" e8 G8 _7 m+ t: j$ O, w* F% m0 q/ [5 ?
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
, Q+ A: V. n; z/ \8 s5 O+ \
" K: f6 `" T9 x
/ F) y# A5 b' N, u
9 I- p+ I% s" d; L 5 J& K) R. j# T7 C
" j& |: z+ n1 m9 `- O; g
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
* D- Q, b* ~5 V: L0 k, I! n 7 D3 d G' ]/ p2 O
" q t" m! d* R- |8 M2 F% S 不过实际利用上会有一定的限制。
5 v% p5 q9 o! b2 K K
/ J1 T& K4 `6 E, N
! M5 X2 f- `! ]' l 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
) z; e5 ` P$ Z; [9 v3 H
# ~+ ~0 H4 v& e# c l3 C9 F7 q* g% e" f! ~$ } E
# s* X" ^9 q7 r3 K# Z
9 A. d+ a9 c) P$ z2 l) F: d
+ o% n$ b$ e+ t, {+ n. Z. Q 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
4 w8 A$ F, ^- Q3 b $ ?3 i- [) B3 b1 ?
0 O3 J$ H* b2 q* J0 x; _
省略...$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 F6 t9 W$ E' d5 S6 D5 ` # R9 w' u: l! z& q
/ {# V; q) S; d# ] r7 r; u2 e
因此要利用成功就需要条件竞争了。! s5 `1 V% i4 b
; L8 K6 f- `. y
$ C6 [8 v4 p2 S0 o7 ? 补丁分析3 a& a. P1 U, ?: P( l9 M
4 a- @; ]/ `# z2 J3 s8 `
, F1 @( J8 p, B/ ~6 _/ L/ I2 F! D , {% K K1 i" Y- F6 }: V
% ?! \2 x5 E% k7 R4 }; P: n' V* o4 U& H5 s
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:4 d4 E* N* R s+ H
$ ^9 l y$ a, Y5 G+ t8 x& p. P. q( I6 O) H+ c* G
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}* |5 y7 J q; a ^6 r5 V
+ H( i! H3 `( d! E+ Z1 s0 f N! M+ {, m3 b* L) w- T `4 G+ b
# m) u5 z3 P$ U. Y
1 y1 R1 o4 X* p' g1 o( ^
K" @0 K1 P" n! \ 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。" m9 r0 W+ \. O6 m/ ]' @9 v" i
7 @/ h- a& ?9 n5 [' ^
! m9 K, E- A( t' u) @8 H* | 在is_allow()中增加对$this->savename的二次检查。# K" a, T; @1 [5 S) V( Q, R# T; [' S
5 B0 s3 w+ D6 F$ ?
! S$ Q* K& `* t& E6 }- J% v 最后* x& o$ {" e- H/ E( s$ b) V
$ [, L& E" K) O9 ~) O$ @) t7 [, h& I- D7 M
嘛,祝各位大师傅中秋快乐!
. I$ m% P8 e! J; i% E
/ x* R* g) |# ~$ P* l6 ?7 v; f8 F
/ R/ d! w' ]8 K1 N
# y2 R. r' J/ ?5 I, Q+ [ ( s$ a' V2 f7 O& \) i8 _
|