: J+ h9 ^# }3 `' [
3 `7 ~& D. S) @1 t1 |9 @6 {; W
M) |$ |& m* ?# q8 f3 k4 R# P/ A$ \7 H+ w* l
前言 N& A" }5 z) a6 ?
0 O. s f+ K9 K! S# d9 z8 G8 D4 c7 C" Y2 H
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。# L% h% l9 i& }7 B6 Y- {- a2 ?
) a9 ^! |. X1 n! V& p) y. o) z0 Y3 F- n9 N5 J
' X6 [" `: G( T' M/ V( U# d
& X5 ?+ H5 p# _4 T- h8 T l7 Y5 o$ }
漏洞分析* G2 S$ [( [3 A2 v- v3 j. r- F
# ]2 ?; w# F! E. i: H. W& X( }0 [6 `4 A) y) i6 \0 Y) h
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
2 C8 x* y6 u. j7 n- g, O2 `+ t $ [) d3 V4 _* }7 Z
! w+ S* w/ c+ c0 A4 W 2 f! k* u q' \6 L4 @4 a4 p8 L
6 K# `0 t7 n3 ^! W% i3 D
% s* M) h. W; s, ~
对应着avatar.inc.php代码如下:% {" V6 n2 b* K* K6 {$ b- g! }
2 L: M6 |7 R( ]. j- S& b
. j: y& K, [/ L! M4 b6 B" D9 S <?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 u |: q2 y , k& N6 F: E, Z/ B2 P
2 f# n9 x" y; d( L; t+ ^7 b. e case 'upload':
" d% O$ W( U# o % ~" h0 Y/ W: f5 n K/ n6 P8 M
& ?, A, |# |( |0 ? if(!$_FILES['file']['size']) {
7 x( S' P) b8 {( P H- V t
, S5 S6 ?) a% t7 e9 k& K# s& g6 c+ l! A- V: C
if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
8 X3 Z& k+ z7 u+ ?5 X+ F" k* I ' a2 n- k( |$ a5 C/ a
+ C0 Y4 a" ]2 `1 N4 o5 p
exit('{"error":1,"message":"Error FILE"}');
. \$ w1 M' @0 m1 p. a) f
, a: {- O( Q6 K1 k; D
+ K1 @: o# y0 P }$ F6 g$ W( ]9 ?% W! M) G9 ~; ^
8 j' N6 @' I/ ^" y
2 ]. n1 y& r1 z6 H require DT_ROOT.'/include/upload.class.php';* N2 N+ Z0 {/ w+ R; K) ^* e8 G
! x1 v4 \" K, k6 c& c; @) I* Y- P) N G, t( t; s! \
4 ^% e* g" U; z
+ Q: i9 w2 M/ z( `; J
3 v9 O% H) r$ {3 ?4 w $ext = file_ext($_FILES['file']['name']);
; B+ U+ Y8 F4 r. n
8 t" q& Q, m7 F& C& L1 I( v9 `, n p
! A! E' p3 k8 e0 m) d1 D$ ~, C+ T $name = 'avatar'.$_userid.'.'.$ext;
/ w) {& X$ f0 l1 e- N5 q , @; D7 ?/ v$ Y. g8 i* G2 a
- q0 i$ g9 K: R5 u/ T8 J $file = DT_ROOT.'/file/temp/'.$name;
, l& S2 d5 K$ o# s: G0 {9 Z
4 `& \) m v# H3 `# `' @/ O7 ~
; g. R1 a+ ]. w9 F) f
8 d/ r3 [% r2 j# N8 v' X 9 C% m3 B9 }/ |( n4 M
0 Y2 `& t$ {7 [' s0 c
if(is_file($file)) file_del($file);
( `& g( J1 ~+ C' [+ j $ c/ }( U" h8 P7 |, Z) e: z4 |
+ b/ S5 ? Y4 h $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');( E* O5 C7 i' o$ X
/ h3 {# |' p6 ^9 D: o2 }0 Q! s; `/ I1 {1 e
# N! s- b: S9 Q- o5 a4 v
- V1 \! [/ X# a/ w5 l
6 d& j# B o' D1 b $upload->adduserid = false;
0 X4 X; y9 a6 g7 B& A $ X3 G" s* y5 l5 r! @5 Y
- _8 A2 q& B8 M! M9 B+ ^
- W! m# q5 I" c! v/ u
8 ?( q+ y8 E( {% i* N7 g% r+ r Z1 Q. Q4 T% z
if($upload->save()) {
8 G; f+ A0 A( b% Y. Z" }2 J) p! b
U5 Q: p# V" s* @
% P( b0 D# E3 I) `! j4 ? ...
7 i0 w& D5 ]; {) }4 X" x
3 s* c* e$ |( K2 Q8 t8 u! q
! f; Z' m/ W4 F+ x } else {; ^5 Q( w, _# [& p' h
) A8 [& t! Z$ J$ p7 g; J
& p2 k1 L* R; E; B: r ...& T( X% \9 V# p }! p, \1 q# j
5 e( l0 H2 S/ J+ c% I/ d
) }: ]# T4 _* ~* j& }+ N
}
8 t6 D6 h& U' S: c7 [
3 |( M! f" i3 e1 R f6 s: ]( `; D- O6 ]3 H" E' m0 ^
break;! H% P! K3 t' `$ c$ e
& K" f+ a2 k! d$ h8 J4 R2 B4 x
* g1 @4 Z1 k- L, q* Z; r
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。' y* _7 A. n* Q, A9 }6 e# L
" O0 r' J5 a" G. x+ C) e
# r5 W/ m' _, v" h
upload对象构造函数如下,include/upload.class.php:25:
8 r- T( [, M/ W; B/ P( H
8 ? X- i3 N! \0 t) u# a8 I/ O* a7 P: O( o% e
<?phpclass upload {
+ `( Q+ _) U. ]/ j
7 b% n0 f, ^# e% x% e/ U5 I9 q0 y, ?" f1 G" I d" V0 v
function __construct($_file, $savepath, $savename = '', $fileformat = '') {7 [1 y9 }( Z3 O8 t3 _- ^
) G) ^1 A/ Y$ w+ N7 q
* a& r+ O5 E/ L+ i: O5 g global $DT, $_userid;
. A4 o! q* X# R0 x
' }: ~+ M4 d% h+ d: W. L8 A% \ Q; S' R! b& f
foreach($_file as $file) {6 k8 N, }# P& o5 F# M3 \) E" f
3 e7 g# J) N- m' w$ {2 |6 b3 r
9 R" q' ~) q1 }/ \: D
$this->file = $file['tmp_name'];
9 I" B+ ^3 X2 w/ Z: o: P
# u/ E6 D# t7 |$ F3 m/ M. L/ S+ F" @' M7 R: J7 Y
$this->file_name = $file['name'];
& V: W4 P; r! X& [4 p! g5 x) v , C7 T, K' o6 }6 c" v* J
1 b- t: K6 L7 f, y+ w/ N, k
$this->file_size = $file['size'];
6 ]( A1 O9 B4 S3 x; o6 f 9 O \2 u. _& Y% e2 E& {
$ j4 ~2 j7 g+ A! `/ ]/ U $this->file_type = $file['type'];
" v: K. q5 t4 I
9 ^9 Y, M9 k/ k2 W0 j2 O& u) V# d
$this->file_error = $file['error'];' K6 B- t& L+ S3 F0 H+ ?
$ c O8 i& `; Q) M7 c! r" _7 d/ Y T9 }8 P; @
0 @& A6 e9 D1 g. w
7 C6 q! J3 n6 t0 x! K! X2 O
( E f- p4 K+ {& Q" m5 k( j+ q% n }
6 p2 S; F z) |2 H( e
! ~+ ^4 T2 c( h) C0 C' t. D) r* f/ I1 L/ V, e1 ?; `
$this->userid = $_userid;
) w4 \; v# B$ h/ R2 U! j) p4 ? 8 s, H6 J$ q1 z* l$ y" d5 Y- G
( Q$ H/ n! T2 q4 ~& U8 H $this->ext = file_ext($this->file_name);5 g8 _. @6 m) G P& A* ^
7 w3 w! z7 G/ ]' h- ~8 ?( h
' ^* G+ ~8 Y5 z& T $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
: X7 `- m6 m4 Y: D! r+ ` $ \2 {( \$ ^, b% L5 z6 X7 v
) q+ O) [: O6 j2 z6 ]8 g# | $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
/ N, d8 v* u2 M7 X3 q- F ( s- D \0 t2 i) m* M' |
$ C4 w. [/ D. O4 o' @9 o $this->savepath = $savepath;
4 X8 k4 I$ w4 n6 N - }3 b0 M' V$ E& I5 G
3 |( Z0 C1 U9 `7 H/ B $this->savename = $savename;
; P, y0 ~2 w3 I5 o- T6 x' N . l% F( `( S* @8 {
3 e4 \: U' @; D: Q) @ }}
* D" N; b' `7 b/ {' A; ?% A! S
U( i: q: L1 ^( Q& v
6 {) g* e- T: h1 a) R( K 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。: g5 s0 N" V/ G" G" C, I
. E# A* v3 m( L, k7 e* X: o
7 F6 R* @4 d& p; U7 S 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 7 M, ^& K1 `" F, _4 a1 H
/ x e5 {) f% P; c0 n' v8 @/ D
+ e- N& r \+ [0 t3 w
$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
( F/ L2 p( {$ z* F
) M) z# m' }0 F. j4 X' {9 H) G6 e) I* X" P4 n
而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
/ _5 W n, Y( h& Y, J' Z; |. [ T
1 {# f& d" n7 f- L8 P; J4 a
; X" S9 ]' \2 H4 X: A2 E
) n8 Y0 E+ v( S# x 4 d1 B* K$ o ]
1 d8 r) _8 U8 T1 T$ x! D 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:4 X" ~" }( I: o- {
2 ~6 i+ o+ |9 g& C1 o: }3 n
+ w, F& F$ h$ e) a) x <?phpclass upload {7 v* ^' p; o# u2 G B5 s/ _
: K1 r! C+ A0 ~' P" _1 F- @9 h
! i. C! A U1 i' f% h function save() {) y3 T0 d- E4 v2 A+ @% A
( l5 v$ j0 V+ V9 h3 H. u! g( A
0 e4 T, L) G i include load('include.lang');
2 J3 [, e5 c7 p+ g / E6 ^" ^6 X/ N) T0 q9 F
$ x8 }; Z, W4 i3 }& |0 J if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
2 d; R4 d9 d4 r( P% B 3 l( q& y% t/ s+ t+ `+ l7 A* o
! ^8 z" u, C! p" x4 _9 V
: o7 l! f/ A0 d4 d- |
4 M0 K$ d, m5 u
6 _6 U. }* J; g2 | if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');* a7 x! Z0 Z* `0 `& x+ `, v
+ e+ D! o& Q$ c
4 ?$ n* X; b5 x# y. ^9 E
" G) P/ e. r: W" \9 [
/ A" Y- R2 P9 a$ ]7 E) I8 |( K3 }* k# f4 J; a1 w4 |
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);5 E( W6 M3 }2 K. E- l `9 t
7 g5 ^0 {/ W, o8 A9 Z& V
! z# L0 S K6 C1 U- s8 b! [4 x! E
( l- O& }; t) s6 g5 \9 _" M 9 w7 g2 ^4 t3 b' K2 q% Z
3 P# h0 Z2 {. N- Z. ]8 z! N1 W9 j $this->set_savepath($this->savepath);
$ K( B( B9 w3 }% a3 _* R+ R 5 V4 _! R& F- |2 _2 \
0 u* T9 x/ G- V( K4 o8 e; L+ U0 l3 Y $this->set_savename($this->savename);
- U: I1 B& P1 ] ) x3 F: ]% }; ^/ x3 U/ N( _& ~1 p
+ G+ Z6 Z2 |$ X0 ~ ' J+ W" @4 y, `5 ~9 k
Z- t: Z9 V3 ? ]
; X" ^3 J* ~- T. Q. a( H/ o
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);8 a) j# q9 l0 L0 r
! p* N, {* k7 x5 i! b2 _
: d+ s7 O$ c* {; S! Y if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);4 Q, Z6 K; Z% x' I# E6 N
0 u7 X# L$ P! b4 V9 W
- T: r" I0 b( `
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);9 i8 O/ S/ k. }
* T3 d& P# B* G2 h; c) w
) k! j* {! s' @/ G
: L- T, u# _# p( h; c+ m0 }
) p. K% i- R! x, m
3 C1 \1 [& q9 l, L( k1 j
$this->image = $this->is_image();
) } Q" a7 ]. Q5 v1 h3 b4 H: q
) z( m/ c4 ~# i) G& Z. {% O* J5 e4 K- N& `: d
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
& j4 l. l3 U: t- R+ M
4 P; c e: }; i* q( Q, V# B* _) l# i# g+ s2 m! k% ?1 b: o
return true;
2 k9 C4 `4 K; Z7 a% N! v2 w& ?
B1 m& O+ r1 ?9 E& i9 B8 B
7 \- a0 {- A2 w+ q5 m$ Q/ `! p }}1 D( m; f/ p' y, H- Z! y& O
' R2 I* e, V1 V, h/ n
* F' ?) F- a7 R) c! q
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
3 j" z9 ]$ a- y- h 4 r& t& i( x% R3 p* j/ R
# k) |& K! z% V( Y' D/ k7 ?" T
<?php9 I9 |: S5 @7 P& e
! y( z( J- Q5 x7 g. E- @3 ^ J$ D3 G* b
function is_allow() {
) k' }% u9 u2 Q/ G
3 H9 A/ |3 X. J* V! E' l
# _, A+ [" D8 i! F! ] if(!$this->fileformat) return false;( g4 K6 A8 L1 P3 V1 S$ h% W
5 T- K- n0 c* A5 B) y
2 X$ L. e1 z. ?# w0 E if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;+ |6 C( v0 S$ d& X9 k, A
3 S; E/ p0 d1 c8 P9 s) A$ t
3 _7 _3 _8 |* A3 v3 l B0 y
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;3 t0 P6 B5 M# J" }% ?$ o, Q7 P& o
+ t8 x5 O+ T/ @
( `0 A1 B! T4 N: ]
return true;
1 Q; \" R' q# y7 t9 W# V& i 1 Y; B0 E% o4 y7 |0 e2 R
" O8 D* D& b/ w& M/ t
}
6 @6 H% S, C! M* m: T) d
) }! O9 ]1 p" q- J" o
; W' e2 T8 P. W: ] 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。6 S* p& W: r; U0 Z; i
) b1 m* A! O& x- _' y; X3 ^: J& `% `- 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文件。
4 }& e$ a3 B% W B 5 _! N7 q: R, q+ f7 S8 ]2 n1 l
( J3 O4 a; e( }1 j9 j, P) G( a2 }& z. Q 漏洞利用
$ o$ K. P p6 l9 l0 ]( @
/ [4 T, `% E* W; r5 @ x, r4 w+ X! |: f
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
" [3 N: V( D/ l- ^3 P5 S
) z$ r# |, u9 R* S" h* ^8 I
9 q; b$ n3 |% B) L. X . U# d$ G+ ]$ a- ] }
( L# } A5 L9 a8 Z5 [2 r4 L
9 V$ n& Z' \: k" t- a
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid1 H8 Q& i1 x6 f, Z4 h' A( S
' s+ j$ V# o$ |0 w: S
# K1 I" Z% ~! L1 a" R0 }1 M 不过实际利用上会有一定的限制。$ [ i2 b( J4 R; _1 s- j
5 O! R( g R! y% H
6 I, \. l, W0 c
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
1 a) a5 [, U3 C1 O* P ! Q' b \: k6 p
! O P9 X, P$ T8 q( O
$ f! r! j3 h) F- d) L" R
0 ?! g# f& Q, G4 `( _. y9 j6 E5 K" \% {
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:: @( E6 h7 P" e
$ [4 W8 ~$ o0 A! S0 S$ m; }
4 g' u' p0 ?+ ]! w& P0 {, G0 v- j7 R 省略...$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 C+ g* R7 I' z9 i k. o9 ~
. S" q! a Y. N2 }- C
$ U1 p- |. N. V2 y/ S4 z) W! Q6 P5 f
因此要利用成功就需要条件竞争了。2 |; u9 g t$ r+ G- C
, W! r* H# ?* C5 K6 M+ q
) I& A4 s# T/ ~3 q, q4 |7 A 补丁分析1 U" R7 p4 p& J# V4 `4 F
$ w% e; w9 @% K- k( S3 H
1 Z3 J6 G) E# M
) E6 F* S+ s! g! `) h
4 C+ r+ }2 B+ f) \
+ T$ [; t D2 r& A b' p 在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
( y- `' z6 p2 d9 W
3 s' w! q" Y. `) t, s. Q
3 S# X& y' y* |9 |' i6 ? function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}# T$ m& |" g+ Y [4 [8 k7 u
8 w0 N9 b: F8 A- p3 y9 F0 l6 J4 R5 W0 t& ^6 y3 f4 R
3 N( f+ [: V7 x8 Y
) ]7 { y" G/ }7 E
% M" e9 j3 Z5 L 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。4 i8 R; z6 B) {8 l1 |: i* F
$ f4 l+ N4 _- X8 C, v
2 m3 D% u# s V! E+ }# W
在is_allow()中增加对$this->savename的二次检查。2 B3 J3 l! V5 {6 O
9 ?0 x2 j( t( ~4 c
; G7 T+ ~; J E% _( l& a7 q7 } 最后( P% `4 A1 H: q5 `; x. G6 l7 V @4 {
~7 n0 q/ ]2 J9 f$ O8 d9 d) j% ~: g% g. \# ]# \# U, X
嘛,祝各位大师傅中秋快乐!
& @* ^8 x6 g, Y : w+ x+ \% R4 ~, B% ?6 n' |. Q
7 n6 j5 u5 n* N2 g* ~- H$ a4 V 1 @& f- b4 K+ W6 Z9 F( C+ ^
+ s5 u3 V: Z% j
|