2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。# l2 B0 ]+ y5 d, ~# F6 }4 G# n
: u" [. w; Y7 B4 {5 B7 k" p* e
; [$ X' B t! t& L$ ] n 根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:+ s/ \) w# n2 r) K7 d
1 M) C. Q, u2 q
对应着avatar.inc.php代码如下:4 e. h1 r. V8 m' T- }
4 ]. ~$ v. x1 ]6 K, E6 w<?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) {
/ f1 y& k2 Q8 Y' L! M case 'upload':
' K: t/ H5 p9 S. K! N% I) V2 E& v. }if(!$_FILES['file']['size']) {; T$ I& b4 d% t+ L1 O
if($DT_PC) dheader('?action=html&reload='.$DT_TIME); p! G P+ a [: q9 a# f+ b0 B0 D
exit('{"error":1,"message":"Error FILE"}');! B/ w# \0 v/ i2 t$ P# V7 Y
. p u+ Q' y: A3 B. [6 z, ?4 s}
* {, b( [7 E7 Q; N+ }, P# `7 S( Q# ^8 Y+ s6 c require DT_ROOT.'/include/upload.class.php';
0 {' k9 L6 K, R0 u 3 _* @! Q, N$ k- ]4 k. F* Z" q. w
2 X" _* F' H, p6 Z+ [, \$ext = file_ext($_FILES['file']['name']);
+ A: y3 W" i8 M; E5 ]7 T- v7 ?8 S' M; H6 x" D# J# K0 ^/ T0 M/ n $name = 'avatar'.$_userid.'.'.$ext;8 p" R4 Y/ y, M
o9 H G( ?0 ~' A# n! j+ W. b7 J" R7 g, `- E' @8 z $file = DT_ROOT.'/file/temp/'.$name;: ]: {* { ~/ b9 r7 V
- }( s- Z2 Z y+ O9 p* r* E
! T$ X" k. N$ n& C1 M% P6 tif(is_file($file)) file_del($file);4 A0 w* q. c2 [' M
9 p5 P; z- S+ y$ J: t6 ?1 u $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png');
6 p8 C# O- b R/ w. r6 J$ q& Z# J8 h' ^% Y4 v' d/ b
2 Z f& A1 g! O0 a- m- K$upload->adduserid = false;
1 q& v" j( f! J$ A) C V7 y5 N: u0 T9 L/ ~, b
+ ~& I& |' W9 a9 k: x8 U# A7 N% U" v! p if($upload->save()) {
8 E5 M- h6 h7 y3 Z# D. m6 [...% t$ k+ y" V/ V2 ~% _9 Q8 q) |
} else {8 r/ s1 }0 k8 a
1 C; T& Y( U; E& }0 ~6 p) j* a/ d% d ...% Y8 J! z, A7 ?1 N4 Y& J k& `
. U. N0 r' x& ~}
break;+ p3 I0 ]0 n5 M! H; H
. Q4 N5 _0 j4 S) U 这里通过$_FILES['file']依次获取了上传文件扩展名$ext、保存临时文件名$name、保存临时文件完整路径$file变量。之后通过new upload();创立一个upload对象,等到$upload->save()时再将文件真正写入。; u4 c4 I' a- ?! X- L- T
upload对象构造函数如下,include/upload.class.php:25: Z- g6 T: Y9 O
% M, c! d0 x) X1 c' Y/ m/ {8 a <?phpclass upload {' F7 ?+ w6 H( n3 x0 Z
8 K! Q1 }; j4 J" A: f* s5 w& O) R0 J/ j& J$ V8 @ function __construct($_file, $savepath, $savename = '', $fileformat = '') {9 ~( M; p, `$ w" Z2 {0 j! G% Z
# A2 `% @6 J1 k' C global $DT, $_userid;
8 e" F }4 j; ]+ {# o7 j! g( r; v6 t; C- {/ g+ H foreach($_file as $file) {
$this->file = $file['tmp_name'];8 }5 W9 }0 \: [4 C* x6 m2 ]
I% M$ ~( [- [* q8 A7 ? $this->file_name = $file['name'];
% G7 \$ G6 V0 C" z, k $this->file_size = $file['size'];
* P% q) }3 s, E: a) U$this->file_type = $file['type'];+ D' h1 h7 X& m/ |/ T+ ^! h
! M- _( A5 }0 z; ^) R+ `' r$this->file_error = $file['error'];
- x$ G, W4 [3 e) \) Q
}
' e; B7 v& w( M: A$this->userid = $_userid;
$this->ext = file_ext($this->file_name);
$this->fileformat = $fileformat ? $fileformat : $DT['uploadtype'];/ G+ D% q8 F7 C+ P$ F
3 n& N; a( }6 K- \: R: n9 ? $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024;
$this->savepath = $savepath;- P! v, R5 O3 a4 L9 U4 p( E! j
" g( i1 m$ f& I- \1 n; }$ l4 w. `, `4 ^# d $this->savename = $savename;
?, n+ t `7 A) u, r }}
. \' s/ u0 j% @! E6 ` 这里通过foreach($_file as $file)来遍历初始化各项参数。而savepath、savename则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')直接传入参数指定。
因此考虑上传了两个文件,第一个文件名是1.php,第二个文件是1.jpg,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中 , U k# u: w+ Z3 e: F, P, ^
$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
; [# T0 E( o1 K5 j- l. `; |8 q+ ^& s- E, k' W9 T- o 而在upload类中,由于多个文件上传,$this->file、$this->file_name、$this->file_type将foreach在第二次循环中被置为jpg文件。测试如下:8 I' y# z: H$ b" S! u
# V+ t5 A6 J1 s; c
回到avatar.inc.php,当进行文件保存时调用$upload->save(),include/upload.class.php:50:
: i2 o4 l1 o7 P" B9 }$ k<?phpclass upload {
function save() {
* i& F+ V9 Z2 h& s( j2 Z) _& `, | D7 x) r# B include load('include.lang');; S% e9 I' I4 h5 W; h3 x0 Z: I9 M: E
5 h4 ^4 ?8 l2 W7 s, U9 k if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')');
- T( E# d& a1 c
. U7 M# Y4 k) h9 n, G- aif($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)');" B9 f, @# c( i4 r( ]
4 ~% `' Q$ x6 s7 C: H0 m. S if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']);
6 H$ ?& F9 F# C9 M2 W I5 E3 A
7 b' {% V! p8 o, y" }9 L$this->set_savepath($this->savepath);
, y# V! z$ [8 m: k $this->set_savename($this->savename);: B- D" J* _8 [, e4 E# f2 M9 |, s% U
9 n# O% B! I0 h: b8 y5 S) N8 f$ D- K6 T5 j0 _9 K2 ~' L* y, s4 s- Z 8 U. a! j) c* R0 _8 H' h
if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']);
' a: q5 G! a$ @+ \$ _6 ` if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']);
2 F5 Y- B# Y% F1 G9 H! K1 D; |* z if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']);
4 u) G" C9 x$ p8 o0 P- Z% g" @: c+ J, j# f5 ]2 I
$this->image = $this->is_image();
1 a1 c% a# n! e c if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD);4 Q* Z1 H- N, _) G5 u4 N
& p+ ?* c: ?9 s; _$ N* H! c" m0 V4 V9 K7 {) |1 V return true;# C( x4 z. @1 ^; h% p, ^/ B- [
}}" q! A( h2 V) G( l, S( A
( D( B3 t& K D5 p 先经过几个基本参数的检查,然后调用$this->is_allow()来进行安全检查 include/upload.class.php:72:0 j) z7 b6 r! N$ k7 ?
7 C0 U1 B2 ^; D* q; U# w<?php
. k$ O: d( G4 v5 v( a) U# v$ e) X0 g/ d8 s) g function is_allow() {
% X* g; `" e; q. A) i) B' ^* p; q- m6 E6 I0 S) z7 I3 k if(!$this->fileformat) return false;9 S1 o7 v1 A( i- i7 F" p
, L+ o8 a. y7 m/ A# i if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false;
1 C/ L$ G4 ~* B0 |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;
/ E' _) _7 P; _; q c# @; breturn true;: E: ]3 A5 s5 j: |
' g1 k6 ~0 |8 N& J# Z- N* L }! \* w' D5 Y" \4 Q7 O
6 S% a2 M7 {# N) C 可以看到这里仅仅对$this->ext进行了检查,如前此时$this->ext为jpg,检查通过。
! F7 g+ n5 L& _/ L/ 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文件。
$ p, j4 q# m2 e D0 h 综上,上传两个文件,其中第一个文件以php为结尾如1.php,用于设置后缀名为php;第二个文件为1.jpg,jpg用于绕过检测,其内容为php一句话木马(图片马)。) y7 A( X* K. S; z+ N
% s" _3 V# z5 w9 r$ v& T3 U R
% K( N: q) E$ m% s) x8 ^. F
" a9 y3 \* G w3 K 然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1是自己的_userid
. [. U( Y% I% t" H不过实际利用上会有一定的限制。
0 i0 t' d. `9 K第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
4 D* I1 s5 d' m ]
]! p1 V, f+ ^6 c
7 n; ^. Q% J# T2 t 第二点是avatar.inc.php中在$upload->save()后,会再次对文件进行检查,然后重命名为xx.jpg:
省略...$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]);省略...& L6 v; ]( X F$ h
$ |) H( s6 K/ W0 n因此要利用成功就需要条件竞争了。
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:5 c) V5 |2 B/ t2 D& t( z; i
" i, G9 A; [$ L+ s2 K) x5 x1 |" k$ x, q" [1 V! l" r function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file));}
! @2 s+ k" ?3 P g& |) I' B1 J V0 [' X3 x2 N" }
在__construct()的foreach中使用了break,获取了第一个文件后就跳出循环。( r( X4 a0 K: z9 e* o" l
% b6 g5 X" [; b0 x1 d/ @6 w0 L9 f3 i2 i: ]/ W5 t6 N* A 在is_allow()中增加对$this->savename的二次检查。( X3 `( e# C$ C6 |7 j
$ M- B, u6 X$ v+ O# \$ M3 C/ j8 U 嘛,祝各位大师傅中秋快乐!# y0 M0 x" q% H2 @$ X! M+ n
l1 z* e% v8 b( @9 ]$ z' H
| 欢迎光临 中国网络渗透测试联盟 (https://cobjon.com/) | Powered by Discuz! X3.2 |