( [0 L- k/ p- G. F8 a4 R3 F6 O8 x8 B9 s, i
2 a) I" y3 \2 ]$ v- h# V
8 |; h/ h7 f) V' {2 Q& t 前言) `# T" e, B9 e5 z* Y1 v5 C
2 h S R9 q4 O) P
7 I7 I" S! h3 ?, `9 F4 Y2 S
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
, ?% K+ ]9 Q/ o+ K5 Y7 Y2 \ # {" Y4 ^7 ?8 j
7 _; `& Q7 \9 d6 I1 u8 n. B9 @
+ |) {% }# N4 T1 F$ y# b
+ B8 e& T" `3 W& C' U: P% ~ P8 z$ y" x5 e3 |2 C
漏洞分析
: @+ D9 h8 m, j8 `
7 l# |" _! p# d. ?* Y, T6 V( f7 s- L& D) B; o2 N6 a4 J2 A( \
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:. l; x3 j5 {6 h. ]
& H4 z9 K6 d- X! \1 L8 r5 Z$ N( T6 H* z. O3 o2 Z; h( V& q$ N5 A& u
) g& `) l/ a1 r' N- g3 F 9 F+ O/ b. q6 s3 x& q. i
. a* V2 T8 x) k! o7 L% O
对应着avatar.inc.php代码如下:
% F" }1 E- W* b2 F! `( |) [
' K5 b; ]+ K" q3 p9 h8 _9 d0 c: n8 p
<?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) {. [+ V4 D5 ~1 |; q+ B4 A
2 a: w: Y3 \5 J A- A# O5 V
6 {' v# A; `! R case 'upload':0 T( y- Z5 w# a* q/ m" w# H, {
" W8 z( H2 y- l8 I1 N
. @, \+ A! r N! H
if(!$_FILES['file']['size']) {% j) ]3 j7 A7 v9 b) ?" w
, ]; g, b+ U( M8 f/ [$ Z/ }
( Q/ S' z; h; {0 |1 q if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
: y% Q% Z; g h& X9 j/ c
" c9 T5 G8 _; `" H: x3 u2 V; n6 \, J& q" A+ b8 \# E; q2 F# w
exit('{"error":1,"message":"Error FILE"}');
4 Z5 [7 [! }+ c+ A5 }/ w $ y) A, j! x& b3 O! m0 J* J
" {/ u% d$ m! E6 ^
}7 ]& o* I$ y: R
+ e j2 U9 O! E9 B9 z: R2 P/ C$ u
4 P/ l/ k+ k7 F) K require DT_ROOT.'/include/upload.class.php';7 |8 V+ w% W% m+ W% w9 J
# ^4 ~! t( O c5 P' ]5 F# D+ C+ ]/ N- G" ]% ?, G: d8 V
! r$ {: z3 i0 y* a8 [3 @) Y
* Q2 X8 k& B3 T1 J$ N2 `4 t+ u4 t9 |
, Z4 \; |8 b: H4 O& k $ext = file_ext($_FILES['file']['name']);
* F8 e. c/ S* p6 K
& {" }" [# K) b- }
2 a& O7 w0 }7 M* k; l# X $name = 'avatar'.$_userid.'.'.$ext;
0 u5 B) t$ W) K. B2 T0 ?
6 ~3 U3 V% C1 d
: ^! n8 e) ?( p' I, B% o $file = DT_ROOT.'/file/temp/'.$name;) _/ q7 g% w# v: z0 `- T- v
: x) _+ e5 Z9 m* F6 g' X
% p% |# r4 U2 U4 V8 E
7 |" M/ p7 h v 5 d6 n4 |, l6 J
9 h# G; l5 k2 J$ |" b+ w; K" e1 \ if(is_file($file)) file_del($file);) M8 Z1 z2 U$ x- X9 `; w
4 d* J0 C4 Z( \; u
9 @; }) w4 N0 s3 _4 B; k
$upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');0 S, _' e1 v5 ]
0 N0 k% y7 N. @1 }: z# S
) h7 K4 ?( F: G. h6 F) G 1 w" Z" J/ [, L& N# S+ ^) w/ |6 `; Q
: I1 G5 G2 t! C' Z8 @" O* D6 p9 F# V4 [
$upload->adduserid = false;
3 s9 U" l7 H: G) k$ g# x9 [) |: ~
. X% G5 [' z3 h: ^7 G
1 Q% P/ u; G8 G" i: U* Q! z: ]! {
" v0 h, g: ~( b+ A6 J3 E- l . Q+ @ ~) n" v; n
1 F& }# a, P3 N4 @: | H if($upload->save()) {, n0 |7 M3 t( N$ G7 b
0 ^3 R( H( y- A7 y6 S) r& [( }$ U, m1 x$ n2 W& I
...
6 B* S6 i# D/ E+ K6 U3 G% {$ b. [
. m5 ]" P7 j1 P( z) D l' X* T9 @, X+ m
3 a3 }5 L. I: s5 ?0 ?5 j* n } else {
6 M% K: g" i- z- ^7 H. Q 2 k" O0 a8 d+ {* Q$ c
# ^! @9 F0 o0 @3 k$ c ...3 a# A+ s+ x# F7 l y& g0 z
! I; A0 h- j% s# t' A: O$ [5 {0 ^' |. i) I
}/ }" Q n M9 @2 L0 p, B* m$ d4 [
! A I/ A- a. s( o) S
" g6 R6 S& ^) r3 \) G8 K2 `( X break;
4 H, P! b' b. B7 {! J; E
+ P% C) k$ d) ?) ~2 z6 o# D& j, [- ] R1 i: _8 K9 L5 d5 Q
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。) X; N2 s" v: _
# [; n7 l; ^+ F6 z$ {0 F! Y! K. w
6 Y3 y2 S0 |$ f3 H/ ]1 U2 j
upload对象构造函数如下,include/upload.class.php:25:8 r- e2 ?. J- T* S
4 ?/ a( x# r2 L- K8 p/ M
7 f; A0 c. j: m: D: H! N3 I, z <?phpclass upload {) R4 [* P# ` ?3 W) \2 B
/ D1 m1 E# o2 s% A; R# _. W3 a* z0 e/ x c% q! ?! [
function __construct($_file, $savepath, $savename = '', $fileformat = '') {& f# j2 C8 B- h4 t. z' O
+ z6 {3 A; g# K/ N. s8 d: G* f, s/ r
global $DT, $_userid;
( X7 ~) m/ o8 u$ |. u! Z; W # f7 S6 w4 l9 s" }
4 f) t( [- Z$ @- ~3 P8 ~9 }% m foreach($_file as $file) {
$ o+ \0 f+ M0 @' ?( e! v. k - D& Y( _% {4 h
6 T: K" \$ E9 ]1 ]" e; l
$this->file = $file['tmp_name'];' |/ O+ P8 n3 y2 `
% f( G A3 |+ V9 z( h1 c
5 X; `- Z7 _( F
$this->file_name = $file['name'];& R+ q' n1 y3 O, p7 F: V; D9 r. u8 D
7 j, [& g1 m0 J" b- M
! u. g8 n. A+ l $this->file_size = $file['size'];
. N* ]6 ^! \4 ^2 Y% E2 \' B 8 W0 g8 R6 H' I4 S% U" e5 U
% A0 C0 g. @$ ?1 e6 \
$this->file_type = $file['type'];4 P0 C% [( U( B! a
T5 A1 R6 P7 b0 V3 T, i
2 g% H u; u: V, d; g9 Q% P4 Y $this->file_error = $file['error'];3 B% u: ]2 T' e$ B* N) g
9 O/ }7 v( J6 N' M4 ]2 S- J( M0 P4 `6 M. Y
: u8 x' d: W0 s0 l9 a4 m7 t
$ x- n$ I+ w$ w6 W9 U( M# X( o& l O& @
}; P" t. i5 L' m5 h
. z5 B! x: u4 u) D) O
( F& k$ Z0 q. \; J5 r3 M
$this->userid = $_userid; _- l7 F$ f$ r+ L, V% i/ T3 I
3 \" ~9 Y1 O1 P, T& ^
- R" J; F- T2 @ S0 ~- Y8 g4 f $this->ext = file_ext($this->file_name);
# }+ l, b1 ~! |) q3 B * t* Z7 W* u- L4 Y( C0 X9 `2 ~8 b
$ h$ R. |+ b( L5 Z( h $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
$ d' b8 t+ d7 N" T7 x( A
1 [1 I% r& `- p+ h" r; }# H
+ d. n3 K% M' C4 \& f" g: H) I $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
0 p& O: |/ R/ \, U) B/ o. [ ; z3 V# O5 L. H0 e* H6 q( o l
% O$ X9 F1 r- m S, T
$this->savepath = $savepath;- ~$ m- c+ h9 m" R8 o
$ f9 [; V( o4 b+ S: B* K2 m0 M) p) a M+ }
$this->savename = $savename;
9 p4 b4 v2 P, J F" L
" {3 d7 f4 A7 H2 R6 c' |, Q/ J' E& p8 j ^7 F
}}5 I: G _' M! W; }# r: `
7 `3 @, ]/ q5 k1 R
" q/ {, R$ X; I2 m% ]; j. r O 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
- ^1 q% d8 y7 h9 l$ n7 |& Z , w; y2 q1 y7 O0 u$ w; i
- k, P7 e0 S( I5 O. O6 u 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
/ a. {( d+ c3 O% K; q $ G' u7 \; @! H- I; Y$ @
4 k8 J. }. d1 R% h$ ~+ q: F% Z5 F, a $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
( s Q2 I: z: g# ~; ?
1 J# t& I3 e0 z
$ D9 ]' S" y" O 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
- {" o) I3 ]5 L1 ]' _
% z# u/ @6 d; x
9 Y* j, g N" Z$ G/ r; h2 C. l 4 L3 x* ~7 e0 A
& C7 x# e# s' }( G4 ~
/ m, s7 ~" ]9 s' U* a- k& m 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:5 |& w0 }( H" o2 `7 f' M" y! c
4 l& h7 j0 r! k" y: f$ Y
G1 g0 U- V8 p1 B5 [
<?phpclass upload {
( ` V# K/ A' X 1 D8 W3 Q% i, x5 h& q. b' I( q
. Z# N/ q- `- r8 m9 v) v' \ function save() {
1 j6 V. X0 F$ S4 C- z + \8 t. N+ J7 e! I
9 Y* t1 I, E+ C/ l include load('include.lang');# Y" u! ~7 j4 |3 X" p+ t6 M
. W9 |; P2 b1 V- b1 A! J
3 C3 e+ @3 t! O: [% Y if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
& K4 [2 Y3 {; y" u ' R# |) {/ a* w2 T+ q
0 A1 E* C- C/ s2 d: i/ l% G
1 W- T+ B3 s" f$ k6 l 1 g" ~( p% |7 k# Z* U& q) q
& P6 a7 P; @* y% l: o' L& m
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
, n8 J& u8 V4 j7 H
% f" q& R5 V f& G9 D& A$ P P
4 F# y: T% _! k, y
( o( [6 v- k& g. u ) J# G, N1 W( v" q9 B
+ g8 |4 \* j' o5 `
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);0 f9 B6 F) }4 d X4 }5 e8 I
% ?( u! n( Y G/ Z, ~
q/ F+ \ P. S0 S/ s- E% y' F 6 c6 H6 f) a5 g' w/ O3 A
: m. d) e/ K% X" ]# F- i3 P( O, p
$this->set_savepath($this->savepath);2 c1 U+ f; l p7 p; p( J
7 o5 k4 Q% m, T1 T" T
5 n8 D' F( I c3 I' x $this->set_savename($this->savename);0 Q4 n. C- f9 ?4 q: F
$ c- L, b5 U0 S
5 C& Y, { K V6 J; q6 }( S7 Z& M
: l' G |/ a' _
$ f9 d# A" ]/ o0 F0 K1 A3 g
' k2 s5 k2 q7 N2 b+ x/ { if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);$ L/ K( X1 k7 h# ]: o, {2 f7 w3 U
& p: l& ~# f B" n+ d
6 [- x: W# g2 `% A- M( J D) K if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
- \9 y- Z, [7 m3 q ! }' n8 l* }. ?, @
: w! p. E4 }& V9 N if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
! k1 m: b. S5 y/ W2 j
5 J* a0 ~+ X/ B7 T) j- `+ L) s
2 B" ~5 o* K5 w2 u 2 ~; K1 w; O8 L2 C: ~7 x) i
: b8 k# K9 S! \( y. o# j- }( e! Q# d) i
$this->image = $this->is_image();
: l6 \. ^5 N/ D: M! p$ q
9 W! R8 I0 T# }% s% v; }0 e, D
6 h2 @9 q" N# q1 O5 d if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);6 F; a) S% K r5 G4 d7 b, F; ?' f
" x/ n" `% V% e0 c4 L
& G6 t, b8 O% _" }, J return true;
& b" X4 `9 ^! B! l % n7 v5 X: x- [; `9 r
# d+ p) F: p& r8 Y$ O7 e6 Q) N4 O7 U }}
! T3 |$ B& F _% P6 V) |2 y 5 H/ ]9 Z& x% y- e, F# n" P* }' |
' E7 B: V6 H( S4 `* a1 Y! D8 P4 e 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:1 A Z3 n# T' O( i% v6 t
) ?# t$ w, {2 |2 |. o( w+ \
' _2 h% b9 E8 j! H2 k' T: B
<?php# {5 |* n6 @4 a+ n, {
, m7 T/ V5 z$ ? W# ]% r+ d& I
- M p+ K+ S {$ `; s9 Z6 L8 Y function is_allow() {
3 G5 Z: s* _; N, P6 g * r9 d# `1 Z; H" r5 h, @6 n# U- N
. }! K) a9 v6 K7 O if(!$this->fileformat) return false;
* P% M$ q, i/ T) _. J
! {9 @( k8 N: W/ n6 i& b6 d7 m# l# D& N4 L& t
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
3 ` F# a3 g2 a* y2 K8 ^ B ( l: d' j; ?9 j2 \- C3 v
7 w9 o: g- I" B/ C. T4 m 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;& |4 L7 u: k: f( I0 O' s
0 [6 W6 r# ?9 ~. O1 s/ O* ^0 d7 b6 ~' G- D
return true;; o& @9 X' J) [9 z1 T' v
" J; a6 ?. Y7 V* b" @$ f7 g
' J- H8 J7 C1 s, w
}/ {& D- U, M( x( q* q s
4 T1 H b$ S/ T' \9 F: Y
- @- j I9 N# r0 N6 l6 ]3 v 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。3 Z2 @6 T+ x) V
K. F+ g5 Y+ J( |, R2 j3 l
6 f/ F- n& d/ E 接着会进行真正的保存。通过$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文件。
% I1 t5 J& c. D8 x+ T5 p9 [' \
% u5 r0 w( ]$ ]( k& b J; |8 q- ~0 Z0 X- |+ K3 D' K& x2 _& i
漏洞利用# E- P2 q" o9 {# k. `& j
3 i5 Y, b" S+ s
( y$ b& l6 y$ W& ?
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。5 O5 ]. r0 r( S: v) p7 v B) L
9 m$ `6 u n2 J$ I7 I7 O! d& o7 c/ y, [2 a o
6 ^4 v) Y4 |) G: Y2 {& T' `) Q
& a/ B- Q# p) Y# O6 Y* p
7 W, y' l' G! E u8 w n4 q 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
2 @8 ~7 d" y1 Z! B' {" ^ 6 j/ ~& ]3 `3 N/ U! L$ |2 R
" ] t2 @/ Y2 u/ t8 r% [, e
不过实际利用上会有一定的限制。" [6 |/ r. L' c8 J# o$ a7 w
( k' }/ P7 J( U/ H' e: E6 q: N
. d0 [3 b) z) Z0 J1 v4 X
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。4 M7 Z* B+ i6 N1 K
0 N6 v6 i+ R+ F5 ?3 E1 Y9 ?
& J. k q1 k. I+ B
6 \! q) \2 S2 O
* V3 k4 c- T# u' }7 @/ G! {% J; Z' R W- i
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:! A% q7 b; b4 N" P- e, N
+ R' I G7 U! _7 P1 p9 \5 e
1 H8 V7 q$ _: T5 c# e 省略...$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]);省略...6 v8 ?3 l+ s$ I L+ d$ J7 T" p
6 f7 A& b7 F k; ]# j
+ l0 h! \5 Y- e# N2 {, y 因此要利用成功就需要条件竞争了。$ K1 s2 x; ]; M, D9 m5 P4 U! g
4 @2 W9 y m7 i0 M- G
& N" ~1 q" m- h
补丁分析
" a- w* ^& H, I+ X9 g
& s4 R( @( {& `1 `
: i% A$ y( e/ n$ A& V # K$ V! Y1 Y, t5 T1 J
3 ^% ~3 I+ i& x: H# v4 [
& J7 z8 e& u" b1 U2 c 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
% H( e; Y9 d/ ?$ R4 c& `
: w, V* ^: P0 K5 P( R/ {9 ^# o i# H
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
$ U1 t+ o0 Q3 P 0 [& e8 N& } J% N" y2 t
4 H- b; V3 Z7 C5 f; q; r
$ w/ a) J; \6 p. v& U: g( Z% q
6 n" q$ Q6 \5 S6 }; g% f9 o
: u1 @7 F, H% ]( M7 t% ^ 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。& A0 d# r5 ^( ?' o3 b
# K9 G, U# a E! G, {0 u) x( L
6 Z G$ M) Y$ d t3 R7 w 在is_allow()中增加对$this->savename的二次检查。- f! g2 `- C( Q& x/ G' b- ~
0 b; R. G7 l$ N* W9 G$ [
/ \: ]. G1 t1 H 最后
# c/ Z1 T Z; n% k; w- @" n1 ^! w4 |1 `3 d
* B1 _7 u4 b0 o3 g 嘛,祝各位大师傅中秋快乐! G. K# h% u* s3 v9 a! h
% b$ S2 u5 k2 P9 Z2 u- |8 N- j
7 o. \9 v$ m9 R2 x . D. L- l* l0 R6 i. B6 @) w" [4 x
- T1 ?9 ~1 i4 d9 c% V. L
|