3 L- ^' o# e5 T
, z! \4 o5 f1 L5 U! g8 A2 N- a. \ D& T
3 M, Y7 D" x5 i& I( h; H
前言
4 B1 F8 _8 V$ Q8 b9 P- ^4 s: X
6 E2 H/ p6 [- k8 |8 P- j& e" G) m9 K
- S' B2 G+ e( m3 h* d9 R 2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。$ w. n* s/ M O4 I6 ~
: G# u* `: c9 {6 g) [$ A2 b4 X
! y% _8 k' L& }+ L
1 D1 Q1 ?8 Q2 V' W: }7 m Z / |, x! i( R" U+ q6 u( X* g
. t- o( [: K5 Q
漏洞分析3 k$ t- ]$ W& q9 K T
2 v R9 [5 \/ Z" h
7 Q, G1 O3 c, J* }9 T$ s* P. ^( Q 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:) q+ O) l; U2 V4 z8 A
! H, n1 N8 j. x$ }) u, v' A- K; I* |7 n' T0 C. T
! |7 ?: @. G' D' K. g4 u5 K
/ }- E1 E/ D( C3 T, L/ w/ ^- t
* D! t0 l$ U% `$ N! ^6 r( D; e) G
对应着avatar.inc.php代码如下:
# N/ A+ o2 R0 t: k6 I
5 m# U: q3 J. m4 F/ s$ A6 \$ ?3 v. U0 W; g: d
<?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) {
- }( u1 h3 K, C; x* f - f8 K% ^. C. X3 }/ `0 w
. D: v; X9 `" ?7 q# E6 o! o8 `! t) K
case 'upload':
0 e6 t9 X. V4 i) y8 Z9 b: b ( v; t. T8 e! u
3 \. J B i. l% j7 Y: Y
if(!$_FILES['file']['size']) {, Q, k( x: F5 b7 R% Z
7 O# y6 E' n" J S
) v/ Y' z9 L7 s if($DT_PC) dheader('?action=html&reload='.$DT_TIME);
2 o+ z& l6 f' j9 S
! k) ^8 Q9 p! J5 w9 b
, r0 C! t$ x+ X exit('{"error":1,"message":"Error FILE"}');9 J! ~& H* C. m/ r( h# n
- a% M e8 l1 J! Z
! l/ |/ O5 j6 N$ i
}" X4 J ]3 r- J! A# |
# M9 L* O9 [2 Q+ _+ l; c$ V* X, T) U# A! X" \, l
require DT_ROOT.'/include/upload.class.php';
& L( y' W/ \) y) p/ t' c) i : A+ e" q0 e( P
! K9 ]/ ^- T4 e4 r( {4 \7 X6 C* X
+ n8 N7 E7 k% \2 |# a N6 A2 o2 Y9 C# G( @ p' [+ n" {
9 { e, F0 J; r s $ext = file_ext($_FILES['file']['name']);- D6 @ V: N5 k
' n+ v' K/ x) P9 I& y& }- i
3 l$ ?: J' `* \8 w% c- l $name = 'avatar'.$_userid.'.'.$ext;
1 M2 {% L& T. u+ ?
+ Q1 a+ D& q% ]
k7 V) J' H& e e- A Q+ c/ L5 ^+ X $file = DT_ROOT.'/file/temp/'.$name;* I1 u( i. L! G0 H
8 S( K1 Y2 M3 X
" k$ m& i( B! ?+ Z5 [
8 W1 B2 f. t4 H9 B
+ J! Z8 b/ u5 x5 H3 t4 b" y; z, s) k! H' v6 }5 C) P; [
if(is_file($file)) file_del($file);3 o9 y/ m. G- Y9 Y
3 }# p* L! _; a2 q* k
1 c2 v# A, s/ `( l $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
/ I7 f- O; N$ u6 j
. G M2 M$ L* Z
$ z0 n! K, K8 d9 N
0 _% v. O: r1 K! A, c3 @
0 `. P4 m1 F, _) j2 S
! C. N+ T* Q, b# p; R, G0 f $upload->adduserid = false;
# O: J0 @# {1 c; W$ v+ p
* L! o+ A$ E! G5 D; O
, m& c1 u0 h, |; l8 O
9 P* Y2 p! h D' f 9 [& w/ W5 w5 B
; e [7 Z8 C# m8 Q- ~4 ]% ]
if($upload->save()) {2 A# [+ g [) l
s2 {: ]# K( Z/ j3 [" v$ h( k
; k! ]0 q% d" I; \ ...
& ]9 u. t" p# |7 I% T2 ? " j w8 F1 Q: R) m
/ W& _/ `% o5 g/ s1 k) A } else {
+ ~/ f& L* D7 q* P
, C) p( j0 N7 N* z' b6 v6 u$ l* X" R; v8 \, a& L- s
...
1 x, k% M \3 p. R6 }/ ]3 C2 ^
' t: m; z6 w1 x5 g/ Q6 c+ Z# O$ I, b7 G8 G7 h
}
; k& U/ q) z. P+ ]7 d% S | ; ]! y" J( W# l; \* p) L
+ o5 r! b( F& h! [& q
break;
# D- U1 F$ G J6 K" h5 I # C& \6 H- P2 s/ ~. x
( @( e& a5 j# h* B
这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。" ]# H$ f4 `* Z N: P- p3 x
2 S3 f4 \: x7 ^' a$ p- \0 N, {+ a) g; @! {% H" u
upload对象构造函数如下,include/upload.class.php:25:# r$ z! i" Q$ `) S* Y
) h6 q5 @5 e, `$ \% }% H+ v; N
* ` t' J% C( N1 Z! N5 m <?phpclass upload {
9 h( Z: X0 T0 \, `/ x) V
. k+ V1 l$ a4 a4 j m8 _. C9 ~0 o3 o4 o) y) \4 {$ t; |% h0 h: k
function __construct($_file, $savepath, $savename = '', $fileformat = '') {/ P+ M* U* e- U$ p
$ t' z$ _( f' _( Z& r& R. ?8 \! \
* X7 E& G7 `% T3 O
global $DT, $_userid;7 k3 r0 |8 S2 O& b. w% v
- y- n" }( O4 d$ e5 h e/ t& S+ z1 V. _# V
. {+ ^% @' d- X d( g( ` foreach($_file as $file) {
, s) O& P, R( ^- T p+ R& ?; T
$ g- h' }+ j! Q8 c) F0 R
! u( x3 L% t4 _8 G* }( A+ U. w $this->file = $file['tmp_name'];% }+ |$ h( B+ F" j
3 L r+ c( R7 G u/ R
/ f2 u# s# u3 t2 C+ U9 l $this->file_name = $file['name'];" D6 q2 V: V" U
: z7 f, e! H* k2 N7 |3 l" _8 C, ]( i7 K
$this->file_size = $file['size'];
" k# u5 w, b! G1 T : `$ F! J3 H# `9 C( \0 _
0 m9 W# Q& J& t
$this->file_type = $file['type'];8 R- F# S6 i$ S4 t5 ~% s# G
2 D/ l7 B! N/ R D
- x$ O& }' S9 F; ]7 J* t
$this->file_error = $file['error'];- a* Q3 I- W4 _7 U( A
( M# u. w( T h) B: y) Z
/ H, g" W% r( z+ G/ H0 c ; q0 `( M5 X+ d8 ^* k: b* @7 t
4 ?/ `0 V6 R" f" x9 ~- w: s# }( M. B" B
}
$ z6 ^$ x& X% _9 n, r- e # w/ O) e& V+ |# G0 b6 A( n# b: Y
$ d6 v. J0 r9 S7 F( f; }4 C8 J
$this->userid = $_userid;
- L4 \9 i, V' S: l, O2 K
* C! f8 t2 A: u; P* m2 K, c; Z- n$ m, o
$this->ext = file_ext($this->file_name);1 p8 \( [% B& Z4 p0 O# `5 X- S
2 E3 h2 M$ p5 D# e/ [5 ~3 O8 `" c( e- ~, ?, G
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];
3 U; D" c1 K# b$ ]6 j ) s. _. G' i$ Y; d
) M( R# }1 q: B. M% h" T' s2 W( G
$this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
- {; ^4 }1 ~+ z* @; Q$ }* k
; m' i& I! c7 Z$ q& @+ M7 u
& I3 a, e% y' u& t4 P. r4 G $this->savepath = $savepath;! \' z, M" v j- \6 Z
7 b' B ]% q3 o7 _4 X. S4 w
/ y% r0 |/ c, Z% p& X6 }7 ~6 _
$this->savename = $savename;
5 S6 o- h0 G3 Z& A+ E& D# _3 s) O 4 ?, p l% P1 f+ Z q8 `
2 ]9 q) N( o9 t
}}5 b' R7 z( }% s; o
0 w) O& r1 p4 N( c, z7 e
0 C2 ?- ]& x; Z0 d 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。7 n8 }/ T! I0 f2 n( e
5 ^7 U& {# n+ u% U
+ u8 a( a9 H/ _/ x 因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 2 j( L8 K: r2 G. ]8 e
7 Q' h6 Y6 p0 ~4 e6 ~7 Z2 C, b
/ a0 x* D5 O9 Z3 i9 h3 i 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.php9 _' X7 ^9 X' d2 G
1 V8 k6 v9 A- C" L7 O8 U
) \( ]( Y: W/ ~7 T. m ^# T 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:
5 i! O) U" R9 r
1 s0 O8 c( H- _/ g' M8 E2 E5 Q' }# M
- q* Y6 ^' U" a0 G 3 `- B: R( o' ^2 a
. L- Q& ~6 M% g 回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:& |, f4 E9 K8 F2 p6 z! G
; S5 R0 W6 A! {2 G
8 V( F( v' f! T <?phpclass upload {7 a/ ?9 r5 i, @$ X
+ j* u6 a: f# x( N0 R3 J5 z& V
8 D, W! n6 z9 c1 \7 Y9 e
function save() {
* U* y; G6 @& m- b) o: M$ O & _' V7 [- A3 Y# |9 T/ a$ `6 n
9 O9 I1 h V/ ^! l6 v4 [5 v1 s
include load('include.lang');
% {/ k9 p1 H0 g. P& ^ ; e$ d: m/ Y: Q; d
3 p6 G! C8 A! z9 ^ if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
, h/ r; G% y& Y( y- D- l
; i" b J' _0 C
5 N" L% e$ i7 z E, y + R; A4 E& f% v& Y, c( `- W
1 o8 r X$ F6 e% P( A( K# f9 u6 k5 h
1 q8 x! K2 F: ^9 \( z3 p
if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');+ p8 N5 a$ X& w# U; g1 Q
Q4 V/ h, ^8 O. l. {+ O# D. i9 G' _3 B* v q0 A6 T, c# X
) F, J6 I9 h G; m' {3 w ; k% s! N5 _3 \- x/ ?3 H1 K
$ O1 y: L, f& h
if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);8 d' S0 i0 f' k. a
* k) w6 }: ~; a5 k4 m x, h# t/ b& x- ]9 N$ ~! G2 I
* s: E: r6 p' l- d9 S) q
8 I' c/ I8 d% X+ ?- ]' L* y, T! {
$this->set_savepath($this->savepath);- l( K9 m2 l7 H* ^/ L2 b1 i
) E! p/ `) e. D* b; Q+ C( `
- ^4 ^0 `. F# H: L. `' } $this->set_savename($this->savename);7 t6 h/ ?2 v( _ y1 D
. T. \+ t, j h$ C# ? L G$ w1 `* [* Q" e7 N8 ]" q
3 G+ e4 C6 ]1 H
' c2 n1 z' E- n+ E' k, c( N4 d% n# m5 e: g" J& f
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);7 y( W0 s; G9 P1 u! Y' }+ N" g
8 l5 u/ u# l6 g. p a
* B+ ^' s. z# x8 R3 D if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);% u. G0 A5 B3 c T u2 p- ~
; _2 _- F( U' l- ]9 t5 \
! K8 b0 Q( G# m5 | if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);& u: O! H c* P
5 u# L! x5 f; |
0 _% k$ W' c# `9 C; a; w5 D * `( \7 ]; K! M
0 l$ }. \: i7 c$ M K% u8 O2 s+ B3 D0 ~0 e' r) s
$this->image = $this->is_image();
" h9 ~9 ]; ]! J3 K% T* T2 A
; c" m$ e) @! X, W. x+ X$ J- {6 {. T e1 p
if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);# A6 d* g( z, L2 c% o/ _" a1 C
& n" u0 ^6 l$ M( w6 d8 Y- A; K: w `1 s8 v9 t7 b5 Q% q7 B
return true;+ b- t; O0 @, I
1 a; S: o; R* l' ~7 p2 T/ D& ]
* G3 ~% D2 F0 D2 E* Z }}
% [+ y' z! @7 u; d4 R
/ V8 `( A5 K/ N+ U4 h
7 e1 P, F- m6 D" h3 h5 x5 } 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:
) j* Z) ^* z1 A# d+ \8 H
9 s' S& s& O. Z# t" z! x: A- N, ~0 S* z0 F7 X8 {6 C5 F2 a
<?php
/ U% y9 x( w5 b8 F1 l$ C( P4 W 1 R8 H3 K: Z9 k: i0 [8 T$ [
" X* s" V' z2 w! ]' ^& i
function is_allow() {
7 u) E: W9 g: F' H8 |
' L2 b9 I4 H" a6 J$ s
3 ^" Q. i' v1 \( a" E/ y2 O if(!$this->fileformat) return false;
& j' c" @( H# A 6 @/ z3 A. i4 c: i0 Y6 }
' p: R9 ~. h6 W' B2 p) z
if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;& i1 C9 D6 {9 f) x
# S D) B2 f' B' ]" ?/ H+ @4 Q' h5 }
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 |$ E0 ?) F' b/ ^
; V7 Z j) u' s6 w% u8 K
! A: F( k3 s9 u' S+ f! `0 I5 V5 C return true;
( i) j/ R& E" n6 @$ C ) O- b4 v/ F* o8 O6 D& Z
W5 Z; c' K1 W9 J7 G
}
7 |' |3 r8 q5 f- i+ H) \. q 5 W# w. `7 o0 V! w. ~. l/ g
. z1 {, Q6 Q p3 r 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。1 o" L, [( [, K' {* I2 G2 f/ J
* k- P+ j7 B. t: s$ W
. Q; P0 v% A+ s7 A8 J @) R 接着会进行真正的保存。通过$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文件。& D- ~) b" s7 F8 _9 ^
+ g9 R7 k8 e6 z. \6 o
: y2 d9 N% t' F2 I: s O 漏洞利用
- c; y/ L9 ?& X$ y, m
# @; j! b) U: k- U& j' z
9 I- o$ @7 _% {4 T2 {; w2 U5 I 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。
2 ~ Z7 p }' |
! O+ |( N5 z, n, b2 b
3 t' L3 K8 S5 F7 V V$ H4 q6 l . G8 u& e$ c! J1 ]
4 p9 }4 c% r4 s( g- ?6 b8 c, }' X; Y* v1 v8 W
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid/ x* p, g/ g6 k
* M+ ? X9 t7 c" R7 H$ r$ O, f5 j! z9 X/ r- @& p0 ]
不过实际利用上会有一定的限制。
' J' |' I' M, C% g% c
" t( T7 c" \# m5 U8 i+ k" n }4 ]) h R* K' K
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
1 T8 O8 f$ k9 S. g; @1 ] ( o& B2 k) K4 k. E- S+ E G+ R
- B/ n) v8 ^1 B3 ^& D
+ N1 a1 B, H, S4 x; X 4 [( q1 S! u5 n! O
' M |$ {; S7 k3 a5 Y8 G
第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:$ Z* X4 C* ], L% ~3 b
4 R$ E( i0 s+ p$ B c2 l9 g7 A% c# R ?8 P* H6 n
省略...$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]);省略...
: {' T8 f8 O" T" o1 {4 G7 N
+ Z3 d2 I; ]( V) |
8 a- k! N7 o, t; @+ ] 因此要利用成功就需要条件竞争了。
( P4 f1 L2 g* H; B# H4 c # s ~ M! K+ g9 \- P8 Q6 k8 J
6 r( x# d' X% v. R7 c
补丁分析% j: m9 q" t" Y, j6 Z: H0 D
1 \: u) I( T7 f' P2 [
+ }3 S. y8 K8 Z8 I5 M; A 4 p& b% y/ f8 G7 I6 s5 W8 x
( |, [5 K* d8 ?5 N& X6 g) B
8 u' j' _0 c& h C
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
; e5 x+ j) R& R; w
: K# t. q" G. b
* G; I: B/ _3 Z, \5 W function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
5 Z9 u7 k$ X! B/ a* Z4 l! J' p " M; b( a }# B: U! z/ e" b
$ Q% ]! q0 q. ~
+ n* Y" n4 ?3 Y; Q& ^4 B1 U% b , E; n( c2 B% b: G
! g* f) R# G' d, p
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。- A- k- e( L4 Q6 z' m
9 n$ @ {; w; W1 _! l+ j& t, M9 ]( w9 g4 F8 q
在is_allow()中增加对$this->savename的二次检查。) t. \- L. a; s2 ]# I
! Y& @ F# P" K, |; b
4 J$ a4 v$ I& n2 s 最后
. A( d/ W6 `- }3 s3 ~3 r4 ~8 I
]0 x4 l# [6 v. [; A/ s% T
+ p; A1 q' H }2 O- t/ G+ _: h 嘛,祝各位大师傅中秋快乐!
_, u- c( ]3 O" B9 ] B 8 S* D% x# p8 n( } ^
9 {0 C1 h" Q" [# j
+ G m! p0 R4 q4 |! l
+ T# X- K" o1 o: V2 w
|