# {4 S2 q) D6 j1 S1 E
- f. ~. q b3 c/ N: I$ I) o5 ^2 y3 t0 v: t) O
* u- P/ \ @* W9 C6 S) \ 前言
% Z( g& ~* J& t* i3 N* S: i9 U# R7 d3 _2 C6 j8 \2 T
! a g" p# F8 Y* g, {% B+ m, R 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。8 ?4 }6 Y2 F/ r3 @0 h& b! R8 _
' c' z( \/ l9 X( g' @0 D
9 ~5 {, m% U+ f( u9 M7 W+ q* R! {
, S7 ]& ]2 H+ Z }4 B1 m( ?
/ K" }6 ^2 F6 L# j- c1 E
* ~. a3 {) |; X- k& X8 I3 ^ 漏洞分析9 ?8 [2 O- P4 E% d
$ r- y+ V9 q/ ~/ }3 K. V
8 u% d% c3 a# @# T3 s9 ]6 A
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:1 h# ?8 [' K3 k
2 i+ a; S! }: x I9 x; o
. d( ]* f, [6 i( n g6 G2 U: ?
% Z6 V; _( A/ F1 {0 Y7 E/ D7 s
/ O0 H1 p1 m* ]% M7 F: y t
3 \ S, P* P) a0 n3 }6 f; p' [ 对应着avatar.inc.php代码如下:
7 a1 c" Y5 U6 A, Y/ h+ [( P
$ K2 @" r/ m2 r5 k
3 d/ C. K w* p( F3 i- o5 N! 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) {
4 I3 Y) m: l8 Q/ u ; v+ y! f9 N+ M! c+ `9 U
2 k! `: B! N1 t$ v case 'upload':
) D3 [6 I8 ?" v8 n% N1 u 6 G3 W/ L4 u/ g5 C: R2 `
! X* J( R, b* r- z
if(!$_FILES['file']['size']) {
7 V7 e4 w0 s. e/ H
4 w, e7 i2 M: j7 ?7 Z3 f! O/ W; Z2 _' x# A4 \4 u
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
8 c1 X3 h/ \0 a A6 w# x
8 L# `( I0 H. G8 ]. B
6 O( n* i* T& q: K2 i4 x) U3 s9 T/ W exit('{"error":1,"message":"Error FILE"}');
) U) c7 D+ H* a/ m1 z& z2 K, n % n5 j z$ U' q* m# C
0 q8 G! E' L% X0 I* V# d# Q$ n, @
}
6 S. D# z& F! x; j1 K( d
0 a$ v6 E* Q6 k
" Y4 p/ [/ H5 T# A# f require DT_ROOT.'/include/upload.class.php';
$ m0 i& k% W5 }. m
8 D1 G6 `% a5 `0 S. D' A$ V" [5 j$ a3 h! z# u/ C
/ n( C- `3 \5 m7 Z
3 T* [. z2 l+ x% l
3 k8 |3 C: x' } $ext = file_ext($_FILES['file']['name']);
2 a% D N' X9 o2 K4 K! t3 V8 @
' `7 l* p% U! @0 K% L$ G- z' J' L: J. [! P6 v. ?/ z$ \- h
$name = 'avatar'.$_userid.'.'.$ext;
- W0 f) l% G- H: s- N) D. m
% }, g5 v. N/ O9 `4 ~/ R& @/ e( ~* K
$file = DT_ROOT.'/file/temp/'.$name;
3 n- w: D% X$ @* `6 q @ B% `. l H8 h8 G0 ]6 u( ^
; R# i/ E a1 i; _: F4 ~2 \. u
* t( X/ I' c( x: i/ V
. v: N, v- O$ `2 K' m+ M: A4 H& |! e9 b6 s4 v% L) ^1 T+ t2 H( g7 l/ K
if(is_file($file)) file_del($file);
: q' _; D0 u4 O6 H4 J$ n: Z ! Q n2 J2 E; {+ b1 d8 C
2 K4 N( ]9 a9 R7 e $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');8 ^) B* C( N5 C5 F! R( X- v; A
& v7 F, U: G& D% ]# n8 t1 B; r1 f( f% [- d0 {( j" C6 q
5 c! _, v: |: s- |4 I
$ a" T8 F. y/ z% @% H y
: J z& ^" f9 q3 [- ] $upload->adduserid = false;$ Q ?8 }! @# O4 X
& l, K; H" a+ P& f# d
' C( \& l) B8 o
9 u5 ]" c' I$ P. A, l& `3 d5 o# k # Z1 W$ j) J/ S/ r3 y7 z
: s* B- j8 m3 J0 _- F( c% d; C! |
if($upload->save()) {( n8 a- c$ }4 p* g" J6 C- D. u
' \& s( n F" K
9 ~: G$ K! a/ N2 p, u6 Z
...
% u6 h' W# }4 c! t
( O5 D( G1 Z" {+ o$ J* q/ @; Z* z4 T! I% r. k( V
} else {
. q0 N) `/ n3 s5 d/ _
# y* e4 N% J+ ^8 E+ t ?$ G# y1 y" [; e$ `0 F' A, h
...* B2 y: z# ]0 E. |# i$ m
6 ^ V- u2 {9 `- \& [# n- G
9 o, c+ v7 g, @" ?. }8 @+ x } { f2 z0 E) R8 F- ]+ C
2 n! Y# @, f6 X* a! s; U0 x- X: M
% S0 b- q* S; m/ L; k- z3 f break;
4 y" d( z* t o% g" b' e3 t
" x2 \& y& T/ X+ n/ |8 |. T" B9 _: l' H
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。
6 `+ y: v# V7 p6 _ 6 d2 {2 ]9 I9 j1 B. a
* H& o c( J; Y! b0 |/ S+ A8 |' ?
upload对象构造函数如下,include/upload.class.php:25:7 s+ P3 I$ o5 d' ]5 C& d
/ b) }$ D; Q G4 G$ n' K8 R, O u( O' k# r& W0 N1 d
<?phpclass upload {
/ Z/ \ ^& F# G/ p & f5 ?7 ^1 W9 s; Q, I' [4 \
6 g# {& S" C& a. _# q function __construct($_file, $savepath, $savename = '', $fileformat = '') {0 C9 N6 h. {4 T
6 O1 a* {6 G' D7 z
1 e7 `) S% N9 Z: I global $DT, $_userid;
( I3 e# J; M6 `' Q2 t9 F 2 M+ h, Y3 N& R' R7 @
, j: X( t" y7 b! L0 M. H% [
foreach($_file as $file) {
! m5 |4 s2 \- |5 g
$ ?- B# m) E. S* n; ?" o* e ^! e+ r8 h
$this->file = $file['tmp_name'];
5 L- l0 p2 m& M1 E! E1 t+ W & M1 Y% C. A6 i2 M, }# u
" a/ d m' i) q; _. i. \ $this->file_name = $file['name'];
: D% d: r8 x+ P+ _3 F* W 6 v& T3 ?* S- u0 @, q3 g1 Z% N+ ]
8 c9 h# B6 W/ M1 t& E! L( R
$this->file_size = $file['size'];4 q3 k9 n) z v' {& k
1 `# j* k ~# F& j/ o
1 r, d7 P& m3 a1 x6 E! s
$this->file_type = $file['type'];
& g& O# f; e; e . {6 D. C+ O& @- I% B d9 \1 @
6 N# u3 F9 _. \& E $this->file_error = $file['error'];8 s5 \2 K6 ^% K
5 g7 P, k; E2 V
$ ]& Z' E& T- j/ u0 ~1 x# h" O # E/ a3 G# W9 _( `6 r
( j1 e: c. L- m# y$ O
$ g7 L, f' L Z' n$ f. {4 k. [ }8 M7 Y1 w7 s( U$ A7 R
' d, ], P! d4 a+ p* P
9 K$ n) z; n/ \ $this->userid = $_userid;
6 W! j! [, w5 s3 @" y
! ^% ^' w# g# |0 k; V+ B6 G2 a( n
2 a. D, y+ @7 Q $this->ext = file_ext($this->file_name);
/ @, H M; u6 r5 Z, u* S
( S8 ^% K$ R/ O' I H6 ~2 \+ R& a/ ^8 _: }/ q
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];! e6 U" e4 K* A+ L7 R
6 u: R) T* w1 _) R
# U: V, @% F7 Z' o7 u7 o( u' [7 V$ T $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;1 V8 U$ {7 K$ [
. ?- f4 p' ~2 _! l( A0 `: N9 A6 W6 Z4 H
$this->savepath = $savepath;, \6 J: }. k6 s* D7 q, D: u
~& d# c. u& n% L
0 R' U/ v) h9 L* s. Y3 D# P $this->savename = $savename;; H% V9 D3 R$ r4 c+ ^4 ^
7 v! F9 z$ E5 F" l
& p2 s6 r+ `: ~3 i5 ^/ y3 k }}! F. z. n) g0 h3 A
' P- F: M" l/ S7 k7 k1 S0 Y6 Y7 z
+ e( Z- U& _& X& C: A 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。2 j, {5 X' j8 \0 [" B. }% C9 K
- Y% l6 x1 X3 s9 o! {
2 F' C+ P4 c( P3 ?4 x6 F% ? A 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 # x/ y" ?% {4 z+ U! ?5 e- N* `
+ A' z, K% O3 ~1 ~& T
8 _8 d q: U* d7 B
$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 w0 i; ]6 i9 x- M+ w4 W K+ f
9 X* Y9 f% c4 q8 p @$ ~4 w4 z/ w+ Y" J( ^3 K" F$ O
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
, K2 g e. A6 O. C; H. A. C / h+ M/ E- c) {1 F5 g4 e; Z* I. c
8 _# Q/ T5 F) n% `+ P
' J9 H% I( z7 P& O6 D7 p2 { 0 k. e0 \( U) F8 p! ]
! ]: Y! O" w6 y 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
7 r$ p8 P" q/ m( R$ g
' K6 t" J& E6 w5 i
& q; x6 L2 G" b3 d <?phpclass upload {+ e) {1 u. z6 P
h2 X/ o: F7 m1 K9 X9 @+ q. |& r0 l3 e, b' o: y" F9 J8 x
function save() {
9 Q Q/ l1 V( f" P( u 0 E4 l* a+ {8 X3 f. ?9 u9 \
+ t0 f, x' s: p; W3 z, p; q9 I include load('include.lang');
8 s. p8 U3 F4 V! ~$ K# _$ ?! N i
% S3 }/ K6 z: R
2 w/ q% C$ E0 j4 U6 `7 p if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');1 C3 R/ c) B) E4 W! g
+ k4 \+ R L3 t3 v4 i) l
$ A7 K) T% u t3 \8 Y$ x
6 L4 n$ u) n' j
, P/ }( T4 Y) @3 G, C0 E" ` g% I. l! \2 a1 f
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
8 F# `' u! _3 v$ \4 x5 ~- Y
m/ c, c% l0 V# E, d; s/ k# J4 c1 @: X T" l* Z
+ O! n y! E( ]9 _ 3 j5 A: e% D6 P8 M" {( a |2 J
q' N5 f/ {3 ?2 a3 _2 o2 ` if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);: Q2 ^! m B3 v$ f9 [
$ Y' R2 J" L I" `! }4 H% W5 Q$ O* ?3 X
; y1 o% |7 |9 g4 [- X " ~/ x! e" G, k) ~' E
8 p4 f5 _# M, Z, Y" {
8 b1 e: l. A$ }* z, ?7 }; ~ $this->set_savepath($this->savepath);
1 l9 n. d6 g, a% y7 [
9 D0 l; P, z8 ^! [3 \/ ~3 T; m0 `9 H+ D" k7 f( v! Q
$this->set_savename($this->savename);
, a4 M$ I0 u. ^7 h2 Q . t! T- m* x0 d+ u5 s& ?1 J
F3 l! W3 Z r; t$ K
$ V/ @* l4 J5 s+ w$ G6 d 2 A: W9 O% T2 i: @- t
$ D9 T/ `6 r7 x2 o/ r
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);8 a2 u1 g- b/ o9 m) \9 l( {
$ P* u& x' Q0 g
Q3 P* X8 ?; _; W8 J if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
' l( X" H' j9 E+ y9 l! e) L: U + J2 U* k' A3 t) j# F; V( ]% a7 Z- X
$ U' I; k0 c9 ]2 J1 c
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
0 ?) ]% L4 d: }% ]: f- ? ; a4 l& C; E% h/ \$ B5 V* t
/ h4 I6 _; W4 @! b ) K$ } J2 r8 z, A- T5 \& [
4 Q- \5 v2 W7 H5 C" x c
* A1 ^+ k @& f- w $this->image = $this->is_image();
9 R, ~) l/ {; j
& u( w& G0 {9 `, ~/ v8 X- z X7 c. l( p4 {, W# }
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
0 V; f4 G4 M* h8 ]- Y6 F" p & j) ?5 Q6 Y( k3 i; Q3 A1 l
* }. N* F4 Q6 F( }. \7 _, R, P
return true;
3 l3 P0 ~7 A1 j7 C
5 ~$ Z" S, }7 B' F: U; Z( Z% N1 x1 {( k' a r0 q
}}
1 {6 r* s* e. }# R
I+ q8 a# Y7 V5 G, C5 M
u$ `$ n# a2 U 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:1 k/ L/ b& x, Z9 E
$ } \! r6 w: T9 F5 f9 K. S3 s2 L
& O7 m) c! e& w: A4 F
<?php; V# i: }% M) s {6 j. }6 Y
, Z7 c! R# u6 D$ d) u; k: D
* J# W( b o# w$ E8 q4 Q
function is_allow() {7 ]* X: a$ N1 v1 B% [8 T* k' o
/ \: y1 U& M0 U1 m0 C) }
" h; z! R/ d& l7 l3 j' E) C if(!$this->fileformat) return false;7 `5 \/ [3 A' [2 l% m: a" ^
2 H+ {% W, ?7 V T! A% ^6 b2 I3 W. M5 g* b
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
) g7 z9 u+ m8 J- L2 o% n 4 I! ?( A2 `+ o5 |8 n' }- x+ X: ~
4 ~3 r) w8 @5 J! u$ D 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 B- g$ \+ t% i, B8 c
1 U# Q5 s- T: Y* Q7 x+ d* P4 M
3 o0 D/ y2 A: c8 f. n8 d. T# a
return true;
% Q; s) K4 f6 t) ?; C2 s% C % @4 m4 A4 t! r$ a0 S
# U3 j" f* K, Y" X% [& C }
0 ]( n5 h0 Q$ Y. M
4 N- p: b+ i: [* ~0 X: z, n7 a0 B
可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。1 u) X4 `" D: i! E" W
) L! E* o) k* l
4 I% r1 `+ T% Z 接着会进行真正的保存。通过$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文件。
! c/ h( I4 U; ?5 s; ], h# f+ w4 ]" r
9 d' N6 }* n: \% T8 c- N* y
8 |* ^8 j* q! ~) f( S1 @2 g2 b; j 漏洞利用: K. P8 ?) I3 }; h, |
% X& Q( Q u3 Z9 e1 C; M5 ~8 w: Z( q9 `$ p1 ^& l* s1 u
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。# M3 Z& k4 Z5 ]/ f! `# `8 q% z
1 F H; D& t7 M3 Y/ {5 `4 j
0 d, l' _$ l" V& d. H ; G, h8 c% j4 u* H m6 C
3 F, i; e# B' B/ `. M8 x2 B* d2 s. d( o( F4 B6 W: }" {/ O# i- T
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid y0 a0 g" Y8 N) j2 j
( G6 ?" w( ^# t( a; v. n4 X; c
4 A: P. F5 u) P9 \% ?' M% |" Y
不过实际利用上会有一定的限制。
0 o, S) J/ G( U; _/ z1 B* r 6 k! e. U v* b
; }+ T& Z4 R, U' d# K, M- t, X
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。! q, |. m' `. \0 l5 L
: G; ]( T: B& e4 c6 h/ g7 [. `3 ^% P- l3 H
1 K L' v6 p- ~6 u* p- Q1 d! i
4 x& M; j% H1 c. y6 z8 q6 D. `
9 Q- e9 y- s! m) m2 M2 s
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
: M6 e- r, c4 |! _& s1 R
) L1 {7 U' ~, e ]
+ u6 U7 W2 G7 Y6 V4 B 省略...$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]);省略...( p' b* ~$ }0 `. V$ H
& g' m* H) @1 d* S1 v+ X5 {
% w- j% H" X" \9 G( w4 [ 因此要利用成功就需要条件竞争了。
: D" Z8 u- B0 U% z! q
* Y3 W2 Q4 X. ?8 L% ^( n$ g$ u/ u3 |
& V3 }" y) T" O5 T8 q2 K' C/ Z' ] 补丁分析0 O9 [% {- I; c" i8 y0 j' ~
1 I' ?1 a# i L5 ~! }0 E5 P2 q% _
; i) i2 q2 m! O' I9 [; O 2 A1 X6 N# Y* f
0 b; D& \+ E/ ?7 ~4 e. [ s; e9 b- _) M% s# I! V$ j" R4 V
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:) x9 B2 |4 G. t @. f; V Y
0 G. t. l. f- A5 k: @2 {% X: V
4 y) C3 |* R/ p8 u+ c$ c7 \( j function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
M) G; T$ q9 b- T4 F" t
$ y# p3 q0 ^1 i+ x& u1 l4 t: _8 H) @
* Y+ I( _4 M: R& _; ^
, K; b# u& @" n4 T( A7 D
, z0 b& K- ?. H* d$ J! _& W( m8 I 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
. O ]6 V' |' h2 c- f1 B3 _3 U 3 e$ M V+ y; o% x+ K3 d- ^2 H
: ^: j$ o# V# ^! y2 V
在is_allow()中增加对$this->savename的二次检查。
& v7 j9 r- H( i9 R 5 q# v' o0 Q d% O, b
) z4 m& j$ ]7 G/ A7 q 最后+ Z+ j: G5 S3 t3 D
# i, }1 g3 V \1 h
/ i2 I } p! n$ d6 Z6 ^; d 嘛,祝各位大师傅中秋快乐!
( H) s i' w! q2 S
& D/ O# B; H. c' N, w
, @3 \: s' A( e* E 5 n4 V4 T, h4 c. E2 h6 n+ F% u' H
! v) _. _# z+ K" o$ O, I
|