8 N. I1 A1 @! H% x% h" F0 S5 Q7 }7 f8 B- U/ v/ `& Z8 @6 P
3 O* ~, L) _6 x( W, K
+ H# q7 f# Z3 e \ 前言
( p! C# Z1 ^1 k6 L n7 Y8 N4 Q; c9 i+ {6 E- R& {5 w3 j8 y! {# ]& m
, [- X' f2 x. N+ t# h# }+ c+ ~$ B
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
1 q- o: A1 I7 R 2 _7 v, B3 J& \, G
; |, t. T0 f$ l0 H1 Y9 v+ b
/ z* M, i+ B" r1 c
* U& f1 F( o0 o( i4 o
+ r" z- I4 e& ~. w& m+ s+ V7 ~ 漏洞分析1 f) k4 d" W7 G9 D! k
4 ]2 S8 M" c* ~
! {1 a, X% ` L( i5 ~! K2 y* ^" s 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
0 x# e7 U+ [2 N
8 T, y: J1 x, P+ y# W
2 N+ u+ f: Y. @5 t c
' F7 y1 O9 w( M" o# ]/ `7 O) j. f 9 O' F# Y! q# Q) C1 f
; M! K! J' z, o" ^+ Q- @4 D
对应着avatar.inc.php代码如下:! c9 [' V% ^4 `% ^
0 p1 Y8 |! s9 L9 m) ^
) c7 B1 x0 F# _' y# p0 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) {! O0 c0 A; o; u2 l1 p
$ N8 [. G' v: e4 b1 f- N
7 r; A5 Z# n3 w* P3 ~/ R case 'upload':3 m3 Y y6 ?5 ]& \7 e7 h
2 a& u( n( ?3 _4 M! F+ `) O' W4 _- c: L3 `% @! D& C" }
if(!$_FILES['file']['size']) {2 y7 L( E M, t, N) k
4 w$ z2 t* `# w- _
( m, }: g& Q; T5 @3 [- E$ J/ x. f if($DT_PC) dheader('?action=html&reload='.$DT_TIME);2 S- T+ N( m/ b% |
* c* L8 ?+ g% T& `$ S2 b+ ~
" N+ K( E4 Z, W' w- [8 f3 G; Q exit('{"error":1,"message":"Error FILE"}');# W8 w; S2 g" K8 e: D+ N: x
' `5 H+ _: z4 C1 R k7 B2 z) k* w* M/ W6 b6 K2 V* \
}
8 j% ^6 m' y- A1 A9 q
& h+ _) z( \! J9 p8 A4 ~0 z+ B& q6 S" Q: E1 d5 v2 _9 z9 F
require DT_ROOT.'/include/upload.class.php';7 A$ d2 H0 Z* [# Y3 I
: c d( @8 V; Q! M, W
+ S3 F& |2 M% E8 p# h6 p
" g4 k! D2 v1 w! A% L6 Z+ y, X. H
: M3 s6 q I) i1 L0 l1 U. X1 H, R3 \' M" K- Q
$ext = file_ext($_FILES['file']['name']);4 |# A. e/ P. B- E3 g
6 K! ], Q1 u) V# e* z6 M7 Z( u
: d, v) ~! m3 q" D1 ?
$name = 'avatar'.$_userid.'.'.$ext;
' b' b8 O5 J* w& x* X& y) w* `3 [) r* z
' x7 B; A. y( N" j) } f4 n
4 N# g: F$ v2 M9 ]5 E $file = DT_ROOT.'/file/temp/'.$name;
6 u$ g6 }- e _5 |
) N9 z; S) P p: r1 o; D- ~
' J+ e, n$ q# I! d' ?+ b& h 1 J. ?" I" _" x0 G* e" H: {
' j& \2 c5 F1 t4 `% G5 m' Z
! k& w# S! f* y& l if(is_file($file)) file_del($file);# K4 B- C: A4 M* d7 N4 ^
1 o4 }! n1 R' K4 b- {
2 ]" N0 [. _% o R7 k v1 \ $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');7 I- T" S+ A Q1 s* I0 j
3 e X$ x8 M$ C# m! o/ Q
$ O, p/ p, {& R* [' k6 V9 W- E5 d% p
( o, y6 d& u3 z. u' J, X9 Q1 s
# D) e) S7 R( r1 F8 ]
$ G: @! |/ {) i5 ^ $upload->adduserid = false;
- G& N9 p( i/ r+ W+ D4 e
5 g6 k# p2 o# f! e" E8 F# j$ L
5 v& v- k( ~2 O; T2 p, z* H- X/ } v
5 U+ k& P1 c' c4 I6 B9 G8 T' S , t% _, ?* [+ E' U" ]4 \0 S, j
3 ^! U# o% I8 l3 ^1 F( g. D if($upload->save()) {3 t! h8 _$ E* T( B! c. U
9 I8 A; z" }& R! A( T5 q
- y" v& d1 h- ?2 X$ @ ...4 V) O, R( ~ ~7 C9 U) L
. v* u5 a2 n. O; f# B% W9 e% {! ^/ w* c# x; u& `/ G0 h( f- K/ X
} else {# X0 v( ?$ [8 y
, ^8 k4 q4 D$ [, X2 G" ?
; n e; K: f" d7 n( | ...5 w7 `- i j! R5 J: _
' {8 e# m- b" ?+ @1 j# d& g! `2 a& [/ b* P4 ]
}1 z- W$ `1 i. a: `
+ G* M+ v7 @2 K2 B( Y$ m( _
. w8 h# P" e# [9 X5 @) L
break;5 X9 y/ Z& f( f; r* ~& R" F
7 ~1 F/ q' |9 s0 ?6 h% W# S1 W+ Y5 f
3 z+ w2 O; u8 J- v' H
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。/ h; i! ]( P5 z( g+ ?& y5 c9 L
& A! U4 w3 u8 K5 b8 [8 e3 [1 y _/ Q$ \7 K) i
upload对象构造函数如下,include/upload.class.php:25:
" }6 n. w+ M5 ~6 `
. D2 S0 ~5 H0 x0 r ~" G% F; a: A7 v$ h2 i: ^# A
<?phpclass upload {
B/ B; l3 {- W9 q w1 w8 [ g8 y. a* R$ S
! X+ s7 f% b: G' B7 h
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
/ R* e3 d0 I1 k* X# W5 h7 J, X ! N: g- K# ` O: ?0 u( ^* x
: p% [! w# h$ ?$ A( ^% z- Q
global $DT, $_userid;
7 G' l/ Q6 x* G6 L0 @" j & v- v+ }+ p- E" p
- \2 f5 [( j, n3 u: F1 g foreach($_file as $file) {
5 E+ p) {# J) _& j5 `
( U; e! w1 |, Y0 X- d* G) Q: h1 o# N+ ?9 a* [4 r
$this->file = $file['tmp_name'];
8 y) Y4 A3 o" O! L2 y 5 F, E# P! l3 W( o/ `2 L4 S' U# A
& |. [) Y* W6 {' U' i
$this->file_name = $file['name'];
) ]( x4 m3 G. U9 A' _. y$ a
3 m, W8 R- D: @7 t7 ?0 {# O O( c* H4 |3 z2 e6 U6 ^6 ?
$this->file_size = $file['size'];2 [' S, e( g9 A0 H/ P* B, J, W8 D5 k
0 j3 { C% }" ^$ r6 ~( a
' f" T, \; R+ e* i $this->file_type = $file['type'];7 j+ c' B, T( h; w
. F |6 P3 P& e; T$ S2 P( Y
) j9 I; {& `" _2 | $this->file_error = $file['error'];8 @( l( ]9 M9 Y5 m
5 L' {( X" E, _2 `
( P6 a" |' H5 K! k
- M. {. C: I" M3 p% D
# b' g% Z4 H; R2 Q, f' _; k/ i" o& K- d- r" r
}
: g! v2 d) C" Z 1 Q) a8 `8 a5 P
9 o; l' E8 g7 W; W& D
$this->userid = $_userid;8 `' D% ?) D% U
+ n+ e* e' q. v* c; ^! O& ~" P; Q" U
$this->ext = file_ext($this->file_name);
& z' ^2 h. S8 ~% E- n0 {5 u ) N" ^/ n' t i' ?$ E5 U
* B8 o& Y* E! [! v2 y2 d $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];7 G+ ?1 v0 M# r' Y5 J1 ~) B9 v
5 z- K6 \5 o: X6 ?; X, u" _2 `
2 s6 R$ Q4 Y. k$ l7 X* q $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
" v0 c3 }& x7 h- E/ H+ Z& [; `$ s 4 Q; L2 l& R3 H2 ~
+ o! Z' W: r5 U $this->savepath = $savepath;, w0 z% n% G0 V. ^: V
, x+ R2 b3 L: V/ h/ q
( p7 B2 W' E# \% z$ V8 B $this->savename = $savename;2 Y, y* V! d. b! n# r9 J' I8 }8 F2 [
* h' J4 S) U9 T
* t! k# E! c7 D, K( [) H3 E; g
}}
# n- {1 V! j8 m+ \5 y # U! y& ?0 S; o' Q# m
- V& J/ p4 T2 E, R/ e( O, u& Q 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。3 u$ L6 ` G+ S) T
& ^! _4 e1 l0 r8 A# t
+ J( g* E, \. h4 S* ~& d
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 9 A @' ^9 Q8 k
4 a4 C0 }5 M% }2 O: w; ?8 V4 S7 ~' }0 ~+ m$ E
$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
8 z) I3 O- g# w# v/ A& d 9 v. }: M3 H3 `; Z
7 z' k' Q- c9 X# g
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:! F; M2 L& G/ N1 k% v/ U5 m
0 M/ Q" U0 i* [4 l. Y) O
% O$ v: {. _, B+ p; {
1 m. Q; m0 Y" G2 v5 H7 w5 a0 y; ?, z; _
: v5 f0 F: R5 ^" e! k+ H2 i6 W' Z) b# B+ X7 Q. k0 r$ j! n
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:2 d0 ?8 ]9 d9 B, u/ q
7 w8 y0 m" D4 i. x% x' h3 U( }) M; o% S8 |
<?phpclass upload {( N' B& M- J! K4 \
/ b1 w2 e/ ]6 a, |$ ]; X0 ?
. o8 [' j, s. L+ S, a! q4 E function save() {! l& i% J, ^% W
d, _* z: }" X1 }, D8 l
0 M) k9 @- I' g: F$ M: T9 f
include load('include.lang');# I2 P" w6 d4 |
1 l. U: A' u$ j7 ^) _6 y7 m
, Z/ R3 v2 A7 O8 M: P1 r5 _, u
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');4 w0 [ h' `; s$ W
: }) u! A! | s& m
- k1 i# T; G1 G2 U9 U+ B; \. p2 u
2 b9 G8 _* n7 y * O P8 D: h# R$ x% s Y6 O
6 i8 [, V+ u& t9 j if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
: Y! U n- E% h, \, [* J ( n9 o2 \3 l7 O( W' q# }: k o
& n0 X% k0 L1 F" {* ]; ]
9 K/ w' C& b6 h/ {9 y4 r
" u7 h+ ?% I. C0 x B! Z- E3 N$ u( }! X& S' h! y9 d1 X( ~
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);& [) A9 Y9 T6 s- c, `, f) p
9 O6 n3 |* p% Q
1 I5 [8 z# q# U* {9 A7 M! x
$ q; p4 h7 o- a8 `2 U' I' p8 a' i ! w: q; g- G4 ?+ n% p7 `
1 V' G5 ~! i3 d( F7 q( x% j3 e $this->set_savepath($this->savepath);" X- c" j% R1 N
) O1 Z {8 r' X- a i& s
2 ]* K6 w/ C* q9 P4 ?" d; Y" _, C $this->set_savename($this->savename);% o g4 ~! P9 B% ?1 ]( O
0 A% E6 r) C0 g
w# e0 |, r6 T- ?5 s1 O
7 I2 `6 L' H! W) q2 I# D6 b
* \6 g; u6 \ w) u. {: P
* n5 ?$ J- Z7 [/ d if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);1 o* U- i0 e) J2 R6 `
! f" [( @; [% `! M- K$ P: t
2 K. G( o6 H I3 ~& u* q3 o
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);) _7 {; z# \+ m. D9 q4 P
0 \. B3 A* |* D8 Q& \$ S1 H7 T% f- d! J% w }
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
1 J) k0 t$ y6 ^# R/ Q! x
' I( Z3 J0 Y7 p4 @: P( X+ Y+ W: T! b( `4 F% X' H j( H! B
8 r3 x9 x; I9 R4 H1 g % f4 U- n3 x) Q% M; h/ u% M
! A6 N" ?/ t$ i2 U $this->image = $this->is_image();( p: X6 }4 _. K3 k$ x& N8 a
/ C4 ~; c! H9 ^4 V1 W2 R0 ^
( u( T7 v4 m& C2 X; p# U
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);( m \" s! |! D" p: J0 D
& Z* P% [) `* Z. ^
) a9 R7 s0 E/ B0 V; K; ^1 r: A% K* s return true;
8 k8 |4 ~. \6 i) _8 ]7 Q # @7 `6 y' j; S a
4 ]+ n9 K6 b0 x$ n( E% y/ F
}}
5 W d8 H) x% x9 D& j D ! h. E. Z) ?8 \* t
# K8 _/ }( V3 |5 j
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:" n: ?# O, t$ K' q2 r
# t" B# Z: L: f6 i* d: o; a
0 ~0 c3 W$ r0 G0 T( |7 H <?php
- @8 a3 w0 k, p# [ S$ m4 @8 l6 {; s
% y+ k; c" |* E& ^: S
1 q' m, q' A" o function is_allow() {
. x, u4 f1 L0 G5 N
# f" P7 _. T1 `- y5 Z' J8 _
0 g4 s1 E; L& } if(!$this->fileformat) return false;
# Z' I* O/ n3 L! c2 j; ^* S
5 [0 Q/ f$ ^8 w' f: {7 f; F5 R8 A1 e9 L
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
6 |* t, K1 ^' \" ?' {6 p
1 u, {3 f1 t$ n0 v: c; B% Z! h# J6 |& f. D# {! W# C
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 r: g' \. n7 Y/ E- w# f7 r
0 X0 @ p1 N X7 n k# L' F0 Y' g6 J
& G8 c- x+ {4 S+ R6 u$ x: J' x
return true;
7 g5 }; v) G- ~1 T+ J9 v7 W7 f
: n+ A0 Z& s- `, g3 ?5 Y9 E2 j0 Y5 L: b: h: Q, o2 b
}6 h4 N9 {1 J$ b6 L) M9 n
+ b5 {4 A& {7 z' U4 Y* z8 l. b8 e
5 Q" {- \# V. n9 C7 S$ H 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
6 X8 ^3 b; z2 [ " J7 R/ i/ j0 A' h
- \- u9 A/ e! K& K n( C7 {! F 接着会进行真正的保存。通过$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文件。# S8 a/ C! s4 o: @' y+ N
7 k' a4 g; ?3 K; Q5 U5 ?6 G
/ p/ g8 H) e% y6 }
漏洞利用2 l2 ]! m' A# M D8 W
( r. n$ K- M$ M: s" S2 k* \; a
4 Y7 ~% R& o1 T; {: ?! A 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。8 |9 [3 D/ x9 V, g6 c4 }
" P% Y, R. k: ?. ?
+ r/ i% w5 |$ E- Q4 v, i n ; ^: h7 q2 v" v* Y- ?6 i$ L: R
- ^, E8 N5 z f" r4 D
, f& Q& V3 R6 o d
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid, S9 z3 E/ y% y+ v- f# _
( |3 R/ N& Z5 S7 E
1 {0 h5 T$ G* _4 C9 G/ w# r
不过实际利用上会有一定的限制。4 F* S7 s: M: V, e, H5 t* }, c$ L- M
6 i9 \8 b# }# m6 ~- F% d6 U0 n
" a# J# [3 F% L) t3 F3 q( X 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。" g/ _$ E& u9 u- I' Z: S E
# m: r) n8 Q' t- e/ g3 B0 R9 Z C' r8 J' l& T5 |$ m
, U& p1 U0 v9 T( A! J7 V
) l7 }9 Q2 A8 c5 \2 P9 y! D( M! G8 }+ _8 K7 Z/ g
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
; V; ?; I4 w; V M& I 7 f; L) \: r, f
# o j; Z% w+ p% k* t7 S# S8 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]);省略...4 m- }9 q+ z& U* c2 t( h
5 D' \: p3 A: J; Q' J0 H, n! U+ c
* @! x9 S7 z: N! a! z 因此要利用成功就需要条件竞争了。- H& O4 O! I& h% {
& P! X3 t `5 J0 z, v. p" m6 V& e7 b, f" W* H
补丁分析
' \/ V6 s& z3 _- a- _# ^
% U: y& O* l6 F0 @& Z
; o; R+ Q" ?% s8 n1 e
( h2 f2 V" s" I/ j8 c+ R, v
' V1 x# ^2 r7 `( c- ]: B; ?0 d4 M# {* G2 P
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:+ l4 E8 |6 C% u. S) p
$ @* u1 M' }: a+ S
; p: p9 l0 H' H, X! _ P function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}6 X2 x% d/ ^4 f$ x3 s1 j
/ {# A. q0 p) k8 b( Z, g
9 }2 W# M4 _8 ~5 W1 i0 Q/ \3 i8 { 2 u, B7 `$ o* w9 {$ t U
7 L8 C! H& A/ k/ B7 V+ G9 l( G! z+ L4 |
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
; X1 x3 \ ?# {1 [. r8 i
4 |! ^4 f, z# T) |0 m9 o1 I, d+ d: d, k* i
在is_allow()中增加对$this->savename的二次检查。
& O* N3 }4 U# y/ G+ C
/ C9 P7 Q4 u2 l1 j7 J3 h
9 Z( l, J8 n# m A& z. C9 |" H' J7 h 最后
/ B& l+ x* k% J1 n. F5 I/ w M+ D* e$ I' e8 e+ [% `
6 f& K l) ~7 K8 ?& \3 Z 嘛,祝各位大师傅中秋快乐!
' F( C: Q, u9 d; G# k- m ( O R2 H+ z0 D9 `2 @9 S j
6 @+ I1 K* i! k* v# z
8 ]/ N+ j5 ^# O3 I0 k
2 j) z8 u$ k# o* ?/ d+ _
|