) q$ S9 S) M! j! ~, L4 a
, U, R: e1 s0 Q9 X5 q2 \3 T5 T; T
+ A6 N0 I. I9 j+ o6 O A% Z8 t
前言9 x+ U+ D- E& \4 l3 ?$ w
/ q5 M. L& e( j/ p7 m- \
. b$ u/ w3 E# `
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
) k6 f( Q1 |- z* I# a " \5 `/ f( S h1 _: t" E3 u8 g! M# u
$ F" s; T; {! }* C/ i# i. s( s! M
$ _$ ~3 _, g! V3 I1 T7 o. B - D `# V& ^0 M) _# o( Q
$ i1 M8 E9 c. o1 _. }1 E X
漏洞分析5 r$ Z. e" g5 x, i, B6 M
8 w1 d- S5 g" t) E0 G
) d q0 t* \: m 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
. s/ h5 v; u# U% T 9 H8 r9 W$ O' A# P" P3 b
- C s) O0 C. n# Y* q ; v; L: p1 y, r% x5 `# L1 U
( z4 F R1 V& P& G9 E1 D' [9 u5 Y4 I; N$ [3 \6 S2 e; Z/ I4 k
对应着avatar.inc.php代码如下:
" `6 i+ ^# x9 H. O* K
0 D2 E5 t' s2 Q$ B6 g% T9 D# o
- q% k, G8 Y1 @" u- |, _ <?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 g- [9 M! G4 `: W, R5 f: r : Z: K/ A7 q9 I+ H
6 L, a0 j9 L/ K0 ~+ ?; ?
case 'upload':- W4 ?: j; g, w. D
1 X9 M s! @" t3 X( ]: m* u) x+ M8 ?! w d) j/ K
if(!$_FILES['file']['size']) {
& w$ u3 a; v: u8 V, Z + N: p, J' {* b8 K [$ H7 V
! V4 I% O" B) I1 t
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
( I) `2 H u" _# r0 ^
9 v) |# I( z9 A, S6 o) w1 G% Y p/ |4 Q9 u. i- @
exit('{"error":1,"message":"Error FILE"}');
& i. [8 C$ e4 i& x
) q3 @3 A$ G' w: l! p1 G, P
, q' M: N: R- }, S0 P" X4 z }; ]9 i8 {: A M1 }9 s' q
: O* B, P& H8 ]! \1 D, p* p9 Q' w8 I. F' ]0 r0 m- f. P
require DT_ROOT.'/include/upload.class.php';9 R( p3 v; l& C) s' Q; @- F$ W5 k
6 U7 D; ]/ v, y5 z: j
3 W4 `2 j4 z" O: x5 x6 A 8 g. W1 D% J( {' s2 ^; K
, d! j4 s; X4 [0 e
3 [: G" H; O8 v3 ^8 d% B6 w( [3 | $ext = file_ext($_FILES['file']['name']);
! U2 }7 L' @( q6 q 5 f1 Z* Q! Z2 u; P) D
+ O8 |% t. \/ J. M% E E: e
$name = 'avatar'.$_userid.'.'.$ext;
; X6 A( t6 }# o* f: T
$ Q' O8 ~) P2 p1 G% z% g1 Z# M2 z8 H& s7 R* w3 ?0 Y* M
$file = DT_ROOT.'/file/temp/'.$name;5 Z" D9 p% C! a
4 F/ A: g. X+ O/ ]( V5 r
; A0 @# H ^7 y3 Z! Y, u
: M7 ~+ B/ d) Y5 i3 L
7 T; C7 \; I8 i7 o0 f7 n! n8 a8 U* a5 ^9 ?: c6 O' a6 Z( f: E
if(is_file($file)) file_del($file);0 q/ Q+ X" R/ s5 @' i
9 V6 U4 @3 q9 R" z% w1 Z- _' W6 G3 I+ j9 s/ F- F. V1 @" o/ z! \
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');0 F. C" x( ~% G1 ^$ N7 ?
7 l2 ?. q4 j" {5 C
& x* t2 |. E- j. c4 K
8 |6 p, I8 \& u O7 Z% ] & M$ i7 n0 G( O+ e! Z4 X
: n. {5 L x( l1 c" d2 x1 ] $upload->adduserid = false;4 R1 j' j& V+ L5 L1 `) V; @
+ G+ w& B4 t; K' }! o+ r2 }: C3 Z6 x3 m: K l6 J- ?
* b" k( [7 |, M" o 4 I- O4 [2 z f2 f; d- y
; {8 I' q- a4 L/ Q# _2 B2 ? if($upload->save()) {
. A+ F1 v5 ~8 T7 a 7 p- X& l/ @$ o0 }
6 h) A$ ?) {$ t4 k' t: Q5 n ...0 K* |# T. J1 K) f& J
q1 y( A0 T+ r. Q
3 c8 ?3 b" x5 y- H$ H) T
} else {6 U; S: n. y9 I7 v* v
6 } u5 F: @% E0 T3 j8 g4 W0 O) l0 o8 @" b: G
...
0 o! n( X- C7 Y" J; I3 R. K0 m* K8 p! t C9 ]( `# I! H7 j( P
' w/ j8 d' R) t5 L! A' T: i
}
/ L9 _7 f M( a# B" S % Q5 T7 Q% r0 G) O# ?
7 B* N; F4 {8 F0 r5 d
break;
' h7 @9 {! S8 Z 5 o$ O' a( s% }; F3 |8 d. u
& c; ?: J( }8 ]8 l X 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
( }$ t' }* `8 P b& d % A# W3 O2 n. n( k
% {. Q7 n& Z& d( _7 W& F
upload对象构造函数如下,include/upload.class.php:25:* F3 X2 W' Y( I
7 N$ Z: N/ t2 \9 S
( Y3 [' v1 w, n7 i) @0 M# e+ c3 e <?phpclass upload {3 B9 `% m2 P- k) a; s1 ~& H
4 Y# v* M2 W$ h# p7 O, J4 D9 ]1 w0 }2 W$ g
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
; i5 f6 G' ~- B- ~1 D+ K+ i 1 c7 n8 X$ e' _
% I. y+ |/ l% P
global $DT, $_userid;, |. @% I5 v" ?, t+ c
* Y6 B7 k. b% J3 U2 D4 d, [. k1 \
" @1 m$ x0 d# [; } foreach($_file as $file) {
t4 P6 X' F& N+ D
{- s' [. ]# O: F/ V" a
- F) t! B$ o8 x/ b8 e6 k% W7 M: t% H $this->file = $file['tmp_name'];0 l3 p* w* \" x, @
% ^: \. V3 d0 Q! g6 k0 ]( I
; C$ i1 M3 a Z" k3 E9 m $this->file_name = $file['name'];
7 b! v# C" r g1 [
! H- G) c! c" r: `" r& B j" H. [$ I; S2 a& B2 j- V
$this->file_size = $file['size']; D Q# a1 F6 G7 e! q# v! N3 R
& ^# ~& V- ]& {% d6 B
2 F5 S/ |* j8 A x+ v/ Q
$this->file_type = $file['type'];
; d* V4 O+ r9 n2 ^* w1 L1 O9 |- {
8 p# _" e- P1 N d7 F n6 Q, b# V a3 x$ O* |5 _/ ?5 k* F- i1 n
$this->file_error = $file['error'];+ y7 \1 q0 ?% s9 C
8 m! N3 J. B u; \! A3 T" y* X5 K
! |9 ^4 O+ I& @7 s, L0 h& s+ R4 e
& u' ^' x, O' g! _* {
& l. }' B8 W. z% g: R
) t- C0 C2 Q' i2 ~( y }
" w$ ~) g6 Y- W2 E% G 7 [, W6 k0 u! _* ` y4 P/ B( ~
0 m8 T. O) ~) R0 C V( A: H7 |+ E $this->userid = $_userid;
6 Z2 |: ~8 x, W6 n; W
/ ^8 e X3 c9 T9 e# n( G( J8 t, B
% J! m1 I" J1 `$ X" Z $this->ext = file_ext($this->file_name);% y3 M3 C5 c+ c. z C; L
1 l! f: R: U! Q5 I' E3 Z, h, c5 O1 ~ \
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
# y% J' H- y! m* B 2 Q3 t5 u" M! L& `' d; e7 _
5 R& @' z5 D, F" o; s1 k $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;, P- u' `( E& l4 J: ~: L+ T/ N
/ i5 Y/ G7 n \# ?& H* s+ W3 t6 x4 o9 x/ W9 B- O! ]5 y" @7 p: p
$this->savepath = $savepath;
0 C+ t }$ n7 \8 z( j; w
. h$ B' ~& C6 p" l& e# @; ~& [
2 P6 X% D2 g1 C $this->savename = $savename;8 L; l+ C5 v U7 X2 w% W- u
; o6 [, Q3 [7 y4 `0 S' m
& X; c# S. w# g+ @6 b }}
) Z# \; @6 ]# r1 j $ Y9 ]/ l: W$ O( x
; S- t- l7 s1 a m 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。3 n: S! t* z* F+ F3 E O& Z" n
: x! c0 k$ d+ n/ {( Z7 A5 F2 T. W8 o, I, j" ]2 O. W3 H
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 & E4 l Z! K S9 t# T2 H7 P
Q* j6 Y; y& [" {' t1 U& q) l1 V) ]1 c9 U+ }' n' J6 ?% T9 |
$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.php6 e& f1 b) p; P; }( }$ I
. y0 l4 ~. L4 B6 h' H. \0 f
M. b9 c+ R4 G( h5 H 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
7 l$ |3 F% t# v& o: c5 W" `
' q. ~2 E8 p8 K; t
; g9 G1 m$ B5 k6 B# [
0 Q! v3 j/ Q: U) X1 L + N8 M6 z% r& A
) i- Y) k2 M, P6 ]
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:0 N5 |* l9 ?% V9 @/ V$ w- E! |
s& w8 h8 k* Q, O+ J, t5 G
. @ q% \ ?( u# r <?phpclass upload {
7 @, T, C6 F+ e
4 I* j/ c% z: l5 p& L, a9 K! q- b6 s$ l" z
function save() {$ V; K8 X6 F3 z c
4 n5 ~) R4 }! ]: ^: s
2 I3 ?; a- n( ~% \, U* n9 R include load('include.lang');: b( B& j6 n- \8 \, ^2 J
$ F) r: q. K2 W j; `) t" s/ ]% W k% w
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
1 s8 q* G( E* H0 K# {8 w% y # [) H3 F6 l" C9 l# N9 k L/ c
$ o# S1 ^. F0 v' r( s; U
3 h: B) g6 ^$ K+ u
9 n6 `3 K0 \( s+ ^ t5 _& H
7 ~' W# t1 x' H- |% k( l# u if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
( d. I; Q* Q' Y* z
' i, f: k8 a1 h/ f5 m2 t# J9 E9 n/ P* s6 ~6 O1 R
- \/ T; M; e3 f, G! l5 A
" B1 G$ ]" @2 a- i Q4 J
, {; e* i" b+ V if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
; _6 J% D7 z+ R* Y& [$ K2 M& F
3 m5 h: Y' B! X2 h- \) S* [* v: n! L% I
* c: E' k$ h- y( f2 w/ B- _
9 d0 S2 O' a7 c1 P" @
8 S& k. j: V8 y( k) y $this->set_savepath($this->savepath);7 f1 O4 z3 T; u; n$ k1 [
7 {5 [/ w, `- ?2 [6 T
+ c7 |8 w$ W3 h3 u6 {$ N $this->set_savename($this->savename);
* ^3 L- z( l, J4 S7 z+ e' L
! R7 o P: h s3 m, w/ N7 k1 m
- Q/ A" m7 d* f& U+ e# k2 w2 ^
4 k, |( k+ q/ X4 R& i" \( W 7 U4 z$ ]1 R7 q$ n! p* N
2 T6 b6 ]- V6 q+ [/ A+ Y if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
# ^+ g2 @7 {; `& e
+ A9 X/ S% }5 Y4 \$ }8 p; W* O' e* j
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
( ?$ z8 X: ^7 V3 r" N# J , w9 V4 V' d3 V w4 r' U. w
3 t1 M0 D2 E K+ e) q3 T# J- f if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
/ S0 x6 e( }5 o: {6 j" }
1 J% D* R" h) a/ d1 z; D
6 C% {8 d4 y- k4 k/ A) |, F5 g2 w
! ~* b$ w2 h5 d" [2 q1 `+ T
6 ^8 Q/ t& ?( M- ~6 R. j# B d% g4 O d) V; B5 d
$this->image = $this->is_image();
" e% v. Z+ n2 N* r9 t
/ s; p% V) {/ F. Z! i. F# _( v: Z% a& J `/ T4 U) _# @) }
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
, g- m* i8 r# j
% r. v6 a' o% l9 K0 H9 M- u g: {
4 p I" i& l2 X' i" O) H" `. _" M return true;
4 H1 G4 x; r6 f8 P; e 1 x7 r$ s: w, g5 o
, T( A% h1 E7 }* t/ o }}
5 v7 p X2 M4 A7 |& t0 J : n8 k) z/ {6 r+ i
/ } s* l$ M% q
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:5 L1 g* H5 a. T @6 w
- f: O: x' }; P4 r% G: D2 ~- J& Z6 I3 h: l# Z' R9 h
<?php
$ b8 m5 U# P/ v/ [ g4 J t 4 r0 a2 l8 q' ^' {5 J0 _7 ~
- D5 [) z U5 Y/ u+ l) S
function is_allow() {( \' Q3 w- z; T( V
, p2 n% p+ g( C2 n" |6 H1 c8 |2 c0 A$ B3 `% n. B* r
if(!$this->fileformat) return false;
9 n. s+ i7 S. K; a 8 u/ H B/ L. B8 C, ^- D+ ~$ n: A
6 m1 V1 [8 z# P, K+ _$ X# I) B5 ^
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;' J* T0 M" Q6 F* N2 }
2 l# e; I8 a' J7 q3 u; i2 u4 Z4 y, \$ g/ ~) ]. f9 p/ g+ c( h
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;, ~6 J1 ^3 \7 v/ a
* r6 `$ K% l. b) o6 F, j- D
! i9 G O2 P! y1 O J0 W: a return true;
, N6 ~' V1 k) h5 P! f ' G3 u F& _! Y5 Y9 T9 O! M
. u4 N+ t. x! Y }
8 a1 x7 C2 A' r3 r% q; L! t
1 |7 P) C5 F# z% q6 @; i6 K0 u+ i7 u. `) o/ ~ \0 q* v3 X
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。. k w2 s! N$ d! H5 S( z+ `7 D
' ~. E$ c1 q2 v8 Q. ?4 v
2 t( _' {; R2 y( E) X0 K
接着会进行真正的保存。通过$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文件。 q( M6 r T I. L
3 J; U8 x$ |6 n, Q8 m1 i, b! |
# W) J" `( |3 _' T* ] 漏洞利用4 T3 b( T+ l. o+ U- }+ V6 R3 {8 t
# O" d8 ]. ~% r* z! W
# H& Q x. i) u+ Y3 r# o
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。4 P# l) O. J2 R T
' F( u* }: F" \
6 J. I( ]( }0 V7 {8 ? $ S7 e# H) I. Y5 u; ]( b
; H3 o# {9 i3 f' t
, ~. n1 b- X H4 [: e u6 p 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid3 y7 i$ o# p& Y5 h8 _
" h+ r w7 Z' j v' b& J' O
1 ^" t% }' K- a
不过实际利用上会有一定的限制。& W& L7 C D2 @* l! j6 N& D) n
5 ^# J6 l) q s" J! v4 F! h
0 b( M Q3 ~9 X) l. p' ~! `3 Z! i
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
% |: Y$ C4 w9 B8 D6 S
# v; b8 @' u0 D( N* Z
' k+ N" M( B5 p, C0 ~
1 q$ t8 Y$ w; m& m" \ 3 i4 h2 M. C; [. x% y! R; G+ |
* e3 q: [( [) v5 K6 F
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:- Q, Z$ B1 F% s$ s) D0 E
" f4 u2 T7 H6 a% W v/ L" P
C6 n$ n. K8 }6 V
省略...$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]);省略...( z9 g% C/ R% u" X" x2 X
8 Q0 i0 g- @0 l$ @ T8 }- r7 Z
3 q$ W ?# n: A' Z 因此要利用成功就需要条件竞争了。
# J& y+ m9 ~- o- v. O; T( b / ^8 z2 E: w( k9 e
( t6 H% h8 [) O9 ~+ C 补丁分析
7 P" J" a" S; q3 B# ] H( ~8 }; Z
5 i; A; k! e& ~: V' W0 Y2 P# i
) W3 U0 w) b: |, u1 J8 B 2 l6 m9 V$ F9 y- r) b/ M9 f& W
, P* M: j, @& j& q8 r! \ 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
+ H' M9 x Y0 U4 U 4 w9 f4 q0 @7 i2 @! c" G
( s3 G# V6 @; R6 o3 t8 b C! i
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
! V6 |" b+ M' P7 D
) [ p0 R& T# |# X; @, K3 ^
. ]# U P% J+ |3 T5 U' K; c6 @ 3 _% G, m/ ]# t+ [4 x
3 q) r4 k! l5 e
% u. ^0 W3 c3 l; B: g 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
- s5 u6 o8 N% z0 B" `6 X9 _
! f7 k- _6 F- N0 A [. u9 B5 \& }5 e X
在is_allow()中增加对$this->savename的二次检查。2 s2 G9 b$ o- `+ E4 ]% ^
( _2 e/ T# E9 U4 k4 T3 ~- n
1 t0 c: R% S/ H7 H4 R 最后
1 c) L. @, _- H1 k9 Z
. g9 } ?* c: W9 E, H' _( o$ B4 h# P g* x* s
嘛,祝各位大师傅中秋快乐!
( {9 z9 i' N: y; _. A
: j3 q2 q" M$ S4 [, F$ Z6 ^( W& T/ J; m" _! c+ K
5 [; O' t7 U% l$ \5 g) [; w
6 r' M) ~0 Q* l6 R D
|