! A- _ N" K0 C- ~; n' h1 I/ `: y7 Q* V4 b
- R3 ^' c) w& L$ b
( [" ?% l4 e8 u/ c* H% d 前言
" F6 |3 i, V9 Q& J% H; f! m
: \9 U8 Z5 `, h9 P' k: D
# j2 y& ?: j, }: \- ~. p 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。8 u0 r, f3 }$ K4 U0 f
w$ o8 p$ \& A; C. F
c) T( }" V% _3 F/ @9 i: k0 k G# O 8 L: F) h3 T4 _3 t E& L. Z J! H' p
3 ?! ^: D9 M* Q: C: q
* E G( J, R Z3 i! Y
漏洞分析 V& a, U' \: q
" T+ n6 j5 y4 k/ Q
0 O6 ~8 d8 {* I! K* u! M; T, Q! D 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
! J- k6 ]/ ]% X2 N
- G v! X- _7 E, u
) N2 ~/ l- \) ]5 b1 [0 G4 Q
% \0 F V3 F% h 2 T- d8 l( h. q* O4 f5 P- O
j- w2 d' e4 g) u6 u! K
对应着avatar.inc.php代码如下:
# `- q) O/ H4 L
# O1 s( Z! `( I& X/ J; O; H; H- Y# Y4 N9 D0 e
<?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) {+ e1 H6 f) C" V. ~3 Q7 [
7 U1 o; M& ]! X. G8 U
) x2 r* ]7 M( K7 m) ?
case 'upload':
" m, d+ r n7 Z" t
& U3 O- |. Q" X7 f, N8 K+ T+ Y. c" ^! O# j) @+ w
if(!$_FILES['file']['size']) {
* X l! v( I" Z) o" |7 R5 U : ~8 d( ^- a( B5 w
5 y. S# g1 p: A. Z$ z0 T: c if($DT_PC) dheader('?action=html&reload='.$DT_TIME);! d& w9 I: i/ s, b5 Q
+ C1 t4 _6 L$ G; @- O# l6 B9 \" ^1 m8 J- }! b. B% i6 x# t3 o
exit('{"error":1,"message":"Error FILE"}');9 a5 }5 T/ C# D" W' @5 n
, B. Q/ b+ H$ Y- G9 J- N0 [# d* ~# W/ u4 Y( N2 m M9 H: D
}3 o9 ^0 z7 H+ n) c& ~/ Y1 P2 S# H
) B. K3 D- Q& ?+ r; R$ t( Q
& @0 B1 s; a k3 L6 N require DT_ROOT.'/include/upload.class.php';6 G! c; ^( ^7 Z0 H" E
3 N/ Y! }. `0 z h- C' J: a) W
6 V, D3 m; I m3 R' \1 ]1 e2 Y; d
0 E0 o& S+ ~: x# P* M$ P$ c* I
5 f1 n- [% i* X1 o) j8 Y9 r) I
, _, \" z! P$ q; n6 H( x $ext = file_ext($_FILES['file']['name']);
$ L8 ]2 K3 e& }" L / ^4 ~2 v3 K* z# w" j+ Q
( ?2 v: @: Q# r4 k: Y7 E* p& Q
$name = 'avatar'.$_userid.'.'.$ext;& w1 p$ n) u% E! ^
3 o% I4 s7 l$ q. e* n2 v3 o& \
$ o) ~/ P4 r; V1 r $file = DT_ROOT.'/file/temp/'.$name;4 f+ F) E ^1 u2 a
1 e/ z: ^# g4 a6 R1 X6 _3 D; r5 p/ B I9 q- i% ] p
) p7 x7 M! w8 ^" E4 @/ e% r8 D
) V* C8 Y& V/ G) b
& y' [6 H0 e3 V# ~
if(is_file($file)) file_del($file);' f- B6 q( O! L5 ^* T6 w
7 d6 W: e; Q3 U3 @0 w% ?1 s
\( ], B4 K! Y. N
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');6 h" A4 S6 \$ G0 C W9 l' J( p: a
5 K; g; ?& }) t$ N* W2 W& X( I( j1 f$ U' p0 o1 H0 ^; w4 Z' f5 |7 A1 I5 S
& C" b {2 Y" y2 X r; t @ / O+ D, X/ \6 M% t% Q9 c6 I
; \( I$ V# S9 C4 Q
$upload->adduserid = false;
5 J9 k9 a7 H/ H* I" F: N' O! v . p' a. p: R) o; p! @
l2 v$ T l" g. g* S2 G' b
& ~( k5 N$ g+ j9 ^1 W: @
7 `) j9 w3 Y4 r( M7 R2 A+ X1 L% N+ z* q+ I
if($upload->save()) {+ D' e9 y3 G6 Z- a) R D K+ o1 ?
+ s3 F& S! h5 n
* e' P% d2 Y( H6 S .... L* _& V( @- T. T
8 t8 X$ z: O. ?! t# m4 n2 M+ N! X6 k6 O/ Q j8 m- \* K3 d
} else {
5 |- R& R; u5 U8 z; q, S; [; }: R
2 K5 B# r) Q; ^ f
1 A! @/ X7 U. A" \7 }- P, F ...- e; X7 U) |9 a& g5 g0 J4 W
& G. t0 A/ a! @! W: J4 \- Z
' J; O# ~$ ?! `' S) ~ c* C }
# |8 a; n+ s. Y7 ?% u" g' I+ F 9 o5 `/ i9 o: q6 G/ i
0 P0 D5 Y2 i" [" M2 M6 ? break;
1 C, `& y+ ~3 A, E, x& { - G; {2 i' g4 ]# S/ V! L3 O" I4 f
: N7 }6 n' ?4 o# _# v- q
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
$ S" ^4 o' T% U E7 ^) Z
7 c- P& {' o+ ~1 K+ G L$ O; _0 \7 n0 o) R+ @, j5 K9 m
upload对象构造函数如下,include/upload.class.php:25:' ?% ~& {2 o* X8 W3 m' @/ r
! b; k1 q1 R* Z# W# l1 B
9 f5 K9 y2 ^, H9 W2 l
<?phpclass upload {
' Z+ [' i; C- h$ X, q" o1 T 1 n8 k( r' X4 x- @
* P; D4 K; B7 f' M" A- I/ f function __construct($_file, $savepath, $savename = '', $fileformat = '') {/ o3 ]0 f: n, v) M; x
4 |9 S# r% ^. p3 ^- W( m
3 @2 P. n6 \4 A* Y9 z7 E) S, x+ F global $DT, $_userid;
0 n* o/ R. Y7 Y( a, n 4 G$ Y- D* V1 s4 J# s
( f! e0 [" O, u! s2 j0 ` foreach($_file as $file) {
; N' M* k _& M! S
" ?; d) G8 h* E' P3 M
1 k+ t. ^' C# d& b( i6 a1 Y" f' S* Z $this->file = $file['tmp_name'];# o' _6 b0 E4 h# A/ B
# P1 f7 r: O' b; n7 Y- Q2 |) k# o
4 o b% y$ m; ?. u $this->file_name = $file['name'];9 I5 C- c4 | n% D5 u
, u+ [+ q7 B, R% E3 A) c6 c
) R- p0 G1 O9 B8 G/ S: A5 m4 y' ]- a $this->file_size = $file['size'];
# g! c8 J# t9 o# U$ P* p
- \, W' J( [: O I' t
- \! Y+ M+ s4 W4 X0 w $this->file_type = $file['type'];2 H: [ E2 w: m* q
2 ]( S$ a, a6 y n
& p/ }: N+ K) a- M9 c, i: W) b $this->file_error = $file['error'];
0 `0 ~2 g) O( g& P0 i2 c; K # n) k* s& H* F
: j7 ?" t3 _3 `
_# K% q% _1 T* u+ l, G4 r. ~- v1 ` * y& j: \% f. o1 \
% F, r* H% X3 {) Z- h
}
- |$ x4 {; v+ |1 J
) k) f) ^& x. [" f1 Q- O F7 B+ Q0 L. ^2 E. Z8 Y9 y" O, H
$this->userid = $_userid;! M6 `6 }* I) U5 x' i" x: J
1 Q1 t8 `1 R: M, R- K6 q) H' k" G
8 x$ I& Y/ G- Z8 }3 U: Z( V9 s u. Z) P
$this->ext = file_ext($this->file_name);1 K2 h( c8 z0 v% g p/ p
. u/ d& P9 y/ `1 N
c+ Z; B6 s2 }! u% {* j $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];5 W+ S q* z7 U
$ r6 ?' ?# W+ T2 a, I% g3 T" g+ }- n
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
5 c7 W. q/ {, I6 w
4 c5 @. h6 }) i8 l6 g8 `
r( F% p# ^# L" k. h $this->savepath = $savepath;
2 s4 `- L) |+ W0 x% |
5 }& [/ v' q, Q Y+ f' U) L
# D X& r$ A& I $this->savename = $savename;
) m% `4 ~ c6 F0 x2 ? % l8 y4 A; K a% w! l$ w D) E$ e
7 v$ E& h9 N/ i! d. K }}
- B' h5 @5 e9 S4 ~ 8 H6 ]+ K6 h ^9 U% B) [
, k, r1 `. ]; \) H" o 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
, w# c3 w& a" P \ A3 l
- t1 @- _8 n3 @* I, U. j2 J
8 M2 z6 t: Y } 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 ) ]' b) W+ ~* ?. I2 t
9 y, r7 n% Z5 w1 n$ t7 h9 |7 ?
' K: d( j0 n$ D7 ?: G5 ]* m% h $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
: _1 i5 n2 @7 S2 \$ H+ E
6 b5 E9 g+ ]( P- D+ j! m% m9 K4 I! I/ A
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:. T, B( l/ a; ]8 ]8 k7 f
7 v1 f0 U# J' L# T
5 k o- J- V% @) }" C
1 h$ @5 @- @+ ~6 Z* c
! y+ o- [" S* L6 q0 U* w6 o, o/ Y" m. N0 }4 {
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
0 _$ F4 Q$ o( B4 J' Q* Q
8 M$ U$ _% w( I1 r6 [8 L/ |0 G+ G* K$ O+ f* [, J( {: n C- q
<?phpclass upload {
3 H# X: I/ T1 s8 v8 S$ E) R & t; k) h) z1 r9 }
* x/ ]" c1 `7 ]8 i
function save() {
" S' l3 A, d( ?" T/ [5 M3 ^
9 Y9 @1 l5 i0 C" ?* s( K
* x4 E3 |: s2 A; x: }" A2 a. |4 K include load('include.lang');+ s4 [3 V( b! \ n- u4 s
& }* S Q: g% T7 d4 L8 ^# d
. V! S9 z P' s2 F' c
if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
: P3 R: Y$ Q7 |* G8 e+ Z0 L 5 n3 i) W5 q! b0 b
d9 v* f8 y4 l' \ A; z1 A
: v( Q: D- _" X
# A6 R: O" p7 o) @
" g& k7 {* V1 s/ q6 H; L2 p if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');5 V7 O( m6 K$ Z- C* \9 s1 F
7 `/ Q. O+ _3 j: t. x
7 C& A1 q5 N' J8 L6 I1 E $ o0 i4 m' X: e( l/ o
# m' j u: G9 A( d
- |* Q4 {# g% y1 P8 A% V if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);$ V n. C4 r. S4 t* r9 D
- N8 x4 Z4 E5 l" } a
8 R& @) U4 e$ ^
0 e; l1 r' U* M& K
r) W; l4 K8 b( h6 H8 U
@2 [0 `5 _4 R( ?, q4 a $this->set_savepath($this->savepath);7 Z$ Q: }. Y3 U: Q
" ~. w1 L+ Z* l7 M Y4 W
$ H) f2 W% o C6 m8 p8 p
$this->set_savename($this->savename);
. z$ r- R8 ~1 K9 h# c& U; Q % } X* r1 Y3 B1 a" ]
- X8 \; S, F1 M
' _9 |, @# i! P' P9 I- _
) s# X6 D5 \2 [1 _' m* ]( o: ?2 D6 C- L& h
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);/ x& r5 r; y) U t6 d, l
5 m" }# G8 B# x2 U3 Z9 _" `) i5 B$ f" H& V
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);) |% ?* q+ A4 s6 ]! \8 B/ @
/ x6 |8 G' r- L: D4 N3 X: L& Y& W1 x3 `; ^
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);: Z% r9 u% o! ?
& N% T: l' a1 y5 g5 q/ b4 o$ m
8 \. O) y1 S4 P% B( M3 v
0 v" d7 X& C( w0 h! g) l
0 |4 t6 \4 K& S7 A
1 _, C+ f0 q6 k
$this->image = $this->is_image();
4 [& [9 L) r" {% F 8 e* k' i3 B6 j1 A" k' k+ t
3 T, ]) ]( u% u+ r0 k6 F if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
0 `) G: {9 v$ `: N ; `0 O3 {$ ? P3 X
/ S, M$ k( f+ W: s4 h8 D return true;
0 f) v- M: l; @6 O5 _
, _* m z" L. r9 R0 D% I( d% `- [4 ?' Q( x1 \$ ?" P: \7 X- j9 l
}}# D7 A B% c! S* @/ v0 q( g
, z/ M8 H" O$ R u% S. e8 J" C. q# g
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:0 @5 n8 s/ x( ~+ @
( e0 d* b- n: q' g; C6 {8 ]
1 x+ k- S1 n% u- m! m, ]
<?php
0 }' \* a: F# r; J! R* X+ x" G
+ G: O! V" { ?+ W/ e
( s" \& @. t5 {7 f9 h function is_allow() {2 ?' Z2 Q- B4 r' k! E1 d2 ~0 `5 n1 w
! W7 S$ [, w5 K) B# Z/ g- ?/ g8 }- Q% Q* @* J# {8 b' K; c4 j
if(!$this->fileformat) return false;3 Z8 M& N0 [' d
; U- o7 a- V2 N3 [! X- W( p% d- Z2 I4 y% I! U' j9 `
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
$ I3 g. F4 W2 a( c( j0 {3 z
0 {# R" J. V1 l8 J- H. j, O
8 ^4 O# j7 o; ` 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;! E- ?: J1 l4 G% U' f! j
) R( y3 ^; P5 V0 | l4 }: X# s, C. z; ?
return true;
* N) b3 U( [$ Q3 Y. y) M0 j 6 _2 p& Y+ T+ ]0 ^( ^# R
: w' X- _4 A' B8 `1 h4 f4 X: d- {
}
, J8 ~5 x8 z" T; H+ X/ H/ _5 W
9 t3 ]* t: Y! Z7 W0 _: G/ ]. N
4 v; \) F) `. `2 k$ ]0 z' Z+ j 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。3 m1 |0 l4 f- n7 b/ Z* E* R
# i( s5 K! K& {0 o( b7 l
( c: L8 ?2 ]# } 接着会进行真正的保存。通过$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文件。
+ [& T2 ]7 t( Y. n% h& E$ a - w0 ~, j: g K0 _# r# s$ s( O
M8 t5 v' l0 C3 r4 X: h) k( q 漏洞利用
( }+ W; y( I) {* x. q# U# \$ _; O3 b2 y8 `- K: S0 R9 K1 m
2 U) [1 q. N0 m. C2 X 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。4 w& L0 j2 o0 d( b* [/ V; G
* V+ O a5 k. z/ o& O
7 O" _) T3 N" k2 i
5 v& L* d+ o, J" K# f0 x
" \4 ^" w% H6 P, K: g
. w( c5 u0 r/ Y- z8 L& g 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid" g7 ^2 q" n& e9 f
4 P+ @% v; Y9 Y) S3 a
# u: }1 T/ ^0 V9 r7 o- C. w 不过实际利用上会有一定的限制。# x8 E- _1 C. q
1 I! U" S4 ~/ n7 l% s. P
& e }6 P: ] V {* `5 `* F# U 第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。 G: D7 }+ k1 h5 d4 i; u: X& E
r# f# H7 V; W9 b7 m* c* q
' L+ C7 {6 `/ {- v. S' t
( M, H! u Q( j, H. E! Y8 ? % q; M( m' c2 s( [
& A# ?( U! j9 M& E* b0 S. R, l 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
% S6 ^' g$ c5 o6 \# z. i$ K1 E . l8 F" U! E3 v8 |" r
' E& G' R- g# b4 `: F
省略...$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]);省略.../ q: h5 [2 l0 j) A1 \ P
$ M( O, O) l- S8 O8 Y& |: ~
) h7 l6 a5 a+ d5 w9 z 因此要利用成功就需要条件竞争了。4 U" N+ P N8 l6 y+ R a2 u
) [$ W8 G; Z' x* r) v$ n
- Q3 N" m6 x4 {- f" B4 X 补丁分析: G$ w* ^% f* s( _. Y4 G
P/ Z2 h* z. [- l1 @4 k$ D+ U# u) V
7 J5 o: X4 c; l" n% f
1 n% }) Z/ }% j2 { e
! @0 x! F- D5 [) n. r9 [2 n( V& Y$ t. D* p- q4 T
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:2 e- k. `" k0 R" W: y) F* |5 e( r
! ?7 G6 C! d1 ~; x t) q# k
. N2 V. B8 ~- B# {) X4 i! [ function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}4 w2 p3 j4 A- @: h! O
' [6 E( X5 j5 U5 T# S
N" q m9 X0 W! n
! ?' |$ L! B$ @' T& }0 J e
P: |' c8 m; R+ Y# f1 ?$ R; t' c! x) u# s p( g, C8 o
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。- V* C) ]2 m" c3 L, }
% Y! O2 V* C8 s! M2 n3 V9 ^% M% c# i6 O& M0 v
在is_allow()中增加对$this->savename的二次检查。" V" c6 }( k4 ~0 J
% @3 b1 g, L- \- O8 ?8 ^. o- h' j0 ]8 k5 Q; {, T# V
最后
0 r2 i8 |3 K( I y
2 C8 E# V6 }+ Q# T2 h U8 \
% @% C7 P {% a" ]* k8 O 嘛,祝各位大师傅中秋快乐!! ?7 w( C6 P- G5 C% p. Z
: r+ `$ N" f* \; o* W
# _) E4 a f' q. ]2 f# w+ _+ ^
+ Y3 Q4 f% x, T1 ^
3 z% L$ B2 V+ v0 x5 k
|