, y' E3 l9 i( R. _( N
' O5 P5 [+ ], ^3 p: x+ u5 N" A% o7 [9 e* O
; g: ^! u- f' z( ]7 j
前言
2 r- t# v& v7 A1 }
7 b9 R" v+ R% y
. q( b9 I" r \8 w+ k5 \2 W: {# A 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
; f# S9 y+ R$ S2 P$ l n. F , ?6 b! q* l5 s- P
8 }' J: z6 ~, X. c5 x, n8 t) [
' @* b! S9 n" q$ |1 i) v( C# i# L
! y" q$ j3 s: q1 R) j: Y( j+ r+ G# V, l# c; {
漏洞分析
% i6 t/ p" z+ d1 f! W. K% E: }9 H5 f2 H
+ x8 t! Y) `. l2 X) s+ w1 c9 } 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:. ~' T. G5 b8 a8 L
) Z- r X- B- ^
4 b. y8 W: C2 P5 x5 _. B* G ' A4 i0 s$ p) u) E: u
' @3 E8 G j. F
* B# p& f. F2 B9 R
对应着avatar.inc.php代码如下:
* A5 f" M! W' s1 G; t
M) @" ~0 C2 e- I: P
( J% m. }6 {, k( {3 o: h <?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) {! @$ E( H( B8 x7 |: K" x2 r: P
2 J& g; J8 [- \
) W. O; K9 q+ s5 |5 I
case 'upload':( B' n* w3 j( B$ p5 ^6 i8 {4 [& H
5 H8 l7 l# g+ V$ E& W* ~
+ D+ [5 }$ u' J if(!$_FILES['file']['size']) {
/ M6 f5 t5 i2 P$ \* n0 S) U, U- C
' [5 Z* L2 w9 y0 F
7 F; g( s6 X q5 O3 U9 s4 ^3 d" n9 v if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
1 r! m3 B: l8 i 9 l. Q1 z9 o- m1 g8 h! M6 e
% k5 T0 d# N8 a+ ^ exit('{"error":1,"message":"Error FILE"}');2 y* O" w* i) }) S- G0 w
! _* `- D& `# H' c& @8 _
8 ~1 ]5 t. x8 ~; W' H- x5 v
}' m) g( D: X9 s5 {; A! V( k8 v
; V! S1 h- a7 Q7 z6 I# {
3 b% O( ~) |7 W2 e o; c6 v# h$ q
require DT_ROOT.'/include/upload.class.php';
3 W" U+ F/ O i. P 9 V: b* p9 N8 K/ B6 l
! P) p3 x. f% }4 X1 Y* x/ P
5 H H7 p6 [" V6 b* |6 e" ~
* U& H, U% ?. O- h" Z8 O
, |+ j5 t2 U4 j K; a. z# K& k $ext = file_ext($_FILES['file']['name']);
: y. a7 t t: ~. Q/ x: |, k& w# o
i7 N- L6 E1 J- f1 U2 n
) ]# _8 @6 C5 W6 |# Q& M $name = 'avatar'.$_userid.'.'.$ext;% h/ S0 |* c, s3 r
; c( o, x9 _& k( k3 p; ]3 A+ N5 p& R6 [5 o" |
$file = DT_ROOT.'/file/temp/'.$name;# s: I7 O8 |/ G p/ A) Y
/ l9 q Y* L. q/ e
6 }$ w% V+ }* w9 C 3 M4 a9 n' ?* `% _' b* W
& D; b4 g$ ^ R D* s
: B, {: U8 p1 b3 o1 L+ E& \ if(is_file($file)) file_del($file);
9 f$ ?2 S+ @4 U* b 2 J) Q M! H# I3 N; K& O
" w* P$ E- H$ P1 s
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
# v, P# j" p& b2 q2 g9 T6 N 3 ~- e1 {3 x/ |) Z* h7 D7 H
6 ]5 W+ v) ^, ^% ]
7 u; @1 \$ o2 o r. \9 V
# @& U3 a$ H j. E5 h8 ^; d, I
& X- [0 [; H' R( x5 L
$upload->adduserid = false;
& p% M: N# v- F/ g" t
) ^; N |5 N0 A# t" W
K$ G w \: w0 A. t; | ! M3 v8 x0 ~- a$ Q' [1 s/ h0 I+ i
7 _6 A! I* r8 f: k* S
2 c, ~2 N! J, P8 r! u* ?$ d# k
if($upload->save()) {2 Q& l1 r4 h1 i# y: O, N- q o7 x
/ ^; b% x% L$ x+ X/ _
( G+ ]- N7 t+ w* `- m ...' Y8 G/ K( I" s" _( i
* H$ U9 p$ r% d" Y' W4 g9 m$ x
+ `3 d" T$ M: {) n1 O& H. x
} else {
! {6 i8 z' Y/ a" ]6 t! x0 Q ! G1 ~; e' D5 W$ G2 X# E& T
4 K' D2 Z+ l6 }8 V# d7 ^2 }: d& J
..." h7 ~8 @5 }* U- J4 i1 U" [
: M: d# }2 Z4 L7 a7 f' }% _6 g
+ y; G- R& y. S8 q }* N4 i+ O7 b. C* T1 b
/ t. ^0 T5 f8 G" c( _5 ]. u
m9 b, F+ J, q6 z- f break;3 l) `& U/ @, r1 M
6 X1 o7 r5 ]$ g- t8 U9 _; @! H9 X3 }1 B5 B4 O5 h
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
4 y! g9 g- _$ d }
8 X5 G& T4 o7 F0 B
4 ?- O7 b- @* z" \& V6 f9 r h upload对象构造函数如下,include/upload.class.php:25:
0 B, ?& D& t% h( y& R
! m9 u4 f- p% Y3 ~! _9 {* }' _3 Q* j4 {2 m3 R( Y
<?phpclass upload {$ N4 o# d* x5 a# M0 w0 o, d0 N
; @- S. Z$ A* s) ~
* v1 n8 h7 r7 ~) D. X$ O
function __construct($_file, $savepath, $savename = '', $fileformat = '') {9 Y2 s, W, L: V% V7 J% m5 v5 j
( s5 A* Y X. T+ G
$ o+ d) @: r6 K: V5 A8 U global $DT, $_userid;
8 R: J/ z) U4 ^9 k* B9 G
( Q" w( W( r" s6 ]5 m5 r6 \& u' ~* \% v# l
foreach($_file as $file) { E( m+ C% f: G6 Z5 ?6 M+ u
: Y* q* z) { u' u! @. q: J [- X+ k' `- B- A
$this->file = $file['tmp_name'];+ r% [& t3 Q( ~+ F& W% I- O1 I' G
: g( R8 s1 s4 `" j* R8 j) C, l0 S% c
$this->file_name = $file['name'];& j) f0 S$ J' X& o/ ]
1 _8 [% S% K2 d9 _+ Z
' q3 M s/ u- @" I4 h, B
$this->file_size = $file['size'];
2 t5 f! `2 B) A( _0 `8 ` $ C, j$ G5 K; n/ O- s$ H4 s, H: j. ^% u
& u/ y( W0 ^' u' E5 c
$this->file_type = $file['type'];4 K" B, B2 C- V
( P2 j, Y% S7 y: \
0 `3 ?6 K- u' n" Y2 L6 U $this->file_error = $file['error'];' d% [1 z' @/ n1 F8 E+ Y" k3 m; B
/ H" g5 m0 w# ^9 u7 F7 E. e
& i- G( F1 G* S+ K* t) X+ x, t
3 G8 U9 V, b1 V8 ~
( p# Z+ _8 A, d* K& R
% C) c' r: v$ `! g6 z5 p }
; m+ h) W- e! p' E: X! f . W+ E" I, V7 ~2 c ^: x/ U6 [
. _5 J, f/ m& Y( f8 V- n) g
$this->userid = $_userid;
7 R9 f; S7 l3 t2 Z5 M9 f9 A/ E* @ ! \" \( @, X3 ^5 U o a! v! ^8 s- a
0 ~' ^, ^3 c7 x/ ?5 \ $this->ext = file_ext($this->file_name);
# Y k5 [8 K4 L2 n# n$ s8 \' \
" @' w7 ^0 Y- w$ Y" n$ w4 `+ X) ^/ S% J9 v) O
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];# ?; U, l" s' y! j2 O
: v5 s* b! v! M* E3 S$ ~) l$ J- f7 Z: {9 j* Z5 v
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
8 A& [5 P6 ]) t: h" X - y' m# W, |+ _7 _% W: i+ L% T5 P
8 y' w; q" g' Q- A' Y$ t
$this->savepath = $savepath;
4 `; K) k0 m) G % }, s* N. r- ^* f
2 G+ v5 ^# D3 I+ l- E, |
$this->savename = $savename;
' e; q3 e' v* H% _
# [& P z1 A' v: u8 @2 d2 h* C/ e8 v5 \7 }( _- S
}}- V5 R% x d1 c+ ^6 E& B
: Y% @% P8 P" _$ f9 U0 Q5 \" c% }3 b% v) N
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
# j7 ^* g$ k) P5 j- L/ D" _ 5 p0 |6 A4 C9 V2 U, i) f
5 [9 ^3 F9 ?. Z" w- j3 B2 C% @) x
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ' d H$ I' p: T% v3 ?: a
3 x @( K" |0 p* x
0 j; m/ p( M# M1 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$ ?0 h( ?- `% n
) Q4 w1 j+ x7 X @* ^# F u( N0 m
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
* H! y1 R! A+ s1 t: E; Z
/ R. ?1 T' x" k! G T
' t7 q! h3 q' F! O! o; y3 w& f8 J8 i
4 b9 _" l8 F0 e& i$ |- U+ w% e2 T 0 W% R, X" ~) k* Q1 M
$ {5 S( G7 q9 y# Q 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
" L1 N6 |9 ?% E5 A A6 t& a/ f
7 G0 U9 O# }6 @3 V$ M+ J6 i6 `$ [3 e& H
<?phpclass upload {
( N& h. I( _0 a0 F( x% }% R$ ]
: S5 c7 v- B2 J/ T, [9 W) @0 [+ n+ m' d' J/ m& P( `
function save() {4 e' A5 z1 Y4 s
; |6 e" J3 z+ m' w& A' U
. m, r6 v, |9 s9 A+ p include load('include.lang');
: k0 y/ l7 q; p ' a3 J7 a6 T% m& P1 L) K7 D
1 [3 l6 _. Q7 ~4 m4 |1 M if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')'); o4 S, Z# m) i0 k
, p& {; j/ ^+ ?4 E
, h& E: _" H+ z+ K" W h' e% ] # `7 _- x9 Y! R& K/ U/ c* L3 A5 j+ Y: c
) f) r3 a: ]* }
! Y+ N, b3 V& M- _& e if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');% z8 A! ~1 [9 j$ g; ` `
# V$ y* i+ F |* J0 i% A( m, |1 S" g7 u
9 r3 R! f+ Y$ c( o6 l
. r& [5 @' m; k9 }$ R6 S1 Z5 l. b R1 q. T2 }
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);+ P8 p8 {9 A9 d" ?" U+ i) _
: }7 Z4 L7 [* v& X' n. H( x g/ E Q$ r7 Y) z: q; p
! x, N" p/ t/ S& c# k, c; h
4 _8 n$ {, y1 `, `! |4 l* G& U
$this->set_savepath($this->savepath);
7 X( N+ d2 o1 f( \
" v( F4 m3 v( i2 v- Z2 K7 J. y9 b- g0 r
$this->set_savename($this->savename);0 Z d6 ~9 C/ K/ J3 p+ j9 c
& }9 R. }! u3 {/ {% ^4 S
) A! Q1 Z, G8 r9 Y4 t+ t1 @
& R' v- {% y$ G) l2 ?: ]& l! f0 y: [ ( h5 I+ L5 j0 L0 z4 v
3 m+ Y% ?1 V% b% A( Y9 |2 Z
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);1 q2 f7 U1 U8 c/ Q% P, X
1 f, X- S: f" u9 M. h* q1 q6 X
! u: u# w% U6 n if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
: I2 c0 ]7 r- J' ]0 p1 @ 6 S d1 ^+ y6 ~! s4 m
/ U8 Z9 A7 o9 [4 D; v& t if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);/ R7 c) N9 B/ y
+ F- X2 Q+ o" v& Z) u" N
6 L' P$ R( ^6 v: p. b1 K
3 K- ~+ _0 T# }
( n6 k) w4 Y! u! p3 `( I9 a+ S! C+ S7 G3 t) v5 {
$this->image = $this->is_image();7 U' u* p. a: h6 Z# U
4 H+ J0 M' [8 W8 k. [
, P6 r4 K$ Y+ Q: y- H" {6 I if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);$ @* v! X% b) A' K
3 z% ^2 p3 {" C
' O( v# ?7 V2 k) \0 [4 _
return true;
$ q% ^2 e# n/ A$ P! `# r ) q% y5 V+ n0 [8 B/ I$ O5 H
, [" i# p5 Y8 b2 c6 q5 u
}}9 @* y7 b! i8 V
1 y+ ~. d8 Q9 q5 B* `
0 j, r! @+ o4 Y* q3 S3 a: j: @3 `
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
( [3 E/ J; N- D# e
" S- l; m& ?; }1 ?7 v' [$ U1 M% q$ Q e" h
<?php- R0 P: c7 Y9 U& U& X
1 u; |* q( N* D/ {% N6 x
/ ~: ^3 V( B5 ~. I function is_allow() {
# @' J+ E2 b* K( c, f
( [: D: ^" M/ ?" v, ]& Z: a( d8 s; o& S; j
if(!$this->fileformat) return false;
/ M2 G, q, O, M% ]. c
2 }- @; k' N* U# N
6 S) {$ c& u6 f$ y. r if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
( H2 z/ f( L0 ?: G
! X7 d0 d" c) `/ o& F3 {3 P/ j, V9 l& j* g
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;
# D6 k! Q- c! m- e1 _
, e2 s6 i. i& J! B! A' U: \! h3 k; f$ c
return true;
# b; l& X. J: E/ N9 ~$ o" t7 Y 3 E% Q0 ~$ p! }9 L) |3 U
6 k" W( ]3 `+ {& K/ K, V J }( k! M y- }' f9 m* X
# t- c/ O" F8 t# v$ ?5 l0 v4 p3 t1 \
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。& W o) L: ?, f" s: n) Z& v
# H" ^# \. \ b# A, i9 J
: I% G0 R6 \- ?; ]( l; Y" I/ O 接着会进行真正的保存。通过$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文件。2 u0 j- d# u, M# P: M
' F8 j! S- W( t, b
$ }# Y! J& \4 r- G1 N$ A6 h1 |3 o
漏洞利用, _* y, u: z6 S0 I% E' H
( X+ g! {- A: o t
, F% i# q+ J7 _4 u5 S- Z0 y
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
! [0 V7 ]0 w' H% s$ O. K6 m
1 B% @- P& m4 f( m8 n3 l( s) j R' p8 u! p
, b9 P4 T& v1 B) X; P" |
; {6 D8 R) e8 n+ `. h+ g2 K) R* A" g+ n1 S
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid- n) D& _' ?& I- g$ [% h
# r) R/ O" ^2 Q4 h' k0 m
1 F3 D; {8 r1 N: \/ h 不过实际利用上会有一定的限制。% s/ p% B0 d- b) I* x) A2 H
! J" u( W1 \! c
8 U* N" v) [( q( }2 f; a- P9 T( W+ q 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
. `2 x- c& S4 R" n6 l$ Z
9 B z4 o4 ] ]0 w5 U3 S. ?1 y! N4 t# Z. T
& j4 S5 V6 u' n: z6 i" W" W' l
2 m+ ~2 o& ^) u* _5 s
: V% l+ `/ k% a8 b. U5 ] 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
`% ~# W: b0 D/ f( C * s1 l2 w+ @, c& Q8 F, C8 [# m
' Q5 N+ S1 s) J9 J& |! Q' L 省略...$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]);省略...9 i5 @: E4 A3 N: J5 C1 K8 i
) n; I% z5 K$ d: K/ F
" ^2 R& _. P8 X3 T9 y
因此要利用成功就需要条件竞争了。7 `4 `$ U5 w& w- A
$ ^# i+ O% U3 u% J
0 ]) f5 F% E6 F* ]0 n/ c* v, a
补丁分析; \" z- p1 k' y( g$ s
0 E R. z+ Z: v0 t0 }
' V8 W2 J* ]/ O3 j/ w0 j
5 U: |' ?; J/ L! ~! g- M1 Y
: d3 A1 @* t. `
# L3 X" \0 y' l w0 O 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
$ g! M/ d$ _/ G6 O: ?! {
$ }; w+ q/ s& c& S1 P/ c
9 C3 d* Z' k$ @8 o function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}/ Y+ {) V3 A3 u! s6 N. w ~
8 ^& @3 D* C1 N" ^" U$ p1 w! O3 q
0 s: ?; k" I$ K7 \ ; |( N% M$ g+ p7 w# q7 u* t
3 w* f: a C- X- v4 s
) ?3 Y6 Y" ^: r8 [4 D" ` 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。# t1 S$ f* R) s. ?
, {6 j/ ^1 U) Q8 w% k2 k9 V! F( y# F+ c' H) i
在is_allow()中增加对$this->savename的二次检查。) {3 j/ b# l8 J v
: \% c; }9 Z) q) F
* l; M7 k( O& m) ^' }$ x6 A 最后
$ O2 Z$ s3 Q/ \% s$ _. T% w5 s$ K n# d. V
; t N- Z2 |( Y" G+ t7 E
嘛,祝各位大师傅中秋快乐!
/ F8 r2 S6 Y% }* T4 b
+ y4 B# r6 Z7 y3 d
- v( f+ l* U) m( Z2 b o& [
E- R1 a6 [' R ) `! p( w; x# T0 J2 o S d
|