3 {# ], N. _" K; ~
: Z$ G4 p; Z0 T7 E
j+ L. j- \0 B2 p" y$ }' k- [8 p- `$ `4 S. L3 k
前言6 c5 }" G% C: i- X
( k7 m, H! v$ n5 M
" P1 P. M8 A; N4 d 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
; ?) s0 P* |* U5 \
, @: O1 ~% H- i1 X; P
0 y4 B+ Y! l" A3 w- C
# v# U9 H+ g% y4 { u0 M) e- A & q" A9 ~" Q* U- T6 Z
1 l9 W6 g7 K0 y* d" d. i$ u. ` 漏洞分析- s' ]* Q! _1 B/ d- z& B
- U% e$ O* z# V
/ T3 F- H# H& Z: o8 ]( K6 Z# j& v 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下: N7 l; I/ R6 \' c% x
, x: N0 p5 C9 O9 i
- F7 ?9 {! T7 E( @" `* L 4 e0 g% x0 O5 P7 H3 _. ~( ~" ]
8 w+ ^4 w# {% s* i: h9 @
, w2 Q# p+ f; H. u- Q( q! |3 L 对应着avatar.inc.php代码如下:
3 ^" u- w7 e( ] # u& P4 |4 G( f, V1 W* s
. h& F$ V0 `" T2 m& h! A
<?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) {5 V9 ~9 g9 [( A1 O( C5 J7 \
/ Y* q1 {# ^, z4 Y, d! G# |
8 `6 @, Y5 t& Q( p M+ c5 V+ M case 'upload':
& j9 i( w1 W9 V9 Q4 P# t! `8 {6 X# p% o/ V! ] % ^; ]- D/ h9 T
0 p9 r& E: U2 }" r, o. P if(!$_FILES['file']['size']) {0 ^. v D7 S; V- n/ R2 j; X
, q' {! ]* T& j3 W+ L
5 c, D w+ P/ O0 b7 t: M" u, e& _
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
+ B4 ~0 G3 w6 ?, o4 y- F G! o ( b% q+ O! G. N v
' s1 j$ o# L0 b( y0 |1 B
exit('{"error":1,"message":"Error FILE"}');
r/ V5 l( D) Q: v- m- z. E6 _
5 R" R' a$ T' ~. \0 }# L. A7 l! W5 w6 e1 z
}
* Q- W0 t. M$ m; u6 S * _0 K0 Z0 p. \- Y3 Z7 d% X7 m
, Z( ]! N& w8 S4 k6 N! ]) K) Y require DT_ROOT.'/include/upload.class.php';
* Z" T) x7 q4 i; }( Z
! `" a4 k' E% H% l" N# k& J# n5 M$ ?3 t6 Y; h4 }$ ^2 C
- e/ M: m+ g: v; ^
" s/ o/ z; K& B, a) e* X h
/ T( n5 V6 ]% B$ A/ J $ext = file_ext($_FILES['file']['name']);
6 O6 q+ J8 M/ x. g! W: E
7 V+ Z" r* y# K5 {0 {; r: a, }/ Y$ M |4 d9 {# E
$name = 'avatar'.$_userid.'.'.$ext;
, _% x/ D9 A1 |' A( D6 e0 F( ~ 8 U/ }4 M k+ A
, F F) {# `8 \+ L8 f+ E+ T% k6 L
$file = DT_ROOT.'/file/temp/'.$name;
2 t0 |' z, r7 H4 U. I; _* e
) c! ?: r% f1 R3 ^+ j& ]" n. H9 m' i
$ j3 a. m6 e u" u; r ( T0 p) n! u/ f8 F
. Z6 t1 q0 u; J ]$ ~8 v7 ^
if(is_file($file)) file_del($file);% C6 S9 r6 s+ O: V
) y% w N$ [ W
! w: V' c1 b3 t. ~& ^9 Y $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');$ ~: P/ q9 k: u
; `; b F0 O" q( F/ _
2 B# e2 K3 o+ w5 F, i8 W$ T
$ P& O: x; U ~( N- h$ S+ q7 j( c' { " d4 z- X2 v& r# m/ ?
$ o( ]5 B# V1 h0 a9 R1 b" r6 i+ \
$upload->adduserid = false;
8 J4 ]: f1 L { 4 d' C* O) a2 R/ C
3 B! o+ B( X+ i& G1 y4 d
9 B% B2 V( |1 S$ F( M3 w% G% q 5 q* M/ C+ t# b8 v) X, H& W
$ q" y- R7 U) A% P
if($upload->save()) {
9 h t! A0 g# s; d* R* H
p$ t! p$ Q9 D4 S
; M, k) v- Y4 z1 e5 M ...
* b$ D. D$ q* S* Y: i
0 s2 e( \" _$ q+ K' r6 w7 l/ [5 K b0 R
} else {
! Y6 v. p+ Q* E c
5 F4 t) }4 q# h H* z/ @) _
% d7 n/ ` j4 O ...
0 ]$ X" Z4 ^7 P1 {' `: ~; h7 g
; a6 @+ I4 G5 I
9 X; K% o/ {& i }0 V1 E3 P6 X& ~5 [ ~
4 e |8 e8 N5 ~# B. i
( Q( U( _' S7 m' [- Z. `$ t1 e break;5 p; n0 w* n0 i$ }8 s% y
- ^ E& ?1 h" R4 u& x9 I3 Z
, L7 E7 N- W: a
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
9 C! C+ g) @/ j1 O0 X% O ; T) N' j3 q5 B. D3 Z8 v
4 [- s& O0 n% e8 K$ | upload对象构造函数如下,include/upload.class.php:25: F& C/ U' ^2 @
$ e2 P* y$ P2 A: W/ W/ U$ }( [$ J; ]+ t. _: k+ G$ |
<?phpclass upload {
5 h. w" K, R' y; n; D3 t% a + Q. D0 m7 s& B
O1 M9 o+ t0 Y* u; K
function __construct($_file, $savepath, $savename = '', $fileformat = '') {4 U" ]2 t" x8 r. y
. [8 P |$ C/ A+ J
e" A' Z- F5 ?* G2 C- A/ C9 |3 V7 G/ R
global $DT, $_userid;
! b: [9 f- s; U0 m! h
7 a7 \0 a& u" M+ b4 d5 _: x: L# P$ r8 |+ | l
foreach($_file as $file) {
) a& G4 _3 h3 z" T2 Q" p. c ) S3 f$ `1 W4 b
% N# K V" g p( ^ D; Z $this->file = $file['tmp_name'];
7 w/ @- T, W9 t9 v( a& F/ U% M! H 4 J \8 |% Y, W0 Z. Y- D. Y( ~/ }# I
& T" C" Z! h$ L7 S
$this->file_name = $file['name'];
5 ?+ e# a4 M: C+ G! a : F2 ~/ d# Z% h
0 l4 W- O- J5 U
$this->file_size = $file['size'];- _* r" e( i6 E% Q. r
/ ~0 ]; x+ `1 J
. a$ m) {6 Y7 M" { $this->file_type = $file['type'];: F) \& x0 s9 g; o% ]9 U" t
, i4 q1 A" P: V8 h
4 _* F! Q0 I9 v. @5 Q- i6 l5 t $this->file_error = $file['error'];
# w, L! Z! s* i' m" b: ? 7 ?* X b( ^. T3 E: ?
" N, u8 T8 H: T' u; D
" k) l! }# `* W & X1 V P2 G- h n
# Q/ k4 D. e4 a! I }& U) r: M* z V- o
3 z' v" o. j3 {1 Y' p+ F' V2 O
& g1 C% A& e+ o9 o Z+ ` $this->userid = $_userid;
. d. J2 u' y, k , C3 ~/ B) `% V, T
2 i" ^0 z- O" J
$this->ext = file_ext($this->file_name);
, m$ S; m& t4 } l5 s2 J, f( k : p( I+ d9 Z' m% b$ f
; d$ z( p. b1 O1 q
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];# K; ` I- I3 T0 z
9 e2 F% p- f& M4 I6 b
, M$ o4 i+ _9 y( T4 y* I $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
& X/ m+ ^3 f" f5 b2 o4 m k/ |2 h ; u; Z7 N. K0 i. _
7 d' v4 X6 i- G3 n. j $this->savepath = $savepath;3 O' z) h$ J' H
! U% c) z+ j# U6 J. B5 a! n! C- ]7 n& a6 @* W7 g! T
$this->savename = $savename;
( ?5 C! e, z. m4 ~* {
# H7 X2 w& C/ \& o0 Z8 L' K8 h% C0 X8 F7 R: Y! }
}}
' Z! o* ^3 u, O& I: b- n
) n* |) f$ I. f% w# ?2 `. I- `# P* n7 X' k6 P2 K
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
9 e: C" z% m. c% f. a) U2 O3 Y 7 C% q, _: |: M, w& I4 ~
6 G# B% F0 x y' K
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 9 J3 i" V7 n1 E# v; g, C
, M3 @1 P( D: }- ^& M7 f* y+ a+ q; @
$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
+ s5 s) }8 |1 ` " P- d$ x$ J* `: |' D$ C
: c8 K. E# M* P* J: a
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
1 e6 g; f+ O$ \. g' U) L ' r4 F$ q* e, @5 f8 X0 b
. u. t# Q% k) m' X: G. @& C
* Z- F$ i! T a 6 e6 u+ I3 j; } C* r
& A1 ?+ C+ ^, g* ]$ @# W. z
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:! H8 L& Q8 O) ^& C5 G [' r
! t& t! C' [+ E% S' v
h1 X+ L2 G, C! ]7 w <?phpclass upload {
; R' Q9 i/ Z8 Z; f0 p s4 j
6 O" [3 g# z7 S3 |" T4 b# X
" n' y$ X4 Z; M+ J+ v# ]; T* U% a function save() {2 {$ V- ~9 V& }- w+ n
5 R! D9 T# [, P- L% o, }
/ q! U+ E$ `7 V6 m6 @! D- C. U
include load('include.lang');9 _# N1 i8 F: u0 ~* u; j7 N
' i1 a/ E* C7 }# B4 o
, h; \7 W9 K+ |6 \ if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
- q1 J# x5 y* V" _2 N! a3 l f- h9 H6 H3 |
: ~ d, |4 a+ C$ y. u% b `
* F N- A* j ?0 Q7 E - V& R. m P, M$ N" ]; u8 m3 V6 r% ~
) t* m7 E; @7 W* x, e
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');; ~8 n+ z x3 ^5 x( H
7 |8 _8 W$ V6 K* B0 o3 S2 t4 a1 W( N; {) [/ G2 J+ `
4 o3 @3 v+ y# z" n# L% n, ] $ {, \* M8 b0 d `6 `
' d: K7 Y7 R$ q2 H if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);. m7 |, e" j5 n( a- x) r4 p: I4 @5 X4 X
; [$ b% h% N' |. r7 k1 M3 m# E$ v. N. }' Z) k8 I) A1 D
5 j3 V2 M9 y1 ?9 a. s7 u5 H+ v0 s
$ h2 s9 F; ]" K$ Z
, D1 i; {* l% K; i* J
$this->set_savepath($this->savepath);/ A f0 |* A8 x% E2 D. M
5 k# s) {: y& O0 x, l
1 o( I3 e, P( U
$this->set_savename($this->savename);; Q- _, ?3 Y) j
6 z& c, `% X/ k2 N
+ y+ T1 F1 h4 l" j 2 U1 \& E7 P v; V' D
2 ^8 _& O( Z2 X" i( A( D$ r
. }; K8 _" G; I+ N% i5 s* X if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);2 {# a1 U" t5 g3 ?
$ ?# h9 o( Y. Z4 o8 }$ ~+ M$ x
2 y4 T0 Q# E1 @# G* Q if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);1 i' A+ a" v/ B Y& w. n! e4 Y
. k, n* k1 Q% b! l, C0 n, P- d: |8 |1 I% M
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
' j: E/ ^7 |' m. p8 h' x
6 K m( e( T8 \2 j3 j: \) K# V9 x6 V+ Q5 h
% Z* o; n% F& S- ?* g" x* X* R5 V+ [! z
3 D9 Z$ ^5 {, y1 D v7 |9 G: A
- g9 F2 n' H ~( S0 h: d! M $this->image = $this->is_image();
6 e1 S9 X( X9 \* s7 W- Y
6 e% N3 W/ E# j9 n `6 W' k
7 E' e$ ^4 q0 ]7 } if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
1 s5 r1 S0 F" ?1 H * i$ D0 ~! t8 P! j I
, X6 T; j: e6 s6 P U6 S" |2 ~
return true;; q5 d+ Z, b1 E' t8 c& N
. k2 c; a/ H! o3 w5 K Y
# P4 W" B. g3 l0 M- N
}}* @/ }) z' T2 B! _& m7 ]
0 `2 S2 ?9 k# |4 X& H$ K
4 H# ~5 X8 ?$ q+ N 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:$ @9 x: y& U4 a
* j/ e( T6 o/ c% J
1 M4 W8 _ [: T <?php
! j6 T8 ]" L ~/ f- i |3 I 1 G% h" |& K) ]( F- f* u; s
1 {0 V& ?% T! a. e `, i: g function is_allow() {+ Z( O6 d- k+ f/ e
$ o0 {& j& c/ S+ k/ P- Y
n$ `0 w7 r% q5 _ if(!$this->fileformat) return false;
0 u' d T; h) y |' L. O
5 _* {0 L$ E/ L: S
! C8 o8 `3 U3 K- n$ j ?8 a if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;8 B& x, P* v. O8 D
4 Y6 l; v. i/ \+ }: |
5 G* y4 L& z9 H# z
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;
( u! ` l# w5 m 5 q+ `9 d0 ]) H( A9 p! M( N) S
; }8 I! N% X9 d7 Y. B- m8 v% ]! n return true;6 E1 V! [/ N2 B- `
" x; R* A. v. k" n/ Q
6 y8 N4 y9 i$ N& O } E$ d. b1 j6 ~+ v3 @
9 b6 G3 c9 D& G# D8 |- J% z5 C, h
B# c% X. `0 o; }6 ] 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
* E* ]/ V8 P8 D 7 ` f. ^1 \& T% f+ u) a. y! m$ _- b I
' {$ [0 K; F& z! W' S4 P0 K+ b5 B
接着会进行真正的保存。通过$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 M/ m# a# c. o4 a2 u 0 _; Z- M) Q( w/ J3 @! i4 a
( A9 H& n0 o: H4 `. @5 F 漏洞利用
( e7 u2 w+ Y. L) r. [" l$ [2 i" u; _' X2 _7 |( g% G0 B: P7 z
. a8 @+ o6 k" p3 v# e# x Q. q) o
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。6 M; V" Y! x# O+ V6 t
. d; K0 n' Z# ~3 q+ x, }2 `
3 O, a+ R9 e- g ) F- m5 W( g8 ^3 L- R
8 U1 X G, `; A/ R7 g
]8 N9 A: H: }1 F% @
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid# ` T4 O+ \$ n2 S8 x" v- T5 \, ~$ f
9 E% B! M1 j8 u' l3 z- I- s
+ @2 F* t3 s; _) j8 w, T1 C7 _
不过实际利用上会有一定的限制。
) _- G' J8 E! ~, Y 1 y( ]3 A2 ]( F# n
1 z P5 ]- s( s
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
' ]" w) S1 P. C 2 A0 m8 O8 v' K) @' [2 P$ j
" h: e' z/ R$ x% l0 G8 D7 B2 U. s
9 s, o9 s% h7 t! N/ x9 Q
- r3 q; n, }! ~. o; f% B& M4 e' F1 J0 ~1 s3 D% |, ~+ [
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:' h m: K" ~0 H' w1 j9 W( s
, B. S* i0 D& t& F
8 U2 R7 x- B" w, `8 S. z 省略...$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]);省略...6 E& n/ O5 P" T6 s9 M
1 A; w6 V8 Y; |# x' \4 q( T& s! V
* e7 r: I0 y& U' Z4 {
因此要利用成功就需要条件竞争了。
+ r* ?* t c, e
: Z# l- j$ z" y" c& h/ p
5 `& `4 r, P6 @! L 补丁分析& K3 k2 ?* |5 U( [' t6 V
5 ^2 J1 w: Z! D; X% w$ i- G9 I
+ R- V4 z5 A' F, c: o; C
7 _# |' e9 N9 z0 B8 a + o: w7 M$ e% T
^: J) G7 ]- ?. x& _- [* s8 |4 P 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
+ I7 W" ^( X$ ] 4 \/ h. d3 x3 x; f2 X
+ b0 m W& H5 s function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}' \+ X- ^! e5 M. {; D- K9 G
+ k1 H! Y, p7 T. t: x& _5 u3 T
6 Z# V2 P7 i4 p5 e
% k1 `6 H# X* E+ [$ X# | " i1 q: C% ^% ]! |: `9 Z- v
! F/ F% {! p. [! R% e) ~1 O
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。. C8 P- h+ Y, d( W! _
6 `& e; `6 p0 v9 _4 {# }5 a1 H& h% A9 z7 T' R
在is_allow()中增加对$this->savename的二次检查。
% [$ G- J" y. H
' u( U- o1 _. z* d. q' Y/ [9 \8 B1 m
最后
$ h1 n# j* N0 J8 G6 W9 ^$ j( N. R% U: ~6 B+ z9 ?- g i+ C0 ?
- S$ {. ~, Q! `$ l. s4 X0 e 嘛,祝各位大师傅中秋快乐!
" U/ B. B# ^# d0 | 9 v& n4 Q: H& W5 T. W5 l4 l
$ j- U' g+ y% @& Q3 d3 m4 G
* ~1 I& f- C9 g) W . |' ~# h9 V0 y
|