2 D; E# A& C* E1 q# ]
1 G$ O& h9 Z4 b% W8 k6 M e. T1 w" c8 W) u
0 s7 n0 l4 Q: d6 i
前言) o0 N" j* S1 q+ p. b9 V
/ l, I5 a& Y) W* t/ b0 i8 g4 B! [
; P1 h( R. b% \2 N! ^; T8 n
2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
5 }/ R: B3 z7 {" [3 M0 Q4 U* d 4 E( P7 h0 j* E' Y2 O) C
7 z* S1 ^ n& T
1 m3 u& ]& h5 X9 k7 q/ [) E8 G \; b . F% b! {5 u( t1 I
( y, b, P4 Y# f$ A( \, k
漏洞分析, F, W+ ~- j; Z+ H: k2 G. N
9 u; n1 z9 N0 y9 j5 _$ l' }! e! w
4 A% |2 ?6 i, R: { 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
! K' F( X$ z; | S" s1 @- M% D* J! A) B 7 a0 O, f& m7 ?5 q' i: U: |, H
3 m* i4 J. f3 u * M. ~, j) _- P; y
& k4 P8 i' _% K! ^& d5 j. G4 M4 y
/ m; [6 ]6 d+ e' o 对应着avatar.inc.php代码如下:0 y- d, F" W* S2 y3 P4 p6 [" Q; ^
]3 V3 X! V9 v9 |. T% |- D c* ^6 W! [- E0 V6 A8 Q
<?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) {6 |. f+ P6 T( Q
3 \6 p2 H$ ]) ^
% {. [# L- L1 @ V6 F/ A case 'upload':7 C7 z# k+ _2 x1 e- \* K
# }7 H, n& _& E/ |
* o) M% l* a! e: m1 R if(!$_FILES['file']['size']) {5 u# z4 C7 a4 N( v: M; {
6 m7 K* E9 q) p& E* W
/ D. E& t. o; N1 `! M if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
( j' f) U6 z- t1 a
5 |; K9 e/ @; y, A) ]% X' a, `# l
) Q* Q1 V9 c" f exit('{"error":1,"message":"Error FILE"}');
/ V; v1 r4 [" ~8 `! G# g: Q# o 3 l' ~4 q" w5 f# c r! O, O
8 _9 t i8 I- L, E# c
}% a) G. X5 L# g3 O" E- t+ o
5 j5 Q+ h; r- B* B# J. U1 ?% f
( h1 B$ S5 A1 }9 U$ h. \* r$ U require DT_ROOT.'/include/upload.class.php';
) P2 O/ m( J# p- k0 O5 a4 q % i4 ^6 M$ q0 g& W# W. v
$ M+ T& \9 a4 c# W/ ~6 o
7 R# D$ o* K, F1 H 7 W; K, |! r& {) d% U k
; d% a4 Z8 ~1 d0 M
$ext = file_ext($_FILES['file']['name']);: _0 P! Q5 h4 V" @( Q
; b' M9 [" Y+ W, V
* l0 X1 y# i# _$ Q# ^* u2 i
$name = 'avatar'.$_userid.'.'.$ext;
. U$ y/ F% ^4 A# @ * G- r9 I+ ?/ M5 Q
3 T0 }2 E( c$ V2 _9 v. X: u1 F5 Z $file = DT_ROOT.'/file/temp/'.$name;
, M" H7 d0 `9 ^* F6 K; P% {/ `# C( ` # w) u' U E6 N/ n0 ~( L" c
& A9 Z) b# H' r
: G3 K9 ~$ [0 |
5 E. [" H* m- X5 g& }' r5 {* v c
6 f4 Z I$ ^; Q1 d* k! r if(is_file($file)) file_del($file);
4 G$ d7 w d) ?( e6 s2 | ' ?6 I4 V/ b2 n: Z+ M9 e7 e
1 U/ Y( {/ q, l2 {; v, A $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');' z+ A/ X9 l; D3 p3 F% J
" c& `( c% I- s8 p7 p( w
) D a7 ^/ w$ s, x( B' R, S
# F x' L# a( c J& A
7 s9 m, H7 g% k1 A' R( ^* [" N6 A8 `- k. j+ ?6 _& |
$upload->adduserid = false;
: x; s/ t$ a1 U* e) ?1 k: ~
1 c$ ?6 N1 z2 U# a z+ V3 j: m# ^
7 V3 W% j) S2 S8 y5 J" P4 P& R
- z+ l: {0 m5 f. U- i) }- f" L7 c; Q0 s% t 6 p4 |; d! n5 ~ i
. P6 c1 `$ y0 o) l
if($upload->save()) {
; a. \0 F. @% ?1 t- f 3 I" k% J3 q' f
) X9 J9 _! B3 \: H9 [7 u ...
; Z1 |% T) ?% M4 P" B& F
0 J. `$ q, B/ ]7 A0 Q6 r$ w( O. v* w2 g+ T" y, s& v' `5 `3 `
} else {
( j7 j2 o7 ?% D* Z0 j+ B
! S2 |6 n( T# _3 n' Q; f$ K8 g9 B8 e" {; g' D* `( Q
...
* v7 d! s/ C: {+ h# R3 @- D ! O( a! _0 [: L& m
- \: e" C& G9 [0 R+ `2 w7 a0 g u
}5 {( z& j- u/ }: I1 j/ X0 w
% |7 c0 P9 ?( S- ]8 B7 F( h
9 J% w0 ?: [1 X! L/ M2 A break;
( q7 O3 `% A8 F7 p9 Y5 D + i. @1 z: U$ ~% C6 U- R* O. Q
: o5 S0 \; Q' K! d; F" s
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。2 V! ^+ K0 e5 m) O( ^
" r( H: L: I$ C8 @2 b( V) Z( z" a
" q2 j( `( p- j# l: q
upload对象构造函数如下,include/upload.class.php:25:# \ x3 R7 _. T: e7 D
4 C' T* z1 E, q/ j7 {3 K, P* J4 \
. H. l- g! G2 s, Q# Z* s1 K2 g <?phpclass upload {
$ g1 Y% l2 A4 w& Y E y
- o1 |+ P7 k0 V$ {9 c9 I, t; ^, U& i: m" d1 Y
function __construct($_file, $savepath, $savename = '', $fileformat = '') {
; s# R }8 I" ~4 s' ?1 ^% y4 u ) V, `% @# \ J8 y6 U
7 \, B m. b0 C" _5 B
global $DT, $_userid;/ L. N7 Z2 ?2 I
* j, z: n# s0 ?4 W7 h
3 g( t" _; x% h foreach($_file as $file) {
1 V, v. |; k/ J; J, o6 r / Y+ d/ I7 P0 P3 P; |7 F
2 U) ^, l" U( k9 a
$this->file = $file['tmp_name'];! V2 ^' b" D5 f; |. d/ q- N3 ~
. ^3 B! V: Z1 H( s) a" g% k' y
% V' o9 w2 w, L. G $this->file_name = $file['name'];
$ Z1 M, e& S: j, p+ A * h* e; B- B0 L4 C0 R. \
) X$ H' L& h; U" F' ?
$this->file_size = $file['size'];# P/ |+ \! ?/ i8 {: }
* f5 Z# w$ D2 \' K" S
. {. O$ Y; A5 Z9 i {
$this->file_type = $file['type'];
) e+ j) C# o. t) T
6 f5 @ R% W( I6 u+ k% R5 E1 |% S# _( W8 Z6 j4 Y
$this->file_error = $file['error'];. K& f$ i, S8 }( H: F0 D+ I" A% ]* `
2 z3 i8 @% K. R
% l3 o7 G$ X# k1 o, m- W4 Q, A : p6 e& \ t9 W4 e) j
% n( O8 Z. H" V( |
2 W! i& n+ l$ ^# n }
+ T' x2 v7 j+ {2 B1 Y ! @; A: m/ u0 r6 I9 S/ W
4 n9 R" A4 ~% i* h0 _ $this->userid = $_userid;5 d) X9 H3 q4 Y1 z
+ v' }! v/ N6 D5 s2 I- ?" k6 u5 k1 ^: O
$this->ext = file_ext($this->file_name);
2 I) h, r( ^/ e ( E" J$ V$ n+ ]
$ d# y4 b) R/ i. q1 C- [7 f
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];2 c8 X! x6 \2 U
/ f2 c9 o& W! g- I' d* g" J) b! C* y! w0 o. ^
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;- @! b# `) v8 O' M" z5 e2 e
. j3 b7 T. a* J* h
8 c# j4 j0 o/ D $this->savepath = $savepath;6 W6 t! ]! v3 P* {: J" Q, r% W
! X, y' K5 u! S. K: _# |2 j
+ D! L/ v8 C' i4 R* s
$this->savename = $savename;
( l- C! t, M& Y1 C2 I
6 h1 x0 X; @" J2 ?, y: U: k3 Q) B$ y8 ?+ _8 g
}}
7 h8 e* w ]8 i2 |
% f. S% h: W* V+ O/ O5 x3 ~! k ^) I
这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。0 T( T: l; `2 h# F# f
1 B( O! C' U: |7 y$ H" ]. j M; C7 j: Z* G2 ]; W
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 5 ]1 L+ [* I! s2 p
7 M& F& b2 k, P) d6 W/ S5 K) {
6 D, U' Y( e5 A: y% 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+ I7 L# ~* k8 i8 A) a
^" F" ^4 ?. K8 p6 X/ K% V+ q/ Z
' D6 o( U& b- b& b" H, K 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:! P0 V& O2 F9 q4 T9 N- s
( e+ @" c0 x' ~. _
: w$ ~5 F, G. @+ J7 @" J$ d
% b' M/ ?! T3 I' r& s : a$ \2 ~& D) @2 S- U, @" Q
& P! e% o$ s& N 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
7 E! X2 J, s% L5 p2 S8 T& J : Q0 G# v1 z' D- t; [
% A; q. k7 Z1 b5 q, }2 q# b9 {
<?phpclass upload {
" f# h) F J: }) b - i, n" E( h z; X2 `* i6 n* H
3 @3 i- {% p$ j3 O2 [
function save() {
& c7 j% f" O9 X6 Y. M) n% |; i 6 {, [5 F0 V8 |! h. _
+ r/ x0 [: ^( c' P6 h- U) g include load('include.lang');, C. Z& c$ I7 m' m8 t
6 |- V }# w1 M m
; a+ o! d, c% n; ]3 h if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
; `0 S7 x' H, b6 J# W / [1 p* I/ M* a1 l
t* z( O0 q% I+ o: W- Z" H0 \ / s2 R' d- a) q; O3 O: ?5 o
) t, z; m$ ]2 S- @
$ W; i" j+ u) I3 ?* f9 N if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');
Y5 E2 v: d2 Z
* Z) E5 W/ P4 L- K$ I# U# U5 K2 y
6 u% q$ s G6 o2 a9 [5 K/ O# d
, [- E% u8 ?, o7 T/ L
- v5 D. z5 I, T" P( @( {) T/ w if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);4 E& J9 \) ]* L4 P7 ~5 s
- U! R) `( Q9 l* q& y& v T
, T, T3 R1 {! E# j/ f
( x- X% r2 m( G% l
. k& J) K, {) n( x3 K; g7 Y: b) L5 a/ P5 C0 p( `
$this->set_savepath($this->savepath);
8 L+ J9 W# k8 y& E. F 9 G1 z* h* p* `' D: o
/ F4 V. |1 [: A0 Y# R( ]& R7 m1 V $this->set_savename($this->savename);, \) ~" M( f+ h, E, i
% |& {% ~3 ^6 W: p' J6 f& Q3 o! `* S- c4 N. L) M0 M- l
& r- g+ o0 c$ ?7 w' S3 ~3 f
, ~7 {; T/ H& c5 X8 K: q, O! q# Z% T" }. S- C
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
' X+ l% k7 w! o0 O " m4 B# o7 u8 I9 M6 {5 L1 {
$ T6 y$ S0 n3 h3 q- @
if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);3 q0 l7 c2 G F7 I* n
$ m3 O& u" |9 {
; V9 u/ V3 h; i. H, K
if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
/ v7 ]. F: X* ]! L; {# t- \+ L
! S: o' r @0 V" U* H, r# n2 a1 w/ a6 n' W8 N- w
/ V2 x5 K& x/ k! {5 q1 S( a
9 p+ m6 x/ r( E) v: C1 Y+ v, |' J$ q8 j% ]: i6 W! B# t
$this->image = $this->is_image();
, P7 J( B+ f# ?$ I. v
% T% k1 X$ M4 {8 L% p h, F$ @# |- U
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);
' u7 Q1 z: A+ y6 @3 G& V ( s5 c( O& A% B i$ o+ A- S6 X& O/ H
2 i4 P# x- ~- U1 h- x; m( d7 | return true;
, x- J( D9 Z7 X5 c4 @9 N, j
0 Q1 J, }7 c) Q2 s$ w1 y% A/ _. b' z" Y' d& R. G) h0 A7 \# L
}}
l0 t* o, I3 v) q+ g- o! l1 _
$ Y/ X( n6 O& ^: m1 K( G& D: l* r" w
先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
8 g# d4 ?4 h3 e0 F9 j! y
& A* x: v* ? E3 L, K# J L* j/ z' G2 i$ ?0 d! P, U2 d" O8 J
<?php6 \: [1 }% n% a" ^
% m% }" V6 x0 z8 r( ]
- |1 I" g, Y: S; K function is_allow() {( D i# X! {# ~
7 [0 y4 K( F3 y
: ~9 i p% x) U, v9 d `0 e
if(!$this->fileformat) return false;
4 I2 |4 q4 N; r& c
* y, L* Z, |6 r2 }4 \+ q& x" x+ V- W1 g2 n. `" r
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;0 S' z' X. U+ r' D
+ T/ L0 n0 s& O7 j% L0 L
6 R! o6 ?/ U# [. W
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;
O1 m \4 E1 g! [6 \6 Q1 k) K# _ 9 U6 _& L4 Q, y6 _
( z) g2 m9 o3 m* J0 N( G; ?
return true;
) U1 j6 ~7 D$ w7 y
" K: _4 o H' C' L ~( d- a E' C# L3 t4 @# j7 }( l0 s2 E
}8 i6 @, E. p7 n* ^* N( O
+ f: z: W& j2 H! H- ~
0 I8 O* ~. \* ?/ m/ k5 j 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
+ T2 C4 l- M" l; T( T0 k+ y. a # R" `7 J! Q, H6 w: o @
6 ~+ i T# M9 d7 S* q. { 接着会进行真正的保存。通过$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文件。
' z3 \( e4 E% [7 M: t 3 |" B, M/ m* P+ b5 m, M+ F* b
: d2 n( L" P7 `+ u" R
漏洞利用* y& B0 @" p' ]3 f" }: c4 e
. ^; I# S! K. \. y( ^: u# S( Y' p
+ R6 c- R$ d* I
综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
9 `2 a8 |. Z; O% `2 O
+ k1 z+ A* R W: z; s1 e3 R" l+ Z
( O- }! S0 w. t6 q5 Q( j 1 I6 R6 q0 r& y- L: B7 ~
% j) |. g. _" q; c( R
4 b! B& |3 N/ k" Q 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid) v* f1 W% {0 o
/ u, v; c% f! d
$ ]) X) p/ }* O3 L: L3 }
不过实际利用上会有一定的限制。
( r: v5 w, P% a7 H0 U; M
5 p* U; q# n2 d9 D* s# H1 {$ t6 D/ }0 `3 N/ }
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
4 b( B: d/ n6 S4 h
( V2 H7 i5 a4 q# G: \* B* A& ^$ g) r* Y. M' ?( A2 X0 I. f, j
/ |5 X1 q: o2 m' d
0 _8 U7 L1 c0 C |: o
' ?( ?' H% S# I( u5 e
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:1 c: p) q) Q/ v7 P+ Y+ h3 X
9 ?, O N: j. |6 B" o
9 e; A: M. m) ~/ x: p 省略...$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]);省略...
& @9 `6 L& r$ S
! `) R; V9 r. E; N
& _! @) T/ H# c3 F 因此要利用成功就需要条件竞争了。! ]. w+ L' Z! ^2 ~9 W. ^- Y# Q$ x0 s
5 Z+ X) c4 p$ j( t& W2 f R
1 ]' Y0 O- b z- O# \. E 补丁分析
+ N$ b: l1 l3 |1 ^" Q4 R, l# d( r6 e8 |/ I: N! t; n O! I
" i8 `9 t# |6 [0 k 1 `! t* U2 c- Z' |- |
2 i" d2 J% Z! p! S, d* O
' u0 B# j9 w9 G9 z3 c
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:+ l' R; g, m, j. `! I
( [9 a+ ~6 X2 W2 E0 S! M
8 p+ E# H ?& W1 W function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}/ m: P) V7 P# l7 [
- N- G& J! }; N( i$ F3 x
4 U- @. n7 x: u5 V7 \( @1 a
1 q0 o5 G8 [4 }' |+ [; W2 x, n
4 B$ x, E4 M) t/ C6 b
4 Q# ~3 C; |! O) O/ R$ s 在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。
( a8 E; f4 h8 A# e: G8 ~# O / A% x3 k& u9 m; m+ q) t# A
" w9 }' ]6 v5 J+ u, o/ p8 p) J 在is_allow()中增加对$this->savename的二次检查。
+ I5 L) r& R$ Q! t S9 [1 |1 X4 g# r 0 N0 \5 D' h$ Y3 X; Y. n2 z" h, U
8 Z7 ]! Y6 Y! X& ~- Z9 Q. ^ 最后
4 a2 i3 e; X: m1 S- l0 h9 H9 C
4 N% Y. I8 v# ~9 y# A4 }
7 ]1 S# t2 `$ Z! i 嘛,祝各位大师傅中秋快乐!. I* p( C# g$ N' e/ Z# c3 J
: L4 j9 c( |# B! G" v4 A5 L' t, W& `4 c. ~
4 L" j, h% E: l9 P0 A1 B L
5 s7 U3 P% v' Y6 k; v1 y# o
|