) t) g1 U/ C* R* O& t# z* i5 b6 @* r9 o% ~" i3 G8 [
( P9 w+ m1 k- s) b+ Y
6 L; I4 w* P& B2 F E
前言
/ g1 ]; z# S7 ?) U F7 R+ E1 V; S1 p' [6 @1 w# Y
! H: x) I7 e6 D m 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。6 @- u& u" @& ~/ `9 V R
2 _: e: c& H% k% J) f
7 V! R# c, W! _! H 4 G- M6 E0 a9 ]# e: V$ k1 B
$ y' I) t4 ~+ K Q4 `7 V) W* T
" z \7 e# Z7 u9 A4 o7 F# y
漏洞分析
( y5 S% K! V# K0 E ?
' [& b A6 X( \, \4 V' J$ g% @/ g
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:3 t7 Z: b6 L- Z( T0 E
$ B! B, r; v) S% w! b+ q
/ Q1 ~4 L; z* R2 }8 p
; W g$ i, Z- y$ G/ l
4 t* q. p( V' {! }! f- T" `
2 M# {6 I3 q* M8 Z% q 对应着avatar.inc.php代码如下:
2 B4 m/ {# t" g9 {- q* n
, H/ q) G: z- ?' @* g
# N0 ]9 |: @0 v <?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) {
: Y3 F# h+ U. g2 a" @/ H % G0 f$ N9 i$ R! J3 A
0 ^3 x# E5 X [8 K. Y case 'upload':
7 X' g( z* O/ Z% a5 Q+ x
2 r4 v/ M- j! _7 y. e6 o9 z
% a% K: a& U: c. Z# g E! Q6 X" K1 S if(!$_FILES['file']['size']) {
k2 P% b! G! y( @, X" c; v
. n4 e r$ V8 v- e l. T6 ?8 \
2 {/ G1 A5 ^3 _" C# y; p if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
' H/ i& k1 R: q/ Y5 }& g [( n
- [- o1 D$ f+ X' }7 t: L, q) ~" W$ @0 e$ j, S
exit('{"error":1,"message":"Error FILE"}');- X- m: F6 `9 o
& e- U$ L: K8 h( Q* Q) w5 R* N3 E- x* T
}
; k; M; Q1 k, R7 K! s
4 f8 p) U+ E5 \3 i7 j3 K" n0 n" H6 p: I
require DT_ROOT.'/include/upload.class.php';% |; ?/ R, h4 @) r" H
e% B3 Q, Y8 ~* A& P0 ^# e) J$ e
5 A: v0 u O7 [, p
9 b6 x+ C. F7 Z0 R/ p
9 m7 q$ ]7 a8 L) N$ w# ^& h$ s $ext = file_ext($_FILES['file']['name']);
' a) {% R0 o% e o9 K 6 X3 P" Y5 y7 u" @" A% G
, i3 i n2 X8 M! M p- m
$name = 'avatar'.$_userid.'.'.$ext;
6 d0 n9 i! d4 |4 X; Q/ i3 m2 x ! X, K. l6 B3 c7 j) x( B7 k2 Q
7 N; }! W( s7 L2 Y1 @0 a* s: K
$file = DT_ROOT.'/file/temp/'.$name;
" }: p; G4 v7 V ( V. y) }" u& \8 R& q4 u: e5 p
! c2 x+ o- e* s4 V
4 @$ j% V+ v/ Y1 k * y0 `2 u2 r5 m2 @+ ]1 Z' v
8 U+ O* N9 _- X A0 b- j if(is_file($file)) file_del($file);
1 o, O+ j8 L( S7 l
; K' ?. P' a6 ?" k- |+ E! ] q& [. t8 A# A! |0 O# J' t0 v5 `
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');1 l7 O& L7 j/ Z* ~
" A+ ~, u' b7 e% Q" y- K
2 |5 L. J% ]+ ]9 i! B
9 G" i$ r0 L/ X5 a5 z8 c% u
; N/ X& j9 m1 {# T' K. c7 d V' w1 Z$ e" @3 ]
$upload->adduserid = false;) b; Z8 v5 y- q: l g7 U
4 o- g- w" S0 L8 a f
1 m: T" V: L# i- Q: X
: ?" u; Y! g" e, ]# e4 w8 T
2 j- L( }3 [1 J4 v6 h& D, i
5 F# N" Y+ o6 X3 q
if($upload->save()) {) ^) j! D4 L# l" R
/ j- }" _4 j+ j1 V& ^
. C, u3 e8 f8 l* F ...6 m) K/ d7 Y; D8 D
7 H: v- q( t7 n8 h) j
7 z1 n& [3 Q& v } else {
K ~, ?, t$ C& x! X$ C4 q" j7 O : G8 Y- X# \! N) P
: g' `6 j: ?, W5 \. g4 K4 o
...
1 e4 t& t, O% ~6 P# o ) f. t! a4 K2 I6 H
! c9 t+ e; w' q) F7 l0 A1 S. U
}
% X3 E+ T% r. [9 g, |8 k# M+ } R
2 B4 \3 m# b3 }. b* W1 A0 R, L6 L+ s" a8 f' z: ?& @
break;. f, A$ B" B0 z* F# A
" s- i% _, m6 [5 q/ h# `3 b+ r$ K D0 Z' g" H/ V4 B* @
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。3 p9 X4 \% [* F/ C6 `5 p5 j1 t
/ V2 E0 C3 q, {& T
) w+ h, C5 d8 D$ @) ^ upload对象构造函数如下,include/upload.class.php:25:
1 T0 |0 X6 b% Q D0 z# q$ v2 ` ( e1 u- a% d# [
U v Y1 L) f2 B <?phpclass upload {
+ J% c8 n I9 ~9 O6 H) F1 k! y
2 E$ N% Q' r* E
* S/ w4 p8 i/ o* { F0 n' F& P function __construct($_file, $savepath, $savename = '', $fileformat = '') {2 `" {+ z9 t' p, m* b* D E
- \8 g0 d) O+ F+ G# I" [
1 x) @" _+ t7 e! t3 s global $DT, $_userid;) N- \' ~: |. @
! b, y. _# x+ G) h4 b" { [7 B
& n9 Q; B8 l% v foreach($_file as $file) {% a$ ^. s+ d( `# ]" C I
1 b% Y9 j4 ^: l g8 o3 a
, w0 H1 A2 x5 Y" H# y4 @
$this->file = $file['tmp_name'];" y4 i3 i9 e8 u _# C7 c
' Q7 z2 t& Q$ k6 p
9 e/ H2 F! B2 K5 U# i $this->file_name = $file['name'];
8 l# ?3 e! p! P1 R9 P8 V$ c4 g, T ( u+ _! [1 h. `7 O# G) @
/ s4 v& S* H( V& W7 ] d. A
$this->file_size = $file['size'];
, M9 u5 W z8 s% j* B
( S/ W6 o1 e# f2 U2 N
) i5 _3 I0 O) H7 ?) | M. f3 A8 L $this->file_type = $file['type'];4 H2 |* O. T. |) m B' ~- W
$ V* @ v8 ^9 f: [6 j. V( U' g9 _5 W( w, Q4 l: t/ c
$this->file_error = $file['error'];
) _3 E1 S# `4 A. l $ L% Y; F1 W7 u( p$ {
: _6 g# v4 `. }9 p# X7 z: q
+ R: ]- S' ]* Y3 e& r2 z
- M; E2 K7 p, @7 [' S
5 Q5 Z! a' J( m- ^+ ? }
$ }9 r9 X! |' B# e9 b$ R
( ~* s: H1 C c5 ?! B
& `$ w8 X" [4 ]5 T+ b3 s $this->userid = $_userid;
: T& \+ Y7 u* v" M9 w1 `2 t ) {% ?" y* B t- X) g0 z2 t4 h
& M2 v( a7 u( ]" Y7 U" x $this->ext = file_ext($this->file_name);3 Y2 L1 n ~$ L2 H. t7 l: ]" b
# E! z' K/ l4 v; r' q; {5 T& s$ W/ M! K# V: B0 e
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];' O* a' Q" Z3 @! t
* [: s! t9 R, ~: E+ [8 J% b7 S
. A6 D+ w* D1 Y $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
V2 ~6 p d6 k4 g2 \1 K% D 3 \9 B4 x4 w; v$ K8 E5 H
1 M& w+ t0 P/ P$ i, n+ y7 X
$this->savepath = $savepath;
3 J8 I; L2 U9 l/ r' x& O
. t4 b6 X5 L% t. h3 o% N2 m8 {6 l! N1 V" D5 v' l0 z
$this->savename = $savename;8 I9 h0 W+ u/ u! m( D/ I) C! q/ E
7 P, W8 Y2 U& G% o' o9 G. p6 X9 K5 K- ], z3 ]! z" o
}}
% y% \ a/ n4 b7 U% G9 A9 x 1 S9 i: S$ ?/ u: r+ D
7 ]$ \$ A, H) Q& V* i
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。3 k& ? b$ o2 H# s
. t; Y/ g1 d O( B$ X) ?2 Z, L" |: V6 T% N0 L
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 & ~* k& Q5 F; S# G; F3 a
* ?4 c( D7 X; g; k
- }" g- h2 U F $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.php4 c5 D+ L' |& o; Z9 [
! v) x# H- M1 A! K
: m! k( H, ^# r1 r: y2 n& a 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:7 ]( v$ s( G9 h, U# l/ ], x9 n
9 q' {+ y- s1 L* t: n( R
+ w4 {) D8 k' J& G% E& K
0 q0 D% @9 c3 Y6 e7 ^
1 z6 b, ?6 ]& a1 ~
6 E. @% {$ k' Y& s6 d7 v9 K$ g$ f 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:- L2 \' [7 Z" V7 _/ |% j( m
( ^4 E2 J! k t1 X1 q, ]& P. @7 m- g v: |0 E
<?phpclass upload {; X, Q( P B. W/ e
. r5 H3 I5 T. K; P
- M8 ]6 J( k/ z1 j- g# N+ s function save() {& i i0 a7 U* t0 o) c
& d5 s* G0 i1 |4 `' @9 r) E; }; g1 B8 j1 v4 O4 O
include load('include.lang');- ^8 l0 `6 f0 G$ ^8 W
0 f0 y7 q2 O4 v! b) h) s
' r" x7 K# @6 B' }1 G0 J; ~" t
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
1 u) K& A! \* M& j( } ( h e/ x9 o8 g5 N
$ v. V6 O! t9 N' u) j: T7 ?
- D$ b" n$ h9 f4 p5 i; _% j- |
4 A1 e/ C a* V) u" W/ @4 L! a+ G# c9 R& g8 I4 ~. C, u3 a
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
7 k b n0 p* W4 P) v ) t, C' b" B) g& b2 T6 G
( v5 S4 a5 ~+ q5 N9 ], P1 A) K
. i( r9 p4 g( O* `( F
# X2 b5 @+ [" M0 G" V5 \, B( E( K1 y$ F* u5 u
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);1 J9 I8 f$ {& A
. B9 b4 }3 j; P
: ]& J' G( |; U2 n1 f
" E: `; j0 C0 i* o
7 @3 m/ }9 ?2 G+ k5 ^# @1 W/ |2 {, c! N. _8 i `9 s
$this->set_savepath($this->savepath);
0 X( a+ p+ ^( I) a( T# W% z- u
+ n- O) U; n* N4 v7 u: e: s( P3 M- ~
$this->set_savename($this->savename); Y/ @9 O8 `$ z+ r
. t0 t: r% A8 y
, p6 n" `' ^. j+ A! B* ~ Y + U. V1 X a# `
$ X) J5 I% d( x' l6 t- M# ?3 }
8 R6 l- @ i3 H5 b if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);; R0 c4 B2 y- }/ w8 g0 N
6 _2 I1 J- P6 W9 N7 ?
% B9 ?+ r$ X) d, Z4 p4 I' u6 Y: |# j if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);- Q) }6 V8 y$ g3 N' B
) d) r2 z8 A) u9 L" p N8 n" U: e) C
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);0 J* t3 N' @' e5 F* p- V
. O9 w+ L+ D& s' r5 T4 Y6 N
' Y7 w! w4 a8 @7 K
& c Q6 t( X, V" j( t- J. w! ]
( E% C9 M/ _. D2 N/ H% w
8 h. m1 _$ n, p$ C! G $this->image = $this->is_image();
) S3 h! e# Y8 |! |, i2 A 5 n- T. K! w3 ~2 u
1 K/ |; K; ] S+ L& R# ?9 t
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
) a% }. J6 ?6 X! f! Q/ b! N * ]% I1 o% l( \4 p. V
4 n$ _3 ?! a$ y3 \+ J
return true;
( d$ D6 t3 i) G' b0 w5 B) Q( b( z 3 Y2 E# r0 G8 _- c( m/ Y" P, o
g: _- }" H; |! {$ L& K
}}' t' z' X+ I) L. l( _( I" f6 T, M
7 H S. o$ e# F w4 R
( c& y5 r1 b: P# M6 X
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
# | x* r6 a+ a3 k " O3 a" M; _& P& [5 U N
, s2 O% ^9 O0 ]% Z" ] <?php
3 [: i% f2 q6 P- P; l. ]! D . e5 q- u9 V5 @3 W
3 V, M/ l: e# i0 V3 ?" U function is_allow() {5 D0 m3 ]/ K( ^
& f0 j8 t7 {" D! E# O7 w( Y0 S. _
. O/ L8 _0 ^0 {/ h n* S# ~4 c" S if(!$this->fileformat) return false;
' \) j- Q+ r5 o7 V- _ $ n. o9 Y8 o0 z; a- y
/ Z" u4 T8 t* ]# b$ ` if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
. h7 x8 i' i/ N8 e$ x
9 d D" I" G( n6 T" x# V
W( I* g0 T7 R- y, d9 a' @ 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;
8 z, R( q- [2 S
7 O% G! y8 ^: n% I% p9 l* \- k1 c" P& F* ?
return true;* s7 R# P n6 p% M4 m) G/ q
i# S: b, g6 R I7 P
( ~, e( O+ B: j% P- ?, d% N }8 n. j2 z/ o9 t1 c+ U
* Z( q' |- a7 r5 d4 Y0 t( j/ {
" g( _8 T( X" Z5 b0 i8 s- u5 X, E% ^ 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。. ^1 N8 i# |$ ?
/ A( v" }: m: n2 e/ V7 t: _+ k. {
0 J5 ]4 C3 g6 j 接着会进行真正的保存。通过$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文件。0 ^! f% X7 j# a s1 h3 K9 R
$ `! L s2 u, [5 Z9 b0 z' N
7 |( o, \; }* n* N 漏洞利用
! \( i9 X o: Y& |0 L& ^6 ]1 f- o; ?9 {4 \9 G8 B
( F: |( \! `9 f# h) m 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。% s# f0 u( K( e. T5 b! Q
+ A( s1 r8 L2 N
~6 i! R) m$ ]9 v$ @ ) L" Z' {. I, w+ t: ?6 r
* o9 Q" R' Z: |8 W% i# j& R
: j. g, N9 [* [8 U
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid. h7 Y2 e( `6 X8 b8 b
0 M$ h& K" v0 y! K
9 b8 O& `; R' p 不过实际利用上会有一定的限制。
, ^2 X1 n' S& r# O
! S8 v; F# m9 `
7 C2 K8 L& f" B& u: A: x: ~/ J 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。. M* |4 r7 ~$ @9 f" I) Q
+ x5 W4 v3 t9 n* R ?/ K
* E2 y: t; _' W# c 0 f5 j7 o% Y2 z8 z$ B9 g0 i; i
- x% B# W/ ?9 a! f9 |
; k& b! a8 {9 U+ ], [) p 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:3 g/ J# y: ?% q* \$ u% {2 i
+ z) h. @9 u7 A5 M3 l$ F# B) p: A: @( D2 X6 W
省略...$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]);省略...
+ ~/ [5 k7 c6 I" @ 1 R" m/ n$ n7 c# l: M3 B6 D
% G7 @' b4 v8 m9 J, e' ~
因此要利用成功就需要条件竞争了。* U! |$ F* V; v/ [* a5 ^
# z H) q; y( h: K7 b: Q: k1 J2 [% r: U0 T* `1 A' v4 k
补丁分析+ t2 z: R4 d9 l2 z; U; K6 k% T
$ i$ D9 d" ~6 @1 P( t
5 k2 ^: }2 H6 ]; ^. Y ' j5 u* \' n; H0 f1 ^
H& ~0 z1 D! ]( u: q
8 O- u4 `) |5 l/ X( _" i
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:4 f2 n4 K% | v2 j
! p6 i( k9 h' f5 W. p' l
- L V7 s3 O- P
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}5 L: h# v. U. i8 M* q9 u/ k$ k
3 D$ S" F8 T4 R' o" V4 N
+ L$ q; [7 t1 _ b
" R1 H9 N# ` F# O! b2 u, J
8 ?& o2 s$ t4 G' x; M' V& ?* F; [: P. D
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
) u6 s% Z! X: t) c% ? . x9 i$ u, X5 h; D$ x, N
3 `+ o1 d* R# q
在is_allow()中增加对$this->savename的二次检查。
& [* G& R; y% E: d0 o $ S! C |# B. m4 L' n
( h& f% j% Q8 c1 l
最后; S6 {- |4 f. N. L) C
/ }2 v3 L/ @$ s* ?: B
2 \0 J: }& B( n, q3 [. ] n2 {' T: T 嘛,祝各位大师傅中秋快乐!# }/ j7 Y/ |# p
n M3 j2 q+ f3 e/ j$ y, R4 Q" E
8 ~, }- K* E( i
: t/ S# H+ G b
4 H- E e8 U5 K6 u" F. N
|