% V6 m7 B D! G2 ^: u- \; g) G# l% h" ^0 Q- h
/ S( {, j. u( _# n1 T; G7 |/ O# E6 ?- `5 |, W; b H8 f
前言
) O2 a. o; K+ E) v: p4 S
; g8 S& Z) T. S5 t* I. I9 a" k" V3 A2 K9 A& A
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
( T! d9 M2 U9 J/ e ( m4 H* K" Q' C6 Q
+ d( ]' b. @& ^( c7 H7 M) G5 b! Y2 A
1 f3 L6 X3 }8 a! K! u 8 z1 o& h7 [7 p
z1 {) u3 I4 |1 C9 ? 漏洞分析
- S6 ~* l- v* X5 _8 C) Q2 t9 z8 L; W, q9 u/ Q3 @* }6 l) Z6 r1 Z
$ G- u+ S; _0 [% m
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
) h6 w* {" Z. j8 L3 k0 x , j$ e- {8 Q, v5 e% h
" g/ |% t2 t. y3 M( x$ e H
8 V1 J+ ^0 Q0 Z! t+ X" F* L" E $ h2 K2 R8 A: D! s# V; q
5 g8 w8 J& q7 J* l2 l 对应着avatar.inc.php代码如下:
! R* K( A2 |, F& [+ u; J' J$ r% B & j) h2 b+ B0 i$ ]' x3 Z, X- F
. m9 [) ?/ u5 @" L- 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) {
0 | b3 ?% ^7 U1 E S5 x( Y# Z* v
$ e. \( [: F, S9 ]" Z5 i& h1 A% F) G. ?$ P+ o
case 'upload':% K$ ]$ U% U' H
- U* D0 y+ M- }% {1 l5 Q+ z( [( u
6 D- E5 B; n6 a# @- X6 q. e) E
if(!$_FILES['file']['size']) {$ p( s8 D4 c; [
" |- P: U3 R% v% f9 D; o* Z
- ]! C( R" @3 n3 y$ V if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
( M6 }( T! ~- f
; k8 K4 P' V; w' Y- e* B2 Y& g8 n- _. d$ u# {1 y+ }6 t
exit('{"error":1,"message":"Error FILE"}');7 y% J% g8 ]: x: e' x
/ r( j" V( A. ~! ^
; p2 m( V5 D3 \ }+ K7 X* c: I: r- s S- U8 F
1 g9 C9 y6 `, q5 S% D, P8 z6 [# F! O5 W/ W* w: {. j" [
require DT_ROOT.'/include/upload.class.php';6 \( W( U# {% P/ |6 L
$ [ c( R# Q5 g, Q% I# o
; G5 G d' k" ?7 N6 j' l
7 E: a" Z1 h2 u7 t 4 d/ ]: B& }5 |' j8 ^+ J
2 t; Y( P' {. I5 A $ext = file_ext($_FILES['file']['name']);7 i0 m0 S/ x3 b0 c
+ w' S/ u8 L9 V7 A$ C3 n3 Y( e( t- s1 q3 L) h7 C5 S6 z/ K
$name = 'avatar'.$_userid.'.'.$ext;) ? p, _( I( m+ Y- s* O
& u3 } H4 Y& N3 k2 a( L( h
- k7 t+ z- R" Y) G6 a
$file = DT_ROOT.'/file/temp/'.$name;0 H. l% b7 j" H; D2 z
0 K1 a5 X" v, \8 R; \& z
2 M8 I F6 Q0 y9 y' @; D1 ~ G& ]! d " z2 J) Y- W# K' n2 q
; |9 M W" T0 f% n5 T
3 A) T7 P* z) E0 e. p* E if(is_file($file)) file_del($file);* W, U7 [. W; h! @- a
5 ^: n0 D* W: a8 ^1 B* j7 Q" `% x( C5 L" h& o( k
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');, o, G% Q7 B1 D0 Y+ [2 B$ }
H. i: s7 c1 d$ ~( z' F( \$ w
7 C+ z: s. P4 T+ X& O3 ~0 f % O2 m+ B0 C z- N P, _8 g; |
7 P ?, y5 z6 @: _
) X) b; q( l5 R+ i6 ?* @
$upload->adduserid = false;
5 u7 O9 t: e5 f+ U 2 Y- g' h, v9 U) ?
6 P/ h$ }7 j. Z0 p) m% r8 V
# U7 m& c% S8 F5 L
( ]3 j' ^# O# d' R4 P. U) V) H; T: h6 D0 e3 I
if($upload->save()) { R6 m% U! o! m' x" q7 Q
; ?4 G" s+ V4 v; z) @: s
1 u% E6 D- n1 J( d6 T. _ ...! S8 e: n# \: T9 X5 ?
2 q p- ^) z# L% ^
7 L* H3 C# f; i } else {
$ g4 @) y7 k" Q4 s 6 b1 `4 ]# E; P' B! v6 l( ~1 w6 i3 y
7 ~- l1 T" X4 l, @' A3 \* G n* U; Y ...! L. x( @# P' `" @) V
# L' K3 z9 n% @- C
0 _1 P6 O7 E; Q; [& Y8 E" ~ }0 m9 ?, R( M1 _) c
; s: [" t% j7 U& ?! {6 {4 o: q, ]# P$ ~# l$ _2 q
break;
5 Y2 s6 s4 q. F7 w, Q& i8 g* ? # k1 P3 Q% |, a4 |+ h% u; i6 [5 |
' ?. b! ^3 o# e0 Y( O0 z- o 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。/ ^* Q, f, G) K8 _$ D+ Z0 r5 s
1 l0 Y2 h9 a: j6 A B
6 X& m9 O1 O9 S- ^+ f upload对象构造函数如下,include/upload.class.php:25:
% ]- X0 q+ b7 R7 n8 D9 I ! o1 i! @. C( }' V E
9 r3 J; s# z0 M C. {# r i, X- o1 B' i <?phpclass upload {& [) K+ T) C: N9 w
3 g8 h; E- W4 o* L0 X; D% B! Y! _
0 r% L+ U( s, q4 g, A7 C
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
, H) @9 y# d' A
# o3 Q) ]+ |9 e0 I+ ]
4 e6 E8 m0 P0 M1 a- t' T; R global $DT, $_userid;
3 G4 X h+ D6 Z& |, |& E& Z, n 0 o: P% U( C7 q% D' M
; v4 [: @ ~" _5 k \7 g" g foreach($_file as $file) {
* I. D' u- N; X3 S- Y- N" } b" A) f* S( ?
6 V7 f& D/ @: X, D9 P: }' k2 x
* S1 l* E6 F( H, K/ Q& H& ? $this->file = $file['tmp_name'];9 Z+ _( ~5 i/ W+ ^( y% d
% L6 ]2 ~5 D% R% H2 q
1 x \4 W1 |* h6 q" s $this->file_name = $file['name'];$ I' T3 f8 O) L& h) u3 u; b
1 N1 i9 i. f, u) x# `: X0 Y/ M
3 ^! e! D' m+ I8 ^! ?. d) _# y $this->file_size = $file['size'];4 X; {& o6 r9 [- M
* S; Y# v" W7 y' E3 D
( H7 Z- Z4 ~+ i $this->file_type = $file['type'];
9 j) W& x) h1 o/ L: V( b' T& u+ {
6 r9 ~: N' r4 n/ K3 O$ u' j6 }( D5 U) a( j# o% Y% F
$this->file_error = $file['error'];
9 I# L& ]6 `. j V6 r ! Q @. l; W, }- a- |, @2 R
f# P5 K7 _1 X* L+ M. U: o
8 W3 {, k' T+ U( I" W. V
: J+ o7 p# b8 H* i+ C
8 c* X+ ~1 g6 z0 J9 m }
% R' z2 N* G5 c$ m$ l $ Z+ _" Q% B% ]- f
" l7 P, T Z! B. h" M0 V $this->userid = $_userid;- }9 m: J: Q/ [+ y- o
0 w& s$ _7 D" y
" x- i: a4 r8 C" a4 M9 E$ j
$this->ext = file_ext($this->file_name);8 z( }8 e; e7 r' Z- ?
" M8 \# k) L/ d5 m
* n& }8 r9 J+ K
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
" Y Z/ E8 Y s8 A" c" Q. Y" B4 W& P a& q% `/ [. E$ M% p. a1 u9 o+ S
' ]( s; X' A# P' F
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;3 w) o7 w& _* _+ w( m1 J
+ L V& v! ^- B/ i1 C2 D6 N! |
8 {; a; h0 n1 U8 _9 \! u% ^ $this->savepath = $savepath;
2 ?' Q8 `* S5 O/ h; L
/ o2 ]: ^5 p( z! ]9 N1 s% g5 y9 z1 D: L/ Q. ]( p7 I: S+ u
$this->savename = $savename;
! ?: k' Y6 G" X9 r! Z $ `. t3 M" H( s) G( R, g* C2 h
9 v' ?/ k/ o- x- H) } }}
( A: R$ w) M# K1 i& G - L! ?9 D4 G0 W# s, ]+ F' L& n0 t/ @
- o' {- J4 _ X' @) D+ l 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。7 `4 ~. H4 [" f' U) r2 x4 I
0 c6 R- i+ t/ q% ?2 H0 `* }4 h& y# Z; {; h3 ?! N! C5 t
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
8 P% U8 g' [/ z# k
4 v7 _8 a+ _9 S: a) W% k
3 J# N, @. }7 ? l- ^9 [ $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.php3 [2 P; Z3 m& Z" c- f. P0 b& d
* Z% X- e3 M) t. _+ P5 d" F' v0 f+ x; x4 {
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:) O4 t8 o9 o8 m6 P* w7 X
3 @6 Q( h7 s D, }2 @+ ~: d2 i5 m \+ w' P/ D. s. I5 G
" P2 S+ ^# w' r2 _3 u - V# Z- I8 _8 W# l
& F9 @) w/ Z$ K' F4 d( Q* P 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:# t9 F9 n5 K+ J4 ^: Z7 ]
( v i6 ` k. `* o! {; U; a( r
) ^( e0 m5 k/ w6 {; H- s& o1 a <?phpclass upload {$ \) v+ t3 n( B
# @( l. b( a8 E% }6 u, o Z4 Q" S. T1 |0 q( T1 \8 B7 ~
function save() {. `8 q( X3 ?) Q3 b( l- g
5 ]+ y3 b( H( B" X/ F1 z9 Z
1 b+ |* C+ l7 |/ a include load('include.lang');
% _; m8 ?# L/ g& @+ R! e 3 C4 d% Z* \5 W, l
+ N2 ~7 `1 ^, C4 O9 D% f
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
/ K v0 B, k' w. c
; g1 K) z s0 v" `& n' `$ ?$ S- o) ]% U; O( n E" [' H0 N
8 d2 h; b; x; n; z" X
5 E8 g; y9 ^$ a5 ?& m( p% R: X
# q. t0 B+ K: [4 e. D S" z; R& A if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');- z0 B2 R1 N S$ [: L7 k
; S9 r5 F6 o! Z7 l9 N" ^. c
, q/ [, N- I' ]# O, K
: r) b3 l, J" `5 v& f9 S* j+ @ 6 `/ d, H" _- B# S! r
% i5 d% q1 ~7 W' ]6 o' `! N
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
( \! S0 E/ d* j 3 x: G! V4 g. Z' ^9 h4 z, p2 `
( A1 k1 g' G$ { , [3 Y3 ^0 \# F9 F
. K5 ^; C( T4 m- m& l( H
- x: J% r4 r& ]9 @ $this->set_savepath($this->savepath);, S# Z3 ]* R) N$ ?7 f
3 a$ }6 d8 R4 [+ }( x+ D4 x* P0 V
$this->set_savename($this->savename);! q' H0 w# L, ^4 \1 j0 c- f& l2 E
# j* w- j/ g% z8 H7 N3 V1 x( [! X4 ~- {
- |) @8 ?3 s- r! Z: I) Q/ w
5 J! I- \& s( I+ C. j
: h1 A K- o/ W: s* z if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);) }: z+ Q$ _2 Q o( g% R3 X
9 q7 ]' _: v6 r9 b
: ~5 c9 s( _1 K- g! ~8 t if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
) F# G# R+ s# V0 |# [/ m( r; g: Y
; G1 _! |+ ]" x+ q. ]1 s5 f( L) K) g3 k* N* K: J7 [
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
' g% y* K9 k0 y' T1 F# w; _" K9 L 2 L6 d4 e4 n# _. F
O1 m2 Y# \7 f& ]8 _" |* { $ S! b7 X$ I0 m7 N0 I- ?* g
/ ]2 z. f5 ~) O4 x
, F7 }! m8 h3 L/ l9 L $this->image = $this->is_image();. G' N. H# h1 K9 J& |3 Y
$ z4 B3 s5 i: q' H) e6 t
$ R$ N9 r$ R. y% l E' g( c" K' Y+ Y
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);: \8 s+ O9 Z" V' O) H# U
9 `3 `" ]4 o8 p$ m1 D3 c- s
3 j, Q$ a. ^1 b2 i& C9 p5 f( S T return true;4 {5 D/ H9 w- g: R" u
2 q2 I9 C/ n* G* a8 l0 f* r
1 c7 M2 t' [/ m) w2 i }}
0 h7 B# ?# E4 q# k# ]% H* @6 ] ( o. d# t3 ~" n
% |) o; i1 Y9 [) S w, `
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:0 ]+ W+ K H5 x, F" k
) @0 z7 m' B- k# e
) ~* w6 P' Y5 q# Q <?php
, N: g( h! Q1 m7 U% E# |/ w( `
7 j* O" H, x7 q# J
- l1 G" p/ Z# ]1 k' E3 N function is_allow() {
$ a# P2 e1 k' b1 z3 r- N( [! a - K* m6 u* t8 A; j, d! A5 ]
; I8 |# V8 ^# B8 I5 i if(!$this->fileformat) return false;
7 o! F, _# y# g' k: c5 u : h" F; e1 n+ T. N* d' T0 D7 L
$ D2 U* M, ]5 \ if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;2 l: ?1 \; |% J ?, D% I
: T2 Y, M! [& b% j; x4 U
1 v! S% t0 z# ]; R' u% a F
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 i" n; w: p" b
) N: ^2 S" G1 t( ?5 l1 h7 R! y2 W% Q% d7 \7 I& l& @
return true;
& K) u) t& E2 b, w% x4 u& | ; f! u1 p o; L% g
* }: a" `! b1 x+ H# ]/ D7 ?) e
}* e$ v( z# {; I* U. b! g4 ^
1 M2 @& u ~2 q, j' W' p
( E3 S$ w0 {8 O% ?) X. ?1 ]3 f 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
6 b" i) K d" s ) A" x1 a4 p) O+ s
/ m2 } C: f( K7 D2 h" H3 ?" ^: 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文件。
; o. b4 ?# x: O* Z- s 7 _3 i: P4 W2 j r" }- N
# l' D$ [3 j, P9 w 漏洞利用: ^9 B7 M- c/ O( C2 @/ X
* Z4 O) L8 G5 P t& Q* _
8 V0 \$ ~, n) t7 i2 ^+ f 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
6 H5 \3 ]7 G; k8 Y! M% P l5 Z
) O- r \* C& } H! w. |8 d
- i9 ~1 O" \6 e \& ~7 g% F( F# U8 \ k " P A% E; r* g* Z* S: y
3 r; ^( m2 I% i
. j" c' w" B+ A; E; {' j' j* O
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid& Y8 k, W u( d4 ?4 V
, t' X4 E/ ^7 M2 @1 G
X0 U) z3 s8 O! Q; k% [( C. P. b: U 不过实际利用上会有一定的限制。
; e, p* X% R- O; u( Z4 B7 z( y
! I) B" ~9 R# B( }9 Q$ v0 k: Q$ R- [
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
4 |3 e: v8 E. H: c : }/ g/ h' V' g$ |
2 p* O5 [ K( R; F4 _9 T9 B
3 ?& Q! {$ R3 m/ v3 e( s * V$ j; k# ~8 p9 Z* z
% ?6 m$ ^- @" _; S0 ?
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
( ^3 |$ j1 v8 f5 d3 @, L4 ?- @) m9 C
( h! V0 \5 L4 U$ f% l Z* x2 u% I4 i1 i$ r4 S) E
省略...$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]);省略...+ L( W8 u% Q$ X! L/ h
! b& T! Y0 q1 p* `5 Z8 |( ?
! |3 w9 i6 C M; P( J2 G 因此要利用成功就需要条件竞争了。
( ?, k, z7 _! l6 ^ # w/ k4 D7 f0 I
5 r: ?& k. \0 G. I 补丁分析3 a5 J. I# j% `, a
! h& D" X4 B9 V9 o# t
' b; j/ G3 f! R' F) x8 j
: j3 {" j; W6 n0 @3 s
5 y2 x N9 J5 c; m, r; V$ q. v! A5 [ q7 d, S4 t# c
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
H' w8 ]5 n: R3 V! {. W 6 R( B) [. E. A. p
" {3 a ~/ I! a2 n: O3 ~9 @ function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
! A( N. H9 u* \4 T8 U( }0 _ ! |% ?, M' H7 R/ {1 w0 l. T4 b, C; q
) Y( V$ |, A' G$ @ - V2 u2 y& a$ k" k$ {& K# e3 ^8 L
- v) t" O' M. U4 t
& J4 ^# ~" Y+ U5 M
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
2 @* {+ G& q5 `) h. q( b3 {1 ? ) g1 f+ O, j% K |; }4 ]8 U6 k
; k% w. \& j9 l7 O: d/ n 在is_allow()中增加对$this->savename的二次检查。6 l6 ]1 D( [( P( M$ V
0 ~ a! P! G) A% y
6 }; m1 t/ Q/ \2 w% } 最后
$ c) S5 \- Q9 R
) e* p& I( b* \9 V. h
0 [( g2 |, [9 N, P5 T( ` 嘛,祝各位大师傅中秋快乐!
9 m8 K( X* c7 q' `/ h0 A
' F5 ?) y# Q2 d t2 b' z$ m
% Z* @: D+ A3 v: J+ p4 e/ s
$ o0 g6 Y/ }( F# \/ b# Z# W
5 g: M* P0 r' D/ C; D4 g1 X- U9 p
|