' p; Q& a' U$ R% J% i5 ]2 W; f! S. ]1 n2 b
7 l" S: a M( H6 c3 U. A
: V: k2 s2 b, d; p
前言
* p& W& T' P0 g8 ?4 F
8 |! `+ t- G/ Y o3 k3 Q; {) l8 P& t1 l) o
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。' l8 W/ b3 Z$ ]# K3 e
8 ~) ~& F3 q! R
1 b/ r2 C" b" T) T* V( u / ]5 r0 h3 N2 e" A. p: W
0 v. F$ k, p1 `4 h" o3 E6 S1 ~3 s# I9 Q+ V
漏洞分析
! a' `6 ~( S" `
+ w( R$ W9 U3 f7 n, n ^' m" m/ c) B- L% e: P
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
) a* k# A. d, M m1 @. Y5 }
2 V! D0 ]% o0 M. l) _
* w7 Q; @$ Q7 {7 W
+ h1 T" E0 ^3 f# f$ h+ ` 2 g* u1 U3 ~6 i- \
& R0 M" D, j/ m3 K* h 对应着avatar.inc.php代码如下:8 q, u" d0 |- \1 x
8 z0 s- A0 d' Y9 q6 `, b4 v4 {( t$ X
<?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) {% j( p2 e3 l! Y C1 ^9 A4 ]0 ] A
% }3 q0 q) z L8 H i: Q3 q# [/ K& W1 e) S) }
case 'upload':% K' K) T# {9 g8 B
' {1 ?5 k' U/ o8 D: V. V& Y5 ?
+ g7 b1 L7 |) N5 V* f. l' Q8 ` if(!$_FILES['file']['size']) {
5 I3 Q8 V; T. ~/ `$ K9 v0 ?0 | ) c, X, a" q" r; y2 d* K, i6 T2 `
( t) e4 W& \6 l& q, d if($DT_PC) dheader('?action=html&reload='.$DT_TIME);+ r! C3 ~' h! r, f' ~# j
& X9 V5 E" R0 b; H
1 ?; y+ |4 W4 P5 }# B1 H- ~# H1 S G& i
exit('{"error":1,"message":"Error FILE"}');. w. B4 f0 ]" _
h% {5 Z$ k/ i I, s+ E1 }) T1 C0 @/ e/ I z" U2 r9 z0 G
}, B; ^: Y& g, a1 p% C* A
3 I( `' m+ p/ ?) e1 A2 y+ e
0 d( R8 L" E0 K% A* c3 V6 S, R
require DT_ROOT.'/include/upload.class.php';
' W, X" R; t( |' {* l
& m0 i; L0 x K: k# g' A6 D! E9 f# b- V
. f- [% ?$ c8 F& h, Z; y. f
8 O6 G% ?6 Y0 Y4 I
* r+ h' H/ N {& c N
$ext = file_ext($_FILES['file']['name']);
' F |" R, P! T9 h4 `, y7 @ 5 x3 r/ r( r7 P: X) S
$ W% f& X" ^* e; o }) k; B5 v $name = 'avatar'.$_userid.'.'.$ext;/ ]1 r5 q: l4 u% I# g# h
. t" M' [7 r: h. w. k: ]+ v) c
) V3 e# r, g. I6 i) U. J $file = DT_ROOT.'/file/temp/'.$name;# z ^, q& s. N- z% p. A
$ n B$ a" ]; I8 a1 c
^, J" r4 M! l5 ?- y G
: l0 u/ Y) R9 d0 v H+ ~: x
s! Y7 z5 R' Z& P* x
9 @8 q2 R1 L9 u" b+ u/ e/ \ if(is_file($file)) file_del($file);1 ?# C0 O0 j* H' ~
/ a# H% Y7 B; W
& W; K3 v& R# c4 d $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');$ s' D" {- r5 a; u7 ^+ T' P
1 C& q, z. }0 ?. n' h
8 M8 n7 d m: B7 v; ^
, Q, I& I2 O- ?! N+ X7 [% C8 i1 i, t 1 \ i6 G F2 H/ l9 G- y
' a4 o( C$ h* j& Q2 s ]7 E P( A
$upload->adduserid = false;7 B: L+ O1 ]% W
$ ~/ F0 R, v" U B9 ~6 U- S, E( V; Q
2 Z8 Z9 N A) J: W r/ E
' C% L `9 n0 U
2 L2 v1 m7 H& G/ Q6 L3 ?
if($upload->save()) {
4 y2 C$ @0 O( ^$ A+ J/ D. z, V
1 F6 T) {! M. I5 W. W
* W% M( N6 M4 ^! _ ...
; x | |- v) } b6 _3 z
/ o f. {3 R7 v5 |+ c% v: Q0 s: p4 t2 `( r8 G6 A
} else {9 F5 \* r. ~1 z3 k, q& \, k
) ]' J- b& S+ w0 x+ h' d
( R" }2 K; w9 p* z- V$ K7 m ...
# ]1 n) ?+ C. W4 e3 \ : m3 O8 a8 q9 U4 U, ^
) J y9 ]% p7 P+ | }
- ^$ t2 V) l6 t7 d4 u5 n% @
; t: @# y$ V2 _! n4 k1 n4 F. Q/ |- l! x7 k. m3 o' C4 J
break;) v/ K" H. B/ ^* D( ]
5 N' A Q. h6 u
( |3 N; I% a' k5 i& _* t+ H3 L% Z" _ 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
! t2 X8 G3 c. V6 M) J& ]. L' X 5 ?' Z; q6 S: ~* |
# F/ I7 Z5 c; Q4 l7 K% F
upload对象构造函数如下,include/upload.class.php:25:$ n5 B& }0 H5 _
5 S5 X1 |0 b# A$ k; K% n! d, H* o2 H3 [2 `$ k. @+ S
<?phpclass upload {6 Q2 A T: D( E% I0 q- A" c/ e
1 A4 ^8 l+ s% I8 J
2 \; K. x* O+ T& ] function __construct($_file, $savepath, $savename = '', $fileformat = '') {) o& Z% R7 f! A1 T$ [4 \' F$ b7 J
, m+ ^# W3 N- T/ B3 x
y# R1 N# |0 G; V
global $DT, $_userid;
+ V( d& h3 |4 I7 W& g. P5 ~4 F
) S3 P1 L4 i2 @- m, `$ s
% X4 A+ z9 s2 K; @- ^+ O foreach($_file as $file) { ^0 S3 \/ E: ?- E8 q5 K/ s
, h2 |7 s8 N- d$ C5 R) k# F) [5 J' r% R) P3 Q% F1 Y( f) }9 K
$this->file = $file['tmp_name'];5 s$ Q3 n, ~7 t- I
+ U2 [: O9 ~4 G2 [: r% {1 Q/ x
9 V& I5 y- K: Y6 W/ f4 q% ]; i
$this->file_name = $file['name'];: ^7 W4 p; A+ a; Q
: [9 g8 q0 C' q; u E
7 t6 A, |/ N; V, v
$this->file_size = $file['size'];+ H& }# d, M" e. g( B% Z
2 b0 N) q+ s! w- n( x& g
( ^6 Q* d; \' o $this->file_type = $file['type'];
' s+ d5 Y n, n / M. X$ _( n" j# h5 I* Z
6 @0 o0 o+ _0 \" M
$this->file_error = $file['error'];; O9 B, |9 p$ e) ~* \9 U
/ `4 W/ @. y& K! A g! D; @' y; ? p9 D, }0 w, Y
3 S6 ~" L3 n2 g N
+ ~) x. p7 `, |) l" a+ c0 u# T4 S1 S/ t; S4 d' _% D
}
* I, t- |4 x. I; E: s& U * U9 k/ j# Z" O0 f. S
5 y; z6 Q0 B3 l" a. E3 n $this->userid = $_userid;' k+ q8 X( b$ \8 n) W
8 ~2 X+ O- X7 ~6 J0 s" o) n3 x! G' t. d. Y v9 }3 f. W
$this->ext = file_ext($this->file_name);7 N% p5 i. v: o G: k3 I, U" Z9 C! ]
) I7 _/ [1 J1 p
$ U! j; J. \% H$ W0 S $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
5 x& v- n# R' `! T+ c8 I1 ]. I
, ?) x! l- J5 o
" u) R# Y6 h+ H# }8 A $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;% j. e+ Q) R9 |. _. y( v
* O& [. @1 A& ?2 |/ y3 k+ C9 o) ~3 l( V- D/ ~+ o- r! e
$this->savepath = $savepath;
5 a) ^4 h/ r- U" ~ 4 f2 o/ B2 c% ^
, [/ O7 N+ Q& m* @
$this->savename = $savename;1 @+ _' F/ R0 A
5 K( u# M: M: r% G, y! _" s1 J$ P8 c2 {0 m8 S* |. e2 e
}}+ g+ i9 ]. i$ T, w5 F+ o
0 M8 G; @# g* r [
_. n1 C. ^+ M0 Y9 L m 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。+ G& E6 j, ]. E' G% q# K
9 @9 V+ [ I+ G5 y4 a; c' v
/ d( R' q% S3 o1 P) P/ n
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
; W$ Z: l1 g" V$ o# n% O . b$ b1 p' ~$ v3 d5 e: h
$ x: t: G2 |+ T5 o c- B! }% k: p $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.php1 r8 X% S4 W' ]4 v* F
$ i/ e* ]* A) }+ ~6 b, c
3 Y4 V0 H: E) d2 L n; u
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
3 B7 h+ S E( a" m9 k
4 E& @2 l+ U9 S! ]0 c
% |) v+ ~% u8 m% m. v9 N. ? * G0 K6 }4 m$ m* d' m1 n
) T& Y: Y: |1 k/ V8 l; `' S' {3 F' }
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
6 S/ \; }( h0 R! \& z" m5 V
; J) e6 N$ n j( r1 {' W3 X2 [( G( a. |2 o- V
<?phpclass upload {* |3 W( H( S. H0 i: x4 b# M. }! o
* V4 p( K8 y- X& i( c$ [& D
) P6 N6 r8 d- `9 N6 M; H# s( c( [
function save() {
: ^! o, d4 P$ W& S0 l 9 z B' j- e( M/ ]) t7 U
& R- A2 l; ^% v* X* J" f3 d
include load('include.lang');
, _( k P. n2 v. Q# C4 Q
; v- |0 l4 ^$ A4 @4 T; o, w: O! p2 n$ X. N( u
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
% a0 d& M& S a% n5 h4 [ + V' G' o0 G+ z
3 J6 a; L) l+ K
4 q1 m/ N+ ^! C" ]% e
. o( X* Q4 a3 z C& G4 m
5 S0 z+ Z- v3 Z. \- W9 P8 b if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
9 B8 Z1 a9 Q4 h A+ S1 ^1 B 3 c% f' k6 p& z$ Y4 n7 v/ s( f9 A
5 g3 ^9 c8 f* v( O7 [) h( c! V
0 ?* @4 {7 {, G, C% R; d' ^" z
, G2 h) M, f( ^& a
1 i- [3 b7 r s% n+ x if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
& T: j. _3 S" a& M
, p, X3 G; N% F E2 e B3 j5 @; q, Z. y# q0 o
6 k' e) @% X. a/ v9 D( k- f1 _
3 \$ O; x* c B4 O. o" z# {
! G- F! ] l' n1 Z& X& a) D& K $this->set_savepath($this->savepath);0 C* T+ B+ ]: V$ v
- _ w$ [6 m8 g$ i# s
; a. s1 ?2 E& k0 q' `1 u+ [% n $this->set_savename($this->savename);3 k9 m3 M1 S0 l
9 z! l8 {9 W% k- ]: c0 j& m: {9 I# {
9 {. `8 r5 f2 G
; V6 {4 t. q; `% N \6 u
E5 s1 G" c) s0 ]$ c+ M* `
$ ]4 O" b+ J3 A% o# T if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
1 |( s# U( Z" E# `& O& z R3 N+ p- H8 z9 F: S; h( P
8 @& J* }1 b) ]. G/ l if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
9 F; w) ?/ L! v' Y" d; H C- N : Z! u- H* o0 n5 n* ^1 w
9 d1 ?$ Y5 [4 ~. F! C) O& H8 y3 y/ V if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);$ T* n- K3 k, B6 O) D
# U. z9 e- A6 D0 Q4 Z. f" z" I( A7 N- J" U" L
* P# }7 g- ^* d" B9 i" F
+ B* @9 h' T. L8 O8 Y% }( ^. v" i9 ^# V' p) Z8 A* p }
$this->image = $this->is_image();0 m& h% r* v8 X
0 g, q3 G- x6 o9 F% N
! y* o1 B7 [+ ~1 G$ ? if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);$ i7 T0 u4 F/ Y% o3 m% _
# f: }! q# e7 e
8 U$ d1 K4 @0 m" o# I; j5 p0 l
return true;
e4 R* ^+ |) t' [
. c. F$ ]) a0 p- S p% l# |- U+ b ?+ V
}}" V2 F+ F1 e0 N* o* t
. p3 m. r; U8 a$ P% s
; _7 K. X" z- ?/ D' X
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
: n# e( @: U) d" A- J/ M/ E 9 S6 Q6 h+ |% O7 ~8 ~2 p
4 M/ L8 X. Q4 k4 u
<?php d9 {& ~' O/ p( ?
5 Y5 d( r6 |5 f9 v3 k9 h3 k4 p, @9 ~; ]# Q5 R8 m1 t
function is_allow() {! f, [5 q% x" n
4 b) w" ~! Y$ D: {! W" q! `" h3 L: X9 m# w+ ~# Y6 b) I
if(!$this->fileformat) return false;
0 ?7 }6 q% V3 H7 M
8 ]! U ]: V2 e& E0 j. t! H8 u, G: X! D
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
% }) s' p9 e: b- x$ k4 b7 R
$ {. s) n- H8 f0 j% M. a# ~9 z- {; Q* ^1 a( a7 B, H9 S
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;
2 |8 L0 K0 f1 K: @7 D
1 |; O- j1 e) G; K+ b3 i
3 q. _4 F7 \* \ return true;
" J" q$ l6 Q! n" k( ? 3 L% [3 |6 A' s7 e1 N% h
6 H6 \& t; z! u! T# q
}8 ^- \% ?% Y$ B1 r
3 z. y u) S8 f
+ e4 N# F, X7 R/ R, O 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
5 e/ t' Y& E5 G. i6 y
$ n, @, o! ?$ S! X9 I4 D( X7 R+ F) ^2 S6 |7 p) Y: N3 V. j: 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文件。
* u8 m4 w8 U" F
# l$ F/ T4 a0 C# c6 Q: e" t4 c7 Q
漏洞利用
7 Y2 e0 g7 T0 F" T
- G+ A" y/ t2 j5 H; S4 U
U- L8 G" ~8 M 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。8 D0 F+ e& b# ^& p; t
1 n! H, I2 \' l. ?- z# m
* A1 D6 m* w! W8 ]! j( _
- {6 V3 ^" R6 U) D
, z4 Q/ _4 b% r9 s w6 i0 g' e* U$ U- O4 F& V
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid- B5 P8 O" x- \9 O( L% ^
7 e; s% H' q* D
# t N* f/ H) I# }
不过实际利用上会有一定的限制。7 L+ q6 u5 H1 Z: M
6 ?& N' d3 E3 u& _ \( o( T: v+ L2 ^" s- n; D* Q' U, H
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。. J- r. l/ `. l
- H6 p" ~! l0 A! \1 ~7 u0 J9 ^
' D1 U0 G, t8 E5 I
9 T, X) l H. @2 m& ^0 N
5 y9 y- R7 K" a( e, |5 R- F1 e
# m- L+ V# G: Z3 L3 ?3 K& a. y 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:6 p9 R- l6 T9 S/ j7 Z
8 U9 [ F: K" K+ l/ Z- _( Z0 D9 \8 F6 ~, y& S6 S0 V( |
省略...$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]);省略...
% @, r" L# W5 T4 ^9 \+ c
: U" d# c: w( A0 q; m7 z
, c9 N/ P; g" c$ s) a 因此要利用成功就需要条件竞争了。
# K# ?6 a1 |8 G* P1 y
. |; z# H- I' \+ ^* S2 @+ k4 O7 @, N) ^" `1 @: H( B8 W
补丁分析' n* s/ `2 w' k9 v# U5 ~
& q" M) ]$ N. Q- x7 I2 u w( l. i1 @8 j0 U+ ?
0 u1 `% I+ \+ r" H0 E
& @2 S" Z! y. Z
* b/ q1 x4 K7 P2 I D
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:2 l. ~! R1 _' y0 w. N( k
6 F3 p1 f. W5 C( P5 \$ N2 A' u0 G" y
1 p4 \" ~/ _- N
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
. g; s; D" |' u8 t * X$ H) {: S; `8 u& Q
A$ [7 C' ]. ~; H8 ~# Z. D9 T
. c E; J2 F) r; D- @9 V2 I1 J
. h4 m$ L% X6 r
; _' c H& S2 |( T- i7 r 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。* \8 D2 U* Y8 N& G% ^* w0 q
9 L5 E: p6 p; m
4 _/ d& c5 ^* l 在is_allow()中增加对$this->savename的二次检查。# v3 E( }8 g2 g. H1 j9 K# F& c
" O( R9 d& w+ Q
3 r: Y% [! p% z: D7 g6 I% J, O 最后6 P6 O9 l( |9 S$ @5 j+ S0 q d" N4 Y
- K \+ t- m7 r3 t2 u
& B( N& R" U# x, v' ` 嘛,祝各位大师傅中秋快乐!
/ @4 S2 w* t5 I& L/ [/ q) Y
. R$ t, n% B k# L
6 `3 f2 v ]+ W$ B 8 ~5 y7 j% j9 t; A
* S7 [, q. R% q& Y% T! ?/ w
|