" R4 G1 I$ Y* V
0 @. e0 y& K. @$ b- C& ~5 v
$ b# `5 _) {$ N+ u
/ N+ h" u+ c# m% ?% z 前言( k9 [& E6 ?: f2 s/ E, Q
3 K9 R* i- R- H. c, p0 b1 f/ W- G% Y, g7 t F
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
; s1 t: D0 O+ {3 ?- ^ ]: H
; l7 Y" _- L" z: S# m. D! O: G7 ?1 T8 o1 E) Z& B
n5 T( S% U. [' q* c8 g+ r3 S
& K( {$ b* [' Z) n/ g. o1 b0 c; C/ ]6 y0 C. q/ f
漏洞分析
& c8 B' E! R, [% M
" T' G; W% c- M) X3 Y+ u5 {6 p
9 [# a- ~5 ^; [6 N' A) A( q8 ~ 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:* ~" i- C5 ?- H0 m1 H, X, \* y
* t" |/ l8 n: |+ U/ ]; h8 L) X; P9 E3 q) x9 s
" c6 S; ]( t z
- j7 }$ [8 ^! T) a
2 ]9 F) c. m& h 对应着avatar.inc.php代码如下:
. b' l: a% a* A l" |& v # [4 n' q. s$ L) J6 |
1 s& T9 o" s5 n( n0 L# E4 l) @
<?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) {% _3 i( L' V5 a& ?9 z
' V' x7 M2 O5 Z( O. s
9 k* l7 Z' P; Z
case 'upload':
& B! q5 j, f8 ^% k( x2 e9 M
8 A( s4 O( ?- h8 n" J6 \/ k- D
/ o& l( r/ T2 }( X" S if(!$_FILES['file']['size']) {
/ A8 _; C" g5 N7 ` ; D- k# |2 q4 {
: b: B% \1 k6 Z3 N% [* P if($DT_PC) dheader('?action=html&reload='.$DT_TIME);% B4 ^! y4 e# Z6 Z! l+ ] O
* X+ [; q. Q1 h# Q6 `$ ~
' z& m2 {- j* B" P. Z exit('{"error":1,"message":"Error FILE"}');
6 O; m6 H$ |) u" @# |$ W$ `
3 m9 w! F1 S9 u3 q3 w
6 @3 ?8 S N* x' `4 g }9 ]; R5 J( P/ S" ^3 b
, Y5 Y! [6 ^# x; `% C2 |( i* k( l( r* E& {5 T2 D
require DT_ROOT.'/include/upload.class.php';: I7 ]4 D2 f+ L. m5 o. V
; t# G B! s. n! K$ |+ L- z
3 [8 N! F) ^+ p1 A5 B9 l! n
* \* A& p3 b# o- O% P$ m
$ H S3 F5 D5 q4 B$ j& s% c4 v- e* t! T0 F
$ext = file_ext($_FILES['file']['name']);8 k' Y8 { Q) e/ z5 G4 }8 v1 x% I2 E
! w, V3 w! Z# }0 S
! D5 E; [4 N" `+ R9 h, H" ` $name = 'avatar'.$_userid.'.'.$ext;+ t9 Z8 ]! y, @2 u X/ y" b
' J, n' v3 `) X8 m3 ?% R
8 v" b4 {& `" |% g) J2 L. ~8 g. V) a
$file = DT_ROOT.'/file/temp/'.$name;9 \9 J- B3 N: G& l* W
5 n- _3 A2 d# B
, O0 F/ E7 m4 Y5 B 2 t2 C) u, B) ^0 \9 k( v
/ V( X# c3 S1 c4 x/ ]! S# k3 N* q
0 ]% `1 W3 O" b z3 {
if(is_file($file)) file_del($file);( f7 `9 _! l- E7 ^' U
9 _+ Z4 }8 o: F7 u2 ?4 `+ `6 _
% @; t, c5 ?9 r L0 E
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
/ |9 x& y J2 c( c) U 3 v0 j: g& g6 a* U( f
0 u9 _4 s7 }- [2 a* a' h# u0 H
: l; d7 \3 K2 x# a7 X$ H$ U$ \) v) O . \6 V! q; h8 Y
2 o" n% H; G) D! ~
$upload->adduserid = false;7 j$ }- {2 D* f' w
5 U* N) J- \4 A0 P% N! ?$ s: F
7 h& y( m5 Y: z
) Z2 T4 u7 h0 }* x
& f, p! [, P4 ~/ @
0 h- G5 E' W G: d" l% j; E4 o9 P if($upload->save()) {
" P( j: b( r4 P3 O
" w/ N! E+ u3 R( Q9 M) {1 {. h- M( U
...
4 A6 a) [# m+ s: e. m: c# v
' X0 ^. l X. b3 {+ E/ D- f
4 D' _* l& t) T8 u# F) |, g6 @ } else {3 N* i6 ], U1 i" z
8 L" D7 E: Q/ m
$ l. {% y: s5 |- Z$ w0 ~6 Q5 T ...
8 S2 V( I( q5 j4 u8 {) k ( Z4 a8 ` w4 N' G( {2 C
0 y5 M6 ?4 H0 @( H6 p2 l
}! s0 `. m7 C+ s/ J1 W; ~- ^
" {+ r% T% U' K2 D, Q- O
$ m$ N* I3 P7 Z% L break;
" L! |% Y1 h. v. i- i) c 5 H! V2 A+ B6 f& g7 B i
; u1 `3 K6 Y1 c# J K' o0 q. V7 G 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
) a( `1 S3 z L* a, d( F# M: a
* e1 M7 U h. K Z z
- p+ M+ l+ t4 N5 m/ B0 t/ f7 M upload对象构造函数如下,include/upload.class.php:25:
4 Z: q) n; L. t% x 1 V5 O, _ Y2 G) T9 J
7 F% E+ r( u* X; y. s
<?phpclass upload {
0 R) n; h! r* }% R* Q1 v* e) O G( ?4 J$ w* @. L
* ?3 \4 u& C7 B' ]1 }9 r( w, A* b function __construct($_file, $savepath, $savename = '', $fileformat = '') {+ z0 k+ @8 N. S0 o9 M0 y( W) k
- {0 _. J7 Y) O) z1 u7 C5 q5 e! N$ @/ f
% ~/ B; y0 c- N3 C1 y- w
global $DT, $_userid;# M, d0 ~+ L! ?3 J/ J# f7 C
( h% _ c* F: l% W1 J. e' U3 p' l" |
$ A8 t, q, }- w4 I foreach($_file as $file) {
% Y% Y4 J) e6 C9 [ I 1 n* R3 r& K1 S' K4 W
/ b# x; N3 D/ x! e" e( }) v0 A $this->file = $file['tmp_name'];( c/ [+ g+ r+ e7 r- L- Y
( S- `: D* C0 o* L
% i0 @/ S5 J1 \$ V; D# q w% m" C& x $this->file_name = $file['name'];8 T7 m7 P+ _8 K; M) C; |
7 E) _ M$ z& U3 C4 m
+ K9 `4 x; w5 W4 R( V/ B $this->file_size = $file['size'];
4 B* d5 C3 q: ^$ O$ e ' w" ]* h4 O9 P( I2 `
, M7 }5 l, B3 n A7 t2 r
$this->file_type = $file['type'];
* j7 d/ ]. x* B: j! o $ c+ F* K. q2 `
2 g" }6 r/ n" i- A% u
$this->file_error = $file['error'];- e1 K" y- _$ Z( f: L( u- P+ ?' J7 H
9 b5 ]/ |' d9 z$ {* G( C( U# D$ x* k6 f$ d' y1 q, P5 |( s* d0 m
; ?1 I" R5 e* l6 b4 u
- S1 J) }; j5 |1 M' z1 i6 v
( w) O" v# l7 E, V* z }
2 r/ C8 ?3 B" Q; j( X) A5 {4 n/ G6 Q
6 q# ^. g; Z' K1 v$ r. @
" \9 W: I( a: @1 C $this->userid = $_userid;/ A* m% S3 n/ L3 q8 n* P- `
/ k- n* U8 H2 R; `# v5 T( M
* E8 g- y. _0 X; ?- d( k. f. @7 j $this->ext = file_ext($this->file_name);4 Z4 x$ o- x* t. L
! f3 j A q* r ~* R, q: p; |5 p- ^
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];5 K* L$ ^) V, \: ]* P8 ?) U9 B
1 O+ C& W# X; o& K6 y! A
* t8 s0 y: N4 }6 [1 g$ \ $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
( c, A4 {- o* p& k7 t% s ' }. @; C1 E! v: U
! }$ c8 U6 D4 B( \( q' X $this->savepath = $savepath;5 ?2 B) v# p5 U6 a `
% Q" L$ K8 t0 n0 t
7 H V% V' u* i8 c& O $this->savename = $savename;
1 r O. v a! l- i" ] . w' j* B* l B" t0 y M( u! F
) b; P7 G) I g1 _) G' i
}}
2 S, I& U& W5 T
; U7 G7 s* ^! X8 A% S6 O0 A5 Q7 {; ]5 {, a
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
0 l' s1 x7 y4 r ' A; H4 v5 E* d7 @7 e/ S- l; t l) E
( V* g+ L6 O0 l$ d 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
6 K* d4 b/ c: f4 H( |* J; q, ^# ` 9 t, Q, K& _7 V2 Y$ u
( N4 l3 C6 }. j% S& V/ N: 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
1 J4 @7 q' x$ P' A
U7 R) c/ t& V4 X9 ?. ^. A2 u( ?* E4 W. m% |$ }7 D: h
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
2 G! u: {! [- ^0 z1 Y2 |$ q: t/ i
2 [/ e; p: W3 X& V s6 G1 z
8 D9 C8 Q; w+ w
+ w" o3 i4 b# z' F & { Y& v$ n9 _9 {; i( ^$ e
/ G e i8 J/ z# I, Y 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:; {& v5 ?# u% @, e2 d) W# p/ n
9 q" Q% n9 y& `: c
# m# Y2 o; ~- ?3 W4 r! ~
<?phpclass upload {, g4 r! A: A3 W6 f1 t1 u# G, }( e
( T+ h" s0 V/ v6 N, m% z& V6 \) T* C9 t# C% W- E
function save() {
7 X0 L* ~; _1 y7 j 9 g8 h5 x" w3 a f0 d/ i8 f \
- f; \& Z! i7 \' N9 L
include load('include.lang');8 \4 h" I8 @+ Q2 w9 L5 k
) |4 h8 S0 D' C; Q! g+ R2 X5 F$ P$ f( @! V+ m2 H" u6 M
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');- ^: E+ \3 [3 a4 ]8 p
3 w& D' U, d' [8 c
) n+ |# Y$ ^ L L& z* w$ q2 v! k! J B) S" t" {
: S& \. g! Y8 a, g' s* L
- _3 c- J( I5 u
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
9 O& Z% G- s# N# q 5 |: z# c# k" M- s; a
6 q) y w+ U( g7 t 4 \8 h }% ~: ?: x& r
: I. h, Q' X1 N& R+ c: k( \% S
+ \) ^, x/ {) e9 S) i; A" r if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
8 y+ [1 F9 W7 C. |8 I& c- r4 T; g) E " f {! }/ F+ X- M2 L. ?! l5 \+ H
- _2 G( R: |2 D 0 ~/ D9 m, j$ d. Z/ w3 P" l
/ C6 Q$ R, S$ L2 m
) V# j" v! U" ]* d
$this->set_savepath($this->savepath);- {. Z* C6 K' v" W
$ w, S0 A% w) _% ]' s" m
* l6 \( x. @% l+ z
$this->set_savename($this->savename);& Q6 c" V* l' s ?' X1 _
{3 }$ ^7 T% ]1 y' y/ I1 @
, U! x6 L. p$ {% T7 E* ?
* j( ^+ G* C+ U/ h" p6 e
+ Y! w' {. ?% f! v% m* G
* Q9 d9 L2 B7 D- d6 d/ T) Z if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
3 _0 b; j8 h9 g* E" H% v 6 Q! V' I5 K4 S; v* D% Q" t# o
, j7 W+ c \. a, D% V' e: C3 u; ]0 o
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
/ j1 ^5 ~( j- {' [: d5 C
5 X0 e4 }4 Q- S; `2 N* G, h
9 {% d3 H1 `, {7 G, y9 Z7 O2 V if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);1 r# h( ~$ X8 O
1 ?+ a( h0 A; {, E3 k; O, L
; c/ I0 P" t' p# h
1 G1 T* q! g" d, t
V7 y0 k6 Q5 N) e5 o, m" ?/ L: J4 r: J: o
$this->image = $this->is_image();
6 k+ h! D! `$ ?
1 ~, [3 |7 i: j$ w8 }+ s
) Z v0 B& p) q5 k4 F+ C if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);4 u, a. e% d! Q; Q! T0 h& q
4 w( W P: \; o: d
: s3 r1 R' K4 m" _ I3 r return true;
- r$ z) a# O; l& {$ ?/ s % N# P, V- O- d
$ x; g! ?& V/ e. \4 A
}}" T1 j* ]5 h, d3 ]! M1 T
8 Y: t7 c, I1 H, o3 E( {' @ V8 o
+ x9 `0 D: t$ A" |/ @" y 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:7 z% H: }! ?# Q. Y9 Y
- Z* k: e) d- b
3 g& B1 I+ a5 g1 d; z. s2 W: R: Y <?php( ^ p" `! b$ _! e
; u9 x: K6 Z2 @% C l* Z; Y" g
* C/ V9 q1 b1 I% j1 F3 J function is_allow() {0 j3 ?: x; t& i* u
) d1 u9 S1 D" j. U7 z4 R: }# q. o2 B0 P5 `2 L
if(!$this->fileformat) return false;
+ u/ h, l* I3 B! E; | 2 G1 j* S# S+ }, B) V! i' A/ m" ^
( f2 r. l! @0 a5 A+ Q7 J% l
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;! ^$ R$ Z- _* d, [& @
+ ^/ {" U f# |& `2 o% d1 R5 N/ C0 {) n8 z6 _& {
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 P, ~- K* V: i # q: J3 T7 D3 y. m7 M
' {1 l4 ~; b( z
return true;
5 Z1 B [4 h) p: R ; y, O* J) A6 i
; x7 K' x7 _& e" k
}
( N# A4 S, a! O! T& o. ~* t9 y+ E a0 ~ " V; J- l9 Q0 W& ~
9 v5 P, ?$ x% f/ r# K5 w. _5 N1 D% x 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。5 w8 X8 w; w* n/ Z
7 y: F6 `9 \5 g9 E
/ ^0 c2 r1 X/ n! L4 f 接着会进行真正的保存。通过$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文件。
" E6 ~% G4 D4 H+ i9 ? 0 O- C0 i3 M; ~' y' \
5 \* ]) M+ p4 Y; Q9 r$ z+ y/ \/ _
漏洞利用
8 @) J( D2 N) G, }/ p
7 H1 @+ r9 f6 r* d
( J- v. Y( c/ a' | 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
1 D; h! \0 x/ T! d
0 h$ B* Q) H% P: X/ ^$ R+ O, K9 R+ _% Q5 ~2 j' i, E; R
/ E$ Z) ^; `6 O- l) z% _/ p " q# Y4 U1 Y; T0 h
0 l* D" d2 f5 ]5 X0 O 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid& w3 g, n" j* {; c5 T! b. Y) e
- I; U' |4 u9 u( }8 x
; ~; H. r8 ]3 n4 N, _0 J5 S$ U' A 不过实际利用上会有一定的限制。
* Z9 t: z) Z; I: b8 G. w, d
* R; y3 g8 l1 i* e6 s4 t
9 s1 q2 S- |' @' {, P' ?9 G4 U 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。% @; u! m1 K9 a1 L( w
. I4 k$ W' w% j7 S ^7 x: O
% O) s- t2 V6 i4 A# Y) `
( l$ ?% o) m8 b9 ?1 A! m S ' v. k* {% @0 J+ ?7 G
$ V) [! d5 f/ s P 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg: A5 j$ D" e; o2 k, t; }) ~: A; C6 E
( K' |+ |& a6 c! M7 I
' Q% V8 @; U/ x% b% i 省略...$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]);省略...# Q% `% Z" |% ^4 s
% N, T! f6 \; A1 H
& p+ U/ ]& z6 j 因此要利用成功就需要条件竞争了。. Z8 |4 _# t2 y8 o, v- l- k
6 w6 C2 r; E( X2 t! s& S7 Z/ D7 O) N* y5 T( q5 f9 q
补丁分析
8 A+ L8 t; |7 Q5 [4 ]1 K& U4 G; m' g: v$ C
- {4 s" d% w9 i
) w% _7 G2 b) y5 A% p3 Q, F8 s ! j3 j. ?3 \! |' [
3 l j1 b: g* e o1 T6 u9 Y$ ~
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
. U4 K/ t0 ^9 H; n7 c( ?3 _2 k
# k/ ?) e0 C; T q, M
8 e* _' p/ P& [3 k) i function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
: W# ^/ X% C. R 2 {) F5 T( w, {" P6 ^
) C2 ~& h& o8 A, I
) H% X( w: Q- y. y( [1 p : R/ Z" \. ]. _( j$ e
8 r' ^2 q, q S% S4 h; t, R1 V
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。6 x( P1 U5 o8 p) F) r8 B5 T; @
9 B6 U8 |% X3 t% N) E3 [7 b
% j# k/ ~7 \: t& a' b 在is_allow()中增加对$this->savename的二次检查。0 e- j: Y/ y1 t
3 W% i$ D5 y7 L* n# u6 Z# z `& N9 j2 ~ q. O: E& }
最后1 x* f0 a9 u# r# M
8 v- p& a9 P" K, P; |
" O% X/ B) o* i4 P
嘛,祝各位大师傅中秋快乐!
* L6 Y# k q" H/ a! s" x 3 L$ e' U) M/ f- ]: x
1 M c5 N0 Z# G 6 E+ q8 _( M8 y# j) h) D& ?1 a
2 o4 p8 K. |7 m U1 [$ `
|