" f' J7 s3 @ }) I) }6 H! U0 Y2 U+ m# D6 t- S; ~
8 N( x0 I* b, @; k" [$ F
1 h; ?1 m* M) ^2 C7 ] 前言
6 \! }. m7 R0 {7 n
9 E2 k' z! B* h+ {: ^. H& q A* `' a5 U
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。+ i( V# s( J. {4 c& j/ H
9 c$ s" U1 o3 d% [9 G) d0 k; F% x$ k/ y; s
' `' X0 u! r3 m: A% m+ e4 |) l
. w9 | ^+ Z; p% K6 H# ]# p2 I6 x9 M& v. ^; S. a* ]1 Y% P0 [. @
漏洞分析
$ |; K7 p* T, b
5 T' t* {( W# V1 l1 X
6 D* L" m8 X$ ]3 h 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
H! ^3 [& I2 s2 F$ G $ l+ A+ s( Y, m& ?
: e- c# e' x; q, Y' Q
! W3 Y6 k Y: r8 h" x& c3 F( \! U
: F' b2 o. j% x2 o& j" s e
9 h3 F; c+ u! g( J! c- m 对应着avatar.inc.php代码如下:
5 @! P5 [' `+ Y1 r
2 s* D+ \/ X& @" G+ i! U( p3 s f
8 G( M, v" x4 |2 F <?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) {
' i- U% g, C% D6 k8 M ! }* u2 q; o- R8 P# U
" P- X1 g+ S1 R7 ]/ e
case 'upload':
) W$ r6 P; F4 i- s+ O( z. v1 ?
& b& m) b/ d5 V2 E* B/ |# `2 a/ x- `4 Q; f* `% @; r
if(!$_FILES['file']['size']) {
3 v K: H) p; p; O
# }3 G5 H ?, ?! p
, X. M' p1 n8 h! F t if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
: J& {2 [3 m$ c
/ L5 X# H5 ]9 B: O+ c A5 J7 D+ x1 I4 G5 B3 o
exit('{"error":1,"message":"Error FILE"}');
# S) `/ A' D# U
1 R( X0 Z0 |: ?. q3 k( n' ^/ e: E" p2 B2 b% I
}
; O) H0 F3 ^/ f$ r
) @3 q: f: f% }8 I4 P3 f# e0 w- @0 Y( Q, m0 z. V, C \& `: t$ D
require DT_ROOT.'/include/upload.class.php';
: Z% Q: s1 A6 H# ~) U
8 p& a2 G2 |4 T- P: {* |9 v; ?
) m/ X9 E; B2 z' c, ?6 U _3 ?
% t& g3 z( N3 p+ n& r# Y
+ y( i, n/ o! j2 j
) K' |- Q. w$ I $ext = file_ext($_FILES['file']['name']);9 H8 @0 ?. Y6 Q# H. L0 F- d
! d- f& P* B" [% L) A: }. O, V( C+ f+ a* y" y/ p$ D O, P
$name = 'avatar'.$_userid.'.'.$ext;
" U! Z: i: M. ]& b+ m8 H
/ k& d0 |: |* a: k/ K! i% W
/ ]; b. c* S) \$ E3 X; L7 t $file = DT_ROOT.'/file/temp/'.$name;
Y( v9 ?9 @. w+ B, @ # Y) {1 @! p4 s% d
9 M! w, |9 ?) ~# q
7 t7 N# X2 z5 N5 U/ i6 y1 j: b
4 ]: `% v' k: `2 p$ ~2 m
1 l6 G5 @1 [( e" i7 } if(is_file($file)) file_del($file);% d! g1 ^' |% g4 m
, ~3 A1 \# u5 \9 y0 }$ _+ o. l+ B0 c, e! F+ f; H* B% Z8 b0 d! k
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');# X: e0 f. {7 H
# B/ L% _( I/ |8 ^. b7 T" ~7 L# o
5 x8 L, J& E/ f* a9 ~9 N
- r L9 z0 c+ C* G9 z/ y$ p 7 l) u/ a7 G8 q5 H$ }
. Z$ U# z4 z$ Q
$upload->adduserid = false;
# w# a/ x1 {$ u" ?, g) g; S: d
; e) J7 y% Q0 _( w4 J8 x, R
! V" ]( x1 y2 F2 ?8 T- _ ; t8 {9 U& E$ s4 c8 l, F
6 W7 s4 `4 }1 h ^6 d) T# Q
~$ [( N$ c$ Y0 \ Y1 f0 g
if($upload->save()) {, z0 I, Y( p f/ |/ A4 A
1 t/ ^3 m' D }9 ]$ b
; k5 s- r$ e& a ...
3 F/ v( p0 L4 B& U. z 3 H* d0 ] g3 V$ E
2 r% m8 ]' }# W) x* e } else {4 {$ Y; D, u7 e
?" z2 L) I6 D B; Z; n
5 T$ V |8 H, E8 B: B, _1 | ...3 q" g6 i. n4 _. i' e
( n: l5 @4 w* ~5 b: m, Y
, ^ e& @" B" R0 e- R6 T; M4 G
}
. i9 e) f. k9 y/ a! b& F& k1 w) L! J
" | d0 {& n. N" |8 _ }+ z! F
, I; h, j* c N. a break;
# ]8 v$ C r1 L' \
: q/ l. R y0 Q. q3 o6 }
8 N: h' d, L! Z; o 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。 z2 k9 B) k! F* i" d% L, D
3 ^4 w: b% u4 c% p
( z8 e9 { J/ P, v) U upload对象构造函数如下,include/upload.class.php:25:
7 ]1 Z7 N L* K' V- E0 q* Y8 K u7 C
$ ~+ J! @ N& Y# b# m
6 G' |' {/ L0 [# I+ b <?phpclass upload {" W2 V& F# H2 o+ s
, c! \: F6 M$ p% X3 f: W `
- J2 x" t, S( _, K" W7 N7 F function __construct($_file, $savepath, $savename = '', $fileformat = '') {
& N$ a( E$ I2 f: }
" @! k/ n8 }( Z1 j) K% T1 R: m. B# a$ l4 J
global $DT, $_userid;
' a- A. u9 _! L S
% z- |9 I5 y; Y$ X- s$ }+ d
: e9 R* w7 P! _! l" }- j8 p6 N foreach($_file as $file) {
: C+ k8 Y# Q3 n c/ l 5 h, R$ g4 l' j4 j
) J9 C1 d. z5 Y- @( h) `. k
$this->file = $file['tmp_name'];; ^3 U0 F: m( i% [" }9 |, }5 o
, C3 o [0 ]' V+ c' g! B
1 R: ? r& ]) [0 [/ n, p) W+ K
$this->file_name = $file['name'];$ d& ` x. X9 Z# a
$ _, g# W+ K, M2 J5 X5 c8 y" H. D) E2 u2 H: D( s
$this->file_size = $file['size'];
$ M( H: i; x% }, F! e
1 x4 F1 E/ f7 H4 M2 y, l1 r) R/ j* B% n! c
$this->file_type = $file['type'];
1 h* u! M7 N. |8 u
6 w, w9 t& N/ U- e# P5 { ]+ O3 j& c/ @! E l! U2 D( ?% @
$this->file_error = $file['error'];
; p3 i% Y/ k k e8 X: d% C8 H
% M8 S4 K3 K- a3 a2 Z
. {0 Y( J" [+ k, q* \
8 ^! J5 s8 v5 ]3 S6 y
/ `3 ~5 N# n: y* e/ b }4 R1 Z& v E6 G
}2 k8 W z/ F3 L
7 h+ V$ J! `+ Z6 c7 x
% q( z$ {- G$ ?0 r& n. U0 z $this->userid = $_userid;" u" \ ]* i! e/ \
" E. U# h4 K9 \* r' C+ u# P- \3 ]
$this->ext = file_ext($this->file_name);
! b% y% f1 P7 C( R& b3 D' r9 r8 p6 n
: Y; _$ R. O) s4 l
$ _" J* b# y5 J, D; n+ h- O; x4 A $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];1 K8 K: Y( Q7 W/ C& e! \
- j0 ?8 s7 h( P) I, p5 @0 {
, M' F/ B* G: D2 z& k* I$ F4 M# T
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
* q X( m% O* _5 M I& `" b
2 q8 |& T n# X! X' K' a* s8 D, C9 i1 {9 X i; F9 x
$this->savepath = $savepath;6 E/ X4 r b% r' {6 K
6 V t$ }) D/ r7 m' i1 F7 d& e
$ s' [+ X; ~6 i" t8 m. a, O( @ $this->savename = $savename;
7 S# I8 N0 j+ G% f* S: X
% H" U# }) o+ u* a5 E* }6 M
1 [! F" f. S; Q; l* M: Z' [ }}
: @6 L: e1 I! \ @& w 5 I5 L, g% Z& n. p4 S8 k, x9 n
. z, B. M( q6 m. w9 o1 j- Y; X 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
f5 z* m- Z4 w6 v& e
k2 i) h/ i1 \
; c3 b! @6 Q* |" S, d 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
- n/ j! H5 B3 O
/ _1 v8 H. V9 F& N3 ]
5 P: U7 `- }3 m9 d $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: j s( x( h5 @1 S$ J1 O, ?3 h4 E
" r9 N& l4 k" e* E9 k
2 }: N1 e, S0 J% f: F- ^6 h
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
6 N: V" G9 N1 l ' Y) v& a/ [2 V/ J& ?
' s- u- ?) I1 g! A0 ~
: F" R' t+ M; [, Y( c) h0 d5 a9 l
0 g5 M- B: M) R" g( g0 n" z; R
1 k; f2 [* F. m; z/ o& x3 I& K 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
b0 x; @9 B" n' Z0 L
9 }7 X5 q8 a" Y$ a% s+ E$ r* Y0 ^( A8 a* U* n3 H. A
<?phpclass upload {
[7 w ^8 [' i$ D 4 f1 U6 A& [0 o* R9 c
7 n; s5 d! X2 L- w) T3 P, G. O
function save() {
: w5 V7 I+ D1 S2 s 2 X" n" D- o: O/ }4 i
. J( o7 f7 n0 M* N# g
include load('include.lang');
$ m& g8 t: u* Q R 7 z0 r9 U4 D" T8 r2 ~' O
8 a1 ]8 `8 u0 M9 @) [0 r if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');0 B- u& j! J+ }# x! S
9 d! |8 X8 o& J
/ C0 R3 V) K/ c2 w
2 \8 r g" `. W* D8 Q5 Y8 x5 |
0 w, E# \' X4 U. L4 _ G2 _- E6 e5 N8 n- V: `, `# \( M3 l8 p8 B
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
5 A& s* j. r( w$ O8 Y; y 4 E0 C% T: {& f" n
4 n/ \8 w: c' D/ R: a& u: K$ o % P7 E6 J5 p O9 y% f: Y; a1 S: S, \( P
" l* y1 o& F/ A5 S. z# d* S$ z& u, e0 h- B* d+ s
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
1 ]6 p9 f' b4 {% h: O: ^, E; j 7 `) m% M5 _6 v/ w0 V
1 i# U3 C; C- }" {
' N: A f. ^# V
~4 J, c" G7 l; g, R6 N
6 q# g5 F! G: j+ X' `5 Y $this->set_savepath($this->savepath);0 h* f6 l9 [- f4 A5 |9 ?' Q* i
+ v! A0 m. y |& h* b
S, q1 `; h9 r, H4 G $this->set_savename($this->savename);/ _7 r7 x. a& D2 w Z
8 o' x# C, H |+ s0 I( }) F: `
& k! s: X% v; j6 y: n 7 U; ]7 t8 F+ S+ q6 {0 d- M
+ f3 F8 A; |9 l
2 A2 O- J) j) c/ ^! o9 ^ if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);* M4 J7 e2 C/ J/ s& j; i2 ?9 A
! _1 b4 \! d* f# S \* R; C' a
- n; ?0 ?+ a; f3 p if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);* z N5 Y" x1 v) U% J
. h8 H% m7 _5 m: ~) v9 U
0 R, q G9 z: e$ L# D if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);& H" t R( U! d; T, o
7 v* l% k$ N* T7 F
" |, D6 c4 U9 g" ^- B) a : L# h, v/ U+ j( N) D; F
& H1 a( ~. U' I- K# c/ {
- y! T4 W* F' z( c1 `* D $this->image = $this->is_image();, w8 o1 C% H* q& X+ ?, d6 c; Q
1 l8 d- |1 G* A( i
4 [" o& P& g* I4 C if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
: h' ^: I! K- {/ @! i8 y* }
$ y- l7 N# \9 X0 Q. ?, U. ~$ @( ]) m/ P
return true;
: C3 ~2 S+ u5 Q n% p6 p$ R/ w
; ~& j+ y) [; \9 p5 T( k# K+ O# j8 L2 `: |# T
}}
2 R" Y6 I4 l- \3 ? & ?) G1 w: ]/ _" j" _9 \# N/ A
6 R5 v& E# s `8 S1 J& d
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
) ]* ~. R9 ?8 m 7 }/ J1 x6 R# @/ k
& p( A( i$ K( J, I' ] <?php
4 G) `0 u8 o: H/ t
1 l2 m) t8 u9 G) a3 l# ]
8 p6 P2 s7 e5 t function is_allow() {
1 q& l- x; j q. U 3 Z. F9 c4 T. A) N) y2 j7 r
@/ z! I' k7 L5 r
if(!$this->fileformat) return false;
. k! `) n8 E8 G5 e( @' X : E& p7 M4 H- C) i
- G0 a1 S* |$ ?% { if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
6 Q z2 q |6 N; S ) R, w7 v. w% G( j6 t3 ]
2 H7 L% N- M1 O) w! S0 m+ Q
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;4 a% O' l4 D5 u- u) ]& X; H
8 n& B- ] _$ `. |7 E
0 k+ h5 h2 G4 d5 m R$ k return true;0 q7 F' v% c5 o" p
1 g. h6 @5 ^2 n) i+ _6 B0 K
+ M2 U2 N$ T$ u9 N! [. B0 L }: ^ L7 t0 G6 n
7 Q/ L/ X. \3 I) r( a
8 d! o6 U4 B* }, I) U2 m 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。8 o8 o% J j% V5 h" G
# t1 u' @6 p* [' x/ T7 h- u, h1 s% Z; T+ T) c8 [5 Q7 v3 Z3 r L3 A
接着会进行真正的保存。通过$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文件。8 t9 c9 W7 X: t4 E' L* n
) O+ T9 C \- m0 v
6 s: I# d3 i& T& Q# Y: ^/ S 漏洞利用
3 K6 m, n# q I4 ?+ ^1 u3 }( q' y5 ^) E" F: L
, j2 V1 t$ N7 }! B+ F2 D
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
" \. ~& j/ e9 ~4 n1 g# G
9 T1 M2 c6 `1 s/ e
* f$ N' A! n6 f L( Y6 Y) ^
- a% m1 Y2 [" H: K ) A0 k! i0 M/ N* G4 ]6 J
" z* X. \* ~ O! x. [4 u; n 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid6 w0 i$ Y' b, B9 e. _
& F1 [1 ?: }3 R% f6 d
! J5 V; l* I$ K! ?1 s2 Y8 K* M 不过实际利用上会有一定的限制。
# F; p9 v+ X8 Z# D
2 f3 Z$ f, C$ U, q) [2 M( ], N) z" J/ q
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
: O; q. z8 Z1 S% p+ j
: v+ I5 b) f% u6 P: y
6 i7 m; w' M+ o
* e' y- g4 t- u3 M9 j- T
( I m" B' P9 @
* n. m4 q7 Y% A$ _" z5 F 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
2 ?, \1 J1 Z) Z( A . z3 m5 Q9 v+ T& R1 W$ m
& P" W, R& ~; g
省略...$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]);省略...% V& T6 p/ h$ T0 g
2 M- v5 N% w$ S4 V, S) S) K; }- h! U2 X
因此要利用成功就需要条件竞争了。. C8 \* p* N/ \
6 D3 N# i2 @- M7 {
; Y, l5 J, F/ R 补丁分析
. Q. T/ z3 \4 d% ~& `! e; k8 J
; S- V. C5 K) @8 B! t) Y1 E" ^: U! N$ m9 J0 F; q( d
$ }- M0 v' j$ k
1 G/ o" j \# O" `. m/ l3 Y$ Q/ A2 i& I* w
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
6 @. q9 @ E8 V
+ a% N f$ ]& c! Q8 X2 K2 t1 `9 X
( R: Y! |: t0 e function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
4 y& E; m/ g" E$ R
: h. r, D1 _# G2 l1 g( [; x1 M1 u4 I; G# ?, @) S) S9 e* u
/ b* M9 m* f. Y% i$ a
B* e; y2 `3 u4 X2 z# A
2 f& e4 i' b2 q8 L0 `3 D 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
- n3 r9 _! a+ _0 m
1 ?. S7 X4 f' M% G9 \* H( w
$ L' G& a9 q! g0 a2 O6 \5 f 在is_allow()中增加对$this->savename的二次检查。
% c/ X r3 r: t$ F {9 `# K % P1 L1 K) B$ g1 v- `. Q
* \, t+ y5 \' J3 |( S
最后
. ^9 z7 |6 R5 [0 {- a/ T. }" X, |! @' v5 g+ V. R
2 V. @% l- j/ D4 W5 t, ~ 嘛,祝各位大师傅中秋快乐!$ B: r, J; p) L. r
' z5 x. o# V( ~& q4 S+ K* j
: O5 a6 e( G3 ?7 @ # Q4 e3 `% }( g' |* o8 M
2 Q% Q2 O" \- h6 D( C( U% ~
|