5 j; {1 \5 w6 A! U
4 @1 _& R# J" Q! I; p- D/ H
2 ?7 }6 b' ~4 m: G6 p
! B0 B7 P/ b$ F5 j( J- p/ g 前言
; v! s' D) a! ]( r/ {" _. L8 a. n
* x7 J% S. M* u5 S
- C- O2 `- K; {& r3 |* |/ ~1 f* O 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
% K" H% }' f4 [ w" p9 K9 F, ?
+ Q( p1 O# m& T! }2 L5 i: B( G% u$ B7 S; y" Z( s# ]
& o w. c; C+ o3 ^
6 ?; K' X- ^/ H3 ^! J# o- {
8 b- L( q5 d2 I6 k/ M# m
漏洞分析
, i( r% g3 J# p' Y
% R! \$ C3 f* n1 e' `
4 }& G- q/ T! ]' K! e( \3 N6 O4 f$ Q 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
% Z' `; E9 L/ v- ~& S " |" @; V. l2 ~9 I
9 e0 ?1 E( I' D0 ~4 f2 j# Z3 `8 ?
$ I3 M0 S8 H& a
$ o# D2 Z# C' G g" K0 ?& ?' d% N5 E# _$ F& d, s
对应着avatar.inc.php代码如下:( d) h$ i; S2 q: ~
& ?5 ~' \' s$ ^0 s% H8 y% I; I6 `3 N$ f! ^; E5 K6 Z
<?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) {' i. k6 s' h7 ~
' q/ o& f2 R; B- q0 A
& h: V/ r4 H3 L9 K* G; c* k- H case 'upload':* R! L, B+ ~8 o- |& [$ {
: e& Y- L8 `7 Z8 Q0 O; D
% W5 G% V% ~+ o5 V- h4 n8 @; r5 u if(!$_FILES['file']['size']) {
8 s& x" {2 f8 b7 y6 b, U ! P4 }& i4 W4 h! t/ P
. z |$ M& q- W+ D' {* J
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
- U6 R" ]! d) H( x' A, R
0 m/ g* b1 D2 O9 E `) m7 ^0 Y: y. o* R" X+ e
exit('{"error":1,"message":"Error FILE"}');5 t' }* p" n& P+ D2 c
9 n8 G2 w$ j$ @) \" L( ~( {% v: n% a1 w) E
}
, w7 l) Q/ m h) }4 X
" Q- E; ~$ w8 n+ s3 o9 p2 \- B8 }) f; ~/ S! r
require DT_ROOT.'/include/upload.class.php';
8 x, a+ g4 `: D, U1 H& {! V9 L
" k* w o C: g1 x) Q* T
; o7 b# ~9 p8 x9 g- o " D% G9 M9 a C! _3 q2 l8 e4 L
! X2 i( l* I# b6 V
8 x0 u4 v& p3 W- e $ext = file_ext($_FILES['file']['name']);
{, `( `% u% {, B& g) n 2 U6 z$ w/ H! P, a
2 _7 g+ {2 F8 K2 x
$name = 'avatar'.$_userid.'.'.$ext;9 S& `) Z/ Z @# o k5 R# K$ t
3 V1 s. `, p0 v1 y
2 X0 a5 U5 \( P7 q
$file = DT_ROOT.'/file/temp/'.$name;
. g9 f+ @& R; }; B' |/ U( k' A 7 F, G2 p/ R! N4 e9 [9 j
* p- v, T4 M5 m* g& h) v
/ h/ q0 F. G! S 6 F& @4 b9 m2 }. u3 `4 J
! U5 e( K" p' ~( q& s+ Y if(is_file($file)) file_del($file);# {& [8 T1 n/ {$ U
3 [* A0 d2 d" D% c
5 q( Y1 d2 G: H5 `! C' M5 m/ x $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
4 K) i M2 F7 `" F + G& ^! ]- J! I1 W% |2 ]( p0 J9 Y
: j+ Z% D6 K r$ ~; ^ + I3 [% p- C( g- p3 e* `
. R# C% L" l9 ]
, M" P+ n' ]. H# R9 N $upload->adduserid = false;
7 W. W4 G3 E1 ^4 Y 0 W+ g, P; w8 K5 p
! w% G; u" `* \* @/ ?
# d! v6 u7 O3 p) }7 X! k5 o % L. n4 _- l% J
; w6 T+ F$ s/ p/ r3 c if($upload->save()) {* _" t2 N: g1 g) X9 ?
2 X9 g3 v4 d. K& Z4 U+ W
. m; i/ n" O: p1 @" B
...
* k7 O8 X" O4 K5 v+ b / d( U* F" r+ n' P
* z7 P) s3 A: R* @
} else {
! j# c6 ]' i; p
F5 W) Y" C/ f; k- n- z: H
( G3 i& A, ^0 j ...) n! U$ ]6 D1 b. W3 _
2 s6 b% k3 T0 u [; Y; Z1 l
- k4 U3 E7 x! z: Y }
& F. v0 D( G: x# }' n# [) \" O8 w2 H% ^ ' B, M, j2 u% t. q! }( Z
7 D4 q" o" Y" U, D- R" v break;
O, n2 t5 {$ @/ h" }% n/ Y: e3 `
: e' ?% X" ~0 D% a, Z
! U+ L1 ]# m: ^3 Q1 w; z3 w9 ^ U" g 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
( S: Y- _! X+ Z6 @/ M5 e+ j, Z - I! A1 T. I' q/ E
8 R, `. ?5 e. \3 p
upload对象构造函数如下,include/upload.class.php:25:) Y8 H1 ]5 }% P# [5 D
" q) F# G! Y) D7 |. ~7 f4 n0 k6 I) V3 M6 d: l
<?phpclass upload {
% c! J6 m/ r* A8 v& g; Y0 z" F7 Z+ K
- r' g' c$ i6 M" n1 y$ t' z( W
9 |+ R4 B+ r& ]9 v- o function __construct($_file, $savepath, $savename = '', $fileformat = '') {
6 N! `+ H/ `5 J( N7 z " U! n8 o% i- N/ u% T8 V
: A& ^1 Q! T- F
global $DT, $_userid;
. {) h* z! Z9 i% V# ~( l
, Y( k4 m4 B' ]* L! l! h% e! A, x% z' f9 ~' B
foreach($_file as $file) {
1 |" `: I# `6 x( Z; q' w 2 p$ M5 g1 p, Q! s% \& [6 @4 E
( K* {, I) Y! H" h. r# |
$this->file = $file['tmp_name'];
4 y# z' n; e. b5 ~7 q2 O) Y7 k& R. t . ~+ {. |' t5 R& B+ p2 L
8 I5 f5 J9 Y! `% r" n0 S $this->file_name = $file['name'];' J' p+ y$ w# N- c( ~; _
% }, A/ b. u- E# J4 g2 M: b: E! r- f) U, u' K0 L$ X
$this->file_size = $file['size'];
6 Q# t3 r' u' L' {+ n ; R- W( A! E6 Z7 K- T: }" `' ]
( v& O1 P2 R! c, s2 m
$this->file_type = $file['type'];$ F0 t G5 j& G1 {1 Z: F
8 x) G3 ?, H# @# P# E: q b" H Z7 {
! m. N. |2 ]8 ?
$this->file_error = $file['error'];8 `. a3 O* G& X6 ^
1 `- P% O, f, P3 ~$ a8 c# w" _
$ t* ~' [6 y) j; l3 L& t" W
$ A& |5 Y/ I& q) Q9 q k0 h; Y* o
& J1 o. n7 _9 |. G! o" z' F
}5 i8 G- v8 G4 T! j( s/ S& X6 M3 O
. @/ K8 l$ ^; E1 f3 s8 ?
1 z. Z) c# I8 y0 W/ |0 d$ p1 W' D $this->userid = $_userid;
% `0 }4 S3 r- V- {% K+ g- b9 e" y / S8 r; L: |* _7 \
+ x+ o7 s: [1 f" ^: v $this->ext = file_ext($this->file_name);
) d" r) [% f5 u5 q. A
' p* L% U' \0 @' S5 V$ u
; ^$ i" B! @# \4 E7 c $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
. I% u) j& [" X* o- P: n $ n3 C L2 z: A% f& D# i- u, K u: b
/ w% F9 ]8 Q4 @. {5 z) h $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;6 `7 p- _/ y$ Y: V* s
* c7 X0 R6 L5 G( ~, |* H6 f
' d2 }8 i4 J! `: e. B2 C$ k $this->savepath = $savepath;! P! v. r/ H* E- [% R9 L, F
( d; x1 e3 p( T
- b' G/ k% H' U9 |% W
$this->savename = $savename;% {7 h7 |4 a0 o* `
. }5 k. r, z+ r, [4 |/ Z5 x& y" t9 V C" ]% ~4 h4 K, ?. f4 `' {" z
}}
8 y% [* ?, S( v% f! N8 I 0 N |0 {4 g2 ^* C: b5 m4 J$ g. _
& ~( W" u: x' d; z" X) Q P; t4 m7 w 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。& ^' b# {0 Y1 B6 X0 }# ]
' z% M6 x; ]- a4 r K, g# l @
7 O( B* M: w5 D, h1 V% d# c$ V& G
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
2 W+ C% }8 q; E+ C& I; e 5 t1 E3 U7 s; X! F" Z }8 r% [& l
' C4 J& d# p" ^ $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.php5 R9 Y; A& _/ M9 f7 F' @
E2 y! B( O7 ~, e( R$ `! S
7 R( ]* k, r5 Q: s+ C H5 O
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
/ {: Z1 W9 R, o6 t2 t8 x9 ~
7 ^# n) S1 d# ]- N/ W9 h3 U! N/ d' W1 j0 H
2 h3 f' L2 H9 H : ~" }3 j7 Z. H- {+ C; ]
& y& U. M/ O3 V! X# i1 R1 G 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
8 y. C( e2 `1 a9 q) E - R- N/ G: F$ h) U0 u
8 t3 B6 G0 ~) N <?phpclass upload {
: L* z; E$ J3 r: g! t9 j( Q3 x ) g5 l. P! ^$ j6 X0 u }5 }, g
+ K) U8 q1 T% k- @5 D
function save() {% z1 W# l; ~' R7 J+ x
+ c- U! z& m4 Z/ d# j9 w( Z/ n, E% P/ b; u% [
include load('include.lang');
u1 {/ l: h4 l1 D$ Q* ]
6 u& I8 h9 [6 A2 V8 Z: A$ q+ X& ~3 z; z: @" m1 v8 D3 ^8 b
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
/ a8 R* P4 l6 [* g8 p( q: t
! Y& Q4 X7 E" |0 P; J
4 S: O/ L" {2 P! x , z3 a& G- o% Q. X; s
. O! `, m9 U7 ~% h! R0 P8 y0 u) W4 m7 w+ J2 W+ @* A
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
' \+ N5 p, @; c8 h8 N+ I" m) L
- @2 [4 E( d0 U# S/ n+ k5 A: [: f+ h; K' a- D
0 J$ C8 p3 m+ \) I3 x; } " l5 Q' M+ G# }. _0 g7 k
1 Y5 c3 q% h9 r6 u0 B- R if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
1 e' x5 w. J' a; k- R - x9 p& x3 c' O0 Y( Y, A
6 ~0 G) q" ?/ W
0 }+ ~% S5 E8 C3 c( P " K3 G+ n. y. \2 n/ x5 _% Z
2 x& \- q t8 }. `# v& B
$this->set_savepath($this->savepath);
( B; x5 K3 a: n- h. w4 q: n
4 {; O8 \6 G% N# W& T% E
0 S, M1 h* f- Z% O8 c/ n& B5 ]. T $this->set_savename($this->savename);
: r% C) n5 K' q4 A$ t6 u4 ~
* }2 X6 p' d) [& @6 N. h4 w% f$ s/ [9 \3 b+ G$ |9 _
$ H; M: \6 I8 s* V" a" p, b
& o% J D+ G5 d& r
5 T! Y6 q3 H1 a1 v% O# {9 r if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);; `/ j& n/ F/ _4 E3 }) U% D# X
% X. ^* y# i" b3 X" e6 m5 G6 G# m. u( _4 V! o W1 F" S
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);9 c/ Q$ `! S3 o1 b$ d. j b
% s& H9 O5 m& j2 ?% D( M! G7 w$ {7 e
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);2 F3 h5 [% B) g/ d4 K
; `4 d% A: x( {1 Q0 E4 m
( o! X& M! L, }$ O! v/ I, B# u
) _3 D! A. C6 j% X# ~+ J2 V $ Z) [; d9 l+ o0 t& o! ^ l, H
8 F' I% I9 N4 s: P/ i
$this->image = $this->is_image();" ?. V! b2 t" h7 F
7 n3 V* s% x5 h2 S1 N! N$ Q* \! N/ r6 J
3 A7 i4 I2 |( Z5 N% d- p8 k4 y
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);( s9 N: p$ n x+ }# |
: c- x. ] G! W3 b, j9 @
' @* y% A3 n8 P5 d- }% m return true;( t) N; B% ~& m% f
! p4 D5 T* X( d# g0 A$ z$ D* }
) P. m; s! L7 [2 f& ] }}
& h \' G5 `" j$ x
* |( s& a+ Q( |" z: H, J- v; F) N2 @# |6 ~
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
. o. z2 D1 g5 ?' X# e1 l ) e: d3 L7 i+ S$ Y5 S+ C$ Q( c) ]
( o( z1 ^- }$ @* k/ a7 Q" l4 ?
<?php- O; a( M8 N; p; o' K1 P
Q5 H4 T: Z2 K9 n
6 e3 N; M% A C6 w5 M( @: c# z# V function is_allow() {7 B) D4 `' H! k2 o' @
6 V' }/ Y+ ?, q' K/ L6 g! o3 A7 z2 t- M2 K
if(!$this->fileformat) return false;
2 b p' y2 C6 q7 K; y
! i: t0 T0 a9 u$ Z' l4 L: z3 X
: C/ b$ `; F- l) ?( s9 y if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;9 o3 {7 K! H, M4 o8 J8 t& s
) S/ D6 z' p6 s d" [7 }
2 i4 [- K. ]: p" e 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;9 k0 k9 q# P2 u- w( [- y
/ N- B' q0 n% U
3 [, u2 ]1 W. i& t" B+ V9 o return true;
) f) Y0 V" p/ _' x8 ~
& k1 a, i, k- x0 V' P' d) ^1 O9 W' O
}
+ |6 p" u1 ?% O" ?% h 4 ?; X1 |( O5 `2 R \+ `- [* [0 R
+ ~ [& m- V, n: m8 Y2 B2 I. z
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。4 O. E4 V1 Z; m2 w3 N; W- }/ @
. C' N( V8 |! e1 Y- h& M7 J: z& b" V+ s' C. L) C% ^- p
接着会进行真正的保存。通过$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文件。" [3 S. m' B6 P" y/ h
( X. A; l' M8 F% T
, x( h5 y' R5 ^- R1 i7 c( {5 L 漏洞利用
/ H% H# E; {+ T& p9 u
3 S4 t% n9 R, Q8 W: Y
; ^- Z$ S4 p) f. B, n( ^ 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。1 \% @% |/ p* R2 Q
8 s7 R" K9 [$ t) H+ C
- u$ Z8 k8 `+ Y; F3 ]% [, `
" l3 t0 K+ D8 d& S
, h$ u8 n" L0 S% V+ J4 s( I5 D8 A5 ?/ u' Q# L
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid A$ S& m4 e) q( t0 V; f
( u0 u; R/ A* h; @1 m7 ?$ Q6 ~
0 \- u: w! L# `' W4 A 不过实际利用上会有一定的限制。
w L; p1 l% Y' p0 ^7 Q& T2 j5 w
$ ]. P9 Z* D6 ~7 ]6 Y5 |
. v( ~$ W: t8 g' Q* x" j 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
% k4 j; `4 A& m, H1 { T1 a: Y# z ; T4 }* a; t- v: T
% s8 E- j0 ^7 f: `/ L# q+ C+ @
- {& J3 Z' v! U : R" v1 R* S7 x, N, L& s3 t$ |
0 h' ~" f+ j" Q) ^- q* B+ h5 D
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
# e9 f% c( M5 V
7 } q, s! K f$ Z( Q7 M$ W9 K1 F
5 |: E* k! m) A7 W. S% [3 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]);省略...
% \& E. X& h8 W: N- s2 y5 ^ 2 t6 r8 R; p# {0 d. b4 M5 X# ~
: J( s% N+ z" ?/ `+ h
因此要利用成功就需要条件竞争了。
$ J3 d$ r" i+ \9 d0 K9 q $ ?, z( M5 @6 `4 M
7 K/ F. W3 f+ g; { u 补丁分析$ ]5 A$ d8 w" { N9 ^, x
8 Z- d0 S, V: b; I' c. m' K, [
5 i% S( R! ]9 k; X
8 a& H* Y& x: |5 m 1 |$ v/ I, q- d1 ?. z
9 `: T+ o# G7 ?1 k
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
5 W% o7 Y& O3 F# H: | 5 q4 ~. Q/ L. b3 x/ H, u
) g" l# S& p' p5 q$ ^ a
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}9 p) d* C8 J, N$ \ _9 i, O2 f
& b7 [ ]+ N! x) w" u
7 q& L8 Z& L1 G
$ L6 b4 e; I% U
; i- l3 Z6 J6 F3 w7 {' X
- O9 x( \; i- j Z5 t: q h: P 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。: X9 f$ ]0 n, ^" N) z# t
- W( Z7 e2 W! Z6 p/ e
* Q7 A, ?0 t G d; J
在is_allow()中增加对$this->savename的二次检查。
+ E r7 ]6 K2 z* x" J# @/ a( y
9 _: f5 @/ C W: n# Q8 y
9 d( B! L6 R6 k 最后6 [% L& \0 F: u
3 j! t- e; H. w- R4 {% j x! U# e- L5 g/ R: M
嘛,祝各位大师傅中秋快乐!
) h, e; \" z: i! X+ j
/ T% Y( g" e7 L4 g
) c' q& V' | W4 p; I8 a6 R; D3 A
0 b$ d6 _# S" ~ r! ^1 Q 1 P/ ], d3 p6 A3 A: a$ |/ ^/ ?
|