" E7 D" o2 }8 A; n
6 Q! A1 `- P6 N7 i2 W1 L0 N
8 G! Z& h# o: [" C: y
- ?! a) N* K. V
前言
" ~8 p' a: y% `' G1 k3 O4 H+ [" b4 `( ]! Q7 Q& u
1 o5 T, B! g2 O3 w# q5 A 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。+ |3 C s, ? w7 M4 {- e
' ~* @. {; G$ n; Y- q2 ~" f' @* c9 ~" @. ~9 {5 J/ x
. x: S+ ~) [8 [6 x ) `; C7 U4 r- W, E
, y9 M" z6 r2 l$ Q' b
漏洞分析
6 y3 S( u" Y/ h1 c% r5 S- Z& R, e4 [$ |1 |+ H0 r
' P( b1 {, \' ]1 G6 y1 Z 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
( l3 Y8 i5 {8 M* M& W
' v5 g: J) q) [/ O3 P2 q3 c: t6 m1 w7 D C) F7 [+ s% d
% n" Q; u7 b' Z" u) S. c
- J, v! ~. u; ?6 v+ s
" u9 `% {$ R1 K* I; U% o 对应着avatar.inc.php代码如下:0 W) |* Z5 _, L
- Z& ~& x3 B# q/ `* q' ?
4 r1 q& P+ y2 D' A0 D/ 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) {) u1 u: m# k, p5 D% u3 }$ S
4 w8 J' m; F3 \. \. T& M6 a( A& Z; ` ~
case 'upload':
: ^ ^' a. }& h9 M4 j
- K0 C4 e1 |" E' Y8 q& O
, o) c1 T! {: u6 s4 i/ \6 E if(!$_FILES['file']['size']) {. R' [. ^5 U6 R: C t% `
4 I2 E" K- G! P" k4 C Z5 g( s5 }- D# E" n: v; w$ j+ ^# M9 p
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
! y6 R( x! K: l5 Q3 q2 V# R
! c- N$ Q& O8 W1 \2 G+ t; A; c$ P" ~3 y: B* x5 Z. C2 v, Z" y9 b" `: v
exit('{"error":1,"message":"Error FILE"}');! m1 H7 b1 R5 e5 V- S
4 W. e! t3 B. S; R8 y% }* p4 v4 e
9 |% v% w! W |8 L) {# s. f8 `
}
5 P/ ^! J* z( t8 g/ M$ Q7 }: H% v
' }& {& P8 U+ P* R* z3 G" a: d! l C$ _ S% X3 s5 H
require DT_ROOT.'/include/upload.class.php'; U/ U& F3 N; h0 |( g
$ m% R3 L- D- P/ |6 Z* E1 b( y; x7 }' O9 i# | q7 s4 g0 C; O
" w# h- [7 m" P& h
# Z6 \7 W; N5 V4 @7 a9 J7 T
( ~% g# L# g5 [3 j0 ~0 p7 q/ l* O) f $ext = file_ext($_FILES['file']['name']);/ E( b J4 m, b8 P* ?3 b$ m
2 J+ i5 R! S+ N( z5 F) P! U
4 G& ?2 G$ w9 @) b/ B
$name = 'avatar'.$_userid.'.'.$ext;
1 {7 M& _7 r, K' i6 ]& F 8 D% d R8 d7 l
! @. z) T+ ~; p6 S9 Z# O* Z% S
$file = DT_ROOT.'/file/temp/'.$name;
9 i2 v6 m! F5 I9 G' {& G * k/ i6 Y" K% {# g9 B. D4 b% f' ~- F! X
, N0 R6 P0 N; }5 P/ l
! b# z$ C- _- u9 H- o / e1 q+ l5 P9 F2 o! _) }! e
w' U0 ?- R0 u4 V if(is_file($file)) file_del($file);
7 Z% ?$ F; d) \" e/ \ : y1 R* [0 w2 |4 u$ p
) k6 R" D' o3 i% H $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
Q3 t8 k8 p6 ^2 p f " r! J$ t4 E0 O3 r! y) o9 u
4 O* ]1 A: K U9 B9 i
* \/ J0 I4 p M3 I. g
6 S' L9 W* ~) i3 e u A4 d5 P* H
& e/ E( Z, e& e $upload->adduserid = false;
9 G% t# E* z9 b ^; i& T$ d" U. j$ p & B" S, o4 D" M/ }9 w
3 k4 m" S( P) W8 |8 ^3 s
1 S! g Z( O1 ?# l6 o- q0 f $ U* b9 C$ d _0 `6 m# o
/ D/ B; S8 }$ R% I6 p if($upload->save()) {. E2 }& h( x$ V3 i- F9 w% ~
) T% L# p) A% p/ \
1 m1 }' f, X H6 }" q9 k
...
# z: U; t' G+ R e% R F
* c! a, M7 ?) Q! G( a. C& _$ E* f/ K- @6 c
} else {! d, L% r9 G1 ]! P9 D# Y. G3 _
^ @7 r8 F! {2 f3 s. B8 A% {' P$ O5 f5 z- Y" j# Q
...
/ ~/ f4 j. k G x1 w ) l) b# b0 x' K( ]% G
0 R2 H1 Z" Y/ c: \2 d, T
}
; l, d7 v+ d9 v( A: J, s6 B9 B
& Z8 K* [4 d- i1 U; A
( x% A! Q' N" ~# B1 x" m; @ break;
/ p0 W0 \4 I4 ] w; s 9 S: B1 l; `. b4 `
& V. G# W$ T2 Q: }
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
5 m4 R* x2 u3 N) R* {2 N$ f
* z3 m% [# `- n* `7 R
6 J7 z+ A/ o) \- r0 r5 S. I upload对象构造函数如下,include/upload.class.php:25:5 K ]: N- ~7 i I' d
6 X, A- P* a( r' v" h; R7 N& w& l! L" {9 \
<?phpclass upload {
' o- d8 |* \$ E5 x S* [; V3 H
9 P M" P% u8 f- b" A
& m3 C0 F7 y/ e3 a& M# c. h function __construct($_file, $savepath, $savename = '', $fileformat = '') {
3 Y v" z5 r* O% m8 s
8 p: Z6 Q9 f7 U# B/ l% S" [. T) O3 h0 ^1 r+ m4 p
global $DT, $_userid;
+ c7 w, {$ v% z$ `: g2 H2 @ 9 r# s9 |% g$ U: {* c/ q
9 f D) P' e4 f$ b. {
foreach($_file as $file) {
% l& u* u T; i6 B $ N( S1 {% Q1 b6 \' O, M
' d7 w2 I, {6 v2 o. c $this->file = $file['tmp_name'];
! z0 v ?& _0 j3 v; C4 F, ? ) \7 U' I4 m M; M+ k0 w' i
4 ~- O2 P# y7 S$ |8 Y+ I: X $this->file_name = $file['name'];% O* Q8 @5 r9 ?3 |" z _, A
) a9 P* V9 ^! c
7 S* o9 a* l1 R- {/ P $this->file_size = $file['size'];& v( ~2 `' T% ?
; W! P2 ^& v" ~' v+ V% H
5 {; k. Y8 P4 ^6 y
$this->file_type = $file['type'];/ v' Q& O. V4 b, N; ?. ^
' l8 k0 x4 P* L& [" t n
% a) @1 Z+ B; p. y K! H8 X- E $this->file_error = $file['error'];
+ O ^0 R+ ~! U+ z8 \$ g; E8 {) [ " \& e: F/ [: J# \! `6 T
) j! M7 H0 X( F3 Q- ^% @
: X6 B9 @ y/ ~+ O$ y8 f6 V 1 c$ w) H3 s+ n2 I3 R
2 L* U2 E& Z- c: h
}$ o" r( g! d2 B- D" }: Y. z$ {
& |: H! Z4 D& v2 i3 g) V
! L$ V7 g& k D# ^* D8 ~8 D $this->userid = $_userid;
4 ^: } L& w1 g0 x, r
; l9 D0 @/ f+ o# ~9 C! ^' ~; g* `- P' n- y+ z
$this->ext = file_ext($this->file_name);
U. u2 P* P0 G \ & O1 k6 q8 y% s# @ h$ ~
5 n# @7 L7 z$ T3 e8 B' B9 J/ N ~5 g# F
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
' x5 w+ w; V0 G4 F' p( u7 A R& p1 u) v% q) f5 t% d. Q
$ H! Y2 E$ L4 Q6 s, s; H$ i $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;' P& _( ~' j1 e9 V7 z
% ^0 @: Q4 n1 m6 R. ]
) ]! y2 Q! j0 a4 {+ z% n: ?: y! o7 P
$this->savepath = $savepath;
% `1 h' @; g# b+ l 9 Q" f4 j& t( C7 t" k7 `
* ]; P9 C$ m+ H8 s, e4 p, v $this->savename = $savename;
6 I* n' q. p4 E9 |/ K; e * ^) ^ Q* I+ D* Z( V2 Y U
/ q0 [4 Q% h. |- B4 I1 `' ~ }}
3 s, j7 Z# z% s, B) R, F. m: h 8 E2 ^6 M, i1 A3 M
2 \7 M' r9 v/ A5 p 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
- c/ E) e& W7 B7 x3 v& P- N 8 k9 V9 y+ H% v! Q; g! T# z" w
+ A, k l" _0 ?8 y# @ 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
3 t) c: g4 j4 j4 u0 h 1 e2 m. U& d) }9 p# S4 z
, w/ a9 Y+ E+ s0 Z' D
$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
& b% \, a4 r$ |3 F5 r
1 k1 e* T# Q/ t) w3 a3 A, p, b: Y' O7 h
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:2 J- V: I+ H4 D; V, ~
/ n6 E3 _* Z% g* `
% T l9 j6 w% c ! Y0 A/ I( z$ [
, S, `% @- Y" p$ |+ Q
% j. d" y5 x: Q: Q% A& P' e& j- o0 r 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:7 ]8 y, ]+ }6 V& b! o
) l+ ]5 q8 S% F- G" p* F( u4 S$ P, J8 e4 J
<?phpclass upload {) u+ x( t; J# t( C% u
! u/ X' r, P8 \- k5 V+ I
6 c0 @6 G, `: l8 k7 V function save() {" L4 I# @* R2 _
3 A" V4 V& g Y$ P6 F* D% o7 j- K& L: O+ Y' K
include load('include.lang');
( N% W+ M/ ^" G, c
- e+ V/ f3 L3 [: ]4 U
) K s* }* I) P if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');) o7 o+ Y$ q3 P& H
( i4 p! k& |6 v3 P( x1 S2 R; x
) n9 `7 m9 C$ V0 n$ z" [: \& s6 {
4 _3 O; ?) |3 a J. ^
7 V+ { L6 Q" j# u( j4 k. V( }; ?, F0 _7 o7 q
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
5 p; R0 C# `/ O( A' n 1 A) z0 x4 A' m4 ^8 m1 A
; n! Y: |" A [- l( n 9 ^& v4 i8 x& q7 n- K
- H8 W' U7 f, R+ M/ D' H9 t" r% }( {5 l2 e" G
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);3 e+ E" J+ n/ u1 u- X( l
- l, X7 p& m, _, c$ e6 m# X/ j
6 }! @( H8 Y* K" s8 P 3 ^( Q) b' S2 O
! D& N7 o! L, q6 S; h& z2 B% e. A9 g2 P
$this->set_savepath($this->savepath);
, Q' k0 Y) S0 k: l( ^) L" S2 C" p
( u" d) V6 ], U& P! g$ U0 e3 x# d& r f/ x# A
$this->set_savename($this->savename);! n1 r# F# r" ~' B3 Q9 A5 Y
8 K% e" u, g2 L s
2 D! O+ n! \& }+ W" P9 E y6 ^ c" |4 B; d$ x
. M2 L( S* g9 q
+ @4 ]4 h; p5 Z if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
/ p" V f5 Q5 ] L* H + N% C5 p, ^# U$ O9 _0 |+ \
! v/ b' o- [0 r* ]; A7 T
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);6 Z/ F5 G) U/ e ?
* @0 [3 r& m8 `, x" j9 _
4 j5 H6 m/ \, `* f) p. i0 E if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);' L7 O X8 f: S% r: [
, u2 D- L, s# U4 y2 a3 h( b6 D1 G' `7 G- o S+ U7 ~4 b; B
7 r$ K: I! c* p4 E
7 y1 h+ O! u u5 Z4 Z! A+ |. O
& c) k$ f/ J* v T2 N $this->image = $this->is_image(); q% ^ n5 C4 Y9 ]9 a
! y9 s+ l2 d, K4 I/ Z* X4 c
! G- q8 Y8 l; M' w2 g) H3 Y U% P( A% W
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
! ?8 r! @7 t; k1 Y. T2 W
( o ^: [ o9 _4 P; ^* c7 F7 |2 I4 W: ~0 d6 r
return true;, h {: ^3 q, U, g7 W4 q! E
& ^& ]$ Z' M7 o* [7 J
* x9 H( n7 E' U! m/ w+ u }}
; ` }. H# J. K3 {- ?3 A 3 c1 _, _. s2 X& Q* l j
. H: Z6 r2 y7 i* g! K( v- r7 U
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
* P8 Q- h5 m% U. S* U0 R/ U * `) |# L8 Y6 f6 W* i2 |" Q: ~
: f" |9 _/ c2 w, ? `$ w9 u <?php0 [0 i/ `% W- E" N X& F$ N! u$ j
- d2 [, G1 L% m; s, u
' _7 Z" R; H0 {- E/ o- C8 A function is_allow() {
, K3 |" f: m2 a8 z4 M" ?0 N
) p# K" p! x" ?6 q: e8 o5 w3 v* |: x$ q I% U2 I9 K
if(!$this->fileformat) return false;% |- A1 q. J$ Q+ H& Q1 j6 r
$ p% o: X" \ }1 X. j% B, R1 s& {
/ v. t8 u+ ?% r. U0 R) Y
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false; Q+ T" t% P( U: p% U
9 f, W6 l7 x8 E2 s8 G
, u1 w% V! B% H0 I) g( | 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;3 R6 Q1 r D% K5 r L7 `" r
7 J9 z/ Q. Q8 U$ t1 ]4 s, q- `) r5 J2 A+ }# S6 m
return true;
- U, J3 A m+ C# _ . h1 ?# T0 m3 ~5 \+ q0 z# z
. e% x& a. o, J! C8 r' w3 l }% h& G7 S; h5 q5 U
3 M6 j+ @- x+ E
/ ]" I. p+ Z5 a$ m& z, d 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。2 W; G/ ?" B4 Z" v. _
" e4 x3 G$ Q! g0 k3 s4 G
$ i4 T$ r4 H8 f( X1 e3 l' X 接着会进行真正的保存。通过$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文件。
* T5 @6 F5 f& I
' Y3 H- C0 l r7 i
; {2 o: g, t! t+ x 漏洞利用" b6 ] j. U- w) u
$ f6 l9 i- N, Y% \% g; H& c; J0 ]' s! X$ ?- y
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
. [& b ]$ z: W1 B1 s5 n
# U6 d# A: m( f& J5 e
, ?: ~3 L) I! a4 j9 o: H9 L6 ^
3 r8 Q7 o- a7 v5 S" l* d8 O1 @1 a
7 P) i' j6 c1 @/ D2 |: b* {& R) E: v; m( Z" R' X
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid+ Z% a! r/ M4 d7 C
: ]6 Z* {; F( r+ w" s' o- O/ P+ o7 Q) l
) W, n- B: ?. m+ ~: ]$ h 不过实际利用上会有一定的限制。
$ F* e7 ^6 H2 z# x4 L! ~ $ N' F4 \) U9 h& P) V' y, H9 ?* U# B
, W9 O; m1 b2 ] 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。7 r: w9 e4 Q; k3 F$ e# g
: R8 P9 H! A, W+ {1 l! j
. g! A& H: j( }
9 I! m' q M3 Q$ Z: u2 { # l4 S2 c* U g9 z
( I& P& A/ e# W6 M6 {
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:( [+ \/ G7 b3 }8 c
/ o2 u9 E, G/ I, v+ r
; L0 t, a7 y5 Z; H' N2 a. a" o7 D 省略...$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]);省略...& u* H M. D/ U+ s3 P
q* @9 N# A3 a" l$ D0 h7 k3 ^4 [; T
$ y) {* o6 t" W
因此要利用成功就需要条件竞争了。
, J) u& ]- {. T8 ^$ d+ [/ q+ N
5 d1 w `/ E. [* g z1 E9 i" h4 E
" @& n/ O3 i U- L2 b C7 P 补丁分析! t- i$ Q; e, {
9 O" \# Q% H' K0 \( j. p
, B0 @( R3 u9 A" \
* e6 O6 R9 h. G% p 3 Y4 k* f8 M; l: X
! ~! V7 @5 H. z& v [1 x! F! W/ l
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
7 l4 G' E3 ~7 [4 d+ ~
7 u! x/ x7 v& }# c. {5 S9 K' J1 Y! p! ?) Y) [; k
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}2 ^: @6 \5 O, m, {( A) ]! z+ D
0 J1 B$ o; S& {0 n3 T' O
7 s* v/ d/ m$ H
0 N [$ b8 L5 h2 J5 A
* e4 s7 i- N/ w. s( y
$ z* I( O8 X2 ?* e! F# s3 B 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
8 c: B1 R2 J/ o7 \4 _* S7 `7 o* x( r
: b( \7 A( C' m4 o' k& Y- f) d$ o9 X- ]
在is_allow()中增加对$this->savename的二次检查。2 `( e' Y% S' A
) {3 w* l2 W) V8 a! w
3 H4 `/ E; S& {: P/ P, l# z8 Z 最后# d+ H% I- F9 B5 f1 }& A( q
6 Z5 S2 t6 x2 [/ u
( A4 g: x9 ?- B) N5 l1 @5 @
嘛,祝各位大师傅中秋快乐!
7 Y/ k. Y9 ?7 ]0 } ! F3 [; j# Q8 X5 s
4 a+ q1 V8 H- G \& w2 |4 X
' t2 j6 S/ U( Z% l. i6 ]% V
* g8 E ]- M3 W* O
|