8 }/ o$ N5 E7 {6 N4 Z3 T1 y/ y0 l G3 }+ Q/ x0 P
1 I6 v+ d/ ]; h$ |& H0 D; n, x I- I9 t( ^4 Z/ J1 e
前言1 |; ~* v( a H1 u( O/ [; b
' k0 d3 Y7 d) q& o
. E. O! i9 \$ s 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。1 O! u4 v! g, ?, \/ R
\" W! i8 e! J* o5 [4 h2 ^
/ ]0 O( U7 k, ?5 v3 `: E& ?# ^
$ W( \. k' H! m9 S; j' {5 D. m ~9 i
+ a! p% j. @8 R) j6 ~
+ @; l/ P# \6 a: |* h3 c" @, [ 漏洞分析0 R* u# l6 z% c' M' D: N' V
" m. B- ^# U K7 x
0 s |, p/ b& ?- o& s7 M# N
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
3 {. U& N ~+ S) \ . M7 T/ ?( O& ?: O' r6 y/ `
: B% l1 {' |1 { m* h, b
# X/ m; P/ }$ @2 `. @ 8 p$ X* n6 X9 U( c& E
- ?7 d4 \ v+ w 对应着avatar.inc.php代码如下:7 L. q5 M' I1 Q9 k. a) |* l
1 y, {5 V- d- I" r4 \8 z# B; _
0 q* u E) Z* r2 c' R% d% T+ w8 V <?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) {) l% i% h- D5 u) x
! h% K* _5 E& n
# O& x) @) |6 D9 g2 W case 'upload':
& ?0 w& a0 W+ t+ H- N
* [8 c3 B6 W3 ?7 C! g+ J# }6 i* ~0 A, x
if(!$_FILES['file']['size']) {3 I' m4 q$ q: r6 E, l* a
1 P$ e t$ V0 V* G0 M. x" n2 o
% l' \4 X+ a, ^) @ if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
, k& |& P7 }) w. c; }. F. T 9 N/ d4 n; Z' p9 k, T2 l
( u/ g- S% P$ i3 R: I- H exit('{"error":1,"message":"Error FILE"}');, l. t. h) z2 ~1 H5 h3 G+ C3 f
r; {/ V( C% L# r* l
( \, ?% P& S) E& u# u }( l& z" c' ~8 p3 e) J" `
( ^, b* q% d( b( T' _$ N9 z: t' P
require DT_ROOT.'/include/upload.class.php';
' T! F; y) Y6 n! t
2 w: }! ]% N; c- G6 `! h0 e* v) |2 ^4 r' D- V) `$ V) a
) V5 w5 ]9 F/ z( ]- ~
) {8 d, `, n; @( A% _$ x) v
/ ?4 ? ~8 h2 H- D7 O $ext = file_ext($_FILES['file']['name']);+ D' L. H, Q" n" J" x9 u! p( G6 s
3 G3 V) E4 ?: ~
0 |; k5 B, P6 F; h0 B3 W! `/ m8 ? $name = 'avatar'.$_userid.'.'.$ext;
% w) U) L9 [4 [- m3 ]- ^: U# D6 O9 ~
$ b1 K7 S/ l; b* T0 z; O& Z* R( h7 H5 }2 n: a( E, v0 I2 J
$file = DT_ROOT.'/file/temp/'.$name;
/ h9 b2 A$ v3 Q8 s $ r( P( m9 @' I W, Q
" D& G3 _6 L' N1 }
+ h! V9 Z8 v+ u& M# t
; E% `6 x( k" T' m4 H4 U- I m) Q3 T' V1 B9 t. K, O9 S5 l
if(is_file($file)) file_del($file);7 O8 \6 j# f' t5 w/ ?. H, ^
2 O! E/ J% S) h, D4 @7 _" } {$ L
. J7 N9 o% q9 l( T8 n8 M $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');! a* ?+ J0 c: N9 F
+ y; Q+ S' p! U: e8 ^
0 O* ^7 z5 e& Y: W0 N5 R( x* ^. S
* |/ z; E/ A) s ) A- k# ]. c1 r0 \# e
9 J5 A' B' z$ Z $upload->adduserid = false;
+ a' j# g# u3 T/ Y* \# f- b 8 r- f! H: Z, t) U
6 B2 e% ?/ w: n1 d3 a
$ y8 C' E% `+ ]
% \3 N/ m& a/ W7 g
4 n3 C6 [6 `) v9 c ~ if($upload->save()) {+ `: V& [, E3 j* n# a
& P) ~( X; A- I" E9 @
' Q+ ?- f' E; e l
...
# \5 H4 j f3 V$ D7 n+ T1 X 8 ]. C1 k8 n5 k
- i. r- l R4 `5 p& c1 ~ } else {
; b, [+ v+ B7 ~1 {' N) ~
$ |2 ]9 E5 |5 X- S$ q- P# ^9 [
' s1 B8 R% ~5 u4 Y- @% l& C ...
# K- y( x) u" g6 @ 8 ]9 o; T# a' n
% _1 G! H) v$ U @ }
; R0 r8 ^& u5 d$ C' ?1 ]% X " B+ p0 W) `, H( `1 i9 z( G! m; P
8 U! K4 i/ g+ J" B" X7 q break;! ]: l6 v( ~% F% z1 N1 y; X( y
) N" g; j( I8 }
" k- K9 d$ e. B6 G/ p- O! W6 o E 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
& |2 W( G, J9 ?3 h2 k! u
( r! K7 Z. B* R$ |1 E4 E' E( V
* I4 T+ M0 Q _! y2 D6 x v2 m, ^2 W upload对象构造函数如下,include/upload.class.php:25:
7 u0 G- ?, ?/ e
) T( \" _& b/ x. N+ @6 L" S" _
# d9 a& }/ C& s& e <?phpclass upload {
- i9 J; b. Y+ ]. a0 k! R
6 G4 O4 Y& j5 t) X
2 b" {; Y9 S, m# i+ [* e4 s function __construct($_file, $savepath, $savename = '', $fileformat = '') {& H/ Q2 z* n4 x
y1 s1 K$ ^4 B' C$ g0 ^1 }0 K& P* L" F! b
global $DT, $_userid;
9 X$ t* j5 ^0 ?$ X 7 p' _* d) h2 ~' J! F& |: M4 q
; G' l+ A- {4 M. x5 H2 l* C
foreach($_file as $file) {
- t A# w$ S$ [
8 s, \- p7 D/ I9 w4 [4 x: \; V! }- L' \. O
$this->file = $file['tmp_name'];
0 D$ M( g$ G% W" }. ?) h O
, y3 }1 X; A: d+ Z* U# K1 K8 z, O- y# o2 f, W# F
$this->file_name = $file['name'];! W) c+ @' R: S
, j% ]' c" B1 W0 u# V2 ?9 c* @4 m% f8 C9 ]/ w* q
$this->file_size = $file['size'];
4 M8 ~/ V. G* S; ^5 {
+ q1 v' Q6 w2 Z# n6 U
4 D8 @) P8 \. X1 S G, Y& L: n $this->file_type = $file['type'];
4 }% K% q5 f* }6 E4 C( ]1 S " ^) H3 D. I) K. ?
4 n2 r8 n% D6 g
$this->file_error = $file['error'];
6 _# A: F, C5 k' Y1 O T8 h
; e3 C. V; J$ u t2 @& R; e+ F5 Q8 m5 D
- P, o* \9 _4 h# T% q& O/ y8 h. W) ?
: n8 ~+ B% ?5 z% N
# f( A; z T' B, R4 o }
3 n$ V/ Y& l$ T* G* W" X8 c
9 v4 W2 t" R* X) S4 ?
2 b( q8 j8 E% W5 U $this->userid = $_userid;6 O5 U: r3 T- L9 n
' d" _$ P+ q1 B o! H. ?0 z T$ P }, S$ I' i8 D* J$ X
$this->ext = file_ext($this->file_name);/ z/ o+ S0 {0 i u# j8 r, y
+ n3 e8 D0 y, S/ |: _7 Y7 a5 U1 c7 ]3 B! b/ O. r5 J
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
' r0 n6 Y1 p0 ^8 O
1 o/ A0 e/ s# y% m
6 [" U) e4 v( p0 V $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;( X5 U- V5 |, L; J& C7 X/ Y
5 l$ W; b* |# t% {( A
! ]& L6 I5 U6 a) Y- Q+ O
$this->savepath = $savepath;- A- t' x. Y; z. d! j
# [8 q7 H& c. W0 |" P5 G
2 T; o( Q" k2 `: H' H, `2 C& F $this->savename = $savename;
" `" j D& J5 e9 \
; o2 W1 i* T, \5 ~; {# N& J6 s
2 w/ R' p) W' @" t- b }}
! q+ }3 o3 g8 h 0 X& R' ^" Z/ h: j# Z6 k$ ^
- C* Q; E! Q3 g( F
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
7 x5 L% ^3 N, y' u 4 H5 T! _- O5 k
W- d/ r( Z+ ]! z" R$ V 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
! ~9 }$ [% V* i* s g 6 X& q4 v# n4 D: [! }
1 y5 l0 O1 {$ Y8 v- J8 \/ ?
$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! S/ x, j& V6 ?7 D% n
+ i0 S* |+ Q# A4 r
; I# F# L! A7 ]! l 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
$ R0 J+ i+ J: f% ~% \. H
: h1 e3 q( c4 P5 p" |
) B1 c/ N! `- x2 x+ w; N: p
- _: o" h. z: m1 T2 R
! z! J7 U p' w" M- b) b3 a4 Y
+ |6 C& I) U, {7 _# z' W6 C 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:7 S& A% g' R/ j1 q& v1 G6 b
! Q# _% K# j6 e {
8 ^, C2 ]+ X. N% g5 z
<?phpclass upload {! O, a' j, F4 a: x- H+ E
6 f" x2 k( _/ v# F
S [0 I+ M" v function save() {" z4 ?7 ^% E% P9 |1 m* x
* o; w' a+ {" [6 J* _% Q$ |4 C
8 M: O9 T e* T3 a. E include load('include.lang'); ^) h* g$ W. ~8 \) N* D
1 [9 o+ C4 f; U3 t5 ~$ \
, N" {) H$ _0 {5 Q* k) i if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
4 w' _3 Y% G) u" @' j
- R# C [, c! ]. l R+ K, T( ]+ `8 D. ?1 B
7 e' N9 z4 a0 \
% O- ~* r8 r! a) P% L! u k( Y
' Q( ?/ g/ j% O6 d7 O if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');0 l, t: [! e6 b
8 O: N; ]* z- ]# r( g+ @7 P
0 `! ^$ @* q1 f$ Z, n9 i1 \
7 h$ y$ T" W8 t' z8 n0 _& e+ R ) N7 s$ |9 c% `$ @
+ b% n5 C! K1 p1 Z; T. |, K& ^ if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
2 o1 D8 H. }6 D8 l2 \# C, W: h. |
7 s1 E' J4 b; }/ W+ D9 X& c$ y. J# g5 D
6 R/ T. D/ b7 q) ]5 X0 A4 t% Y
$ r) r7 b# d r% ?2 G
5 r; `2 L1 z" E0 ? B+ w
$this->set_savepath($this->savepath);
3 d, c$ R4 k5 e, N Z
1 k) @) J8 } u9 {6 D% k
5 E j G) r+ y- g$ r. T# ~ $this->set_savename($this->savename);
0 s# b ?3 ^) b: e
2 ^7 L) e) i' D$ _ m/ x& G+ i
( r! W$ l1 \! @6 j( E 7 L8 ^) Q% u; t
: u i* L( M4 Z, n4 I. \
7 c0 p% \3 b( e1 w% u! e if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
' o( R% p* ?' w1 y0 O; t4 b& i$ U
2 m7 w) I& X& ^/ R; H' H0 |0 C! g
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
" E3 F& t9 N) R) r
! l: P. ]$ y+ |/ W( l+ }+ C3 q6 C& _# z
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
8 ~8 d9 k; W6 p6 A8 \
8 m9 q0 g( M" m# ~0 c* S9 R! l5 A- {. N) T. \& E) ~( J
$ O) w0 w, k$ L+ ^, Q& ]
3 l5 Z8 d' u2 H' H9 l( x2 |
# n' d7 q$ w! } $this->image = $this->is_image();# p- f- I7 M4 Z; l. U7 V
, o2 f) V5 Z! J$ w( n
7 V. M# f- g$ d) e
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
! h0 p" Q- H( P4 j9 ~ ; W( _& F' c1 r0 v L4 J
: |1 ]& S# c3 {- I return true;
: _$ X& |$ z# V
! }7 H$ L+ [! Q$ [, N) C1 u2 f0 L* x( v! f8 T1 |
}}
* C/ J$ d! p; `9 S
0 A7 p. R3 D3 f( e" x: s' r2 p- T; H
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
. P+ `; ^" w( g3 [' K$ s& ^
+ g! \0 M1 O0 u( O
3 v8 I* D6 G" v' f K& K; o <?php
0 m# [% b# |% N/ T
4 p8 E, n0 `9 C/ `: j* k# M
% {& n3 J9 X. }% T! x function is_allow() {
! R; q9 B. \" \8 p
8 t. t6 P, o: R, |8 E! o
: K' m5 i/ k% w if(!$this->fileformat) return false;" P! s* w4 F+ W! ]6 z0 u
7 N- H6 D" J8 ^3 r
1 b p7 ~' N* M( s# T8 |5 M( T, i if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;+ w- y- r3 @. r; G
. J7 l0 ], |" v9 v! q7 P; @6 p& ~% e, ]
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;
5 W3 _5 R2 \) z5 q/ b4 r p$ f
D$ t! J+ _9 D' S( _1 O- j: b
9 @' u$ N, @/ T" d8 E) i return true;
- ^( S$ q6 C# F" T3 b, |
- a" M! R. t: `, Z0 B8 [ u) q
# y" y; k' B# Z# K. F5 \ U }
- L( X# Q0 M# d0 R' w% C" y
/ }" w9 D* Y Q. ^% ~4 r5 |4 p, L, F, n/ @
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。; r* z9 Q. T. ^ A% l$ ~1 G
3 J$ l1 K5 k0 S& ^0 Y. d
, O4 k, j; [# W2 \; s 接着会进行真正的保存。通过$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文件。
% b/ ^7 E, @8 Y8 u7 V/ _; Y0 W
* S' }5 H/ M9 Z9 [" z/ m8 R# ?; z. |) C
漏洞利用
" N4 Y+ o+ {) A) h9 G% a9 y* b; y$ ]6 |1 K2 {4 d8 j/ s, y
1 m- v' X+ c+ ]0 h. C) s
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
* _( F3 D+ @8 _; J# { U" _ + W; D6 V @' ~* `
" i) o8 i. J" \& S( h
% I$ S9 O) P$ K1 ~7 W5 k+ o/ I a5 A
+ C4 R0 i( G& R# Q# c; Z0 R
% T' m. u6 H ]4 [0 b5 C/ Y! h! P 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid/ A+ s+ J) V/ o6 S, I+ N3 B
: f" \! V9 j; ]# Y! W
- c" l4 M/ \' H: K$ x3 U0 Z 不过实际利用上会有一定的限制。
) t! v' n* @4 M1 M& o
h3 O7 j9 i7 K) b2 }0 E: }# z+ B* x
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。+ _+ @5 o" q( o1 Z6 J
- \7 k, \6 z8 ^3 {9 b1 I l" D) X0 B; z9 Q2 z
& K# ^7 f+ u3 \3 u& ?" \. w+ P
, j, J* X; o2 ~# I- x ^# u( G8 N7 I4 Y" i
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:/ F$ a( T0 |% g6 v* a
: }/ X) \8 \/ {
2 f3 P9 P% A; {
省略...$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]);省略..." d3 `. r0 m' G0 L$ x, P) y
# N! U. d/ S3 q0 }2 |9 L$ e" }: n- g. F
因此要利用成功就需要条件竞争了。1 a" l: ?! i) V' B
7 [, o6 k( K# j6 u+ B! w/ B
% i# W1 F% R5 t, s9 e 补丁分析3 [. M- O% J% E i3 Y% Q5 \! H% n
' b; }7 p+ F1 T& y# ?# ~1 _7 f, Y6 j3 a$ C5 o' G
' Y, I; x$ W! v( T ! `9 N) g6 j# u# _& _1 G8 m+ N
2 _: B* L( A( |/ q2 \
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
5 R0 u ^5 o3 `0 {
1 _) l* F$ Y( }9 ^. ]4 T5 M- H6 V4 e5 W
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
. I% H. @ F8 @$ N, @ . O1 k% w4 p8 B2 G& X+ f! a v4 w
8 U8 w" e9 V# M8 O6 E 7 C3 g% X; B6 z% q0 ^8 h8 m
1 S! w3 f( ?4 D7 L) v3 j
/ v9 O3 a. a. b. |9 K* j 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
( S1 n [7 [8 R2 m( t* A! ^
J8 |# P3 ~1 c4 }' Z2 |3 K
! X4 ~/ o& @# f k 在is_allow()中增加对$this->savename的二次检查。
& D) H2 R, d: R4 z& H$ @+ z
1 K2 ?3 U* ?+ N* x: G, } S+ b/ f. Q9 A8 ?! S
最后
_7 O" A0 X7 H( e# h: I; Q5 H# }4 W3 }0 ]
$ _6 v( @0 w% T" a, j- B& q/ k; o 嘛,祝各位大师傅中秋快乐!
& t7 b7 ?4 H# j) I) N9 r+ k& D' j: J " S7 @2 [( k7 \! G2 K
8 y, [1 y6 F9 n# M3 M- F, G, p
( T7 L/ b% M7 B$ F$ G2 [2 ^
9 A$ M& e" f& r: g. E4 x
|