1 i# y! q9 b4 Q% p
$ J9 h8 z+ [" T7 l( w r2 w* E; l( u! B7 ~
2 {' n- m+ m; d7 B: [0 i# T
前言
% P6 c8 F0 j3 j# ?' |' V/ w9 r8 ~# o5 U2 W% y% h( y! W) y* }: E& y
0 A& `1 S( ?/ ~5 F1 n! X 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。 R. T2 V. N" K1 R7 ?, }+ ~2 C
" m/ Z. _0 [, f! c8 ^5 `' x$ k
9 w) }4 f ~& [; F& p
% U, ^8 G% g# K5 t! T0 _# e7 Y! ^5 O
4 m3 c# S8 R9 u& }
4 t; x) y# j1 G 漏洞分析
% Y+ Z" l# ? C# b- C8 D% z' d1 M
% R1 R2 ~+ k# K% C' l/ t$ @
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:- W3 G, W! k$ ?5 Q9 O
o7 E- M) F( V$ _6 [
, x) I$ G- ^6 @, m4 L# ]2 Z
7 {; g q+ }5 b8 f! r, D( L 9 e7 Z4 R+ G( Z$ ^: a
" V; y. b! t, K+ S y% T 对应着avatar.inc.php代码如下: r0 k# R0 f2 k
- _8 W q0 w3 e6 P `9 k" O* S$ [- _2 d8 h3 _6 m
<?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) {
; i3 Y4 ^0 W! Q / k+ u! L$ E$ t) b
! Q( L7 P1 f* @, p- p case 'upload':/ c5 H& g' y) V4 C. K9 e5 b
+ f6 H8 S* l2 D7 D! d3 }
- a8 V7 q, Q7 O) N! l/ D if(!$_FILES['file']['size']) {. |. S' `1 h. J$ H( E8 {
. a5 t2 a3 @3 N0 _# P
# {! j3 w5 ?) u if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
0 \7 m+ l9 S6 i
4 X9 r+ Z" M4 P U) i, j& j: X
- X( v! E7 b8 K, K% B, U3 Z exit('{"error":1,"message":"Error FILE"}');
" E& n/ G. ]/ c. o8 s' G1 _; W
- @! ~% J) s9 }. R0 _; P
, a+ U* O. G7 x! I6 {+ } }
- j' f# ? z; i8 G: g : U. E* x9 [1 t
& }4 t% g2 Y3 P4 V! q# j
require DT_ROOT.'/include/upload.class.php';8 K) t7 |4 D l( R3 f4 h }
) E8 k: F. z8 \4 S6 f `9 q. W5 n4 f, b n W- D
2 x* F& V2 s8 w& C * z# T0 e. ]- B
% _6 o- c+ F: S$ V: q4 \ $ext = file_ext($_FILES['file']['name']);
. x0 X% Z. [0 B+ Z4 _- B/ W ' P+ i& t+ Q5 w% d$ J" ?$ e, t
# J; c) K$ Y7 \* K5 [
$name = 'avatar'.$_userid.'.'.$ext;
7 s( L- Z0 }$ V7 b 8 T5 B4 Y$ T8 _7 p$ D! a3 S
0 T9 G! f. I% l/ C% X $file = DT_ROOT.'/file/temp/'.$name;
8 h$ R1 G1 V3 U* f5 O* C$ Y
/ R3 B. n3 [& ^5 y" f% {; o' I: J2 P5 _/ A0 x. j9 o# Z1 o3 a
; u, R! W6 D- F3 f% @3 r
* Y' \2 \7 u7 h2 p9 i# o; t
9 m% j) A- q* q/ L) @3 e3 Z4 U if(is_file($file)) file_del($file);
# z; b6 |; t* t6 C & N% H$ ]9 {; X. q. W
0 `2 W; E' C% \+ X* J- F
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
3 B4 F7 w4 e2 J [& I# q) A* j$ _
- }3 g! q/ d; u) L% y6 [* f7 d+ z! u' V1 l
; y/ k' [: G( t 2 s3 b% B1 J6 o! o
8 V/ l2 C' X1 {" D- K ^ $upload->adduserid = false;# b' U4 Q Z9 _9 A9 D2 B
8 t0 C+ K: G" S! K
! l G& ~7 s3 Y 9 P" {+ |, |4 }6 w4 W
& Q& i% ]* x% N- Y- U b+ Z* i, ^ Q; k+ e. v
if($upload->save()) {
# F( v4 \) `# _3 T ; U& H( b/ L3 p+ _ N
# p! L; v0 l) X4 e
...
% n1 B) j& K' t2 j4 h2 w : m$ ~2 m) M0 [* F
* C5 [( y6 L0 a" L4 u% N2 c } else {
; Y ?' |/ }$ ~; N" l. b m, l: \ k4 Z- `& u# O2 [
/ _- s) q: t; h8 j5 |4 T! v; ~ ...5 o; Q2 |5 h( M J# Q
. d; r! |3 o; ^7 E3 \" z- _
& T! K1 a! }3 y7 h; W }0 ] e$ x) r& ^. c9 b0 U4 h; @
- b D, [0 c* D, E5 d( ^% |2 Q
: S4 p1 d. E1 [! t break;3 r2 @1 V2 [2 k: ]
8 X" D- L& y( H5 Z9 Y6 n T
* o; p4 q; \+ Q! } q/ G 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
: X0 L8 o$ m/ x 9 s; x0 m& _9 Y. r
0 R% ^2 k( q; {# F upload对象构造函数如下,include/upload.class.php:25:( M# Y; } ] l- P
& ^2 o# p# R. c0 e q. J; Q5 u3 ]# n) h, `
<?phpclass upload {# f: W" ~4 e; b7 q; h* k' [
, k. ]9 m, D* o N# A$ K$ Z2 c7 A; w: T' o4 u
function __construct($_file, $savepath, $savename = '', $fileformat = '') {1 A, s6 w6 b% b/ A/ i* d
" m4 a4 X. B2 `$ H& S" r# J' h- d/ y( h
global $DT, $_userid;/ { h8 P, C" y9 K8 `% z
& A; ~2 K2 N: e) g/ n. S# m9 b" T3 Q _6 F
foreach($_file as $file) {
& y5 b# S* F$ }) v: h3 n* f & U& }& G: f) l+ G+ w$ T
; t8 p; p# n) M1 w3 r1 n $this->file = $file['tmp_name'];
$ C/ w" @% G K8 F3 {- l ) V0 D( H; z% \: E
, b2 U3 k2 A ^- ^) x5 W2 v- {8 n! D $this->file_name = $file['name'];% Q0 L0 \3 ]) v) K/ L7 @" }9 A& Y
0 h* l! F$ ^# j: S6 ^# z2 \; B: {% j/ r, d% K
$this->file_size = $file['size'];; b/ J' b, g' Q1 z4 J3 S$ _
3 V8 Y& G& s1 Y3 C: e: ] z1 K
% ?5 M; F# z7 D1 X, i/ ~3 }& t* ^3 }
$this->file_type = $file['type'];+ ^+ L2 W9 t+ G$ y8 \2 t
9 { L1 m, E+ I) [. a6 X, ^. {5 {% o$ R& F7 }! B/ O( Y
$this->file_error = $file['error'];- \8 L% I3 @: M( X1 L7 r- F2 c
3 K5 J. D9 N6 P. ~- g, ~' x0 w% c/ p @( x: F$ h X
! {8 H0 X3 w( c4 |; _3 f
3 K6 X7 Y3 [4 g# I6 X1 M, q3 M. d. S! U4 I% M$ Z
} o7 b' u6 \) d$ r: k; Q
1 i; x- U1 N, M# Y
9 y) y: [9 [' g$ T4 p0 n0 N) c
$this->userid = $_userid;
' L0 V4 `' d+ b: A; ^) S. t. m 3 i3 {% p1 Y0 D) O
' r0 x3 l( y4 F+ a% x6 a $this->ext = file_ext($this->file_name);
1 m1 R! \! [: ]( R+ y # p7 ^" b+ S# n8 l; _; G
0 T5 k+ K/ \) r: ?: r $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];8 t1 K1 ]8 ^4 n( D: t
- u8 V) n+ B) F" g3 m7 \/ p4 F
) k3 t( R4 j& F. t) T% Y# { $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
1 Y* u' N9 }! { 1 `0 E4 U3 i* S* G
/ i" @$ V: i+ {6 k* X $this->savepath = $savepath;! T! x) W, K8 T3 m2 M
' H! I1 C- [3 k2 E- K# [1 L
, C4 C' M/ H7 ~2 j/ w+ F3 d $this->savename = $savename;7 B# l% c5 i( b3 }5 G
& f5 k0 O$ ?* J; P- d. j- {9 e
. Q' g5 Y! h+ x0 A
}}
" r, |2 ?( a' u; l 7 t+ R* k o0 S5 M5 p8 ^
& T- T9 @" T7 S) O: g- N# E
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
6 B6 ^' C* `1 G2 W2 W/ `" K
8 f! y" f5 v4 q' P, A- ~1 Q8 V( J* [4 q3 P. f; G3 f
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ! f1 {6 | d! F: n7 O, V
: I' T* t% k1 R Z& W8 @/ `/ ]
# y6 [, a1 }: h/ a( `) o $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 m2 C4 W6 U5 D z" t: S
* u& [ m3 \) i+ w
' W. e# u7 {3 Y) y2 P7 ?6 | 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:1 n9 i4 u `, l) o+ s
# B) Z1 l: O8 R' s1 W; v6 M) G$ U! T# }7 U
- n- t$ L/ N# B# e D& `4 A( m
/ `& g, {; _9 W& I
+ G5 z6 A, t5 ~5 _
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:" N* p; n$ b' c: Y
, x" q, w; J; M; Q( [ Z- ~ h& H- L- y) `" M3 H/ w
<?phpclass upload {3 ^. K: g. K# D" @8 k8 k- n, x
8 U, c# e) X) H( M6 ~) x! G0 R# z( q/ y0 b# O2 S, S b
function save() {$ k: b9 c# ~- N4 o9 c) u
: Y e( k, M9 Y+ v3 @7 B
, F1 B( J. W( x) s# q, M# D3 S include load('include.lang');4 S3 |5 G6 Z! Y/ _" U0 f) a8 w
% m- L7 L3 ]# `% {
4 d7 y9 J2 Q. K9 P# e if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');1 c( _+ W# g; z& s5 K5 J Z% \( k9 M9 ~, C
: s8 W4 I/ [4 {: x2 m5 P
) n: w/ }" z! d& ]+ v
- f @/ K# x8 r% {9 H 2 {; o, D# ~/ m
! m1 J1 c+ f: H# s+ y
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
5 e7 m& b7 G {5 n* F( @4 J 0 B6 V, P, u) T1 s, o9 U
/ ^- }. H+ V, a4 H q w
5 t3 ~' O+ S) o) h d; } + l) C: l9 b; I5 p4 ^
' a* g( J: A/ m i' d0 i, U
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
% q6 U' c2 a+ A+ n ! X# t. v0 \9 n" ^8 v* |6 X
1 r1 |) H4 ~4 k
% b. e# Q! y( Z6 v
9 K1 A9 B6 g; M0 |! }9 }- e4 b4 p8 E; T+ F4 |7 ?) [) d( D1 m
$this->set_savepath($this->savepath);) k7 G" n: t& G1 Y: t4 o0 e
9 {* k1 I+ T! o0 @; p' _
9 r0 u5 L# o' c# V# j7 }: c E$ s/ @
$this->set_savename($this->savename);1 m% e7 e6 O7 g0 r+ b. @
0 H3 B- n! d( b$ s7 |
% x+ U+ x3 @7 f+ V$ n
# L1 z" d% ^6 U" y V! G2 x1 |1 }1 Q0 N5 o
" O" v0 h- Y* J/ k* X: \& `
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
8 Y9 e0 p7 [6 C' B, \2 k6 N
; l# B# |; s& f. N% G5 ` o* `" Z0 z" @3 O0 q" a. x6 a
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);) W* d6 d9 _$ t1 Q( o
8 O/ w* ^! L$ a3 r
- m; I0 i9 Z; R: P
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);% {7 y$ D# X% a2 t$ H7 O" U$ \
6 p1 }4 ^* Q# }# ?3 N
6 Z+ Z+ b( x$ `0 |# v" H
; s/ F( g9 K# D9 v% M 0 X. O/ S1 X: C R, o; ]
9 B9 [) I/ G C' ` $this->image = $this->is_image();
9 H: ^' [% f6 \0 ?
% V# H/ N' J0 g6 V, u
" [( p* X; t7 Z4 c7 [; n if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);" ^0 h0 b3 t8 m, r! y% v( M+ E
* H! `- `# [1 l1 u5 E; }) e4 c. }' H; f
return true;
- A# b. ?. V' ~4 ?8 b
( y% L; |' _& z' q7 Y( U5 a: T$ `( G3 c3 X# Q
}}
. ]/ l" U3 B2 t5 W 5 b, N9 S. h& ~2 y( i" N* w( ^
2 [4 w3 m+ k. y6 W& C$ _ 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:0 g& t' p9 h& u6 z; B
) P7 e4 m, A( {
+ M O9 o* H& \" |; [& R/ @, J; W. {
<?php X* x7 z4 N$ y& U# v) M
@2 Z$ ~4 s! |' N- I: g; b5 n2 V7 L4 }) u- h$ y3 ?, z
function is_allow() {
$ m: [( X" k8 h O+ O' l- r% h
5 R8 p: i$ F: }* @! k# k( k* s z
if(!$this->fileformat) return false;
9 Q5 W# Y v* q3 ` |
+ E0 l6 Z8 ?. I3 U6 Z
3 g9 |) [, J. h' b6 k7 \ if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
. \- e( M$ n' \! h- s
' K+ x$ w: Z8 H) g% [" R% L% i1 O
6 F" C. y0 |' b# j: D+ r) ` 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;; ~$ o$ g2 c2 w. t; X9 I
; r9 `3 J0 ]7 F2 b) r/ m7 [7 b4 ~6 p$ C% Q8 Z* U9 l5 L
return true;( _5 J [6 K8 k6 T, b
+ ]" ^! S. {( P! h% X) [$ T
5 [6 @. E$ s0 Z/ o1 K- G- S }
S% T# g' w7 h$ {7 B
$ ]9 P+ V' I4 _& R) ^% e6 V$ m# e' o! X
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
. W4 p$ z) i# X& ~! k# X" \ * }( O. B# W# j8 h
: q* Y1 Y5 {, Y- f2 N; L. ]" p4 _ 接着会进行真正的保存。通过$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文件。
7 u$ q0 d/ j6 [& z4 ^ ! L0 i4 f0 s# h. t# W( ]5 w- e8 E
- m' s+ t: F6 S. Q- B9 N8 b
漏洞利用
r, n$ A6 V2 W: N9 Y, C9 F U2 J' t7 k9 d) `, J2 L
a' ~4 a2 V; A. ]9 N% [/ R/ G 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
4 R% X) S. T; a( o& ~7 i7 D
, f' i8 [, x/ x# u: F% @+ I% |7 ]3 x3 P
5 D5 l0 m2 @( D# B7 v! h + W* s4 K" m$ n% U, H+ e
9 [# x3 q: ~) |2 b0 R 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid: B& y4 R5 s! Y$ k/ \
& A Z( W2 _, T( `- A* N$ X
( g# \- ~% z5 x" j 不过实际利用上会有一定的限制。
, d* g) \, k! _( h7 z6 T7 t7 y6 L- g
2 A1 U4 x5 \1 \$ W. C0 m: r# A7 S# w, }0 E+ W& T. Q
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。& M, D1 e( i/ T( w
* W$ e7 ^! X* f2 i- k: u) [9 O6 ]) d
, i _" {6 t; C" Y* S7 R- r
% F( _; K6 L5 V: Y w+ b( @) W' d
* V3 a6 @5 F0 u9 O/ K/ t7 Y3 [* n 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
+ w+ }! B- E! f% v
6 L. z2 k! K! J/ C8 }# R7 ~( N. L6 g- S# @! K, ^9 g$ t
省略...$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]);省略...2 @" b& m7 ]) p8 z" M9 c& R/ d. ^6 \
2 {6 r [, o; r- j
# ~4 s0 a2 T/ a2 V$ { 因此要利用成功就需要条件竞争了。8 Y, w# n) I1 I. Z/ E% s
3 M8 V1 h) v' a z
! @7 k: l$ q% M: }+ v4 L+ g) p 补丁分析
( D% J- {. w5 b5 H" s$ ~! w( _; @ \+ {3 w$ m O( L9 J
) N r. u8 Q" E. ]% w: k. D( M
3 J( A# a# z7 C5 D1 r# c
! F; D$ B% A @
* h2 v/ Y/ U" J9 Y. h0 n" q$ E4 S 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:* m1 C# m- z1 g" e
) X, T/ n: @+ O$ H# o8 w" r4 L H( V8 X7 y. U
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}; M) x1 c% y& q. B
* x; {# u: G3 t) F6 q% T8 T1 I
$ `/ p5 z4 Z% `& w( ]( m
$ _! t1 h1 }5 I9 v" u0 `: f
: q& U2 V" d: d9 L! @. y# M
0 j U6 @# r0 \1 q' e 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。- a9 ?, V2 {1 y, U7 D% Q
3 J" p: H( y' O! W: p" }: w$ J
; n3 O5 x+ G% }* E! j" v6 |( b: o! c 在is_allow()中增加对$this->savename的二次检查。
+ x& }3 S h4 B \ % ^! ]: ~" k5 J
+ ~3 H0 I8 i- b+ d9 @; H2 A 最后: h, p1 j: B/ K( X. ?; V0 j
! k0 r; S4 o, h0 R! M" L8 A
" b2 Q1 g* i7 i0 I 嘛,祝各位大师傅中秋快乐!
7 ~+ w% Y# o6 w
# v* }; p" ^( {: Y5 V. a
5 n4 _" m3 T: v6 H ! `) t1 U$ c7 \3 n: A/ O7 |2 u
4 S1 F- J. M0 p% ^& X7 s
|