9 A1 b1 [. n% p
" m: q* i k# e1 E/ _6 @+ |
7 V- c4 Q; F1 t* x3 R
/ K) U# Y6 D8 m5 \! \1 L 前言
: \! R/ R: ?. Y5 H: i& y/ B$ n, d9 L1 n
$ U S/ P+ d! A) x$ P8 w' m 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。) Y% k2 K1 N) I
" o' C$ v7 @" @- O7 x
- [# ^+ L4 w$ h, }/ u+ C
7 l% Z6 P4 N/ e/ _2 Q8 R A
# Q; t" Z! g% o- e7 L: o5 M, J" N, n4 M8 n& m
漏洞分析1 R+ V6 g0 |3 Q" U8 z6 {/ q d
% `( I0 K, W7 s, t
2 |4 @9 K: j/ y# e; P 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
5 I" ]8 Y5 Y5 R 1 x; Y" g: _& N# R% L
8 ~/ x: x6 a+ s
8 v5 @5 x1 m9 L4 M$ }0 c , k1 K( K+ G% b0 B& O9 x! Y
6 j2 w9 b( ^1 [ 对应着avatar.inc.php代码如下:
1 Z/ `" T& A$ \0 s8 P2 [ 6 s3 ?! V* ^; }" a0 |' q2 v$ o9 u
/ u: \) H; n0 {, r) N <?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( h# ~( P" G$ ]( o " N7 P$ S* [2 K, h) X! ~) _# b& k
# V! k6 [8 D9 Z* C! s1 [8 N
case 'upload':! s) G" k( e0 I
# ?# G3 H, R3 e: t# p- m- ?
' X5 Z4 m! R' L" a7 r0 {# R+ | if(!$_FILES['file']['size']) {
; r( j7 z& o" S- E( ^! @ 7 O' d4 D3 t1 z# P3 Z, Z! ?
5 C4 o- o; Z" [# U if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
- A8 ]" p, }) w* b: G4 m. I + c9 q4 i6 \' b
* i8 X/ W" U3 ~9 } ?% G1 g
exit('{"error":1,"message":"Error FILE"}');1 U# Q: A* E* \; s, x
' @9 n" F+ ]' s& h
# ?7 f x4 l% y6 j. @- s
}
4 r/ T- U1 m2 q- [) C2 q' I % O$ @4 [. K+ [6 r2 N! C
9 r& q. |4 D" `) \3 v3 r. U( Q
require DT_ROOT.'/include/upload.class.php';! r5 k4 R! A2 \9 y. q6 {: M% |7 B
5 [7 `9 b' t$ g5 O0 T- b6 ~6 Y$ ?/ I; a) ~6 U
, g, E) ^( \* h+ E7 X9 P* q
' j8 o1 A R; c: P5 I. z7 F. S( l
; R! V' p% W% j5 q( F $ext = file_ext($_FILES['file']['name']);
8 L2 {3 F( J X ! ~ ?7 g+ s( V4 Y9 t9 H
5 W$ U: M% R/ g) E/ V0 A $name = 'avatar'.$_userid.'.'.$ext;
2 [/ ]- W) T3 J/ y/ Z- z. Z ! @# L5 {3 T' F m) Q( I
! \# j/ l8 J3 p" m3 D# w9 g2 i
$file = DT_ROOT.'/file/temp/'.$name;9 O, E; B, u8 W3 h/ X- R$ C
& c: o8 q1 g C. ~/ V' B+ u7 m
7 ~! |# K( g( E# M7 m. s. d& R
; X) B: S% L- j( G
5 t7 E# W0 t6 L- A- h7 [% C$ \* O. h2 O; t
if(is_file($file)) file_del($file);4 J/ Z3 V! F) D4 b
) o) J; Q6 k. m) N& e' o" H' \
% t. [( a7 W6 j8 w7 Y' M) V% n3 ]* q $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
: X7 n4 s ~# b+ N; ^
# }+ C j `1 r" \/ o- L. y7 D2 ~ _9 d- v2 b' j
8 | ]& V8 H7 t ]) R) z9 E . E4 h4 G# P; e5 `( U8 p& K
0 b h. F# _) l& Y& C' c
$upload->adduserid = false;: m. {2 N8 v- L' h; @, @% M
. Y3 G. H4 j8 o2 R0 m" B+ N
3 E: N* A! S- U6 e
6 R ]) w7 {) L% G # ]% Z5 l1 b }3 [
9 W" G: W3 A9 E4 C- T if($upload->save()) {
* O/ b/ U9 N% Q% h8 Z3 X
1 w' I; I: A+ }& S; l. e8 a+ v# H7 p+ J6 Z0 t9 ?1 S8 Z/ q; e
...
# T) j: r. a; ]+ j' t * r! d4 |3 `3 B
" J# Z% _; M, \% o } else {! s$ K9 o3 B* B% }, A
9 v5 ^( Z$ R! O2 H% v( Q- V1 n: Z& y' s' g# G3 L" W
...
2 e$ k, Z2 a; D# V9 }& ^( x
( j6 _. L5 ?3 i5 J* M; g9 g% d8 x, D0 ?
}& V1 l$ t0 ^# }' a4 @: a
, o3 E) W5 r* X! Q& B3 r& T4 s
: p4 x/ z/ F$ I6 q9 p% d) Q$ g break;2 r. ^7 K. I8 ?3 D' g+ ?+ Q9 B }
; Z, C2 f2 f: m+ o! U/ L& b* {* b# i5 }3 J6 }
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。& s8 A7 L# p7 Y' l
5 _8 t1 j3 _$ b! E
2 |- l4 Y- x* x$ F+ t upload对象构造函数如下,include/upload.class.php:25:
4 k$ l8 `* I: Q8 ^1 V b; H- E ] + n$ w1 n; |6 N3 V: N6 }& Q# { S
5 W- q' p# U+ J" p" D L5 w& y
<?phpclass upload {
( l7 |' ~& L9 n K2 Q
5 f& A( A7 W8 m( x% X; k2 l. z8 p' H- D. c* T- k# Y8 ^+ M
function __construct($_file, $savepath, $savename = '', $fileformat = '') {* m; \! {" L& R1 p, i H3 n* F
" n; F0 s4 }6 o) K$ D/ P: P! e5 W7 d5 s% {
global $DT, $_userid;
( }. u# Q) n7 |) B D2 R* q& z8 i
2 u* Q1 t3 h3 H2 n2 f7 c8 M+ U5 z4 {; c7 ^9 ~& l" V5 I6 C( S
foreach($_file as $file) {4 j0 [1 e; P( m! V5 F R
6 I7 j% _: _5 L& L4 t! n' |) E1 u |" ]2 V' w* R( n2 G, M
$this->file = $file['tmp_name'];1 Q/ U5 B! F) e( H, c; ^! ^
& ]+ k2 g2 y+ b0 W1 t1 |- E2 T& M' _7 E
$this->file_name = $file['name'];
" s/ e7 v4 T% b' u# X, E; N1 `( L 0 J% y1 c4 k3 \7 p
4 h+ ~) [8 G x& C) J# d2 p- M
$this->file_size = $file['size'];
* u; v: o5 R3 h 3 B }0 |% p) b, C# d( ~
* K6 g$ N5 X5 s( u- C $this->file_type = $file['type']; H$ J$ V) R7 b7 s
" c; B, v, J6 a- X% b# {" @* R
5 Z+ ?' s2 o( f2 V $this->file_error = $file['error'];
" b& S& k: b7 R" r
- r. \0 J# F' a! ]
- i7 A" O* @* q9 C/ @! n
, s$ N6 r! s- _1 w Y; c9 a2 H0 g. _% h: L! U. F
; [. g1 F% k& s" ? ~& ~" @, r7 v
}2 |6 f" X2 b: v% Q0 D9 P
4 Z! n: {. e( r+ w7 |- }% N9 _* c; |- z
$this->userid = $_userid;$ q/ @1 s q: X; I
_2 m8 d; H e3 G+ Z
4 i! {( K& I8 I- u" `( n- x @ $this->ext = file_ext($this->file_name);' b/ a" h) {" o6 v# h- D0 }% U- ~! x
% U8 y$ C4 ?6 f3 s
: g" c$ S3 W0 S $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];- \$ L7 ?9 r- H8 i
f$ }4 a+ v. ^ }6 l# i6 m/ h' p' h; E# k
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;7 s# [3 |; l" C9 H0 m! c& E
$ E' \1 @, m' o* k. N
; J; O% |9 `+ p' c. E* E
$this->savepath = $savepath;3 I. z9 l/ c2 P; U
; g) P2 \: K4 q, W, i x5 c) X2 m f* K8 c
$this->savename = $savename;
6 A3 {5 I# V# J$ \
5 T7 m+ Z6 X! j
2 `, g' Q; n7 i }}
* T6 }0 ?+ d3 `- e0 q+ i
$ o L% d0 y5 x1 a
6 \) H8 }: P. w5 U7 a& P7 @. V( v 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。6 \) R2 y9 o; d1 u
6 } c, V; J: `, g
: N9 x N! |# x# h1 g4 [
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 * i/ r2 ` K2 x
- _$ y- N' @& s4 O4 h" `
) ?, t& ]$ \8 \* S8 l $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
' d5 }! ?# q1 R8 p) F% `: y! H
1 i3 W: N0 p$ J+ c, [/ g# Z% K3 t
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:7 u# S: ]; o9 y }& c
9 e/ f) p/ E c. Z+ _" w! ~9 d, g8 O
( A1 N: U, r- [* u
/ X* C1 Y% w ~# v
6 d' J; d4 `3 H. t3 c; t* L y- A7 P& R+ B* I( g% i3 m% N
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:1 a5 b$ p3 ]! _0 k
% S/ Z: p% d3 X7 ^" o
. q2 g, B/ Z4 ^: C9 S& m <?phpclass upload {
% x( e: |& J3 B! S 0 s0 O' v! s( Q; e% L
0 H+ {& v( P% r2 l5 X% g* T0 K8 K% ~
function save() {6 I- r# g& ]. |! U, `. Y
) |. e" a" H1 N' @% d5 W* S5 y9 |( ?. e- D
include load('include.lang');
; N" Z$ {( W2 J 7 c7 S7 I: |6 {1 v# y: a
6 o% y. k+ B |0 }1 A Y if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
; l3 @ v+ t! R6 @ - z: S9 g) Z& I5 o& A1 [
. A9 I s' V! j/ E) z' p% _, T! A& D
# K, o/ T. _7 v/ s
5 \% j" R0 ~7 }/ r3 T* [
8 ~' x. p _7 b8 k; K
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
; B @# B0 \4 c
, A8 Y5 @; M5 ?4 d
/ F; J( }9 f0 J, B3 V
7 r) Q+ o6 {4 K3 K6 h, L
; H. }7 X/ c5 z5 Y' v; p9 W# [, Z( ^
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);2 E. B7 o$ d; j, O2 Y, Z% k
+ c/ j# c: S( D F! A
! m" [7 O% J* O4 p# E" } ! e. A. }; |9 z8 u$ k
! w; P* d- s/ y
- c! u; H4 @. ?( A: u; p $this->set_savepath($this->savepath);5 ]% w' P, G. [
1 n; U& q3 ?6 a4 }- M9 }0 V& C! }
* I* }* K$ S) F1 r $this->set_savename($this->savename);
7 M6 S4 P" Z- \* b# Y7 U
4 ~" c5 A& q" H' p4 z8 M9 a3 m" l" B8 d/ l- O
3 T' E) d% c3 S# h, R! V+ w; ~# s
5 g4 a, X, g. S, ]! ^9 g" r( b
$ O5 L% q/ q' q* O7 s. S
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
; i4 B2 z5 G( s9 x3 Z 0 ?! q1 G6 T+ j2 \0 n* C
( }5 U4 J( g8 _+ B
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
. _* g4 h' x: d
1 a3 D+ r) i" F5 a+ R: i7 b- S u* ]: ?5 R4 m1 [
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
7 G9 O, ]; I$ d( s5 G
% v' g$ I$ O; Z$ [
& T* U% D" T" I& Q! Z 9 E* q+ Z1 j5 v/ F u! s9 ^
7 U% H9 X4 t7 E& H6 o. x" |
/ G/ f5 P: _7 F! T$ k $this->image = $this->is_image();0 }1 z* j4 Q6 ?' f8 J8 `
- G# [8 B6 R( I; n# {
$ w0 i- k& x I4 ^: q$ c& E( R if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);" Q4 H; {' O7 P; z+ s7 q# ]
) U6 `( v2 c6 S8 X G, u! r# h7 o% o. Y( o7 {5 d ~
return true;
* c( R/ ~2 V$ l( _ & J1 H2 ]- g7 }6 J+ z6 F6 v `
# o9 f: j0 J6 r0 l8 s+ H" m }}' r V% l# T8 Q6 R5 I
! k5 x0 t" y5 N1 W9 Z# P- r+ C# A( `, d
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
4 t" ^: d) K+ c1 L. J, b$ _
/ W# f2 d, B: u- q1 Z* t# h
. R5 Q/ s! j; y! l7 I <?php& d. V; Z) J' P8 t' p
9 o( l( o/ v" l" V/ b
1 @ F( I A. r$ G function is_allow() {
/ _4 V6 E7 D; ]+ k; T 7 ]4 u- P% v( l9 T) J6 s$ p
& }1 u& D( S# {. s3 q& K- @ if(!$this->fileformat) return false;
- T. ^ l- T. b5 A
9 `& B6 T' _7 d+ i! b: _7 g+ k( ?: c2 P
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
- m* E" W) u$ q0 ~
* Z1 @; B( u2 h" z5 W! g3 H& ?# T
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;
- p8 P6 ^# W0 Q t% f7 ?6 }! o
/ v' {' _: q( D) ?6 {9 T2 l; @ O5 q, p! E) z0 U: R* w) Y- @
return true;$ F3 T* z6 R4 N" z" P: g
& _8 o4 _) W" Q/ W! g, h
5 d9 n: `, g W2 g% I
}. W/ r' P! V% a' t, e' }
- {$ ^6 c' [+ Z4 k
% k! e" L/ j4 Z8 p 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。5 P% g) T! M7 C8 }! ~6 f" ]9 E9 S4 Z
5 D/ H/ U$ W4 ^+ J" A/ N6 v, n( \
0 ^( B( p2 _0 C1 R9 V6 K& Q2 r
接着会进行真正的保存。通过$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文件。
; ]9 N, ^4 s# U
% j' Y$ Z4 l" ^& j5 T6 w: s- f" T- H) @7 s T
漏洞利用
$ \/ ~% x1 J- u4 q/ o
9 D5 B& j1 w) C. c
5 d( O+ E4 }5 f9 u- c* x 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
$ w% k T; H) D X8 @8 \0 X5 x $ p% u( e$ m8 V3 G6 Z
4 S: w# G5 z0 t* W
9 f% |: p7 o, {* K
+ _8 T" S. N8 C+ A: ~
; e# H( J& W/ c; Q 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid; Z7 a r2 v0 J c# \ V
6 @$ F& T) n# u* b$ k; U
# i I, L8 o! b 不过实际利用上会有一定的限制。# {3 F+ I2 ~5 c# Y/ V
2 G' v9 G |( B8 X
5 A) K6 G) i% s$ a' [4 u9 I; {
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。* p7 e: x1 u% R
( I$ ~' _1 M0 |( e
+ T# t& b# C2 c
3 Y% z% ~+ a9 x$ M6 b4 I$ K. X
# Z. c$ |. j. o) G0 ^
) X6 b9 _: f" l' o. a, b V 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:, o4 q# j& P2 P3 n, v4 |, Y9 L
$ M+ A3 U$ v" P0 B0 s
D6 r8 V' r5 U3 b5 D* V T; M 省略...$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# y' H3 D" I7 U0 d2 Z
k+ E* e5 l/ |: z; @
1 W4 |, a7 a1 L+ C 因此要利用成功就需要条件竞争了。
9 D7 d0 m2 D, ~ - o3 y5 F; P8 z
5 V5 ?/ o9 j9 `$ q$ Z$ h( j3 v 补丁分析5 ]5 {7 |& L. g8 @; p) ^ Q1 u, L, W
5 | j0 r. n" w3 S; C2 ?, g3 o7 [
0 X, e! \: o. g+ Y & Q$ C6 t1 |4 y! O+ {3 M
/ ~+ q$ t$ H+ O% }- j* x n/ D! K3 H8 P% U9 E' v \
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
& \4 a' C# N" u+ e , ~" A/ ~4 ?( T# z, w) Z
! o' P- Y% F3 M1 M
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
6 R% Z$ U( ]: @* e \ 6 R4 U; V! t. v# Y0 T
! H. [7 g6 J4 h9 E. c: k
; [! s r% {/ ?; C8 O8 m
; T8 g& s+ r) O5 o) j( I: h# k
. M5 U- m# u/ z9 w 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
* T3 p$ O* c# d) o( G
: Z9 m, t: w( N, B8 M
% |, p( F# L* Q _ 在is_allow()中增加对$this->savename的二次检查。
" Z8 h: @& F2 y# J
: O0 k3 B- c; E: d, I) ^% ?1 Z' U: I
/ | _3 _" ]8 [5 w! Q5 n ~ 最后; h) j Z. Z- G9 S
" O7 ~# B5 A7 Q q- u* Q0 D4 w. v
) k! _$ v* v, s1 l
嘛,祝各位大师傅中秋快乐!
- L/ ?0 b# s" L' r
: \) l" Q' X6 a6 B$ l q% R
- P: `0 b; \) h8 S# f/ | ' _( O) C/ A0 Z' N3 R
; A; z7 ~4 q* {
|